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.
- package/LICENSE +1 -1
- package/README.md +118 -68
- package/dist/yungle.mjs +2897 -568
- 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(
|
|
165
|
+
constructor(status2, code, message, details) {
|
|
6
166
|
super(message);
|
|
7
|
-
this.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
|
-
|
|
41
|
-
|
|
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
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
*
|
|
374
|
+
* What may be retried after a 5xx or a lost connection.
|
|
139
375
|
*
|
|
140
|
-
* GET and DELETE are safe by nature. POST is
|
|
141
|
-
*
|
|
142
|
-
*
|
|
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
|
|
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((
|
|
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
|
|
220
|
-
const token = rest[
|
|
457
|
+
for (let i2 = 0; i2 < rest.length; i2++) {
|
|
458
|
+
const token = rest[i2];
|
|
221
459
|
if (token === "--") {
|
|
222
|
-
positionals.push(...rest.slice(
|
|
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[
|
|
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
|
-
|
|
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
|
|
255
|
-
if (!Number.isInteger(
|
|
256
|
-
return
|
|
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 :
|
|
901
|
+
return configured && configured.startsWith("/") ? configured : join2(homedir(), fallback);
|
|
270
902
|
}
|
|
271
|
-
var CONFIG_PATH =
|
|
272
|
-
var RESUME_PATH =
|
|
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((
|
|
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/
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
381
|
-
const
|
|
382
|
-
for (
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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
|
|
1127
|
+
return { origin: url.origin, slug: m[1], recipient: url.searchParams.get("r"), hasKey: url.hash.length > 1 };
|
|
413
1128
|
}
|
|
414
|
-
function
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
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
|
|
429
|
-
function mcpClients(os = platform(), home = homedir2(),
|
|
430
|
-
const appData =
|
|
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" ?
|
|
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:
|
|
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:
|
|
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:
|
|
2523
|
+
configPath: join4(home, ".codeium", "windsurf", "mcp_config.json")
|
|
465
2524
|
}
|
|
466
2525
|
];
|
|
467
2526
|
}
|
|
468
2527
|
function serverEntry(opts) {
|
|
469
|
-
const
|
|
470
|
-
if (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(
|
|
484
|
-
if (
|
|
485
|
-
if (typeof
|
|
486
|
-
if (Array.isArray(
|
|
487
|
-
if (!Array.isArray(
|
|
488
|
-
return
|
|
489
|
-
}
|
|
490
|
-
if (typeof
|
|
491
|
-
const ka = Object.keys(
|
|
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(
|
|
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
|
|
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
|
|
511
|
-
if (!
|
|
512
|
-
await uploadOne(
|
|
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
|
|
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((
|
|
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: () =>
|
|
2838
|
+
onSuccess: () => resolve4(),
|
|
581
2839
|
onError: (err) => reject(err)
|
|
582
2840
|
});
|
|
583
2841
|
upload.start();
|
|
584
2842
|
});
|
|
585
2843
|
}
|
|
586
2844
|
|
|
587
|
-
// src/
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
case "
|
|
633
|
-
|
|
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 "
|
|
639
|
-
return await
|
|
640
|
-
case "
|
|
641
|
-
return await
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
656
|
-
const
|
|
657
|
-
if (
|
|
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
|
-
|
|
661
|
-
|
|
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
|
|
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:
|
|
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
|
-
{
|
|
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
|
-
|
|
704
|
-
|
|
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(
|
|
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
|
-
|
|
709
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
|
718
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
3065
|
+
await streamAll(files, created);
|
|
3066
|
+
const after = await freshClient(flags, api);
|
|
746
3067
|
await dropSession(key);
|
|
747
|
-
const
|
|
748
|
-
const finalized = await api.finalizeTransfer(created.transfer.id, {
|
|
3068
|
+
const finalized = await after.finalizeTransfer(created.transfer.id, {
|
|
749
3069
|
...recipients.length ? { recipients } : {},
|
|
750
|
-
...
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
|
|
812
|
-
await dropSession(key);
|
|
813
|
-
const collection = await api.getCollection(collectionId);
|
|
3169
|
+
endProgress();
|
|
814
3170
|
return out(
|
|
815
3171
|
json,
|
|
816
|
-
{
|
|
817
|
-
`
|
|
818
|
-
|
|
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
|
-
|
|
822
|
-
const
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
).
|
|
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
|
-
|
|
853
|
-
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}
|
|
861
|
-
|
|
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
|
-
{
|
|
865
|
-
|
|
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
|
|
869
|
-
const
|
|
870
|
-
if (
|
|
871
|
-
return
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
|
916
|
-
|
|
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
|
|
919
|
-
|
|
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
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
`);
|
|
925
|
-
|
|
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
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3351
|
+
for (let waited = 0; waited < interval && !stop; waited += 250) await new Promise((r2) => setTimeout(r2, 250));
|
|
940
3352
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
`);
|
|
944
|
-
else process.stderr.write(`${message}
|
|
3353
|
+
if (!json) process.stderr.write(`
|
|
3354
|
+
${e.dim("Stopped.")}
|
|
945
3355
|
`);
|
|
946
|
-
return
|
|
3356
|
+
return 0;
|
|
947
3357
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
const
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
|
|
982
|
-
|
|
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
|
-
|
|
992
|
-
|
|
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
|
-
|
|
1050
|
-
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|