hollware 0.5.0 → 0.8.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 CHANGED
@@ -63,8 +63,9 @@ reported, and the command exits with code 1. The token never appears in its outp
63
63
 
64
64
  `init`, `migrate`, `deploy`, `doctor` and `token` run the project's own `wrangler` (the nearest
65
65
  `node_modules/.bin/wrangler`) against its D1 database, and read the binding from `wrangler.jsonc`.
66
- `import` speaks MCP to `<url>/mcp` with the token as a bearer token. The CLI never loads
67
- `hollware.config.ts`.
66
+ `import` speaks MCP to `<url>/mcp` with the token as a bearer token. The structure of the sites
67
+ lives in the database (ADR-033): `init` gives a new site its first definition from a preset, and
68
+ its owner shapes it in the admin, or an agent over MCP.
68
69
 
69
70
  ## Links
70
71
 
@@ -157,18 +157,29 @@ function insertToken(token) {
157
157
  const kind = token.kind === void 0 ? ["", ""] : [", kind", `, ${quote(token.kind)}`];
158
158
  return `INSERT INTO tokens (id, site_id, user_id, label, hash, scopes, expires_at, created_at, updated_at${kind[0]}) 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)}${kind[1]});`;
159
159
  }
160
+ /** The row of the site: with a definition, given only to a site that has none yet. */
161
+ function siteRow(site, now, structure) {
162
+ if (structure === void 0) return `INSERT INTO sites (id, created_at, updated_at) VALUES (${quote(site)}, ${quote(now)}, ${quote(now)}) ON CONFLICT (id) DO NOTHING;`;
163
+ return `INSERT INTO sites (id, definition, version, created_at, updated_at) VALUES (${quote(site)}, ${quote(JSON.stringify(structure.definition))}, 1, ${quote(now)}, ${quote(now)}) ON CONFLICT (id) DO UPDATE SET definition = excluded.definition, version = 1, updated_at = excluded.updated_at WHERE sites.definition IS NULL;`;
164
+ }
165
+ /** The change that made version 1 of the site, by its owner, if this script made it. */
166
+ function siteChange(site, ownerId, now, structure) {
167
+ if (structure === void 0) return [];
168
+ return [`INSERT OR IGNORE INTO site_changes (id, site_id, version, actor_user_id, actor_meta, before, after, created_at, updated_at) SELECT ${quote(structure.changeId)}, id, 1, ${quote(ownerId)}, NULL, NULL, definition, ${quote(now)}, ${quote(now)} FROM sites WHERE id = ${quote(site)} AND version = 1 AND updated_at = ${quote(now)};`];
169
+ }
160
170
  /**
161
171
  * What `init` writes, in one script: the site (left alone if it is already there), its owner, a
162
172
  * member of the site with the role owner, and the owner's first token. `users.role` gets the
163
173
  * owner too, the one name of a role that its CHECK and `members` share.
164
174
  */
165
- function initScript({ site, ownerId, memberId, ownerName, token }) {
175
+ function initScript({ site, ownerId, memberId, ownerName, token, structure }) {
166
176
  const name = ownerName === void 0 ? "NULL" : quote(ownerName);
167
177
  return [
168
- `INSERT INTO sites (id, created_at, updated_at) VALUES (${quote(site)}, ${quote(token.now)}, ${quote(token.now)}) ON CONFLICT (id) DO NOTHING;`,
178
+ siteRow(site, token.now, structure),
169
179
  `INSERT INTO users (id, name, role, created_at, updated_at) VALUES (${quote(ownerId)}, ${name}, 'owner', ${quote(token.now)}, ${quote(token.now)});`,
170
180
  `INSERT INTO members (id, site_id, user_id, role, created_at, updated_at) VALUES (${quote(memberId)}, ${quote(site)}, ${quote(ownerId)}, 'owner', ${quote(token.now)}, ${quote(token.now)});`,
171
181
  insertToken(token),
182
+ ...siteChange(site, ownerId, token.now, structure),
172
183
  ""
173
184
  ].join("\n");
174
185
  }
@@ -176,11 +187,12 @@ function initScript({ site, ownerId, memberId, ownerName, token }) {
176
187
  * What `init` writes for a site of a deployment that has an owner already: the site, the owner as
177
188
  * its owner too (one person across the sites of a deployment), and a token of that site.
178
189
  */
179
- function anotherSiteScript({ site, ownerId, memberId, token }) {
190
+ function anotherSiteScript({ site, ownerId, memberId, token, structure }) {
180
191
  return [
181
- `INSERT INTO sites (id, created_at, updated_at) VALUES (${quote(site)}, ${quote(token.now)}, ${quote(token.now)}) ON CONFLICT (id) DO NOTHING;`,
192
+ siteRow(site, token.now, structure),
182
193
  `INSERT INTO members (id, site_id, user_id, role, created_at, updated_at) VALUES (${quote(memberId)}, ${quote(site)}, ${quote(ownerId)}, 'owner', ${quote(token.now)}, ${quote(token.now)});`,
183
194
  insertToken(token),
195
+ ...siteChange(site, ownerId, token.now, structure),
184
196
  ""
185
197
  ].join("\n");
186
198
  }
@@ -1,10 +1,10 @@
1
1
  import { r as CliError } from "./format-BpRYEsGB.mjs";
2
2
  import { n as failureReason, r as stripAnsi } from "./wrangler-BLIfwbcu.mjs";
3
3
  import { t as parseFlags } from "./args-CljWh-kZ.mjs";
4
- import { f as ownerQuery, i as resolveDatabase, r as query } from "./d1-D5A6OVDD.mjs";
5
- import { t as publicUrlOf } from "./public-url-DURHzCaC.mjs";
6
- import { i as migrate } from "./migrate-D6Sgxnwx.mjs";
7
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { f as ownerQuery, i as resolveDatabase, r as query } from "./d1-CK4nF7p5.mjs";
5
+ import { t as publicUrlOf } from "./public-url-NLPRzxCN.mjs";
6
+ import { i as migrate } from "./migrate-BBfF-mkE.mjs";
7
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  //#region src/commands/deploy.ts
10
10
  const DEPLOY_USAGE = [
@@ -75,6 +75,7 @@ async function deploy(argv, io) {
75
75
  io.out(`Set HOLLWARE_PUBLIC_URL to ${url} in wrangler.jsonc. Deploying again with it ...`);
76
76
  await wranglerDeploy(io);
77
77
  }
78
+ await ensureMasterSecret(io);
78
79
  await migrate([
79
80
  "--remote",
80
81
  "--database",
@@ -90,12 +91,42 @@ async function deploy(argv, io) {
90
91
  "Commit wrangler.jsonc: it holds the ids of what the deploy created, and the public URL.",
91
92
  ...owners.length === 0 ? [
92
93
  "",
93
- "Next: create your site and its owner in the remote database, with the key of the site in",
94
- "hollware.config.ts. It prints a token for your agents and a link to set up your passkey:",
94
+ "Next: create your site and its owner in the remote database. It prints a token for your",
95
+ "agents and a link to set up your passkey; shape the site in the admin afterwards:",
95
96
  " hollware init --site <key> --remote"
96
97
  ] : []
97
98
  ].join("\n"));
98
99
  return 0;
99
100
  }
101
+ /** The Worker secret that seals the secrets Hollware makes for the structure (ADR-033). */
102
+ const MASTER_SECRET = "HOLLWARE_SECRET";
103
+ /**
104
+ * Gives the Worker its `HOLLWARE_SECRET` when it has none: 32 random bytes in hex, through a file
105
+ * that only the user may read, deleted at once, so the value is never printed nor kept. One the
106
+ * Worker has is left as it is: a new one would leave the sealed secrets unopenable.
107
+ */
108
+ async function ensureMasterSecret(io) {
109
+ const listed = await io.wrangler(["secret", "list"]);
110
+ if (listed.code !== 0) throw new CliError(`wrangler could not list the secrets of the Worker: ${failureReason(listed)}.`);
111
+ if (listed.stdout.includes(`"HOLLWARE_SECRET"`)) return;
112
+ const value = Array.from(io.randomBytes(32), (byte) => byte.toString(16).padStart(2, "0")).join("");
113
+ const directory = io.tempDir();
114
+ try {
115
+ const file = join(directory, "secrets.json");
116
+ writeFileSync(file, JSON.stringify({ [MASTER_SECRET]: value }), { mode: 384 });
117
+ const put = await io.wrangler([
118
+ "secret",
119
+ "bulk",
120
+ file
121
+ ]);
122
+ if (put.code !== 0) throw new CliError(`wrangler could not set ${MASTER_SECRET}: ${failureReason(put)}.`);
123
+ } finally {
124
+ rmSync(directory, {
125
+ recursive: true,
126
+ force: true
127
+ });
128
+ }
129
+ io.out(`Made ${MASTER_SECRET}, which seals the secrets Hollware makes, such as those of webhooks.`);
130
+ }
100
131
  //#endregion
101
132
  export { deploy };
@@ -1,8 +1,8 @@
1
1
  import { r as CliError } from "./format-BpRYEsGB.mjs";
2
2
  import { n as failureReason } from "./wrangler-BLIfwbcu.mjs";
3
3
  import { t as parseFlags } from "./args-CljWh-kZ.mjs";
4
- import { S as parseJsonc, f as ownerQuery, r as query, t as chooseTarget } from "./d1-D5A6OVDD.mjs";
5
- import { a as parsePendingMigrations } from "./migrate-D6Sgxnwx.mjs";
4
+ import { S as parseJsonc, f as ownerQuery, r as query, t as chooseTarget } from "./d1-CK4nF7p5.mjs";
5
+ import { a as parsePendingMigrations } from "./migrate-BBfF-mkE.mjs";
6
6
  import { existsSync, readFileSync, readdirSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  //#region src/commands/doctor.ts
@@ -73,7 +73,7 @@ async function checkDatabase(io, target) {
73
73
  const pending = parsePendingMigrations(listed.stdout);
74
74
  if (pending.length > 0) return [fail(`the ${target} database lacks ${pending.length} ${pending.length === 1 ? "migration" : "migrations"}`, `Run "hollware migrate --${target}".`)];
75
75
  const [owners = []] = await query(io.wrangler, "DB", target, ownerQuery());
76
- return [pass(`the ${target} database has every migration`), owners.length > 0 ? pass(`the ${target} database has an owner`) : fail(`the ${target} database has no owner yet`, `Run "hollware init --site <key> --${target}", with the key of your site in hollware.config.ts.`)];
76
+ return [pass(`the ${target} database has every migration`), owners.length > 0 ? pass(`the ${target} database has an owner`) : fail(`the ${target} database has no owner yet`, `Run "hollware init --site <key> --${target}", with the key your site is to have, such as my-site.`)];
77
77
  }
78
78
  async function doctor(argv, io) {
79
79
  const { values } = parseFlags({
package/dist/index.mjs CHANGED
@@ -105,12 +105,12 @@ async function main(argv, io) {
105
105
  }
106
106
  async function run(command, args, io) {
107
107
  switch (command) {
108
- case "init": return (await import("./init-D-q1JzcO.mjs")).init(args, io);
109
- case "migrate": return (await import("./migrate-R7NAXgQT.mjs")).migrate(args, io);
110
- case "deploy": return (await import("./deploy-B89D8gdO.mjs")).deploy(args, io);
111
- case "doctor": return (await import("./doctor-Djx_3mLE.mjs")).doctor(args, io);
112
- case "token": return (await import("./token-DeVLRP0z.mjs")).token(args, io);
113
- case "invite": return (await import("./invite-PpZ5QNLx.mjs")).invite(args, io);
108
+ case "init": return (await import("./init-CBZPzLBs.mjs")).init(args, io);
109
+ case "migrate": return (await import("./migrate-Ka2Mm06e.mjs")).migrate(args, io);
110
+ case "deploy": return (await import("./deploy-4tusYOMV.mjs")).deploy(args, io);
111
+ case "doctor": return (await import("./doctor-DiKAktVd.mjs")).doctor(args, io);
112
+ case "token": return (await import("./token-BND4Rsxj.mjs")).token(args, io);
113
+ case "invite": return (await import("./invite-Ck-aqpN8.mjs")).invite(args, io);
114
114
  default: return (await import("./import-DG8AABZQ.mjs")).importContent(args, io);
115
115
  }
116
116
  }
@@ -1,24 +1,30 @@
1
1
  import { i as UsageError, r as CliError } from "./format-BpRYEsGB.mjs";
2
2
  import { t as parseFlags } from "./args-CljWh-kZ.mjs";
3
- import { _ as parseSiteKey, a as runFile, c as insertInvitation, f as ownerQuery, h as parseOwnerName, i as resolveDatabase, n as describeDatabase, o as anotherSiteScript, r as query, s as initScript, t as chooseTarget } from "./d1-D5A6OVDD.mjs";
4
- import { n as setupLink, t as publicUrlOf } from "./public-url-DURHzCaC.mjs";
3
+ import { _ as parseSiteKey, a as runFile, c as insertInvitation, f as ownerQuery, h as parseOwnerName, i as resolveDatabase, n as describeDatabase, o as anotherSiteScript, r as query, s as initScript, t as chooseTarget } from "./d1-CK4nF7p5.mjs";
4
+ import { n as setupLink, t as publicUrlOf } from "./public-url-NLPRzxCN.mjs";
5
5
  import { i as newPersonalToken, n as newId, r as newInvitationSecret, t as inDays } from "./entropy-5uxirwFD.mjs";
6
- import { rmSync, writeFileSync } from "node:fs";
6
+ import { existsSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
- import { PERSONAL_TOKEN_TTL_DAYS, SCOPES, hashPersonalToken } from "@hollware/core";
8
+ import { PERSONAL_TOKEN_TTL_DAYS, PRESETS, SCOPES, hashPersonalToken, sitePreset } from "@hollware/core";
9
9
  //#region src/commands/init.ts
10
10
  const INIT_USAGE = [
11
- "Usage: hollware init --site <key> [--local | --remote] [--owner-name <name>] [--database <name>] [--url <url>]",
11
+ "Usage: hollware init --site <key> [--preset <blog | docs | empty>] [--locales <list>]",
12
+ " [--local | --remote] [--owner-name <name>] [--database <name>] [--url <url>]",
12
13
  "",
13
14
  "Creates a site, its owner and the owner's first personal token in the project's D1 database,",
14
15
  "in one step, and prints the token once, with a link to set up the passkey of the admin (valid",
15
16
  "for 7 days). On a database that has an owner already, it adds the site to that owner, who signs",
16
17
  "in to the admin as before, and prints a token of the new site. It does nothing when the site",
17
18
  "has an owner.",
19
+ "The site starts from a preset, which its owner then shapes in the admin, or an agent over MCP",
20
+ "(ADR-033). A project with a hollware.config.ts keeps it instead: the Worker moves it into the",
21
+ "database on its first request.",
18
22
  "Run \"hollware migrate\" first: the database needs its tables.",
19
23
  "",
20
24
  "Options:",
21
- " --site <key> The site, by the key it has in hollware.config.ts (required)",
25
+ " --site <key> The key of the site, such as my-site; it never changes (required)",
26
+ " --preset <preset> blog, docs or empty: a title alone (default: blog)",
27
+ " --locales <list> The locales, comma-separated, the first the default (default: en)",
22
28
  " --local Use the local database (the default)",
23
29
  " --remote Use the database in Cloudflare",
24
30
  " --owner-name <name> The name of the owner",
@@ -37,6 +43,8 @@ async function init(argv, io) {
37
43
  local: { type: "boolean" },
38
44
  remote: { type: "boolean" },
39
45
  "owner-name": { type: "string" },
46
+ preset: { type: "string" },
47
+ locales: { type: "string" },
40
48
  database: { type: "string" },
41
49
  url: { type: "string" },
42
50
  help: { type: "boolean" }
@@ -46,8 +54,9 @@ async function init(argv, io) {
46
54
  io.out(INIT_USAGE);
47
55
  return 0;
48
56
  }
49
- if (values.site === void 0) throw new UsageError("--site is required: name the site, as in hollware.config.ts.");
57
+ if (values.site === void 0) throw new UsageError("--site is required: name the site, such as --site my-site.");
50
58
  const site = parseSiteKey(values.site);
59
+ const definition = initialDefinition(io.cwd, values.preset, values.locales);
51
60
  const ownerName = values["owner-name"] === void 0 ? void 0 : parseOwnerName(values["owner-name"]);
52
61
  const target = chooseTarget(values);
53
62
  const database = resolveDatabase(io.cwd, values.database);
@@ -63,7 +72,8 @@ async function init(argv, io) {
63
72
  site,
64
73
  ownerId: first["id"],
65
74
  database,
66
- target
75
+ target,
76
+ definition
67
77
  });
68
78
  const now = io.now();
69
79
  const timestamp = now.toISOString();
@@ -83,6 +93,10 @@ async function init(argv, io) {
83
93
  now: timestamp
84
94
  });
85
95
  const memberId = newId(io, "member");
96
+ const structure = definition === null ? void 0 : {
97
+ definition,
98
+ changeId: newId(io, "siteChange")
99
+ };
86
100
  const script = initScript({
87
101
  site,
88
102
  ownerId,
@@ -97,7 +111,8 @@ async function init(argv, io) {
97
111
  scopes: SCOPES,
98
112
  expiresAt,
99
113
  now: timestamp
100
- }
114
+ },
115
+ structure
101
116
  }).concat(`${setup}\n`);
102
117
  const directory = io.tempDir();
103
118
  try {
@@ -136,16 +151,22 @@ async function init(argv, io) {
136
151
  }
137
152
  /** `init` for one more site of a deployment whose owner exists: that owner owns the new site too. */
138
153
  async function addSite(io, where) {
139
- const { site, ownerId, database, target } = where;
154
+ const { site, ownerId, database, target, definition } = where;
140
155
  const now = io.now();
141
156
  const timestamp = now.toISOString();
142
157
  const tokenId = newId(io, "token");
143
158
  const secret = newPersonalToken(io);
144
159
  const expiresAt = inDays(now, PERSONAL_TOKEN_TTL_DAYS);
160
+ const memberId = newId(io, "member");
161
+ const structure = definition === null ? void 0 : {
162
+ definition,
163
+ changeId: newId(io, "siteChange")
164
+ };
145
165
  const script = anotherSiteScript({
146
166
  site,
147
167
  ownerId,
148
- memberId: newId(io, "member"),
168
+ memberId,
169
+ structure,
149
170
  token: {
150
171
  id: tokenId,
151
172
  site,
@@ -185,5 +206,23 @@ async function addSite(io, where) {
185
206
  ].join("\n"));
186
207
  return 0;
187
208
  }
209
+ /** A locale tag, as a site's definition takes it: `en`, `es-CL`. */
210
+ const LOCALE = /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
211
+ /**
212
+ * The first definition of the site (ADR-033), from `--preset` and `--locales`; `null` for a
213
+ * project with a hollware.config.ts, whose Worker moves it into the database on its first request.
214
+ */
215
+ function initialDefinition(cwd, preset, locales) {
216
+ if (existsSync(join(cwd, "hollware.config.ts"))) {
217
+ if (preset !== void 0 || locales !== void 0) throw new UsageError("this project has a hollware.config.ts, which the Worker moves into the database: leave out --preset and --locales, or delete the config first.");
218
+ return null;
219
+ }
220
+ const chosen = preset ?? "blog";
221
+ if (!PRESETS.some((name) => name === chosen)) throw new UsageError(`--preset is one of ${PRESETS.join(", ")}, not "${chosen}".`);
222
+ const list = (locales ?? "en").split(",").map((locale) => locale.trim()).filter((locale) => locale !== "");
223
+ const bad = list.find((locale) => !LOCALE.test(locale));
224
+ if (list.length === 0 || bad !== void 0) throw new UsageError(`--locales is a comma-separated list of locales, such as en,es; "${bad ?? ""}" is not one.`);
225
+ return sitePreset(chosen, list, list[0] ?? "en");
226
+ }
188
227
  //#endregion
189
228
  export { init };
@@ -1,9 +1,9 @@
1
1
  import { i as UsageError, r as CliError } from "./format-BpRYEsGB.mjs";
2
2
  import { t as parseFlags } from "./args-CljWh-kZ.mjs";
3
- import { _ as parseSiteKey, c as insertInvitation, d as ownerOfSiteQuery, i as resolveDatabase, p as parseExpiresInDays, r as query, t as chooseTarget, x as sitesQuery } from "./d1-D5A6OVDD.mjs";
4
- import { n as setupLink, t as publicUrlOf } from "./public-url-DURHzCaC.mjs";
3
+ import { _ as parseSiteKey, c as insertInvitation, d as ownerOfSiteQuery, i as resolveDatabase, p as parseExpiresInDays, r as query, t as chooseTarget, x as sitesQuery } from "./d1-CK4nF7p5.mjs";
4
+ import { n as setupLink, t as publicUrlOf } from "./public-url-NLPRzxCN.mjs";
5
5
  import { n as newId, r as newInvitationSecret, t as inDays } from "./entropy-5uxirwFD.mjs";
6
- import { o as WHERE_OPTIONS, s as chooseSite } from "./token-aUnd79vh.mjs";
6
+ import { o as WHERE_OPTIONS, s as chooseSite } from "./token-CLQpD4ed.mjs";
7
7
  import { hashPersonalToken } from "@hollware/core";
8
8
  //#region src/commands/invite.ts
9
9
  const INVITE_USAGE = [
@@ -1,7 +1,7 @@
1
1
  import { r as CliError } from "./format-BpRYEsGB.mjs";
2
2
  import { n as failureReason, r as stripAnsi } from "./wrangler-BLIfwbcu.mjs";
3
3
  import { t as parseFlags } from "./args-CljWh-kZ.mjs";
4
- import { i as resolveDatabase, n as describeDatabase, t as chooseTarget } from "./d1-D5A6OVDD.mjs";
4
+ import { i as resolveDatabase, n as describeDatabase, t as chooseTarget } from "./d1-CK4nF7p5.mjs";
5
5
  import { existsSync, mkdirSync, statSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
7
  //#region src/commands/migrate.ts
@@ -9,9 +9,11 @@ const MIGRATE_USAGE = [
9
9
  "Usage: hollware migrate [--local | --remote] [--check] [--database <name>]",
10
10
  "",
11
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, or on a new",
14
- "database that has none of the migrations yet, there is no backup.",
12
+ "anything to the remote database it exports a backup, and it stops if the backup fails. D1",
13
+ "cannot export a database with a full-text index (0.5 and later): it takes a Time Travel",
14
+ "bookmark instead, which restores the database as it was. The export can make the database",
15
+ "unavailable for a moment; with nothing pending, or on a new database that has none of the",
16
+ "migrations yet, there is no backup.",
15
17
  "",
16
18
  "Options:",
17
19
  " --local Use the local database (the default)",
@@ -110,10 +112,38 @@ async function takeBackup(io, database) {
110
112
  file,
111
113
  "--skip-confirmation"
112
114
  ]);
113
- if (exported.code !== 0) throw new CliError(`the backup of ${where} failed, so no migration was applied: ${failureReason(exported)}`);
115
+ if (exported.code !== 0) {
116
+ const reason = failureReason(exported);
117
+ if (/virtual tables?/i.test(reason)) return takeBookmark(io, database);
118
+ throw new CliError(`the backup of ${where} failed, so no migration was applied: ${reason}`);
119
+ }
114
120
  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.`);
115
121
  return file;
116
122
  }
123
+ /**
124
+ * The backup of a database D1 cannot export: a Time Travel bookmark of now, and the command that
125
+ * restores the database to it.
126
+ */
127
+ async function takeBookmark(io, database) {
128
+ const where = describeDatabase("remote", database);
129
+ io.out(`D1 cannot export ${where}, which has a full-text index; taking a Time Travel bookmark instead ...`);
130
+ const info = await io.wrangler([
131
+ "d1",
132
+ "time-travel",
133
+ "info",
134
+ database,
135
+ "--json"
136
+ ]);
137
+ let bookmark = null;
138
+ try {
139
+ const parsed = JSON.parse(stripAnsi(info.stdout));
140
+ bookmark = typeof parsed === "object" && parsed !== null && "bookmark" in parsed ? parsed.bookmark : null;
141
+ } catch {
142
+ bookmark = null;
143
+ }
144
+ if (info.code !== 0 || typeof bookmark !== "string" || bookmark === "") throw new CliError(`no Time Travel bookmark of ${where} could be taken, so no migration was applied: ${info.code === 0 ? "wrangler answered no bookmark" : failureReason(info)}`);
145
+ return `the Time Travel bookmark ${bookmark}; restore it with "npx wrangler d1 time-travel restore ${database} --bookmark=${bookmark}"`;
146
+ }
117
147
  /** `20261012T120000Z`: a moment, in UTC, that is safe in a file name. */
118
148
  function timestamp(date) {
119
149
  return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
@@ -0,0 +1,2 @@
1
+ import { i as migrate } from "./migrate-BBfF-mkE.mjs";
2
+ export { migrate };
@@ -1,5 +1,5 @@
1
1
  import { i as UsageError } from "./format-BpRYEsGB.mjs";
2
- import { S as parseJsonc } from "./d1-D5A6OVDD.mjs";
2
+ import { S as parseJsonc } from "./d1-CK4nF7p5.mjs";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  //#region src/public-url.ts
@@ -0,0 +1,2 @@
1
+ import { c as token } from "./token-CLQpD4ed.mjs";
2
+ export { token };
@@ -1,6 +1,6 @@
1
1
  import { i as UsageError, n as table, r as CliError, t as printable } from "./format-BpRYEsGB.mjs";
2
2
  import { t as parseFlags } from "./args-CljWh-kZ.mjs";
3
- import { _ as parseSiteKey, b as sitesAndOwnerQuery, g as parseScopes, i as resolveDatabase, l as insertToken, m as parseLabel, p as parseExpiresInDays, r as query, t as chooseTarget, u as listTokens, v as parseTokenId, x as sitesQuery, y as revokeToken } from "./d1-D5A6OVDD.mjs";
3
+ import { _ as parseSiteKey, b as sitesAndOwnerQuery, g as parseScopes, i as resolveDatabase, l as insertToken, m as parseLabel, p as parseExpiresInDays, r as query, t as chooseTarget, u as listTokens, v as parseTokenId, x as sitesQuery, y as revokeToken } from "./d1-CK4nF7p5.mjs";
4
4
  import { a as newReadToken, i as newPersonalToken, n as newId, t as inDays } from "./entropy-5uxirwFD.mjs";
5
5
  import { PERSONAL_TOKEN_TTL_DAYS, hashPersonalToken } from "@hollware/core";
6
6
  //#region src/commands/token.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hollware",
3
- "version": "0.5.0",
3
+ "version": "0.8.0",
4
4
  "description": "The Hollware CLI: init, migrate, token, import and export.",
5
5
  "homepage": "https://hollware.com",
6
6
  "bugs": {
@@ -31,7 +31,7 @@
31
31
  "provenance": true
32
32
  },
33
33
  "dependencies": {
34
- "@hollware/core": "0.5.0",
34
+ "@hollware/core": "0.8.0",
35
35
  "@modelcontextprotocol/client": "2.3.1",
36
36
  "zod": "4.6.5"
37
37
  },
@@ -1,2 +0,0 @@
1
- import { i as migrate } from "./migrate-D6Sgxnwx.mjs";
2
- export { migrate };
@@ -1,2 +0,0 @@
1
- import { c as token } from "./token-aUnd79vh.mjs";
2
- export { token };