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