@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.
- package/README.md +36 -16
- package/dist/flared.js +783 -40
- 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
|
|
17
|
-
|
|
|
18
|
-
| `flared login [--token-stdin]`
|
|
19
|
-
| `flared logout`
|
|
20
|
-
| `flared whoami`
|
|
21
|
-
| `flared link create URL [--slug S] [--title T] [--idempotency-key K]`
|
|
22
|
-
| `flared link list [--search Q] [--limit N] [--cursor C] [--all]`
|
|
23
|
-
| `flared link get LINK`
|
|
24
|
-
| `flared link update LINK [--destination URL] [--title T \| --clear-title]`
|
|
25
|
-
| `flared link disable LINK`, `flared link enable LINK`
|
|
26
|
-
| `flared link qr LINK [--format svg\|png] [--out FILE]`
|
|
27
|
-
| `flared
|
|
28
|
-
| `flared
|
|
29
|
-
|
|
30
|
-
`
|
|
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/
|
|
60
|
-
|
|
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
|
|
64
|
-
|
|
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
|
|
69
|
-
|
|
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
|
|
75
|
-
return
|
|
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
|
|
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((
|
|
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
|
|
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
|
|
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((
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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 = (
|
|
863
|
-
for (const character of
|
|
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
|
|
881
|
-
text +=
|
|
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.
|
|
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
|
}
|