yungle-cli 0.1.2 → 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 +118 -68
  3. package/dist/yungle.mjs +2897 -568
  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",
@@ -210,16 +445,19 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
210
445
  // positional, and the filter was never applied. The help text promised
211
446
  // `--client <id>`, so the command line was correct and the parser was not.
212
447
  // `helpFlagsAreParsed` in args.test.ts now derives this from HELP.
213
- "client"
448
+ "client",
449
+ "forward-to",
450
+ "interval"
214
451
  ]);
452
+ var OPTIONAL_VALUE = /* @__PURE__ */ new Set(["key"]);
215
453
  function parseArgs(argv) {
216
454
  const [command = "help", ...rest] = argv;
217
455
  const positionals = [];
218
456
  const flags = {};
219
- for (let i = 0; i < rest.length; i++) {
220
- const token = rest[i];
457
+ for (let i2 = 0; i2 < rest.length; i2++) {
458
+ const token = rest[i2];
221
459
  if (token === "--") {
222
- positionals.push(...rest.slice(i + 1));
460
+ positionals.push(...rest.slice(i2 + 1));
223
461
  break;
224
462
  }
225
463
  if (token.startsWith("--")) {
@@ -230,12 +468,16 @@ function parseArgs(argv) {
230
468
  continue;
231
469
  }
232
470
  if (VALUE_FLAGS.has(body)) {
233
- const value = rest[i + 1];
471
+ const value = rest[i2 + 1];
234
472
  if (value === void 0 || value.startsWith("--")) {
473
+ if (OPTIONAL_VALUE.has(body)) {
474
+ flags[body] = true;
475
+ continue;
476
+ }
235
477
  throw new Error(`--${body} needs a value.`);
236
478
  }
237
479
  flags[body] = value;
238
- i++;
480
+ i2++;
239
481
  continue;
240
482
  }
241
483
  flags[body] = true;
@@ -251,25 +493,415 @@ function listFlag(value) {
251
493
  }
252
494
  function numberFlag(value, name) {
253
495
  if (value === void 0 || typeof value === "boolean") return void 0;
254
- const n = Number(value);
255
- if (!Number.isInteger(n) || n <= 0) throw new Error(`--${name} must be a positive whole number.`);
256
- 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;
257
499
  }
258
500
  function stringFlag(value) {
259
501
  return typeof value === "string" ? value : void 0;
260
502
  }
261
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
+
262
894
  // src/config.ts
263
895
  import { createHash } from "node:crypto";
264
896
  import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
265
897
  import { homedir } from "node:os";
266
- import { dirname, join } from "node:path";
898
+ import { dirname, join as join2 } from "node:path";
267
899
  function xdg(envVar, fallback) {
268
900
  const configured = process.env[envVar];
269
- return configured && configured.startsWith("/") ? configured : join(homedir(), fallback);
901
+ return configured && configured.startsWith("/") ? configured : join2(homedir(), fallback);
270
902
  }
271
- var CONFIG_PATH = join(xdg("XDG_CONFIG_HOME", ".config"), "yungle", "config.json");
272
- 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");
273
905
  async function readConfig() {
274
906
  try {
275
907
  return JSON.parse(await readFile(CONFIG_PATH, "utf8"));
@@ -289,11 +921,37 @@ async function resolveKey() {
289
921
  const baseUrl = process.env.YUNGLE_API_URL?.trim() || config.baseUrl;
290
922
  if (fromEnv) return { apiKey: fromEnv, baseUrl, source: "YUNGLE_API_KEY" };
291
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
+ }
292
928
  return null;
293
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
+ }
294
952
  var SESSION_TTL_MS = 2 * 60 * 60 * 1e3;
295
953
  function sessionKey(inputs, target) {
296
- 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);
297
955
  return createHash("sha256").update(JSON.stringify([target, parts])).digest("hex").slice(0, 32);
298
956
  }
299
957
  async function readSessions() {
@@ -315,15 +973,15 @@ async function findSession(key) {
315
973
  async function saveSession(key, session) {
316
974
  const sessions = await readSessions();
317
975
  sessions[key] = session;
318
- await mkdir(dirname(RESUME_PATH), { recursive: true });
319
- 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 });
320
978
  }
321
979
  async function dropSession(key) {
322
980
  const sessions = await readSessions();
323
981
  if (!(key in sessions)) return;
324
982
  delete sessions[key];
325
- await mkdir(dirname(RESUME_PATH), { recursive: true });
326
- 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 });
327
985
  }
328
986
  var URL_PATH = RESUME_PATH.replace(/uploads\.json$/, "upload-urls.json");
329
987
  async function readUploadUrls() {
@@ -337,8 +995,8 @@ async function saveUploadUrl(fileId, uploadUrl) {
337
995
  const current = await readUploadUrls();
338
996
  if (current[fileId] === uploadUrl) return;
339
997
  current[fileId] = uploadUrl;
340
- await mkdir(dirname(URL_PATH), { recursive: true });
341
- 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 });
342
1000
  }
343
1001
  async function clearUploadUrls(fileIds) {
344
1002
  const current = await readUploadUrls();
@@ -349,91 +1007,1492 @@ async function clearUploadUrls(fileIds) {
349
1007
  changed = true;
350
1008
  }
351
1009
  }
352
- if (changed) await writeFile(URL_PATH, JSON.stringify(current));
1010
+ if (changed) await writeFile(URL_PATH, JSON.stringify(current), { mode: 384 });
353
1011
  }
354
1012
 
355
- // src/files.ts
356
- import { readdir, stat } from "node:fs/promises";
357
- import { basename, join as join2, relative, resolve, sep } from "node:path";
358
- function isHidden(name) {
359
- return name.startsWith(".");
360
- }
361
- async function collectFiles(inputs) {
362
- const out2 = [];
363
- for (const input of inputs) {
364
- const absolute = resolve(input);
365
- const info = await stat(absolute);
366
- if (info.isDirectory()) {
367
- await walk(absolute, basename(absolute), out2);
368
- } else {
369
- out2.push({
370
- path: absolute,
371
- name: basename(absolute),
372
- size: info.size,
373
- relativeDir: "",
374
- mtimeMs: info.mtimeMs
375
- });
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 };
376
1063
  }
377
1064
  }
378
- 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) };
379
1074
  }
380
- async function walk(dir, prefix, out2) {
381
- const entries = await readdir(dir, { withFileTypes: true });
382
- for (const entry of entries) {
383
- if (isHidden(entry.name)) continue;
384
- const full = join2(dir, entry.name);
385
- if (entry.isDirectory()) {
386
- await walk(full, `${prefix}/${entry.name}`, out2);
387
- } else if (entry.isFile()) {
388
- const info = await stat(full);
389
- out2.push({
390
- path: full,
391
- name: entry.name,
392
- size: info.size,
393
- relativeDir: prefix,
394
- mtimeMs: info.mtimeMs
395
- });
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
+ }
396
1088
  }
397
1089
  }
1090
+ return dp[a2.length][b.length];
398
1091
  }
399
- function toFileInputs(files) {
400
- return files.map((f) => ({
401
- name: f.name,
402
- size: f.size,
403
- ...f.relativeDir ? { path: f.relativeDir } : {}
404
- }));
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;
405
1103
  }
406
- function pairWithTargets(files, targets) {
407
- 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])) {
408
1123
  throw new Error(
409
- `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}`
410
1125
  );
411
1126
  }
412
- 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 };
413
1128
  }
414
- function formatBytes(bytes) {
415
- if (bytes < 1024) return `${bytes} B`;
416
- const units = ["KB", "MB", "GB", "TB"];
417
- let value = bytes / 1024;
418
- let unit = 0;
419
- while (value >= 1024 && unit < units.length - 1) {
420
- value /= 1024;
421
- 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";
1133
+ }
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";
422
1154
  }
423
- return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
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;
424
2483
  }
425
2484
 
426
2485
  // src/mcp-install.ts
427
2486
  import { homedir as homedir2, platform } from "node:os";
428
- import { join as join3 } from "node:path";
429
- function mcpClients(os = platform(), home = homedir2(), env = process.env) {
430
- const appData = env.APPDATA ?? join3(home, "AppData", "Roaming");
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");
431
2490
  return [
432
2491
  {
433
2492
  id: "claude-desktop",
434
2493
  label: "Claude Desktop",
435
2494
  serversKey: "mcpServers",
436
- configPath: os === "darwin" ? join3(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : os === "win32" ? join3(appData, "Claude", "claude_desktop_config.json") : (
2495
+ configPath: os === "darwin" ? join4(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : os === "win32" ? join4(appData, "Claude", "claude_desktop_config.json") : (
437
2496
  // No Linux build of Claude Desktop exists, so there is nothing to write.
438
2497
  null
439
2498
  )
@@ -442,7 +2501,7 @@ function mcpClients(os = platform(), home = homedir2(), env = process.env) {
442
2501
  id: "claude-code",
443
2502
  label: "Claude Code",
444
2503
  serversKey: "mcpServers",
445
- configPath: join3(home, ".claude.json"),
2504
+ configPath: join4(home, ".claude.json"),
446
2505
  viaCli: {
447
2506
  bin: "claude",
448
2507
  // `-s user` so it applies everywhere, not just the directory the install
@@ -455,20 +2514,20 @@ function mcpClients(os = platform(), home = homedir2(), env = process.env) {
455
2514
  id: "cursor",
456
2515
  label: "Cursor",
457
2516
  serversKey: "mcpServers",
458
- configPath: join3(home, ".cursor", "mcp.json")
2517
+ configPath: join4(home, ".cursor", "mcp.json")
459
2518
  },
460
2519
  {
461
2520
  id: "windsurf",
462
2521
  label: "Windsurf",
463
2522
  serversKey: "mcpServers",
464
- configPath: join3(home, ".codeium", "windsurf", "mcp_config.json")
2523
+ configPath: join4(home, ".codeium", "windsurf", "mcp_config.json")
465
2524
  }
466
2525
  ];
467
2526
  }
468
2527
  function serverEntry(opts) {
469
- const env = { YUNGLE_API_KEY: opts.apiKey };
470
- if (opts.baseUrl) env.YUNGLE_API_URL = opts.baseUrl;
471
- return { command: "npx", args: ["-y", "yungle-mcp"], env };
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 };
472
2531
  }
473
2532
  function mergeServerEntry(existing, serversKey, name, entry) {
474
2533
  const root = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
@@ -480,36 +2539,235 @@ function mergeServerEntry(existing, serversKey, name, entry) {
480
2539
  root[serversKey] = nextServers;
481
2540
  return before === void 0 ? { kind: "added", config: root } : { kind: "updated", config: root, previous: before };
482
2541
  }
483
- function deepEqual(a, b) {
484
- if (a === b) return true;
485
- if (typeof a !== typeof b || a === null || b === null) return false;
486
- if (Array.isArray(a) || Array.isArray(b)) {
487
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
488
- return a.every((v, i) => deepEqual(v, b[i]));
489
- }
490
- if (typeof a !== "object") return false;
491
- const ka = Object.keys(a);
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);
492
2551
  const kb = Object.keys(b);
493
2552
  if (ka.length !== kb.length) return false;
494
2553
  return ka.every(
495
- (k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a[k], b[k])
2554
+ (k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a2[k], b[k])
496
2555
  );
497
2556
  }
498
2557
  function writeScopes(scopes) {
499
2558
  return scopes.filter((s) => s.endsWith(":write"));
500
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
+ }
501
2759
 
502
2760
  // src/upload.ts
503
2761
  import { createReadStream } from "node:fs";
504
- import { stat as stat2 } from "node:fs/promises";
2762
+ import { stat as stat4 } from "node:fs/promises";
505
2763
  var CONCURRENCY = 3;
506
2764
  async function uploadAll(files, endpoint, onProgress) {
507
2765
  const queue = [...files];
508
2766
  const workers = Array.from({ length: Math.min(CONCURRENCY, queue.length) }, async () => {
509
2767
  for (; ; ) {
510
- const next = queue.shift();
511
- if (!next) return;
512
- await uploadOne(next, endpoint, onProgress);
2768
+ const next2 = queue.shift();
2769
+ if (!next2) return;
2770
+ await uploadOne(next2, endpoint, onProgress);
513
2771
  }
514
2772
  });
515
2773
  await Promise.all(workers);
@@ -517,11 +2775,11 @@ async function uploadAll(files, endpoint, onProgress) {
517
2775
  }
518
2776
  async function uploadOne(file, endpoint, onProgress) {
519
2777
  const tus = await import("tus-js-client");
520
- const { size } = await stat2(file.path);
2778
+ const { size } = await stat4(file.path);
521
2779
  const urls = await readUploadUrls();
522
2780
  const uploadUrl = urls[file.target.id];
523
2781
  const started = Date.now();
524
- await new Promise((resolve2, reject) => {
2782
+ await new Promise((resolve4, reject) => {
525
2783
  const upload = new tus.Upload(createReadStream(file.path), {
526
2784
  endpoint,
527
2785
  /**
@@ -577,553 +2835,624 @@ async function uploadOne(file, endpoint, onProgress) {
577
2835
  resumed: Boolean(uploadUrl)
578
2836
  });
579
2837
  },
580
- onSuccess: () => resolve2(),
2838
+ onSuccess: () => resolve4(),
581
2839
  onError: (err) => reject(err)
582
2840
  });
583
2841
  upload.start();
584
2842
  });
585
2843
  }
586
2844
 
587
- // src/index.ts
588
- var HELP = `yungle \u2014 send and sync files with Yungle
589
-
590
- yungle auth login save an API key
591
- yungle auth status show which key is in use
592
- yungle send <paths\u2026> send files, print the share link
593
- --to <email> recipient (repeatable, or comma-separated)
594
- --message <text> note for the recipients
595
- --title <text> label for your dashboard
596
- --password <text> recipients must enter this
597
- --expires <days> lifetime, clamped to your plan
598
- yungle push <paths\u2026> --collection <id> upload into a collection
599
- --folder <id> target folder
600
- yungle ls transfers|collections|contacts
601
- yungle rm transfer <id> revoke a transfer
602
- yungle mcp install let an AI assistant read your Yungle
603
- --client <id> claude-desktop | claude-code | cursor | windsurf
604
- --dry-run show what would change, write nothing
605
- yungle mcp status show where it is installed
606
-
607
- Global flags
608
- --json machine-readable output
609
- --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
+ }
610
2877
 
611
- Credentials are read from YUNGLE_API_KEY, then ${CONFIG_PATH}.
612
- Docs: https://yungle.co/developers`;
613
- 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);
614
2883
  let parsed;
615
2884
  try {
616
- parsed = parseArgs(argv);
2885
+ parsed = parseArgs(argv.length ? argv : ["help"]);
617
2886
  } catch (err) {
618
- process.stderr.write(`${err.message}
619
- `);
620
- return 2;
2887
+ return fail(new CliError(err.message, "yungle help", "usage", 2), false);
621
2888
  }
622
2889
  const { command, positionals, flags } = parsed;
623
2890
  const json = flags.json === true;
624
2891
  try {
2892
+ if (flags.help === true && command !== "help") return help([command], json);
625
2893
  switch (command) {
626
2894
  case "help":
627
2895
  case "--help":
628
2896
  case "-h":
629
- process.stdout.write(`${HELP}
630
- `);
631
- return 0;
632
- case "auth":
633
- 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);
634
2911
  case "send":
635
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);
636
2925
  case "push":
637
2926
  return await push(positionals, flags, json);
638
- case "ls":
639
- return await list(positionals, flags, json);
640
- case "rm":
641
- return await remove(positionals, flags, json);
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);
642
2933
  case "mcp":
643
- return await mcp(positionals, flags, json);
644
- default:
645
- process.stderr.write(`Unknown command: ${command}
646
-
647
- ${HELP}
648
- `);
649
- return 2;
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
+ }
650
2945
  }
651
2946
  } catch (err) {
2947
+ endProgress();
652
2948
  return fail(err, json);
653
2949
  }
654
2950
  }
655
- async function auth(positionals, flags, json) {
656
- const sub = positionals[0] ?? "status";
657
- if (sub === "login") {
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);
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) {
658
2978
  let key = stringFlag(flags.key);
659
2979
  if (!key) {
660
- process.stderr.write(
661
- `Create a key at https://yungle.co/dashboard/settings/api
662
- and paste it here.
663
-
664
- `
665
- );
666
- const rl = createInterface({ input: process.stdin, output: process.stderr });
667
- key = (await rl.question("API key: ")).trim();
668
- rl.close();
669
- }
670
- if (!key) {
671
- process.stderr.write("No key given.\n");
672
- return 2;
2980
+ if (!interactive(flags)) throw new CliError("Give the key: --key yk_live_\u2026", void 0, "usage", 2);
2981
+ key = await askKey();
673
2982
  }
674
- const client2 = new YungleClient({ apiKey: key, baseUrl: stringFlag(flags.url) });
675
- const me = await client2.me();
2983
+ const me2 = await new YungleClient({ apiKey: key, baseUrl: url, userAgent: USER_AGENT }).me();
676
2984
  const config = await readConfig();
677
- await writeConfig({ ...config, apiKey: key, baseUrl: stringFlag(flags.url) ?? config.baseUrl });
678
- return out(
679
- json,
680
- { ok: true, workspace: me.workspace.email, plan: me.plan.name, scopes: me.key.scopes },
681
- `Signed in as ${me.workspace.email ?? me.workspace.id} (${me.plan.name ?? "free"}).
682
- Saved to ${CONFIG_PATH}`
683
- );
684
- }
685
- if (sub === "status") {
686
- const resolved = await resolveKey();
687
- if (!resolved) {
688
- return out(json, { authenticated: false }, "Not signed in. Run `yungle auth login`.", 1);
689
- }
690
- const me = await client(flags).me();
2985
+ await writeConfig({ ...config, apiKey: key, baseUrl: url ?? config.baseUrl });
691
2986
  return out(
692
2987
  json,
693
- { authenticated: true, source: resolved.source, ...me },
694
- [
695
- `Workspace: ${me.workspace.email ?? me.workspace.id}`,
696
- `Plan: ${me.plan.name ?? "free"}`,
697
- `Storage: ${formatBytes(me.plan.usedBytes)} of ${formatBytes(me.plan.quotaBytes)}`,
698
- `Key: ${me.key.name} (${me.key.scopes.join(", ")})`,
699
- `Source: ${resolved.source}`
700
- ].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}`)])
701
2990
  );
702
2991
  }
703
- process.stderr.write("Usage: yungle auth login | yungle auth status\n");
704
- 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
+ );
705
3022
  }
706
- 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;
707
3028
  if (paths.length === 0) {
708
- process.stderr.write("Nothing to send. Give me one or more files or folders.\n");
709
- 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;
710
3035
  }
711
3036
  const api = client(flags);
712
- const files = await collectFiles(paths);
713
- if (files.length === 0) {
714
- process.stderr.write("Those paths contained no files.\n");
715
- 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 = [];
716
3043
  }
717
- const total = files.reduce((n, f) => n + f.size, 0);
718
- progress(`Sending ${files.length} file${files.length === 1 ? "" : "s"} \xB7 ${formatBytes(total)}
719
- `);
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}`);
720
3050
  const key = sessionKey(files, "transfer");
721
3051
  const existing = await findSession(key);
722
3052
  let created;
723
3053
  if (existing && existing.kind === "transfer") {
724
- progress(" resuming the interrupted upload\n");
725
- created = {
726
- transfer: { id: existing.targetId },
727
- tusEndpoint: existing.tusEndpoint,
728
- files: existing.files
729
- };
3054
+ note("Resuming the interrupted upload.");
3055
+ created = { transfer: { id: existing.targetId }, tusEndpoint: existing.tusEndpoint, files: existing.files };
730
3056
  } else {
731
3057
  const fresh = await api.createTransfer({
732
3058
  files: toFileInputs(files),
733
3059
  title: stringFlag(flags.title),
734
3060
  expiresInDays: numberFlag(flags.expires, "expires")
735
3061
  });
736
- created = { transfer: fresh.transfer, tusEndpoint: fresh.tusEndpoint, files: fresh.files.map((f, i) => ({ ...f, path: files[i].path })) };
737
- await saveSession(key, {
738
- createdAt: Date.now(),
739
- kind: "transfer",
740
- targetId: fresh.transfer.id,
741
- tusEndpoint: fresh.tusEndpoint,
742
- files: created.files
743
- });
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 });
744
3064
  }
745
- await streamAll(files, created, api);
3065
+ await streamAll(files, created);
3066
+ const after = await freshClient(flags, api);
746
3067
  await dropSession(key);
747
- const recipients = listFlag(flags.to);
748
- const finalized = await api.finalizeTransfer(created.transfer.id, {
3068
+ const finalized = await after.finalizeTransfer(created.transfer.id, {
749
3069
  ...recipients.length ? { recipients } : {},
750
- ...stringFlag(flags.message) ? { message: stringFlag(flags.message) } : {},
3070
+ ...message ? { message } : {},
751
3071
  ...stringFlag(flags.password) ? { password: stringFlag(flags.password) } : {},
752
3072
  ...stringFlag(flags.title) ? { title: stringFlag(flags.title) } : {}
753
3073
  });
3074
+ const t2 = finalized.transfer;
754
3075
  return out(
755
3076
  json,
756
- {
757
- id: finalized.transfer.id,
758
- url: finalized.transfer.url,
759
- expiresAt: finalized.transfer.expiresAt,
760
- notified: finalized.notified
761
- },
3077
+ { id: t2.id, url: t2.url, expiresAt: t2.expiresAt, notified: finalized.notified },
762
3078
  [
763
- finalized.transfer.url,
764
- finalized.notified.length ? `Emailed ${finalized.notified.join(", ")}` : "Link only \u2014 nobody was emailed."
765
- ].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
766
3089
  );
767
3090
  }
768
- async function push(paths, flags, json) {
769
- const collectionId = stringFlag(flags.collection);
770
- if (!collectionId) {
771
- process.stderr.write("Which collection? Pass --collection <id>.\n");
772
- return 2;
773
- }
774
- if (paths.length === 0) {
775
- process.stderr.write("Nothing to push.\n");
776
- 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();
777
3117
  }
778
- const api = client(flags);
779
- const files = await collectFiles(paths);
780
- if (files.length === 0) {
781
- process.stderr.write("Those paths contained no files.\n");
782
- 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
+ }
783
3145
  }
784
- const total = files.reduce((n, f) => n + f.size, 0);
785
- progress(`Uploading ${files.length} file${files.length === 1 ? "" : "s"} \xB7 ${formatBytes(total)}
786
- `);
787
- const key = sessionKey(files, `collection:${collectionId}:${stringFlag(flags.folder) ?? ""}`);
788
- const existing = await findSession(key);
789
- let targets;
790
- if (existing && existing.kind === "collection") {
791
- progress(" resuming the interrupted upload\n");
792
- targets = { tusEndpoint: existing.tusEndpoint, files: existing.files };
793
- } else {
794
- const fresh = await api.addCollectionFiles(
795
- collectionId,
796
- toFileInputs(files),
797
- stringFlag(flags.folder) ?? null
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"
798
3151
  );
799
- targets = {
800
- tusEndpoint: fresh.tusEndpoint,
801
- files: fresh.files.map((f, i) => ({ ...f, path: files[i].path }))
802
- };
803
- await saveSession(key, {
804
- createdAt: Date.now(),
805
- kind: "collection",
806
- targetId: collectionId,
807
- tusEndpoint: fresh.tusEndpoint,
808
- files: targets.files
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`));
809
3166
  });
3167
+ saved.push(dest);
810
3168
  }
811
- await streamAll(files, targets, api);
812
- await dropSession(key);
813
- const collection = await api.getCollection(collectionId);
3169
+ endProgress();
814
3170
  return out(
815
3171
  json,
816
- { collection: collection.collection.id, uploaded: files.length, url: collection.collection.url },
817
- `Uploaded ${files.length} file${files.length === 1 ? "" : "s"} to \u201C${collection.collection.title}\u201D.
818
- ${collection.collection.url ?? ""}`
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")
819
3175
  );
820
3176
  }
821
- async function list(positionals, flags, json) {
822
- const what = positionals[0] ?? "transfers";
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;
3187
+ }
3188
+ throw new CliError("None of your transfers matches that.", "yungle transfers", "not_found");
3189
+ }
3190
+ async function status(positionals, flags, json) {
823
3191
  const api = client(flags);
824
- if (what === "transfers") {
825
- const { transfers } = await api.listTransfers();
826
- return out(
827
- json,
828
- { transfers },
829
- transfers.length === 0 ? "No transfers yet." : transfers.map(
830
- (t) => `${t.id} ${pad(t.title ?? t.firstFileName ?? "(untitled)", 32)} ${pad(formatBytes(t.sizeBytes), 9)} ${t.downloadCount} dl ${t.url}`
831
- ).join("\n")
832
- );
833
- }
834
- if (what === "collections") {
835
- const { collections } = await api.listCollections();
836
- return out(
837
- json,
838
- { collections },
839
- collections.length === 0 ? "No collections yet." : collections.map(
840
- (c) => `${c.id} ${pad(c.title, 32)} ${pad(`${c.fileCount} files`, 12)} ${formatBytes(c.sizeBytes)}`
841
- ).join("\n")
842
- );
843
- }
844
- if (what === "contacts") {
845
- const { contacts } = await api.listContacts();
846
- return out(
847
- json,
848
- { contacts },
849
- 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")
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` }))
850
3200
  );
851
3201
  }
852
- process.stderr.write("Usage: yungle ls transfers|collections|contacts\n");
853
- return 2;
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}`);
854
3223
  }
855
- async function remove(positionals, flags, json) {
856
- const [what, id] = positionals;
857
- if (what !== "transfer" || !id) {
858
- process.stderr.write("Usage: yungle rm transfer <id>\n");
859
- return 2;
860
- }
861
- const { transfer } = await client(flags).revokeTransfer(id);
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>")}`, "");
862
3232
  return out(
863
3233
  json,
864
- { id: transfer.id, status: transfer.status },
865
- `Revoked. The link no longer works and the content is unrecoverable.`
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")
866
3249
  );
867
3250
  }
868
- function client(flags) {
869
- const apiKey = process.env.YUNGLE_API_KEY?.trim() ?? cachedKey;
870
- if (!apiKey) throw new NotSignedIn();
871
- return new YungleClient({ apiKey, baseUrl: stringFlag(flags.url) ?? cachedBaseUrl });
872
- }
873
- var NotSignedIn = class extends Error {
874
- constructor() {
875
- super("Not signed in. Run `yungle auth login`, or set YUNGLE_API_KEY.");
876
- this.name = "NotSignedIn";
877
- }
878
- };
879
- var cachedKey;
880
- var cachedBaseUrl;
881
- async function loadCredentials() {
882
- const resolved = await resolveKey();
883
- cachedKey = resolved?.apiKey;
884
- cachedBaseUrl = resolved?.baseUrl;
885
- }
886
- async function streamAll(files, targets, _api) {
887
- const paired = pairWithTargets(files, targets.files);
888
- const totalBytes = paired.reduce((n, { file }) => n + file.size, 0);
889
- const sentPer = /* @__PURE__ */ new Map();
890
- const finished = /* @__PURE__ */ new Set();
891
- const started = Date.now();
892
- let anyResumed = false;
893
- const render = () => {
894
- let sent = 0;
895
- for (const value of sentPer.values()) sent += value;
896
- const pct = totalBytes ? Math.floor(sent / totalBytes * 100) : 100;
897
- const rate = sent / Math.max(1, (Date.now() - started) / 1e3);
898
- progress(
899
- `\r ${String(pct).padStart(3)}% ${pad(`${formatBytes(sent)} of ${formatBytes(totalBytes)}`, 22)}${pad(`${formatBytes(rate)}/s`, 12)}${finished.size}/${paired.length} files${anyResumed ? " (resumed)" : ""} `
900
- );
901
- };
902
- await uploadAll(
903
- paired.map(({ file, target }) => ({ path: file.path, target })),
904
- targets.tusEndpoint,
905
- (p) => {
906
- if (p.resumed) anyResumed = true;
907
- sentPer.set(p.id, p.sent);
908
- if (p.sent >= p.total) finished.add(p.id);
909
- render();
910
- }
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")
911
3262
  );
912
- render();
913
- progress("\n");
914
3263
  }
915
- function progress(text) {
916
- if (process.stderr.isTTY) process.stderr.write(text);
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
+ );
917
3276
  }
918
- function pad(text, width) {
919
- return text.length > width ? `${text.slice(0, width - 1)}\u2026` : text.padEnd(width);
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);
920
3287
  }
921
- function out(json, data, human, code = 0) {
922
- process.stdout.write(json ? `${JSON.stringify(data, null, 2)}
923
- ` : `${human}
924
- `);
925
- return code;
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))}`);
3296
+ const key = sessionKey(files, `collection:${collectionId}:${stringFlag(flags.folder) ?? ""}`);
3297
+ const existing = await findSession(key);
3298
+ let targets;
3299
+ if (existing && existing.kind === "collection") {
3300
+ note("Resuming the interrupted upload.");
3301
+ targets = { tusEndpoint: existing.tusEndpoint, files: existing.files };
3302
+ } else {
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 });
3306
+ }
3307
+ await streamAll(files, targets);
3308
+ await dropSession(key);
3309
+ const { collection } = await (await freshClient(flags, api)).getCollection(collectionId);
3310
+ return out(
3311
+ json,
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
3315
+ );
926
3316
  }
927
- function fail(err, json) {
928
- if (err instanceof YungleApiError) {
929
- if (json) {
930
- process.stdout.write(`${JSON.stringify({ error: { code: err.code, message: err.message, details: err.details } }, null, 2)}
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;
3322
+ const api = client(flags);
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);
3329
+ }
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 })}
931
3345
  `);
932
- } else {
933
- process.stderr.write(`${err.message}
3346
+ else process.stdout.write(` ${accentOut(sym.ok)} ${join6(f.relativeDir, f.name)} ${o.dim(formatBytes(f.size))}
934
3347
  `);
935
- if (err.code === "upgrade_required") {
936
- process.stderr.write("See https://yungle.co/dashboard/settings/plan\n");
937
3348
  }
3349
+ await saveUploaded(dir, collectionId, uploaded);
938
3350
  }
939
- return 1;
3351
+ for (let waited = 0; waited < interval && !stop; waited += 250) await new Promise((r2) => setTimeout(r2, 250));
940
3352
  }
941
- const message = err instanceof Error ? err.message : String(err);
942
- if (json) process.stdout.write(`${JSON.stringify({ error: { code: "cli_error", message } }, null, 2)}
943
- `);
944
- else process.stderr.write(`${message}
3353
+ if (!json) process.stderr.write(`
3354
+ ${e.dim("Stopped.")}
945
3355
  `);
946
- return 1;
3356
+ return 0;
947
3357
  }
948
- async function mcp(positionals, flags, json) {
949
- const sub = positionals[0] ?? "install";
950
- if (sub !== "install" && sub !== "status") {
951
- process.stderr.write("Usage: yungle mcp install | yungle mcp status\n");
952
- return 2;
953
- }
954
- const { readFile: readFile2, writeFile: writeFile2, mkdir: mkdir2, copyFile } = await import("node:fs/promises");
955
- const { dirname: dirname2 } = await import("node:path");
956
- const all = mcpClients();
957
- const only = stringFlag(flags.client);
958
- if (only && !all.some((c) => c.id === only)) {
959
- process.stderr.write(
960
- `Unknown client: ${only}. Known: ${all.map((c) => c.id).join(", ")}
961
- `
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}`;
3375
+ }
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")}`, "");
3388
+ return out(
3389
+ json,
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")
962
3396
  );
963
- return 2;
964
3397
  }
965
- const targets = only ? all.filter((c) => c.id === only) : all;
966
- if (sub === "status") {
967
- const rows = [];
968
- for (const c of targets) {
969
- let installed = false;
970
- if (c.configPath) {
971
- try {
972
- const doc = JSON.parse(await readFile2(c.configPath, "utf8"));
973
- const servers = doc[c.serversKey];
974
- installed = !!servers && typeof servers === "object" && "yungle" in servers;
975
- } catch {
976
- installed = false;
977
- }
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}`);
3405
+ }
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
+ `);
978
3429
  }
979
- rows.push({ client: c.id, path: c.configPath, installed });
980
3430
  }
981
- return out(
982
- json,
983
- { clients: rows },
984
- rows.map(
985
- (r) => `${pad(r.client, 16)} ${r.installed ? "installed" : r.path ? "not installed" : "n/a on this platform"}`
986
- ).join("\n")
987
- );
3431
+ cursor3 = page.nextCursor ?? cursor3;
3432
+ if (!page.hasMore) await new Promise((r2) => setTimeout(r2, 2e3));
988
3433
  }
3434
+ }
3435
+ async function freshClient(flags, current) {
3436
+ if (process.env.YUNGLE_API_KEY?.trim()) return current;
989
3437
  const resolved = await resolveKey();
990
- if (!resolved) {
991
- process.stderr.write(
992
- `No API key. Run \`yungle auth login\` first, or set YUNGLE_API_KEY.
993
- Get a key at https://yungle.co/dashboard/settings/api \u2014 reading is free on any plan.
994
- `
995
- );
996
- return 1;
997
- }
998
- let scopes;
999
- try {
1000
- const me = await client(flags).me();
1001
- scopes = me.key.scopes ?? [];
1002
- } catch (err) {
1003
- process.stderr.write(
1004
- `Could not check what this key is allowed to do: ${err.message}
1005
- Not installing \u2014 an agent should never get a key of unknown scope.
1006
- `
1007
- );
1008
- return 1;
1009
- }
1010
- const writes = writeScopes(scopes);
1011
- if (writes.length > 0) {
1012
- process.stderr.write(
1013
- `This key can write (${writes.join(", ")}).
1014
-
1015
- The MCP server has no tool that sends, invites, revokes or deletes, so a
1016
- write scope grants an assistant nothing it can use \u2014 but it does put a
1017
- credential that can change your account into a config file an assistant
1018
- reads. Create a read-only key instead:
1019
-
1020
- https://yungle.co/dashboard/settings/api
1021
-
1022
- then \`yungle auth login\` with it and re-run this.
1023
- `
1024
- );
1025
- return 1;
1026
- }
1027
- const entry = serverEntry({ apiKey: resolved.apiKey, baseUrl: resolved.baseUrl });
1028
- const dryRun = flags["dry-run"] === true;
1029
- const results = [];
1030
- for (const c of targets) {
1031
- const r = await installInto(c, entry, { dryRun, readFile: readFile2, writeFile: writeFile2, mkdir: mkdir2, copyFile, dirname: dirname2 });
1032
- results.push({ client: c.id, ...r });
1033
- }
1034
- const changed = results.filter((r) => r.action === "added" || r.action === "updated");
1035
- const lines = results.map((r) => `${pad(r.client, 16)} ${r.action}${r.detail ? ` \u2014 ${r.detail}` : ""}`);
1036
- if (changed.length > 0 && !dryRun) {
1037
- lines.push("");
1038
- lines.push("Restart the client(s) above to pick it up.");
1039
- lines.push('Then ask: "what did I send recently?" or "did the client download it?"');
1040
- lines.push("");
1041
- lines.push(`Your API key is now also in ${changed.length === 1 ? "that config file" : "those config files"}, in plain text.`);
1042
- }
1043
- if (changed.length === 0 && !dryRun) {
1044
- lines.push("");
1045
- lines.push("Nothing to do. `yungle mcp status` shows where it is installed.");
1046
- }
1047
- return out(json, { installed: results, dryRun }, lines.join("\n"));
3438
+ if (!resolved || resolved.apiKey === cachedKey) return current;
3439
+ cachedKey = resolved.apiKey;
3440
+ return client(flags);
1048
3441
  }
1049
- async function installInto(c, entry, io) {
1050
- if (!c.configPath) return { action: "skipped", detail: "not available on this platform" };
1051
- if (c.viaCli) {
1052
- const { spawnSync } = await import("node:child_process");
1053
- const probe = spawnSync(c.viaCli.bin, ["--version"], { stdio: "ignore", shell: false });
1054
- if (probe.error) return { action: "skipped", detail: `${c.viaCli.bin} not installed` };
1055
- if (io.dryRun) {
1056
- return { action: "would run", detail: `${c.viaCli.bin} mcp add-json -s user yungle \u2026` };
1057
- }
1058
- const existed = spawnSync(c.viaCli.bin, ["mcp", "get", "yungle"], { stdio: "ignore", shell: false }).status === 0;
1059
- if (existed) {
1060
- spawnSync(c.viaCli.bin, ["mcp", "remove", "-s", "user", "yungle"], {
1061
- stdio: "ignore",
1062
- shell: false
1063
- });
1064
- }
1065
- const res = spawnSync(c.viaCli.bin, c.viaCli.args("yungle", JSON.stringify(entry)), {
1066
- stdio: "pipe",
1067
- shell: false,
1068
- encoding: "utf8"
1069
- });
1070
- if (res.status !== 0) {
1071
- const why = (res.stderr || res.stdout || "").trim().split("\n")[0] || `exit ${res.status}`;
1072
- return { action: "failed", detail: why };
1073
- }
1074
- return {
1075
- action: existed ? "updated" : "added",
1076
- detail: `via ${c.viaCli.bin} mcp add-json`
1077
- };
1078
- }
1079
- let raw = null;
1080
- try {
1081
- raw = await io.readFile(c.configPath, "utf8");
1082
- } catch {
1083
- let dirExists = true;
1084
- try {
1085
- await io.readFile(io.dirname(c.configPath), "utf8");
1086
- } catch (err) {
1087
- dirExists = err.code === "EISDIR";
1088
- }
1089
- if (!dirExists) return { action: "skipped", detail: "not installed" };
1090
- }
1091
- let existing = {};
1092
- if (raw !== null && raw.trim() !== "") {
1093
- try {
1094
- existing = JSON.parse(raw);
1095
- } catch {
1096
- return {
1097
- action: "refused",
1098
- path: c.configPath,
1099
- detail: `${c.configPath} is not valid JSON \u2014 fix or move it, nothing was written`
1100
- };
1101
- }
1102
- if (existing === null || typeof existing !== "object" || Array.isArray(existing)) {
1103
- return {
1104
- action: "refused",
1105
- path: c.configPath,
1106
- detail: `${c.configPath} is not a JSON object \u2014 nothing was written`
1107
- };
1108
- }
1109
- }
1110
- const merged = mergeServerEntry(existing, c.serversKey, "yungle", entry);
1111
- if (merged.kind === "unchanged") return { action: "already current", path: c.configPath };
1112
- if (io.dryRun) {
1113
- return { action: `would be ${merged.kind}`, path: c.configPath, detail: c.configPath };
1114
- }
1115
- if (raw !== null) {
1116
- try {
1117
- await io.copyFile(c.configPath, `${c.configPath}.yungle-bak`);
1118
- } catch {
1119
- }
1120
- }
1121
- await io.mkdir(io.dirname(c.configPath), { recursive: true });
1122
- await io.writeFile(c.configPath, `${JSON.stringify(merged.config, null, 2)}
1123
- `, { mode: 384 });
1124
- return { action: merged.kind, path: c.configPath, detail: c.configPath };
3442
+ function client(flags) {
3443
+ const apiKey = process.env.YUNGLE_API_KEY?.trim() || cachedKey;
3444
+ if (!apiKey) throw new NotSignedIn();
3445
+ return new YungleClient({ apiKey, baseUrl: stringFlag(flags.url) ?? cachedBaseUrl, userAgent: USER_AGENT });
3446
+ }
3447
+ var cachedKey;
3448
+ var cachedBaseUrl;
3449
+ async function loadCredentials() {
3450
+ const resolved = await resolveKey();
3451
+ cachedKey = resolved?.apiKey;
3452
+ cachedBaseUrl = resolved?.baseUrl;
1125
3453
  }
1126
3454
  export {
3455
+ CLI_VERSION,
1127
3456
  HELP,
1128
3457
  loadCredentials,
1129
3458
  main