wikimemory 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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/dist/npm-cli/scripts/cli-options.js +29 -0
  4. package/dist/npm-cli/scripts/cli.js +79 -0
  5. package/dist/npm-cli/scripts/client-tools.js +57 -0
  6. package/dist/npm-cli/scripts/deployment-record.js +72 -0
  7. package/dist/npm-cli/scripts/dev.js +49 -0
  8. package/dist/npm-cli/scripts/lifecycle-runtime.js +38 -0
  9. package/dist/npm-cli/scripts/package-root.js +15 -0
  10. package/dist/npm-cli/scripts/passkeys.js +197 -0
  11. package/dist/npm-cli/scripts/setup.js +523 -0
  12. package/dist/npm-cli/scripts/status.js +49 -0
  13. package/dist/npm-cli/scripts/uninstall.js +220 -0
  14. package/dist/npm-cli/scripts/upgrade.js +301 -0
  15. package/dist/npm-cli/src/version.js +2 -0
  16. package/dist/web/assets/index-CjnFBnXp.css +1 -0
  17. package/dist/web/assets/index-buhGyrxi.js +72 -0
  18. package/dist/web/index.html +14 -0
  19. package/migrations/0001_initial.sql +297 -0
  20. package/migrations/0002_passkeys.sql +30 -0
  21. package/migrations/0003_passkey_management.sql +38 -0
  22. package/migrations/0004_credential_bound_registration_tokens.sql +18 -0
  23. package/package.json +89 -0
  24. package/release-manifest.json +22 -0
  25. package/skills/wikimemory-ingest/SKILL.md +24 -0
  26. package/skills/wikimemory-ingest/agents/openai.yaml +4 -0
  27. package/skills/wikimemory-install/SKILL.md +81 -0
  28. package/skills/wikimemory-install/agents/openai.yaml +4 -0
  29. package/skills/wikimemory-lint/SKILL.md +18 -0
  30. package/skills/wikimemory-lint/agents/openai.yaml +4 -0
  31. package/skills/wikimemory-recall/SKILL.md +20 -0
  32. package/skills/wikimemory-recall/agents/openai.yaml +4 -0
  33. package/src/auth/local.ts +131 -0
  34. package/src/auth/passkey-api.ts +100 -0
  35. package/src/auth/passkey-management.ts +150 -0
  36. package/src/auth/passkey.ts +908 -0
  37. package/src/auth/props.ts +96 -0
  38. package/src/auth/resource.ts +36 -0
  39. package/src/domain/crypto.ts +27 -0
  40. package/src/domain/errors.ts +32 -0
  41. package/src/domain/export-service.ts +363 -0
  42. package/src/domain/guards.ts +9 -0
  43. package/src/domain/memory-service.ts +1092 -0
  44. package/src/domain/secret-scanner.ts +45 -0
  45. package/src/domain/text-chunk.ts +24 -0
  46. package/src/domain/types.ts +169 -0
  47. package/src/env.ts +20 -0
  48. package/src/index.ts +138 -0
  49. package/src/mcp/schemas.ts +117 -0
  50. package/src/mcp/server.ts +506 -0
  51. package/src/version.ts +2 -0
  52. package/src/web/app.ts +299 -0
@@ -0,0 +1,220 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFile, unlink, writeFile } from "node:fs/promises";
3
+ import process from "node:process";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { pathToFileURL } from "node:url";
6
+ import { removePackagedDeploymentRecord, uninstallRuntime } from "./lifecycle-runtime.js";
7
+ import { bindingProperty, configValue, deploymentListIndicatesExisting } from "./setup.js";
8
+ const CONFIG_PATH = uninstallRuntime.config;
9
+ const STATE_PATH = uninstallRuntime.installProgress;
10
+ const UNINSTALL_STATE_PATH = uninstallRuntime.uninstallProgress;
11
+ export function parseUninstallOptions(args) {
12
+ let apply = false;
13
+ let confirmation = null;
14
+ let help = false;
15
+ for (let index = 0; index < args.length; index += 1) {
16
+ const argument = args[index];
17
+ if (argument === "--apply")
18
+ apply = true;
19
+ else if (argument === "--help")
20
+ help = true;
21
+ else if (argument === "--confirm") {
22
+ const value = args[index + 1];
23
+ if (value === undefined || value.startsWith("--"))
24
+ throw new Error("--confirm requires the exact Worker name");
25
+ confirmation = value;
26
+ index += 1;
27
+ }
28
+ else
29
+ throw new Error(`Unknown option: ${argument ?? ""}`);
30
+ }
31
+ if (confirmation !== null && !apply)
32
+ throw new Error("--confirm is valid only with --apply");
33
+ return { apply, confirmation, help };
34
+ }
35
+ export function resolveUninstallTargets(config) {
36
+ const accountId = configValue(config, "account_id");
37
+ const workerName = configValue(config, "name");
38
+ const databaseName = bindingProperty(config, "DB", "database_name");
39
+ const databaseId = bindingProperty(config, "DB", "database_id");
40
+ const kvNamespaceId = bindingProperty(config, "OAUTH_KV", "id");
41
+ if (accountId === null ||
42
+ workerName === null ||
43
+ databaseName === null ||
44
+ databaseId === null ||
45
+ kvNamespaceId === null) {
46
+ throw new Error(`${CONFIG_PATH} does not identify one exact Worker, D1 database, and KV namespace`);
47
+ }
48
+ return { accountId, workerName, databaseName, databaseId, kvNamespaceId };
49
+ }
50
+ function summary(targets) {
51
+ return `Cloudflare uninstall targets:\n Account ID: ${targets.accountId}\n Worker: ${targets.workerName}\n D1 database: ${targets.databaseName} (${targets.databaseId})\n KV namespace ID: ${targets.kvNamespaceId}`;
52
+ }
53
+ export function clientRemovalInstructions(connectorName = "wikimemory") {
54
+ return `Client registrations are not removed automatically. Disconnect each configured client separately:\n\n codex mcp logout ${connectorName}\n codex mcp remove ${connectorName}\n\n claude mcp logout ${connectorName}\n claude mcp remove --scope user ${connectorName}\n\nFor Claude web or mobile, remove the custom connector from Settings > Connectors.`;
55
+ }
56
+ async function run(args, allowFailure = false) {
57
+ console.log(`\n> npx ${args.join(" ")}`);
58
+ const result = await new Promise((resolve, reject) => {
59
+ const child = spawn("npx", args, { stdio: ["ignore", "pipe", "pipe"] });
60
+ let stdout = "";
61
+ let stderr = "";
62
+ child.stdout.setEncoding("utf8");
63
+ child.stderr.setEncoding("utf8");
64
+ child.stdout.on("data", (chunk) => {
65
+ stdout += chunk;
66
+ process.stdout.write(chunk);
67
+ });
68
+ child.stderr.on("data", (chunk) => {
69
+ stderr += chunk;
70
+ process.stderr.write(chunk);
71
+ });
72
+ child.on("error", (error) => {
73
+ reject(error);
74
+ });
75
+ child.on("close", (code) => {
76
+ resolve({ stdout, stderr, exitCode: code ?? -1 });
77
+ });
78
+ });
79
+ if (result.exitCode !== 0 && !allowFailure)
80
+ throw new Error(`npx exited with status ${result.exitCode}`);
81
+ return result;
82
+ }
83
+ async function requireExactName(workerName, supplied) {
84
+ let answer = supplied;
85
+ if (answer === null) {
86
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
87
+ answer = await prompt.question(`Type the Worker name ${workerName} to permanently delete all listed cloud resources: `);
88
+ prompt.close();
89
+ }
90
+ if (answer !== workerName)
91
+ throw new Error("Confirmation did not exactly match the Worker name");
92
+ }
93
+ async function loadProgress(targets) {
94
+ try {
95
+ const parsed = JSON.parse(await readFile(UNINSTALL_STATE_PATH, "utf8"));
96
+ if (typeof parsed !== "object" ||
97
+ parsed === null ||
98
+ !("targets" in parsed) ||
99
+ !("workerDeleted" in parsed) ||
100
+ !("kvDeleted" in parsed) ||
101
+ !("databaseDeleted" in parsed)) {
102
+ throw new Error("Invalid uninstall progress state");
103
+ }
104
+ const savedTargets = parsed.targets;
105
+ if (typeof savedTargets !== "object" ||
106
+ savedTargets === null ||
107
+ JSON.stringify(savedTargets) !== JSON.stringify(targets)) {
108
+ throw new Error("Uninstall progress does not match the current production config");
109
+ }
110
+ if (typeof parsed.workerDeleted !== "boolean" ||
111
+ typeof parsed.kvDeleted !== "boolean" ||
112
+ typeof parsed.databaseDeleted !== "boolean") {
113
+ throw new Error("Invalid uninstall progress flags");
114
+ }
115
+ return {
116
+ targets,
117
+ workerDeleted: parsed.workerDeleted,
118
+ kvDeleted: parsed.kvDeleted,
119
+ databaseDeleted: parsed.databaseDeleted
120
+ };
121
+ }
122
+ catch (error) {
123
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
124
+ return { targets, workerDeleted: false, kvDeleted: false, databaseDeleted: false };
125
+ }
126
+ throw error;
127
+ }
128
+ }
129
+ async function saveProgress(progress) {
130
+ await writeFile(UNINSTALL_STATE_PATH, `${JSON.stringify(progress, null, 2)}\n`, "utf8");
131
+ }
132
+ export async function runUninstall(args, runCommand = run) {
133
+ const options = parseUninstallOptions(args);
134
+ if (options.help) {
135
+ console.log(`Usage: ${uninstallRuntime.executable} # preview only\n ${uninstallRuntime.executable} --apply # prompt for exact Worker name\n ${uninstallRuntime.executable} --apply --confirm WORKER_NAME`);
136
+ return;
137
+ }
138
+ const targets = resolveUninstallTargets(await readFile(CONFIG_PATH, "utf8"));
139
+ console.log(`${summary(targets)}\n\nThis permanently deletes the remote memory database and cannot be undone.`);
140
+ if (!options.apply) {
141
+ console.log(`\nPreview only. Rerun with --apply to perform this uninstall.\n\n${clientRemovalInstructions(targets.workerName)}`);
142
+ return;
143
+ }
144
+ await requireExactName(targets.workerName, options.confirmation);
145
+ const progress = await loadProgress(targets);
146
+ await saveProgress(progress);
147
+ if (!progress.workerDeleted) {
148
+ const probe = await runCommand([
149
+ "wrangler",
150
+ "deployments",
151
+ "list",
152
+ "--name",
153
+ targets.workerName,
154
+ "--json",
155
+ "--config",
156
+ CONFIG_PATH
157
+ ], true);
158
+ if (deploymentListIndicatesExisting(probe)) {
159
+ await runCommand([
160
+ "wrangler",
161
+ "delete",
162
+ targets.workerName,
163
+ "--force",
164
+ "--config",
165
+ CONFIG_PATH
166
+ ]);
167
+ }
168
+ else {
169
+ console.log(`\nWorker ${targets.workerName} is already absent; continuing partial-install cleanup.`);
170
+ }
171
+ progress.workerDeleted = true;
172
+ await saveProgress(progress);
173
+ }
174
+ if (!progress.kvDeleted) {
175
+ await runCommand([
176
+ "wrangler",
177
+ "kv",
178
+ "namespace",
179
+ "delete",
180
+ "--namespace-id",
181
+ targets.kvNamespaceId,
182
+ "--skip-confirmation",
183
+ "--config",
184
+ CONFIG_PATH
185
+ ]);
186
+ progress.kvDeleted = true;
187
+ await saveProgress(progress);
188
+ }
189
+ if (!progress.databaseDeleted) {
190
+ await runCommand([
191
+ "wrangler",
192
+ "d1",
193
+ "delete",
194
+ targets.databaseId,
195
+ "--skip-confirmation",
196
+ "--config",
197
+ CONFIG_PATH
198
+ ]);
199
+ progress.databaseDeleted = true;
200
+ await saveProgress(progress);
201
+ }
202
+ await unlink(CONFIG_PATH);
203
+ try {
204
+ await unlink(STATE_PATH);
205
+ }
206
+ catch (error) {
207
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
208
+ throw error;
209
+ }
210
+ await removePackagedDeploymentRecord();
211
+ await unlink(UNINSTALL_STATE_PATH);
212
+ console.log(`\nRemoved the Worker, KV namespace, D1 database, and local production config. The remote data is not recoverable.\n\n${clientRemovalInstructions(targets.workerName)}`);
213
+ }
214
+ const entrypoint = process.argv[1];
215
+ if (entrypoint !== undefined && import.meta.url === pathToFileURL(entrypoint).href) {
216
+ runUninstall(process.argv.slice(2)).catch((error) => {
217
+ console.error(`\nUninstall failed: ${error instanceof Error ? error.message : "Unknown error"}`);
218
+ process.exitCode = 1;
219
+ });
220
+ }
@@ -0,0 +1,301 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import process from "node:process";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { z } from "zod";
8
+ import { deploymentRecordPath, readDeploymentRecord, writeDeploymentRecord } from "./deployment-record.js";
9
+ import { packageRoot } from "./package-root.js";
10
+ const PACKAGE_ROOT = packageRoot();
11
+ const DEPLOYMENT_NAME = /^[a-z0-9][a-z0-9-]{0,62}$/u;
12
+ const MIGRATION_NAME = /^\d{4}_[a-z0-9_]+\.sql$/u;
13
+ const SHA256 = /^[a-f0-9]{64}$/u;
14
+ const RELEASE_MANIFEST_SCHEMA = z
15
+ .object({
16
+ version: z.string().regex(/^\d+\.\d+\.\d+$/u),
17
+ schemaVersion: z.string().regex(MIGRATION_NAME),
18
+ migrations: z.array(z
19
+ .object({ name: z.string().regex(MIGRATION_NAME), sha256: z.string().regex(SHA256) })
20
+ .strict())
21
+ })
22
+ .strict();
23
+ export function parseUpgradeOptions(args) {
24
+ let deployment = "wikimemory";
25
+ let recordPath = null;
26
+ let yes = false;
27
+ let help = false;
28
+ for (let index = 0; index < args.length; index += 1) {
29
+ const argument = args[index];
30
+ if (argument === "--yes")
31
+ yes = true;
32
+ else if (argument === "--help")
33
+ help = true;
34
+ else if (argument === "--deployment" || argument === "--record") {
35
+ const value = args[index + 1];
36
+ if (value === undefined || value.startsWith("--"))
37
+ throw new Error(`${argument} requires a value`);
38
+ if (argument === "--deployment") {
39
+ if (!DEPLOYMENT_NAME.test(value))
40
+ throw new Error("Invalid deployment name");
41
+ deployment = value;
42
+ }
43
+ else
44
+ recordPath = value;
45
+ index += 1;
46
+ }
47
+ else
48
+ throw new Error(`Unknown option: ${argument ?? ""}`);
49
+ }
50
+ return { deployment, recordPath, yes, help };
51
+ }
52
+ export function validateReleaseManifest(value) {
53
+ const manifest = RELEASE_MANIFEST_SCHEMA.parse(value);
54
+ const names = manifest.migrations.map((item) => item.name);
55
+ if (new Set(names).size !== names.length ||
56
+ names.some((name, index) => index > 0 && name <= (names[index - 1] ?? "")))
57
+ throw new Error("Release migrations must be unique and strictly ordered");
58
+ if (names.at(-1) !== manifest.schemaVersion)
59
+ throw new Error("Release schemaVersion must equal the final migration");
60
+ return manifest;
61
+ }
62
+ export function planMigrations(manifest, appliedNames) {
63
+ if (appliedNames.length > manifest.migrations.length)
64
+ throw new Error("Installed schema is newer than this Wikimemory release");
65
+ for (const [index, applied] of appliedNames.entries()) {
66
+ if (manifest.migrations[index]?.name !== applied)
67
+ throw new Error("Installed migration history does not match this release");
68
+ }
69
+ return manifest.migrations.slice(appliedNames.length);
70
+ }
71
+ export function compareSemanticVersions(left, right) {
72
+ const version = z.string().regex(/^\d+\.\d+\.\d+$/u);
73
+ const leftParts = version.parse(left).split(".").map(Number);
74
+ const rightParts = version.parse(right).split(".").map(Number);
75
+ for (let index = 0; index < 3; index += 1) {
76
+ const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
77
+ if (difference !== 0)
78
+ return Math.sign(difference);
79
+ }
80
+ return 0;
81
+ }
82
+ export function productionUpgradeConfig(record, packageRoot) {
83
+ return `${JSON.stringify({
84
+ $schema: join(packageRoot, "node_modules", "wrangler", "config-schema.json"),
85
+ name: record.workerName,
86
+ account_id: record.accountId,
87
+ main: join(packageRoot, "src", "index.ts"),
88
+ compatibility_date: "2026-07-18",
89
+ compatibility_flags: ["nodejs_compat"],
90
+ assets: {
91
+ directory: join(packageRoot, "dist", "web"),
92
+ binding: "ASSETS",
93
+ not_found_handling: "single-page-application"
94
+ },
95
+ vars: { APP_ENV: "production", APP_BASE_URL: record.origin },
96
+ d1_databases: [
97
+ {
98
+ binding: "DB",
99
+ database_name: record.databaseName,
100
+ database_id: record.databaseId,
101
+ migrations_dir: join(packageRoot, "migrations")
102
+ }
103
+ ],
104
+ kv_namespaces: [{ binding: "OAUTH_KV", id: record.kvId }]
105
+ }, null, 2)}\n`;
106
+ }
107
+ const PRODUCTION_UPGRADE_CONFIG_SCHEMA = z.object({
108
+ name: z.string(),
109
+ account_id: z.string(),
110
+ main: z.string(),
111
+ assets: z.object({ directory: z.string() }),
112
+ vars: z.object({ APP_BASE_URL: z.string() }),
113
+ d1_databases: z.array(z.object({ database_id: z.string() })),
114
+ kv_namespaces: z.array(z.object({ id: z.string() }))
115
+ });
116
+ export function parseProductionUpgradeConfig(value) {
117
+ return PRODUCTION_UPGRADE_CONFIG_SCHEMA.parse(value);
118
+ }
119
+ export function validateRemoteTargets(record, remote) {
120
+ if (!remote.accounts.some((account) => account.id === record.accountId))
121
+ throw new Error("Authenticated identity cannot access the recorded Cloudflare account");
122
+ if (!remote.databases.some((database) => database.uuid === record.databaseId && database.name === record.databaseName))
123
+ throw new Error("Recorded D1 database ID and name do not match Cloudflare");
124
+ if (!remote.namespaces.some((namespace) => namespace.id === record.kvId && namespace.title === record.kvName))
125
+ throw new Error("Recorded KV namespace ID and name do not match Cloudflare");
126
+ }
127
+ function migrationBundle(sql, migration) {
128
+ const name = migration.name.replaceAll("'", "''");
129
+ return `${sql.trimEnd()}\n\nINSERT INTO d1_migrations(name) VALUES ('${name}');\n`;
130
+ }
131
+ async function command(commandName, args) {
132
+ const { spawn } = await import("node:child_process");
133
+ return await new Promise((resolve, reject) => {
134
+ const child = spawn(commandName, args, { stdio: ["ignore", "pipe", "pipe"] });
135
+ let stdout = "";
136
+ let stderr = "";
137
+ child.stdout.setEncoding("utf8");
138
+ child.stderr.setEncoding("utf8");
139
+ child.stdout.on("data", (chunk) => {
140
+ stdout += chunk;
141
+ process.stdout.write(chunk);
142
+ });
143
+ child.stderr.on("data", (chunk) => {
144
+ stderr += chunk;
145
+ process.stderr.write(chunk);
146
+ });
147
+ child.on("error", (error) => {
148
+ reject(error);
149
+ });
150
+ child.on("close", (code) => {
151
+ resolve({ stdout, stderr, exitCode: code ?? -1 });
152
+ });
153
+ });
154
+ }
155
+ function parsedJson(stdout) {
156
+ return JSON.parse(stdout);
157
+ }
158
+ async function confirmUpgrade(summary, automatic) {
159
+ console.log(summary);
160
+ if (automatic)
161
+ return;
162
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
163
+ const answer = (await prompt.question("Apply this upgrade? [y/N] ")).trim().toLowerCase();
164
+ prompt.close();
165
+ if (answer !== "y" && answer !== "yes")
166
+ throw new Error("Cancelled");
167
+ }
168
+ async function verifyRelease(origin, manifest) {
169
+ const health = await fetch(`${origin}/health`, { redirect: "error" });
170
+ const healthBody = z
171
+ .object({ status: z.literal("ok"), service: z.literal("wikimemory"), version: z.string() })
172
+ .parse(await health.json());
173
+ if (!health.ok || healthBody.version !== manifest.version)
174
+ throw new Error("Deployed Worker version verification failed");
175
+ const ready = await fetch(`${origin}/ready`, { redirect: "error" });
176
+ const readyBody = z
177
+ .object({
178
+ status: z.literal("ready"),
179
+ service: z.literal("wikimemory"),
180
+ version: z.string(),
181
+ schemaVersion: z.string()
182
+ })
183
+ .parse(await ready.json());
184
+ if (!ready.ok ||
185
+ readyBody.version !== manifest.version ||
186
+ readyBody.schemaVersion !== manifest.schemaVersion)
187
+ throw new Error("Deployed schema version verification failed");
188
+ const discovery = await fetch(`${origin}/.well-known/oauth-protected-resource/mcp`, {
189
+ redirect: "error"
190
+ });
191
+ if (!discovery.ok)
192
+ throw new Error("OAuth protected-resource discovery verification failed");
193
+ const app = await fetch(`${origin}/app`, { redirect: "error" });
194
+ if (!app.ok || !(await app.text()).includes('<div id="root"></div>'))
195
+ throw new Error("React application verification failed");
196
+ }
197
+ export async function runUpgrade(args) {
198
+ const options = parseUpgradeOptions(args);
199
+ if (options.help) {
200
+ console.log("Usage: wikimemory upgrade [--deployment NAME | --record PATH] [--yes]");
201
+ return;
202
+ }
203
+ const recordPath = options.recordPath ?? deploymentRecordPath(options.deployment);
204
+ const record = await readDeploymentRecord(recordPath);
205
+ const manifest = validateReleaseManifest(parsedJson(await readFile(join(PACKAGE_ROOT, "release-manifest.json"), "utf8")));
206
+ for (const migration of manifest.migrations) {
207
+ const sql = await readFile(join(PACKAGE_ROOT, "migrations", migration.name), "utf8");
208
+ const digest = createHash("sha256").update(sql, "utf8").digest("hex");
209
+ if (digest !== migration.sha256)
210
+ throw new Error(`Packaged migration checksum mismatch: ${migration.name}`);
211
+ }
212
+ const temporary = await mkdtemp(join(tmpdir(), "wikimemory-upgrade-"));
213
+ try {
214
+ const configPath = join(temporary, "wrangler.jsonc");
215
+ await writeFile(configPath, productionUpgradeConfig(record, PACKAGE_ROOT), "utf8");
216
+ const common = ["--config", configPath];
217
+ const [whoami, deployments, d1, kv] = await Promise.all([
218
+ command("npx", ["wrangler", "whoami", "--json"]),
219
+ command("npx", [
220
+ "wrangler",
221
+ "deployments",
222
+ "list",
223
+ "--name",
224
+ record.workerName,
225
+ "--json",
226
+ ...common
227
+ ]),
228
+ command("npx", ["wrangler", "d1", "list", "--json", ...common]),
229
+ command("npx", ["wrangler", "kv", "namespace", "list", ...common])
230
+ ]);
231
+ if ([whoami, deployments, d1, kv].some((result) => result.exitCode !== 0))
232
+ throw new Error("Cloudflare target preflight failed");
233
+ const whoamiBody = z
234
+ .object({ accounts: z.array(z.object({ id: z.string() })) })
235
+ .parse(parsedJson(whoami.stdout));
236
+ const deploymentBody = z.array(z.unknown()).parse(parsedJson(deployments.stdout));
237
+ if (deploymentBody.length === 0)
238
+ throw new Error("Recorded Worker does not exist");
239
+ validateRemoteTargets(record, {
240
+ accounts: whoamiBody.accounts,
241
+ databases: z
242
+ .array(z.object({ uuid: z.string(), name: z.string() }))
243
+ .parse(parsedJson(d1.stdout)),
244
+ namespaces: z
245
+ .array(z.object({ id: z.string(), title: z.string() }))
246
+ .parse(parsedJson(kv.stdout))
247
+ });
248
+ const listed = await command("npx", [
249
+ "wrangler",
250
+ "d1",
251
+ "execute",
252
+ record.databaseId,
253
+ "--remote",
254
+ "--json",
255
+ ...common,
256
+ "--command",
257
+ "SELECT name FROM d1_migrations ORDER BY id"
258
+ ]);
259
+ if (listed.exitCode !== 0)
260
+ throw new Error("Could not read installed migrations");
261
+ const query = z
262
+ .array(z.object({ results: z.array(z.object({ name: z.string() })) }))
263
+ .parse(parsedJson(listed.stdout));
264
+ const applied = query.flatMap((result) => result.results.map((row) => row.name));
265
+ const pending = planMigrations(manifest, applied);
266
+ const health = await fetch(`${record.origin}/health`, { redirect: "error" });
267
+ const current = z.object({ version: z.string().optional() }).parse(await health.json()).version ??
268
+ record.installedVersion;
269
+ if (!health.ok)
270
+ throw new Error("Current deployment health check failed");
271
+ if (compareSemanticVersions(current, manifest.version) > 0)
272
+ throw new Error(`Refusing to downgrade Wikimemory from ${current} to ${manifest.version}`);
273
+ await confirmUpgrade(`\nWikimemory upgrade\n Account: ${record.accountId}\n Worker: ${record.workerName}\n D1: ${record.databaseName} (${record.databaseId})\n KV: ${record.kvName} (${record.kvId})\n Origin: ${record.origin}\n Version: ${current} -> ${manifest.version}\n Migrations: ${pending.map((item) => item.name).join(", ") || "none"}`, options.yes);
274
+ for (const migration of pending) {
275
+ const bundlePath = join(temporary, migration.name);
276
+ const sql = await readFile(join(PACKAGE_ROOT, "migrations", migration.name), "utf8");
277
+ await writeFile(bundlePath, migrationBundle(sql, migration), "utf8");
278
+ const result = await command("npx", [
279
+ "wrangler",
280
+ "d1",
281
+ "execute",
282
+ record.databaseId,
283
+ "--remote",
284
+ ...common,
285
+ "--file",
286
+ bundlePath
287
+ ]);
288
+ if (result.exitCode !== 0)
289
+ throw new Error(`Migration failed: ${migration.name}`);
290
+ }
291
+ const deployed = await command("npx", ["wrangler", "deploy", "--strict", ...common]);
292
+ if (deployed.exitCode !== 0)
293
+ throw new Error("Worker deployment failed");
294
+ await verifyRelease(record.origin, manifest);
295
+ await writeDeploymentRecord({ ...record, installedVersion: manifest.version }, recordPath);
296
+ console.log(`\nWikimemory ${manifest.version} is ready. Schema: ${manifest.schemaVersion}`);
297
+ }
298
+ finally {
299
+ await rm(temporary, { recursive: true, force: true });
300
+ }
301
+ }
@@ -0,0 +1,2 @@
1
+ export const WIKIMEMORY_VERSION = "0.2.0";
2
+ export const LATEST_SCHEMA_VERSION = "0004_credential_bound_registration_tokens.sql";
@@ -0,0 +1 @@
1
+ :root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bg:#f5f6f8;--panel:#fff;--ink:#19211e;--muted:#66716c;--line:#dfe4e1;--accent:#176b52;--danger:#a83a3a}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}*{box-sizing:border-box}body{background:var(--bg);color:var(--ink);margin:0;font:400 1rem/1.6 ui-sans-serif,system-ui,sans-serif}a{color:var(--accent)}header{border-bottom:1px solid var(--line);background:var(--panel);flex-wrap:wrap;align-items:center;gap:.5rem;padding:.75rem max(1rem,50% - 36rem);display:flex}header a{padding:.4rem;text-decoration:none}.brand{align-items:center;gap:.5rem;margin-right:auto;font-weight:800;display:flex}.brand span{background:var(--accent);color:#fff;border-radius:.55rem;place-items:center;width:2rem;height:2rem;display:grid}main{width:min(72rem,100% - 2rem);margin:auto;padding:3rem 0 6rem}.center{place-items:center;min-height:100vh;padding:1rem;display:grid}.center .panel{width:min(32rem,100%)}h1,h2{line-height:1.2}.lede,.muted,small{color:var(--muted)}.panel,.card{border:1px solid var(--line);background:var(--panel);border-radius:.85rem;padding:1.25rem}.panel{margin-bottom:1rem}.grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,19rem),1fr));gap:1rem;display:grid}.meta{flex-wrap:wrap;gap:.4rem;display:flex}.meta span{background:#e7f4ef;border-radius:999px;padding:.1rem .45rem;font-size:.8rem}.search{gap:.5rem;margin:1rem 0 2rem;display:flex}input{background:var(--panel);width:100%;color:inherit;font:inherit;border:1px solid #b8c2bd;border-radius:.55rem;padding:.7rem}label{margin:1rem 0;font-weight:650;display:block}button,.button{border:1px solid var(--accent);background:var(--accent);color:#fff;cursor:pointer;border-radius:.55rem;padding:.7rem .95rem;font:600 .9rem system-ui;text-decoration:none;display:inline-flex}button:disabled{opacity:.5}.danger{border-color:var(--danger);color:var(--danger);background:0 0;margin-top:.5rem}.list{padding:0;list-style:none}.list li{border-bottom:1px solid var(--line);padding:.75rem 0}.list small{display:block}.notice{border-left:.3rem solid var(--accent);background:#e7f4ef;padding:.7rem 1rem}.document-body{border:1px solid var(--line);background:var(--panel);color:inherit;font:inherit;white-space:pre-wrap;overflow-wrap:anywhere;border-radius:.85rem;margin:2rem 0;padding:1.25rem;overflow-x:auto}.metadata{gap:.5rem;margin:1.5rem 0;display:grid}.metadata div{grid-template-columns:minmax(7rem,.25fr) 1fr;gap:1rem;display:grid}.metadata dt{color:var(--muted);font-weight:650}.metadata dd{margin:0}@media (prefers-color-scheme:dark){:root{--bg:#111714;--panel:#19201d;--ink:#edf3ef;--muted:#a6b0ab;--line:#2c3732;--accent:#74c9aa;--danger:#ef8585}.meta span,.notice{background:#183a2f}}