hollware 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -11
- package/dist/args-CljWh-kZ.mjs +20 -0
- package/dist/d1-Bd-sdoDd.mjs +281 -0
- package/dist/entropy-CIYbTHkj.mjs +17 -0
- package/dist/format-BpRYEsGB.mjs +38 -0
- package/dist/import-Dlpn_w3s.mjs +506 -0
- package/dist/index.d.mts +49 -8
- package/dist/index.mjs +75 -28
- package/dist/init-Cl9Epsbp.mjs +108 -0
- package/dist/migrate-CLHWuUje.mjs +118 -0
- package/dist/token-BLkrPLio.mjs +228 -0
- package/dist/version-BMhdZaNo.mjs +9 -0
- package/dist/wrangler-BLIfwbcu.mjs +106 -0
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -3,20 +3,59 @@
|
|
|
3
3
|
The command line of [Hollware](https://github.com/hollware/hollware), an open-source headless
|
|
4
4
|
CMS for the agent era, native to Cloudflare Workers.
|
|
5
5
|
|
|
6
|
-
> **Status: pre-alpha
|
|
7
|
-
>
|
|
6
|
+
> **Status: pre-alpha.** `init`, `migrate`, `token` and `import` work, against a Hollware that
|
|
7
|
+
> is still being built. `export` arrives in v0.2. POSIX shells only: it has not been tried on
|
|
8
|
+
> Windows.
|
|
8
9
|
|
|
9
|
-
##
|
|
10
|
+
## Commands
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
| Command | Purpose |
|
|
13
|
+
| --------- | -------------------------------------------------------------------------------- |
|
|
14
|
+
| `init` | Create a site, its owner and a first personal token in the project's D1 database |
|
|
15
|
+
| `migrate` | Apply D1 migrations; `--check` lists the pending ones, `--remote` backs up first |
|
|
16
|
+
| `token` | Create, list and revoke personal tokens |
|
|
17
|
+
| `import` | Import Markdown files over MCP, as drafts, and publish them with `--publish` |
|
|
18
|
+
| `export` | Export entries as Markdown files (v0.2) |
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
Every command answers `--help`. A command that fails prints one message on stderr, with what to
|
|
21
|
+
do, and exits with code 1.
|
|
22
|
+
|
|
23
|
+
## A first run
|
|
24
|
+
|
|
25
|
+
In a Hollware project (the starter has the D1 binding `DB` in its `wrangler.jsonc`):
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
hollware migrate # create the tables in the local database
|
|
29
|
+
hollware init --site blog # create the site and its owner; prints a token, once
|
|
30
|
+
hollware token create --label ci --scopes content:read,content:write
|
|
31
|
+
hollware token list # never shows a hash
|
|
32
|
+
hollware token revoke tok_01JAB3XQ8ZK4M6N2P5R7S9T0VW
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Add `--remote` to work on the database in Cloudflare instead (it needs `wrangler login` or a
|
|
36
|
+
`CLOUDFLARE_API_TOKEN`). `migrate --remote` first exports the database to
|
|
37
|
+
`.wrangler/hollware-backups` and stops if that fails. `--database <name>` picks another D1
|
|
38
|
+
database than the binding `DB`. `--site <key>` picks a site when the database has several.
|
|
39
|
+
|
|
40
|
+
Put the token that `init` printed in a file only you can read, and import a directory of
|
|
41
|
+
Markdown, laid out as `<collection>/<locale>/<slug>.md`, into a running Hollware:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
hollware import ./content --url https://cms.example.com --token-file ./token.txt --dry-run
|
|
45
|
+
hollware import ./content --url https://cms.example.com --token-file ./token.txt --publish
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`import` validates every file first and writes nothing if one would be refused. Running it
|
|
49
|
+
again changes nothing: every entry answers `unchanged`. With `--publish` it publishes the
|
|
50
|
+
entries that have unpublished changes, and it acknowledges no warning: an entry with warnings is
|
|
51
|
+
reported, and the command exits with code 1. The token never appears in its output.
|
|
52
|
+
|
|
53
|
+
## How it works
|
|
54
|
+
|
|
55
|
+
`init`, `migrate` and `token` run the project's own `wrangler` (the nearest
|
|
56
|
+
`node_modules/.bin/wrangler`) against its D1 database, and read the binding from `wrangler.jsonc`.
|
|
57
|
+
`import` speaks MCP to `<url>/mcp` with the token as a bearer token. The CLI never loads
|
|
58
|
+
`hollware.config.ts`.
|
|
20
59
|
|
|
21
60
|
## Links
|
|
22
61
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { i as UsageError } from "./format-BpRYEsGB.mjs";
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
//#region src/args.ts
|
|
4
|
+
/**
|
|
5
|
+
* `parseArgs` in strict mode: an unknown option, a missing value or a stray argument is a
|
|
6
|
+
* `UsageError` with one short sentence, instead of Node's error with its advice about `--`.
|
|
7
|
+
*/
|
|
8
|
+
function parseFlags(config) {
|
|
9
|
+
try {
|
|
10
|
+
return parseArgs(config);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error instanceof TypeError && "code" in error && String(error.code).startsWith("ERR_PARSE_ARGS_")) {
|
|
13
|
+
const [sentence = error.message] = error.message.split(". ");
|
|
14
|
+
throw new UsageError(`${sentence.charAt(0).toLowerCase()}${sentence.slice(1)}.`);
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { parseFlags as t };
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { i as UsageError, r as CliError, t as printable } from "./format-BpRYEsGB.mjs";
|
|
2
|
+
import { n as failureReason } from "./wrangler-BLIfwbcu.mjs";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { SCOPES, SITE_KEY_MAX_LENGTH, isId, isScope, isSiteKey } from "@hollware/core";
|
|
6
|
+
import * as z from "zod";
|
|
7
|
+
//#region src/jsonc.ts
|
|
8
|
+
/**
|
|
9
|
+
* Reads JSON with comments, the format of `wrangler.jsonc`: `//` and `/* *\/` comments and a
|
|
10
|
+
* comma before a closing bracket are dropped, and a comment marker inside a string is left
|
|
11
|
+
* alone. It does nothing else: whatever `JSON.parse` rejects after that is an error.
|
|
12
|
+
*/
|
|
13
|
+
function parseJsonc(text) {
|
|
14
|
+
return JSON.parse(stripJsonc(text));
|
|
15
|
+
}
|
|
16
|
+
function stripJsonc(text) {
|
|
17
|
+
const source = text.startsWith("") ? text.slice(1) : text;
|
|
18
|
+
let out = "";
|
|
19
|
+
let mode = "code";
|
|
20
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
21
|
+
const char = source.charAt(i);
|
|
22
|
+
const next = source.charAt(i + 1);
|
|
23
|
+
if (mode === "string") {
|
|
24
|
+
out += char;
|
|
25
|
+
if (char === "\\") {
|
|
26
|
+
out += next;
|
|
27
|
+
i += 1;
|
|
28
|
+
} else if (char === "\"") mode = "code";
|
|
29
|
+
} else if (mode === "line-comment") {
|
|
30
|
+
if (char === "\n") {
|
|
31
|
+
out += char;
|
|
32
|
+
mode = "code";
|
|
33
|
+
}
|
|
34
|
+
} else if (mode === "block-comment") {
|
|
35
|
+
if (char === "*" && next === "/") {
|
|
36
|
+
i += 1;
|
|
37
|
+
mode = "code";
|
|
38
|
+
} else if (char === "\n") out += char;
|
|
39
|
+
} else if (char === "\"") {
|
|
40
|
+
out += char;
|
|
41
|
+
mode = "string";
|
|
42
|
+
} else if (char === "/" && next === "/") {
|
|
43
|
+
i += 1;
|
|
44
|
+
mode = "line-comment";
|
|
45
|
+
} else if (char === "/" && next === "*") {
|
|
46
|
+
i += 1;
|
|
47
|
+
mode = "block-comment";
|
|
48
|
+
} else out += char;
|
|
49
|
+
}
|
|
50
|
+
if (mode === "block-comment") throw new SyntaxError("a /* comment is never closed");
|
|
51
|
+
return dropTrailingCommas(out);
|
|
52
|
+
}
|
|
53
|
+
/** Drops a comma that only whitespace separates from a closing bracket, outside strings. */
|
|
54
|
+
function dropTrailingCommas(code) {
|
|
55
|
+
let out = "";
|
|
56
|
+
let inString = false;
|
|
57
|
+
for (let i = 0; i < code.length; i += 1) {
|
|
58
|
+
const char = code.charAt(i);
|
|
59
|
+
if (inString) {
|
|
60
|
+
out += char;
|
|
61
|
+
if (char === "\\") {
|
|
62
|
+
out += code.charAt(i + 1);
|
|
63
|
+
i += 1;
|
|
64
|
+
} else if (char === "\"") inString = false;
|
|
65
|
+
} else if (char === "\"") {
|
|
66
|
+
out += char;
|
|
67
|
+
inString = true;
|
|
68
|
+
} else if (char === "," && /^\s*[}\]]/.test(code.slice(i + 1))) continue;
|
|
69
|
+
else out += char;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/sql.ts
|
|
75
|
+
/**
|
|
76
|
+
* Everything the CLI says to the database. A value a person types is checked first and then
|
|
77
|
+
* enters a statement only as a SQL string literal, `'` doubled, so it cannot become part of the
|
|
78
|
+
* statement; the statements themselves are fixed templates. Nothing else in the package builds
|
|
79
|
+
* SQL.
|
|
80
|
+
*/
|
|
81
|
+
/** A SQL string literal: the value in single quotes, with every single quote doubled. */
|
|
82
|
+
function quote(value) {
|
|
83
|
+
if (value.includes("\0")) throw new TypeError("a SQL string cannot hold a NUL character");
|
|
84
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
85
|
+
}
|
|
86
|
+
/** A value a person typed, quoted and escaped for a message; invisible characters become `?`. */
|
|
87
|
+
function shown(value) {
|
|
88
|
+
return printable(JSON.stringify(value));
|
|
89
|
+
}
|
|
90
|
+
/** A site key: lowercase letters and digits joined by single hyphens, like a slug. */
|
|
91
|
+
function parseSiteKey(value) {
|
|
92
|
+
if (!isSiteKey(value)) throw new UsageError(`invalid --site ${shown(value)}: a site key is lowercase letters and digits joined by single hyphens, up to ${SITE_KEY_MAX_LENGTH} characters, such as "blog" or "my-site".`);
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
const LABEL_MAX = 64;
|
|
96
|
+
const OWNER_NAME_MAX = 100;
|
|
97
|
+
const FORBIDDEN_IN_TEXT = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u;
|
|
98
|
+
function parseText(value, flag, max) {
|
|
99
|
+
const text = value.trim();
|
|
100
|
+
const length = Array.from(text).length;
|
|
101
|
+
if (length < 1 || length > max || FORBIDDEN_IN_TEXT.test(text)) throw new UsageError(`invalid ${flag} ${shown(value)}: use 1 to ${max} characters, with no control or invisible formatting characters.`);
|
|
102
|
+
return text;
|
|
103
|
+
}
|
|
104
|
+
/** The label of a token: 1 to 64 characters, trimmed, with nothing that a terminal would act on. */
|
|
105
|
+
function parseLabel(value) {
|
|
106
|
+
return parseText(value, "--label", LABEL_MAX);
|
|
107
|
+
}
|
|
108
|
+
/** The name of the owner: the same rules as a label, up to 100 characters. */
|
|
109
|
+
function parseOwnerName(value) {
|
|
110
|
+
return parseText(value, "--owner-name", OWNER_NAME_MAX);
|
|
111
|
+
}
|
|
112
|
+
/** The scopes of `--scopes`: a comma-separated list of known scopes, without repeats. */
|
|
113
|
+
function parseScopes(value) {
|
|
114
|
+
const names = value.split(",").map((name) => name.trim());
|
|
115
|
+
const scopes = [];
|
|
116
|
+
for (const name of names) {
|
|
117
|
+
if (!isScope(name)) throw new UsageError(`invalid --scopes: ${shown(name)} is not a scope. Choose from ${SCOPES.join(", ")}.`);
|
|
118
|
+
if (!scopes.includes(name)) scopes.push(name);
|
|
119
|
+
}
|
|
120
|
+
return scopes;
|
|
121
|
+
}
|
|
122
|
+
/** The days of `--expires-in-days`: a whole number from 1 to 365. */
|
|
123
|
+
function parseExpiresInDays(value) {
|
|
124
|
+
const days = /^[0-9]{1,4}$/.test(value) ? Number(value) : 0;
|
|
125
|
+
if (days < 1 || days > 365) throw new UsageError(`invalid --expires-in-days ${shown(value)}: use a whole number of days from 1 to 365.`);
|
|
126
|
+
return days;
|
|
127
|
+
}
|
|
128
|
+
/** The id of a token, `tok_` and 26 characters, as `token list` shows it. */
|
|
129
|
+
function parseTokenId(value) {
|
|
130
|
+
if (!isId("tok", value)) throw new UsageError(`invalid token id ${shown(value)}: a token id is tok_ followed by 26 characters. "hollware token list" shows them.`);
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
const DATABASE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
134
|
+
/** A D1 database, by name or by binding, as wrangler takes it. It never starts with a hyphen. */
|
|
135
|
+
function parseDatabase(value) {
|
|
136
|
+
if (!DATABASE.test(value)) throw new UsageError(`invalid --database ${shown(value)}: use the name or the binding of a D1 database (letters, digits, "_", "-" and ".", not starting with "-").`);
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
/** The statement that finds an owner. */
|
|
140
|
+
function ownerQuery() {
|
|
141
|
+
return "SELECT id FROM users WHERE role = 'owner' LIMIT 1";
|
|
142
|
+
}
|
|
143
|
+
/** One statement for the sites of the database and one for its first owner. */
|
|
144
|
+
function sitesAndOwnerQuery() {
|
|
145
|
+
return "SELECT id FROM sites ORDER BY id; SELECT id FROM users WHERE role = 'owner' ORDER BY created_at, id LIMIT 1;";
|
|
146
|
+
}
|
|
147
|
+
/** The sites of the database. */
|
|
148
|
+
function sitesQuery() {
|
|
149
|
+
return "SELECT id FROM sites ORDER BY id;";
|
|
150
|
+
}
|
|
151
|
+
/** The row of a new token. */
|
|
152
|
+
function insertToken(token) {
|
|
153
|
+
return `INSERT INTO tokens (id, site_id, user_id, label, hash, scopes, expires_at, created_at, updated_at) VALUES (${quote(token.id)}, ${quote(token.site)}, ${quote(token.userId)}, ${quote(token.label)}, ${quote(token.hash)}, ${quote(JSON.stringify(token.scopes))}, ${quote(token.expiresAt)}, ${quote(token.now)}, ${quote(token.now)});`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* What `init` writes, in one script: the site (left alone if it is already there), its owner
|
|
157
|
+
* and the owner's first token.
|
|
158
|
+
*/
|
|
159
|
+
function initScript({ site, ownerId, ownerName, token }) {
|
|
160
|
+
const name = ownerName === void 0 ? "NULL" : quote(ownerName);
|
|
161
|
+
return [
|
|
162
|
+
`INSERT INTO sites (id, created_at, updated_at) VALUES (${quote(site)}, ${quote(token.now)}, ${quote(token.now)}) ON CONFLICT (id) DO NOTHING;`,
|
|
163
|
+
`INSERT INTO users (id, name, role, created_at, updated_at) VALUES (${quote(ownerId)}, ${name}, 'owner', ${quote(token.now)}, ${quote(token.now)});`,
|
|
164
|
+
insertToken(token),
|
|
165
|
+
""
|
|
166
|
+
].join("\n");
|
|
167
|
+
}
|
|
168
|
+
/** The tokens of a site. It never selects the hash. */
|
|
169
|
+
function listTokens(site) {
|
|
170
|
+
return `SELECT id, label, scopes, expires_at, revoked_at, last_used_at, created_at FROM tokens WHERE site_id = ${quote(site)} ORDER BY created_at, id;`;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Revokes a token that is not revoked yet and tells what is stored for the id afterwards: the
|
|
174
|
+
* first statement returns the id when it revoked the token, the second the row, if there is one.
|
|
175
|
+
*/
|
|
176
|
+
function revokeToken(id, site, now) {
|
|
177
|
+
const scope = site === void 0 ? "" : ` AND site_id = ${quote(site)}`;
|
|
178
|
+
return `UPDATE tokens SET revoked_at = ${quote(now)}, updated_at = ${quote(now)} WHERE id = ${quote(id)}${scope} AND revoked_at IS NULL RETURNING id; SELECT id, revoked_at FROM tokens WHERE id = ${quote(id)}${scope};`;
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/d1.ts
|
|
182
|
+
/** The two flags that choose a target. A command without either works on the local database. */
|
|
183
|
+
function chooseTarget(flags) {
|
|
184
|
+
if (flags.local === true && flags.remote === true) throw new UsageError("--local and --remote cannot be used together: choose one.");
|
|
185
|
+
return flags.remote === true ? "remote" : "local";
|
|
186
|
+
}
|
|
187
|
+
/** Where a command ran, for the messages: `the remote database "DB"`. */
|
|
188
|
+
function describeDatabase(target, database) {
|
|
189
|
+
return `the ${target} database "${database}"`;
|
|
190
|
+
}
|
|
191
|
+
const CONFIG_FILES = ["wrangler.jsonc", "wrangler.json"];
|
|
192
|
+
const WranglerConfig = z.looseObject({ d1_databases: z.array(z.looseObject({ binding: z.string() })).optional() });
|
|
193
|
+
/**
|
|
194
|
+
* The D1 database a command works on, as wrangler takes it (a name or a binding). `--database`
|
|
195
|
+
* says it. Without it, it is the binding `DB` of the project's `wrangler.jsonc`, which the CLI
|
|
196
|
+
* reads, as JSON with comments, and checks; it never loads `hollware.config.ts`.
|
|
197
|
+
*/
|
|
198
|
+
function resolveDatabase(cwd, database) {
|
|
199
|
+
if (database !== void 0) return parseDatabase(database);
|
|
200
|
+
const file = CONFIG_FILES.map((name) => join(cwd, name)).find((path) => existsSync(path));
|
|
201
|
+
if (file === void 0) {
|
|
202
|
+
const hint = existsSync(join(cwd, "wrangler.toml")) ? " (wrangler.toml is not read)" : "";
|
|
203
|
+
throw new CliError(`there is no wrangler.jsonc in ${cwd}${hint}. Run hollware from the project directory, or name the D1 database with --database <name>.`);
|
|
204
|
+
}
|
|
205
|
+
const bindings = readBindings(file);
|
|
206
|
+
if (bindings.includes("DB")) return "DB";
|
|
207
|
+
throw new CliError(bindings.length === 0 ? `${file} has no D1 database. Add a D1 binding named "DB" (the starter has one), or name the database with --database <name>.` : `${file} has no D1 binding named "DB" (it has ${bindings.join(", ")}). Rename the binding, or choose one with --database <name or binding>.`);
|
|
208
|
+
}
|
|
209
|
+
function readBindings(file) {
|
|
210
|
+
let text;
|
|
211
|
+
try {
|
|
212
|
+
text = readFileSync(file, "utf8");
|
|
213
|
+
} catch (error) {
|
|
214
|
+
throw new CliError(`could not read ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
215
|
+
}
|
|
216
|
+
let json;
|
|
217
|
+
try {
|
|
218
|
+
json = parseJsonc(text);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw new CliError(`${file} is not valid JSON with comments: ${error instanceof Error ? error.message : String(error)}`);
|
|
221
|
+
}
|
|
222
|
+
const config = WranglerConfig.safeParse(json);
|
|
223
|
+
if (!config.success) {
|
|
224
|
+
const [issue] = config.error.issues;
|
|
225
|
+
throw new CliError(`${file} has an unexpected shape at "${issue?.path.join(".") ?? ""}": ${issue?.message ?? "invalid"}`);
|
|
226
|
+
}
|
|
227
|
+
return (config.data.d1_databases ?? []).map((entry) => entry.binding);
|
|
228
|
+
}
|
|
229
|
+
const Answer = z.array(z.object({
|
|
230
|
+
results: z.array(z.record(z.string(), z.unknown())),
|
|
231
|
+
success: z.boolean()
|
|
232
|
+
}));
|
|
233
|
+
/**
|
|
234
|
+
* Runs SQL through `wrangler d1 execute --json` and returns the rows of each statement, in
|
|
235
|
+
* order. A database without the Hollware tables is reported with the command that creates them.
|
|
236
|
+
*/
|
|
237
|
+
async function query(wrangler, database, target, sql) {
|
|
238
|
+
return readAnswer(await wrangler([
|
|
239
|
+
"d1",
|
|
240
|
+
"execute",
|
|
241
|
+
database,
|
|
242
|
+
`--${target}`,
|
|
243
|
+
"--json",
|
|
244
|
+
"--command",
|
|
245
|
+
sql
|
|
246
|
+
]), database, target);
|
|
247
|
+
}
|
|
248
|
+
/** Runs the SQL of a file in one `wrangler d1 execute --json`: all of it, or none. */
|
|
249
|
+
async function runFile(wrangler, database, target, file) {
|
|
250
|
+
readAnswer(await wrangler([
|
|
251
|
+
"d1",
|
|
252
|
+
"execute",
|
|
253
|
+
database,
|
|
254
|
+
`--${target}`,
|
|
255
|
+
"--json",
|
|
256
|
+
"--file",
|
|
257
|
+
file
|
|
258
|
+
]), database, target);
|
|
259
|
+
}
|
|
260
|
+
function readAnswer(result, database, target) {
|
|
261
|
+
if (result.code !== 0) {
|
|
262
|
+
const reason = failureReason(result);
|
|
263
|
+
if (reason.includes("no such table")) {
|
|
264
|
+
const flag = database === "DB" ? "" : ` --database ${database}`;
|
|
265
|
+
throw new CliError(`the database has no Hollware tables yet. Run "hollware migrate --${target}${flag}" first.`);
|
|
266
|
+
}
|
|
267
|
+
throw new CliError(`wrangler could not run the SQL: ${reason}`);
|
|
268
|
+
}
|
|
269
|
+
let json;
|
|
270
|
+
try {
|
|
271
|
+
json = JSON.parse(result.stdout);
|
|
272
|
+
} catch {
|
|
273
|
+
throw new CliError(`wrangler did not answer with JSON: ${failureReason(result)}`);
|
|
274
|
+
}
|
|
275
|
+
const answer = Answer.safeParse(json);
|
|
276
|
+
if (!answer.success) throw new CliError("wrangler answered with JSON that has an unexpected shape.");
|
|
277
|
+
if (answer.data.some((statement) => !statement.success)) throw new CliError("wrangler reported that a statement did not succeed.");
|
|
278
|
+
return answer.data.map((statement) => statement.results);
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
export { sitesAndOwnerQuery as _, runFile as a, listTokens as c, parseLabel as d, parseOwnerName as f, revokeToken as g, parseTokenId as h, resolveDatabase as i, ownerQuery as l, parseSiteKey as m, describeDatabase as n, initScript as o, parseScopes as p, query as r, insertToken as s, chooseTarget as t, parseExpiresInDays as u, sitesQuery as v };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ID_PREFIXES, generatePersonalToken, ulid } from "@hollware/core";
|
|
2
|
+
//#region src/entropy.ts
|
|
3
|
+
/** A new id of a kind, such as `usr_01JAB…`, from the clock and the random source of `io`. */
|
|
4
|
+
function newId(io, kind) {
|
|
5
|
+
return `${ID_PREFIXES[kind]}_${ulid(io.now().getTime(), io.randomBytes(10))}`;
|
|
6
|
+
}
|
|
7
|
+
/** A new personal token (`hw_pat_` and 52 characters) from the random source of `io`. */
|
|
8
|
+
function newPersonalToken(io) {
|
|
9
|
+
return generatePersonalToken(io.randomBytes(52));
|
|
10
|
+
}
|
|
11
|
+
const DAY_MS = 864e5;
|
|
12
|
+
/** The moment, in ISO 8601 UTC, that is `days` days after `from`. */
|
|
13
|
+
function inDays(from, days) {
|
|
14
|
+
return new Date(from.getTime() + days * DAY_MS).toISOString();
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { newId as n, newPersonalToken as r, inDays as t };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* An error the person at the keyboard can act on. `main` prints its message on stderr, behind
|
|
4
|
+
* the name of the command, and exits with code 1. Anything that is not a `CliError` is a bug or
|
|
5
|
+
* a broken install, and escapes with its stack trace.
|
|
6
|
+
*/
|
|
7
|
+
var CliError = class extends Error {
|
|
8
|
+
name = "CliError";
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* A mistake in the arguments: an unknown option, a missing or a malformed value. `main` adds a
|
|
12
|
+
* pointer to the help of the command after the message.
|
|
13
|
+
*/
|
|
14
|
+
var UsageError = class extends CliError {
|
|
15
|
+
name = "UsageError";
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/format.ts
|
|
19
|
+
/**
|
|
20
|
+
* Text for the terminal: a value that came from the database or from a server is made safe to
|
|
21
|
+
* print, and rows are laid out in aligned columns.
|
|
22
|
+
*/
|
|
23
|
+
const UNSAFE_FOR_TERMINAL = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
24
|
+
/** `text` with every control or invisible formatting character replaced by `?`. */
|
|
25
|
+
function printable(text) {
|
|
26
|
+
return text.replace(UNSAFE_FOR_TERMINAL, "?");
|
|
27
|
+
}
|
|
28
|
+
/** Rows of cells in columns as wide as their widest cell, separated by two spaces. */
|
|
29
|
+
function table(header, rows) {
|
|
30
|
+
const all = [header, ...rows];
|
|
31
|
+
const widths = header.map((_, column) => Math.max(...all.map((row) => Array.from(row[column] ?? "").length)));
|
|
32
|
+
return all.map((row) => row.map((cell, column) => {
|
|
33
|
+
const padding = " ".repeat((widths[column] ?? 0) - Array.from(cell).length);
|
|
34
|
+
return column === row.length - 1 ? cell : `${cell}${padding}`;
|
|
35
|
+
}).join(" ")).join("\n");
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { UsageError as i, table as n, CliError as r, printable as t };
|