@medialit/cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +52 -2
  2. package/dist/index.mjs +617 -0
  3. package/package.json +45 -4
package/README.md CHANGED
@@ -1,3 +1,53 @@
1
- # Temporary Holding Version
1
+ # @medialit/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Upload and manage [MediaLit](https://medialit.cloud) files from the terminal.
4
+
5
+ ```bash
6
+ npm install -g @medialit/cli
7
+ medialit login
8
+ medialit upload photo.jpg --public
9
+ ```
10
+
11
+ ## Log in
12
+
13
+ ```bash
14
+ medialit login
15
+ ```
16
+
17
+ Your browser opens to sign in. If you have more than one app, you choose the app the CLI works with. Run `medialit login` again to choose a different one.
18
+
19
+ For a self-hosted MediaLit, pass its URL once. Later commands use the server you last logged in to:
20
+
21
+ ```bash
22
+ medialit login --endpoint https://medialit.example.com
23
+ ```
24
+
25
+ Without a browser, for example over SSH, use `--no-browser` and open the printed URL on another device whose browser can reach this machine's `127.0.0.1`.
26
+
27
+ In CI, skip logging in and set `MEDIALIT_API_KEY` (and `MEDIALIT_ENDPOINT` if you self-host).
28
+
29
+ ## Commands
30
+
31
+ | Command | What it does |
32
+ | -------------------------- | -------------------------------------------------- |
33
+ | `medialit upload <files…>` | Uploads and seals files, then prints their URLs |
34
+ | `medialit ls` | Lists sealed files, newest first |
35
+ | `medialit get <id>` | Prints a file's URL, a fresh one for private files |
36
+ | `medialit seal <id>` | Keeps a temporary upload |
37
+ | `medialit rm <id>` | Deletes a file |
38
+ | `medialit whoami` | Shows the server, account, app and storage used |
39
+ | `medialit logout` | Logs out of the server |
40
+
41
+ Uploads are private unless you pass `--public`. Add `--caption <text>` or `--group <name>` to label them, or `--temp` to leave them unsealed so MediaLit deletes them after 24 hours. Large files are uploaded in chunks and retried if the connection drops.
42
+
43
+ `ls` takes `--public`, `--private`, `--group <name>`, `--page <n>` and `--limit <n>`.
44
+
45
+ Every command takes `--json`. URLs and JSON go to stdout and messages to stderr, so you can pipe the output:
46
+
47
+ ```bash
48
+ medialit upload report.pdf --public | pbcopy
49
+ ```
50
+
51
+ ## Where your login is stored
52
+
53
+ In `~/.config/medialit/credentials.json` (`%APPDATA%\medialit` on Windows), readable only by you. Set `MEDIALIT_CONFIG_DIR` to use another folder.
package/dist/index.mjs ADDED
@@ -0,0 +1,617 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { parseArgs } from "node:util";
5
+ import { createRequire } from "node:module";
6
+
7
+ // src/commands.ts
8
+ import { spawn } from "node:child_process";
9
+ import { createReadStream } from "node:fs";
10
+ import { stat } from "node:fs/promises";
11
+ import { basename } from "node:path";
12
+ import { lookup } from "mime-types";
13
+ import { uploadFile } from "@medialit/uploader";
14
+
15
+ // src/credentials.ts
16
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
17
+ import { homedir } from "node:os";
18
+ import { join } from "node:path";
19
+ var DEFAULT_ENDPOINT = "https://api.medialit.cloud";
20
+ function normalizeEndpoint(endpoint) {
21
+ return new URL(endpoint).toString().replace(/\/+$/, "");
22
+ }
23
+ function configDir() {
24
+ if (process.env.MEDIALIT_CONFIG_DIR) return process.env.MEDIALIT_CONFIG_DIR;
25
+ if (process.platform === "win32") {
26
+ return join(
27
+ process.env.APPDATA || join(homedir(), "AppData", "Roaming"),
28
+ "medialit"
29
+ );
30
+ }
31
+ return join(
32
+ process.env.XDG_CONFIG_HOME || join(homedir(), ".config"),
33
+ "medialit"
34
+ );
35
+ }
36
+ var credentialsFile = () => join(configDir(), "credentials.json");
37
+ async function loadCredentials() {
38
+ try {
39
+ const parsed = JSON.parse(await readFile(credentialsFile(), "utf8"));
40
+ return { servers: {}, ...parsed };
41
+ } catch {
42
+ return { servers: {} };
43
+ }
44
+ }
45
+ async function saveCredentials(credentials) {
46
+ await mkdir(configDir(), { recursive: true, mode: 448 });
47
+ await writeFile(credentialsFile(), JSON.stringify(credentials, null, 2), {
48
+ mode: 384
49
+ });
50
+ await chmod(credentialsFile(), 384);
51
+ }
52
+
53
+ // src/oauth.ts
54
+ import { createHash, randomBytes } from "node:crypto";
55
+ import http from "node:http";
56
+ var SCOPES = "openid email offline_access data:read data:write";
57
+ var CALLBACK_PATH = "/callback";
58
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
59
+ async function discover(endpoint) {
60
+ const resource = await getJson(
61
+ `${endpoint}/.well-known/oauth-protected-resource`
62
+ );
63
+ const issuer = resource.authorization_servers?.[0];
64
+ if (!issuer) {
65
+ throw new Error(`${endpoint} does not support logging in with OAuth`);
66
+ }
67
+ const issuerUrl = new URL(issuer);
68
+ const path = issuerUrl.pathname === "/" ? "" : issuerUrl.pathname;
69
+ const server = await getJson(
70
+ `${issuerUrl.origin}/.well-known/oauth-authorization-server${path}`
71
+ );
72
+ return {
73
+ authorizationEndpoint: server.authorization_endpoint,
74
+ tokenEndpoint: server.token_endpoint,
75
+ registrationEndpoint: server.registration_endpoint,
76
+ revocationEndpoint: server.revocation_endpoint,
77
+ userinfoEndpoint: server.userinfo_endpoint,
78
+ resource: resource.resource
79
+ };
80
+ }
81
+ async function register(metadata) {
82
+ if (!metadata.registrationEndpoint) {
83
+ throw new Error("This server does not let the CLI register itself");
84
+ }
85
+ const response = await fetch(metadata.registrationEndpoint, {
86
+ method: "POST",
87
+ headers: { "content-type": "application/json" },
88
+ body: JSON.stringify({
89
+ client_name: "MediaLit CLI",
90
+ application_type: "native",
91
+ // The server accepts any port on a loopback redirect (RFC 8252).
92
+ redirect_uris: [`http://127.0.0.1${CALLBACK_PATH}`],
93
+ grant_types: ["authorization_code", "refresh_token"],
94
+ response_types: ["code"],
95
+ token_endpoint_auth_method: "none",
96
+ scope: SCOPES
97
+ })
98
+ });
99
+ const body = await readJson(response);
100
+ if (!response.ok || !body.client_id) {
101
+ throw new Error(
102
+ `Could not register the CLI: ${errorMessage(body, response)}`
103
+ );
104
+ }
105
+ return body.client_id;
106
+ }
107
+ async function authorize(metadata, clientId, openUrl) {
108
+ const verifier = base64url(randomBytes(32));
109
+ const challenge = base64url(createHash("sha256").update(verifier).digest());
110
+ const state = base64url(randomBytes(16));
111
+ const callback = await listenForCallback(state);
112
+ const redirectUri = `http://127.0.0.1:${callback.port}${CALLBACK_PATH}`;
113
+ const url = new URL(metadata.authorizationEndpoint);
114
+ url.search = new URLSearchParams({
115
+ response_type: "code",
116
+ client_id: clientId,
117
+ redirect_uri: redirectUri,
118
+ scope: SCOPES,
119
+ state,
120
+ code_challenge: challenge,
121
+ code_challenge_method: "S256",
122
+ resource: metadata.resource
123
+ }).toString();
124
+ openUrl(url.toString());
125
+ const code = await callback.code;
126
+ const tokens = await requestTokens(metadata.tokenEndpoint, {
127
+ grant_type: "authorization_code",
128
+ code,
129
+ redirect_uri: redirectUri,
130
+ client_id: clientId,
131
+ code_verifier: verifier,
132
+ resource: metadata.resource
133
+ });
134
+ if (!tokens.email && metadata.userinfoEndpoint) {
135
+ tokens.email = await fetchEmail(
136
+ metadata.userinfoEndpoint,
137
+ tokens.accessToken
138
+ );
139
+ }
140
+ return tokens;
141
+ }
142
+ async function fetchEmail(userinfoEndpoint, accessToken) {
143
+ try {
144
+ const response = await fetch(userinfoEndpoint, {
145
+ headers: { authorization: `Bearer ${accessToken}` }
146
+ });
147
+ const body = await readJson(response);
148
+ return typeof body.email === "string" ? body.email : void 0;
149
+ } catch {
150
+ return void 0;
151
+ }
152
+ }
153
+ function refresh(tokenEndpoint, input) {
154
+ return requestTokens(tokenEndpoint, {
155
+ grant_type: "refresh_token",
156
+ refresh_token: input.refreshToken,
157
+ client_id: input.clientId,
158
+ resource: input.resource
159
+ });
160
+ }
161
+ async function revoke(revocationEndpoint, input) {
162
+ await fetch(revocationEndpoint, {
163
+ method: "POST",
164
+ headers: { "content-type": "application/x-www-form-urlencoded" },
165
+ body: new URLSearchParams({
166
+ token: input.token,
167
+ token_type_hint: "refresh_token",
168
+ client_id: input.clientId
169
+ })
170
+ });
171
+ }
172
+ async function listenForCallback(state) {
173
+ let settle;
174
+ const code = new Promise((resolve, reject) => {
175
+ settle = { resolve, reject };
176
+ });
177
+ const server = http.createServer((req, res) => {
178
+ const url = new URL(req.url || "/", "http://127.0.0.1");
179
+ if (url.pathname !== CALLBACK_PATH) {
180
+ res.writeHead(404).end();
181
+ return;
182
+ }
183
+ const error = url.searchParams.get("error");
184
+ const received = url.searchParams.get("code");
185
+ if (error || !received || url.searchParams.get("state") !== state) {
186
+ respond(res, 400, "Login failed. Return to your terminal.");
187
+ settle.reject(
188
+ new Error(
189
+ url.searchParams.get("error_description") || error || "The login response was not valid"
190
+ )
191
+ );
192
+ return;
193
+ }
194
+ respond(res, 200, "You're logged in. You can close this tab.");
195
+ settle.resolve(received);
196
+ });
197
+ await new Promise(
198
+ (resolve) => server.listen(0, "127.0.0.1", resolve)
199
+ );
200
+ const timeout = setTimeout(
201
+ () => settle.reject(new Error("Timed out waiting for the login")),
202
+ LOGIN_TIMEOUT_MS
203
+ );
204
+ code.finally(() => {
205
+ clearTimeout(timeout);
206
+ server.close();
207
+ }).catch(() => void 0);
208
+ return { port: server.address().port, code };
209
+ }
210
+ function respond(res, status, message) {
211
+ res.writeHead(status, { "content-type": "text/html; charset=utf-8" });
212
+ res.end(
213
+ `<!doctype html><title>MediaLit CLI</title><body style="font-family:system-ui;display:grid;place-items:center;height:90vh"><p>${message}</p></body>`
214
+ );
215
+ }
216
+ async function requestTokens(tokenEndpoint, params) {
217
+ const response = await fetch(tokenEndpoint, {
218
+ method: "POST",
219
+ headers: { "content-type": "application/x-www-form-urlencoded" },
220
+ body: new URLSearchParams(params)
221
+ });
222
+ const body = await readJson(response);
223
+ if (!response.ok || !body.access_token) {
224
+ throw new Error(errorMessage(body, response));
225
+ }
226
+ return {
227
+ accessToken: body.access_token,
228
+ refreshToken: body.refresh_token,
229
+ expiresAt: body.expires_in ? Date.now() + body.expires_in * 1e3 : void 0,
230
+ email: jwtClaim(body.id_token, "email"),
231
+ appId: jwtClaim(body.access_token, "app_id")
232
+ };
233
+ }
234
+ function jwtClaim(token, name) {
235
+ if (typeof token !== "string") return void 0;
236
+ const payload = token.split(".")[1];
237
+ if (!payload) return void 0;
238
+ try {
239
+ const value = JSON.parse(Buffer.from(payload, "base64url").toString())[name];
240
+ return typeof value === "string" ? value : void 0;
241
+ } catch {
242
+ return void 0;
243
+ }
244
+ }
245
+ async function getJson(url) {
246
+ const response = await fetch(url);
247
+ if (!response.ok) {
248
+ throw new Error(`${url} responded with ${response.status}`);
249
+ }
250
+ return response.json();
251
+ }
252
+ async function readJson(response) {
253
+ return response.json().catch(() => ({}));
254
+ }
255
+ function errorMessage(body, response) {
256
+ return body?.error_description || body?.error || `the server responded with ${response.status}`;
257
+ }
258
+ function base64url(bytes) {
259
+ return bytes.toString("base64url");
260
+ }
261
+
262
+ // src/session.ts
263
+ import { MediaLit } from "medialit";
264
+ var REFRESH_MARGIN_MS = 60 * 1e3;
265
+ async function resolveEndpoint(flag) {
266
+ if (flag) return normalizeEndpoint(flag);
267
+ if (process.env.MEDIALIT_ENDPOINT) {
268
+ return normalizeEndpoint(process.env.MEDIALIT_ENDPOINT);
269
+ }
270
+ return (await loadCredentials()).current || DEFAULT_ENDPOINT;
271
+ }
272
+ async function getClient(endpointFlag) {
273
+ const endpoint = await resolveEndpoint(endpointFlag);
274
+ if (process.env.MEDIALIT_API_KEY) {
275
+ return {
276
+ client: new MediaLit({
277
+ apiKey: process.env.MEDIALIT_API_KEY,
278
+ endpoint
279
+ }),
280
+ endpoint
281
+ };
282
+ }
283
+ const credentials = await loadCredentials();
284
+ const server = credentials.servers[endpoint];
285
+ if (!server?.accessToken) {
286
+ throw new Error(
287
+ `Not logged in to ${endpoint}. Run \`medialit login\` first.`
288
+ );
289
+ }
290
+ if (server.expiresAt && server.expiresAt - Date.now() < REFRESH_MARGIN_MS) {
291
+ if (!server.refreshToken) {
292
+ throw new Error(
293
+ "Your login has expired. Run `medialit login` again."
294
+ );
295
+ }
296
+ let tokens;
297
+ try {
298
+ tokens = await refresh(server.tokenEndpoint, {
299
+ clientId: server.clientId,
300
+ refreshToken: server.refreshToken,
301
+ resource: server.resource
302
+ });
303
+ } catch {
304
+ throw new Error(
305
+ "Your login has expired. Run `medialit login` again."
306
+ );
307
+ }
308
+ server.accessToken = tokens.accessToken;
309
+ server.refreshToken = tokens.refreshToken || server.refreshToken;
310
+ server.expiresAt = tokens.expiresAt;
311
+ server.appId = tokens.appId || server.appId;
312
+ await saveCredentials(credentials);
313
+ }
314
+ return {
315
+ client: new MediaLit({ accessToken: server.accessToken, endpoint }),
316
+ endpoint
317
+ };
318
+ }
319
+
320
+ // src/commands.ts
321
+ async function login(flags) {
322
+ const endpoint = await resolveEndpoint(flags.endpoint);
323
+ const metadata = await discover(endpoint);
324
+ const credentials = await loadCredentials();
325
+ const previous = credentials.servers[endpoint];
326
+ const clientId = previous?.clientId || await register(metadata);
327
+ const tokens = await authorize(metadata, clientId, (url) => {
328
+ log(`Opening your browser to log in to ${endpoint}.`);
329
+ log(`If it doesn't open, visit:
330
+
331
+ ${url}
332
+ `);
333
+ if (flags.browser !== false) openBrowser(url);
334
+ });
335
+ credentials.servers[endpoint] = {
336
+ clientId,
337
+ tokenEndpoint: metadata.tokenEndpoint,
338
+ revocationEndpoint: metadata.revocationEndpoint,
339
+ resource: metadata.resource,
340
+ ...tokens
341
+ };
342
+ credentials.current = endpoint;
343
+ await saveCredentials(credentials);
344
+ output(
345
+ flags,
346
+ { endpoint, email: tokens.email, appId: tokens.appId },
347
+ () => [
348
+ `Logged in${tokens.email ? ` as ${tokens.email}` : ""}.`,
349
+ tokens.appId ? `Uploads go to app ${tokens.appId}.` : "",
350
+ "Run `medialit login` again to choose a different app."
351
+ ].filter(Boolean).join("\n")
352
+ );
353
+ }
354
+ async function logout(flags) {
355
+ const endpoint = await resolveEndpoint(flags.endpoint);
356
+ const credentials = await loadCredentials();
357
+ const server = credentials.servers[endpoint];
358
+ if (server?.refreshToken && server.revocationEndpoint) {
359
+ await revoke(server.revocationEndpoint, {
360
+ clientId: server.clientId,
361
+ token: server.refreshToken
362
+ }).catch(() => void 0);
363
+ }
364
+ if (server) {
365
+ credentials.servers[endpoint] = {
366
+ clientId: server.clientId,
367
+ tokenEndpoint: server.tokenEndpoint,
368
+ revocationEndpoint: server.revocationEndpoint,
369
+ resource: server.resource
370
+ };
371
+ await saveCredentials(credentials);
372
+ }
373
+ output(flags, { endpoint }, () => `Logged out of ${endpoint}.`);
374
+ }
375
+ async function whoami(flags) {
376
+ const { client, endpoint } = await getClient(flags.endpoint);
377
+ const server = (await loadCredentials()).servers[endpoint];
378
+ const usingApiKey = !!process.env.MEDIALIT_API_KEY;
379
+ const [stats, count] = await Promise.all([
380
+ client.getStats(),
381
+ client.getCount()
382
+ ]);
383
+ const info = {
384
+ endpoint,
385
+ auth: usingApiKey ? "api-key" : "login",
386
+ email: usingApiKey ? void 0 : server?.email,
387
+ appId: usingApiKey ? void 0 : server?.appId,
388
+ files: count,
389
+ storage: stats.storage,
390
+ maxStorage: stats.maxStorage
391
+ };
392
+ output(
393
+ flags,
394
+ info,
395
+ () => [
396
+ `Server: ${endpoint}`,
397
+ usingApiKey ? "Using: MEDIALIT_API_KEY" : `Account: ${info.email || "unknown"}`,
398
+ info.appId ? `App: ${info.appId}` : "",
399
+ `Files: ${count}`,
400
+ `Storage: ${formatBytes(stats.storage)} of ${formatBytes(stats.maxStorage)}`
401
+ ].filter(Boolean).join("\n")
402
+ );
403
+ }
404
+ async function upload(paths, flags) {
405
+ if (!paths.length) throw new Error("Pass at least one file to upload.");
406
+ const { client, endpoint } = await getClient(flags.endpoint);
407
+ const results = [];
408
+ for (const path of paths) {
409
+ const info = await stat(path).catch(() => null);
410
+ if (!info?.isFile()) throw new Error(`${path} is not a file`);
411
+ const fileName = basename(path);
412
+ const media = await uploadFile(createReadStream(path), {
413
+ getSignature: async () => ({
414
+ signature: await client.getSignature({ group: flags.group }),
415
+ endpoint
416
+ }),
417
+ fileName,
418
+ mimeType: lookup(fileName) || void 0,
419
+ access: flags.public ? "public" : "private",
420
+ caption: flags.caption,
421
+ // Chunks let a dropped connection resume instead of restarting.
422
+ chunkSize: 8 * 1024 * 1024,
423
+ onProgress: ({ percentage }) => progress(`${fileName} ${Math.floor(percentage)}%`)
424
+ });
425
+ progress("");
426
+ const kept = flags.temp ? media : await client.seal(media.mediaId);
427
+ results.push(kept);
428
+ if (!flags.json) {
429
+ log(`${kept.originalFileName} ${kept.mediaId}`);
430
+ console.log(kept.file);
431
+ }
432
+ }
433
+ if (flags.json) console.log(JSON.stringify(results, null, 2));
434
+ else if (flags.temp) {
435
+ log(
436
+ "\nThese uploads are temporary. Seal them with `medialit seal <id>` or they are deleted after 24 hours."
437
+ );
438
+ }
439
+ }
440
+ async function list(flags) {
441
+ const { client } = await getClient(flags.endpoint);
442
+ const access = flags.public ? "public" : flags.private ? "private" : void 0;
443
+ const media = await client.list(
444
+ Number(flags.page || 1),
445
+ Number(flags.limit || 20),
446
+ { access, group: flags.group }
447
+ );
448
+ output(
449
+ flags,
450
+ media,
451
+ () => media.length ? table(
452
+ media.map((m) => [
453
+ m.mediaId,
454
+ m.access,
455
+ formatBytes(m.size),
456
+ m.originalFileName
457
+ ])
458
+ ) : "No files."
459
+ );
460
+ }
461
+ async function get(ids, flags) {
462
+ const { client } = await getClient(flags.endpoint);
463
+ const media = await client.get(
464
+ requireId(ids)
465
+ );
466
+ output(flags, media, () => media.file);
467
+ }
468
+ async function seal(ids, flags) {
469
+ const { client } = await getClient(flags.endpoint);
470
+ const media = await client.seal(
471
+ requireId(ids)
472
+ );
473
+ output(flags, media, () => media.file);
474
+ }
475
+ async function remove(ids, flags) {
476
+ const { client } = await getClient(flags.endpoint);
477
+ const id = requireId(ids);
478
+ await client.delete(id);
479
+ output(flags, { deleted: true, mediaId: id }, () => `Deleted ${id}.`);
480
+ }
481
+ function requireId(ids) {
482
+ if (ids.length !== 1) throw new Error("Pass one media ID.");
483
+ return ids[0];
484
+ }
485
+ function output(flags, data, human) {
486
+ console.log(flags.json ? JSON.stringify(data, null, 2) : human());
487
+ }
488
+ function log(message) {
489
+ console.error(message);
490
+ }
491
+ function progress(message) {
492
+ if (!process.stderr.isTTY) return;
493
+ process.stderr.write(`\r\x1B[2K${message}`);
494
+ }
495
+ function table(rows) {
496
+ const widths = rows[0].map(
497
+ (_, i) => Math.max(...rows.map((row) => row[i].length))
498
+ );
499
+ return rows.map(
500
+ (row) => row.map(
501
+ (cell, i) => i === row.length - 1 ? cell : cell.padEnd(widths[i])
502
+ ).join(" ")
503
+ ).join("\n");
504
+ }
505
+ function formatBytes(bytes) {
506
+ const units = ["B", "KB", "MB", "GB", "TB"];
507
+ let value = bytes;
508
+ let unit = 0;
509
+ while (value >= 1024 && unit < units.length - 1) {
510
+ value /= 1024;
511
+ unit++;
512
+ }
513
+ const rounded = value >= 10 || unit === 0 ? Math.round(value) : value.toFixed(1);
514
+ return `${rounded} ${units[unit]}`;
515
+ }
516
+ function openBrowser(url) {
517
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
518
+ try {
519
+ spawn(command, args, { stdio: "ignore", detached: true }).on("error", () => void 0).unref();
520
+ } catch {
521
+ }
522
+ }
523
+
524
+ // src/index.ts
525
+ var HELP = `Usage: medialit <command> [options]
526
+
527
+ Commands:
528
+ login Log in with your browser and choose an app
529
+ logout Log out of the server
530
+ whoami Show the server, account, app and storage used
531
+ upload <files...> Upload and seal files, then print their URLs
532
+ ls List sealed files, newest first
533
+ get <id> Print a file's URL (a fresh one for private files)
534
+ seal <id> Keep a temporary upload
535
+ rm <id> Delete a file
536
+
537
+ Options:
538
+ --endpoint <url> MediaLit server, for self-hosted instances
539
+ (default: the server you last logged in to,
540
+ or https://api.medialit.cloud)
541
+ --json Print JSON
542
+ --public upload: make files public. ls: only public files
543
+ --private ls: only private files
544
+ --caption <text> upload: caption for the files
545
+ --group <name> upload: put files in a group. ls: only that group
546
+ --temp upload: don't seal, so unused files are deleted
547
+ --page <n>, --limit <n> ls: page through files
548
+ --no-browser login: print the URL instead of opening it
549
+ -h, --help Show this help
550
+ -v, --version Show the version
551
+
552
+ Environment:
553
+ MEDIALIT_API_KEY Use an API key instead of logging in, for CI
554
+ MEDIALIT_ENDPOINT Same as --endpoint
555
+ `;
556
+ async function main(argv) {
557
+ const { values, positionals } = parseArgs({
558
+ args: argv,
559
+ allowPositionals: true,
560
+ options: {
561
+ endpoint: { type: "string" },
562
+ json: { type: "boolean" },
563
+ public: { type: "boolean" },
564
+ private: { type: "boolean" },
565
+ caption: { type: "string" },
566
+ group: { type: "string" },
567
+ temp: { type: "boolean" },
568
+ page: { type: "string" },
569
+ limit: { type: "string" },
570
+ "no-browser": { type: "boolean" },
571
+ help: { type: "boolean", short: "h" },
572
+ version: { type: "boolean", short: "v" }
573
+ }
574
+ });
575
+ const flags = {
576
+ ...values,
577
+ browser: !values["no-browser"]
578
+ };
579
+ const [command, ...args] = positionals;
580
+ if (values.version) {
581
+ const require2 = createRequire(import.meta.url);
582
+ console.log(require2("../package.json").version);
583
+ return;
584
+ }
585
+ if (values.help || !command) {
586
+ console.log(HELP);
587
+ return;
588
+ }
589
+ switch (command) {
590
+ case "login":
591
+ return login(flags);
592
+ case "logout":
593
+ return logout(flags);
594
+ case "whoami":
595
+ return whoami(flags);
596
+ case "upload":
597
+ return upload(args, flags);
598
+ case "ls":
599
+ case "list":
600
+ return list(flags);
601
+ case "get":
602
+ return get(args, flags);
603
+ case "seal":
604
+ return seal(args, flags);
605
+ case "rm":
606
+ case "delete":
607
+ return remove(args, flags);
608
+ default:
609
+ throw new Error(
610
+ `Unknown command "${command}". Run \`medialit --help\`.`
611
+ );
612
+ }
613
+ }
614
+ main(process.argv.slice(2)).catch((err) => {
615
+ console.error(`error: ${err.message}`);
616
+ process.exitCode = 1;
617
+ });
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@medialit/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Upload and manage MediaLit files from the terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "medialit": "dist/index.mjs"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "test": "bun --filter medialit --filter @medialit/uploader build && node --import tsx --test '__tests__/**/*.test.ts'",
15
+ "prepublishOnly": "bun run build"
16
+ },
17
+ "keywords": [
18
+ "medialit",
19
+ "cli",
20
+ "upload",
21
+ "storage"
22
+ ],
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/codelitdev/medialit"
27
+ },
28
+ "dependencies": {
29
+ "@medialit/uploader": "^0.1.0",
30
+ "medialit": "^0.2.0",
31
+ "mime-types": "^3.0.2"
32
+ },
33
+ "devDependencies": {
34
+ "@types/mime-types": "^3.0.1",
35
+ "@types/node": "^20.0.0",
36
+ "tsup": "^8.0.0",
37
+ "tsx": "^4.19.3",
38
+ "typescript": "^5.0.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "packageManager": "bun@1.4.1"
47
+ }