@flaredlink/cli 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +36 -16
  2. package/dist/flared.js +776 -39
  3. package/package.json +1 -4
package/README.md CHANGED
@@ -13,21 +13,41 @@ Create an API token in the Flared app under Settings. `flared login` asks for it
13
13
 
14
14
  ## Commands
15
15
 
16
- | Command | What it does |
17
- | -------------------------------------------------------------------------- | ------------------------------------------------------------------- |
18
- | `flared login [--token-stdin]` | Save an API token after the API accepts it. |
19
- | `flared logout` | Delete the saved token. Revoke it in Settings to stop it working. |
20
- | `flared whoami` | Show the token, its scopes, and the API URL. |
21
- | `flared link create URL [--slug S] [--title T] [--idempotency-key K]` | Create a link and print its short URL. Reuse a key to retry safely. |
22
- | `flared link list [--search Q] [--limit N] [--cursor C] [--all]` | List links, newest first. |
23
- | `flared link get LINK` | Show one link. |
24
- | `flared link update LINK [--destination URL] [--title T \| --clear-title]` | Change the destination or the title. |
25
- | `flared link disable LINK`, `flared link enable LINK` | Stop or restart redirects. A disabled link keeps its slug. |
26
- | `flared link qr LINK [--format svg\|png] [--out FILE]` | Make a QR code. SVG goes to standard output without `--out`. |
27
- | `flared analytics LINK [--from YYYY-MM-DD] [--to YYYY-MM-DD]` | Clicks by day, country, referrer, and device. |
28
- | `flared usage` | Recorded clicks this month and the allowance. |
29
-
30
- `LINK` is a link ID or a slug. Every command accepts `--json` for machine-readable output and `--api-url URL`.
16
+ | Command | What it does |
17
+ | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
18
+ | `flared login [--token-stdin]` | Save an API token after the API accepts it. |
19
+ | `flared logout` | Delete the saved token. Revoke it in Settings to stop it working. |
20
+ | `flared whoami` | Show the token, its scopes, and the API URL. |
21
+ | `flared link create URL [--slug S] [--title T] [--domain HOSTNAME] [--idempotency-key K]` | Create a link and print its short URL. Reuse a key to retry safely. |
22
+ | `flared link list [--search Q] [--limit N] [--cursor C] [--all]` | List links, newest first. |
23
+ | `flared link get LINK` | Show one link. |
24
+ | `flared link update LINK [--destination URL] [--title T \| --clear-title]` | Change the destination or the title. |
25
+ | `flared link disable LINK`, `flared link enable LINK` | Stop or restart redirects. A disabled link keeps its slug. |
26
+ | `flared link qr LINK [--format svg\|png] [--out FILE]` | Make a QR code. SVG goes to standard output without `--out`. |
27
+ | `flared domain list` | List domains, their status, and the DNS records to create. |
28
+ | `flared domain add HOSTNAME` | Add a subdomain you own and print the CNAME record to create. |
29
+ | `flared domain check DOMAIN` | Check the DNS record and the HTTPS certificate now. |
30
+ | `flared domain remove DOMAIN --yes` | Remove a domain. Its links stop redirecting until you add it again. |
31
+ | `flared analytics LINK [--from YYYY-MM-DD] [--to YYYY-MM-DD]` | Clicks by day, country, referrer, and device. |
32
+ | `flared usage` | Usage against the workspace limits, with warnings. |
33
+ | `flared export --out FILE` | Save every link and the retained daily analytics as one JSON file. |
34
+
35
+ `LINK` is a link ID or a slug. `DOMAIN` is a domain hostname or ID. Every command accepts `--json` for machine-readable output and `--api-url URL`.
36
+
37
+ ## Custom domains
38
+
39
+ Use a subdomain you own, such as `go.example.com`, for your short links:
40
+
41
+ ```sh
42
+ flared domain add go.example.com
43
+ # Create the CNAME record it prints, then:
44
+ flared domain check go.example.com
45
+ flared link create https://example.com/launch --domain go.example.com
46
+ ```
47
+
48
+ `domain add`, `check`, and `remove` need a token with the `domains:write` scope; `domain list` and `--domain` need `domains:read`. A domain serves links once its status is `active`. Without `--domain`, a link uses the default domain. `flared domain remove` tells you how many active links stop and needs `--yes` to go ahead. The links and their slugs stay reserved, so adding the domain again restores them.
49
+
50
+ `flared link qr` encodes the short URL: SVG by default, or a 512 × 512 PNG with `--format png --out FILE`.
31
51
 
32
52
  ## Self-hosted Flared
33
53
 
@@ -41,7 +61,7 @@ Point the CLI at your instance's API with `--api-url` or `FLARED_API_URL`, for e
41
61
  | 1 | Unexpected failure |
42
62
  | 2 | Wrong command usage |
43
63
  | 3 | Not signed in, or the token is invalid, expired, or revoked |
44
- | 4 | Not allowed, for example a missing scope |
64
+ | 4 | Not allowed, for example a missing scope or a blocked link |
45
65
  | 5 | Not found |
46
66
  | 6 | Rejected input or a conflict, such as a slug in use |
47
67
  | 7 | Plan limit reached |
package/dist/flared.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/main.ts
4
- import { writeFile as writeFile2 } from "node:fs/promises";
4
+ import { open, rename as rename2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
5
5
 
6
6
  // src/cli.ts
7
7
  import { parseArgs } from "node:util";
8
- import QRCode from "qrcode";
9
8
 
10
9
  // ../contracts/src/analytics.ts
11
10
  var clickEventMaxAgeMs = 48 * 60 * 60 * 1000;
12
11
  var clickEventMaxSkewMs = 5 * 60 * 1000;
12
+ var dayPattern = /^\d{4}-\d{2}-\d{2}$/;
13
13
  function isCount(value) {
14
14
  return Number.isSafeInteger(value) && value >= 0;
15
15
  }
@@ -26,7 +26,100 @@ function isUsage(value) {
26
26
  if (typeof value !== "object" || value === null)
27
27
  return false;
28
28
  const usage = value;
29
- return typeof usage.month === "string" && isCount(usage.clicks) && isCount(usage.clickLimit) && typeof usage.asOf === "string";
29
+ return typeof usage.month === "string" && isCount(usage.clicks) && isCount(usage.clickLimit) && isCount(usage.unrecordedClicks) && (usage.unrecordedSince === null || typeof usage.unrecordedSince === "string") && isLimitUsage(usage.links) && isLimitUsage(usage.domains) && isCount(usage.retentionDays) && Array.isArray(usage.warnings) && usage.warnings.every(isUsageWarning) && typeof usage.asOf === "string";
30
+ }
31
+ function isLimitUsage(value) {
32
+ if (typeof value !== "object" || value === null)
33
+ return false;
34
+ const usage = value;
35
+ return isCount(usage.used) && isCount(usage.limit);
36
+ }
37
+ function isUsageWarning(value) {
38
+ if (typeof value !== "object" || value === null)
39
+ return false;
40
+ const warning = value;
41
+ return (warning.resource === "clicks" || warning.resource === "links" || warning.resource === "domains") && (warning.level === 80 || warning.level === 100);
42
+ }
43
+
44
+ // ../contracts/src/links.ts
45
+ class LinkInputError extends Error {
46
+ field;
47
+ constructor(field, message) {
48
+ super(message);
49
+ this.field = field;
50
+ }
51
+ }
52
+ function isRecord(value) {
53
+ return typeof value === "object" && value !== null && !Array.isArray(value);
54
+ }
55
+ function isLink(value) {
56
+ if (!isRecord(value))
57
+ return false;
58
+ return typeof value.id === "string" && typeof value.domainId === "string" && typeof value.hostname === "string" && typeof value.slug === "string" && typeof value.shortUrl === "string" && typeof value.destination === "string" && (value.title === null || typeof value.title === "string") && typeof value.enabled === "boolean" && typeof value.createdAt === "string" && typeof value.updatedAt === "string";
59
+ }
60
+ function isListedLink(value) {
61
+ if (!isRecord(value) || !isLink(value))
62
+ return false;
63
+ const clicks = value.clicksLast30Days;
64
+ return clicks === null || Number.isSafeInteger(clicks) && clicks >= 0;
65
+ }
66
+ function isLinkPage(value) {
67
+ return isRecord(value) && Array.isArray(value.links) && value.links.every(isListedLink) && (value.nextCursor === null || typeof value.nextCursor === "string");
68
+ }
69
+
70
+ // ../contracts/src/domains.ts
71
+ var domainStates = ["pending", "verifying", "active", "failed", "disabled"];
72
+ var domainFailures = [
73
+ "dns_not_found",
74
+ "record_removed",
75
+ "certificate_failed",
76
+ "blocked",
77
+ "expired",
78
+ "provider_error"
79
+ ];
80
+ function isDomainFailure(value) {
81
+ return typeof value === "string" && domainFailures.includes(value);
82
+ }
83
+ var domainClaimLifetimeMs = 7 * 24 * 60 * 60 * 1000;
84
+ var domainCheckIntervalMs = 60 * 1000;
85
+ var blockedTopLevels = new Set(["localhost", "local", "internal", "invalid", "test", "example"]);
86
+ var labelPattern = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
87
+ function normalizeHostname(value) {
88
+ const invalid = (message) => new LinkInputError("hostname", message);
89
+ if (typeof value !== "string" || !value.trim())
90
+ throw invalid("Enter a hostname such as go.example.com.");
91
+ const trimmed = value.trim().toLowerCase().replace(/\.$/, "");
92
+ if (trimmed.length > 253 || /[\s/:?#@[\]*\\%]/.test(trimmed))
93
+ throw invalid("Enter only the hostname, such as go.example.com.");
94
+ let hostname;
95
+ try {
96
+ hostname = new URL(`https://${trimmed}`).hostname;
97
+ } catch {
98
+ throw invalid("Enter a valid hostname, such as go.example.com.");
99
+ }
100
+ const labels = hostname.split(".");
101
+ if (!labels.every((label) => labelPattern.test(label)) || hostname.length > 253)
102
+ throw invalid("Enter a valid hostname, such as go.example.com.");
103
+ if (labels.every((label) => /^\d+$/.test(label)) || /^\d+$/.test(labels[labels.length - 1]))
104
+ throw invalid("Use a hostname, not an IP address.");
105
+ if (blockedTopLevels.has(labels[labels.length - 1]))
106
+ throw invalid("Use a hostname on a public domain.");
107
+ if (labels.length < 3)
108
+ throw invalid("Use a subdomain such as go.example.com. Root domains are not supported.");
109
+ return hostname;
110
+ }
111
+ function isRecord2(value) {
112
+ return typeof value === "object" && value !== null && !Array.isArray(value);
113
+ }
114
+ var isCount2 = (value) => Number.isSafeInteger(value) && value >= 0;
115
+ function isDomain(value) {
116
+ if (!isRecord2(value))
117
+ return false;
118
+ const error = value.error;
119
+ return typeof value.id === "string" && typeof value.hostname === "string" && (value.kind === "platform" || value.kind === "workspace") && domainStates.includes(value.state) && typeof value.isDefault === "boolean" && Array.isArray(value.records) && value.records.every((record) => isRecord2(record) && record.type === "CNAME" && typeof record.name === "string" && typeof record.value === "string") && (error === null || isRecord2(error) && isDomainFailure(error.code) && typeof error.message === "string") && (value.activeLinks === null || isCount2(value.activeLinks)) && typeof value.createdAt === "string" && (value.activatedAt === null || typeof value.activatedAt === "string");
120
+ }
121
+ function isDomainPage(value) {
122
+ return isRecord2(value) && Array.isArray(value.domains) && value.domains.every(isDomain) && isCount2(value.used) && isCount2(value.limit);
30
123
  }
31
124
 
32
125
  // ../contracts/src/errors.ts
@@ -38,6 +131,10 @@ var errorStatus = {
38
131
  DEFAULT_DOMAIN_UNAVAILABLE: 409,
39
132
  DOMAIN_UNAVAILABLE: 422,
40
133
  PLAN_LIMIT_REACHED: 403,
134
+ DOMAIN_TAKEN: 409,
135
+ DOMAIN_LIMIT_REACHED: 403,
136
+ DOMAINS_UNAVAILABLE: 503,
137
+ DOMAIN_CHECK_TOO_SOON: 429,
41
138
  RATE_LIMITED: 429,
42
139
  UNAUTHENTICATED: 401,
43
140
  ORIGIN_REJECTED: 403,
@@ -46,6 +143,9 @@ var errorStatus = {
46
143
  TOKEN_LIMIT_REACHED: 403,
47
144
  NO_WORKSPACE: 403,
48
145
  WORKSPACE_PENDING: 503,
146
+ ACCOUNT_DELETING: 409,
147
+ WORKSPACE_SUSPENDED: 403,
148
+ LINK_BLOCKED: 409,
49
149
  NOT_FOUND: 404,
50
150
  METHOD_NOT_ALLOWED: 405,
51
151
  PAYLOAD_TOO_LARGE: 413,
@@ -56,23 +156,38 @@ function isErrorCode(value) {
56
156
  return typeof value === "string" && Object.hasOwn(errorStatus, value);
57
157
  }
58
158
 
59
- // ../contracts/src/links.ts
60
- function isRecord(value) {
159
+ // ../contracts/src/export.ts
160
+ var exportFormat = "flared.export/1";
161
+ var exportDimensions = ["country", "device", "referrer"];
162
+ function isRecord3(value) {
61
163
  return typeof value === "object" && value !== null && !Array.isArray(value);
62
164
  }
63
- function isLink(value) {
64
- if (!isRecord(value))
65
- return false;
66
- return typeof value.id === "string" && typeof value.domainId === "string" && typeof value.hostname === "string" && typeof value.slug === "string" && typeof value.shortUrl === "string" && typeof value.destination === "string" && (value.title === null || typeof value.title === "string") && typeof value.enabled === "boolean" && typeof value.createdAt === "string" && typeof value.updatedAt === "string";
165
+ function isCount3(value) {
166
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
67
167
  }
68
- function isListedLink(value) {
69
- if (!isRecord(value) || !isLink(value))
70
- return false;
71
- const clicks = value.clicksLast30Days;
72
- return clicks === null || Number.isSafeInteger(clicks) && clicks >= 0;
168
+ function isDay(value) {
169
+ return typeof value === "string" && dayPattern.test(value);
73
170
  }
74
- function isLinkPage(value) {
75
- return isRecord(value) && Array.isArray(value.links) && value.links.every(isListedLink) && (value.nextCursor === null || typeof value.nextCursor === "string");
171
+ function isCursor(value) {
172
+ return value === null || typeof value === "string";
173
+ }
174
+ function isExportLinkPage(value) {
175
+ return isRecord3(value) && Array.isArray(value.links) && value.links.every(isLink) && isCursor(value.nextCursor);
176
+ }
177
+ function isAnalyticsPage(value, isRow) {
178
+ return isRecord3(value) && isDay(value.from) && isCount3(value.retentionDays) && Array.isArray(value.rows) && value.rows.every(isRow) && isCursor(value.nextCursor);
179
+ }
180
+ function isTotal(value) {
181
+ return isRecord3(value) && typeof value.linkId === "string" && isDay(value.day) && isCount3(value.clicks);
182
+ }
183
+ function isDimension(value) {
184
+ return isTotal(value) && isRecord3(value) && exportDimensions.some((dimension) => dimension === value.dimension) && typeof value.value === "string";
185
+ }
186
+ function isExportTotalsPage(value) {
187
+ return isAnalyticsPage(value, isTotal);
188
+ }
189
+ function isExportDimensionsPage(value) {
190
+ return isAnalyticsPage(value, isDimension);
76
191
  }
77
192
 
78
193
  // ../contracts/src/tokens.ts
@@ -81,11 +196,12 @@ var tokenScopes = [
81
196
  "links:write",
82
197
  "analytics:read",
83
198
  "domains:read",
199
+ "domains:write",
84
200
  "usage:read"
85
201
  ];
86
202
  var scopePresets = {
87
203
  full: [...tokenScopes],
88
- read: tokenScopes.filter((scope) => scope !== "links:write")
204
+ read: tokenScopes.filter((scope) => scope.endsWith(":read"))
89
205
  };
90
206
  var tokenPrefix = "flr_";
91
207
  function isTokenScope(value) {
@@ -187,6 +303,8 @@ function createClient(options) {
187
303
  }
188
304
  throw new FlaredApiError("NETWORK_ERROR", "Could not reach the Flared API.", null);
189
305
  }
306
+ if (response.status === 204)
307
+ return { status: 204, headers: response.headers, body: null };
190
308
  if (response.ok) {
191
309
  const body = await response.json().catch(() => {
192
310
  return;
@@ -206,6 +324,20 @@ function createClient(options) {
206
324
  }
207
325
  }
208
326
  const id = (value) => encodeURIComponent(value);
327
+ const after = (cursor) => cursor ? `?cursor=${encodeURIComponent(cursor)}` : "";
328
+ async function page(path, valid) {
329
+ const { status, body } = await request("GET", path, { retry: true });
330
+ if (!valid(body))
331
+ throw invalid(status);
332
+ return body;
333
+ }
334
+ async function domainFrom(promise) {
335
+ const { status, body } = await promise;
336
+ const domain = record(body)?.domain;
337
+ if (!isDomain(domain))
338
+ throw invalid(status);
339
+ return domain;
340
+ }
209
341
  async function linkFrom(promise) {
210
342
  const { status, body } = await promise;
211
343
  const link = record(body)?.link;
@@ -273,10 +405,424 @@ function createClient(options) {
273
405
  if (!isUsage(usage))
274
406
  throw invalid(status);
275
407
  return usage;
408
+ },
409
+ async listDomains() {
410
+ const { status, body } = await request("GET", "/domains", { retry: true });
411
+ if (!isDomainPage(body))
412
+ throw invalid(status);
413
+ return body;
414
+ },
415
+ async addDomain(hostname) {
416
+ const response = request("POST", "/domains", { body: { hostname }, retry: true });
417
+ const { status } = await response;
418
+ return { domain: await domainFrom(response), created: status === 201 };
419
+ },
420
+ checkDomain(domainId) {
421
+ return domainFrom(request("POST", `/domains/${id(domainId)}/check`, { retry: false }));
422
+ },
423
+ async removeDomain(domainId) {
424
+ await request("DELETE", `/domains/${id(domainId)}`, { retry: false });
425
+ },
426
+ exportLinks(cursor) {
427
+ return page(`/export/links${after(cursor)}`, isExportLinkPage);
428
+ },
429
+ exportDailyTotals(cursor) {
430
+ return page(`/export/daily-totals${after(cursor)}`, isExportTotalsPage);
431
+ },
432
+ exportDailyDimensions(cursor) {
433
+ return page(`/export/daily-dimensions${after(cursor)}`, isExportDimensionsPage);
276
434
  }
277
435
  };
278
436
  }
279
437
 
438
+ // ../client/src/export.ts
439
+ async function writeExport(source, write, options = {}) {
440
+ const progress = { links: 0, dailyTotals: 0, dailyDimensions: 0 };
441
+ const exportedAt = new Date((options.now ?? Date.now)()).toISOString();
442
+ await write(`{"format":${JSON.stringify(exportFormat)},"exportedAt":"${exportedAt}"`);
443
+ async function section(name, read) {
444
+ await write(`,"${name}":[`);
445
+ let cursor;
446
+ do {
447
+ options.signal?.throwIfAborted();
448
+ const page = await read(cursor);
449
+ if (page.rows.length > 0)
450
+ await write((progress[name] > 0 ? "," : "") + page.rows.map((row) => JSON.stringify(row)).join(","));
451
+ progress[name] += page.rows.length;
452
+ options.onProgress?.({ ...progress });
453
+ cursor = page.nextCursor ?? undefined;
454
+ } while (cursor);
455
+ await write("]");
456
+ }
457
+ await section("links", async (cursor) => {
458
+ const page = await source.exportLinks(cursor);
459
+ return { rows: page.links, nextCursor: page.nextCursor };
460
+ });
461
+ let retained = null;
462
+ await section("dailyTotals", async (cursor) => {
463
+ const page = await source.exportDailyTotals(cursor);
464
+ retained ??= { from: page.from, retentionDays: page.retentionDays };
465
+ return page;
466
+ });
467
+ await section("dailyDimensions", (cursor) => source.exportDailyDimensions(cursor));
468
+ const { from, retentionDays } = retained ?? { from: null, retentionDays: null };
469
+ await write(`,"analyticsFrom":${JSON.stringify(from)},"retentionDays":${JSON.stringify(retentionDays)}}
470
+ `);
471
+ return progress;
472
+ }
473
+
474
+ // ../../node_modules/.bun/lean-qr@2.7.4/node_modules/lean-qr/index.mjs
475
+ var t = [0.2, 3 / 8, 5 / 9, 2 / 3];
476
+ var o = (o2, e) => (r) => {
477
+ const n = 4 * o2 + r - 4, s = "*-04-39?2$%%$%%'$%''%'''%')(%'))%(++'(++'(+.'+-.',/3',33)-/5)-43).36)058*18<+37<+4:<,4:E,5<A-7>C/8@F/:EH/<EK0=FM1?IP2@KS3BNV4DPY5FS\\6HV_6IXb7K[e8N^i9Pam;Rdp<Tgt".charCodeAt(n) - 35, f = n > 8 ? s : 1, c = e / f | 0, i = e % f, l = f - i, a = n > 8 ? c * t[r] + (o2 > 5) & -2 : s, _ = c - a;
478
+ return { t: l * _ + i * _ + i, o: [[l, _], [i, _ + 1]], i: a };
479
+ };
480
+ var e = { min: 0, L: 0, M: 1, Q: 2, H: 3, max: 3 };
481
+ var r = (t2) => new Uint8Array(t2);
482
+ var n = (t2) => {
483
+ const o2 = new Error(`lean-qr error ${t2}`);
484
+ throw o2.code = t2, o2;
485
+ };
486
+ var s = (t2) => "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:".indexOf(t2);
487
+ var f = (t2) => t2.charCodeAt(0);
488
+ var c = (...t2) => (o2, e2) => t2.forEach((t3) => t3(o2, e2));
489
+ var i = (t2) => (o2) => {
490
+ o2.eci !== t2 && (o2.push(7, 4), t2 < 128 ? o2.push(t2, 8) : t2 < 16384 ? o2.push(32768 | t2, 16) : o2.push(12582912 | t2, 24), o2.eci = t2);
491
+ };
492
+ var l = (t2) => (o2, e2) => {
493
+ o2.push(4, 4), o2.push(t2.length, 8 + 8 * (e2 > 9)), t2.forEach((t3) => o2.push(t3, 8));
494
+ };
495
+ var a = (t2, o2, e2, r2, n2 = (t3, o3) => e2(t3.length, o3), s2 = r2 ? (o3) => c(i(r2), t2(o3)) : t2) => (s2.test = o2, s2.l = e2, s2.est = n2, s2.eci = r2 && [r2], s2);
496
+ var _ = a((t2) => (o2, e2) => {
497
+ o2.push(1, 4), o2.push(t2.length, 10 + 2 * (e2 > 26) + 2 * (e2 > 9));
498
+ let r2 = 0;
499
+ for (;r2 < t2.length - 2; r2 += 3)
500
+ o2.push(+t2.slice(r2, r2 + 3), 10);
501
+ r2 < t2.length - 1 ? o2.push(+t2.slice(r2), 7) : r2 < t2.length && o2.push(+t2[r2], 4);
502
+ }, (t2) => /[0-9]/.test(t2), (t2, o2) => 14 + 2 * (o2 > 26) + 2 * (o2 > 9) + 10 * t2 / 3);
503
+ var u = a((t2) => (o2, e2) => {
504
+ o2.push(2, 4), o2.push(t2.length, 9 + 2 * (e2 > 26) + 2 * (e2 > 9));
505
+ let r2 = 0;
506
+ for (;r2 < t2.length - 1; r2 += 2)
507
+ o2.push(45 * s(t2[r2]) + s(t2[r2 + 1]), 11);
508
+ r2 < t2.length && o2.push(s(t2[r2]), 6);
509
+ }, (t2) => s(t2) >= 0, (t2, o2) => 13 + 2 * (o2 > 26) + 2 * (o2 > 9) + 5.5 * t2);
510
+ var d = a((t2) => l([...t2].map(f)), (t2) => f(t2) < 128, (t2, o2) => 12 + 8 * (o2 > 9) + 8 * t2);
511
+ d._ = true, d.u = true;
512
+ var p = a(d, (t2) => f(t2) < 256, d.l, 3);
513
+ p._ = true;
514
+ var m = new TextEncoder;
515
+ var h = a((t2) => l(m.encode(t2)), () => 1, 0, 26, (t2, o2) => 12 + 8 * (o2 > 9) + 8 * m.encode(t2).length);
516
+ h._ = true;
517
+ var w = () => {
518
+ const t2 = new Map, o2 = new TextDecoder("sjis"), e2 = r(2);
519
+ for (let r2 = 0;r2 < 7973; ++r2)
520
+ e2[0] = r2 / 192 + 129 + 64 * (r2 > 5951), e2[1] = r2 % 192 + 64, t2.set(o2.decode(e2), r2);
521
+ return t2.delete("�"), w = () => t2, t2;
522
+ };
523
+ var y = a((t2) => (o2, e2) => {
524
+ o2.push(8, 4), o2.push(t2.length, 8 + 2 * (e2 > 26) + 2 * (e2 > 9));
525
+ for (const e3 of t2)
526
+ o2.push(w().get(e3), 13);
527
+ }, (t2) => w().has(t2), (t2, o2) => 12 + 2 * (o2 > 26) + 2 * (o2 > 9) + 13 * t2);
528
+ y._ = true;
529
+ var g = [_, u, d, p, y, h];
530
+ var b = { auto: (t2, { modes: o2 = g } = {}) => (e2, r2) => {
531
+ const s2 = o2.map((o3, e3) => {
532
+ const n2 = new Map, s3 = (t3, o4) => (n2.has(t3) || n2.set(t3, o4(t3, r2)), n2.get(t3));
533
+ return { m: o3, h: 1 << e3, C: o3.est("", r2), S: o3.l ? (t3, e4) => s3(e4 - t3, o3.l) : (e4, r3) => s3(t2.slice(e4, r3), o3.est) };
534
+ });
535
+ let f2 = [{ v: 0 }], c2 = 0, i2 = 0, l2 = -1;
536
+ for (const o3 of [...t2, ""]) {
537
+ let t3 = 0;
538
+ if (o3)
539
+ for (const e3 of s2)
540
+ e3.m.test(o3) && (t3 |= e3.h);
541
+ if (!o3 || t3 !== l2) {
542
+ if (l2 !== -1) {
543
+ const t4 = new Set(f2.map((t5) => t5.D)), o4 = [];
544
+ for (const { m: e3, C: r3, S: n2, h: a3 } of s2)
545
+ if (l2 & a3) {
546
+ const s3 = n2(c2, i2);
547
+ for (const l3 of e3.eci ?? t4)
548
+ if (!e3.u || !l3) {
549
+ let t5;
550
+ for (const o5 of f2)
551
+ if (o5.D === l3 || e3.eci) {
552
+ const f3 = o5.m === e3 && o5.D === l3, a4 = f3 ? o5.V : o5, _2 = e3._ && f3 ? o5.v + s3 - r3 : a4.v + 12 * (a4.D !== l3) + (f3 ? n2(f3 ? o5.$ : c2, i2) : s3);
553
+ (!t5 || _2 < t5.v) && (t5 = { $: f3 ? o5.$ : c2, V: a4, m: e3, D: l3, A: i2, v: _2 });
554
+ }
555
+ t5 && o4.push(t5);
556
+ }
557
+ }
558
+ f2 = o4;
559
+ }
560
+ l2 = t3, c2 = i2;
561
+ }
562
+ i2 += o3.length;
563
+ }
564
+ f2.length || n(5);
565
+ const a2 = [];
566
+ for (let o3 = f2.reduce((t3, o4) => o4.v < t3.v ? o4 : t3);o3.m; o3 = o3.V)
567
+ a2.push(o3.m(t2.slice(o3.$, o3.A)));
568
+ a2.reverse().forEach((t3) => t3(e2, r2));
569
+ }, multi: c, eci: i, bytes: l, numeric: _, alphaNumeric: u, ascii: d, iso8859_1: p, shift_jis: y, utf8: h };
570
+ var C = () => ({ F: r(2956), I: 0, push(t2, o2) {
571
+ for (let e2 = o2, r2 = 8 - (7 & this.I);e2 > 0; e2 -= r2, r2 = 8)
572
+ this.F[this.I >> 3] |= t2 << r2 >> e2, this.I += e2 < r2 ? e2 : r2;
573
+ } });
574
+ var x = (t2, o2 = t2 * t2, e2 = r(o2)) => ({ size: t2, K: e2, get: (o3, r2) => o3 >= 0 && o3 < t2 && !!(1 & e2[r2 * t2 + o3]), toString({ on: o3 = "##", off: e3 = " ", lf: r2 = `
575
+ `, pad: n2 = 4, padX: s2 = n2, padY: f2 = n2 } = {}) {
576
+ let c2 = "";
577
+ for (let n3 = -f2;n3 < t2 + f2; ++n3) {
578
+ for (let r3 = -s2;r3 < t2 + s2; ++r3)
579
+ c2 += this.get(r3, n3) ? o3 : e3;
580
+ c2 += r2;
581
+ }
582
+ return c2;
583
+ }, toImageData(o3, { on: e3 = [0, 0, 0], off: r2 = [0, 0, 0, 0], pad: n2 = 4, padX: s2 = n2, padY: f2 = n2 } = {}) {
584
+ const c2 = t2 + 2 * s2, i2 = t2 + 2 * f2, l2 = o3.createImageData(c2, i2), a2 = new Uint32Array(l2.data.buffer);
585
+ l2.data.set([...e3, 255]);
586
+ const _2 = a2[0];
587
+ l2.data.set([...r2, 255]);
588
+ const u2 = a2[0];
589
+ for (let t3 = 0;t3 < i2; ++t3)
590
+ for (let o4 = 0;o4 < c2; ++o4)
591
+ a2[t3 * c2 + o4] = this.get(o4 - s2, t3 - f2) ? _2 : u2;
592
+ return l2;
593
+ }, toCanvas(t3, o3) {
594
+ const e3 = t3.getContext("2d"), r2 = this.toImageData(e3, o3);
595
+ t3.width = r2.width, t3.height = r2.height, e3.putImageData(r2, 0, 0);
596
+ }, toDataURL({ type: t3 = "image/png", scale: o3 = 1, ...e3 } = {}) {
597
+ const r2 = document.createElement("canvas"), n2 = r2.getContext("2d"), s2 = this.toImageData(n2, e3);
598
+ return r2.width = s2.width * o3, r2.height = s2.height * o3, n2.putImageData(s2, 0, 0), n2.imageSmoothingEnabled = false, n2.globalCompositeOperation = "copy", n2.drawImage(r2, 0, 0, s2.width, s2.height, 0, 0, r2.width, r2.height), r2.toDataURL(t3, 1);
599
+ } });
600
+ var z = [(t2, o2) => 1 & (t2 ^ o2), (t2, o2) => 1 & o2, (t2) => t2 % 3, (t2, o2) => (t2 + o2) % 3, (t2, o2) => 1 & (t2 / 3 ^ o2 >> 1), (t2, o2) => (t2 & o2 & 1) + t2 * o2 % 3, (t2, o2) => (t2 & o2) + t2 * o2 % 3 & 1, (t2, o2) => (t2 ^ o2) + t2 * o2 % 3 & 1];
601
+ var E = r(511);
602
+ for (let t2 = 0, o2 = 1;t2 < 255; o2 = 2 * o2 ^ 285 * (o2 > 127))
603
+ E[E[o2 + 255] = t2++] = o2;
604
+ var M = (t2) => E[t2 % 255];
605
+ var S = (t2) => E[t2 + 255];
606
+ var v = (t2, o2) => {
607
+ const e2 = r(t2.length + o2.length - 1);
608
+ for (let r2 = 0;r2 < t2.length; ++r2)
609
+ for (let n2 = 0;n2 < o2.length; ++n2)
610
+ e2[r2 + n2] ^= M(t2[r2] + o2[n2]);
611
+ return e2.map(S);
612
+ };
613
+ var D = (t2, o2) => {
614
+ const e2 = r(t2.length + o2.length - 1);
615
+ e2.set(t2, 0);
616
+ for (let r2 = 0;r2 < t2.length; ++r2)
617
+ if (e2[r2]) {
618
+ const t3 = S(e2[r2]);
619
+ for (let n2 = 0;n2 < o2.length; ++n2)
620
+ e2[r2 + n2] ^= M(o2[n2] + t3);
621
+ }
622
+ return e2.slice(t2.length);
623
+ };
624
+ var L = [[0], [0, 0]];
625
+ for (let t2 = 1;t2 < 30; ++t2)
626
+ L.push(v(L[t2], [0, t2]));
627
+ var V = (t2, o2) => {
628
+ const e2 = [[], []];
629
+ let n2 = 0, s2 = 0;
630
+ for (const [r2, f3] of o2.o)
631
+ for (let c2 = 0;c2 < r2; ++c2, n2 += f3) {
632
+ const r3 = t2.slice(n2, n2 + f3);
633
+ e2[0].push(r3), e2[1].push(D(r3, L[o2.i])), s2 += f3 + o2.i;
634
+ }
635
+ const f2 = r(s2);
636
+ s2 = 0;
637
+ for (const t3 of e2)
638
+ for (let o3 = 0, e3 = -1;e3 < s2; ++o3) {
639
+ e3 = s2;
640
+ for (const e4 of t3)
641
+ o3 < e4.length && (f2[s2++] = e4[o3]);
642
+ }
643
+ return f2;
644
+ };
645
+ var $ = (t2, o2, e2) => {
646
+ let r2 = t2 <<= e2;
647
+ for (let t3 = 134217728;t3 >>= 1; )
648
+ r2 & t3 && (r2 ^= o2 * (t3 >> e2));
649
+ return r2 | t2;
650
+ };
651
+ var A = ({ size: t2, K: o2 }, e2) => {
652
+ const r2 = (e3, r3, n3, s3) => {
653
+ for (;n3-- > 0; e3 += t2)
654
+ o2.fill(s3, e3, e3 + r3);
655
+ }, n2 = (o3, e3, n3) => {
656
+ for (let s3 = 0;s3++ < 3; n3 -= 2)
657
+ r2(e3 * t2 + o3 - (n3 >> 1) * (t2 + 1), n3, n3, 2 | s3);
658
+ }, s2 = 2 * ((t2 - 13) / (1 + (e2 / 7 | 0)) / 2 + 0.75 | 0);
659
+ if (e2 > 1)
660
+ for (let o3 = t2 - 7;o3 > 8; o3 -= s2) {
661
+ for (let t3 = o3;t3 > 8; t3 -= s2)
662
+ n2(o3, t3, 5);
663
+ o3 < t2 - 7 && n2(o3, 6, 5);
664
+ }
665
+ if (e2 > 6)
666
+ for (let r3 = $(e2, 7973, 12), n3 = 1;n3 < 7; ++n3)
667
+ for (let e3 = 12;e3-- > 9; r3 >>= 1)
668
+ o2[n3 * t2 - e3] = 2 | 1 & r3;
669
+ r2(7, 2, 9, 2), r2(t2 - 8, 8, 9, 2);
670
+ for (let e3 = 0;e3 < t2; ++e3)
671
+ o2[6 * t2 + e3] = 3 ^ 1 & e3;
672
+ n2(3, 3, 7), n2(t2 - 4, 3, 7);
673
+ for (let e3 = 0;e3 < t2; ++e3)
674
+ for (let r3 = e3;r3 < t2; ++r3)
675
+ o2[r3 * t2 + e3] = o2[e3 * t2 + r3];
676
+ o2[(t2 - 8) * t2 + 8] = 3;
677
+ };
678
+ var F = ({ size: t2, K: o2 }) => {
679
+ const e2 = [];
680
+ for (let r2 = t2 - 2, n2 = t2, s2 = -1;r2 >= 0; r2 -= 2) {
681
+ for (r2 === 5 && (r2 = 4);n2 += s2, n2 !== -1 && n2 !== t2; ) {
682
+ const s3 = n2 * t2 + r2;
683
+ o2[s3 + 1] || e2.push(s3 + 1), o2[s3] || e2.push(s3);
684
+ }
685
+ s2 *= -1;
686
+ }
687
+ return e2;
688
+ };
689
+ var H = ({ K: t2 }, o2, e2) => o2.forEach((o3, r2) => t2[o3] = e2[r2 >> 3] >> (7 & ~r2) & 1);
690
+ var I = ({ size: t2, K: o2 }, e2, r2, n2) => {
691
+ for (let r3 = 0;r3 < t2; ++r3)
692
+ for (let n3 = 0;n3 < t2; ++n3) {
693
+ const s3 = r3 * t2 + n3;
694
+ o2[s3] ^= !(e2(n3, r3) || 2 & o2[s3]);
695
+ }
696
+ let s2 = 21522 ^ $((1 ^ n2) << 3 | r2, 1335, 10);
697
+ for (let e3 = 0;e3++ < 8; s2 >>= 1)
698
+ o2[(e3 - (e3 < 7)) * t2 + 8] = 1 & s2, o2[9 * t2 - e3] = 1 & s2;
699
+ for (let e3 = 8;--e3, s2; s2 >>= 1)
700
+ o2[8 * t2 + e3 - (e3 < 7)] = 1 & s2, o2[(t2 - e3) * t2 + 8] = 1 & s2;
701
+ };
702
+ var K = ({ size: t2, K: o2 }, e2 = 0, r2 = 0) => {
703
+ for (let n2 = 0;n2 < t2; ++n2) {
704
+ for (let s2 = 0;s2 < 2; ++s2)
705
+ for (let f2, c2 = 0, i2 = 0, l2 = 2;c2 < t2; ++c2) {
706
+ const a2 = 1 & o2[s2 ? n2 * t2 + c2 : c2 * t2 + n2];
707
+ r2 += a2, i2 = (i2 >> 1 | 2098176) & (3047517 ^ a2 - 1), 2049 & i2 && (e2 += 40), a2 ^ l2 && (f2 = 0), l2 = a2, e2 += ++f2 === 5 ? 3 : f2 > 5;
708
+ }
709
+ if (n2)
710
+ for (let r3 = t2 + n2, s2 = 5 * o2[n2 - 1] ^ o2[n2];r3 < t2 * t2; r3 += t2) {
711
+ const t3 = 5 * o2[r3 - 1] ^ o2[r3];
712
+ e2 += 3 * !(1 & (s2 | t3) | 4 & (s2 ^ t3)), s2 = t3;
713
+ }
714
+ }
715
+ return e2 + 10 * (10 * Math.abs(r2 / (t2 * t2) - 1) | 0);
716
+ };
717
+ var N = [];
718
+ var P = (t2 = n(1), { minCorrectionLevel: e2 = 0, maxCorrectionLevel: r2 = 3, minVersion: s2 = 1, maxVersion: f2 = 40, mask: c2, trailer: i2 = 60433, ...l2 } = {}) => {
719
+ r2 < e2 && n(3), f2 < s2 && n(2), typeof t2 == "string" && (t2 = b.auto(t2, l2));
720
+ for (let n2 = s2, l3 = 0;n2 <= f2; ++n2) {
721
+ let s3 = N[n2];
722
+ s3 || (N[n2] = s3 = x(4 * n2 + 17), A(s3, n2), s3.p = F(s3));
723
+ const f3 = o(n2, s3.p.length >> 3);
724
+ if (8 * f3(e2).t < l3)
725
+ continue;
726
+ const a2 = C();
727
+ t2(a2, n2), l3 = a2.I;
728
+ for (let t3 = r2;t3 >= e2; --t3) {
729
+ const o2 = f3(t3);
730
+ if (8 * o2.t < l3)
731
+ continue;
732
+ for (a2.I = l3 + 11 & -8;a2.I < 8 * o2.t; )
733
+ a2.push(i2, 16);
734
+ const e3 = x(s3.size, s3.K);
735
+ return H(e3, s3.p, V(a2.F, o2)), (z[c2] ? [z[c2]] : z).map((o3, r3) => {
736
+ const n3 = x(e3.size, e3.K);
737
+ return I(n3, o3, c2 ?? r3, t3), n3.s = z[c2] ? 0 : K(n3), n3;
738
+ }).reduce((t4, o3) => o3.s < t4.s ? o3 : t4);
739
+ }
740
+ }
741
+ n(4);
742
+ };
743
+ P.with = (...t2) => (o2, e2) => P(o2, { modes: [...g, ...t2], ...e2 });
744
+
745
+ // ../client/src/qr.ts
746
+ var defaultQrSize = 512;
747
+ var quietZone = 4;
748
+ function qrMatrix(text) {
749
+ return P(text, { minCorrectionLevel: e.M });
750
+ }
751
+ function qrSvg(text, size = defaultQrSize) {
752
+ const code = qrMatrix(text);
753
+ const span = code.size + quietZone * 2;
754
+ let path = "";
755
+ for (let y2 = 0;y2 < code.size; y2 += 1)
756
+ for (let x2 = 0;x2 < code.size; x2 += 1)
757
+ if (code.get(x2, y2))
758
+ path += `M${x2 + quietZone} ${y2 + quietZone}h1v1h-1z`;
759
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${span} ${span}" width="${size}" height="${size}" shape-rendering="crispEdges"><rect width="${span}" height="${span}" fill="#fff"/><path d="${path}" fill="#000"/></svg>`;
760
+ }
761
+ async function qrPng(text, size = defaultQrSize) {
762
+ const code = qrMatrix(text);
763
+ const span = code.size + quietZone * 2;
764
+ const scale = Math.max(1, Math.floor(size / span));
765
+ const width = Math.max(size, span * scale);
766
+ const offset = Math.floor((width - code.size * scale) / 2);
767
+ const rowBytes = Math.ceil(width / 8);
768
+ const raw = new Uint8Array((rowBytes + 1) * width);
769
+ for (let y2 = 0;y2 < width; y2 += 1) {
770
+ const row = y2 * (rowBytes + 1);
771
+ const moduleY = Math.floor((y2 - offset) / scale);
772
+ for (let x2 = 0;x2 < width; x2 += 1) {
773
+ const moduleX = Math.floor((x2 - offset) / scale);
774
+ const dark = x2 >= offset && y2 >= offset && moduleX < code.size && moduleY < code.size && code.get(moduleX, moduleY);
775
+ if (!dark)
776
+ raw[row + 1 + (x2 >> 3)] |= 128 >> (x2 & 7);
777
+ }
778
+ }
779
+ const header = new Uint8Array(13);
780
+ const view = new DataView(header.buffer);
781
+ view.setUint32(0, width);
782
+ view.setUint32(4, width);
783
+ header.set([1, 0, 0, 0, 0], 8);
784
+ return concat([
785
+ new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
786
+ chunk("IHDR", header),
787
+ chunk("IDAT", await deflate(raw)),
788
+ chunk("IEND", new Uint8Array(0))
789
+ ]);
790
+ }
791
+ async function deflate(data) {
792
+ const stream = new Blob([data]).stream().pipeThrough(new CompressionStream("deflate"));
793
+ return new Uint8Array(await new Response(stream).arrayBuffer());
794
+ }
795
+ function chunk(type, data) {
796
+ const out = new Uint8Array(12 + data.length);
797
+ const view = new DataView(out.buffer);
798
+ view.setUint32(0, data.length);
799
+ out.set(new TextEncoder().encode(type), 4);
800
+ out.set(data, 8);
801
+ view.setUint32(8 + data.length, crc32(out.subarray(4, 8 + data.length)));
802
+ return out;
803
+ }
804
+ var crcTable = Array.from({ length: 256 }, (_2, n2) => {
805
+ let c2 = n2;
806
+ for (let k = 0;k < 8; k += 1)
807
+ c2 = c2 & 1 ? 3988292384 ^ c2 >>> 1 : c2 >>> 1;
808
+ return c2 >>> 0;
809
+ });
810
+ function crc32(bytes) {
811
+ let c2 = 4294967295;
812
+ for (const byte of bytes)
813
+ c2 = crcTable[(c2 ^ byte) & 255] ^ c2 >>> 8;
814
+ return (c2 ^ 4294967295) >>> 0;
815
+ }
816
+ function concat(parts) {
817
+ const out = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));
818
+ let at = 0;
819
+ for (const part of parts) {
820
+ out.set(part, at);
821
+ at += part.length;
822
+ }
823
+ return out;
824
+ }
825
+
280
826
  // src/config.ts
281
827
  import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
282
828
  import { homedir } from "node:os";
@@ -301,6 +847,9 @@ function exitCodeFor(code) {
301
847
  return exitCodes.unauthenticated;
302
848
  case "INSUFFICIENT_SCOPE":
303
849
  case "NO_WORKSPACE":
850
+ case "ACCOUNT_DELETING":
851
+ case "WORKSPACE_SUSPENDED":
852
+ case "LINK_BLOCKED":
304
853
  case "ORIGIN_REJECTED":
305
854
  case "REAUTH_REQUIRED":
306
855
  return exitCodes.forbidden;
@@ -312,13 +861,17 @@ function exitCodeFor(code) {
312
861
  case "IDEMPOTENCY_KEY_REQUIRED":
313
862
  case "DEFAULT_DOMAIN_UNAVAILABLE":
314
863
  case "DOMAIN_UNAVAILABLE":
864
+ case "DOMAIN_TAKEN":
315
865
  return exitCodes.rejected;
316
866
  case "PLAN_LIMIT_REACHED":
317
867
  case "TOKEN_LIMIT_REACHED":
868
+ case "DOMAIN_LIMIT_REACHED":
318
869
  return exitCodes.planLimit;
319
870
  case "RATE_LIMITED":
871
+ case "DOMAIN_CHECK_TOO_SOON":
320
872
  return exitCodes.rateLimited;
321
873
  case "SERVICE_UNAVAILABLE":
874
+ case "DOMAINS_UNAVAILABLE":
322
875
  case "WORKSPACE_PENDING":
323
876
  case "NETWORK_ERROR":
324
877
  return exitCodes.unavailable;
@@ -411,7 +964,7 @@ function resolveToken(env, saved) {
411
964
 
412
965
  // src/output.ts
413
966
  function table(rows) {
414
- const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
967
+ const widths = rows[0].map((_2, column) => Math.max(...rows.map((row) => row[column].length)));
415
968
  return rows.map((row) => row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ")).join(`
416
969
  `);
417
970
  }
@@ -421,6 +974,11 @@ function shorten(text, length) {
421
974
  function utc(iso) {
422
975
  return `${iso.slice(0, 16).replace("T", " ")} UTC`;
423
976
  }
977
+ function linkStatus(link) {
978
+ if (link.blocked)
979
+ return `blocked (${link.blocked.reason})`;
980
+ return link.enabled ? "enabled" : "disabled";
981
+ }
424
982
  function linkTable(links) {
425
983
  if (links.length === 0)
426
984
  return "No links.";
@@ -429,7 +987,7 @@ function linkTable(links) {
429
987
  ...links.map((link) => [
430
988
  link.shortUrl,
431
989
  link.clicksLast30Days === null ? "-" : String(link.clicksLast30Days),
432
- link.enabled ? "enabled" : "disabled",
990
+ linkStatus(link),
433
991
  shorten(link.destination, 60)
434
992
  ])
435
993
  ]);
@@ -439,7 +997,7 @@ function linkDetails(link) {
439
997
  ["Short URL", link.shortUrl],
440
998
  ["Destination", link.destination],
441
999
  ["Title", link.title ?? "-"],
442
- ["Status", link.enabled ? "enabled" : "disabled"],
1000
+ ["Status", linkStatus(link)],
443
1001
  ["Created", utc(link.createdAt)],
444
1002
  ["Updated", utc(link.updatedAt)],
445
1003
  ["ID", link.id]
@@ -448,7 +1006,7 @@ function linkDetails(link) {
448
1006
  function top(rows) {
449
1007
  if (rows.length === 0)
450
1008
  return "-";
451
- return [...rows].sort((a, b) => b.clicks - a.clicks).slice(0, 5).map((row) => `${row.value} ${row.clicks}`).join(", ");
1009
+ return [...rows].sort((a2, b2) => b2.clicks - a2.clicks).slice(0, 5).map((row) => `${row.value} ${row.clicks}`).join(", ");
452
1010
  }
453
1011
  function analyticsReport(link, analytics) {
454
1012
  const clicks = analytics.total === 1 ? "1 click" : `${analytics.total} clicks`;
@@ -467,8 +1025,24 @@ function analyticsReport(link, analytics) {
467
1025
  ].join(`
468
1026
  `);
469
1027
  }
1028
+ function used(used2, limit, warning) {
1029
+ return `${used2} of ${limit}${warning ? ` (${warning.level}%)` : ""}`;
1030
+ }
470
1031
  function usageReport(usage) {
471
- return `${usage.clicks} of ${usage.clickLimit} clicks recorded in ${usage.month} (as of ${utc(usage.asOf)}).`;
1032
+ const warning = (resource) => usage.warnings.find((item) => item.resource === resource);
1033
+ const rows = [
1034
+ [`Clicks in ${usage.month}`, used(usage.clicks, usage.clickLimit, warning("clicks"))],
1035
+ ["Active links", used(usage.links.used, usage.links.limit, warning("links"))],
1036
+ ["Custom domains", used(usage.domains.used, usage.domains.limit, warning("domains"))],
1037
+ ["History", `${usage.retentionDays} days`],
1038
+ ["As of", utc(usage.asOf)]
1039
+ ];
1040
+ if (usage.unrecordedSince)
1041
+ rows.splice(1, 0, [
1042
+ "Not recorded",
1043
+ `${usage.unrecordedClicks} clicks since ${utc(usage.unrecordedSince)}`
1044
+ ]);
1045
+ return table(rows);
472
1046
  }
473
1047
  function identityReport(identity, apiUrl) {
474
1048
  const rows = identity.kind === "token" ? [
@@ -477,9 +1051,51 @@ function identityReport(identity, apiUrl) {
477
1051
  ] : [["Session", "browser session"]];
478
1052
  return table([...rows, ["Scopes", identity.scopes.join(", ")], ["API", apiUrl]]);
479
1053
  }
1054
+ function domainKind(domain) {
1055
+ if (domain.kind === "workspace")
1056
+ return "yours";
1057
+ return domain.isDefault ? "default" : "shared";
1058
+ }
1059
+ function domainTable(page) {
1060
+ const rows = [
1061
+ ["HOSTNAME", "STATUS", "KIND", "DNS RECORD"],
1062
+ ...page.domains.map((domain) => [
1063
+ domain.hostname,
1064
+ domain.state,
1065
+ domainKind(domain),
1066
+ domain.records.map((record2) => `${record2.type} ${record2.name} -> ${record2.value}`).join(", ") || "-"
1067
+ ])
1068
+ ];
1069
+ const problems = page.domains.flatMap((domain) => domain.error ? [`${domain.hostname}: ${domain.error.message}`] : []);
1070
+ return [
1071
+ table(rows),
1072
+ "",
1073
+ `${page.used} of ${page.limit} domains used.`,
1074
+ ...problems.length ? ["", ...problems] : []
1075
+ ].join(`
1076
+ `);
1077
+ }
1078
+ function domainDetails(domain) {
1079
+ const lines = [
1080
+ table([
1081
+ ["Hostname", domain.hostname],
1082
+ ["Status", domain.state],
1083
+ ...domain.error ? [["Problem", domain.error.message]] : [],
1084
+ ...domain.activeLinks === null ? [] : [["Active links", String(domain.activeLinks)]],
1085
+ ["ID", domain.id]
1086
+ ])
1087
+ ];
1088
+ if (domain.state !== "active" && domain.records.length > 0)
1089
+ lines.push("", `Create ${domain.records.length === 1 ? "this DNS record" : "these DNS records"} where you manage the domain, then run flared domain check:`, "", table([
1090
+ ["TYPE", "NAME", "VALUE"],
1091
+ ...domain.records.map((record2) => [record2.type, record2.name, record2.value])
1092
+ ]));
1093
+ return lines.join(`
1094
+ `);
1095
+ }
480
1096
 
481
1097
  // src/version.ts
482
- var cliVersion = "0.1.1";
1098
+ var cliVersion = "0.2.0";
483
1099
 
484
1100
  // src/cli.ts
485
1101
  var optionTypes = {
@@ -500,7 +1116,9 @@ var optionTypes = {
500
1116
  format: { type: "string" },
501
1117
  out: { type: "string" },
502
1118
  from: { type: "string" },
503
- to: { type: "string" }
1119
+ to: { type: "string" },
1120
+ domain: { type: "string" },
1121
+ yes: { type: "boolean" }
504
1122
  };
505
1123
  var uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
506
1124
  async function resolveLink(client, reference) {
@@ -514,6 +1132,19 @@ async function resolveLink(client, reference) {
514
1132
  throw new CliError(`No link with the slug "${reference}".`, exitCodes.notFound, "NOT_FOUND");
515
1133
  throw new UsageError(`Several links use the slug "${reference}". Use the link ID.`);
516
1134
  }
1135
+ async function resolveDomain(client, reference, workspaceOnly) {
1136
+ let hostname;
1137
+ try {
1138
+ hostname = normalizeHostname(reference);
1139
+ } catch {
1140
+ hostname = reference.trim().toLowerCase().replace(/\.$/, "");
1141
+ }
1142
+ const { domains } = await client.listDomains();
1143
+ const match = domains.find((domain) => (domain.id === reference || domain.hostname === hostname) && (!workspaceOnly || domain.kind === "workspace"));
1144
+ if (!match)
1145
+ throw new CliError(`No domain "${reference}" in this workspace. Run flared domain list.`, exitCodes.notFound, "NOT_FOUND");
1146
+ return match;
1147
+ }
517
1148
  function limitOf(value) {
518
1149
  if (value === undefined)
519
1150
  return;
@@ -581,13 +1212,22 @@ ${identityReport(identity, apiUrl)}`, {
581
1212
  }
582
1213
  },
583
1214
  "link create": {
584
- usage: "flared link create URL [--slug SLUG] [--title TITLE] [--idempotency-key KEY]",
585
- summary: "Create a short link and print its URL.",
586
- options: ["slug", "title", "idempotency-key"],
1215
+ usage: "flared link create URL [--slug SLUG] [--title TITLE] [--domain HOSTNAME] [--idempotency-key KEY]",
1216
+ summary: "Create a short link and print its URL. Without --domain, it uses the default domain.",
1217
+ options: ["slug", "title", "domain", "idempotency-key"],
587
1218
  args: 1,
588
1219
  async run({ args, options, client, print }) {
589
- const result = await (await client()).createLink({
1220
+ const api = await client();
1221
+ let domainId;
1222
+ if (options.domain !== undefined) {
1223
+ const domain = await resolveDomain(api, options.domain, false);
1224
+ if (domain.state !== "active")
1225
+ throw new CliError(`${domain.hostname} is ${domain.state}. Links can use it once it is active.`, exitCodes.rejected, "DOMAIN_UNAVAILABLE");
1226
+ domainId = domain.id;
1227
+ }
1228
+ const result = await api.createLink({
590
1229
  destination: args[0],
1230
+ ...domainId ? { domainId } : {},
591
1231
  ...options.slug ? { slug: options.slug } : {},
592
1232
  ...options.title ? { title: options.title } : {},
593
1233
  ...options["idempotency-key"] ? { idempotencyKey: options["idempotency-key"] } : {}
@@ -691,9 +1331,9 @@ More links: --cursor ${page.nextCursor}` : "";
691
1331
  throw new UsageError("With --json, give --out FILE.");
692
1332
  const api = await client();
693
1333
  const link = await api.getLink(await resolveLink(api, args[0]));
694
- const data = format === "svg" ? await QRCode.toString(link.shortUrl, { type: "svg", margin: 2 }) : new Uint8Array(await QRCode.toBuffer(link.shortUrl, { type: "png", width: 512 }));
695
1334
  if (!options.out)
696
- return io.stdout(String(data).trimEnd());
1335
+ return io.stdout(qrSvg(link.shortUrl, defaultQrSize));
1336
+ const data = format === "svg" ? qrSvg(link.shortUrl, defaultQrSize) : await qrPng(link.shortUrl, defaultQrSize);
697
1337
  await io.writeFile(options.out, data);
698
1338
  print(`Saved the QR code for ${link.shortUrl} to ${options.out}.`, {
699
1339
  shortUrl: link.shortUrl,
@@ -702,6 +1342,61 @@ More links: --cursor ${page.nextCursor}` : "";
702
1342
  });
703
1343
  }
704
1344
  },
1345
+ "domain list": {
1346
+ usage: "flared domain list",
1347
+ summary: "List the domains for links, their status, and the DNS records to create.",
1348
+ options: [],
1349
+ args: 0,
1350
+ async run({ client, print }) {
1351
+ const page = await (await client()).listDomains();
1352
+ print(domainTable(page), page);
1353
+ }
1354
+ },
1355
+ "domain add": {
1356
+ usage: "flared domain add HOSTNAME",
1357
+ summary: "Add a subdomain you own, such as go.example.com, and print the DNS record to create.",
1358
+ options: [],
1359
+ args: 1,
1360
+ async run({ args, client, print }) {
1361
+ const { domain, created } = await (await client()).addDomain(args[0]);
1362
+ const intro = created ? `Added ${domain.hostname}.` : `${domain.hostname} is already in this workspace.`;
1363
+ print(`${intro}
1364
+
1365
+ ${domainDetails(domain)}`, { domain, created });
1366
+ }
1367
+ },
1368
+ "domain check": {
1369
+ usage: "flared domain check HOSTNAME_OR_ID",
1370
+ summary: "Check the DNS record and the HTTPS certificate now.",
1371
+ options: [],
1372
+ args: 1,
1373
+ async run({ args, client, print }) {
1374
+ const api = await client();
1375
+ const domain = await api.checkDomain((await resolveDomain(api, args[0], true)).id);
1376
+ print(domainDetails(domain), { domain });
1377
+ }
1378
+ },
1379
+ "domain remove": {
1380
+ usage: "flared domain remove HOSTNAME_OR_ID --yes",
1381
+ summary: "Remove a domain. Its links stop redirecting; adding it again restores them.",
1382
+ options: ["yes"],
1383
+ args: 1,
1384
+ async run({ args, options, client, print }) {
1385
+ const api = await client();
1386
+ const domain = await resolveDomain(api, args[0], true);
1387
+ const count = domain.activeLinks ?? 0;
1388
+ const stops = `${count} active ${count === 1 ? "link" : "links"}`;
1389
+ if (!options.yes)
1390
+ throw new UsageError(`Removing ${domain.hostname} stops ${stops}. Run again with --yes to remove it.`);
1391
+ await api.removeDomain(domain.id);
1392
+ const restore = count ? ` ${stops} stopped redirecting. Add the domain again to restore them.` : "";
1393
+ print(`Removed ${domain.hostname}.${restore}`, {
1394
+ removed: true,
1395
+ domain: { id: domain.id, hostname: domain.hostname },
1396
+ activeLinks: count
1397
+ });
1398
+ }
1399
+ },
705
1400
  analytics: {
706
1401
  usage: "flared analytics ID_OR_SLUG [--from YYYY-MM-DD] [--to YYYY-MM-DD]",
707
1402
  summary: "Show clicks by day, country, referrer, and device.",
@@ -714,9 +1409,34 @@ More links: --cursor ${page.nextCursor}` : "";
714
1409
  print(analyticsReport(link, analytics), { analytics });
715
1410
  }
716
1411
  },
1412
+ export: {
1413
+ usage: "flared export --out FILE",
1414
+ summary: "Save every link and the retained daily analytics as one JSON file. The token needs links:read and analytics:read.",
1415
+ options: ["out"],
1416
+ args: 0,
1417
+ async run({ options, io, json, client, print }) {
1418
+ const out = options.out;
1419
+ if (!out)
1420
+ throw new UsageError("Give the file to write with --out FILE.");
1421
+ const api = await client();
1422
+ const file = await io.openFile(out);
1423
+ if (!json)
1424
+ io.stderr(`Exporting to ${out}…`);
1425
+ let counts;
1426
+ try {
1427
+ counts = await writeExport(api, (text) => file.write(text));
1428
+ await file.finish();
1429
+ } catch (error) {
1430
+ await file.discard();
1431
+ throw error;
1432
+ }
1433
+ const numbers = new Intl.NumberFormat("en-US");
1434
+ print(`Saved ${numbers.format(counts.links)} links, ${numbers.format(counts.dailyTotals)} daily totals, and ${numbers.format(counts.dailyDimensions)} daily breakdown rows to ${out}.`, { file: out, ...counts });
1435
+ }
1436
+ },
717
1437
  usage: {
718
1438
  usage: "flared usage",
719
- summary: "Show recorded clicks this month and the allowance.",
1439
+ summary: "Show usage against the workspace limits.",
720
1440
  options: [],
721
1441
  args: 0,
722
1442
  async run({ client, print }) {
@@ -743,11 +1463,11 @@ function help() {
743
1463
  }
744
1464
  function findCommand(positionals) {
745
1465
  const [first, second, ...rest] = positionals;
746
- if (first === "link") {
747
- const name = `link ${second ?? ""}`;
1466
+ if (first === "link" || first === "domain") {
1467
+ const name = `${first} ${second ?? ""}`;
748
1468
  const command2 = commands[name];
749
1469
  if (!command2)
750
- throw new UsageError(`Unknown link command "${second ?? ""}".`);
1470
+ throw new UsageError(`Unknown ${first} command "${second ?? ""}".`);
751
1471
  return { name, command: command2, args: rest };
752
1472
  }
753
1473
  const command = first === undefined ? undefined : commands[first];
@@ -859,8 +1579,8 @@ function promptSecret(question) {
859
1579
  else
860
1580
  resolve(value);
861
1581
  };
862
- const onData = (chunk) => {
863
- for (const character of chunk) {
1582
+ const onData = (chunk2) => {
1583
+ for (const character of chunk2) {
864
1584
  if (character === "\r" || character === `
865
1585
  `)
866
1586
  return done();
@@ -877,8 +1597,8 @@ function promptSecret(question) {
877
1597
  }
878
1598
  async function readAll(stream) {
879
1599
  let text = "";
880
- for await (const chunk of stream)
881
- text += chunk.toString();
1600
+ for await (const chunk2 of stream)
1601
+ text += chunk2.toString();
882
1602
  return text;
883
1603
  }
884
1604
 
@@ -892,5 +1612,22 @@ process.exitCode = await run(process.argv.slice(2), {
892
1612
  stdinIsTTY: process.stdin.isTTY === true,
893
1613
  readSecret: promptSecret,
894
1614
  readStdin: () => readAll(process.stdin),
895
- writeFile: (path, data) => writeFile2(path, data)
1615
+ writeFile: (path, data) => writeFile2(path, data),
1616
+ async openFile(path) {
1617
+ const partial = `${path}.partial`;
1618
+ const handle = await open(partial, "w");
1619
+ return {
1620
+ write: async (text) => {
1621
+ await handle.write(text);
1622
+ },
1623
+ async finish() {
1624
+ await handle.close();
1625
+ await rename2(partial, path);
1626
+ },
1627
+ async discard() {
1628
+ await handle.close().catch(() => {});
1629
+ await rm2(partial, { force: true });
1630
+ }
1631
+ };
1632
+ }
896
1633
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flaredlink/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Create, edit, and measure Flared short links from your terminal.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -20,8 +20,5 @@
20
20
  ],
21
21
  "engines": {
22
22
  "node": ">=20"
23
- },
24
- "dependencies": {
25
- "qrcode": "1.5.4"
26
23
  }
27
24
  }