yungle-cli 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +122 -40
  3. package/dist/yungle.mjs +2973 -377
  4. package/package.json +22 -17
package/dist/yungle.mjs CHANGED
@@ -1,14 +1,177 @@
1
1
  #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ try {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ } catch (e2) {
12
+ throw mod = 0, e2;
13
+ }
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+
32
+ // ../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js
33
+ var require_picocolors = __commonJS({
34
+ "../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js"(exports, module) {
35
+ var p = process || {};
36
+ var argv = p.argv || [];
37
+ var env2 = p.env || {};
38
+ var isColorSupported = !(!!env2.NO_COLOR || argv.includes("--no-color")) && (!!env2.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env2.TERM !== "dumb" || !!env2.CI);
39
+ var formatter = (open2, close, replace = open2) => (input) => {
40
+ let string = "" + input, index = string.indexOf(close, open2.length);
41
+ return ~index ? open2 + replaceClose(string, close, replace, index) + close : open2 + string + close;
42
+ };
43
+ var replaceClose = (string, close, replace, index) => {
44
+ let result = "", cursor3 = 0;
45
+ do {
46
+ result += string.substring(cursor3, index) + replace;
47
+ cursor3 = index + close.length;
48
+ index = string.indexOf(close, cursor3);
49
+ } while (~index);
50
+ return result + string.substring(cursor3);
51
+ };
52
+ var createColors = (enabled = isColorSupported) => {
53
+ let f = enabled ? formatter : () => String;
54
+ return {
55
+ isColorSupported: enabled,
56
+ reset: f("\x1B[0m", "\x1B[0m"),
57
+ bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
58
+ dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
59
+ italic: f("\x1B[3m", "\x1B[23m"),
60
+ underline: f("\x1B[4m", "\x1B[24m"),
61
+ inverse: f("\x1B[7m", "\x1B[27m"),
62
+ hidden: f("\x1B[8m", "\x1B[28m"),
63
+ strikethrough: f("\x1B[9m", "\x1B[29m"),
64
+ black: f("\x1B[30m", "\x1B[39m"),
65
+ red: f("\x1B[31m", "\x1B[39m"),
66
+ green: f("\x1B[32m", "\x1B[39m"),
67
+ yellow: f("\x1B[33m", "\x1B[39m"),
68
+ blue: f("\x1B[34m", "\x1B[39m"),
69
+ magenta: f("\x1B[35m", "\x1B[39m"),
70
+ cyan: f("\x1B[36m", "\x1B[39m"),
71
+ white: f("\x1B[37m", "\x1B[39m"),
72
+ gray: f("\x1B[90m", "\x1B[39m"),
73
+ bgBlack: f("\x1B[40m", "\x1B[49m"),
74
+ bgRed: f("\x1B[41m", "\x1B[49m"),
75
+ bgGreen: f("\x1B[42m", "\x1B[49m"),
76
+ bgYellow: f("\x1B[43m", "\x1B[49m"),
77
+ bgBlue: f("\x1B[44m", "\x1B[49m"),
78
+ bgMagenta: f("\x1B[45m", "\x1B[49m"),
79
+ bgCyan: f("\x1B[46m", "\x1B[49m"),
80
+ bgWhite: f("\x1B[47m", "\x1B[49m"),
81
+ blackBright: f("\x1B[90m", "\x1B[39m"),
82
+ redBright: f("\x1B[91m", "\x1B[39m"),
83
+ greenBright: f("\x1B[92m", "\x1B[39m"),
84
+ yellowBright: f("\x1B[93m", "\x1B[39m"),
85
+ blueBright: f("\x1B[94m", "\x1B[39m"),
86
+ magentaBright: f("\x1B[95m", "\x1B[39m"),
87
+ cyanBright: f("\x1B[96m", "\x1B[39m"),
88
+ whiteBright: f("\x1B[97m", "\x1B[39m"),
89
+ bgBlackBright: f("\x1B[100m", "\x1B[49m"),
90
+ bgRedBright: f("\x1B[101m", "\x1B[49m"),
91
+ bgGreenBright: f("\x1B[102m", "\x1B[49m"),
92
+ bgYellowBright: f("\x1B[103m", "\x1B[49m"),
93
+ bgBlueBright: f("\x1B[104m", "\x1B[49m"),
94
+ bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
95
+ bgCyanBright: f("\x1B[106m", "\x1B[49m"),
96
+ bgWhiteBright: f("\x1B[107m", "\x1B[49m")
97
+ };
98
+ };
99
+ module.exports = createColors();
100
+ module.exports.createColors = createColors;
101
+ }
102
+ });
103
+
104
+ // ../../node_modules/.pnpm/sisteransi@1.0.5/node_modules/sisteransi/src/index.js
105
+ var require_src = __commonJS({
106
+ "../../node_modules/.pnpm/sisteransi@1.0.5/node_modules/sisteransi/src/index.js"(exports, module) {
107
+ "use strict";
108
+ var ESC2 = "\x1B";
109
+ var CSI2 = `${ESC2}[`;
110
+ var beep = "\x07";
111
+ var cursor3 = {
112
+ to(x, y2) {
113
+ if (!y2) return `${CSI2}${x + 1}G`;
114
+ return `${CSI2}${y2 + 1};${x + 1}H`;
115
+ },
116
+ move(x, y2) {
117
+ let ret = "";
118
+ if (x < 0) ret += `${CSI2}${-x}D`;
119
+ else if (x > 0) ret += `${CSI2}${x}C`;
120
+ if (y2 < 0) ret += `${CSI2}${-y2}A`;
121
+ else if (y2 > 0) ret += `${CSI2}${y2}B`;
122
+ return ret;
123
+ },
124
+ up: (count = 1) => `${CSI2}${count}A`,
125
+ down: (count = 1) => `${CSI2}${count}B`,
126
+ forward: (count = 1) => `${CSI2}${count}C`,
127
+ backward: (count = 1) => `${CSI2}${count}D`,
128
+ nextLine: (count = 1) => `${CSI2}E`.repeat(count),
129
+ prevLine: (count = 1) => `${CSI2}F`.repeat(count),
130
+ left: `${CSI2}G`,
131
+ hide: `${CSI2}?25l`,
132
+ show: `${CSI2}?25h`,
133
+ save: `${ESC2}7`,
134
+ restore: `${ESC2}8`
135
+ };
136
+ var scroll = {
137
+ up: (count = 1) => `${CSI2}S`.repeat(count),
138
+ down: (count = 1) => `${CSI2}T`.repeat(count)
139
+ };
140
+ var erase3 = {
141
+ screen: `${CSI2}2J`,
142
+ up: (count = 1) => `${CSI2}1J`.repeat(count),
143
+ down: (count = 1) => `${CSI2}J`.repeat(count),
144
+ line: `${CSI2}2K`,
145
+ lineEnd: `${CSI2}K`,
146
+ lineStart: `${CSI2}1K`,
147
+ lines(count) {
148
+ let clear = "";
149
+ for (let i2 = 0; i2 < count; i2++)
150
+ clear += this.line + (i2 < count - 1 ? cursor3.up() : "");
151
+ if (count)
152
+ clear += cursor3.left;
153
+ return clear;
154
+ }
155
+ };
156
+ module.exports = { cursor: cursor3, scroll, erase: erase3, beep };
157
+ }
158
+ });
159
+
160
+ // src/index.ts
161
+ import { isAbsolute, join as join6, relative as relative2, resolve as resolve3 } from "node:path";
2
162
 
3
163
  // ../../packages/api-client/dist/index.mjs
4
164
  var YungleApiError = class extends Error {
5
- constructor(status, code, message, details) {
165
+ constructor(status2, code, message, details) {
6
166
  super(message);
7
- this.status = status;
167
+ this.status = status2;
8
168
  this.code = code;
9
169
  this.details = details;
10
170
  this.name = "YungleApiError";
11
171
  }
172
+ status;
173
+ code;
174
+ details;
12
175
  /** Worth trying again: throttled, or our fault. */
13
176
  get retryable() {
14
177
  return this.status === 429 || this.status >= 500;
@@ -20,25 +183,49 @@ var YungleApiError = class extends Error {
20
183
  }
21
184
  };
22
185
  var DEFAULT_BASE = "https://yungle.co/api/v1";
186
+ function pageQuery(page, extra = {}) {
187
+ const params = new URLSearchParams(extra);
188
+ if (page.limit !== void 0) params.set("limit", String(page.limit));
189
+ if (page.cursor) params.set("cursor", page.cursor);
190
+ const q = params.toString();
191
+ return q ? `?${q}` : "";
192
+ }
193
+ var CLIENT_VERSION = true ? "0.2.0" : "dev";
23
194
  var YungleClient = class {
24
195
  baseUrl;
25
196
  apiKey;
26
197
  doFetch;
27
198
  maxRetries;
199
+ userAgent;
28
200
  constructor(options) {
29
201
  if (!options.apiKey) throw new Error("An API key is required.");
30
202
  this.apiKey = options.apiKey;
31
203
  this.baseUrl = (options.baseUrl ?? DEFAULT_BASE).replace(/\/+$/, "");
32
204
  this.doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
33
205
  this.maxRetries = options.maxRetries ?? 3;
206
+ this.userAgent = options.userAgent ?? `yungle-client/${CLIENT_VERSION}`;
34
207
  }
35
208
  // ── Account ───────────────────────────────────────────────────────────────
36
209
  me() {
37
210
  return this.request("GET", "/me");
38
211
  }
39
212
  // ── Transfers ─────────────────────────────────────────────────────────────
40
- listTransfers() {
41
- return this.request("GET", "/transfers");
213
+ /**
214
+ * One page of transfers, newest first — 100 by default, up to 500. Pass the
215
+ * returned `nextCursor` back as `cursor` for the next page; it is null on the
216
+ * last one. `allTransfers()` does the walking for you.
217
+ */
218
+ listTransfers(page = {}) {
219
+ return this.request("GET", `/transfers${pageQuery(page)}`);
220
+ }
221
+ /** Every transfer, newest first, fetched a page at a time as you iterate. */
222
+ async *allTransfers(pageSize = 500) {
223
+ let cursor3;
224
+ do {
225
+ const page = await this.listTransfers({ limit: pageSize, cursor: cursor3 });
226
+ yield* page.transfers;
227
+ cursor3 = page.nextCursor ?? void 0;
228
+ } while (cursor3);
42
229
  }
43
230
  /**
44
231
  * Create a draft. Nothing is live and nobody is emailed until `finalizeTransfer`.
@@ -90,11 +277,24 @@ var YungleClient = class {
90
277
  deleteCollection(id) {
91
278
  return this.request("DELETE", `/collections/${enc(id)}`);
92
279
  }
93
- /** `folderId` omitted lists everything; `'root'` lists the top level only. */
94
- listCollectionFiles(id, folderId) {
95
- const query = folderId ? `?folderId=${encodeURIComponent(folderId)}` : "";
280
+ /**
281
+ * `folderId` omitted lists everything; `'root'` lists the top level only.
282
+ * Without `page.limit` the whole collection comes back in one response; with
283
+ * it, follow `nextCursor` (or use `allCollectionFiles`).
284
+ */
285
+ listCollectionFiles(id, folderId, page = {}) {
286
+ const query = pageQuery(page, folderId ? { folderId } : {});
96
287
  return this.request("GET", `/collections/${enc(id)}/files${query}`);
97
288
  }
289
+ /** Every file in a collection, oldest first, a page at a time. */
290
+ async *allCollectionFiles(id, folderId, pageSize = 1e3) {
291
+ let cursor3;
292
+ do {
293
+ const page = await this.listCollectionFiles(id, folderId, { limit: pageSize, cursor: cursor3 });
294
+ yield* page.files;
295
+ cursor3 = page.nextCursor ?? void 0;
296
+ } while (cursor3);
297
+ }
98
298
  addCollectionFiles(id, files, folderId) {
99
299
  return this.request("POST", `/collections/${enc(id)}/files`, { files, folderId });
100
300
  }
@@ -116,6 +316,42 @@ var YungleClient = class {
116
316
  removeGuest(id, guestId) {
117
317
  return this.request("DELETE", `/collections/${enc(id)}/guests/${enc(guestId)}`);
118
318
  }
319
+ // ── Webhooks ──────────────────────────────────────────────────────────────
320
+ listWebhooks() {
321
+ return this.request("GET", "/webhooks");
322
+ }
323
+ /** `url: null` makes a pull endpoint. The `secret` is returned here only. */
324
+ createWebhook(input) {
325
+ return this.request("POST", "/webhooks", input);
326
+ }
327
+ getWebhook(id) {
328
+ return this.request("GET", `/webhooks/${enc(id)}`);
329
+ }
330
+ updateWebhook(id, input) {
331
+ return this.request("PATCH", `/webhooks/${enc(id)}`, input);
332
+ }
333
+ deleteWebhook(id) {
334
+ return this.request("DELETE", `/webhooks/${enc(id)}`);
335
+ }
336
+ rotateWebhookSecret(id) {
337
+ return this.request("POST", `/webhooks/${enc(id)}/rotate-secret`);
338
+ }
339
+ testWebhook(id) {
340
+ return this.request("POST", `/webhooks/${enc(id)}/test`);
341
+ }
342
+ listWebhookDeliveries(id) {
343
+ return this.request("GET", `/webhooks/${enc(id)}/deliveries`);
344
+ }
345
+ retryWebhookDelivery(id, deliveryId) {
346
+ return this.request("POST", `/webhooks/${enc(id)}/deliveries/${enc(deliveryId)}/retry`);
347
+ }
348
+ /**
349
+ * An endpoint's events, oldest first, after `cursor`. `nextCursor` comes back
350
+ * even when nothing is new, so keep the latest and poll with it.
351
+ */
352
+ listWebhookEvents(id, page = {}) {
353
+ return this.request("GET", `/webhooks/${enc(id)}/events${pageQuery(page)}`);
354
+ }
119
355
  // ── Contacts ──────────────────────────────────────────────────────────────
120
356
  listContacts() {
121
357
  return this.request("GET", "/contacts");
@@ -135,15 +371,15 @@ var YungleClient = class {
135
371
  }
136
372
  // ── Transport ─────────────────────────────────────────────────────────────
137
373
  /**
138
- * `idempotent` widens what may be retried.
374
+ * What may be retried after a 5xx or a lost connection.
139
375
  *
140
- * GET and DELETE are safe by nature. POST is not — retrying `createTransfer`
141
- * after a 500 would make a second draft — so it is opt-in per method, and set
142
- * only where the server documents the operation as repeatable. Retrying a
143
- * *throttle* is always fine regardless: a 429 means nothing happened.
376
+ * GET and DELETE are safe by nature. A POST is safe because it carries an
377
+ * `Idempotency-Key` (see below). PATCH is not retried. Retrying a *throttle*
378
+ * is always fine regardless: a 429 means nothing happened.
144
379
  */
145
380
  async request(method, path, body, opts) {
146
- const safe = method === "GET" || method === "DELETE" || opts?.idempotent === true;
381
+ const idempotencyKey = method === "POST" ? globalThis.crypto.randomUUID() : void 0;
382
+ const safe = method === "GET" || method === "DELETE" || idempotencyKey !== void 0 || opts?.idempotent === true;
147
383
  let attempt = 0;
148
384
  for (; ; ) {
149
385
  const res = await this.doFetch(`${this.baseUrl}${path}`, {
@@ -151,6 +387,8 @@ var YungleClient = class {
151
387
  headers: {
152
388
  Authorization: `Bearer ${this.apiKey}`,
153
389
  Accept: "application/json",
390
+ "User-Agent": this.userAgent,
391
+ ...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {},
154
392
  ...body === void 0 ? {} : { "Content-Type": "application/json" }
155
393
  },
156
394
  ...body === void 0 ? {} : { body: JSON.stringify(body) }
@@ -187,12 +425,9 @@ function backoffMs(attempt, error) {
187
425
  return Math.min(2 ** attempt * 500, 8e3) + Math.floor(Math.random() * 250);
188
426
  }
189
427
  function sleep(ms) {
190
- return new Promise((resolve2) => setTimeout(resolve2, ms));
428
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
191
429
  }
192
430
 
193
- // src/index.ts
194
- import { createInterface } from "node:readline/promises";
195
-
196
431
  // src/args.ts
197
432
  var VALUE_FLAGS = /* @__PURE__ */ new Set([
198
433
  "to",
@@ -204,16 +439,25 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
204
439
  "folder",
205
440
  "out",
206
441
  "url",
207
- "key"
442
+ "key",
443
+ // `yungle mcp install --client cursor` silently installed into EVERY client
444
+ // before this was added: the flag parsed as a boolean, `cursor` became a stray
445
+ // positional, and the filter was never applied. The help text promised
446
+ // `--client <id>`, so the command line was correct and the parser was not.
447
+ // `helpFlagsAreParsed` in args.test.ts now derives this from HELP.
448
+ "client",
449
+ "forward-to",
450
+ "interval"
208
451
  ]);
452
+ var OPTIONAL_VALUE = /* @__PURE__ */ new Set(["key"]);
209
453
  function parseArgs(argv) {
210
454
  const [command = "help", ...rest] = argv;
211
455
  const positionals = [];
212
456
  const flags = {};
213
- for (let i = 0; i < rest.length; i++) {
214
- const token = rest[i];
457
+ for (let i2 = 0; i2 < rest.length; i2++) {
458
+ const token = rest[i2];
215
459
  if (token === "--") {
216
- positionals.push(...rest.slice(i + 1));
460
+ positionals.push(...rest.slice(i2 + 1));
217
461
  break;
218
462
  }
219
463
  if (token.startsWith("--")) {
@@ -224,12 +468,16 @@ function parseArgs(argv) {
224
468
  continue;
225
469
  }
226
470
  if (VALUE_FLAGS.has(body)) {
227
- const value = rest[i + 1];
471
+ const value = rest[i2 + 1];
228
472
  if (value === void 0 || value.startsWith("--")) {
473
+ if (OPTIONAL_VALUE.has(body)) {
474
+ flags[body] = true;
475
+ continue;
476
+ }
229
477
  throw new Error(`--${body} needs a value.`);
230
478
  }
231
479
  flags[body] = value;
232
- i++;
480
+ i2++;
233
481
  continue;
234
482
  }
235
483
  flags[body] = true;
@@ -245,25 +493,415 @@ function listFlag(value) {
245
493
  }
246
494
  function numberFlag(value, name) {
247
495
  if (value === void 0 || typeof value === "boolean") return void 0;
248
- const n = Number(value);
249
- if (!Number.isInteger(n) || n <= 0) throw new Error(`--${name} must be a positive whole number.`);
250
- return n;
496
+ const n3 = Number(value);
497
+ if (!Number.isInteger(n3) || n3 <= 0) throw new Error(`--${name} must be a positive whole number.`);
498
+ return n3;
251
499
  }
252
500
  function stringFlag(value) {
253
501
  return typeof value === "string" ? value : void 0;
254
502
  }
255
503
 
504
+ // src/files.ts
505
+ import { readdir, stat } from "node:fs/promises";
506
+ import { basename, join, relative, resolve, sep } from "node:path";
507
+ function isHidden(name) {
508
+ return name.startsWith(".");
509
+ }
510
+ async function collectFiles(inputs) {
511
+ const out2 = [];
512
+ for (const input of inputs) {
513
+ const absolute = resolve(input);
514
+ const info = await stat(absolute);
515
+ if (info.isDirectory()) {
516
+ await walk(absolute, basename(absolute), out2);
517
+ } else {
518
+ out2.push({
519
+ path: absolute,
520
+ name: basename(absolute),
521
+ size: info.size,
522
+ relativeDir: "",
523
+ mtimeMs: info.mtimeMs
524
+ });
525
+ }
526
+ }
527
+ return out2;
528
+ }
529
+ async function walk(dir, prefix, out2) {
530
+ const entries = await readdir(dir, { withFileTypes: true });
531
+ for (const entry of entries) {
532
+ if (isHidden(entry.name)) continue;
533
+ const full = join(dir, entry.name);
534
+ if (entry.isDirectory()) {
535
+ await walk(full, `${prefix}/${entry.name}`, out2);
536
+ } else if (entry.isFile()) {
537
+ const info = await stat(full);
538
+ out2.push({
539
+ path: full,
540
+ name: entry.name,
541
+ size: info.size,
542
+ relativeDir: prefix,
543
+ mtimeMs: info.mtimeMs
544
+ });
545
+ }
546
+ }
547
+ }
548
+ function toFileInputs(files) {
549
+ return files.map((f) => ({
550
+ name: f.name,
551
+ size: f.size,
552
+ ...f.relativeDir ? { path: f.relativeDir } : {}
553
+ }));
554
+ }
555
+ function pairWithTargets(files, targets) {
556
+ if (files.length !== targets.length) {
557
+ throw new Error(
558
+ `The server returned ${targets.length} upload targets for ${files.length} files. Refusing to guess which is which.`
559
+ );
560
+ }
561
+ return files.map((file, i2) => ({ file, target: targets[i2] }));
562
+ }
563
+ function formatBytes(bytes) {
564
+ if (bytes < 1024) return `${bytes} B`;
565
+ const units = ["KB", "MB", "GB", "TB"];
566
+ let value = bytes / 1024;
567
+ let unit = 0;
568
+ while (value >= 1024 && unit < units.length - 1) {
569
+ value /= 1024;
570
+ unit++;
571
+ }
572
+ return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
573
+ }
574
+
575
+ // src/ui.ts
576
+ var import_picocolors = __toESM(require_picocolors(), 1);
577
+ var env = process.env;
578
+ function colourFor(stream) {
579
+ if (env.NO_COLOR) return false;
580
+ if (env.FORCE_COLOR && env.FORCE_COLOR !== "0") return true;
581
+ return Boolean(stream.isTTY) && env.TERM !== "dumb";
582
+ }
583
+ var outColour = colourFor(process.stdout);
584
+ var errColour = colourFor(process.stderr);
585
+ var o = import_picocolors.default.createColors(outColour);
586
+ var e = import_picocolors.default.createColors(errColour);
587
+ var trueColour = env.COLORTERM === "truecolor" || env.COLORTERM === "24bit";
588
+ function brand(enabled, fallback) {
589
+ return (s) => !enabled ? s : trueColour ? `\x1B[38;2;52;168;104m${s}\x1B[39m` : fallback(s);
590
+ }
591
+ var accentOut = brand(outColour, o.green);
592
+ var accentErr = brand(errColour, e.green);
593
+ var ascii = process.platform === "win32" && !env.WT_SESSION && env.TERM_PROGRAM !== "vscode";
594
+ var sym = {
595
+ ok: ascii ? "\u221A" : "\u2714",
596
+ fail: ascii ? "\xD7" : "\u2716",
597
+ warn: ascii ? "!" : "\u25B2",
598
+ arrow: ascii ? "->" : "\u2192",
599
+ dot: "\xB7",
600
+ full: ascii ? "#" : "\u2588",
601
+ empty: ascii ? "-" : "\u2591"
602
+ };
603
+ var isTTY = Boolean(process.stdout.isTTY);
604
+ function interactive(flags) {
605
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY) && flags.json !== true && flags.yes !== true && !env.CI;
606
+ }
607
+ function formatDuration(seconds) {
608
+ const s = Math.max(0, Math.round(seconds));
609
+ if (s < 60) return `${s}s`;
610
+ const m = Math.floor(s / 60);
611
+ if (m < 60) return `${m}m ${String(s % 60).padStart(2, "0")}s`;
612
+ return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, "0")}m`;
613
+ }
614
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
615
+ function formatDate(iso, now = /* @__PURE__ */ new Date()) {
616
+ if (!iso) return "\u2014";
617
+ const d = new Date(iso);
618
+ const day = `${d.getDate()} ${MONTHS[d.getMonth()]}`;
619
+ return d.getFullYear() === now.getFullYear() ? day : `${day} ${d.getFullYear()}`;
620
+ }
621
+ function formatUntil(iso, now = Date.now()) {
622
+ if (!iso) return "no expiry";
623
+ const ms = new Date(iso).getTime() - now;
624
+ if (ms <= 0) return "expired";
625
+ const h2 = ms / 36e5;
626
+ if (h2 < 1) return `in ${Math.max(1, Math.round(ms / 6e4))} min`;
627
+ if (h2 < 48) return `in ${Math.round(h2)} hours`;
628
+ return `in ${Math.round(h2 / 24)} days`;
629
+ }
630
+ function plural(n3, one, many = `${one}s`) {
631
+ return `${n3} ${n3 === 1 ? one : many}`;
632
+ }
633
+ function bar(fraction, width = 20) {
634
+ const f = Math.min(1, Math.max(0, fraction));
635
+ const full = Math.round(f * width);
636
+ return accentErr(sym.full.repeat(full)) + e.dim(sym.empty.repeat(width - full));
637
+ }
638
+ function progressLine(done, total, startedAt, extra = "") {
639
+ const secs = Math.max(0.5, (Date.now() - startedAt) / 1e3);
640
+ const rate = done / secs;
641
+ const pct = total > 0 ? done / total : 1;
642
+ const left = rate > 0 && total > done ? `${formatDuration((total - done) / rate)} left` : "";
643
+ const parts = [`${formatBytes(done)} of ${formatBytes(total)}`, `${formatBytes(rate)}/s`, left, extra].filter(Boolean);
644
+ return ` ${bar(pct)} ${String(Math.floor(pct * 100)).padStart(3)}% ${e.dim(parts.join(` ${sym.dot} `))}`;
645
+ }
646
+ var lastLine = "";
647
+ function drawProgress(line) {
648
+ if (!process.stderr.isTTY || line === lastLine) return;
649
+ lastLine = line;
650
+ process.stderr.write(`\r\x1B[2K${line}`);
651
+ }
652
+ function endProgress() {
653
+ if (!process.stderr.isTTY || !lastLine) return;
654
+ process.stderr.write("\r\x1B[2K");
655
+ lastLine = "";
656
+ }
657
+ var ANSI = /\x1b\[[0-9;]*m/g;
658
+ var visible = (s) => s.replace(ANSI, "").length;
659
+ function fit(s, width) {
660
+ const len = visible(s);
661
+ if (len <= width) return s + " ".repeat(width - len);
662
+ const plain = s.replace(ANSI, "");
663
+ return `${plain.slice(0, Math.max(0, width - 1))}\u2026`;
664
+ }
665
+ function table(rows, opts = { flex: 0 }) {
666
+ const all = opts.header ? [opts.header.map((h2) => o.dim(h2)), ...rows] : rows;
667
+ if (all.length === 0) return "";
668
+ const cols = all[0].length;
669
+ const widths = Array.from({ length: cols }, (_, i2) => Math.max(...all.map((r2) => visible(r2[i2] ?? ""))));
670
+ const gap = 2;
671
+ const total = widths.reduce((a2, b) => a2 + b, 0) + gap * (cols - 1) + 2;
672
+ const room = process.stdout.columns ?? 120;
673
+ if (total > room) widths[opts.flex] = Math.max(8, widths[opts.flex] - (total - room));
674
+ return all.map((r2) => ` ${r2.map((cell, i2) => i2 === cols - 1 && i2 !== opts.flex ? cell : fit(cell, widths[i2])).join(" ".repeat(gap))}`.trimEnd()).join("\n");
675
+ }
676
+ function success(title, lines = []) {
677
+ return [` ${accentOut(sym.ok)} ${o.bold(title)}`, ...lines.map((l) => ` ${l}`)].join("\n");
678
+ }
679
+ function next(command, why) {
680
+ return ` ${o.dim("Next:")} ${accentOut(command)}${why ? o.dim(` (${why})`) : ""}`;
681
+ }
682
+ function note(text2) {
683
+ process.stderr.write(` ${e.dim(text2)}
684
+ `);
685
+ }
686
+ function heading(text2) {
687
+ process.stderr.write(`
688
+ ${text2}
689
+ `);
690
+ }
691
+
692
+ // src/help.ts
693
+ var HELP = `yungle \u2014 send, receive and keep files with Yungle
694
+
695
+ Get started
696
+ yungle login sign in with your browser
697
+ --key <key> use an API key instead (servers, CI); asks if left out
698
+ --no-browser print the link instead of opening it
699
+ yungle whoami show who you are signed in as
700
+ yungle logout sign out and revoke the session
701
+
702
+ Send and receive
703
+ yungle send <paths\u2026> send files or folders, print the link
704
+ --to <email> recipient (repeatable, or comma-separated)
705
+ --message <text> note for the recipients
706
+ --title <text> label for your dashboard
707
+ --password <text> recipients must enter this
708
+ --expires <days> lifetime, clamped to your plan
709
+ yungle get <link> download a transfer sent to you
710
+ --out <dir> where to save (default: current directory)
711
+ --zip one zip instead of separate files
712
+ --password <text> for a protected link; asks if left out
713
+ yungle status [<id|link>] who opened and downloaded a transfer
714
+ yungle transfers list your recent transfers
715
+ yungle revoke <id> stop a transfer's link for good
716
+
717
+ Collections
718
+ yungle push <paths\u2026> --collection <id> upload into a collection
719
+ --folder <id> target folder
720
+ yungle watch <dir> --collection <id> keep uploading new files as they appear
721
+ --interval <seconds> how often to look (default 10)
722
+ --existing also upload what is already in the folder
723
+ yungle collections list your collections
724
+ yungle contacts list your contacts
725
+
726
+ In the browser
727
+ yungle open [<id>|plan|keys|webhooks] open the dashboard, or a transfer
728
+
729
+ Developers
730
+ yungle webhooks ls list webhook endpoints
731
+ yungle webhooks listen print events as they happen
732
+ --forward-to <url> also POST each one, signed, to a local server
733
+ yungle mcp install connect an AI assistant on this machine
734
+ --client <id> claude-desktop | claude-code | cursor | windsurf
735
+ --dry-run show what would change, write nothing
736
+ --allow-write accept a key with transfers:write (share links)
737
+ yungle mcp status show where it is installed
738
+ yungle completion <shell> tab completion for zsh, bash or fish
739
+ yungle --version print the version
740
+
741
+ Global flags
742
+ --json machine-readable output
743
+ --yes never ask; for scripts
744
+ --url <base> use another Yungle (development)
745
+
746
+ Run a command with --help for examples. Docs: https://yungle.co/developers/cli`;
747
+ var COMMANDS = [
748
+ "login",
749
+ "whoami",
750
+ "logout",
751
+ "send",
752
+ "get",
753
+ "status",
754
+ "transfers",
755
+ "revoke",
756
+ "push",
757
+ "watch",
758
+ "collections",
759
+ "contacts",
760
+ "open",
761
+ "webhooks",
762
+ "mcp",
763
+ "completion",
764
+ "help"
765
+ ];
766
+ var ALIASES = {
767
+ "auth login": ["login", "--key"],
768
+ "auth status": ["whoami"],
769
+ "ls transfers": ["transfers"],
770
+ "ls collections": ["collections"],
771
+ "ls contacts": ["contacts"],
772
+ ls: ["transfers"],
773
+ "rm transfer": ["revoke"],
774
+ upload: ["push"],
775
+ download: ["get"]
776
+ };
777
+ function resolveAlias(argv) {
778
+ for (const n3 of [2, 1]) {
779
+ const key = argv.slice(0, n3).join(" ");
780
+ const to = ALIASES[key];
781
+ if (to && argv.length >= n3) return [...to, ...argv.slice(n3)];
782
+ }
783
+ return argv;
784
+ }
785
+ var EXAMPLES = {
786
+ login: ["yungle login", "yungle login --key # on a server: paste a key from Settings \u2192 API keys"],
787
+ send: [
788
+ 'yungle send ~/Shoot --to anna@studio.nl --message "Final selects"',
789
+ "yungle send report.pdf # just a link, nobody emailed",
790
+ "yungle send dist/ --json | jq -r .url # in a script"
791
+ ],
792
+ get: ["yungle get https://yungle.co/t/emerald-palm-a5mt", "yungle get <link> --zip --out ~/Downloads"],
793
+ status: ["yungle status # pick from your recent transfers", "yungle status https://yungle.co/t/emerald-palm-a5mt"],
794
+ push: ["yungle push ~/Shoot --collection 01J8Z3M9Q0W4", "yungle push raw/ --collection 01J8Z3M9Q0W4 --folder 01J8Z4AB"],
795
+ watch: ["yungle watch ~/Renders --collection 01J8Z3M9Q0W4", "yungle watch /Volumes/CARD --collection 01J8Z3M9Q0W4 --interval 30"],
796
+ open: ["yungle open", "yungle open 01J8Z3M9Q0W4 # a transfer", "yungle open keys"],
797
+ webhooks: ["yungle webhooks listen --forward-to http://localhost:8080/hooks"],
798
+ mcp: ["yungle mcp install --client claude-code --dry-run", "yungle mcp status"],
799
+ completion: ["yungle completion zsh > ~/.zfunc/_yungle", "yungle completion bash >> ~/.bashrc", "yungle completion fish > ~/.config/fish/completions/yungle.fish"]
800
+ };
801
+ function renderHelp(text2) {
802
+ return text2.split("\n").map((line) => {
803
+ if (/^\S/.test(line) && !line.startsWith("yungle") && !line.startsWith("Run ")) return o.bold(line);
804
+ const cmd = line.match(/^( {2})(yungle [^ ].*?)( {2,}.*)?$/);
805
+ if (cmd) return `${cmd[1]}${accentOut(cmd[2])}${o.dim(cmd[3] ?? "")}`;
806
+ if (/^ {2,4}--/.test(line)) return o.dim(line);
807
+ return line;
808
+ }).join("\n");
809
+ }
810
+ function commandHelp(command) {
811
+ const lines = HELP.split("\n");
812
+ const start = lines.findIndex((l) => l.startsWith(` yungle ${command}`));
813
+ if (start < 0) return null;
814
+ const block2 = [lines[start]];
815
+ for (let i2 = start + 1; i2 < lines.length && (lines[i2].startsWith(" ") || lines[i2].startsWith(` yungle ${command} `)); i2++) {
816
+ block2.push(lines[i2]);
817
+ }
818
+ const ex = EXAMPLES[command];
819
+ return [...block2, ...ex ? ["", "Examples", ...ex.map((l) => ` ${l}`)] : []].join("\n");
820
+ }
821
+
822
+ // src/completion.ts
823
+ function flagsByCommand() {
824
+ const out2 = {};
825
+ let current = null;
826
+ for (const line of HELP.split("\n")) {
827
+ const cmd = line.match(/^ {2}yungle ([a-z-]+)/);
828
+ if (cmd) {
829
+ current = cmd[1];
830
+ out2[current] ??= [];
831
+ continue;
832
+ }
833
+ const flag = line.match(/^ {4}(--[a-z-]+)/);
834
+ if (flag && current) out2[current].push(flag[1]);
835
+ if (/^\S/.test(line)) current = null;
836
+ }
837
+ for (const k of Object.keys(out2)) out2[k] = [.../* @__PURE__ */ new Set([...out2[k], "--json", "--help"])];
838
+ return out2;
839
+ }
840
+ var PATH_COMMANDS = ["send", "push", "watch"];
841
+ function completionScript(shell) {
842
+ const flags = flagsByCommand();
843
+ const cmds = COMMANDS.join(" ");
844
+ if (shell === "bash") {
845
+ const cases = Object.entries(flags).map(([c2, f]) => ` ${c2}) opts="${f.join(" ")}" ;;`).join("\n");
846
+ return `# yungle bash completion. Add to ~/.bashrc: eval "$(yungle completion bash)"
847
+ _yungle() {
848
+ local cur="\${COMP_WORDS[COMP_CWORD]}" cmd="\${COMP_WORDS[1]}" opts=""
849
+ if [ "$COMP_CWORD" -eq 1 ]; then
850
+ COMPREPLY=( $(compgen -W "${cmds}" -- "$cur") ); return
851
+ fi
852
+ case "$cmd" in
853
+ ${cases}
854
+ esac
855
+ if [[ "$cur" == -* ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
856
+ else case "$cmd" in ${PATH_COMMANDS.join("|")}) COMPREPLY=( $(compgen -f -- "$cur") ) ;; esac
857
+ fi
858
+ }
859
+ complete -o default -F _yungle yungle
860
+ `;
861
+ }
862
+ if (shell === "zsh") {
863
+ const cases = Object.entries(flags).map(([c2, f]) => ` ${c2}) _arguments '*:file:_files' ${f.map((x) => `'${x}[]'`).join(" ")} ;;`).join("\n");
864
+ return `#compdef yungle
865
+ # yungle zsh completion. Save as _yungle somewhere on your $fpath, e.g.:
866
+ # yungle completion zsh > "\${fpath[1]}/_yungle"
867
+ _yungle() {
868
+ if (( CURRENT == 2 )); then
869
+ compadd ${cmds}
870
+ return
871
+ fi
872
+ case $words[2] in
873
+ ${cases}
874
+ esac
875
+ }
876
+ _yungle "$@"
877
+ `;
878
+ }
879
+ if (shell === "fish") {
880
+ const lines = [
881
+ "# yungle fish completion. Save to ~/.config/fish/completions/yungle.fish",
882
+ `complete -c yungle -f -n '__fish_use_subcommand' -a '${cmds}'`
883
+ ];
884
+ for (const [c2, f] of Object.entries(flags)) {
885
+ for (const flag of f) lines.push(`complete -c yungle -n '__fish_seen_subcommand_from ${c2}' -l ${flag.slice(2)}`);
886
+ }
887
+ for (const c2 of PATH_COMMANDS) lines.push(`complete -c yungle -n '__fish_seen_subcommand_from ${c2}' -F`);
888
+ return `${lines.join("\n")}
889
+ `;
890
+ }
891
+ return null;
892
+ }
893
+
256
894
  // src/config.ts
257
895
  import { createHash } from "node:crypto";
258
896
  import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
259
897
  import { homedir } from "node:os";
260
- import { dirname, join } from "node:path";
898
+ import { dirname, join as join2 } from "node:path";
261
899
  function xdg(envVar, fallback) {
262
900
  const configured = process.env[envVar];
263
- return configured && configured.startsWith("/") ? configured : join(homedir(), fallback);
901
+ return configured && configured.startsWith("/") ? configured : join2(homedir(), fallback);
264
902
  }
265
- var CONFIG_PATH = join(xdg("XDG_CONFIG_HOME", ".config"), "yungle", "config.json");
266
- var RESUME_PATH = join(xdg("XDG_CACHE_HOME", ".cache"), "yungle", "uploads.json");
903
+ var CONFIG_PATH = join2(xdg("XDG_CONFIG_HOME", ".config"), "yungle", "config.json");
904
+ var RESUME_PATH = join2(xdg("XDG_CACHE_HOME", ".cache"), "yungle", "uploads.json");
267
905
  async function readConfig() {
268
906
  try {
269
907
  return JSON.parse(await readFile(CONFIG_PATH, "utf8"));
@@ -283,11 +921,37 @@ async function resolveKey() {
283
921
  const baseUrl = process.env.YUNGLE_API_URL?.trim() || config.baseUrl;
284
922
  if (fromEnv) return { apiKey: fromEnv, baseUrl, source: "YUNGLE_API_KEY" };
285
923
  if (config.apiKey) return { apiKey: config.apiKey, baseUrl, source: CONFIG_PATH };
924
+ if (config.oauth) {
925
+ const token = await freshAccessToken(config, baseUrl);
926
+ if (token) return { apiKey: token, baseUrl, source: `${CONFIG_PATH} (yungle login)` };
927
+ }
286
928
  return null;
287
929
  }
930
+ var CLI_CLIENT_ID = "yungle-cli";
931
+ var DEFAULT_API = "https://yungle.co/api/v1";
932
+ function originOf(baseUrl) {
933
+ return new URL(baseUrl ?? DEFAULT_API).origin;
934
+ }
935
+ async function freshAccessToken(config, baseUrl) {
936
+ const o2 = config.oauth;
937
+ if (!o2) return null;
938
+ if (o2.expiresAt - Date.now() > 10 * 6e4) return o2.accessToken;
939
+ const res = await fetch(`${originOf(baseUrl)}/oauth/token`, {
940
+ method: "POST",
941
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
942
+ body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: o2.refreshToken, client_id: CLI_CLIENT_ID })
943
+ }).catch(() => null);
944
+ if (!res?.ok) return null;
945
+ const t2 = await res.json();
946
+ await writeConfig({
947
+ ...config,
948
+ oauth: { accessToken: t2.access_token, refreshToken: t2.refresh_token, expiresAt: Date.now() + t2.expires_in * 1e3 }
949
+ });
950
+ return t2.access_token;
951
+ }
288
952
  var SESSION_TTL_MS = 2 * 60 * 60 * 1e3;
289
953
  function sessionKey(inputs, target) {
290
- const parts = inputs.map((f) => [f.path, f.size, Math.floor(f.mtimeMs)]).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
954
+ const parts = inputs.map((f) => [f.path, f.size, Math.floor(f.mtimeMs)]).sort((a2, b) => a2[0] < b[0] ? -1 : a2[0] > b[0] ? 1 : 0);
291
955
  return createHash("sha256").update(JSON.stringify([target, parts])).digest("hex").slice(0, 32);
292
956
  }
293
957
  async function readSessions() {
@@ -309,15 +973,15 @@ async function findSession(key) {
309
973
  async function saveSession(key, session) {
310
974
  const sessions = await readSessions();
311
975
  sessions[key] = session;
312
- await mkdir(dirname(RESUME_PATH), { recursive: true });
313
- await writeFile(RESUME_PATH, JSON.stringify(sessions));
976
+ await mkdir(dirname(RESUME_PATH), { recursive: true, mode: 448 });
977
+ await writeFile(RESUME_PATH, JSON.stringify(sessions), { mode: 384 });
314
978
  }
315
979
  async function dropSession(key) {
316
980
  const sessions = await readSessions();
317
981
  if (!(key in sessions)) return;
318
982
  delete sessions[key];
319
- await mkdir(dirname(RESUME_PATH), { recursive: true });
320
- await writeFile(RESUME_PATH, JSON.stringify(sessions));
983
+ await mkdir(dirname(RESUME_PATH), { recursive: true, mode: 448 });
984
+ await writeFile(RESUME_PATH, JSON.stringify(sessions), { mode: 384 });
321
985
  }
322
986
  var URL_PATH = RESUME_PATH.replace(/uploads\.json$/, "upload-urls.json");
323
987
  async function readUploadUrls() {
@@ -331,8 +995,8 @@ async function saveUploadUrl(fileId, uploadUrl) {
331
995
  const current = await readUploadUrls();
332
996
  if (current[fileId] === uploadUrl) return;
333
997
  current[fileId] = uploadUrl;
334
- await mkdir(dirname(URL_PATH), { recursive: true });
335
- await writeFile(URL_PATH, JSON.stringify(current));
998
+ await mkdir(dirname(URL_PATH), { recursive: true, mode: 448 });
999
+ await writeFile(URL_PATH, JSON.stringify(current), { mode: 384 });
336
1000
  }
337
1001
  async function clearUploadUrls(fileIds) {
338
1002
  const current = await readUploadUrls();
@@ -343,103 +1007,1779 @@ async function clearUploadUrls(fileIds) {
343
1007
  changed = true;
344
1008
  }
345
1009
  }
346
- if (changed) await writeFile(URL_PATH, JSON.stringify(current));
1010
+ if (changed) await writeFile(URL_PATH, JSON.stringify(current), { mode: 384 });
347
1011
  }
348
1012
 
349
- // src/files.ts
350
- import { readdir, stat } from "node:fs/promises";
351
- import { basename, join as join2, relative, resolve, sep } from "node:path";
352
- function isHidden(name) {
353
- return name.startsWith(".");
354
- }
355
- async function collectFiles(inputs) {
356
- const out2 = [];
357
- for (const input of inputs) {
358
- const absolute = resolve(input);
359
- const info = await stat(absolute);
360
- if (info.isDirectory()) {
361
- await walk(absolute, basename(absolute), out2);
362
- } else {
363
- out2.push({
364
- path: absolute,
365
- name: basename(absolute),
366
- size: info.size,
367
- relativeDir: "",
368
- mtimeMs: info.mtimeMs
369
- });
1013
+ // src/errors.ts
1014
+ var CliError = class extends Error {
1015
+ constructor(message, hint, code = "cli_error", exitCode = 1) {
1016
+ super(message);
1017
+ this.hint = hint;
1018
+ this.code = code;
1019
+ this.exitCode = exitCode;
1020
+ this.name = "CliError";
1021
+ }
1022
+ hint;
1023
+ code;
1024
+ exitCode;
1025
+ };
1026
+ var NotSignedIn = class extends CliError {
1027
+ constructor() {
1028
+ super("You are not signed in.", "yungle login", "not_signed_in");
1029
+ }
1030
+ };
1031
+ function explain(err) {
1032
+ if (err instanceof CliError) return { message: err.message, hint: err.hint, code: err.code };
1033
+ if (err instanceof YungleApiError) {
1034
+ const base = { code: err.code, details: err.details };
1035
+ switch (err.code) {
1036
+ case "unauthorized":
1037
+ return { ...base, message: "Your sign-in is no longer valid (revoked, expired or mistyped).", hint: "yungle login" };
1038
+ case "insufficient_scope": {
1039
+ const required = typeof err.details?.required === "string" ? err.details.required : null;
1040
+ return {
1041
+ ...base,
1042
+ message: err.message,
1043
+ hint: required === "transfers:send" ? "yungle open keys (allow emailing, or send without --to)" : "yungle open keys (create a key that may do this)"
1044
+ };
1045
+ }
1046
+ case "upgrade_required":
1047
+ return { ...base, message: err.message, hint: "yungle open plan" };
1048
+ case "quota_exceeded":
1049
+ return { ...base, message: "Your storage is full.", hint: "yungle open (free up space, or upgrade)" };
1050
+ case "out_of_credit":
1051
+ return { ...base, message: err.message, hint: "yungle open usage (add credit)" };
1052
+ case "rate_limited":
1053
+ return {
1054
+ ...base,
1055
+ message: `Too many requests.${err.retryAfterSeconds ? ` Try again in ${err.retryAfterSeconds}s.` : " Try again shortly."}`
1056
+ };
1057
+ case "not_found":
1058
+ return { ...base, message: err.message, hint: "yungle transfers (list what you have)" };
1059
+ case "email_budget_exhausted":
1060
+ return { ...base, message: err.message, hint: "Share the link yourself; the transfer is live." };
1061
+ default:
1062
+ return { ...base, message: err.message };
370
1063
  }
371
1064
  }
372
- return out2;
1065
+ const e2 = err;
1066
+ const netCode = e2?.cause?.code ?? e2?.code;
1067
+ if (netCode && ["ENOTFOUND", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "EAI_AGAIN"].includes(netCode)) {
1068
+ return { code: "network", message: "Could not reach Yungle. Check your connection and try again." };
1069
+ }
1070
+ if (e2?.code === "ENOENT" && typeof e2.path === "string") {
1071
+ return { code: "not_found", message: `No such file or folder: ${e2.path}` };
1072
+ }
1073
+ return { code: "cli_error", message: e2 instanceof Error ? e2.message : String(err) };
373
1074
  }
374
- async function walk(dir, prefix, out2) {
375
- const entries = await readdir(dir, { withFileTypes: true });
376
- for (const entry of entries) {
377
- if (isHidden(entry.name)) continue;
378
- const full = join2(dir, entry.name);
379
- if (entry.isDirectory()) {
380
- await walk(full, `${prefix}/${entry.name}`, out2);
381
- } else if (entry.isFile()) {
382
- const info = await stat(full);
383
- out2.push({
384
- path: full,
385
- name: entry.name,
386
- size: info.size,
387
- relativeDir: prefix,
388
- mtimeMs: info.mtimeMs
389
- });
1075
+ function distance(a2, b) {
1076
+ const dp = Array.from({ length: a2.length + 1 }, (_, i2) => [i2, ...Array(b.length).fill(0)]);
1077
+ for (let j = 1; j <= b.length; j++) dp[0][j] = j;
1078
+ for (let i2 = 1; i2 <= a2.length; i2++) {
1079
+ for (let j = 1; j <= b.length; j++) {
1080
+ dp[i2][j] = Math.min(
1081
+ dp[i2 - 1][j] + 1,
1082
+ dp[i2][j - 1] + 1,
1083
+ dp[i2 - 1][j - 1] + (a2[i2 - 1] === b[j - 1] ? 0 : 1)
1084
+ );
1085
+ if (i2 > 1 && j > 1 && a2[i2 - 1] === b[j - 2] && a2[i2 - 2] === b[j - 1]) {
1086
+ dp[i2][j] = Math.min(dp[i2][j], dp[i2 - 2][j - 2] + 1);
1087
+ }
390
1088
  }
391
1089
  }
1090
+ return dp[a2.length][b.length];
392
1091
  }
393
- function toFileInputs(files) {
394
- return files.map((f) => ({
395
- name: f.name,
396
- size: f.size,
397
- ...f.relativeDir ? { path: f.relativeDir } : {}
398
- }));
1092
+ function suggest(word, known) {
1093
+ let best = null;
1094
+ let bestD = Infinity;
1095
+ for (const k of known) {
1096
+ const d = distance(word.toLowerCase(), k);
1097
+ if (d < bestD) {
1098
+ bestD = d;
1099
+ best = k;
1100
+ }
1101
+ }
1102
+ return best !== null && bestD <= Math.max(1, Math.floor(best.length / 3)) ? best : null;
399
1103
  }
400
- function pairWithTargets(files, targets) {
401
- if (files.length !== targets.length) {
1104
+
1105
+ // src/get.ts
1106
+ import { createWriteStream } from "node:fs";
1107
+ import { mkdir as mkdir2, rename, stat as stat2 } from "node:fs/promises";
1108
+ import { basename as basename2, join as join3 } from "node:path";
1109
+ import { Readable } from "node:stream";
1110
+ import { pipeline } from "node:stream/promises";
1111
+ var SLUG = /^[A-Za-z0-9_-]{8,128}$/;
1112
+ function parseTransferLink(input, defaultOrigin) {
1113
+ const text2 = input.trim();
1114
+ if (SLUG.test(text2)) return { origin: defaultOrigin, slug: text2, recipient: null, hasKey: false };
1115
+ let url;
1116
+ try {
1117
+ url = new URL(text2);
1118
+ } catch {
1119
+ throw new Error(`Not a Yungle transfer link: ${text2}`);
1120
+ }
1121
+ const m = url.pathname.match(/^\/t\/([^/]+)\/?$/);
1122
+ if (!m || !SLUG.test(m[1])) {
402
1123
  throw new Error(
403
- `The server returned ${targets.length} upload targets for ${files.length} files. Refusing to guess which is which.`
1124
+ url.pathname.startsWith("/c/") ? "That is a collection link. `yungle get` downloads transfers (/t/\u2026) for now." : `Not a Yungle transfer link: ${text2}`
404
1125
  );
405
1126
  }
406
- return files.map((file, i) => ({ file, target: targets[i] }));
1127
+ return { origin: url.origin, slug: m[1], recipient: url.searchParams.get("r"), hasKey: url.hash.length > 1 };
407
1128
  }
408
- function formatBytes(bytes) {
409
- if (bytes < 1024) return `${bytes} B`;
410
- const units = ["KB", "MB", "GB", "TB"];
411
- let value = bytes / 1024;
412
- let unit = 0;
413
- while (value >= 1024 && unit < units.length - 1) {
414
- value /= 1024;
415
- unit++;
416
- }
417
- return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
1129
+ function safeFileName(name) {
1130
+ const last = basename2(name.replace(/\\/g, "/"));
1131
+ const cleaned = last.replace(/[\x00-\x1f<>:"|?*]/g, "_").replace(/^\.+$/, "_").trim();
1132
+ return cleaned || "file";
418
1133
  }
419
-
420
- // src/upload.ts
421
- import { createReadStream } from "node:fs";
422
- import { stat as stat2 } from "node:fs/promises";
423
- var CONCURRENCY = 3;
424
- async function uploadAll(files, endpoint, onProgress) {
425
- const queue = [...files];
426
- const workers = Array.from({ length: Math.min(CONCURRENCY, queue.length) }, async () => {
427
- for (; ; ) {
428
- const next = queue.shift();
429
- if (!next) return;
430
- await uploadOne(next, endpoint, onProgress);
431
- }
1134
+ function dedupeNames(names) {
1135
+ const used = /* @__PURE__ */ new Set();
1136
+ return names.map((name) => {
1137
+ let candidate = name;
1138
+ const dot = name.lastIndexOf(".");
1139
+ const [stem, ext] = dot > 0 ? [name.slice(0, dot), name.slice(dot)] : [name, ""];
1140
+ for (let n3 = 2; used.has(candidate.toLowerCase()); n3++) candidate = `${stem} (${n3})${ext}`;
1141
+ used.add(candidate.toLowerCase());
1142
+ return candidate;
1143
+ });
1144
+ }
1145
+ function resumeFrom(partialBytes, expectedSize) {
1146
+ if (partialBytes <= 0 || partialBytes >= expectedSize) return null;
1147
+ return partialBytes;
1148
+ }
1149
+ var ManifestError = class extends Error {
1150
+ constructor(code, message) {
1151
+ super(message);
1152
+ this.code = code;
1153
+ this.name = "ManifestError";
1154
+ }
1155
+ code;
1156
+ };
1157
+ async function fetchManifest(link, password2, userAgent) {
1158
+ const res = await fetch(`${link.origin}/api/t/${encodeURIComponent(link.slug)}/manifest`, {
1159
+ method: "POST",
1160
+ headers: { "Content-Type": "application/json", "User-Agent": userAgent },
1161
+ body: JSON.stringify({ ...password2 ? { password: password2 } : {}, ...link.recipient ? { r: link.recipient } : {} })
1162
+ });
1163
+ const body = await res.json().catch(() => null);
1164
+ if (!res.ok || !body || body.error) {
1165
+ const code = body?.error?.code ?? `http_${res.status}`;
1166
+ const message = code === "not_found" ? "No such transfer. Check the link, or it may have been deleted." : body?.error?.message ?? `The server answered ${res.status}.`;
1167
+ throw new ManifestError(code, message);
1168
+ }
1169
+ return body;
1170
+ }
1171
+ async function downloadTo(url, dest, expectedSize, userAgent, onBytes) {
1172
+ await mkdir2(join3(dest, ".."), { recursive: true });
1173
+ const part = `${dest}.part`;
1174
+ const have = await stat2(part).then((s) => s.size, () => 0);
1175
+ const from = expectedSize === null ? null : resumeFrom(have, expectedSize);
1176
+ const res = await fetch(url, {
1177
+ headers: { "User-Agent": userAgent, ...from !== null ? { Range: `bytes=${from}-` } : {} }
1178
+ });
1179
+ if (res.status === 410) throw new ManifestError("expired", "The transfer expired, was revoked, or reached its download limit.");
1180
+ if (!res.ok || !res.body) throw new ManifestError(`http_${res.status}`, `Download failed with ${res.status}.`);
1181
+ const appending = from !== null && res.status === 206;
1182
+ if (appending) onBytes(from);
1183
+ const counter = new TransformStream({
1184
+ transform(chunk, controller) {
1185
+ onBytes(chunk.byteLength);
1186
+ controller.enqueue(chunk);
1187
+ }
1188
+ });
1189
+ await pipeline(
1190
+ Readable.fromWeb(res.body.pipeThrough(counter)),
1191
+ createWriteStream(part, { flags: appending ? "a" : "w" })
1192
+ );
1193
+ await rename(part, dest);
1194
+ }
1195
+
1196
+ // ../../node_modules/.pnpm/@clack+core@1.5.1/node_modules/@clack/core/dist/index.mjs
1197
+ import { styleText } from "node:util";
1198
+ import { stdout, stdin } from "node:process";
1199
+ import l__default from "node:readline";
1200
+
1201
+ // ../../node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/utils.js
1202
+ var getCodePointsLength = /* @__PURE__ */ (() => {
1203
+ const SURROGATE_PAIR_RE = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
1204
+ return (input) => {
1205
+ let surrogatePairsNr = 0;
1206
+ SURROGATE_PAIR_RE.lastIndex = 0;
1207
+ while (SURROGATE_PAIR_RE.test(input)) {
1208
+ surrogatePairsNr += 1;
1209
+ }
1210
+ return input.length - surrogatePairsNr;
1211
+ };
1212
+ })();
1213
+ var isFullWidth = (x) => {
1214
+ return x === 12288 || x >= 65281 && x <= 65376 || x >= 65504 && x <= 65510;
1215
+ };
1216
+ var isWideNotCJKTNotEmoji = (x) => {
1217
+ return x === 8987 || x === 9001 || x >= 12272 && x <= 12287 || x >= 12289 && x <= 12350 || x >= 12441 && x <= 12543 || x >= 12549 && x <= 12591 || x >= 12593 && x <= 12686 || x >= 12688 && x <= 12771 || x >= 12783 && x <= 12830 || x >= 12832 && x <= 12871 || x >= 12880 && x <= 19903 || x >= 65040 && x <= 65049 || x >= 65072 && x <= 65106 || x >= 65108 && x <= 65126 || x >= 65128 && x <= 65131 || x >= 127488 && x <= 127490 || x >= 127504 && x <= 127547 || x >= 127552 && x <= 127560 || x >= 131072 && x <= 196605 || x >= 196608 && x <= 262141;
1218
+ };
1219
+
1220
+ // ../../node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/index.js
1221
+ var ANSI_RE = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]|\u001b\]8;[^;]*;.*?(?:\u0007|\u001b\u005c)/y;
1222
+ var CONTROL_RE = /[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y;
1223
+ var CJKT_WIDE_RE = /(?:(?![\uFF61-\uFF9F\uFF00-\uFFEF])[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Tangut}]){1,1000}/yu;
1224
+ var TAB_RE = /\t{1,1000}/y;
1225
+ var EMOJI_RE = new RegExp("[\\u{1F1E6}-\\u{1F1FF}]{2}|\\u{1F3F4}[\\u{E0061}-\\u{E007A}]{2}[\\u{E0030}-\\u{E0039}\\u{E0061}-\\u{E007A}]{1,3}\\u{E007F}|(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation})(?:\\u200D(?:\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F\\u20E3?))*", "yu");
1226
+ var LATIN_RE = /(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y;
1227
+ var MODIFIER_RE = new RegExp("\\p{M}+", "gu");
1228
+ var NO_TRUNCATION = { limit: Infinity, ellipsis: "" };
1229
+ var getStringTruncatedWidth = (input, truncationOptions = {}, widthOptions = {}) => {
1230
+ const LIMIT = truncationOptions.limit ?? Infinity;
1231
+ const ELLIPSIS = truncationOptions.ellipsis ?? "";
1232
+ const ELLIPSIS_WIDTH = truncationOptions?.ellipsisWidth ?? (ELLIPSIS ? getStringTruncatedWidth(ELLIPSIS, NO_TRUNCATION, widthOptions).width : 0);
1233
+ const ANSI_WIDTH = 0;
1234
+ const CONTROL_WIDTH = widthOptions.controlWidth ?? 0;
1235
+ const TAB_WIDTH = widthOptions.tabWidth ?? 8;
1236
+ const EMOJI_WIDTH = widthOptions.emojiWidth ?? 2;
1237
+ const FULL_WIDTH_WIDTH = 2;
1238
+ const REGULAR_WIDTH = widthOptions.regularWidth ?? 1;
1239
+ const WIDE_WIDTH = widthOptions.wideWidth ?? FULL_WIDTH_WIDTH;
1240
+ const PARSE_BLOCKS = [
1241
+ [LATIN_RE, REGULAR_WIDTH],
1242
+ [ANSI_RE, ANSI_WIDTH],
1243
+ [CONTROL_RE, CONTROL_WIDTH],
1244
+ [TAB_RE, TAB_WIDTH],
1245
+ [EMOJI_RE, EMOJI_WIDTH],
1246
+ [CJKT_WIDE_RE, WIDE_WIDTH]
1247
+ ];
1248
+ let indexPrev = 0;
1249
+ let index = 0;
1250
+ let length = input.length;
1251
+ let lengthExtra = 0;
1252
+ let truncationEnabled = false;
1253
+ let truncationIndex = length;
1254
+ let truncationLimit = Math.max(0, LIMIT - ELLIPSIS_WIDTH);
1255
+ let unmatchedStart = 0;
1256
+ let unmatchedEnd = 0;
1257
+ let width = 0;
1258
+ let widthExtra = 0;
1259
+ outer: while (true) {
1260
+ if (unmatchedEnd > unmatchedStart || index >= length && index > indexPrev) {
1261
+ const unmatched = input.slice(unmatchedStart, unmatchedEnd) || input.slice(indexPrev, index);
1262
+ lengthExtra = 0;
1263
+ for (const char of unmatched.replaceAll(MODIFIER_RE, "")) {
1264
+ const codePoint = char.codePointAt(0) || 0;
1265
+ if (isFullWidth(codePoint)) {
1266
+ widthExtra = FULL_WIDTH_WIDTH;
1267
+ } else if (isWideNotCJKTNotEmoji(codePoint)) {
1268
+ widthExtra = WIDE_WIDTH;
1269
+ } else {
1270
+ widthExtra = REGULAR_WIDTH;
1271
+ }
1272
+ if (width + widthExtra > truncationLimit) {
1273
+ truncationIndex = Math.min(truncationIndex, Math.max(unmatchedStart, indexPrev) + lengthExtra);
1274
+ }
1275
+ if (width + widthExtra > LIMIT) {
1276
+ truncationEnabled = true;
1277
+ break outer;
1278
+ }
1279
+ lengthExtra += char.length;
1280
+ width += widthExtra;
1281
+ }
1282
+ unmatchedStart = unmatchedEnd = 0;
1283
+ }
1284
+ if (index >= length) {
1285
+ break outer;
1286
+ }
1287
+ for (let i2 = 0, l = PARSE_BLOCKS.length; i2 < l; i2++) {
1288
+ const [BLOCK_RE, BLOCK_WIDTH] = PARSE_BLOCKS[i2];
1289
+ BLOCK_RE.lastIndex = index;
1290
+ if (BLOCK_RE.test(input)) {
1291
+ lengthExtra = BLOCK_RE === CJKT_WIDE_RE ? getCodePointsLength(input.slice(index, BLOCK_RE.lastIndex)) : BLOCK_RE === EMOJI_RE ? 1 : BLOCK_RE.lastIndex - index;
1292
+ widthExtra = lengthExtra * BLOCK_WIDTH;
1293
+ if (width + widthExtra > truncationLimit) {
1294
+ truncationIndex = Math.min(truncationIndex, index + Math.floor((truncationLimit - width) / BLOCK_WIDTH));
1295
+ }
1296
+ if (width + widthExtra > LIMIT) {
1297
+ truncationEnabled = true;
1298
+ break outer;
1299
+ }
1300
+ width += widthExtra;
1301
+ unmatchedStart = indexPrev;
1302
+ unmatchedEnd = index;
1303
+ index = indexPrev = BLOCK_RE.lastIndex;
1304
+ continue outer;
1305
+ }
1306
+ }
1307
+ index += 1;
1308
+ }
1309
+ return {
1310
+ width: truncationEnabled ? truncationLimit : width,
1311
+ index: truncationEnabled ? truncationIndex : length,
1312
+ truncated: truncationEnabled,
1313
+ ellipsed: truncationEnabled && LIMIT >= ELLIPSIS_WIDTH
1314
+ };
1315
+ };
1316
+ var dist_default = getStringTruncatedWidth;
1317
+
1318
+ // ../../node_modules/.pnpm/fast-string-width@3.0.2/node_modules/fast-string-width/dist/index.js
1319
+ var NO_TRUNCATION2 = {
1320
+ limit: Infinity,
1321
+ ellipsis: "",
1322
+ ellipsisWidth: 0
1323
+ };
1324
+ var fastStringWidth = (input, options = {}) => {
1325
+ return dist_default(input, NO_TRUNCATION2, options).width;
1326
+ };
1327
+ var dist_default2 = fastStringWidth;
1328
+
1329
+ // ../../node_modules/.pnpm/fast-wrap-ansi@0.2.2/node_modules/fast-wrap-ansi/lib/main.js
1330
+ var ESC = "\x1B";
1331
+ var CSI = "\x9B";
1332
+ var END_CODE = 39;
1333
+ var ANSI_ESCAPE_BELL = "\x07";
1334
+ var ANSI_CSI = "[";
1335
+ var ANSI_OSC = "]";
1336
+ var ANSI_SGR_TERMINATOR = "m";
1337
+ var ANSI_ESCAPE_LINK = `${ANSI_OSC}8;;`;
1338
+ var GROUP_REGEX = new RegExp(`(?:\\${ANSI_CSI}(?<code>\\d+)m|\\${ANSI_ESCAPE_LINK}(?<uri>.*)${ANSI_ESCAPE_BELL})`, "y");
1339
+ var getClosingCode = (openingCode) => {
1340
+ if (openingCode >= 30 && openingCode <= 37)
1341
+ return 39;
1342
+ if (openingCode >= 90 && openingCode <= 97)
1343
+ return 39;
1344
+ if (openingCode >= 40 && openingCode <= 47)
1345
+ return 49;
1346
+ if (openingCode >= 100 && openingCode <= 107)
1347
+ return 49;
1348
+ if (openingCode === 1 || openingCode === 2)
1349
+ return 22;
1350
+ if (openingCode === 3)
1351
+ return 23;
1352
+ if (openingCode === 4)
1353
+ return 24;
1354
+ if (openingCode === 7)
1355
+ return 27;
1356
+ if (openingCode === 8)
1357
+ return 28;
1358
+ if (openingCode === 9)
1359
+ return 29;
1360
+ if (openingCode === 0)
1361
+ return 0;
1362
+ return void 0;
1363
+ };
1364
+ var wrapAnsiCode = (code) => `${ESC}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
1365
+ var wrapAnsiHyperlink = (url) => `${ESC}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`;
1366
+ var wrapWord = (rows, word, columns) => {
1367
+ const characters = word[Symbol.iterator]();
1368
+ let isInsideEscape = false;
1369
+ let isInsideLinkEscape = false;
1370
+ let lastRow = rows.at(-1);
1371
+ let visible2 = lastRow === void 0 ? 0 : dist_default2(lastRow);
1372
+ let currentCharacter = characters.next();
1373
+ let nextCharacter = characters.next();
1374
+ let rawCharacterIndex = 0;
1375
+ while (!currentCharacter.done) {
1376
+ const character = currentCharacter.value;
1377
+ const characterLength = dist_default2(character);
1378
+ if (visible2 + characterLength <= columns) {
1379
+ rows[rows.length - 1] += character;
1380
+ } else {
1381
+ rows.push(character);
1382
+ visible2 = 0;
1383
+ }
1384
+ if (character === ESC || character === CSI) {
1385
+ isInsideEscape = true;
1386
+ isInsideLinkEscape = word.startsWith(ANSI_ESCAPE_LINK, rawCharacterIndex + 1);
1387
+ }
1388
+ if (isInsideEscape) {
1389
+ if (isInsideLinkEscape) {
1390
+ if (character === ANSI_ESCAPE_BELL) {
1391
+ isInsideEscape = false;
1392
+ isInsideLinkEscape = false;
1393
+ }
1394
+ } else if (character === ANSI_SGR_TERMINATOR) {
1395
+ isInsideEscape = false;
1396
+ }
1397
+ } else {
1398
+ visible2 += characterLength;
1399
+ if (visible2 === columns && !nextCharacter.done) {
1400
+ rows.push("");
1401
+ visible2 = 0;
1402
+ }
1403
+ }
1404
+ currentCharacter = nextCharacter;
1405
+ nextCharacter = characters.next();
1406
+ rawCharacterIndex += character.length;
1407
+ }
1408
+ lastRow = rows.at(-1);
1409
+ if (!visible2 && lastRow !== void 0 && lastRow.length && rows.length > 1) {
1410
+ rows[rows.length - 2] += rows.pop();
1411
+ }
1412
+ };
1413
+ var stringVisibleTrimSpacesRight = (string) => {
1414
+ const words = string.split(" ");
1415
+ let last = words.length;
1416
+ while (last) {
1417
+ if (dist_default2(words[last - 1])) {
1418
+ break;
1419
+ }
1420
+ last--;
1421
+ }
1422
+ if (last === words.length) {
1423
+ return string;
1424
+ }
1425
+ return words.slice(0, last).join(" ") + words.slice(last).join("");
1426
+ };
1427
+ var exec = (string, columns, options = {}) => {
1428
+ if (options.trim !== false && string.trim() === "") {
1429
+ return "";
1430
+ }
1431
+ let returnValue = "";
1432
+ let escapeCode;
1433
+ let escapeUrl;
1434
+ const words = string.split(" ");
1435
+ let rows = [""];
1436
+ let rowLength = 0;
1437
+ for (let index = 0; index < words.length; index++) {
1438
+ const word = words[index];
1439
+ if (options.trim !== false) {
1440
+ const row = rows.at(-1) ?? "";
1441
+ const trimmed = row.trimStart();
1442
+ if (row.length !== trimmed.length) {
1443
+ rows[rows.length - 1] = trimmed;
1444
+ rowLength = dist_default2(trimmed);
1445
+ }
1446
+ }
1447
+ if (index !== 0) {
1448
+ if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) {
1449
+ rows.push("");
1450
+ rowLength = 0;
1451
+ }
1452
+ if (rowLength || options.trim === false) {
1453
+ rows[rows.length - 1] += " ";
1454
+ rowLength++;
1455
+ }
1456
+ }
1457
+ const wordLength = dist_default2(word);
1458
+ if (options.hard && wordLength > columns) {
1459
+ const remainingColumns = columns - rowLength;
1460
+ const breaksStartingThisLine = 1 + Math.floor((wordLength - remainingColumns - 1) / columns);
1461
+ const breaksStartingNextLine = Math.floor((wordLength - 1) / columns);
1462
+ if (breaksStartingNextLine < breaksStartingThisLine) {
1463
+ rows.push("");
1464
+ }
1465
+ wrapWord(rows, word, columns);
1466
+ rowLength = dist_default2(rows.at(-1) ?? "");
1467
+ continue;
1468
+ }
1469
+ if (rowLength + wordLength > columns && rowLength && wordLength) {
1470
+ if (options.wordWrap === false && rowLength < columns) {
1471
+ wrapWord(rows, word, columns);
1472
+ rowLength = dist_default2(rows.at(-1) ?? "");
1473
+ continue;
1474
+ }
1475
+ rows.push("");
1476
+ rowLength = 0;
1477
+ }
1478
+ if (rowLength + wordLength > columns && options.wordWrap === false) {
1479
+ wrapWord(rows, word, columns);
1480
+ rowLength = dist_default2(rows.at(-1) ?? "");
1481
+ continue;
1482
+ }
1483
+ rows[rows.length - 1] += word;
1484
+ rowLength += wordLength;
1485
+ }
1486
+ if (options.trim !== false) {
1487
+ rows = rows.map((row) => stringVisibleTrimSpacesRight(row));
1488
+ }
1489
+ const preString = rows.join("\n");
1490
+ let inSurrogate = false;
1491
+ for (let i2 = 0; i2 < preString.length; i2++) {
1492
+ const character = preString[i2];
1493
+ returnValue += character;
1494
+ if (!inSurrogate) {
1495
+ inSurrogate = character >= "\uD800" && character <= "\uDBFF";
1496
+ if (inSurrogate) {
1497
+ continue;
1498
+ }
1499
+ } else {
1500
+ inSurrogate = false;
1501
+ }
1502
+ if (character === ESC || character === CSI) {
1503
+ GROUP_REGEX.lastIndex = i2 + 1;
1504
+ const groupsResult = GROUP_REGEX.exec(preString);
1505
+ const groups = groupsResult?.groups;
1506
+ if (groups?.code !== void 0) {
1507
+ const code = Number.parseFloat(groups.code);
1508
+ escapeCode = code === END_CODE ? void 0 : code;
1509
+ } else if (groups?.uri !== void 0) {
1510
+ escapeUrl = groups.uri.length === 0 ? void 0 : groups.uri;
1511
+ }
1512
+ }
1513
+ if (preString[i2 + 1] === "\n") {
1514
+ if (escapeUrl) {
1515
+ returnValue += wrapAnsiHyperlink("");
1516
+ }
1517
+ const closingCode = escapeCode ? getClosingCode(escapeCode) : void 0;
1518
+ if (escapeCode && closingCode) {
1519
+ returnValue += wrapAnsiCode(closingCode);
1520
+ }
1521
+ } else if (character === "\n") {
1522
+ if (escapeCode && getClosingCode(escapeCode)) {
1523
+ returnValue += wrapAnsiCode(escapeCode);
1524
+ }
1525
+ if (escapeUrl) {
1526
+ returnValue += wrapAnsiHyperlink(escapeUrl);
1527
+ }
1528
+ }
1529
+ }
1530
+ return returnValue;
1531
+ };
1532
+ var CRLF_OR_LF = /\r?\n/;
1533
+ function wrapAnsi(string, columns, options) {
1534
+ return String(string).normalize().split(CRLF_OR_LF).map((line) => exec(line, columns, options)).join("\n");
1535
+ }
1536
+
1537
+ // ../../node_modules/.pnpm/@clack+core@1.5.1/node_modules/@clack/core/dist/index.mjs
1538
+ var import_sisteransi = __toESM(require_src(), 1);
1539
+ function findCursor(s, o2, l) {
1540
+ if (!l.some((r2) => !r2.disabled))
1541
+ return s;
1542
+ const t2 = s + o2, n3 = Math.max(l.length - 1, 0), e2 = t2 < 0 ? n3 : t2 > n3 ? 0 : t2;
1543
+ return l[e2]?.disabled ? findCursor(e2, o2 < 0 ? -1 : 1, l) : e2;
1544
+ }
1545
+ var a$1 = ["up", "down", "left", "right", "space", "enter", "cancel"];
1546
+ var t = [
1547
+ "January",
1548
+ "February",
1549
+ "March",
1550
+ "April",
1551
+ "May",
1552
+ "June",
1553
+ "July",
1554
+ "August",
1555
+ "September",
1556
+ "October",
1557
+ "November",
1558
+ "December"
1559
+ ];
1560
+ var settings = {
1561
+ actions: new Set(a$1),
1562
+ aliases: /* @__PURE__ */ new Map([
1563
+ // vim support
1564
+ ["k", "up"],
1565
+ ["j", "down"],
1566
+ ["h", "left"],
1567
+ ["l", "right"],
1568
+ ["", "cancel"],
1569
+ // opinionated defaults!
1570
+ ["escape", "cancel"]
1571
+ ]),
1572
+ messages: {
1573
+ cancel: "Canceled",
1574
+ error: "Something went wrong"
1575
+ },
1576
+ withGuide: true,
1577
+ accessible: void 0,
1578
+ date: {
1579
+ monthNames: [...t],
1580
+ messages: {
1581
+ required: "Please enter a valid date",
1582
+ invalidMonth: "There are only 12 months in a year",
1583
+ invalidDay: (n3, e2) => `There are only ${n3} days in ${e2}`,
1584
+ afterMin: (n3) => `Date must be on or after ${n3.toISOString().slice(0, 10)}`,
1585
+ beforeMax: (n3) => `Date must be on or before ${n3.toISOString().slice(0, 10)}`
1586
+ }
1587
+ }
1588
+ };
1589
+ function isAccessible(n3) {
1590
+ if (n3 !== void 0) return n3;
1591
+ if (settings.accessible !== void 0) return settings.accessible;
1592
+ const e2 = process.env.ACCESSIBLE;
1593
+ return e2 !== void 0 && e2 !== "" && e2 !== "0" && e2 !== "false";
1594
+ }
1595
+ function isActionKey(n3, e2) {
1596
+ if (typeof n3 == "string")
1597
+ return settings.aliases.get(n3) === e2;
1598
+ for (const s of n3)
1599
+ if (s !== void 0 && isActionKey(s, e2))
1600
+ return true;
1601
+ return false;
1602
+ }
1603
+ function diffLines(i2, s) {
1604
+ if (i2 === s) return;
1605
+ const e2 = i2.split(`
1606
+ `), t2 = s.split(`
1607
+ `), r2 = Math.max(e2.length, t2.length), f = [];
1608
+ for (let n3 = 0; n3 < r2; n3++)
1609
+ e2[n3] !== t2[n3] && f.push(n3);
1610
+ return {
1611
+ lines: f,
1612
+ numLinesBefore: e2.length,
1613
+ numLinesAfter: t2.length,
1614
+ numLines: r2
1615
+ };
1616
+ }
1617
+ var R = globalThis.process.platform.startsWith("win");
1618
+ var CANCEL_SYMBOL = /* @__PURE__ */ Symbol("clack:cancel");
1619
+ function isCancel(e2) {
1620
+ return e2 === CANCEL_SYMBOL;
1621
+ }
1622
+ function setRawMode(e2, r2) {
1623
+ const o2 = e2;
1624
+ o2.isTTY && o2.setRawMode(r2);
1625
+ }
1626
+ var getColumns = (e2) => "columns" in e2 && typeof e2.columns == "number" ? e2.columns : 80;
1627
+ var getRows = (e2) => "rows" in e2 && typeof e2.rows == "number" ? e2.rows : 20;
1628
+ function wrapTextWithPrefix(e2, r2, o2, n3 = o2, s = o2, t2) {
1629
+ const f = getColumns(e2 ?? stdout);
1630
+ return wrapAnsi(r2, f - o2.length, {
1631
+ hard: true,
1632
+ trim: false
1633
+ }).split(`
1634
+ `).map((c2, i2, m) => {
1635
+ const d = t2 ? t2(c2, i2) : c2;
1636
+ return i2 === 0 ? `${n3}${d}` : i2 === m.length - 1 ? `${s}${d}` : `${o2}${d}`;
1637
+ }).join(`
1638
+ `);
1639
+ }
1640
+ function runValidation(e2, a2) {
1641
+ if ("~standard" in e2) {
1642
+ const n3 = e2["~standard"].validate(a2);
1643
+ return n3 instanceof Promise ? n3.then((r2) => r2.issues?.at(0)?.message) : n3.issues?.at(0)?.message;
1644
+ }
1645
+ return e2(a2);
1646
+ }
1647
+ var y = class {
1648
+ input;
1649
+ output;
1650
+ _abortSignal;
1651
+ rl;
1652
+ opts;
1653
+ _render;
1654
+ _track = false;
1655
+ _prevFrame = "";
1656
+ _subscribers = /* @__PURE__ */ new Map();
1657
+ _cursor = 0;
1658
+ state = "initial";
1659
+ error = "";
1660
+ value;
1661
+ userInput = "";
1662
+ /**
1663
+ * Whether accessible (static, screen-reader friendly) output is enabled for
1664
+ * this prompt, resolved from the `accessible` option, the global setting,
1665
+ * and the `ACCESSIBLE` env var.
1666
+ */
1667
+ get accessible() {
1668
+ return isAccessible(this.opts.accessible);
1669
+ }
1670
+ constructor(t2, e2 = true) {
1671
+ const { input: i2 = stdin, output: s = stdout, render: r2, signal: n3, ...o2 } = t2;
1672
+ this.opts = o2, this.onKeypress = this.onKeypress.bind(this), this.close = this.close.bind(this), this.render = this.render.bind(this), this._render = r2.bind(this), this._track = e2, this._abortSignal = n3, this.input = i2, this.output = s;
1673
+ }
1674
+ /**
1675
+ * Unsubscribe all listeners
1676
+ */
1677
+ unsubscribe() {
1678
+ this._subscribers.clear();
1679
+ }
1680
+ /**
1681
+ * Set a subscriber with opts
1682
+ * @param event - The event name
1683
+ */
1684
+ setSubscriber(t2, e2) {
1685
+ const i2 = this._subscribers.get(t2) ?? [];
1686
+ i2.push(e2), this._subscribers.set(t2, i2);
1687
+ }
1688
+ /**
1689
+ * Subscribe to an event
1690
+ * @param event - The event name
1691
+ * @param cb - The callback
1692
+ */
1693
+ on(t2, e2) {
1694
+ this.setSubscriber(t2, { cb: e2 });
1695
+ }
1696
+ /**
1697
+ * Subscribe to an event once
1698
+ * @param event - The event name
1699
+ * @param cb - The callback
1700
+ */
1701
+ once(t2, e2) {
1702
+ this.setSubscriber(t2, { cb: e2, once: true });
1703
+ }
1704
+ /**
1705
+ * Emit an event with data
1706
+ * @param event - The event name
1707
+ * @param data - The data to pass to the callback
1708
+ */
1709
+ emit(t2, ...e2) {
1710
+ const i2 = this._subscribers.get(t2) ?? [], s = [];
1711
+ for (const r2 of i2)
1712
+ r2.cb(...e2), r2.once && s.push(() => i2.splice(i2.indexOf(r2), 1));
1713
+ for (const r2 of s)
1714
+ r2();
1715
+ }
1716
+ prompt() {
1717
+ return new Promise((t2) => {
1718
+ if (this._abortSignal) {
1719
+ if (this._abortSignal.aborted)
1720
+ return this.state = "cancel", this.close(), t2(CANCEL_SYMBOL);
1721
+ this._abortSignal.addEventListener(
1722
+ "abort",
1723
+ () => {
1724
+ this.state = "cancel", this.close();
1725
+ },
1726
+ { once: true }
1727
+ );
1728
+ }
1729
+ this.rl = l__default.createInterface({
1730
+ input: this.input,
1731
+ tabSize: 2,
1732
+ prompt: "",
1733
+ escapeCodeTimeout: 50,
1734
+ terminal: true
1735
+ }), this.rl.prompt(), this.opts.initialUserInput !== void 0 && this._setUserInput(this.opts.initialUserInput, true), this.input.on("keypress", this.onKeypress), setRawMode(this.input, true), this.output.on("resize", this.render), this.render(), this.once("submit", () => {
1736
+ this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t2(this.value);
1737
+ }), this.once("cancel", () => {
1738
+ this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t2(CANCEL_SYMBOL);
1739
+ });
1740
+ });
1741
+ }
1742
+ _isActionKey(t2, e2) {
1743
+ return t2 === " ";
1744
+ }
1745
+ _shouldSubmit(t2, e2) {
1746
+ return true;
1747
+ }
1748
+ _setValue(t2) {
1749
+ this.value = t2, this.emit("value", this.value);
1750
+ }
1751
+ _setUserInput(t2, e2) {
1752
+ this.userInput = t2 ?? "", this.emit("userInput", this.userInput), e2 && this._track && this.rl && (this.rl.write(this.userInput), this._cursor = this.rl.cursor);
1753
+ }
1754
+ _clearUserInput() {
1755
+ this.rl?.write(null, { ctrl: true, name: "u" }), this._setUserInput("");
1756
+ }
1757
+ async onKeypress(t2, e2) {
1758
+ if (this.state !== "validating") {
1759
+ if (this._track && e2.name !== "return" && (e2.name && this._isActionKey(t2, e2) && this.rl?.write(null, { ctrl: true, name: "h" }), this._cursor = this.rl?.cursor ?? 0, this._setUserInput(this.rl?.line)), this.state === "error" && (this.state = "active"), e2?.name && (!this._track && settings.aliases.has(e2.name) && this.emit("cursor", settings.aliases.get(e2.name)), settings.actions.has(e2.name) && this.emit("cursor", e2.name)), t2 && (t2.toLowerCase() === "y" || t2.toLowerCase() === "n") && this.emit("confirm", t2.toLowerCase() === "y"), this.emit("key", t2, e2), e2?.name === "return" && this._shouldSubmit(t2, e2)) {
1760
+ if (this.opts.validate) {
1761
+ const i2 = runValidation(this.opts.validate, this.value);
1762
+ let s;
1763
+ i2 instanceof Promise ? (this.state = "validating", this.render(), s = await i2) : s = i2, s && (this.error = s instanceof Error ? s.message : s, this.state = "error", this.rl?.write(this.userInput));
1764
+ }
1765
+ this.state !== "error" && (this.state = "submit");
1766
+ }
1767
+ isActionKey([t2, e2?.name, e2?.sequence], "cancel") && (this.state = "cancel"), (this.state === "submit" || this.state === "cancel") && this.emit("finalize"), this.render(), (this.state === "submit" || this.state === "cancel") && this.close();
1768
+ }
1769
+ }
1770
+ close() {
1771
+ this.input.unpipe(), this.input.removeListener("keypress", this.onKeypress), this.output.write(`
1772
+ `), setRawMode(this.input, false), this.rl?.close(), this.rl = void 0, this.emit(`${this.state}`, this.value), this.unsubscribe();
1773
+ }
1774
+ restoreCursor() {
1775
+ const t2 = wrapAnsi(this._prevFrame, process.stdout.columns, { hard: true, trim: false }).split(`
1776
+ `).length - 1;
1777
+ this.output.write(import_sisteransi.cursor.move(-999, t2 * -1));
1778
+ }
1779
+ render() {
1780
+ const t2 = wrapAnsi(this._render(this) ?? "", process.stdout.columns, {
1781
+ hard: true,
1782
+ trim: false
1783
+ });
1784
+ if (t2 !== this._prevFrame) {
1785
+ if (this.state === "initial")
1786
+ this.output.write(import_sisteransi.cursor.hide);
1787
+ else {
1788
+ const e2 = diffLines(this._prevFrame, t2), i2 = getRows(this.output);
1789
+ if (this.restoreCursor(), e2) {
1790
+ const s = Math.max(0, e2.numLinesAfter - i2), r2 = Math.max(0, e2.numLinesBefore - i2);
1791
+ let n3 = e2.lines.find((o2) => o2 >= s);
1792
+ if (n3 === void 0) {
1793
+ this._prevFrame = t2;
1794
+ return;
1795
+ }
1796
+ if (e2.lines.length === 1) {
1797
+ this.output.write(import_sisteransi.cursor.move(0, n3 - r2)), this.output.write(import_sisteransi.erase.lines(1));
1798
+ const o2 = t2.split(`
1799
+ `);
1800
+ this.output.write(o2[n3]), this._prevFrame = t2, this.output.write(import_sisteransi.cursor.move(0, o2.length - n3 - 1));
1801
+ return;
1802
+ } else if (e2.lines.length > 1) {
1803
+ if (s < r2)
1804
+ n3 = s;
1805
+ else {
1806
+ const h2 = n3 - r2;
1807
+ h2 > 0 && this.output.write(import_sisteransi.cursor.move(0, h2));
1808
+ }
1809
+ this.output.write(import_sisteransi.erase.down());
1810
+ const f = t2.split(`
1811
+ `).slice(n3);
1812
+ this.output.write(f.join(`
1813
+ `)), this._prevFrame = t2;
1814
+ return;
1815
+ }
1816
+ }
1817
+ this.output.write(import_sisteransi.erase.down());
1818
+ }
1819
+ this.output.write(t2), this.state === "initial" && (this.state = "active"), this._prevFrame = t2;
1820
+ }
1821
+ }
1822
+ };
1823
+ var r = class extends y {
1824
+ get cursor() {
1825
+ return this.value ? 0 : 1;
1826
+ }
1827
+ get _value() {
1828
+ return this.cursor === 0;
1829
+ }
1830
+ constructor(t2) {
1831
+ super(t2, false), this.value = !!t2.initialValue, this.on("userInput", () => {
1832
+ this.value = this._value;
1833
+ }), this.on("confirm", (i2) => {
1834
+ this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = i2, this.state = "submit", this.close();
1835
+ }), this.on("cursor", () => {
1836
+ this.value = !this.value;
1837
+ });
1838
+ }
1839
+ };
1840
+ var u$1 = class u extends y {
1841
+ _mask = "\u2022";
1842
+ get cursor() {
1843
+ return this._cursor;
1844
+ }
1845
+ get masked() {
1846
+ return this.userInput.replaceAll(/./g, this._mask);
1847
+ }
1848
+ get userInputWithCursor() {
1849
+ if (this.state === "submit" || this.state === "cancel")
1850
+ return this.masked;
1851
+ const t2 = this.userInput;
1852
+ if (this.cursor >= t2.length)
1853
+ return `${this.masked}${styleText(["inverse", "hidden"], "_")}`;
1854
+ const s = this.masked, r2 = s.slice(0, this.cursor), i2 = s.slice(this.cursor, this.cursor + 1), o2 = s.slice(this.cursor + 1);
1855
+ return `${r2}${styleText("inverse", i2)}${o2}`;
1856
+ }
1857
+ clear() {
1858
+ this._clearUserInput();
1859
+ }
1860
+ constructor({ mask: t2, ...s }) {
1861
+ super(s), this._mask = t2 ?? "\u2022", this.on("userInput", (r2) => {
1862
+ this._setValue(r2);
1863
+ }), this.on("finalize", () => {
1864
+ this.value === void 0 && (this.value = "");
1865
+ });
1866
+ }
1867
+ };
1868
+ var n$1 = class n extends y {
1869
+ options;
1870
+ cursor = 0;
1871
+ get _selectedValue() {
1872
+ return this.options[this.cursor];
1873
+ }
1874
+ changeValue() {
1875
+ const e2 = this._selectedValue;
1876
+ this.value = e2 === void 0 ? void 0 : e2.value;
1877
+ }
1878
+ constructor(e2) {
1879
+ super(e2, false), this.options = e2.options;
1880
+ const o2 = this.options.findIndex(({ value: s }) => s === e2.initialValue), t2 = o2 === -1 ? 0 : o2;
1881
+ this.cursor = this.options[t2]?.disabled ? findCursor(t2, 1, this.options) : t2, this.changeValue(), this.on("cursor", (s) => {
1882
+ switch (s) {
1883
+ case "left":
1884
+ case "up":
1885
+ this.cursor = findCursor(this.cursor, -1, this.options);
1886
+ break;
1887
+ case "down":
1888
+ case "right":
1889
+ this.cursor = findCursor(this.cursor, 1, this.options);
1890
+ break;
1891
+ }
1892
+ this.changeValue();
1893
+ });
1894
+ }
1895
+ };
1896
+ var n2 = class extends y {
1897
+ get userInputWithCursor() {
1898
+ if (this.state === "submit")
1899
+ return this.userInput;
1900
+ const t2 = this.userInput;
1901
+ if (this.cursor >= t2.length)
1902
+ return `${this.userInput}\u2588`;
1903
+ const r2 = t2.slice(0, this.cursor), s = t2.slice(this.cursor, this.cursor + 1), e2 = t2.slice(this.cursor + 1);
1904
+ return `${r2}${styleText("inverse", s)}${e2}`;
1905
+ }
1906
+ get cursor() {
1907
+ return this._cursor;
1908
+ }
1909
+ constructor(t2) {
1910
+ super({
1911
+ ...t2,
1912
+ initialUserInput: t2.initialUserInput ?? t2.initialValue
1913
+ }), this.on("userInput", (r2) => {
1914
+ this._setValue(r2);
1915
+ }), this.on("finalize", () => {
1916
+ this.value || (this.value = t2.defaultValue), this.value === void 0 && (this.value = "");
1917
+ });
1918
+ }
1919
+ };
1920
+
1921
+ // ../../node_modules/.pnpm/@clack+prompts@1.8.1/node_modules/@clack/prompts/dist/index.mjs
1922
+ import { styleText as styleText2, stripVTControlCharacters } from "node:util";
1923
+ import process$1 from "node:process";
1924
+ var import_sisteransi2 = __toESM(require_src(), 1);
1925
+ function isUnicodeSupported() {
1926
+ if (process$1.platform !== "win32") {
1927
+ return process$1.env.TERM !== "linux";
1928
+ }
1929
+ return Boolean(process$1.env.CI) || Boolean(process$1.env.WT_SESSION) || Boolean(process$1.env.TERMINUS_SUBLIME) || process$1.env.ConEmuTask === "{cmd::Cmder}" || process$1.env.TERM_PROGRAM === "Terminus-Sublime" || process$1.env.TERM_PROGRAM === "vscode" || process$1.env.TERM === "xterm-256color" || process$1.env.TERM === "alacritty" || process$1.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
1930
+ }
1931
+ var unicode = isUnicodeSupported();
1932
+ var unicodeOr = (o2, e2) => unicode ? o2 : e2;
1933
+ var S_STEP_ACTIVE = unicodeOr("\u25C6", "*");
1934
+ var S_STEP_CANCEL = unicodeOr("\u25A0", "x");
1935
+ var S_STEP_ERROR = unicodeOr("\u25B2", "x");
1936
+ var S_STEP_SUBMIT = unicodeOr("\u25C7", "o");
1937
+ var S_BAR_START = unicodeOr("\u250C", "T");
1938
+ var S_BAR = unicodeOr("\u2502", "|");
1939
+ var S_BAR_END = unicodeOr("\u2514", "\u2014");
1940
+ var S_BAR_START_RIGHT = unicodeOr("\u2510", "T");
1941
+ var S_BAR_END_RIGHT = unicodeOr("\u2518", "\u2014");
1942
+ var S_RADIO_ACTIVE = unicodeOr("\u25CF", ">");
1943
+ var S_RADIO_INACTIVE = unicodeOr("\u25CB", " ");
1944
+ var S_CHECKBOX_ACTIVE = unicodeOr("\u25FB", "[\u2022]");
1945
+ var S_CHECKBOX_SELECTED = unicodeOr("\u25FC", "[+]");
1946
+ var S_CHECKBOX_INACTIVE = unicodeOr("\u25FB", "[ ]");
1947
+ var S_PASSWORD_MASK = unicodeOr("\u25AA", "\u2022");
1948
+ var S_BAR_H = unicodeOr("\u2500", "-");
1949
+ var S_CORNER_TOP_RIGHT = unicodeOr("\u256E", "+");
1950
+ var S_CONNECT_LEFT = unicodeOr("\u251C", "+");
1951
+ var S_CORNER_BOTTOM_RIGHT = unicodeOr("\u256F", "+");
1952
+ var S_CORNER_BOTTOM_LEFT = unicodeOr("\u2570", "+");
1953
+ var S_CORNER_TOP_LEFT = unicodeOr("\u256D", "+");
1954
+ var S_INFO = unicodeOr("\u25CF", "\u2022");
1955
+ var S_SUCCESS = unicodeOr("\u25C6", "*");
1956
+ var S_WARN = unicodeOr("\u25B2", "!");
1957
+ var S_ERROR = unicodeOr("\u25A0", "x");
1958
+ var symbol = (o2) => {
1959
+ switch (o2) {
1960
+ case "initial":
1961
+ case "active":
1962
+ return styleText2("cyan", S_STEP_ACTIVE);
1963
+ case "cancel":
1964
+ return styleText2("red", S_STEP_CANCEL);
1965
+ case "error":
1966
+ return styleText2("yellow", S_STEP_ERROR);
1967
+ case "submit":
1968
+ return styleText2("green", S_STEP_SUBMIT);
1969
+ case "validating":
1970
+ return styleText2("dim", S_STEP_ACTIVE);
1971
+ }
1972
+ };
1973
+ var symbolBar = (o2) => {
1974
+ switch (o2) {
1975
+ case "initial":
1976
+ case "active":
1977
+ return styleText2("cyan", S_BAR);
1978
+ case "cancel":
1979
+ return styleText2("red", S_BAR);
1980
+ case "error":
1981
+ return styleText2("yellow", S_BAR);
1982
+ case "submit":
1983
+ return styleText2("green", S_BAR);
1984
+ }
1985
+ };
1986
+ function formatInstructionFooter(o2, e2) {
1987
+ const r2 = [`${e2 ? `${styleText2("cyan", S_BAR)} ` : ""}${o2.join(" \u2022 ")}`];
1988
+ return e2 && r2.push(styleText2("cyan", S_BAR_END)), r2;
1989
+ }
1990
+ var I = (l, e2, w, p, b, C = false) => {
1991
+ let r2 = e2, O = 0;
1992
+ if (C)
1993
+ for (let i2 = p - 1; i2 >= w; i2--) {
1994
+ const m = l[i2];
1995
+ if (m && (r2 -= m.length), O++, r2 <= b) break;
1996
+ }
1997
+ else
1998
+ for (let i2 = w; i2 < p; i2++) {
1999
+ const m = l[i2];
2000
+ if (m && (r2 -= m.length), O++, r2 <= b) break;
2001
+ }
2002
+ return { lineCount: r2, removals: O };
2003
+ };
2004
+ var limitOptions = ({
2005
+ cursor: l,
2006
+ options: e2,
2007
+ style: w,
2008
+ output: p = process.stdout,
2009
+ maxItems: b = Number.POSITIVE_INFINITY,
2010
+ columnPadding: C = 0,
2011
+ rowPadding: r2 = 4
2012
+ }) => {
2013
+ const i2 = getColumns(p) - C, m = getRows(p), M = styleText2("dim", "..."), v = Math.max(m - r2, 0), a2 = Math.max(Math.min(b, v), 5);
2014
+ let f = 0;
2015
+ l >= a2 - 3 && (f = Math.max(
2016
+ Math.min(l - a2 + 3, e2.length - a2),
2017
+ 0
2018
+ ));
2019
+ let d = a2 < e2.length && f > 0, c2 = a2 < e2.length && f + a2 < e2.length;
2020
+ const W = Math.min(
2021
+ f + a2,
2022
+ e2.length
2023
+ ), s = [];
2024
+ let g = 0;
2025
+ d && g++, c2 && g++;
2026
+ const T = f + (d ? 1 : 0), y2 = W - (c2 ? 1 : 0);
2027
+ for (let t2 = T; t2 < y2; t2++) {
2028
+ const n3 = e2[t2], o2 = n3 ? w(n3, t2 === l) : "", h2 = wrapAnsi(o2, i2, {
2029
+ hard: true,
2030
+ trim: false
2031
+ }).split(`
2032
+ `);
2033
+ s.push(h2), g += h2.length;
2034
+ }
2035
+ if (g > v) {
2036
+ let t2 = 0, n3 = 0, o2 = g;
2037
+ const h2 = l - T;
2038
+ let u4 = v;
2039
+ const L = () => I(s, o2, 0, h2, u4), E = () => I(
2040
+ s,
2041
+ o2,
2042
+ h2 + 1,
2043
+ s.length,
2044
+ u4,
2045
+ true
2046
+ );
2047
+ d ? ({ lineCount: o2, removals: t2 } = L(), o2 > u4 && (c2 || (u4 -= 1), { lineCount: o2, removals: n3 } = E())) : (c2 || (u4 -= 1), { lineCount: o2, removals: n3 } = E(), o2 > u4 && (u4 -= 1, { lineCount: o2, removals: t2 } = L())), t2 > 0 && (d = true, s.splice(0, t2)), n3 > 0 && (c2 = true, s.splice(s.length - n3, n3));
2048
+ }
2049
+ const x = [];
2050
+ d && x.push(M);
2051
+ for (const t2 of s)
2052
+ for (const n3 of t2)
2053
+ x.push(n3);
2054
+ return c2 && x.push(M), x;
2055
+ };
2056
+ var confirm = (e2) => {
2057
+ const a2 = e2.active ?? "Yes", o2 = e2.inactive ?? "No";
2058
+ return new r({
2059
+ active: a2,
2060
+ inactive: o2,
2061
+ signal: e2.signal,
2062
+ input: e2.input,
2063
+ output: e2.output,
2064
+ initialValue: e2.initialValue ?? true,
2065
+ render() {
2066
+ const i2 = e2.withGuide ?? settings.withGuide, u4 = `${symbol(this.state)} `, l = i2 ? `${styleText2("gray", S_BAR)} ` : "", f = wrapTextWithPrefix(
2067
+ e2.output,
2068
+ e2.message,
2069
+ l,
2070
+ u4
2071
+ ), s = `${i2 ? `${styleText2("gray", S_BAR)}
2072
+ ` : ""}${f}
2073
+ `, c2 = this.value ? a2 : o2;
2074
+ switch (this.state) {
2075
+ case "submit": {
2076
+ const r2 = i2 ? `${styleText2("gray", S_BAR)} ` : "";
2077
+ return `${s}${r2}${styleText2("dim", c2)}`;
2078
+ }
2079
+ case "cancel": {
2080
+ const r2 = i2 ? `${styleText2("gray", S_BAR)} ` : "";
2081
+ return `${s}${r2}${styleText2(["strikethrough", "dim"], c2)}${i2 ? `
2082
+ ${styleText2("gray", S_BAR)}` : ""}`;
2083
+ }
2084
+ default: {
2085
+ const r2 = i2 ? `${styleText2("cyan", S_BAR)} ` : "", g = i2 ? styleText2("cyan", S_BAR_END) : "";
2086
+ return `${s}${r2}${this.value ? `${styleText2("green", S_RADIO_ACTIVE)} ${a2}` : `${styleText2("dim", S_RADIO_INACTIVE)} ${styleText2("dim", a2)}`}${e2.vertical ? i2 ? `
2087
+ ${styleText2("cyan", S_BAR)} ` : `
2088
+ ` : ` ${styleText2("dim", "/")} `}${this.value ? `${styleText2("dim", S_RADIO_INACTIVE)} ${styleText2("dim", o2)}` : `${styleText2("green", S_RADIO_ACTIVE)} ${o2}`}
2089
+ ${g}
2090
+ `;
2091
+ }
2092
+ }
2093
+ }
2094
+ }).prompt();
2095
+ };
2096
+ var MULTISELECT_INSTRUCTIONS = [
2097
+ `${styleText2("dim", "\u2191/\u2193")} to navigate`,
2098
+ `${styleText2("dim", "Space:")} select`,
2099
+ `${styleText2("dim", "Enter:")} confirm`
2100
+ ];
2101
+ var password = (e2) => new u$1({
2102
+ validate: e2.validate,
2103
+ mask: e2.mask ?? S_PASSWORD_MASK,
2104
+ signal: e2.signal,
2105
+ input: e2.input,
2106
+ output: e2.output,
2107
+ render() {
2108
+ const r2 = e2.withGuide ?? settings.withGuide, o2 = `${r2 ? `${styleText2("gray", S_BAR)}
2109
+ ` : ""}${symbol(this.state)} ${e2.message}
2110
+ `, m = this.userInputWithCursor, i2 = this.masked;
2111
+ switch (this.state) {
2112
+ case "error": {
2113
+ const s = r2 ? `${styleText2("yellow", S_BAR)} ` : "", n3 = r2 ? `${styleText2("yellow", S_BAR_END)} ` : "", d = i2 ?? "";
2114
+ return e2.clearOnError && this.clear(), `${o2.trim()}
2115
+ ${s}${d}
2116
+ ${n3}${styleText2("yellow", this.error)}
2117
+ `;
2118
+ }
2119
+ case "submit": {
2120
+ const s = r2 ? `${styleText2("gray", S_BAR)} ` : "", n3 = i2 ? styleText2("dim", i2) : "";
2121
+ return `${o2}${s}${n3}`;
2122
+ }
2123
+ case "cancel": {
2124
+ const s = r2 ? `${styleText2("gray", S_BAR)} ` : "", n3 = i2 ? styleText2(["strikethrough", "dim"], i2) : "";
2125
+ return `${o2}${s}${n3}${i2 && r2 ? `
2126
+ ${styleText2("gray", S_BAR)}` : ""}`;
2127
+ }
2128
+ default: {
2129
+ const s = r2 ? `${styleText2("cyan", S_BAR)} ` : "", n3 = r2 ? styleText2("cyan", S_BAR_END) : "";
2130
+ return `${o2}${s}${m}
2131
+ ${n3}
2132
+ `;
2133
+ }
2134
+ }
2135
+ }
2136
+ }).prompt();
2137
+ var u3 = {
2138
+ light: unicodeOr("\u2500", "-"),
2139
+ heavy: unicodeOr("\u2501", "="),
2140
+ block: unicodeOr("\u2588", "#")
2141
+ };
2142
+ var SELECT_INSTRUCTIONS = [
2143
+ `${styleText2("dim", "\u2191/\u2193")} to navigate`,
2144
+ `${styleText2("dim", "Enter:")} confirm`
2145
+ ];
2146
+ var c = (t2, o2) => t2.includes(`
2147
+ `) ? t2.split(`
2148
+ `).map((d) => o2(d)).join(`
2149
+ `) : o2(t2);
2150
+ var select = (t2) => {
2151
+ const o2 = (n3, m) => {
2152
+ if (n3 === void 0)
2153
+ return "";
2154
+ const s = n3.label ?? String(n3.value);
2155
+ switch (m) {
2156
+ case "disabled":
2157
+ return `${styleText2("gray", S_RADIO_INACTIVE)} ${c(s, (i2) => styleText2("gray", i2))}${n3.hint ? ` ${styleText2("dim", `(${n3.hint ?? "disabled"})`)}` : ""}`;
2158
+ case "selected":
2159
+ return `${c(s, (i2) => styleText2("dim", i2))}`;
2160
+ case "active":
2161
+ return `${styleText2("green", S_RADIO_ACTIVE)} ${s}${n3.hint ? ` ${styleText2("dim", `(${n3.hint})`)}` : ""}`;
2162
+ case "cancelled":
2163
+ return `${c(s, (i2) => styleText2(["strikethrough", "dim"], i2))}`;
2164
+ default:
2165
+ return `${styleText2("dim", S_RADIO_INACTIVE)} ${c(s, (i2) => styleText2("dim", i2))}`;
2166
+ }
2167
+ }, d = t2.showInstructions ?? true;
2168
+ return new n$1({
2169
+ options: t2.options,
2170
+ signal: t2.signal,
2171
+ input: t2.input,
2172
+ output: t2.output,
2173
+ initialValue: t2.initialValue,
2174
+ render() {
2175
+ const n3 = t2.withGuide ?? settings.withGuide, m = `${symbol(this.state)} `, s = `${symbolBar(this.state)} `, i2 = wrapTextWithPrefix(
2176
+ t2.output,
2177
+ t2.message,
2178
+ s,
2179
+ m
2180
+ ), u4 = `${n3 ? `${styleText2("gray", S_BAR)}
2181
+ ` : ""}${i2}
2182
+ `;
2183
+ switch (this.state) {
2184
+ case "submit": {
2185
+ const r2 = n3 ? `${styleText2("gray", S_BAR)} ` : "", a2 = wrapTextWithPrefix(
2186
+ t2.output,
2187
+ o2(this.options[this.cursor], "selected"),
2188
+ r2
2189
+ );
2190
+ return `${u4}${a2}`;
2191
+ }
2192
+ case "cancel": {
2193
+ const r2 = n3 ? `${styleText2("gray", S_BAR)} ` : "", a2 = wrapTextWithPrefix(
2194
+ t2.output,
2195
+ o2(this.options[this.cursor], "cancelled"),
2196
+ r2
2197
+ );
2198
+ return `${u4}${a2}${n3 ? `
2199
+ ${styleText2("gray", S_BAR)}` : ""}`;
2200
+ }
2201
+ default: {
2202
+ const r2 = n3 ? `${styleText2("cyan", S_BAR)} ` : "", a2 = u4.split(`
2203
+ `).length, p = d ? formatInstructionFooter(SELECT_INSTRUCTIONS, n3) : n3 ? [styleText2("cyan", S_BAR_END)] : [], f = p.join(`
2204
+ `), b = p.length + 1;
2205
+ return `${u4}${r2}${limitOptions({
2206
+ output: t2.output,
2207
+ cursor: this.cursor,
2208
+ options: this.options,
2209
+ maxItems: t2.maxItems,
2210
+ columnPadding: r2.length,
2211
+ rowPadding: a2 + b,
2212
+ style: (g, x) => o2(g, g.disabled ? "disabled" : x ? "active" : "inactive")
2213
+ }).join(`
2214
+ ${r2}`)}
2215
+ ${f}
2216
+ `;
2217
+ }
2218
+ }
2219
+ }
2220
+ }).prompt();
2221
+ };
2222
+ var i = `${styleText2("gray", S_BAR)} `;
2223
+ var text = (t2) => new n2({
2224
+ validate: t2.validate,
2225
+ placeholder: t2.placeholder,
2226
+ defaultValue: t2.defaultValue,
2227
+ initialValue: t2.initialValue,
2228
+ output: t2.output,
2229
+ signal: t2.signal,
2230
+ input: t2.input,
2231
+ render() {
2232
+ const r2 = t2?.withGuide ?? settings.withGuide, l = `${`${r2 ? `${styleText2("gray", S_BAR)}
2233
+ ` : ""}${symbol(this.state)} `}${t2.message}
2234
+ `, d = t2.placeholder && t2.placeholder.length > 0 ? (
2235
+ // biome-ignore lint/style/noNonNullAssertion: guarded by placeholder.length > 0
2236
+ styleText2("inverse", t2.placeholder[0]) + styleText2("dim", t2.placeholder.slice(1))
2237
+ ) : styleText2(["inverse", "hidden"], "_"), o2 = this.userInput ? this.userInputWithCursor : d, s = this.value ?? "";
2238
+ switch (this.state) {
2239
+ case "validating": {
2240
+ const n3 = r2 ? `${styleText2("cyan", S_BAR)} ` : "", i2 = r2 ? styleText2("cyan", S_BAR_END) : "", c2 = styleText2("dim", o2), $ = styleText2("dim", "Validating...");
2241
+ return `${l}${n3}${c2}
2242
+ ${i2} ${$}
2243
+ `;
2244
+ }
2245
+ case "error": {
2246
+ const n3 = this.error ? ` ${styleText2("yellow", this.error)}` : "", i2 = r2 ? `${styleText2("yellow", S_BAR)} ` : "", c2 = r2 ? styleText2("yellow", S_BAR_END) : "";
2247
+ return `${l.trim()}
2248
+ ${i2}${o2}
2249
+ ${c2}${n3}
2250
+ `;
2251
+ }
2252
+ case "submit": {
2253
+ const n3 = s ? `${r2 ? " " : ""}${styleText2("dim", s)}` : "", i2 = r2 ? styleText2("gray", S_BAR) : "";
2254
+ return `${l}${i2}${n3}`;
2255
+ }
2256
+ case "cancel": {
2257
+ const n3 = s ? ` ${styleText2(["strikethrough", "dim"], s)}` : "", i2 = r2 ? styleText2("gray", S_BAR) : "";
2258
+ return `${l}${i2}${n3}${s.trim() ? `
2259
+ ${i2}` : ""}`;
2260
+ }
2261
+ default: {
2262
+ const n3 = r2 ? `${styleText2("cyan", S_BAR)} ` : "", i2 = r2 ? styleText2("cyan", S_BAR_END) : "";
2263
+ return `${l}${n3}${o2}
2264
+ ${i2}
2265
+ `;
2266
+ }
2267
+ }
2268
+ }
2269
+ }).prompt();
2270
+
2271
+ // src/guided.ts
2272
+ import { stat as stat3 } from "node:fs/promises";
2273
+ import { resolve as resolve2 } from "node:path";
2274
+ function answered(v) {
2275
+ if (isCancel(v) || typeof v === "symbol") throw new CliError("Cancelled.", void 0, "cancelled", 130);
2276
+ return v;
2277
+ }
2278
+ async function askSend() {
2279
+ const path = answered(
2280
+ await text({
2281
+ message: "What should I send?",
2282
+ placeholder: "a file or folder, e.g. ~/Shoot",
2283
+ validate: (v) => v?.trim() ? void 0 : "Give a path."
2284
+ })
2285
+ ).trim();
2286
+ const expanded = path.startsWith("~/") ? resolve2(process.env.HOME ?? "", path.slice(2)) : resolve2(path);
2287
+ await stat3(expanded).catch(() => {
2288
+ throw new CliError(`No such file or folder: ${path}`, void 0, "not_found");
2289
+ });
2290
+ const to = answered(
2291
+ await text({
2292
+ message: "Email it to anyone?",
2293
+ placeholder: "addresses, comma-separated; leave empty for a link only"
2294
+ })
2295
+ ).split(/[,\s]+/).filter(Boolean);
2296
+ const message = to.length ? answered(await text({ message: "A note for them?", placeholder: "optional" })).trim() || void 0 : void 0;
2297
+ return { paths: [expanded], to, message };
2298
+ }
2299
+ async function confirm2(message) {
2300
+ return answered(await confirm({ message, initialValue: true }));
2301
+ }
2302
+ async function askLink() {
2303
+ return answered(
2304
+ await text({ message: "Paste the link", placeholder: "https://yungle.co/t/\u2026", validate: (v) => v?.includes("/t/") || /^[\w-]{8,}$/.test(v ?? "") ? void 0 : "That is not a transfer link." })
2305
+ ).trim();
2306
+ }
2307
+ async function askPassword() {
2308
+ return answered(await password({ message: "This transfer has a password" })).trim();
2309
+ }
2310
+ async function pick(message, options) {
2311
+ return answered(await select({ message, options }));
2312
+ }
2313
+ async function askKey() {
2314
+ return answered(
2315
+ await password({
2316
+ message: "Paste an API key (create one at yungle.co/dashboard/settings/api)",
2317
+ validate: (v) => v?.startsWith("yk_live_") ? void 0 : "Keys start with yk_live_."
2318
+ })
2319
+ ).trim();
2320
+ }
2321
+
2322
+ // src/listen.ts
2323
+ import { createHmac } from "node:crypto";
2324
+ var LISTEN_DESCRIPTION = "yungle webhooks listen";
2325
+ var ALL = [
2326
+ "transfer.ready",
2327
+ "transfer.downloaded",
2328
+ "transfer.expiring",
2329
+ "transfer.expired",
2330
+ "collection.file_uploaded"
2331
+ ];
2332
+ function signForForward(secret, body, now = Date.now()) {
2333
+ const t2 = Math.floor(now / 1e3);
2334
+ return `t=${t2},v1=${createHmac("sha256", secret).update(`${t2}.${body}`).digest("hex")}`;
2335
+ }
2336
+ async function ensureListenEndpoint(api) {
2337
+ const { webhooks: webhooks2 } = await api.listWebhooks();
2338
+ const existing = webhooks2.find((w) => w.mode === "pull" && w.description === LISTEN_DESCRIPTION);
2339
+ if (existing) {
2340
+ if (!existing.enabled) await api.updateWebhook(existing.id, { enabled: true });
2341
+ const { secret } = await api.rotateWebhookSecret(existing.id);
2342
+ return { id: existing.id, secret };
2343
+ }
2344
+ try {
2345
+ const { webhook, secret } = await api.createWebhook({ url: null, events: ALL, description: LISTEN_DESCRIPTION });
2346
+ return { id: webhook.id, secret };
2347
+ } catch (err) {
2348
+ if (err instanceof YungleApiError && err.code === "upgrade_required" && /collection/.test(err.message)) {
2349
+ const { webhook, secret } = await api.createWebhook({
2350
+ url: null,
2351
+ events: ALL.filter((e2) => e2.startsWith("transfer.")),
2352
+ description: LISTEN_DESCRIPTION
2353
+ });
2354
+ return { id: webhook.id, secret };
2355
+ }
2356
+ if (err instanceof YungleApiError && err.code === "insufficient_scope") {
2357
+ throw new Error(
2358
+ "This session cannot add webhook endpoints: `yungle login` never grants that, so a phished code cannot wire your account to someone else\u2019s server. Use an API key with webhooks:write (yungle login --key), or add a pull endpoint in Settings \u2192 Webhooks and listen with a key."
2359
+ );
2360
+ }
2361
+ if (err instanceof YungleApiError && err.code === "upgrade_required") {
2362
+ throw new Error(
2363
+ "The free plan includes one webhook endpoint and this workspace already has one. Remove it in Settings \u2192 Webhooks to listen here, or upgrade for more."
2364
+ );
2365
+ }
2366
+ throw err;
2367
+ }
2368
+ }
2369
+ async function cursorAtNow(api, id) {
2370
+ let cursor3;
2371
+ for (; ; ) {
2372
+ const page = await api.listWebhookEvents(id, { limit: 500, cursor: cursor3 });
2373
+ cursor3 = page.nextCursor ?? cursor3;
2374
+ if (!page.hasMore) return cursor3;
2375
+ }
2376
+ }
2377
+ function describeEvent(e2) {
2378
+ const d = e2.data;
2379
+ if (d.file) return `${d.file.name} \u2192 ${d.collection?.title ?? "collection"}`;
2380
+ const what = d.transfer?.title || d.transfer?.id || "";
2381
+ if (d.download) return `${what}${d.download.recipient ? ` by ${d.download.recipient}` : ""}`;
2382
+ return d.reason ? `${what} (${d.reason})` : what;
2383
+ }
2384
+
2385
+ // src/login.ts
2386
+ import { spawn } from "node:child_process";
2387
+ var SCOPES = [
2388
+ "transfers:read",
2389
+ "transfers:write",
2390
+ "collections:read",
2391
+ "collections:write",
2392
+ "contacts:read",
2393
+ "contacts:write",
2394
+ "webhooks:read",
2395
+ "webhooks:write"
2396
+ ];
2397
+ function nextPoll(answer, interval) {
2398
+ if (answer.access_token) return { done: true };
2399
+ switch (answer.error) {
2400
+ case "authorization_pending":
2401
+ return { wait: interval };
2402
+ case "slow_down":
2403
+ return { wait: interval + 5 };
2404
+ // RFC 8628 §3.5
2405
+ case "access_denied":
2406
+ return { fail: "The request was declined in the browser." };
2407
+ case "expired_token":
2408
+ return { fail: "The code expired. Run `yungle login` again." };
2409
+ default:
2410
+ return { fail: `Sign-in failed: ${answer.error ?? "unexpected answer"}.` };
2411
+ }
2412
+ }
2413
+ function openBrowser(url) {
2414
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
2415
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
2416
+ try {
2417
+ spawn(cmd, args, { stdio: "ignore", detached: true }).on("error", () => void 0).unref();
2418
+ } catch {
2419
+ }
2420
+ }
2421
+ async function deviceLogin(baseUrl, opts) {
2422
+ const origin = originOf(baseUrl);
2423
+ const start = await fetch(`${origin}/oauth/device_authorization`, {
2424
+ method: "POST",
2425
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2426
+ body: new URLSearchParams({ client_id: CLI_CLIENT_ID, scope: SCOPES.join(" ") })
2427
+ });
2428
+ if (!start.ok) throw new Error(`Could not start sign-in (HTTP ${start.status}).`);
2429
+ const d = await start.json();
2430
+ process.stderr.write(`
2431
+ Open ${d.verification_uri}
2432
+ and type the code ${d.user_code}
2433
+
2434
+ `);
2435
+ if (opts.openBrowser) openBrowser(d.verification_uri);
2436
+ let interval = d.interval;
2437
+ const deadline = Date.now() + d.expires_in * 1e3;
2438
+ while (Date.now() < deadline) {
2439
+ await new Promise((r2) => setTimeout(r2, interval * 1e3));
2440
+ const res = await fetch(`${origin}/oauth/token`, {
2441
+ method: "POST",
2442
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2443
+ body: new URLSearchParams({
2444
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
2445
+ device_code: d.device_code,
2446
+ client_id: CLI_CLIENT_ID
2447
+ })
2448
+ });
2449
+ const answer = await res.json();
2450
+ const step = nextPoll(answer, interval);
2451
+ if ("fail" in step) throw new Error(step.fail);
2452
+ if ("done" in step) {
2453
+ const config = await readConfig();
2454
+ await writeConfig({
2455
+ ...config,
2456
+ ...baseUrl ? { baseUrl } : {},
2457
+ oauth: {
2458
+ accessToken: answer.access_token,
2459
+ refreshToken: answer.refresh_token,
2460
+ expiresAt: Date.now() + (answer.expires_in ?? 3600) * 1e3
2461
+ }
2462
+ });
2463
+ return answer.scope ?? "";
2464
+ }
2465
+ interval = step.wait;
2466
+ }
2467
+ throw new Error("The code expired. Run `yungle login` again.");
2468
+ }
2469
+ async function logout(baseUrl) {
2470
+ const config = await readConfig();
2471
+ if (!config.oauth) return false;
2472
+ const origin = originOf(baseUrl ?? config.baseUrl);
2473
+ for (const token of [config.oauth.refreshToken, config.oauth.accessToken]) {
2474
+ await fetch(`${origin}/oauth/revoke`, {
2475
+ method: "POST",
2476
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2477
+ body: new URLSearchParams({ token })
2478
+ }).catch(() => void 0);
2479
+ }
2480
+ const { oauth: _gone, ...rest } = config;
2481
+ await writeConfig(rest);
2482
+ return true;
2483
+ }
2484
+
2485
+ // src/mcp-install.ts
2486
+ import { homedir as homedir2, platform } from "node:os";
2487
+ import { join as join4 } from "node:path";
2488
+ function mcpClients(os = platform(), home = homedir2(), env2 = process.env) {
2489
+ const appData = env2.APPDATA ?? join4(home, "AppData", "Roaming");
2490
+ return [
2491
+ {
2492
+ id: "claude-desktop",
2493
+ label: "Claude Desktop",
2494
+ serversKey: "mcpServers",
2495
+ configPath: os === "darwin" ? join4(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : os === "win32" ? join4(appData, "Claude", "claude_desktop_config.json") : (
2496
+ // No Linux build of Claude Desktop exists, so there is nothing to write.
2497
+ null
2498
+ )
2499
+ },
2500
+ {
2501
+ id: "claude-code",
2502
+ label: "Claude Code",
2503
+ serversKey: "mcpServers",
2504
+ configPath: join4(home, ".claude.json"),
2505
+ viaCli: {
2506
+ bin: "claude",
2507
+ // `-s user` so it applies everywhere, not just the directory the install
2508
+ // happened to be run from — the default scope is `local`, which would
2509
+ // silently give a per-project install nobody asked for.
2510
+ args: (name, json) => ["mcp", "add-json", "-s", "user", name, json]
2511
+ }
2512
+ },
2513
+ {
2514
+ id: "cursor",
2515
+ label: "Cursor",
2516
+ serversKey: "mcpServers",
2517
+ configPath: join4(home, ".cursor", "mcp.json")
2518
+ },
2519
+ {
2520
+ id: "windsurf",
2521
+ label: "Windsurf",
2522
+ serversKey: "mcpServers",
2523
+ configPath: join4(home, ".codeium", "windsurf", "mcp_config.json")
2524
+ }
2525
+ ];
2526
+ }
2527
+ function serverEntry(opts) {
2528
+ const env2 = { YUNGLE_API_KEY: opts.apiKey };
2529
+ if (opts.baseUrl) env2.YUNGLE_API_URL = opts.baseUrl;
2530
+ return { command: "npx", args: ["-y", "yungle-mcp"], env: env2 };
2531
+ }
2532
+ function mergeServerEntry(existing, serversKey, name, entry) {
2533
+ const root = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
2534
+ const servers = root[serversKey];
2535
+ const nextServers = servers && typeof servers === "object" && !Array.isArray(servers) ? { ...servers } : {};
2536
+ const before = nextServers[name];
2537
+ if (before !== void 0 && deepEqual(before, entry)) return { kind: "unchanged" };
2538
+ nextServers[name] = entry;
2539
+ root[serversKey] = nextServers;
2540
+ return before === void 0 ? { kind: "added", config: root } : { kind: "updated", config: root, previous: before };
2541
+ }
2542
+ function deepEqual(a2, b) {
2543
+ if (a2 === b) return true;
2544
+ if (typeof a2 !== typeof b || a2 === null || b === null) return false;
2545
+ if (Array.isArray(a2) || Array.isArray(b)) {
2546
+ if (!Array.isArray(a2) || !Array.isArray(b) || a2.length !== b.length) return false;
2547
+ return a2.every((v, i2) => deepEqual(v, b[i2]));
2548
+ }
2549
+ if (typeof a2 !== "object") return false;
2550
+ const ka = Object.keys(a2);
2551
+ const kb = Object.keys(b);
2552
+ if (ka.length !== kb.length) return false;
2553
+ return ka.every(
2554
+ (k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a2[k], b[k])
2555
+ );
2556
+ }
2557
+ function writeScopes(scopes) {
2558
+ return scopes.filter((s) => s.endsWith(":write"));
2559
+ }
2560
+ function unusableWriteScopes(scopes) {
2561
+ return writeScopes(scopes).filter((s) => s !== "transfers:write");
2562
+ }
2563
+
2564
+ // src/output.ts
2565
+ function out(json, data, pretty, plain = pretty, code = 0) {
2566
+ const human = isTTY ? `${pretty.startsWith("\n") ? "" : "\n"}${pretty}
2567
+ ` : `${plain}
2568
+ `;
2569
+ process.stdout.write(json ? `${JSON.stringify(data, null, 2)}
2570
+ ` : human);
2571
+ return code;
2572
+ }
2573
+ function fail(err, json) {
2574
+ const x = explain(err);
2575
+ if (json) {
2576
+ process.stdout.write(`${JSON.stringify({ error: { code: x.code, message: x.message, ...x.details ? { details: x.details } : {} } }, null, 2)}
2577
+ `);
2578
+ } else {
2579
+ process.stderr.write(`
2580
+ ${e.red(sym.fail)} ${x.message}
2581
+ `);
2582
+ if (x.hint) process.stderr.write(` ${e.dim(sym.arrow)} ${accentErr(x.hint)}
2583
+ `);
2584
+ process.stderr.write("\n");
2585
+ }
2586
+ return x.code === "usage" ? 2 : 1;
2587
+ }
2588
+
2589
+ // src/mcp-command.ts
2590
+ async function mcp(positionals, flags, json, client2) {
2591
+ const sub = positionals[0] ?? "install";
2592
+ if (sub !== "install" && sub !== "status") {
2593
+ throw new CliError(`Unknown: yungle mcp ${sub}.`, "yungle mcp install or yungle mcp status", "usage", 2);
2594
+ }
2595
+ const { readFile: readFile3, writeFile: writeFile3, mkdir: mkdir4, copyFile } = await import("node:fs/promises");
2596
+ const { dirname: dirname3 } = await import("node:path");
2597
+ const all = mcpClients();
2598
+ const only = stringFlag(flags.client);
2599
+ if (only && !all.some((c2) => c2.id === only)) {
2600
+ throw new CliError(`Unknown client: ${only}.`, `--client ${all.map((c2) => c2.id).join(" | ")}`, "usage", 2);
2601
+ }
2602
+ const targets = only ? all.filter((c2) => c2.id === only) : all;
2603
+ if (sub === "status") {
2604
+ const rows = [];
2605
+ for (const c2 of targets) {
2606
+ let installed = false;
2607
+ if (c2.configPath) {
2608
+ try {
2609
+ const doc = JSON.parse(await readFile3(c2.configPath, "utf8"));
2610
+ const servers = doc[c2.serversKey];
2611
+ installed = !!servers && typeof servers === "object" && "yungle" in servers;
2612
+ } catch {
2613
+ installed = false;
2614
+ }
2615
+ }
2616
+ rows.push({ client: c2.id, path: c2.configPath, installed });
2617
+ }
2618
+ return out(
2619
+ json,
2620
+ { clients: rows },
2621
+ table(
2622
+ rows.map((r2) => [r2.client, r2.installed ? "installed" : r2.path ? "not installed" : "not available on this platform"]),
2623
+ { flex: 1 }
2624
+ )
2625
+ );
2626
+ }
2627
+ const resolved = await resolveKey();
2628
+ if (!resolved) {
2629
+ throw new CliError(
2630
+ "The local MCP server needs an API key. Or skip keys: add https://yungle.co/mcp to your assistant and sign in there.",
2631
+ "yungle login --key",
2632
+ "not_signed_in"
2633
+ );
2634
+ }
2635
+ if (resolved.source.includes("(yungle login)")) {
2636
+ throw new CliError(
2637
+ "The local MCP server needs an API key; a browser sign-in expires within the hour. Easiest: add https://yungle.co/mcp to your assistant and sign in there. Or save a key for this:",
2638
+ "yungle login --key",
2639
+ "not_signed_in"
2640
+ );
2641
+ }
2642
+ let scopes;
2643
+ try {
2644
+ const me = await client2(flags).me();
2645
+ scopes = me.key.scopes ?? [];
2646
+ } catch (err) {
2647
+ throw new CliError(
2648
+ `Could not check what this key may do (${err.message}), so nothing was installed: an assistant should never get a key of unknown scope.`
2649
+ );
2650
+ }
2651
+ const writes = writeScopes(scopes);
2652
+ const unusable = unusableWriteScopes(scopes);
2653
+ if (unusable.length > 0 || writes.length > 0 && flags["allow-write"] !== true) {
2654
+ throw new CliError(
2655
+ (unusable.length > 0 ? `This key can ${unusable.join(", ")}, which the MCP server never uses.` : `This key can write (${writes.join(", ")}).`) + " Give an assistant as little as it needs: the :read scopes to answer questions, plus transfers:write (with --allow-write) to share links. Or skip keys: add https://yungle.co/mcp to your assistant and sign in.",
2656
+ "yungle open keys",
2657
+ "insufficient_scope"
2658
+ );
2659
+ }
2660
+ const entry = serverEntry({ apiKey: resolved.apiKey, baseUrl: resolved.baseUrl });
2661
+ const dryRun = flags["dry-run"] === true;
2662
+ const results = [];
2663
+ for (const c2 of targets) {
2664
+ const r2 = await installInto(c2, entry, { dryRun, readFile: readFile3, writeFile: writeFile3, mkdir: mkdir4, copyFile, dirname: dirname3 });
2665
+ results.push({ client: c2.id, ...r2 });
2666
+ }
2667
+ const changed = results.filter((r2) => r2.action === "added" || r2.action === "updated");
2668
+ const lines = [table(results.map((r2) => [r2.client, `${r2.action}${r2.detail ? ` \u2014 ${r2.detail}` : ""}`]), { flex: 1 })];
2669
+ if (changed.length > 0 && !dryRun) {
2670
+ lines.push("");
2671
+ lines.push("Restart the client(s) above to pick it up.");
2672
+ lines.push('Then ask: "what did I send recently?" or "did the client download it?"');
2673
+ lines.push("");
2674
+ lines.push(`Your API key is now also in ${changed.length === 1 ? "that config file" : "those config files"}, in plain text.`);
2675
+ }
2676
+ if (changed.length === 0 && !dryRun) {
2677
+ lines.push("");
2678
+ lines.push("Nothing to do. `yungle mcp status` shows where it is installed.");
2679
+ }
2680
+ return out(json, { installed: results, dryRun }, lines.join("\n"));
2681
+ }
2682
+ async function installInto(c2, entry, io) {
2683
+ if (!c2.configPath) return { action: "skipped", detail: "not available on this platform" };
2684
+ if (c2.viaCli) {
2685
+ const { spawnSync } = await import("node:child_process");
2686
+ const probe = spawnSync(c2.viaCli.bin, ["--version"], { stdio: "ignore", shell: false });
2687
+ if (probe.error) return { action: "skipped", detail: `${c2.viaCli.bin} not installed` };
2688
+ if (io.dryRun) {
2689
+ return { action: "would run", detail: `${c2.viaCli.bin} mcp add-json -s user yungle \u2026` };
2690
+ }
2691
+ const existed = spawnSync(c2.viaCli.bin, ["mcp", "get", "yungle"], { stdio: "ignore", shell: false }).status === 0;
2692
+ if (existed) {
2693
+ spawnSync(c2.viaCli.bin, ["mcp", "remove", "-s", "user", "yungle"], {
2694
+ stdio: "ignore",
2695
+ shell: false
2696
+ });
2697
+ }
2698
+ const res = spawnSync(c2.viaCli.bin, c2.viaCli.args("yungle", JSON.stringify(entry)), {
2699
+ stdio: "pipe",
2700
+ shell: false,
2701
+ encoding: "utf8"
2702
+ });
2703
+ if (res.status !== 0) {
2704
+ const why = (res.stderr || res.stdout || "").trim().split("\n")[0] || `exit ${res.status}`;
2705
+ return { action: "failed", detail: why };
2706
+ }
2707
+ return {
2708
+ action: existed ? "updated" : "added",
2709
+ detail: `via ${c2.viaCli.bin} mcp add-json`
2710
+ };
2711
+ }
2712
+ let raw = null;
2713
+ try {
2714
+ raw = await io.readFile(c2.configPath, "utf8");
2715
+ } catch {
2716
+ let dirExists = true;
2717
+ try {
2718
+ await io.readFile(io.dirname(c2.configPath), "utf8");
2719
+ } catch (err) {
2720
+ dirExists = err.code === "EISDIR";
2721
+ }
2722
+ if (!dirExists) return { action: "skipped", detail: "not installed" };
2723
+ }
2724
+ let existing = {};
2725
+ if (raw !== null && raw.trim() !== "") {
2726
+ try {
2727
+ existing = JSON.parse(raw);
2728
+ } catch {
2729
+ return {
2730
+ action: "refused",
2731
+ path: c2.configPath,
2732
+ detail: `${c2.configPath} is not valid JSON \u2014 fix or move it, nothing was written`
2733
+ };
2734
+ }
2735
+ if (existing === null || typeof existing !== "object" || Array.isArray(existing)) {
2736
+ return {
2737
+ action: "refused",
2738
+ path: c2.configPath,
2739
+ detail: `${c2.configPath} is not a JSON object \u2014 nothing was written`
2740
+ };
2741
+ }
2742
+ }
2743
+ const merged = mergeServerEntry(existing, c2.serversKey, "yungle", entry);
2744
+ if (merged.kind === "unchanged") return { action: "already current", path: c2.configPath };
2745
+ if (io.dryRun) {
2746
+ return { action: `would be ${merged.kind}`, path: c2.configPath, detail: c2.configPath };
2747
+ }
2748
+ if (raw !== null) {
2749
+ try {
2750
+ await io.copyFile(c2.configPath, `${c2.configPath}.yungle-bak`);
2751
+ } catch {
2752
+ }
2753
+ }
2754
+ await io.mkdir(io.dirname(c2.configPath), { recursive: true });
2755
+ await io.writeFile(c2.configPath, `${JSON.stringify(merged.config, null, 2)}
2756
+ `, { mode: 384 });
2757
+ return { action: merged.kind, path: c2.configPath, detail: c2.configPath };
2758
+ }
2759
+
2760
+ // src/upload.ts
2761
+ import { createReadStream } from "node:fs";
2762
+ import { stat as stat4 } from "node:fs/promises";
2763
+ var CONCURRENCY = 3;
2764
+ async function uploadAll(files, endpoint, onProgress) {
2765
+ const queue = [...files];
2766
+ const workers = Array.from({ length: Math.min(CONCURRENCY, queue.length) }, async () => {
2767
+ for (; ; ) {
2768
+ const next2 = queue.shift();
2769
+ if (!next2) return;
2770
+ await uploadOne(next2, endpoint, onProgress);
2771
+ }
432
2772
  });
433
2773
  await Promise.all(workers);
434
2774
  await clearUploadUrls(files.map((f) => f.target.id));
435
2775
  }
436
2776
  async function uploadOne(file, endpoint, onProgress) {
437
2777
  const tus = await import("tus-js-client");
438
- const { size } = await stat2(file.path);
2778
+ const { size } = await stat4(file.path);
439
2779
  const urls = await readUploadUrls();
440
2780
  const uploadUrl = urls[file.target.id];
441
2781
  const started = Date.now();
442
- await new Promise((resolve2, reject) => {
2782
+ await new Promise((resolve4, reject) => {
443
2783
  const upload = new tus.Upload(createReadStream(file.path), {
444
2784
  endpoint,
445
2785
  /**
@@ -495,299 +2835,615 @@ async function uploadOne(file, endpoint, onProgress) {
495
2835
  resumed: Boolean(uploadUrl)
496
2836
  });
497
2837
  },
498
- onSuccess: () => resolve2(),
2838
+ onSuccess: () => resolve4(),
499
2839
  onError: (err) => reject(err)
500
2840
  });
501
2841
  upload.start();
502
2842
  });
503
2843
  }
504
2844
 
505
- // src/index.ts
506
- var HELP = `yungle \u2014 send and sync files with Yungle
507
-
508
- yungle auth login save an API key
509
- yungle auth status show which key is in use
510
- yungle send <paths\u2026> send files, print the share link
511
- --to <email> recipient (repeatable, or comma-separated)
512
- --message <text> note for the recipients
513
- --title <text> label for your dashboard
514
- --password <text> recipients must enter this
515
- --expires <days> lifetime, clamped to your plan
516
- yungle push <paths\u2026> --collection <id> upload into a collection
517
- --folder <id> target folder
518
- yungle ls transfers|collections|contacts
519
- yungle rm transfer <id> revoke a transfer
520
-
521
- Global flags
522
- --json machine-readable output
523
- --url <base> override the API base URL
2845
+ // src/watch.ts
2846
+ import { createHash as createHash2 } from "node:crypto";
2847
+ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2848
+ import { dirname as dirname2, join as join5 } from "node:path";
2849
+ function fingerprint(f) {
2850
+ return `${f.path}|${f.size}|${Math.floor(f.mtimeMs)}`;
2851
+ }
2852
+ function planTick(current, lastSeen, uploaded) {
2853
+ const seen = new Set(current.map(fingerprint));
2854
+ const ready = current.filter((f) => {
2855
+ const fp = fingerprint(f);
2856
+ return !uploaded.has(fp) && lastSeen.has(fp);
2857
+ });
2858
+ return { ready, seen };
2859
+ }
2860
+ function statePath(dir, collectionId) {
2861
+ const key = createHash2("sha256").update(`${dir}
2862
+ ${collectionId}`).digest("hex").slice(0, 16);
2863
+ return join5(dirname2(RESUME_PATH), `watch-${key}.json`);
2864
+ }
2865
+ async function loadUploaded(dir, collectionId) {
2866
+ try {
2867
+ return new Set(JSON.parse(await readFile2(statePath(dir, collectionId), "utf8")));
2868
+ } catch {
2869
+ return /* @__PURE__ */ new Set();
2870
+ }
2871
+ }
2872
+ async function saveUploaded(dir, collectionId, uploaded) {
2873
+ const p = statePath(dir, collectionId);
2874
+ await mkdir3(dirname2(p), { recursive: true, mode: 448 });
2875
+ await writeFile2(p, JSON.stringify([...uploaded]), { mode: 384 });
2876
+ }
524
2877
 
525
- Credentials are read from YUNGLE_API_KEY, then ${CONFIG_PATH}.
526
- Docs: https://yungle.co/developers`;
527
- async function main(argv) {
2878
+ // src/index.ts
2879
+ var CLI_VERSION = true ? "0.2.0" : "dev";
2880
+ var USER_AGENT = `yungle-cli/${CLI_VERSION}`;
2881
+ async function main(argvIn) {
2882
+ const argv = resolveAlias(argvIn);
528
2883
  let parsed;
529
2884
  try {
530
- parsed = parseArgs(argv);
2885
+ parsed = parseArgs(argv.length ? argv : ["help"]);
531
2886
  } catch (err) {
532
- process.stderr.write(`${err.message}
533
- `);
534
- return 2;
2887
+ return fail(new CliError(err.message, "yungle help", "usage", 2), false);
535
2888
  }
536
2889
  const { command, positionals, flags } = parsed;
537
2890
  const json = flags.json === true;
538
2891
  try {
2892
+ if (flags.help === true && command !== "help") return help([command], json);
539
2893
  switch (command) {
540
2894
  case "help":
541
2895
  case "--help":
542
2896
  case "-h":
543
- process.stdout.write(`${HELP}
544
- `);
545
- return 0;
546
- case "auth":
547
- return await auth(positionals, flags, json);
2897
+ if (argvIn.length === 0 && interactive(flags)) return await menu(flags);
2898
+ return help(positionals, json);
2899
+ case "version":
2900
+ case "--version":
2901
+ case "-v":
2902
+ return out(json, { version: CLI_VERSION }, CLI_VERSION);
2903
+ case "login":
2904
+ return await login(flags, json);
2905
+ case "logout": {
2906
+ const had = await logout(stringFlag(flags.url));
2907
+ return out(json, { signedOut: had }, had ? success("Signed out", [o.dim("The session is revoked.")]) : " Not signed in with `yungle login`; nothing to do.");
2908
+ }
2909
+ case "whoami":
2910
+ return await whoami(flags, json);
548
2911
  case "send":
549
2912
  return await send(positionals, flags, json);
2913
+ case "get":
2914
+ return await get(positionals, flags, json);
2915
+ case "status":
2916
+ return await status(positionals, flags, json);
2917
+ case "transfers":
2918
+ return await transfers(flags, json);
2919
+ case "collections":
2920
+ return await collections(flags, json);
2921
+ case "contacts":
2922
+ return await contacts(flags, json);
2923
+ case "revoke":
2924
+ return await revoke(positionals, flags, json);
550
2925
  case "push":
551
2926
  return await push(positionals, flags, json);
552
- case "ls":
553
- return await list(positionals, flags, json);
554
- case "rm":
555
- return await remove(positionals, flags, json);
556
- default:
557
- process.stderr.write(`Unknown command: ${command}
558
-
559
- ${HELP}
560
- `);
561
- return 2;
2927
+ case "watch":
2928
+ return await watch(positionals, flags, json);
2929
+ case "open":
2930
+ return await open(positionals, flags, json);
2931
+ case "webhooks":
2932
+ return await webhooks(positionals, flags, json);
2933
+ case "mcp":
2934
+ return await mcp(positionals, flags, json, client);
2935
+ case "completion": {
2936
+ const script = completionScript(positionals[0] ?? "");
2937
+ if (!script) throw new CliError("Which shell?", "yungle completion zsh | bash | fish", "usage", 2);
2938
+ process.stdout.write(script);
2939
+ return 0;
2940
+ }
2941
+ default: {
2942
+ const guess = suggest(command, COMMANDS);
2943
+ throw new CliError(`Unknown command: ${command}.`, guess ? `yungle ${guess} (did you mean this?)` : "yungle help", "usage", 2);
2944
+ }
562
2945
  }
563
2946
  } catch (err) {
2947
+ endProgress();
564
2948
  return fail(err, json);
565
2949
  }
566
2950
  }
567
- async function auth(positionals, flags, json) {
568
- const sub = positionals[0] ?? "status";
569
- if (sub === "login") {
570
- let key = stringFlag(flags.key);
571
- if (!key) {
572
- process.stderr.write(
573
- `Create a key at https://yungle.co/dashboard/settings/api
574
- and paste it here.
575
-
576
- `
577
- );
578
- const rl = createInterface({ input: process.stdin, output: process.stderr });
579
- key = (await rl.question("API key: ")).trim();
580
- rl.close();
2951
+ function help(positionals, json) {
2952
+ const topic = positionals[0];
2953
+ if (topic) {
2954
+ const text2 = commandHelp(topic);
2955
+ if (!text2) {
2956
+ const guess = suggest(topic, COMMANDS);
2957
+ throw new CliError(`No help for "${topic}".`, guess ? `yungle help ${guess}` : "yungle help", "usage", 2);
581
2958
  }
2959
+ return out(json, { command: topic, help: text2 }, renderHelp(text2), text2);
2960
+ }
2961
+ return out(json, { help: HELP }, renderHelp(HELP), HELP);
2962
+ }
2963
+ async function menu(flags) {
2964
+ const signedIn = Boolean(await resolveKey());
2965
+ const choice = await pick("What would you like to do?", [
2966
+ ...signedIn ? [] : [{ value: "login", label: "Sign in", hint: "opens your browser" }],
2967
+ { value: "send", label: "Send files or a folder" },
2968
+ { value: "get", label: "Download a link someone sent you" },
2969
+ ...signedIn ? [{ value: "status", label: "See who downloaded a transfer" }, { value: "open", label: "Open the dashboard" }] : [],
2970
+ { value: "help", label: "Show all commands" }
2971
+ ]);
2972
+ if (choice === "help") return help([], false);
2973
+ return main([choice, ...stringFlag(flags.url) ? ["--url", stringFlag(flags.url)] : []]);
2974
+ }
2975
+ async function login(flags, json) {
2976
+ const url = stringFlag(flags.url);
2977
+ if (flags.key !== void 0) {
2978
+ let key = stringFlag(flags.key);
582
2979
  if (!key) {
583
- process.stderr.write("No key given.\n");
584
- return 2;
2980
+ if (!interactive(flags)) throw new CliError("Give the key: --key yk_live_\u2026", void 0, "usage", 2);
2981
+ key = await askKey();
585
2982
  }
586
- const client2 = new YungleClient({ apiKey: key, baseUrl: stringFlag(flags.url) });
587
- const me = await client2.me();
2983
+ const me2 = await new YungleClient({ apiKey: key, baseUrl: url, userAgent: USER_AGENT }).me();
588
2984
  const config = await readConfig();
589
- await writeConfig({ ...config, apiKey: key, baseUrl: stringFlag(flags.url) ?? config.baseUrl });
590
- return out(
591
- json,
592
- { ok: true, workspace: me.workspace.email, plan: me.plan.name, scopes: me.key.scopes },
593
- `Signed in as ${me.workspace.email ?? me.workspace.id} (${me.plan.name ?? "free"}).
594
- Saved to ${CONFIG_PATH}`
595
- );
596
- }
597
- if (sub === "status") {
598
- const resolved = await resolveKey();
599
- if (!resolved) {
600
- return out(json, { authenticated: false }, "Not signed in. Run `yungle auth login`.", 1);
601
- }
602
- const me = await client(flags).me();
2985
+ await writeConfig({ ...config, apiKey: key, baseUrl: url ?? config.baseUrl });
603
2986
  return out(
604
2987
  json,
605
- { authenticated: true, source: resolved.source, ...me },
606
- [
607
- `Workspace: ${me.workspace.email ?? me.workspace.id}`,
608
- `Plan: ${me.plan.name ?? "free"}`,
609
- `Storage: ${formatBytes(me.plan.usedBytes)} of ${formatBytes(me.plan.quotaBytes)}`,
610
- `Key: ${me.key.name} (${me.key.scopes.join(", ")})`,
611
- `Source: ${resolved.source}`
612
- ].join("\n")
2988
+ { ok: true, workspace: me2.workspace.email, plan: me2.plan.name, scopes: me2.key.scopes },
2989
+ success(`Signed in as ${me2.workspace.email ?? me2.workspace.id}`, [o.dim(`with key ${me2.key.name} \xB7 saved to ${CONFIG_PATH}`)])
613
2990
  );
614
2991
  }
615
- process.stderr.write("Usage: yungle auth login | yungle auth status\n");
616
- return 2;
2992
+ const scope = await deviceLogin(url, { openBrowser: flags["no-browser"] !== true });
2993
+ await loadCredentials();
2994
+ const me = await client(flags).me().catch(() => null);
2995
+ return out(
2996
+ json,
2997
+ { signedIn: true, scope, workspace: me?.workspace.email ?? null },
2998
+ [
2999
+ success(`Signed in${me?.workspace.email ? ` as ${me.workspace.email}` : ""}`, [o.dim(scope.split(" ").join(", "))]),
3000
+ "",
3001
+ next("yungle send <folder>", "send something")
3002
+ ].join("\n")
3003
+ );
3004
+ }
3005
+ async function whoami(flags, json) {
3006
+ const resolved = await resolveKey();
3007
+ if (!resolved) throw new NotSignedIn();
3008
+ const me = await client(flags).me();
3009
+ const via = resolved.source.includes("(yungle login)") ? "browser sign-in" : resolved.source === "YUNGLE_API_KEY" ? "YUNGLE_API_KEY" : `key in ${CONFIG_PATH}`;
3010
+ return out(
3011
+ json,
3012
+ { authenticated: true, source: resolved.source, ...me },
3013
+ [
3014
+ ` ${o.bold(me.workspace.email ?? me.workspace.id)} ${o.dim(me.plan.name ?? "Free plan")}`,
3015
+ ` ${o.dim("Storage")} ${formatBytes(me.plan.usedBytes)} of ${formatBytes(me.plan.quotaBytes)}`,
3016
+ ` ${o.dim("Signed in")} ${via} ${o.dim(`(${me.key.name})`)}`,
3017
+ ` ${o.dim("May")} ${me.key.scopes.join(", ")}`,
3018
+ ...me.key.canEmail === false ? [` ${o.dim("Email")} ${o.dim("no, links only")} ${o.dim("(yungle login --key to email recipients)")}`] : []
3019
+ ].join("\n"),
3020
+ me.workspace.email ?? me.workspace.id
3021
+ );
617
3022
  }
618
- async function send(paths, flags, json) {
3023
+ async function send(pathsIn, flags, json) {
3024
+ let paths = pathsIn;
3025
+ let recipients = listFlag(flags.to);
3026
+ let message = stringFlag(flags.message);
3027
+ let askedFor = false;
619
3028
  if (paths.length === 0) {
620
- process.stderr.write("Nothing to send. Give me one or more files or folders.\n");
621
- return 2;
3029
+ if (!interactive(flags)) throw new CliError("Nothing to send.", "yungle send <file or folder> [--to someone@example.com]", "usage", 2);
3030
+ const answers = await askSend();
3031
+ paths = answers.paths;
3032
+ recipients = answers.to;
3033
+ message = answers.message;
3034
+ askedFor = true;
622
3035
  }
623
3036
  const api = client(flags);
624
- const files = await collectFiles(paths);
625
- if (files.length === 0) {
626
- process.stderr.write("Those paths contained no files.\n");
627
- return 2;
3037
+ if (recipients.length > 0 && (await api.me()).key.canEmail === false) {
3038
+ const hint = "yungle login --key (an API key can email; or leave out --to)";
3039
+ if (!interactive(flags)) throw new CliError("A browser sign-in can make links but cannot email people.", hint, "insufficient_scope");
3040
+ note("A browser sign-in can make links but cannot email people.");
3041
+ if (!await confirm2("Send it as a link you share yourself?")) throw new CliError("Cancelled; nothing was sent.", hint, "cancelled", 130);
3042
+ recipients = [];
628
3043
  }
629
- const total = files.reduce((n, f) => n + f.size, 0);
630
- progress(`Sending ${files.length} file${files.length === 1 ? "" : "s"} \xB7 ${formatBytes(total)}
631
- `);
3044
+ const files = await collectFiles(paths);
3045
+ if (files.length === 0) throw new CliError("Those paths contain no files (hidden files are skipped).", void 0, "usage", 2);
3046
+ const total = files.reduce((n3, f) => n3 + f.size, 0);
3047
+ const summary = `${plural(files.length, "file")} ${sym.dot} ${formatBytes(total)}${recipients.length ? ` to ${recipients.join(", ")}` : ""}`;
3048
+ if (askedFor && !await confirm2(`Send ${summary}?`)) throw new CliError("Cancelled; nothing was sent.", void 0, "cancelled", 130);
3049
+ heading(`Sending ${summary}`);
632
3050
  const key = sessionKey(files, "transfer");
633
3051
  const existing = await findSession(key);
634
3052
  let created;
635
3053
  if (existing && existing.kind === "transfer") {
636
- progress(" resuming the interrupted upload\n");
637
- created = {
638
- transfer: { id: existing.targetId },
639
- tusEndpoint: existing.tusEndpoint,
640
- files: existing.files
641
- };
3054
+ note("Resuming the interrupted upload.");
3055
+ created = { transfer: { id: existing.targetId }, tusEndpoint: existing.tusEndpoint, files: existing.files };
642
3056
  } else {
643
3057
  const fresh = await api.createTransfer({
644
3058
  files: toFileInputs(files),
645
3059
  title: stringFlag(flags.title),
646
3060
  expiresInDays: numberFlag(flags.expires, "expires")
647
3061
  });
648
- created = { transfer: fresh.transfer, tusEndpoint: fresh.tusEndpoint, files: fresh.files.map((f, i) => ({ ...f, path: files[i].path })) };
649
- await saveSession(key, {
650
- createdAt: Date.now(),
651
- kind: "transfer",
652
- targetId: fresh.transfer.id,
653
- tusEndpoint: fresh.tusEndpoint,
654
- files: created.files
655
- });
3062
+ created = { transfer: fresh.transfer, tusEndpoint: fresh.tusEndpoint, files: fresh.files.map((f, i2) => ({ ...f, path: files[i2].path })) };
3063
+ await saveSession(key, { createdAt: Date.now(), kind: "transfer", targetId: fresh.transfer.id, tusEndpoint: fresh.tusEndpoint, files: created.files });
656
3064
  }
657
- await streamAll(files, created, api);
3065
+ await streamAll(files, created);
3066
+ const after = await freshClient(flags, api);
658
3067
  await dropSession(key);
659
- const recipients = listFlag(flags.to);
660
- const finalized = await api.finalizeTransfer(created.transfer.id, {
3068
+ const finalized = await after.finalizeTransfer(created.transfer.id, {
661
3069
  ...recipients.length ? { recipients } : {},
662
- ...stringFlag(flags.message) ? { message: stringFlag(flags.message) } : {},
3070
+ ...message ? { message } : {},
663
3071
  ...stringFlag(flags.password) ? { password: stringFlag(flags.password) } : {},
664
3072
  ...stringFlag(flags.title) ? { title: stringFlag(flags.title) } : {}
665
3073
  });
3074
+ const t2 = finalized.transfer;
666
3075
  return out(
667
3076
  json,
668
- {
669
- id: finalized.transfer.id,
670
- url: finalized.transfer.url,
671
- expiresAt: finalized.transfer.expiresAt,
672
- notified: finalized.notified
673
- },
3077
+ { id: t2.id, url: t2.url, expiresAt: t2.expiresAt, notified: finalized.notified },
674
3078
  [
675
- finalized.transfer.url,
676
- finalized.notified.length ? `Emailed ${finalized.notified.join(", ")}` : "Link only \u2014 nobody was emailed."
677
- ].join("\n")
3079
+ success("Sent", [
3080
+ accentOut(t2.url),
3081
+ o.dim(
3082
+ `${finalized.notified.length ? `Emailed ${finalized.notified.join(", ")}` : "Link only; nobody was emailed"} ${sym.dot} expires ${formatDate(t2.expiresAt)}`
3083
+ )
3084
+ ]),
3085
+ "",
3086
+ next(`yungle status ${t2.id.slice(-8)}`, "see who downloaded")
3087
+ ].join("\n"),
3088
+ t2.url
678
3089
  );
679
3090
  }
680
- async function push(paths, flags, json) {
681
- const collectionId = stringFlag(flags.collection);
682
- if (!collectionId) {
683
- process.stderr.write("Which collection? Pass --collection <id>.\n");
684
- return 2;
3091
+ async function streamAll(files, targets) {
3092
+ const paired = pairWithTargets(files, targets.files);
3093
+ const totalBytes = paired.reduce((n3, { file }) => n3 + file.size, 0);
3094
+ const sentPer = /* @__PURE__ */ new Map();
3095
+ const finished = /* @__PURE__ */ new Set();
3096
+ const started = Date.now();
3097
+ let resumed = false;
3098
+ const render = () => {
3099
+ let sent = 0;
3100
+ for (const v of sentPer.values()) sent += v;
3101
+ drawProgress(progressLine(sent, totalBytes, started, `${finished.size}/${paired.length} files${resumed ? " (resumed)" : ""}`));
3102
+ };
3103
+ const timer = setInterval(render, 250);
3104
+ try {
3105
+ await uploadAll(
3106
+ paired.map(({ file, target }) => ({ path: file.path, target })),
3107
+ targets.tusEndpoint,
3108
+ (p) => {
3109
+ if (p.resumed) resumed = true;
3110
+ sentPer.set(p.id, p.sent);
3111
+ if (p.sent >= p.total) finished.add(p.id);
3112
+ }
3113
+ );
3114
+ } finally {
3115
+ clearInterval(timer);
3116
+ endProgress();
685
3117
  }
686
- if (paths.length === 0) {
687
- process.stderr.write("Nothing to push.\n");
688
- return 2;
3118
+ }
3119
+ async function get(positionals, flags, json) {
3120
+ let input = positionals[0];
3121
+ if (!input) {
3122
+ if (!interactive(flags)) throw new CliError("Which link?", "yungle get https://yungle.co/t/\u2026", "usage", 2);
3123
+ input = await askLink();
3124
+ }
3125
+ const apiBase = stringFlag(flags.url) ?? cachedBaseUrl ?? "https://yungle.co/api/v1";
3126
+ const link = parseTransferLink(input, new URL(apiBase).origin);
3127
+ const outDir = resolve3(stringFlag(flags.out) ?? ".");
3128
+ let password2 = stringFlag(flags.password);
3129
+ let manifest;
3130
+ for (; ; ) {
3131
+ try {
3132
+ manifest = await fetchManifest(link, password2, USER_AGENT);
3133
+ break;
3134
+ } catch (err) {
3135
+ const needs = err instanceof ManifestError && (err.code === "password_required" || err.code === "wrong_password");
3136
+ if (!needs || !interactive(flags)) {
3137
+ if (err instanceof ManifestError && err.code === "password_required") {
3138
+ throw new CliError("This transfer is password protected.", `yungle get ${input} --password <password>`, "password_required");
3139
+ }
3140
+ throw err;
3141
+ }
3142
+ if (err.code === "wrong_password") note("That password is not right.");
3143
+ password2 = await askPassword();
3144
+ }
3145
+ }
3146
+ if (manifest.e2ee) {
3147
+ throw new CliError(
3148
+ "This transfer is end-to-end encrypted: only a browser holding the key in the link can open it.",
3149
+ "Open the link in a browser.",
3150
+ "e2ee"
3151
+ );
3152
+ }
3153
+ if (manifest.files.length === 0) return out(json, { saved: [] }, " This transfer has no files.");
3154
+ const useZip = flags.zip === true && manifest.zipUrl !== null;
3155
+ const jobs = useZip ? [{ url: manifest.zipUrl, name: `yungle-${link.slug}.zip`, size: null }] : dedupeNames(manifest.files.map((f) => safeFileName(f.name))).map((name, i2) => ({ url: manifest.files[i2].downloadUrl, name, size: manifest.files[i2].size }));
3156
+ const total = manifest.files.reduce((n3, f) => n3 + f.size, 0);
3157
+ heading(`Downloading ${plural(manifest.files.length, "file")} ${sym.dot} ${formatBytes(total)}`);
3158
+ let received = 0;
3159
+ const started = Date.now();
3160
+ const saved = [];
3161
+ for (const [i2, job] of jobs.entries()) {
3162
+ const dest = join6(outDir, job.name);
3163
+ await downloadTo(job.url, dest, job.size, USER_AGENT, (n3) => {
3164
+ received += n3;
3165
+ drawProgress(progressLine(Math.min(received, total), total, started, `${i2 + 1}/${jobs.length} files`));
3166
+ });
3167
+ saved.push(dest);
3168
+ }
3169
+ endProgress();
3170
+ return out(
3171
+ json,
3172
+ { saved, message: manifest.message, expiresAt: manifest.expiresAt },
3173
+ success(`Saved ${saved.length === 1 ? shown(saved[0]) : `${plural(saved.length, "file")} to ${shown(outDir)}`}`, manifest.message ? [o.italic(`\u201C${manifest.message}\u201D`)] : []),
3174
+ saved.join("\n")
3175
+ );
3176
+ }
3177
+ function shown(path) {
3178
+ const rel = relative2(process.cwd(), path);
3179
+ return !rel ? "." : rel.startsWith("..") || isAbsolute(rel) ? path : `./${rel}`;
3180
+ }
3181
+ async function findTransferId(api, ref) {
3182
+ if (/^[0-9A-HJKMNP-TV-Z]{26}$/.test(ref)) return ref;
3183
+ const slug = ref.match(/\/t\/([^/?#]+)/)?.[1] ?? ref;
3184
+ const short = /^[0-9A-HJKMNP-TV-Z]{6,25}$/i.test(ref) ? ref.toUpperCase() : null;
3185
+ for await (const t2 of api.allTransfers()) {
3186
+ if (t2.slug === slug || short && t2.id.endsWith(short)) return t2.id;
689
3187
  }
3188
+ throw new CliError("None of your transfers matches that.", "yungle transfers", "not_found");
3189
+ }
3190
+ async function status(positionals, flags, json) {
690
3191
  const api = client(flags);
691
- const files = await collectFiles(paths);
692
- if (files.length === 0) {
693
- process.stderr.write("Those paths contained no files.\n");
694
- return 2;
3192
+ let ref = positionals[0];
3193
+ if (!ref) {
3194
+ if (!interactive(flags)) throw new CliError("Which transfer?", "yungle status <id or link>", "usage", 2);
3195
+ const { transfers: transfers2 } = await api.listTransfers({ limit: 15 });
3196
+ if (transfers2.length === 0) throw new CliError("You have not sent anything yet.", "yungle send <folder>");
3197
+ ref = await pick(
3198
+ "Which transfer?",
3199
+ transfers2.map((t3) => ({ value: t3.id, label: label(t3), hint: `${formatDate(t3.createdAt)} ${sym.dot} ${t3.downloadCount} downloads` }))
3200
+ );
695
3201
  }
696
- const total = files.reduce((n, f) => n + f.size, 0);
697
- progress(`Uploading ${files.length} file${files.length === 1 ? "" : "s"} \xB7 ${formatBytes(total)}
698
- `);
3202
+ const id = await findTransferId(api, ref);
3203
+ const [{ transfer: t2, files, recipients }, receipts] = await Promise.all([api.getTransfer(id), api.transferDownloads(id)]);
3204
+ const live = t2.status === "active" && !(t2.expiresAt && new Date(t2.expiresAt).getTime() < Date.now());
3205
+ const state = live ? accentOut(`${sym.ok} live ${sym.dot} expires ${formatUntil(t2.expiresAt)}`) : o.dim(`${t2.status === "active" ? "expired" : t2.status}`);
3206
+ const lines = [
3207
+ ` ${o.bold(t2.title ?? (files[0] ? `${files[0].name}${files.length > 1 ? ` + ${files.length - 1} more` : ""}` : "Untitled transfer"))} ${state}`,
3208
+ ` ${accentOut(t2.url)}`,
3209
+ ` ${o.dim(`${plural(files.length, "file")} ${sym.dot} ${formatBytes(t2.sizeBytes)} ${sym.dot} ${receipts.totalDownloads ? `downloaded ${plural(receipts.totalDownloads, "time")}` : "not downloaded yet"}`)}`
3210
+ ];
3211
+ if (recipients.length) {
3212
+ lines.push("", table(
3213
+ recipients.map((r2) => [
3214
+ r2.email,
3215
+ r2.bouncedAt ? o.red("email bounced") : r2.downloaded ? accentOut("downloaded") : r2.opened ? "opened" : r2.notifiedAt ? o.dim("emailed") : o.dim("email queued")
3216
+ ]),
3217
+ { flex: 0, header: ["Recipient", "Status"] }
3218
+ ));
3219
+ }
3220
+ const last = receipts.downloads[0];
3221
+ if (last) lines.push("", ` ${o.dim(`Last download ${formatDate(last.createdAt)}${last.recipientEmail ? ` by ${last.recipientEmail}` : ""}`)}`);
3222
+ return out(json, { transfer: t2, files, recipients, downloads: receipts }, lines.join("\n"), `${t2.url} ${receipts.totalDownloads}`);
3223
+ }
3224
+ function label(t2) {
3225
+ return t2.title ?? t2.firstFileName ?? "Untitled";
3226
+ }
3227
+ async function transfers(flags, json) {
3228
+ const { transfers: transfers2 } = await client(flags).listTransfers({ limit: 25 });
3229
+ if (transfers2.length === 0) return out(json, { transfers: transfers2 }, ` No transfers yet.
3230
+
3231
+ ${next("yungle send <folder>")}`, "");
3232
+ return out(
3233
+ json,
3234
+ { transfers: transfers2 },
3235
+ table(
3236
+ transfers2.map((t2) => [
3237
+ o.dim(t2.id.slice(-8)),
3238
+ label(t2),
3239
+ formatBytes(t2.sizeBytes),
3240
+ t2.downloadCount ? accentOut(`${t2.downloadCount} dl`) : o.dim("0 dl"),
3241
+ t2.status === "active" ? formatUntil(t2.expiresAt) : o.dim(t2.status)
3242
+ ]),
3243
+ // No link column: a link cut to fit cannot be copied. `status` shows it.
3244
+ { flex: 1, header: ["ID", "Title", "Size", "Downloads", "Expires"] }
3245
+ ) + `
3246
+
3247
+ ${next("yungle status <id>", "who downloaded")}`,
3248
+ transfers2.map((t2) => `${t2.id} ${t2.url}`).join("\n")
3249
+ );
3250
+ }
3251
+ async function collections(flags, json) {
3252
+ const { collections: collections2 } = await client(flags).listCollections();
3253
+ if (collections2.length === 0) return out(json, { collections: collections2 }, " No collections yet.", "");
3254
+ return out(
3255
+ json,
3256
+ { collections: collections2 },
3257
+ table(
3258
+ collections2.map((c2) => [o.dim(c2.id), c2.title, plural(c2.fileCount, "file"), formatBytes(c2.sizeBytes)]),
3259
+ { flex: 1, header: ["ID", "Title", "Files", "Size"] }
3260
+ ),
3261
+ collections2.map((c2) => `${c2.id} ${c2.title}`).join("\n")
3262
+ );
3263
+ }
3264
+ async function contacts(flags, json) {
3265
+ const { contacts: contacts2 } = await client(flags).listContacts();
3266
+ if (contacts2.length === 0) return out(json, { contacts: contacts2 }, " No contacts yet.", "");
3267
+ return out(
3268
+ json,
3269
+ { contacts: contacts2 },
3270
+ table(
3271
+ contacts2.map((c2) => [[c2.firstName, c2.lastName].filter(Boolean).join(" ") || o.dim("\u2014"), c2.email, o.dim(c2.company ?? "")]),
3272
+ { flex: 0, header: ["Name", "Email", "Company"] }
3273
+ ),
3274
+ contacts2.map((c2) => c2.email).join("\n")
3275
+ );
3276
+ }
3277
+ async function revoke(positionals, flags, json) {
3278
+ const ref = positionals[0];
3279
+ if (!ref) throw new CliError("Which transfer?", "yungle revoke <id or link>", "usage", 2);
3280
+ const api = client(flags);
3281
+ const id = await findTransferId(api, ref);
3282
+ if (interactive(flags) && !await confirm2("Revoke it? The link stops working and the files are destroyed. This cannot be undone.")) {
3283
+ throw new CliError("Cancelled; nothing changed.", void 0, "cancelled", 130);
3284
+ }
3285
+ const { transfer } = await api.revokeTransfer(id);
3286
+ return out(json, { id: transfer.id, status: transfer.status }, success("Revoked", [o.dim("The link no longer works and the files are gone.")]), transfer.id);
3287
+ }
3288
+ async function push(paths, flags, json) {
3289
+ const collectionId = stringFlag(flags.collection);
3290
+ if (!collectionId) throw new CliError("Which collection?", "yungle collections then yungle push <paths> --collection <id>", "usage", 2);
3291
+ if (paths.length === 0) throw new CliError("Nothing to upload.", `yungle push <paths> --collection ${collectionId}`, "usage", 2);
3292
+ const api = client(flags);
3293
+ const files = await collectFiles(paths);
3294
+ if (files.length === 0) throw new CliError("Those paths contain no files (hidden files are skipped).", void 0, "usage", 2);
3295
+ heading(`Uploading ${plural(files.length, "file")} ${sym.dot} ${formatBytes(files.reduce((n3, f) => n3 + f.size, 0))}`);
699
3296
  const key = sessionKey(files, `collection:${collectionId}:${stringFlag(flags.folder) ?? ""}`);
700
3297
  const existing = await findSession(key);
701
3298
  let targets;
702
3299
  if (existing && existing.kind === "collection") {
703
- progress(" resuming the interrupted upload\n");
3300
+ note("Resuming the interrupted upload.");
704
3301
  targets = { tusEndpoint: existing.tusEndpoint, files: existing.files };
705
3302
  } else {
706
- const fresh = await api.addCollectionFiles(
707
- collectionId,
708
- toFileInputs(files),
709
- stringFlag(flags.folder) ?? null
710
- );
711
- targets = {
712
- tusEndpoint: fresh.tusEndpoint,
713
- files: fresh.files.map((f, i) => ({ ...f, path: files[i].path }))
714
- };
715
- await saveSession(key, {
716
- createdAt: Date.now(),
717
- kind: "collection",
718
- targetId: collectionId,
719
- tusEndpoint: fresh.tusEndpoint,
720
- files: targets.files
721
- });
3303
+ const fresh = await api.addCollectionFiles(collectionId, toFileInputs(files), stringFlag(flags.folder) ?? null);
3304
+ targets = { tusEndpoint: fresh.tusEndpoint, files: fresh.files.map((f, i2) => ({ ...f, path: files[i2].path })) };
3305
+ await saveSession(key, { createdAt: Date.now(), kind: "collection", targetId: collectionId, tusEndpoint: fresh.tusEndpoint, files: targets.files });
722
3306
  }
723
- await streamAll(files, targets, api);
3307
+ await streamAll(files, targets);
724
3308
  await dropSession(key);
725
- const collection = await api.getCollection(collectionId);
3309
+ const { collection } = await (await freshClient(flags, api)).getCollection(collectionId);
726
3310
  return out(
727
3311
  json,
728
- { collection: collection.collection.id, uploaded: files.length, url: collection.collection.url },
729
- `Uploaded ${files.length} file${files.length === 1 ? "" : "s"} to \u201C${collection.collection.title}\u201D.
730
- ${collection.collection.url ?? ""}`
3312
+ { collection: collection.id, uploaded: files.length, url: collection.url },
3313
+ success(`Uploaded ${plural(files.length, "file")} to \u201C${collection.title}\u201D`, collection.url ? [accentOut(collection.url)] : []),
3314
+ collection.url ?? collection.id
731
3315
  );
732
3316
  }
733
- async function list(positionals, flags, json) {
734
- const what = positionals[0] ?? "transfers";
3317
+ async function watch(positionals, flags, json) {
3318
+ const dir = positionals[0] ? resolve3(positionals[0]) : null;
3319
+ const collectionId = stringFlag(flags.collection);
3320
+ if (!dir || !collectionId) throw new CliError("Which folder, into which collection?", "yungle watch <folder> --collection <id>", "usage", 2);
3321
+ const interval = (numberFlag(flags.interval, "interval") ?? 10) * 1e3;
735
3322
  const api = client(flags);
736
- if (what === "transfers") {
737
- const { transfers } = await api.listTransfers();
738
- return out(
739
- json,
740
- { transfers },
741
- transfers.length === 0 ? "No transfers yet." : transfers.map(
742
- (t) => `${t.id} ${pad(t.title ?? t.firstFileName ?? "(untitled)", 32)} ${pad(formatBytes(t.sizeBytes), 9)} ${t.downloadCount} dl ${t.url}`
743
- ).join("\n")
744
- );
3323
+ const { collection } = await api.getCollection(collectionId);
3324
+ const uploaded = await loadUploaded(dir, collectionId);
3325
+ let lastSeen = /* @__PURE__ */ new Set();
3326
+ if (flags.existing !== true) {
3327
+ for (const f of await collectFiles([dir])) uploaded.add(fingerprint(f));
3328
+ await saveUploaded(dir, collectionId, uploaded);
745
3329
  }
746
- if (what === "collections") {
747
- const { collections } = await api.listCollections();
748
- return out(
749
- json,
750
- { collections },
751
- collections.length === 0 ? "No collections yet." : collections.map(
752
- (c) => `${c.id} ${pad(c.title, 32)} ${pad(`${c.fileCount} files`, 12)} ${formatBytes(c.sizeBytes)}`
753
- ).join("\n")
754
- );
3330
+ if (!json) heading(`Watching ${dir} ${sym.arrow} \u201C${collection.title}\u201D. New files go up once they stop changing. Ctrl-C to stop.`);
3331
+ let stop = false;
3332
+ process.once("SIGINT", () => {
3333
+ stop = true;
3334
+ });
3335
+ while (!stop) {
3336
+ const current = await collectFiles([dir]);
3337
+ const { ready, seen } = planTick(current, lastSeen, uploaded);
3338
+ lastSeen = seen;
3339
+ if (ready.length > 0) {
3340
+ const target = await (await freshClient(flags, api)).addCollectionFiles(collectionId, toFileInputs(ready), stringFlag(flags.folder) ?? null);
3341
+ await streamAll(ready, target);
3342
+ for (const f of ready) {
3343
+ uploaded.add(fingerprint(f));
3344
+ if (json) process.stdout.write(`${JSON.stringify({ uploaded: f.path, size: f.size })}
3345
+ `);
3346
+ else process.stdout.write(` ${accentOut(sym.ok)} ${join6(f.relativeDir, f.name)} ${o.dim(formatBytes(f.size))}
3347
+ `);
3348
+ }
3349
+ await saveUploaded(dir, collectionId, uploaded);
3350
+ }
3351
+ for (let waited = 0; waited < interval && !stop; waited += 250) await new Promise((r2) => setTimeout(r2, 250));
3352
+ }
3353
+ if (!json) process.stderr.write(`
3354
+ ${e.dim("Stopped.")}
3355
+ `);
3356
+ return 0;
3357
+ }
3358
+ var PLACES = {
3359
+ plan: "/dashboard/settings/plan",
3360
+ keys: "/dashboard/settings/api",
3361
+ webhooks: "/dashboard/settings/webhooks",
3362
+ usage: "/dashboard/settings/usage",
3363
+ developers: "/dashboard/developers"
3364
+ };
3365
+ async function open(positionals, flags, json) {
3366
+ const origin = originOf(stringFlag(flags.url) ?? cachedBaseUrl);
3367
+ const what = positionals[0];
3368
+ let path = "/dashboard";
3369
+ if (what && PLACES[what]) path = PLACES[what];
3370
+ else if (what) {
3371
+ const api = client(flags);
3372
+ const id = await findTransferId(api, what);
3373
+ const { transfer } = await api.getTransfer(id);
3374
+ path = `/dashboard/transfers/${transfer.slug}`;
755
3375
  }
756
- if (what === "contacts") {
757
- const { contacts } = await api.listContacts();
3376
+ const url = `${origin}${path}`;
3377
+ if (!json) openBrowser(url);
3378
+ return out(json, { url }, ` ${o.dim("Opened")} ${accentOut(url)}`, url);
3379
+ }
3380
+ async function webhooks(positionals, flags, json) {
3381
+ const api = client(flags);
3382
+ const sub = positionals[0] ?? "ls";
3383
+ if (sub === "ls") {
3384
+ const { webhooks: webhooks2 } = await api.listWebhooks();
3385
+ if (webhooks2.length === 0) return out(json, { webhooks: webhooks2 }, ` No webhook endpoints.
3386
+
3387
+ ${next("yungle open webhooks")}`, "");
758
3388
  return out(
759
3389
  json,
760
- { contacts },
761
- contacts.length === 0 ? "No contacts yet." : contacts.map((c) => `${c.id} ${pad([c.firstName, c.lastName].filter(Boolean).join(" ") || "\u2014", 24)} ${c.email}`).join("\n")
3390
+ { webhooks: webhooks2 },
3391
+ table(
3392
+ webhooks2.map((w) => [o.dim(w.id.slice(-8)), w.enabled ? accentOut("active") : o.dim("paused"), w.url ?? o.dim("(pull)"), o.dim(w.events.join(", "))]),
3393
+ { flex: 2, header: ["ID", "State", "URL", "Events"] }
3394
+ ),
3395
+ webhooks2.map((w) => `${w.id} ${w.url ?? ""}`).join("\n")
762
3396
  );
763
3397
  }
764
- process.stderr.write("Usage: yungle ls transfers|collections|contacts\n");
765
- return 2;
766
- }
767
- async function remove(positionals, flags, json) {
768
- const [what, id] = positionals;
769
- if (what !== "transfer" || !id) {
770
- process.stderr.write("Usage: yungle rm transfer <id>\n");
771
- return 2;
3398
+ if (sub !== "listen") throw new CliError(`Unknown: yungle webhooks ${sub}.`, "yungle webhooks ls or yungle webhooks listen", "usage", 2);
3399
+ const forwardTo = stringFlag(flags["forward-to"]);
3400
+ const { id, secret } = await ensureListenEndpoint(api);
3401
+ let cursor3 = await cursorAtNow(api, id);
3402
+ if (!json) {
3403
+ heading(`Listening for events${forwardTo ? ` ${sym.arrow} ${forwardTo}` : ""}. Ctrl-C to stop.`);
3404
+ if (forwardTo) note(`Signing secret for this session: ${secret}`);
772
3405
  }
773
- const { transfer } = await client(flags).revokeTransfer(id);
774
- return out(
775
- json,
776
- { id: transfer.id, status: transfer.status },
777
- `Revoked. The link no longer works and the content is unrecoverable.`
778
- );
3406
+ for (; ; ) {
3407
+ const page = await api.listWebhookEvents(id, { limit: 100, cursor: cursor3 });
3408
+ for (const { deliveryId, ...event } of page.events) {
3409
+ const body = JSON.stringify(event);
3410
+ if (json) process.stdout.write(`${body}
3411
+ `);
3412
+ else process.stdout.write(` ${o.dim(event.createdAt.slice(11, 19))} ${accentOut(event.type.padEnd(26))}${describeEvent(event)}
3413
+ `);
3414
+ if (forwardTo) {
3415
+ const res = await fetch(forwardTo, {
3416
+ method: "POST",
3417
+ headers: {
3418
+ "Content-Type": "application/json",
3419
+ "User-Agent": USER_AGENT,
3420
+ "Yungle-Event-Id": event.id,
3421
+ "Yungle-Event-Type": event.type,
3422
+ "Yungle-Delivery-Id": deliveryId,
3423
+ "Yungle-Signature": signForForward(secret, body)
3424
+ },
3425
+ body
3426
+ }).catch((err) => ({ ok: false, status: err.message }));
3427
+ if (!json) process.stderr.write(` ${e.dim(`${sym.arrow} ${forwardTo} ${"status" in res ? res.status : ""}`)}
3428
+ `);
3429
+ }
3430
+ }
3431
+ cursor3 = page.nextCursor ?? cursor3;
3432
+ if (!page.hasMore) await new Promise((r2) => setTimeout(r2, 2e3));
3433
+ }
3434
+ }
3435
+ async function freshClient(flags, current) {
3436
+ if (process.env.YUNGLE_API_KEY?.trim()) return current;
3437
+ const resolved = await resolveKey();
3438
+ if (!resolved || resolved.apiKey === cachedKey) return current;
3439
+ cachedKey = resolved.apiKey;
3440
+ return client(flags);
779
3441
  }
780
3442
  function client(flags) {
781
- const apiKey = process.env.YUNGLE_API_KEY?.trim() ?? cachedKey;
3443
+ const apiKey = process.env.YUNGLE_API_KEY?.trim() || cachedKey;
782
3444
  if (!apiKey) throw new NotSignedIn();
783
- return new YungleClient({ apiKey, baseUrl: stringFlag(flags.url) ?? cachedBaseUrl });
3445
+ return new YungleClient({ apiKey, baseUrl: stringFlag(flags.url) ?? cachedBaseUrl, userAgent: USER_AGENT });
784
3446
  }
785
- var NotSignedIn = class extends Error {
786
- constructor() {
787
- super("Not signed in. Run `yungle auth login`, or set YUNGLE_API_KEY.");
788
- this.name = "NotSignedIn";
789
- }
790
- };
791
3447
  var cachedKey;
792
3448
  var cachedBaseUrl;
793
3449
  async function loadCredentials() {
@@ -795,69 +3451,9 @@ async function loadCredentials() {
795
3451
  cachedKey = resolved?.apiKey;
796
3452
  cachedBaseUrl = resolved?.baseUrl;
797
3453
  }
798
- async function streamAll(files, targets, _api) {
799
- const paired = pairWithTargets(files, targets.files);
800
- const totalBytes = paired.reduce((n, { file }) => n + file.size, 0);
801
- const sentPer = /* @__PURE__ */ new Map();
802
- const finished = /* @__PURE__ */ new Set();
803
- const started = Date.now();
804
- let anyResumed = false;
805
- const render = () => {
806
- let sent = 0;
807
- for (const value of sentPer.values()) sent += value;
808
- const pct = totalBytes ? Math.floor(sent / totalBytes * 100) : 100;
809
- const rate = sent / Math.max(1, (Date.now() - started) / 1e3);
810
- progress(
811
- `\r ${String(pct).padStart(3)}% ${pad(`${formatBytes(sent)} of ${formatBytes(totalBytes)}`, 22)}${pad(`${formatBytes(rate)}/s`, 12)}${finished.size}/${paired.length} files${anyResumed ? " (resumed)" : ""} `
812
- );
813
- };
814
- await uploadAll(
815
- paired.map(({ file, target }) => ({ path: file.path, target })),
816
- targets.tusEndpoint,
817
- (p) => {
818
- if (p.resumed) anyResumed = true;
819
- sentPer.set(p.id, p.sent);
820
- if (p.sent >= p.total) finished.add(p.id);
821
- render();
822
- }
823
- );
824
- render();
825
- progress("\n");
826
- }
827
- function progress(text) {
828
- if (process.stderr.isTTY) process.stderr.write(text);
829
- }
830
- function pad(text, width) {
831
- return text.length > width ? `${text.slice(0, width - 1)}\u2026` : text.padEnd(width);
832
- }
833
- function out(json, data, human, code = 0) {
834
- process.stdout.write(json ? `${JSON.stringify(data, null, 2)}
835
- ` : `${human}
836
- `);
837
- return code;
838
- }
839
- function fail(err, json) {
840
- if (err instanceof YungleApiError) {
841
- if (json) {
842
- process.stdout.write(`${JSON.stringify({ error: { code: err.code, message: err.message, details: err.details } }, null, 2)}
843
- `);
844
- } else {
845
- process.stderr.write(`${err.message}
846
- `);
847
- if (err.code === "upgrade_required") {
848
- process.stderr.write("See https://yungle.co/dashboard/settings/plan\n");
849
- }
850
- }
851
- return 1;
852
- }
853
- const message = err instanceof Error ? err.message : String(err);
854
- if (json) process.stdout.write(`${JSON.stringify({ error: { code: "cli_error", message } }, null, 2)}
855
- `);
856
- else process.stderr.write(`${message}
857
- `);
858
- return 1;
859
- }
860
3454
  export {
3455
+ CLI_VERSION,
3456
+ HELP,
861
3457
  loadCredentials,
862
3458
  main
863
3459
  };