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