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/dist/index.mjs CHANGED
@@ -1,11 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, realpathSync } from "node:fs";
2
+ import { i as UsageError, r as CliError, t as printable } from "./format-BpRYEsGB.mjs";
3
+ import { t as readVersion } from "./version-BMhdZaNo.mjs";
4
+ import { t as createWranglerRunner } from "./wrangler-BLIfwbcu.mjs";
5
+ import { mkdtempSync, realpathSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
3
8
  import { pathToFileURL } from "node:url";
4
9
  //#region src/index.ts
5
10
  const COMMANDS = [
6
11
  {
7
12
  name: "init",
8
- summary: "Set up Hollware in a project"
13
+ summary: "Set up a site, its owner and a first token in the project's D1 database"
9
14
  },
10
15
  {
11
16
  name: "migrate",
@@ -13,7 +18,7 @@ const COMMANDS = [
13
18
  },
14
19
  {
15
20
  name: "token",
16
- summary: "Create, list and revoke access tokens"
21
+ summary: "Create, list and revoke personal tokens"
17
22
  },
18
23
  {
19
24
  name: "import",
@@ -21,34 +26,36 @@ const COMMANDS = [
21
26
  },
22
27
  {
23
28
  name: "export",
24
- summary: "Export entries as Markdown files"
29
+ summary: "Export entries as Markdown files (planned for v0.2, not implemented yet)"
25
30
  }
26
31
  ];
27
32
  const NAME_WIDTH = Math.max(...COMMANDS.map((command) => command.name.length));
28
33
  const USAGE = [
29
- "Usage: hollware <command>",
30
- "",
31
- "Pre-alpha: no command is implemented yet.",
34
+ "Usage: hollware <command> [options]",
32
35
  "",
33
36
  "Commands:",
34
- ...COMMANDS.map(({ name, summary }) => ` ${name.padEnd(NAME_WIDTH)} ${summary} (not implemented yet)`),
37
+ ...COMMANDS.map(({ name, summary }) => ` ${name.padEnd(NAME_WIDTH)} ${summary}`),
35
38
  "",
36
39
  "Options:",
37
40
  " --help Show this help",
38
- " --version Print the version"
41
+ " --version Print the version",
42
+ "",
43
+ "Run \"hollware <command> --help\" for the options of a command.",
44
+ "init, migrate and token work on the project in the current directory, through its wrangler."
39
45
  ].join("\n");
40
- function readVersion() {
41
- const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
42
- if (typeof manifest.version !== "string") throw new Error("package.json has no version");
43
- return manifest.version;
44
- }
46
+ const TOKEN_COMMANDS = [
47
+ "create",
48
+ "list",
49
+ "revoke"
50
+ ];
45
51
  /**
46
- * Runs the CLI and returns its exit code. Each call to `io.out` or `io.err` is one
47
- * message; whoever owns the streams adds the trailing newline. Only the first
48
- * argument is looked at.
52
+ * Runs the CLI and returns its exit code. Each call to `io.out` or `io.err` is one message;
53
+ * whoever owns the streams adds the trailing newline. The command is the first argument; the
54
+ * rest are its own. A `CliError` becomes one message on stderr and the exit code 1, and
55
+ * anything else is a bug, which escapes.
49
56
  */
50
- function main(argv, io) {
51
- const [first] = argv;
57
+ async function main(argv, io) {
58
+ const [first, ...rest] = argv;
52
59
  if (first === void 0 || first === "--help") {
53
60
  io.out(USAGE);
54
61
  return 0;
@@ -57,12 +64,36 @@ function main(argv, io) {
57
64
  io.out(`hollware ${readVersion()}`);
58
65
  return 0;
59
66
  }
60
- if (COMMANDS.some((command) => command.name === first)) {
61
- io.err(`hollware ${first}: not implemented yet`);
67
+ if (first === "export") {
68
+ io.err("hollware export: not implemented yet (planned for v0.2)");
62
69
  return 1;
63
70
  }
64
- io.err(`hollware: unknown command or flag "${first}"\n\n${USAGE}`);
65
- return 1;
71
+ if (![
72
+ "init",
73
+ "migrate",
74
+ "token",
75
+ "import"
76
+ ].includes(first)) {
77
+ io.err(`hollware: unknown command or flag "${printable(first)}"\n\n${USAGE}`);
78
+ return 1;
79
+ }
80
+ const label = first === "token" && TOKEN_COMMANDS.includes(rest[0] ?? "") ? `token ${rest[0]}` : first;
81
+ try {
82
+ return await run(first, rest, io);
83
+ } catch (error) {
84
+ if (!(error instanceof CliError)) throw error;
85
+ const hint = error instanceof UsageError ? ` Run "hollware ${label} --help" for the options.` : "";
86
+ io.err(`hollware ${label}: ${error.message}${hint}`);
87
+ return 1;
88
+ }
89
+ }
90
+ async function run(command, args, io) {
91
+ switch (command) {
92
+ case "init": return (await import("./init-Cl9Epsbp.mjs")).init(args, io);
93
+ case "migrate": return (await import("./migrate-CLHWuUje.mjs")).migrate(args, io);
94
+ case "token": return (await import("./token-BLkrPLio.mjs")).token(args, io);
95
+ default: return (await import("./import-Dlpn_w3s.mjs")).importContent(args, io);
96
+ }
66
97
  }
67
98
  function isEntryPoint() {
68
99
  const entry = process.argv[1];
@@ -73,9 +104,25 @@ function isEntryPoint() {
73
104
  return false;
74
105
  }
75
106
  }
76
- if (isEntryPoint()) process.exitCode = main(process.argv.slice(2), {
77
- out: (s) => process.stdout.write(`${s}\n`),
78
- err: (s) => process.stderr.write(`${s}\n`)
79
- });
107
+ async function readStdin() {
108
+ if (process.stdin.isTTY) return void 0;
109
+ const chunks = [];
110
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
111
+ return Buffer.concat(chunks).toString("utf8");
112
+ }
113
+ function processIo() {
114
+ const cwd = process.cwd();
115
+ return {
116
+ out: (message) => process.stdout.write(`${message}\n`),
117
+ err: (message) => process.stderr.write(`${message}\n`),
118
+ cwd,
119
+ now: () => /* @__PURE__ */ new Date(),
120
+ randomBytes: (length) => crypto.getRandomValues(new Uint8Array(length)),
121
+ readStdin,
122
+ wrangler: createWranglerRunner(cwd),
123
+ tempDir: () => mkdtempSync(join(tmpdir(), "hollware-"))
124
+ };
125
+ }
126
+ if (isEntryPoint()) process.exitCode = await main(process.argv.slice(2), processIo());
80
127
  //#endregion
81
- export { main };
128
+ export { createWranglerRunner, main };
@@ -0,0 +1,108 @@
1
+ import { i as UsageError, r as CliError } from "./format-BpRYEsGB.mjs";
2
+ import { t as parseFlags } from "./args-CljWh-kZ.mjs";
3
+ import { a as runFile, f as parseOwnerName, i as resolveDatabase, l as ownerQuery, m as parseSiteKey, n as describeDatabase, o as initScript, r as query, t as chooseTarget } from "./d1-Bd-sdoDd.mjs";
4
+ import { n as newId, r as newPersonalToken, t as inDays } from "./entropy-CIYbTHkj.mjs";
5
+ import { rmSync, writeFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { PERSONAL_TOKEN_TTL_DAYS, SCOPES, hashPersonalToken } from "@hollware/core";
8
+ //#region src/commands/init.ts
9
+ const INIT_USAGE = [
10
+ "Usage: hollware init --site <key> [--local | --remote] [--owner-name <name>] [--database <name>]",
11
+ "",
12
+ "Creates a site, its owner and the owner's first personal token in the project's D1 database,",
13
+ "in one step, and prints the token once. It does nothing when the database already has an owner.",
14
+ "Run \"hollware migrate\" first: the database needs its tables.",
15
+ "",
16
+ "Options:",
17
+ " --site <key> The site, by the key it has in hollware.config.ts (required)",
18
+ " --local Use the local database (the default)",
19
+ " --remote Use the database in Cloudflare",
20
+ " --owner-name <name> The name of the owner",
21
+ " --database <name> The D1 database, by name or binding (default: the binding \"DB\" of wrangler.jsonc)",
22
+ " --help Show this help"
23
+ ].join("\n");
24
+ /** The label of the token `init` creates. */
25
+ const FIRST_TOKEN_LABEL = "hollware init";
26
+ async function init(argv, io) {
27
+ const { values } = parseFlags({
28
+ args: [...argv],
29
+ allowPositionals: false,
30
+ options: {
31
+ site: { type: "string" },
32
+ local: { type: "boolean" },
33
+ remote: { type: "boolean" },
34
+ "owner-name": { type: "string" },
35
+ database: { type: "string" },
36
+ help: { type: "boolean" }
37
+ }
38
+ });
39
+ if (values.help === true) {
40
+ io.out(INIT_USAGE);
41
+ return 0;
42
+ }
43
+ if (values.site === void 0) throw new UsageError("--site is required: name the site, as in hollware.config.ts.");
44
+ const site = parseSiteKey(values.site);
45
+ const ownerName = values["owner-name"] === void 0 ? void 0 : parseOwnerName(values["owner-name"]);
46
+ const target = chooseTarget(values);
47
+ const database = resolveDatabase(io.cwd, values.database);
48
+ const [owners = []] = await query(io.wrangler, database, target, ownerQuery());
49
+ const [existing] = owners;
50
+ if (existing !== void 0) {
51
+ const id = typeof existing.id === "string" ? ` (${existing.id})` : "";
52
+ throw new CliError(`the database already has an owner${id}, so there is nothing to set up. Create another token with "hollware token create --label <text>".`);
53
+ }
54
+ const now = io.now();
55
+ const timestamp = now.toISOString();
56
+ const ownerId = newId(io, "user");
57
+ const tokenId = newId(io, "token");
58
+ const secret = newPersonalToken(io);
59
+ const expiresAt = inDays(now, PERSONAL_TOKEN_TTL_DAYS);
60
+ const script = initScript({
61
+ site,
62
+ ownerId,
63
+ ownerName,
64
+ token: {
65
+ id: tokenId,
66
+ site,
67
+ userId: ownerId,
68
+ label: FIRST_TOKEN_LABEL,
69
+ hash: await hashPersonalToken(secret),
70
+ scopes: SCOPES,
71
+ expiresAt,
72
+ now: timestamp
73
+ }
74
+ });
75
+ const directory = io.tempDir();
76
+ try {
77
+ const file = join(directory, "init.sql");
78
+ writeFileSync(file, script, { mode: 384 });
79
+ await runFile(io.wrangler, database, target, file);
80
+ } finally {
81
+ rmSync(directory, {
82
+ recursive: true,
83
+ force: true
84
+ });
85
+ }
86
+ io.out([
87
+ `Hollware is ready on ${describeDatabase(target, database)}.`,
88
+ "",
89
+ `Site ${site}`,
90
+ `Owner ${ownerId}`,
91
+ `Token ${tokenId}`,
92
+ `Scopes ${SCOPES.join(" ")}`,
93
+ `Expires ${expiresAt} (in ${PERSONAL_TOKEN_TTL_DAYS} days)`,
94
+ "",
95
+ "Your personal token. It is shown only once: Hollware keeps just its hash.",
96
+ "",
97
+ secret,
98
+ "",
99
+ "Next:",
100
+ " Save the token in a file only you can read, then import content:",
101
+ " hollware import <directory> --url <your Hollware URL> --token-file <file>",
102
+ " An MCP client sends it as the header \"Authorization: Bearer <token>\" to <your Hollware URL>/mcp.",
103
+ " More tokens: hollware token create --label <text>"
104
+ ].join("\n"));
105
+ return 0;
106
+ }
107
+ //#endregion
108
+ export { init };
@@ -0,0 +1,118 @@
1
+ import { r as CliError } from "./format-BpRYEsGB.mjs";
2
+ import { n as failureReason, r as stripAnsi } from "./wrangler-BLIfwbcu.mjs";
3
+ import { t as parseFlags } from "./args-CljWh-kZ.mjs";
4
+ import { i as resolveDatabase, n as describeDatabase, t as chooseTarget } from "./d1-Bd-sdoDd.mjs";
5
+ import { existsSync, mkdirSync, statSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ //#region src/commands/migrate.ts
8
+ const MIGRATE_USAGE = [
9
+ "Usage: hollware migrate [--local | --remote] [--check] [--database <name>]",
10
+ "",
11
+ "Applies the D1 migrations the project has not applied yet, with wrangler. Before it applies",
12
+ "anything to the remote database it exports a backup, and it stops if the backup fails. The",
13
+ "export can make the database unavailable for a moment; with nothing pending there is no backup.",
14
+ "",
15
+ "Options:",
16
+ " --local Use the local database (the default)",
17
+ " --remote Use the database in Cloudflare, after a backup in .wrangler/hollware-backups",
18
+ " --check Only list the pending migrations; exit with code 1 when there are some",
19
+ " --database <name> The D1 database, by name or binding (default: the binding \"DB\" of wrangler.jsonc)",
20
+ " --help Show this help"
21
+ ].join("\n");
22
+ /** Where the backups before a remote migration go, below the project: wrangler's own, git-ignored folder. */
23
+ const BACKUP_DIRECTORY = join(".wrangler", "hollware-backups");
24
+ async function migrate(argv, io) {
25
+ const { values } = parseFlags({
26
+ args: [...argv],
27
+ allowPositionals: false,
28
+ options: {
29
+ local: { type: "boolean" },
30
+ remote: { type: "boolean" },
31
+ check: { type: "boolean" },
32
+ database: { type: "string" },
33
+ help: { type: "boolean" }
34
+ }
35
+ });
36
+ if (values.help === true) {
37
+ io.out(MIGRATE_USAGE);
38
+ return 0;
39
+ }
40
+ const target = chooseTarget(values);
41
+ const database = resolveDatabase(io.cwd, values.database);
42
+ const where = describeDatabase(target, database);
43
+ const listed = await io.wrangler([
44
+ "d1",
45
+ "migrations",
46
+ "list",
47
+ database,
48
+ `--${target}`
49
+ ]);
50
+ if (listed.code !== 0) throw new CliError(`wrangler could not list the migrations: ${failureReason(listed)}`);
51
+ const pending = parsePendingMigrations(listed.stdout);
52
+ if (pending.length === 0) {
53
+ io.out(`No pending migrations on ${where}.`);
54
+ return 0;
55
+ }
56
+ const count = `${pending.length} ${pending.length === 1 ? "migration" : "migrations"}`;
57
+ if (values.check === true) {
58
+ io.out([`${count} pending on ${where}:`, ...pending.map((name) => ` ${name}`)].join("\n"));
59
+ throw new CliError(`${count} pending. Run "hollware migrate --${target}" to apply them.`);
60
+ }
61
+ const backup = target === "remote" ? await takeBackup(io, database) : void 0;
62
+ const applied = await io.wrangler([
63
+ "d1",
64
+ "migrations",
65
+ "apply",
66
+ database,
67
+ `--${target}`
68
+ ]);
69
+ if (applied.code !== 0) {
70
+ const saved = backup === void 0 ? "" : ` The backup taken before is in ${backup}.`;
71
+ throw new CliError(`wrangler stopped while applying the migrations: ${failureReason(applied)}. The migrations before the failing one stay applied; "hollware migrate --check" shows what is left.${saved}`);
72
+ }
73
+ io.out([
74
+ `Applied ${count} to ${where}:`,
75
+ ...pending.map((name) => ` ${name}`),
76
+ ...backup === void 0 ? [] : [`Backup taken before: ${backup}`]
77
+ ].join("\n"));
78
+ return 0;
79
+ }
80
+ /**
81
+ * The migrations that `wrangler d1 migrations list` says are pending. It only knows two
82
+ * answers, "No migrations to apply!" and a table headed "Migrations to be applied:"; anything
83
+ * else is an error, so that a wrangler that words it differently never reads as "up to date".
84
+ */
85
+ function parsePendingMigrations(stdout) {
86
+ const text = stripAnsi(stdout);
87
+ if (text.includes("No migrations to apply!")) return [];
88
+ const marker = text.indexOf("Migrations to be applied:");
89
+ if (marker !== -1) {
90
+ const names = text.slice(marker).split("\n").map((line) => /^[│|]\s*(.+?)\s*[│|]$/.exec(line.trim())?.[1]).filter((cell) => cell !== void 0).slice(1);
91
+ if (names.length > 0) return names;
92
+ }
93
+ throw new CliError("could not read the answer of \"wrangler d1 migrations list\": it neither says that there is nothing to apply nor lists the pending migrations. Is the wrangler of this project a version hollware does not know?");
94
+ }
95
+ async function takeBackup(io, database) {
96
+ const where = describeDatabase("remote", database);
97
+ const file = join(io.cwd, BACKUP_DIRECTORY, `${database}-${timestamp(io.now())}.sql`);
98
+ mkdirSync(dirname(file), { recursive: true });
99
+ io.out(`Backing up ${where} to ${file} ...`);
100
+ const exported = await io.wrangler([
101
+ "d1",
102
+ "export",
103
+ database,
104
+ "--remote",
105
+ "--output",
106
+ file,
107
+ "--skip-confirmation"
108
+ ]);
109
+ if (exported.code !== 0) throw new CliError(`the backup of ${where} failed, so no migration was applied: ${failureReason(exported)}`);
110
+ if (!existsSync(file) || statSync(file).size === 0) throw new CliError(`wrangler reported success but wrote no backup to ${file}, so no migration was applied. Check "wrangler d1 export ${database} --remote --output <file>" by hand.`);
111
+ return file;
112
+ }
113
+ /** `20261012T120000Z`: a moment, in UTC, that is safe in a file name. */
114
+ function timestamp(date) {
115
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
116
+ }
117
+ //#endregion
118
+ export { migrate };
@@ -0,0 +1,228 @@
1
+ import { i as UsageError, n as table, r as CliError, t as printable } from "./format-BpRYEsGB.mjs";
2
+ import { t as parseFlags } from "./args-CljWh-kZ.mjs";
3
+ import { _ as sitesAndOwnerQuery, c as listTokens, d as parseLabel, g as revokeToken, h as parseTokenId, i as resolveDatabase, m as parseSiteKey, p as parseScopes, r as query, s as insertToken, t as chooseTarget, u as parseExpiresInDays, v as sitesQuery } from "./d1-Bd-sdoDd.mjs";
4
+ import { n as newId, r as newPersonalToken, t as inDays } from "./entropy-CIYbTHkj.mjs";
5
+ import { PERSONAL_TOKEN_TTL_DAYS, hashPersonalToken } from "@hollware/core";
6
+ //#region src/commands/token.ts
7
+ const WHERE_FLAGS = [
8
+ " --site <key> The site (default: the only site of the database)",
9
+ " --local Use the local database (the default)",
10
+ " --remote Use the database in Cloudflare",
11
+ " --database <name> The D1 database, by name or binding (default: the binding \"DB\" of wrangler.jsonc)",
12
+ " --help Show this help"
13
+ ];
14
+ const TOKEN_USAGE = [
15
+ "Usage: hollware token <command> [options]",
16
+ "",
17
+ "Commands:",
18
+ " create Create a personal token and print it once",
19
+ " list List the tokens of a site, never their hashes",
20
+ " revoke Revoke a token",
21
+ "",
22
+ "Run \"hollware token <command> --help\" for the options of a command."
23
+ ].join("\n");
24
+ const TOKEN_CREATE_USAGE = [
25
+ "Usage: hollware token create --label <text> [--scopes <list>] [--expires-in-days <n>]",
26
+ " [--site <key>] [--local | --remote] [--database <name>]",
27
+ "",
28
+ "Creates a personal token for the site's owner and prints it once; Hollware keeps only its hash.",
29
+ "",
30
+ "Options:",
31
+ " --label <text> What the token is for, 1 to 64 characters (required)",
32
+ " --scopes <list> Comma-separated: content:read, content:write, content:publish, schema:admin",
33
+ " (default: content:read,content:write)",
34
+ ` --expires-in-days <n> Days until it expires, 1 to 365 (default: ${PERSONAL_TOKEN_TTL_DAYS})`,
35
+ ...WHERE_FLAGS
36
+ ].join("\n");
37
+ const TOKEN_LIST_USAGE = [
38
+ "Usage: hollware token list [--site <key>] [--local | --remote] [--database <name>]",
39
+ "",
40
+ "Lists the tokens of a site: id, label, status, expiry, last use and scopes. Never the hashes.",
41
+ "",
42
+ "Options:",
43
+ ...WHERE_FLAGS
44
+ ].join("\n");
45
+ const TOKEN_REVOKE_USAGE = [
46
+ "Usage: hollware token revoke <id> [--site <key>] [--local | --remote] [--database <name>]",
47
+ "",
48
+ "Revokes a token: the server refuses it from now on. \"hollware token list\" shows the ids.",
49
+ "",
50
+ "Options:",
51
+ ...WHERE_FLAGS
52
+ ].join("\n");
53
+ /** The scopes of a token that `--scopes` does not name: to read and to write drafts, not to publish. */
54
+ const DEFAULT_SCOPES = ["content:read", "content:write"];
55
+ async function token(argv, io) {
56
+ const [subcommand, ...rest] = argv;
57
+ switch (subcommand) {
58
+ case void 0:
59
+ case "--help":
60
+ io.out(TOKEN_USAGE);
61
+ return 0;
62
+ case "create": return createToken(rest, io);
63
+ case "list": return listTokenRows(rest, io);
64
+ case "revoke": return revokeTokenById(rest, io);
65
+ default: throw new UsageError(`unknown command "${printable(subcommand)}": use create, list or revoke.`);
66
+ }
67
+ }
68
+ const WHERE_OPTIONS = {
69
+ site: { type: "string" },
70
+ local: { type: "boolean" },
71
+ remote: { type: "boolean" },
72
+ database: { type: "string" },
73
+ help: { type: "boolean" }
74
+ };
75
+ async function createToken(argv, io) {
76
+ const { values } = parseFlags({
77
+ args: [...argv],
78
+ allowPositionals: false,
79
+ options: {
80
+ ...WHERE_OPTIONS,
81
+ label: { type: "string" },
82
+ scopes: { type: "string" },
83
+ "expires-in-days": { type: "string" }
84
+ }
85
+ });
86
+ if (values.help === true) {
87
+ io.out(TOKEN_CREATE_USAGE);
88
+ return 0;
89
+ }
90
+ if (values.label === void 0) throw new UsageError("--label is required: say what the token is for, such as \"ci\" or \"claude-code\".");
91
+ const label = parseLabel(values.label);
92
+ const siteFlag = values.site === void 0 ? void 0 : parseSiteKey(values.site);
93
+ const scopes = values.scopes === void 0 ? DEFAULT_SCOPES : parseScopes(values.scopes);
94
+ const days = values["expires-in-days"] === void 0 ? PERSONAL_TOKEN_TTL_DAYS : parseExpiresInDays(values["expires-in-days"]);
95
+ const target = chooseTarget(values);
96
+ const database = resolveDatabase(io.cwd, values.database);
97
+ const [siteRows = [], ownerRows = []] = await query(io.wrangler, database, target, sitesAndOwnerQuery());
98
+ const site = chooseSite(siteRows, siteFlag);
99
+ const ownerId = firstId(ownerRows);
100
+ if (ownerId === void 0) throw new CliError("the database has no owner yet. Run \"hollware init --site <key>\" first.");
101
+ const now = io.now();
102
+ const id = newId(io, "token");
103
+ const secret = newPersonalToken(io);
104
+ const expiresAt = inDays(now, days);
105
+ await query(io.wrangler, database, target, insertToken({
106
+ id,
107
+ site,
108
+ userId: ownerId,
109
+ label,
110
+ hash: await hashPersonalToken(secret),
111
+ scopes,
112
+ expiresAt,
113
+ now: now.toISOString()
114
+ }));
115
+ io.out([
116
+ `Created token ${id} for the site "${site}".`,
117
+ "",
118
+ `Label ${label}`,
119
+ `Scopes ${scopes.join(" ")}`,
120
+ `Expires ${expiresAt} (in ${days} ${days === 1 ? "day" : "days"})`,
121
+ "",
122
+ "The token. It is shown only once: Hollware keeps just its hash.",
123
+ "",
124
+ secret
125
+ ].join("\n"));
126
+ return 0;
127
+ }
128
+ async function listTokenRows(argv, io) {
129
+ const { values } = parseFlags({
130
+ args: [...argv],
131
+ allowPositionals: false,
132
+ options: WHERE_OPTIONS
133
+ });
134
+ if (values.help === true) {
135
+ io.out(TOKEN_LIST_USAGE);
136
+ return 0;
137
+ }
138
+ const siteFlag = values.site === void 0 ? void 0 : parseSiteKey(values.site);
139
+ const target = chooseTarget(values);
140
+ const database = resolveDatabase(io.cwd, values.database);
141
+ const [siteRows = []] = await query(io.wrangler, database, target, sitesQuery());
142
+ const site = chooseSite(siteRows, siteFlag);
143
+ const [rows = []] = await query(io.wrangler, database, target, listTokens(site));
144
+ if (rows.length === 0) {
145
+ io.out(`The site "${site}" has no tokens.`);
146
+ return 0;
147
+ }
148
+ const now = io.now();
149
+ io.out(table([
150
+ "ID",
151
+ "LABEL",
152
+ "STATUS",
153
+ "EXPIRES",
154
+ "LAST USED",
155
+ "SCOPES"
156
+ ], rows.map((row) => [
157
+ text(row.id),
158
+ text(row.label),
159
+ statusOf(row, now),
160
+ text(row.expires_at, "never"),
161
+ text(row.last_used_at, "never"),
162
+ scopesOf(row.scopes)
163
+ ])));
164
+ return 0;
165
+ }
166
+ async function revokeTokenById(argv, io) {
167
+ const { values, positionals } = parseFlags({
168
+ args: [...argv],
169
+ allowPositionals: true,
170
+ options: WHERE_OPTIONS
171
+ });
172
+ if (values.help === true) {
173
+ io.out(TOKEN_REVOKE_USAGE);
174
+ return 0;
175
+ }
176
+ const [rawId, ...extra] = positionals;
177
+ if (rawId === void 0 || extra.length > 0) throw new UsageError("revoke takes the id of one token: hollware token revoke <id>.");
178
+ const id = parseTokenId(rawId);
179
+ const site = values.site === void 0 ? void 0 : parseSiteKey(values.site);
180
+ const target = chooseTarget(values);
181
+ const database = resolveDatabase(io.cwd, values.database);
182
+ const [revoked = [], current = []] = await query(io.wrangler, database, target, revokeToken(id, site, io.now().toISOString()));
183
+ if (revoked.length > 0) {
184
+ io.out(`Revoked token ${id}.`);
185
+ return 0;
186
+ }
187
+ const [row] = current;
188
+ if (row === void 0) {
189
+ const inSite = site === void 0 ? "" : ` in the site "${site}"`;
190
+ throw new CliError(`there is no token ${id}${inSite}. "hollware token list" shows the tokens.`);
191
+ }
192
+ io.out(`Token ${id} was already revoked at ${text(row.revoked_at)}. Nothing changed.`);
193
+ return 0;
194
+ }
195
+ /** The site a command acts on: the one `--site` names (already checked), or the only one of the database. */
196
+ function chooseSite(rows, site) {
197
+ const sites = rows.map((row) => row.id).filter((id) => typeof id === "string");
198
+ if (site !== void 0) {
199
+ if (!sites.includes(site)) throw new CliError(`the database has no site "${site}"${sites.length === 0 ? "" : ` (it has ${sites.join(", ")})`}. Run "hollware init --site ${site}" to create it.`);
200
+ return site;
201
+ }
202
+ const [only, ...others] = sites;
203
+ if (only === void 0) throw new CliError("the database has no site yet. Run \"hollware init --site <key>\" first.");
204
+ if (others.length > 0) throw new CliError(`the database has several sites (${sites.join(", ")}): choose one with --site <key>.`);
205
+ return only;
206
+ }
207
+ function firstId(rows) {
208
+ const [row] = rows;
209
+ return typeof row?.id === "string" ? row.id : void 0;
210
+ }
211
+ function text(value, otherwise = "") {
212
+ return typeof value === "string" ? printable(value) : otherwise;
213
+ }
214
+ function statusOf(row, now) {
215
+ if (typeof row.revoked_at === "string") return "revoked";
216
+ if (typeof row.expires_at === "string" && Date.parse(row.expires_at) <= now.getTime()) return "expired";
217
+ return "active";
218
+ }
219
+ function scopesOf(value) {
220
+ if (typeof value !== "string") return "";
221
+ try {
222
+ const parsed = JSON.parse(value);
223
+ if (Array.isArray(parsed)) return parsed.filter((scope) => typeof scope === "string").map(printable).join(" ");
224
+ } catch {}
225
+ return printable(value);
226
+ }
227
+ //#endregion
228
+ export { token };
@@ -0,0 +1,9 @@
1
+ import { readFileSync } from "node:fs";
2
+ //#region src/version.ts
3
+ function readVersion() {
4
+ const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
5
+ if (typeof manifest.version !== "string") throw new Error("package.json has no version");
6
+ return manifest.version;
7
+ }
8
+ //#endregion
9
+ export { readVersion as t };