wikimemory 0.2.5 → 0.2.7

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.
@@ -3,7 +3,7 @@ import { mkdir } from "node:fs/promises";
3
3
  import process from "node:process";
4
4
  import { WIKIMEMORY_VERSION } from "../src/version.js";
5
5
  import { deploymentArguments, installArguments } from "./cli-options.js";
6
- import { deploymentPaths } from "./deployment-record.js";
6
+ import { deploymentPaths, requireInstalledDeployment } from "./deployment-record.js";
7
7
  import { packageRoot } from "./package-root.js";
8
8
  import { conciseError } from "./subprocess.js";
9
9
  const [command, ...args] = process.argv.slice(2);
@@ -34,6 +34,19 @@ async function main() {
34
34
  }
35
35
  const parsed = deploymentArguments(args);
36
36
  const paths = deploymentPaths(parsed.deployment);
37
+ const requiresInstalledDeployment = !parsed.remaining.includes("--help") &&
38
+ (command === "recover" ||
39
+ command === "status" ||
40
+ command === "passkeys" ||
41
+ command === "connect" ||
42
+ command === "uninstall" ||
43
+ (command === "upgrade" && !parsed.remaining.includes("--record")));
44
+ if (requiresInstalledDeployment) {
45
+ const requirement = command === "recover" || command === "passkeys" || command === "uninstall"
46
+ ? "config"
47
+ : "record";
48
+ await requireInstalledDeployment(parsed.deployment, requirement);
49
+ }
37
50
  if (command === "install" || command === "recover") {
38
51
  await mkdir(paths.directory, { recursive: true, mode: 0o700 });
39
52
  process.env["WIKIMEMORY_PACKAGE_ROOT"] = root;
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import { z } from "zod";
@@ -44,6 +44,43 @@ export function deploymentPaths(deployment = "wikimemory") {
44
44
  passkeyClient: join(directory, "passkey-client.json")
45
45
  };
46
46
  }
47
+ export async function requireInstalledDeployment(deployment, requirement = "record") {
48
+ const requestedPaths = deploymentPaths(deployment);
49
+ const requested = requirement === "record" ? requestedPaths.record : requestedPaths.config;
50
+ try {
51
+ await access(requested);
52
+ return;
53
+ }
54
+ catch (error) {
55
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
56
+ throw error;
57
+ }
58
+ const configRoot = process.env["XDG_CONFIG_HOME"] ?? join(homedir(), ".config");
59
+ const deploymentsRoot = join(configRoot, "wikimemory", "deployments");
60
+ let installed = [];
61
+ try {
62
+ const entries = (await readdir(deploymentsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && DEPLOYMENT_NAME.test(entry.name));
63
+ const candidates = await Promise.all(entries.map(async (entry) => {
64
+ try {
65
+ await access(join(deploymentsRoot, entry.name, "deployment.json"));
66
+ return entry.name;
67
+ }
68
+ catch (error) {
69
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
70
+ return null;
71
+ throw error;
72
+ }
73
+ }));
74
+ installed = candidates.filter((name) => name !== null).sort();
75
+ }
76
+ catch (error) {
77
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
78
+ throw error;
79
+ }
80
+ if (installed.length === 0)
81
+ throw new Error(`No deployment named “${deployment}”. Run wikimemory install first.`);
82
+ throw new Error(`No deployment named “${deployment}”. Installed: ${installed.join(", ")}. Use --deployment NAME.`);
83
+ }
47
84
  export function deploymentRecordFromConfig(configText, installedVersion, kvName) {
48
85
  const config = PRODUCTION_CONFIG_SCHEMA.parse(JSON.parse(configText));
49
86
  const database = config.d1_databases.at(0);
@@ -0,0 +1,4 @@
1
+ export const DEPLOYMENT_VERIFY_ATTEMPTS = 25;
2
+ export function deploymentVerifyDelay(completedAttempt) {
3
+ return Math.min(1000 * 2 ** completedAttempt, 5000);
4
+ }
@@ -9,6 +9,7 @@ import { pathToFileURL } from "node:url";
9
9
  import { z } from "zod";
10
10
  import { WIKIMEMORY_VERSION } from "../src/version.js";
11
11
  import { deploymentRecordFromConfig, writeDeploymentRecord } from "./deployment-record.js";
12
+ import { DEPLOYMENT_VERIFY_ATTEMPTS, deploymentVerifyDelay } from "./deployment-wait.js";
12
13
  import { installedRecordPath, packageRoot, setupRuntime } from "./lifecycle-runtime.js";
13
14
  import { commandFailureMessage, conciseError, runCommand } from "./subprocess.js";
14
15
  export { commandFailureMessage } from "./subprocess.js";
@@ -319,7 +320,7 @@ function bootstrapSecret() {
319
320
  const hash = createHash("sha256").update(raw, "utf8").digest("hex");
320
321
  return { raw, hash };
321
322
  }
322
- export async function verifyEndpoint(origin, path, expectedStatus, fetcher = fetch, sleeper = sleep, attempts = 6) {
323
+ export async function verifyEndpoint(origin, path, expectedStatus, fetcher = fetch, sleeper = sleep, attempts = DEPLOYMENT_VERIFY_ATTEMPTS) {
323
324
  let finalDetail = "no response";
324
325
  for (let attempt = 0; attempt < attempts; attempt += 1) {
325
326
  try {
@@ -344,7 +345,7 @@ export async function verifyEndpoint(origin, path, expectedStatus, fetcher = fet
344
345
  finalDetail = error instanceof Error ? error.message : "Unknown request failure";
345
346
  }
346
347
  if (attempt + 1 < attempts)
347
- await sleeper(250 * 2 ** attempt);
348
+ await sleeper(deploymentVerifyDelay(attempt));
348
349
  }
349
350
  throw new Error(`Deployment ${path} check failed after ${attempts} attempts: ${finalDetail}`);
350
351
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { deploymentRecordPath, readDeploymentRecord } from "./deployment-record.js";
3
+ import { isReactApplicationShell } from "./web-shell.js";
3
4
  const HEALTH_SCHEMA = z.object({
4
5
  status: z.literal("ok"),
5
6
  service: z.literal("wikimemory"),
@@ -29,7 +30,7 @@ export async function deploymentStatus(deployment) {
29
30
  throw new Error("Health and schema versions disagree");
30
31
  if (discovery.resource !== `${record.origin}/mcp`)
31
32
  throw new Error("OAuth discovery reports an unexpected MCP resource");
32
- if (!(await appResponse.text()).includes('<div id="root"></div>'))
33
+ if (!isReactApplicationShell(await appResponse.text()))
33
34
  throw new Error("React application shell verification failed");
34
35
  return {
35
36
  deployment,
@@ -6,8 +6,10 @@ import process from "node:process";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import { z } from "zod";
8
8
  import { deploymentRecordPath, readDeploymentRecord, writeDeploymentRecord } from "./deployment-record.js";
9
+ import { DEPLOYMENT_VERIFY_ATTEMPTS, deploymentVerifyDelay } from "./deployment-wait.js";
9
10
  import { packageRoot } from "./package-root.js";
10
11
  import { commandFailureMessage, runCommand } from "./subprocess.js";
12
+ import { isReactApplicationShell } from "./web-shell.js";
11
13
  const PACKAGE_ROOT = packageRoot();
12
14
  const DEPLOYMENT_NAME = /^[a-z0-9][a-z0-9-]{0,62}$/u;
13
15
  const MIGRATION_NAME = /^\d{4}_[a-z0-9_]+\.sql$/u;
@@ -173,7 +175,7 @@ async function responseJson(response, label) {
173
175
  throw new Error(`${label} returned non-JSON content`);
174
176
  }
175
177
  }
176
- export async function verifyRelease(origin, manifest, fetcher = fetch, sleeper = sleep, attempts = 6) {
178
+ export async function verifyRelease(origin, manifest, fetcher = fetch, sleeper = sleep, attempts = DEPLOYMENT_VERIFY_ATTEMPTS) {
177
179
  let finalError;
178
180
  for (let attempt = 0; attempt < attempts; attempt += 1) {
179
181
  try {
@@ -203,14 +205,14 @@ export async function verifyRelease(origin, manifest, fetcher = fetch, sleeper =
203
205
  if (!discovery.ok)
204
206
  throw new Error(`OAuth protected-resource discovery returned HTTP ${discovery.status}`);
205
207
  const app = await fetcher(`${origin}/app`, { redirect: "error" });
206
- if (!app.ok || !(await app.text()).includes('<div id="root"></div>'))
208
+ if (!app.ok || !isReactApplicationShell(await app.text()))
207
209
  throw new Error("React application verification failed");
208
210
  return;
209
211
  }
210
212
  catch (error) {
211
213
  finalError = error;
212
214
  if (attempt + 1 < attempts)
213
- await sleeper(250 * 2 ** attempt);
215
+ await sleeper(deploymentVerifyDelay(attempt));
214
216
  }
215
217
  }
216
218
  const detail = finalError instanceof Error ? finalError.message : "Unknown verification failure";
@@ -0,0 +1,5 @@
1
+ const ROOT_CONTAINER = /<div\s+id=["']root["'][^>]*>/u;
2
+ const MODULE_ASSET = /<script\s+[^>]*type=["']module["'][^>]*src=["']\/assets\//u;
3
+ export function isReactApplicationShell(html) {
4
+ return ROOT_CONTAINER.test(html) && MODULE_ASSET.test(html);
5
+ }
@@ -1,2 +1,2 @@
1
- export const WIKIMEMORY_VERSION = "0.2.5";
1
+ export const WIKIMEMORY_VERSION = "0.2.7";
2
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}}html{text-size-adjust:100%}*{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}}