leak-cli 2026.2.17-beta.0 → 2026.2.17

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/scripts/buy.js CHANGED
@@ -11,20 +11,23 @@ import {
11
11
  } from "@x402/core/http";
12
12
  import { registerExactEvmScheme } from "@x402/evm/exact/client";
13
13
  import { privateKeyToAccount } from "viem/accounts";
14
+ import { DOWNLOAD_CODE_HEADER } from "../src/download_code.js";
15
+ import { createUi } from "./ui.js";
14
16
 
15
- const SKILL_NAME = "leak";
17
+ const SKILL_NAME = "leak-buy";
18
+ const outUi = createUi(process.stdout);
19
+ const errUi = createUi(process.stderr);
16
20
 
17
21
  function usageAndExit(code = 1) {
18
- console.log(
19
- "Usage: leak buy <promo_or_download_url> (--buyer-private-key-file <path> | --buyer-private-key-stdin) [--out <path> | --basename <name>]",
20
- );
21
- console.log("Examples:");
22
- console.log(
23
- " leak buy https://xxxx.trycloudflare.com/ --buyer-private-key-file ./buyer.key",
24
- );
25
- console.log(
26
- " cat ./buyer.key | leak buy https://xxxx.trycloudflare.com/download --buyer-private-key-stdin --basename myfile",
27
- );
22
+ console.log(outUi.heading("Leak Buy CLI"));
23
+ console.log("");
24
+ console.log(outUi.section("Usage"));
25
+ console.log(" leak buy <promo_or_download_url> [--download-code <code> | --download-code-stdin] [--buyer-private-key-file <path> | --buyer-private-key-stdin] [--out <path> | --basename <name>]");
26
+ console.log("");
27
+ console.log(outUi.section("Examples"));
28
+ console.log(" leak buy https://xxxx.trycloudflare.com/ --buyer-private-key-file ./buyer.key");
29
+ console.log(" leak buy https://xxxx.trycloudflare.com/download --download-code friends-only --buyer-private-key-file ./buyer.key --basename myfile");
30
+ console.log(" printf '%s\\n' 'friends-only' | leak buy https://xxxx.trycloudflare.com/download --download-code-stdin --out ./downloads/file.bin");
28
31
  process.exit(code);
29
32
  }
30
33
 
@@ -83,20 +86,24 @@ function readPrivateKeyFromFile(filePath) {
83
86
  return firstLine;
84
87
  }
85
88
 
86
- function readPrivateKeyFromStdin() {
89
+ function readFirstLineFromStdin(kindLabel) {
87
90
  let data = "";
88
91
  try {
89
92
  data = fs.readFileSync(0, "utf8");
90
93
  } catch {
91
- throw new Error("Failed to read private key from stdin");
94
+ throw new Error(`Failed to read ${kindLabel} from stdin`);
92
95
  }
93
96
  const firstLine = String(data).split(/\r?\n/, 1)[0]?.trim() || "";
94
97
  if (!firstLine) {
95
- throw new Error("No private key received on stdin");
98
+ throw new Error(`No ${kindLabel} received on stdin`);
96
99
  }
97
100
  return firstLine;
98
101
  }
99
102
 
103
+ function readPrivateKeyFromStdin() {
104
+ return readFirstLineFromStdin("buyer private key");
105
+ }
106
+
100
107
  function resolveBuyerPrivateKey(args) {
101
108
  if (typeof args["buyer-private-key"] !== "undefined") {
102
109
  throw new Error(
@@ -121,8 +128,31 @@ function resolveBuyerPrivateKey(args) {
121
128
  );
122
129
  }
123
130
 
131
+ function resolveDownloadCode(args) {
132
+ const hasInlineCode = typeof args["download-code"] !== "undefined";
133
+ const hasStdinCode = Boolean(args["download-code-stdin"]);
134
+
135
+ if (hasInlineCode && hasStdinCode) {
136
+ throw new Error("Use exactly one download code input: --download-code or --download-code-stdin");
137
+ }
138
+ if (hasInlineCode && args["download-code"] === true) {
139
+ throw new Error("--download-code requires a value");
140
+ }
141
+ if (hasStdinCode && Boolean(args["buyer-private-key-stdin"])) {
142
+ throw new Error("--download-code-stdin cannot be combined with --buyer-private-key-stdin");
143
+ }
144
+ if (hasInlineCode) {
145
+ const code = String(args["download-code"] || "").trim();
146
+ if (!code) throw new Error("--download-code cannot be empty");
147
+ return code;
148
+ }
149
+ if (hasStdinCode) {
150
+ return readFirstLineFromStdin("download code");
151
+ }
152
+ return "";
153
+ }
154
+
124
155
  function filenameFromContentDisposition(cd) {
125
- // Very small parser: attachment; filename="foo.ext"
126
156
  if (!cd) return null;
127
157
  const m = String(cd).match(/filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i);
128
158
  const raw = m ? (m[1] || m[2]) : null;
@@ -147,11 +177,6 @@ function explorerTxUrl(network, transaction) {
147
177
  return null;
148
178
  }
149
179
 
150
- function formatTriedEndpoints(tried) {
151
- if (!Array.isArray(tried) || tried.length === 0) return "";
152
- return `\nTried:\n- ${tried.join("\n- ")}`;
153
- }
154
-
155
180
  function normalizeInputUrl(value) {
156
181
  let parsed;
157
182
  try {
@@ -180,23 +205,6 @@ function normalizeSameOriginDownloadUrl(candidate, origin, sourceLabel) {
180
205
  return parsed.toString();
181
206
  }
182
207
 
183
- async function probeX402Endpoint(url) {
184
- try {
185
- const response = await fetch(url, { method: "GET" });
186
- if (response.status === 402) {
187
- const paymentRequiredHeader = getHeaderCaseInsensitive(response.headers, "PAYMENT-REQUIRED");
188
- if (paymentRequiredHeader) return { kind: "x402", response, paymentRequiredHeader };
189
- return { kind: "not-x402", response };
190
- }
191
- if (response.status === 410) {
192
- return { kind: "ended", response };
193
- }
194
- return { kind: "other", response };
195
- } catch (err) {
196
- return { kind: "error", error: err };
197
- }
198
- }
199
-
200
208
  async function fetchJsonPayload(url) {
201
209
  try {
202
210
  const response = await fetch(url, {
@@ -220,221 +228,248 @@ async function fetchJsonPayload(url) {
220
228
 
221
229
  async function resolveDownloadUrl(input) {
222
230
  const inputUrl = normalizeInputUrl(input);
223
- const tried = [];
224
231
  const isRootPath = inputUrl.pathname === "/" || inputUrl.pathname === "";
225
232
  const isDownloadPath = inputUrl.pathname === "/download";
226
233
 
227
- tried.push(`direct probe ${inputUrl.toString()}`);
228
- const directProbe = await probeX402Endpoint(inputUrl.toString());
229
- if (directProbe.kind === "x402") {
230
- return {
231
- downloadUrl: inputUrl.toString(),
232
- firstProbe: directProbe,
233
- tried,
234
- };
234
+ if (isDownloadPath) {
235
+ return { downloadUrl: inputUrl.toString() };
235
236
  }
236
- if (directProbe.kind === "ended") {
237
- throw new Error(`Sale has ended at ${inputUrl.toString()}${formatTriedEndpoints(tried)}`);
238
- }
239
- if (!isRootPath && !isDownloadPath) {
237
+
238
+ if (!isRootPath) {
240
239
  throw new Error(
241
- `Unsupported path for buy flow: ${inputUrl.pathname}. Use a promo URL (/) or /download URL.${formatTriedEndpoints(tried)}`,
240
+ `Unsupported path for buy flow: ${inputUrl.pathname}. Use a promo URL (/) or /download URL.`,
242
241
  );
243
242
  }
244
243
 
245
244
  const rfcResourceUrl = new URL(`/.well-known/skills/${SKILL_NAME}/resource.json`, inputUrl.origin).toString();
246
- tried.push(`rfc resource ${rfcResourceUrl}`);
247
245
  const rfcFetch = await fetchJsonPayload(rfcResourceUrl);
248
246
  if (rfcFetch.ok && (rfcFetch.response.status === 200 || rfcFetch.response.status === 410)) {
249
247
  const resourceStatus = String(rfcFetch.body?.status || "").toLowerCase();
250
248
  if (rfcFetch.response.status === 410 || resourceStatus === "ended") {
251
- throw new Error(`Sale has ended according to ${rfcResourceUrl}${formatTriedEndpoints(tried)}`);
249
+ throw new Error(`Sale has ended according to ${rfcResourceUrl}`);
252
250
  }
253
251
  if (typeof rfcFetch.body?.download_url === "string" && rfcFetch.body.download_url) {
254
- const rfcDownloadUrl = normalizeSameOriginDownloadUrl(
255
- rfcFetch.body.download_url,
256
- inputUrl.origin,
257
- rfcResourceUrl,
258
- );
259
- tried.push(`probe discovered RFC download ${rfcDownloadUrl}`);
260
- const rfcProbe = await probeX402Endpoint(rfcDownloadUrl);
261
- if (rfcProbe.kind === "x402") {
262
- return {
263
- downloadUrl: rfcDownloadUrl,
264
- firstProbe: rfcProbe,
265
- tried,
266
- };
267
- }
268
- if (rfcProbe.kind === "ended") {
269
- throw new Error(`Sale has ended at ${rfcDownloadUrl}${formatTriedEndpoints(tried)}`);
270
- }
252
+ return {
253
+ downloadUrl: normalizeSameOriginDownloadUrl(
254
+ rfcFetch.body.download_url,
255
+ inputUrl.origin,
256
+ rfcResourceUrl,
257
+ ),
258
+ discovery: rfcFetch.body,
259
+ };
271
260
  }
272
261
  }
273
262
 
274
263
  const legacyDiscoveryUrl = new URL("/.well-known/leak", inputUrl.origin).toString();
275
- tried.push(`legacy discovery ${legacyDiscoveryUrl}`);
276
264
  const legacyFetch = await fetchJsonPayload(legacyDiscoveryUrl);
277
265
  if (legacyFetch.ok && (legacyFetch.response.status === 200 || legacyFetch.response.status === 410)) {
278
266
  if (legacyFetch.response.status === 410) {
279
- throw new Error(`Sale has ended according to ${legacyDiscoveryUrl}${formatTriedEndpoints(tried)}`);
267
+ throw new Error(`Sale has ended according to ${legacyDiscoveryUrl}`);
280
268
  }
281
269
  if (typeof legacyFetch.body?.resource?.download_url === "string" && legacyFetch.body.resource.download_url) {
282
- const legacyDownloadUrl = normalizeSameOriginDownloadUrl(
283
- legacyFetch.body.resource.download_url,
284
- inputUrl.origin,
285
- legacyDiscoveryUrl,
286
- );
287
- tried.push(`probe discovered legacy download ${legacyDownloadUrl}`);
288
- const legacyProbe = await probeX402Endpoint(legacyDownloadUrl);
289
- if (legacyProbe.kind === "x402") {
290
- return {
291
- downloadUrl: legacyDownloadUrl,
292
- firstProbe: legacyProbe,
293
- tried,
294
- };
295
- }
296
- if (legacyProbe.kind === "ended") {
297
- throw new Error(`Sale has ended at ${legacyDownloadUrl}${formatTriedEndpoints(tried)}`);
298
- }
270
+ return {
271
+ downloadUrl: normalizeSameOriginDownloadUrl(
272
+ legacyFetch.body.resource.download_url,
273
+ inputUrl.origin,
274
+ legacyDiscoveryUrl,
275
+ ),
276
+ discovery: legacyFetch.body.resource,
277
+ };
299
278
  }
300
279
  }
301
280
 
302
- if (isRootPath) {
303
- const fallbackDownloadUrl = new URL("/download", inputUrl.origin).toString();
304
- tried.push(`root fallback ${fallbackDownloadUrl}`);
305
- const fallbackProbe = await probeX402Endpoint(fallbackDownloadUrl);
306
- if (fallbackProbe.kind === "x402") {
307
- return {
308
- downloadUrl: fallbackDownloadUrl,
309
- firstProbe: fallbackProbe,
310
- tried,
311
- };
281
+ return { downloadUrl: new URL("/download", inputUrl.origin).toString() };
282
+ }
283
+
284
+ function responseBodyText(response) {
285
+ return response.text().catch(() => "");
286
+ }
287
+
288
+ function resolveOutputPath(args, serverFilename) {
289
+ const safeServerFilename = sanitizeFilename(serverFilename || "downloaded.bin");
290
+ if (args.out) {
291
+ return String(args.out);
292
+ }
293
+ if (args.basename) {
294
+ const safeBase = sanitizeFilename(args.basename);
295
+ const ext = path.extname(safeServerFilename) || "";
296
+ return `./${safeBase}${ext}`;
297
+ }
298
+ return `./${safeServerFilename}`;
299
+ }
300
+
301
+ async function saveBinaryResponse(response, args, suggestedFilename) {
302
+ const serverFilename =
303
+ suggestedFilename ||
304
+ filenameFromContentDisposition(response.headers.get("content-disposition")) ||
305
+ "downloaded.bin";
306
+
307
+ const outPath = resolveOutputPath(args, serverFilename);
308
+ const buf = Buffer.from(await response.arrayBuffer());
309
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
310
+ fs.writeFileSync(outPath, buf);
311
+ console.log(outUi.statusLine("ok", `Saved ${buf.length} bytes -> ${outPath}`));
312
+ }
313
+
314
+ async function finalizeDownloadResponse(response, { args, downloadUrl }) {
315
+ const contentType = (response.headers.get("content-type") || "").toLowerCase();
316
+ if (!contentType.includes("application/json")) {
317
+ await saveBinaryResponse(response, args, null);
318
+ return;
319
+ }
320
+
321
+ const data = await response.json().catch(() => null);
322
+ if (!data || typeof data !== "object") {
323
+ throw new Error("Unexpected non-download JSON response");
324
+ }
325
+
326
+ if (typeof data.token === "string" && data.token) {
327
+ const tokenUrl = new URL(downloadUrl);
328
+ tokenUrl.searchParams.set("token", data.token);
329
+
330
+ const r3 = await fetch(tokenUrl.toString(), { method: "GET" });
331
+ if (!r3.ok) {
332
+ const text = await responseBodyText(r3);
333
+ throw new Error(`Download failed ${r3.status}: ${text}`);
312
334
  }
313
- if (fallbackProbe.kind === "ended") {
314
- throw new Error(`Sale has ended at ${fallbackDownloadUrl}${formatTriedEndpoints(tried)}`);
335
+
336
+ await saveBinaryResponse(r3, args, data.filename || null);
337
+ return;
338
+ }
339
+
340
+ if (typeof data.download_url === "string" && data.download_url) {
341
+ const sameOriginDownload = normalizeSameOriginDownloadUrl(
342
+ data.download_url,
343
+ new URL(downloadUrl).origin,
344
+ downloadUrl,
345
+ );
346
+ const r3 = await fetch(sameOriginDownload, { method: "GET" });
347
+ if (!r3.ok) {
348
+ const text = await responseBodyText(r3);
349
+ throw new Error(`Download failed ${r3.status}: ${text}`);
315
350
  }
351
+ await saveBinaryResponse(r3, args, data.filename || null);
352
+ return;
316
353
  }
317
354
 
318
- throw new Error(
319
- `Could not resolve an x402 download endpoint from ${inputUrl.toString()}${formatTriedEndpoints(tried)}`,
320
- );
355
+ throw new Error(`Unexpected JSON response from download endpoint: ${JSON.stringify(data)}`);
321
356
  }
322
357
 
323
- async function main() {
324
- const args = parseArgs(process.argv.slice(2));
325
- const inputUrl = args._[0];
326
- if (!inputUrl) usageAndExit(1);
358
+ function maybePrintPaymentReceipt(response) {
359
+ const paymentResponseHeader =
360
+ getHeaderCaseInsensitive(response.headers, "PAYMENT-RESPONSE") ||
361
+ getHeaderCaseInsensitive(response.headers, "X-PAYMENT-RESPONSE");
362
+ if (!paymentResponseHeader) return;
327
363
 
328
- let buyerPk;
329
364
  try {
330
- buyerPk = resolveBuyerPrivateKey(args);
365
+ const receipt = decodePaymentResponseHeader(paymentResponseHeader);
366
+ const explorer = explorerTxUrl(receipt.network, receipt.transaction);
367
+ console.log("");
368
+ console.log(outUi.section("Payment Receipt"));
369
+ const rows = [
370
+ { key: "network", value: receipt.network },
371
+ receipt.payer ? { key: "payer", value: receipt.payer } : null,
372
+ { key: "tx", value: receipt.transaction },
373
+ explorer ? { key: "explorer", value: explorer } : null,
374
+ ];
375
+ for (const line of outUi.formatRows(rows)) {
376
+ console.log(line);
377
+ }
331
378
  } catch (err) {
332
- console.error(err.message || String(err));
333
- process.exit(1);
379
+ console.error(
380
+ errUi.statusLine(
381
+ "warn",
382
+ `Could not decode PAYMENT-RESPONSE header (${err.message || String(err)})`,
383
+ ),
384
+ );
334
385
  }
386
+ }
335
387
 
336
- let account;
337
- try {
338
- account = privateKeyToAccount(normalizePrivateKey(buyerPk));
339
- } catch {
340
- console.error("Invalid buyer private key format.");
341
- process.exit(1);
342
- }
343
- const client = new x402Client();
344
- registerExactEvmScheme(client, { signer: account });
388
+ async function main() {
389
+ const args = parseArgs(process.argv.slice(2));
390
+ const inputUrl = args._[0];
391
+ if (!inputUrl) usageAndExit(1);
392
+
393
+ const downloadCode = resolveDownloadCode(args);
345
394
 
346
395
  const resolved = await resolveDownloadUrl(inputUrl);
347
396
  const downloadUrl = resolved.downloadUrl;
348
397
  if (downloadUrl !== inputUrl) {
349
- console.log(`[buy] resolved purchase endpoint: ${downloadUrl}`);
350
- }
351
-
352
- // 1) Request: expect 402 with PAYMENT-REQUIRED header.
353
- const r1 = resolved.firstProbe?.response || await fetch(downloadUrl, { method: "GET" });
354
- if (r1.status !== 402) {
355
- const text = await r1.text().catch(() => "");
356
- throw new Error(`Expected 402, got ${r1.status}: ${text}`);
398
+ console.log(outUi.statusLine("info", `Resolved purchase endpoint: ${downloadUrl}`));
357
399
  }
358
400
 
359
- const paymentRequiredHeader =
360
- resolved.firstProbe?.paymentRequiredHeader || getHeaderCaseInsensitive(r1.headers, "PAYMENT-REQUIRED");
361
- if (!paymentRequiredHeader) throw new Error("Missing PAYMENT-REQUIRED header");
362
-
363
- const paymentRequired = decodePaymentRequiredHeader(paymentRequiredHeader);
364
- const payload = await client.createPaymentPayload(paymentRequired);
365
- const paymentHeader = encodePaymentSignatureHeader(payload);
401
+ const initialHeaders = {};
402
+ if (downloadCode) initialHeaders[DOWNLOAD_CODE_HEADER] = downloadCode;
366
403
 
367
- // 2) Retry with payment signature, expect token JSON.
368
- const r2 = await fetch(downloadUrl, {
404
+ const r1 = await fetch(downloadUrl, {
369
405
  method: "GET",
370
- headers: {
371
- "PAYMENT-SIGNATURE": paymentHeader,
372
- },
406
+ headers: initialHeaders,
373
407
  });
374
408
 
375
- if (!r2.ok) {
376
- const text = await r2.text().catch(() => "");
377
- throw new Error(`Payment failed ${r2.status}: ${text}`);
409
+ if (r1.status === 410) {
410
+ throw new Error(`Sale has ended at ${downloadUrl}`);
378
411
  }
379
412
 
380
- const paymentResponseHeader =
381
- getHeaderCaseInsensitive(r2.headers, "PAYMENT-RESPONSE")
382
- || getHeaderCaseInsensitive(r2.headers, "X-PAYMENT-RESPONSE");
383
- if (paymentResponseHeader) {
413
+ if (r1.status === 401) {
414
+ const text = await responseBodyText(r1);
415
+ throw new Error(
416
+ `Download code required or invalid (send header ${DOWNLOAD_CODE_HEADER}). ${text}`,
417
+ );
418
+ }
419
+
420
+ if (r1.status === 402) {
421
+ const paymentRequiredHeader = getHeaderCaseInsensitive(r1.headers, "PAYMENT-REQUIRED");
422
+ if (!paymentRequiredHeader) throw new Error("Missing PAYMENT-REQUIRED header");
423
+
424
+ let buyerPk;
384
425
  try {
385
- const receipt = decodePaymentResponseHeader(paymentResponseHeader);
386
- const explorer = explorerTxUrl(receipt.network, receipt.transaction);
387
- console.log("Payment receipt:");
388
- console.log(`- network: ${receipt.network}`);
389
- if (receipt.payer) console.log(`- payer: ${receipt.payer}`);
390
- console.log(`- tx: ${receipt.transaction}`);
391
- if (explorer) console.log(`- explorer: ${explorer}`);
426
+ buyerPk = resolveBuyerPrivateKey(args);
392
427
  } catch (err) {
393
- console.error(`[buy] warning: could not decode PAYMENT-RESPONSE header (${err.message || String(err)})`);
428
+ throw new Error(`${err.message || String(err)} (payment required by seller)`);
429
+ }
430
+
431
+ let account;
432
+ try {
433
+ account = privateKeyToAccount(normalizePrivateKey(buyerPk));
434
+ } catch {
435
+ throw new Error("Invalid buyer private key format.");
394
436
  }
395
- }
396
437
 
397
- const data = await r2.json();
398
- const token = data?.token;
399
- if (!token) throw new Error(`Missing token in response: ${JSON.stringify(data)}`);
438
+ const client = new x402Client();
439
+ registerExactEvmScheme(client, { signer: account });
400
440
 
401
- // Determine download URL for the token step.
402
- const base = new URL(downloadUrl);
403
- const tokenUrl = new URL(base.toString());
404
- tokenUrl.searchParams.set("token", token);
441
+ const paymentRequired = decodePaymentRequiredHeader(paymentRequiredHeader);
442
+ const payload = await client.createPaymentPayload(paymentRequired);
443
+ const paymentHeader = encodePaymentSignatureHeader(payload);
405
444
 
406
- // 3) Download with token.
407
- const r3 = await fetch(tokenUrl.toString(), { method: "GET" });
408
- if (!r3.ok) {
409
- const text = await r3.text().catch(() => "");
410
- throw new Error(`Download failed ${r3.status}: ${text}`);
411
- }
445
+ const r2 = await fetch(downloadUrl, {
446
+ method: "GET",
447
+ headers: {
448
+ ...initialHeaders,
449
+ "PAYMENT-SIGNATURE": paymentHeader,
450
+ },
451
+ });
412
452
 
413
- const serverFilename =
414
- data?.filename ||
415
- filenameFromContentDisposition(r3.headers.get("content-disposition")) ||
416
- "downloaded.bin";
417
- const safeServerFilename = sanitizeFilename(serverFilename);
453
+ if (!r2.ok) {
454
+ const text = await responseBodyText(r2);
455
+ throw new Error(`Payment failed ${r2.status}: ${text}`);
456
+ }
418
457
 
419
- let outPath;
420
- if (args.out) {
421
- outPath = String(args.out);
422
- } else if (args.basename) {
423
- const safeBase = sanitizeFilename(args.basename);
424
- const ext = path.extname(safeServerFilename) || "";
425
- outPath = `./${safeBase}${ext}`;
426
- } else {
427
- outPath = `./${safeServerFilename}`;
458
+ maybePrintPaymentReceipt(r2);
459
+ await finalizeDownloadResponse(r2, { args, downloadUrl });
460
+ return;
428
461
  }
429
462
 
430
- const buf = Buffer.from(await r3.arrayBuffer());
431
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
432
- fs.writeFileSync(outPath, buf);
463
+ if (!r1.ok) {
464
+ const text = await responseBodyText(r1);
465
+ throw new Error(`Download failed ${r1.status}: ${text}`);
466
+ }
433
467
 
434
- console.log(`Saved ${buf.length} bytes -> ${outPath}`);
468
+ await finalizeDownloadResponse(r1, { args, downloadUrl });
435
469
  }
436
470
 
437
471
  main().catch((e) => {
438
- console.error(e);
472
+ const detail = e?.stack || e?.message || String(e);
473
+ console.error(errUi.statusLine("error", detail));
439
474
  process.exit(1);
440
475
  });
package/scripts/cli.js CHANGED
@@ -1,13 +1,81 @@
1
1
  #!/usr/bin/env node
2
+ import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { spawn } from "node:child_process";
6
+ import { createUi } from "./ui.js";
5
7
 
6
8
  const __filename = fileURLToPath(import.meta.url);
7
9
  const __dirname = path.dirname(__filename);
10
+ const PACKAGE_JSON_PATH = path.resolve(__dirname, "..", "package.json");
11
+ const outUi = createUi(process.stdout);
12
+ const errUi = createUi(process.stderr);
8
13
 
9
14
  const sub = process.argv[2];
10
15
 
16
+ function readVersion() {
17
+ try {
18
+ const parsed = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, "utf8"));
19
+ const version = String(parsed?.version || "").trim();
20
+ return version || "unknown";
21
+ } catch {
22
+ return "unknown";
23
+ }
24
+ }
25
+
26
+ function printVersion() {
27
+ console.log(outUi.section(`leak-cli ${readVersion()}`));
28
+ }
29
+
30
+ function printHelp() {
31
+ console.log(outUi.heading("Leak CLI"));
32
+ console.log(outUi.muted(`version ${readVersion()}`));
33
+ console.log("");
34
+ console.log(outUi.section("Usage"));
35
+ console.log(" leak publish [prefill flags]");
36
+ console.log(" leak --file <path> [publish flags]");
37
+ console.log(" leak buy <promo_or_download_url> [buy flags]");
38
+ console.log(" leak host --config <path> [host flags]");
39
+ console.log(" leak config [show|--write-env]");
40
+ console.log(" leak version");
41
+ console.log("");
42
+ console.log(outUi.section("Publish Flags"));
43
+ console.log(" --access-mode <mode>");
44
+ console.log(" --download-code <code> | --download-code-stdin");
45
+ console.log(" --price <usdc> --window <duration>");
46
+ console.log(" --pay-to <address> --network <caip2> --port <port>");
47
+ console.log(" --confirmed --public --og-title --og-description --og-image-url");
48
+ console.log("");
49
+ console.log(outUi.section("Buy Flags"));
50
+ console.log(" --download-code <code> | --download-code-stdin");
51
+ console.log(" --buyer-private-key-file <path> | --buyer-private-key-stdin");
52
+ console.log(" --out <path> | --basename <name>");
53
+ console.log("");
54
+ console.log(outUi.section("Host Flags"));
55
+ console.log(" --config <path>");
56
+ console.log(" --proxy-host <host> --proxy-port <port>");
57
+ console.log(" --public --public-confirm I_UNDERSTAND_PUBLIC_EXPOSURE");
58
+ console.log(" --dry-run");
59
+ console.log("");
60
+ console.log(outUi.section("Examples"));
61
+ console.log(" leak publish");
62
+ console.log(" leak publish --file ./song.mp3 --access-mode download-code-only-no-payment");
63
+ console.log(" leak --file ./song.mp3 --access-mode payment-only-no-download-code");
64
+ console.log(" leak --file ./song.mp3 --access-mode download-code-only-no-payment --download-code \"friends-only\"");
65
+ console.log(" leak buy https://xxxx.trycloudflare.com/ --download-code \"friends-only\"");
66
+ console.log(" leak host --config ./examples/multi-host.example.json --dry-run");
67
+ console.log(" leak host --config ./examples/multi-host.example.json --public --public-confirm I_UNDERSTAND_PUBLIC_EXPOSURE");
68
+ console.log(" leak config");
69
+ console.log(" leak version");
70
+ console.log("");
71
+ console.log(outUi.section("Notes"));
72
+ console.log(" share / as promo (social card); buy can start from / or /download.");
73
+ console.log(" buyer private key is required only when seller access mode includes payment.");
74
+ console.log("");
75
+ console.log(outUi.section("Backward-compatible"));
76
+ console.log(" leak leak --file <path> ...");
77
+ }
78
+
11
79
  function runSubcommand(scriptName, argv) {
12
80
  const scriptPath = path.resolve(__dirname, scriptName);
13
81
  const child = spawn(process.execPath, [scriptPath, ...argv], {
@@ -15,38 +83,37 @@ function runSubcommand(scriptName, argv) {
15
83
  });
16
84
 
17
85
  child.on("error", (err) => {
18
- console.error(`Failed to launch ${scriptName}: ${err.message}`);
86
+ console.error(errUi.statusLine("error", `Failed to launch ${scriptName}: ${err.message}`));
19
87
  process.exit(1);
20
88
  });
21
89
 
22
90
  child.on("exit", (code, signal) => {
23
91
  if (signal) {
24
- console.error(`${scriptName} exited via signal ${signal}`);
92
+ console.error(errUi.statusLine("error", `${scriptName} exited via signal ${signal}`));
25
93
  process.exit(1);
26
94
  }
27
95
  process.exit(code ?? 1);
28
96
  });
29
97
  }
30
98
 
31
- if (!sub || sub === "--help" || sub === "-h") {
32
- console.log("Usage:");
33
- console.log(" leak --file <path> [--price <usdc>] [--window <duration>] [--pay-to <address>] [--network <caip2>] [--port <port>] [--confirmed] [--public] [--og-title <text>] [--og-description <text>] [--og-image-url <https://...|./image.png>] [--ended-window-seconds <seconds>]");
34
- console.log(" leak buy <promo_or_download_url> (--buyer-private-key-file <path> | --buyer-private-key-stdin) [--out <path> | --basename <name>]");
35
- console.log(" leak config");
36
- console.log(" leak config show");
37
- console.log(" leak config --write-env");
38
- console.log("");
39
- console.log("Notes:");
40
- console.log(" share / as promo (social card); buy can start from / or /download.");
41
- console.log("Backward-compatible:");
42
- console.log(" leak leak --file <path> ...");
99
+ if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
100
+ printHelp();
101
+ process.exit(0);
102
+ }
103
+
104
+ if (sub === "--version" || sub === "-v" || sub === "version") {
105
+ printVersion();
43
106
  process.exit(0);
44
107
  }
45
108
 
46
109
  if (sub === "leak") {
47
110
  runSubcommand("leak.js", process.argv.slice(3));
111
+ } else if (sub === "publish") {
112
+ runSubcommand("leak.js", ["--wizard", ...process.argv.slice(3)]);
48
113
  } else if (sub === "buy") {
49
114
  runSubcommand("buy.js", process.argv.slice(3));
115
+ } else if (sub === "host") {
116
+ runSubcommand("host.js", process.argv.slice(3));
50
117
  } else if (sub === "config") {
51
118
  runSubcommand("config.js", process.argv.slice(3));
52
119
  } else {