wikimemory 0.2.4 → 0.2.6

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,8 +3,9 @@ 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
+ import { conciseError } from "./subprocess.js";
8
9
  const [command, ...args] = process.argv.slice(2);
9
10
  function usage() {
10
11
  return `Wikimemory ${WIKIMEMORY_VERSION}\nPersonal, passkey-protected memory for Claude, Codex, and other MCP clients.\n\nUsage: wikimemory COMMAND [OPTIONS]\n\nStart a personal Cloudflare-hosted instance:\n npx --yes wikimemory install\n\nTry it locally without a Cloudflare deployment:\n npx --yes wikimemory dev\n\nCommands:\n wikimemory install [--deployment NAME] [installer options]\n wikimemory recover [--deployment NAME]\n wikimemory dev [wrangler dev options]\n wikimemory status [--deployment NAME]\n wikimemory upgrade [--deployment NAME] [--yes]\n wikimemory passkeys [--deployment NAME] list|add|revoke\n wikimemory connect [--deployment NAME] codex|claude\n wikimemory skills install codex|claude\n wikimemory uninstall [--deployment NAME] [--apply]\n wikimemory --version\n\nUse \`wikimemory COMMAND --help\` for command-specific options.`;
@@ -33,6 +34,18 @@ async function main() {
33
34
  }
34
35
  const parsed = deploymentArguments(args);
35
36
  const paths = deploymentPaths(parsed.deployment);
37
+ const requiresInstalledDeployment = command === "recover" ||
38
+ command === "status" ||
39
+ command === "passkeys" ||
40
+ command === "connect" ||
41
+ command === "uninstall" ||
42
+ (command === "upgrade" && !parsed.remaining.includes("--record"));
43
+ if (requiresInstalledDeployment) {
44
+ const requirement = command === "recover" || command === "passkeys" || command === "uninstall"
45
+ ? "config"
46
+ : "record";
47
+ await requireInstalledDeployment(parsed.deployment, requirement);
48
+ }
36
49
  if (command === "install" || command === "recover") {
37
50
  await mkdir(paths.directory, { recursive: true, mode: 0o700 });
38
51
  process.env["WIKIMEMORY_PACKAGE_ROOT"] = root;
@@ -74,6 +87,6 @@ async function main() {
74
87
  throw new Error(`Unknown command: ${command}`);
75
88
  }
76
89
  main().catch((error) => {
77
- console.error(`Wikimemory failed: ${error instanceof Error ? error.message : "Unknown error"}`);
90
+ console.error(`Wikimemory failed: ${conciseError(error)}`);
78
91
  process.exitCode = 1;
79
92
  });
@@ -1,24 +1,19 @@
1
- import { spawn } from "node:child_process";
2
1
  import { cp, mkdir } from "node:fs/promises";
3
2
  import { homedir } from "node:os";
4
3
  import { join } from "node:path";
5
4
  import { deploymentRecordPath, readDeploymentRecord } from "./deployment-record.js";
5
+ import { commandFailureMessage, runCommand } from "./subprocess.js";
6
6
  function client(value) {
7
7
  if (value === "codex" || value === "claude")
8
8
  return value;
9
9
  throw new Error("Client must be codex or claude");
10
10
  }
11
- async function run(command, args) {
12
- await new Promise((resolve, reject) => {
13
- const child = spawn(command, args, { stdio: "inherit" });
14
- child.on("error", reject);
15
- child.on("close", (code) => {
16
- if (code === 0)
17
- resolve();
18
- else
19
- reject(new Error(`${command} exited with status ${code ?? "unknown"}`));
20
- });
11
+ async function run(command, args, interactive = false) {
12
+ const result = await runCommand(command, args, {
13
+ ...(interactive ? { forwardLimitBytes: 4000, inheritStdin: true } : {})
21
14
  });
15
+ if (result.exitCode !== 0)
16
+ throw new Error(commandFailureMessage(`${command} client configuration`, result));
22
17
  }
23
18
  export async function connectClient(deployment, selected) {
24
19
  const target = client(selected);
@@ -26,7 +21,8 @@ export async function connectClient(deployment, selected) {
26
21
  const endpoint = `${record.origin}/mcp`;
27
22
  if (target === "codex") {
28
23
  await run("codex", ["mcp", "add", deployment, "--url", endpoint]);
29
- await run("codex", ["mcp", "login", deployment, "--scopes", "memory:read,memory:write"]);
24
+ console.log("Complete authorization in your browser…");
25
+ await run("codex", ["mcp", "login", deployment, "--scopes", "memory:read,memory:write"], true);
30
26
  }
31
27
  else {
32
28
  await run("claude", [
@@ -39,8 +35,10 @@ export async function connectClient(deployment, selected) {
39
35
  deployment,
40
36
  endpoint
41
37
  ]);
42
- await run("claude", ["mcp", "login", deployment]);
38
+ console.log("Complete authorization in your browser…");
39
+ await run("claude", ["mcp", "login", deployment], true);
43
40
  }
41
+ console.log(`Connected ${target} to ${deployment}.`);
44
42
  }
45
43
  export async function installSkills(packageRoot, selected) {
46
44
  const target = client(selected);
@@ -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
+ }
@@ -1,6 +1,6 @@
1
- import { spawn } from "node:child_process";
2
1
  import { mkdir, writeFile } from "node:fs/promises";
3
2
  import { join, resolve } from "node:path";
3
+ import { commandFailureMessage, runCommand } from "./subprocess.js";
4
4
  export function localConfig(packageRoot) {
5
5
  return `${JSON.stringify({
6
6
  $schema: join(packageRoot, "node_modules", "wrangler", "config-schema.json"),
@@ -27,24 +27,19 @@ export function localConfig(packageRoot) {
27
27
  }, null, 2)}\n`;
28
28
  }
29
29
  async function runWrangler(args, inherited = false) {
30
- await new Promise((resolvePromise, reject) => {
31
- const child = spawn("npx", ["wrangler", ...args], {
32
- stdio: inherited ? "inherit" : ["ignore", "inherit", "inherit"]
33
- });
34
- child.on("error", reject);
35
- child.on("close", (code) => {
36
- if (code === 0)
37
- resolvePromise();
38
- else
39
- reject(new Error(`Wrangler exited with status ${code ?? "unknown"}`));
40
- });
30
+ const result = await runCommand("npx", ["wrangler", ...args], {
31
+ ...(inherited ? { forwardLimitBytes: 8000, inheritStdin: true } : {})
41
32
  });
33
+ if (result.exitCode !== 0)
34
+ throw new Error(commandFailureMessage("Wrangler", result));
42
35
  }
43
36
  export async function runDev(packageRoot, args) {
44
37
  const stateDirectory = resolve(".wikimemory", "dev");
45
38
  await mkdir(stateDirectory, { recursive: true });
46
39
  const configPath = join(stateDirectory, "wrangler.jsonc");
47
40
  await writeFile(configPath, localConfig(packageRoot), "utf8");
41
+ console.log("Preparing local database…");
48
42
  await runWrangler(["d1", "migrations", "apply", "wikimemory", "--local", "--config", configPath]);
43
+ console.log("Starting local Wikimemory…");
49
44
  await runWrangler(["dev", "--config", configPath, ...args], true);
50
45
  }
@@ -7,6 +7,7 @@ import { pathToFileURL } from "node:url";
7
7
  import { z } from "zod";
8
8
  import { passkeyRuntime } from "./lifecycle-runtime.js";
9
9
  import { configuredOrigin } from "./setup.js";
10
+ import { conciseError } from "./subprocess.js";
10
11
  const PRODUCTION_CONFIG = passkeyRuntime.config;
11
12
  const CLI_STATE = passkeyRuntime.client;
12
13
  const CALLBACK = "http://127.0.0.1:45831/callback";
@@ -191,7 +192,7 @@ export async function runPasskeys(args) {
191
192
  const entrypoint = process.argv[1];
192
193
  if (entrypoint !== undefined && import.meta.url === pathToFileURL(entrypoint).href) {
193
194
  runPasskeys(process.argv.slice(2)).catch((error) => {
194
- console.error(`Passkey command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
195
+ console.error(`Passkey command failed: ${conciseError(error)}`);
195
196
  process.exitCode = 1;
196
197
  });
197
198
  }
@@ -1,4 +1,3 @@
1
- import { spawn } from "node:child_process";
2
1
  import { createHash, randomBytes } from "node:crypto";
3
2
  import { constants } from "node:fs";
4
3
  import { access, mkdtemp, readdir, readFile, rm, unlink, writeFile } from "node:fs/promises";
@@ -10,7 +9,10 @@ import { pathToFileURL } from "node:url";
10
9
  import { z } from "zod";
11
10
  import { WIKIMEMORY_VERSION } from "../src/version.js";
12
11
  import { deploymentRecordFromConfig, writeDeploymentRecord } from "./deployment-record.js";
12
+ import { DEPLOYMENT_VERIFY_ATTEMPTS, deploymentVerifyDelay } from "./deployment-wait.js";
13
13
  import { installedRecordPath, packageRoot, setupRuntime } from "./lifecycle-runtime.js";
14
+ import { commandFailureMessage, conciseError, runCommand } from "./subprocess.js";
15
+ export { commandFailureMessage } from "./subprocess.js";
14
16
  const PACKAGE_ROOT = packageRoot;
15
17
  const CONFIG_PATH = setupRuntime.config;
16
18
  const STATE_PATH = setupRuntime.progress;
@@ -55,16 +57,6 @@ function cloudflareOperation(args) {
55
57
  return "KV namespace creation";
56
58
  return `Cloudflare ${command}`;
57
59
  }
58
- function conciseDiagnostic(result) {
59
- const raw = result.stderr.trim() || result.stdout.trim();
60
- const plain = raw.replaceAll("\u001b", "").replaceAll(/\s+/gu, " ").trim();
61
- if (plain === "")
62
- return `process exited with status ${result.exitCode}`;
63
- return plain.length > 600 ? `${plain.slice(0, 597)}...` : plain;
64
- }
65
- export function commandFailureMessage(operation, result) {
66
- return `${operation} failed: ${conciseDiagnostic(result)}`;
67
- }
68
60
  async function sleep(milliseconds) {
69
61
  await new Promise((resolve) => {
70
62
  setTimeout(resolve, milliseconds);
@@ -146,44 +138,18 @@ async function exists(path) {
146
138
  }
147
139
  }
148
140
  async function run(command, args, input, allowFailure = false) {
149
- const result = await new Promise((resolve, reject) => {
150
- const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
151
- let stdout = "";
152
- let stderr = "";
153
- child.stdout.setEncoding("utf8");
154
- child.stderr.setEncoding("utf8");
155
- child.stdout.on("data", (chunk) => {
156
- stdout += chunk;
157
- });
158
- child.stderr.on("data", (chunk) => {
159
- stderr += chunk;
160
- });
161
- child.on("error", (error) => {
162
- reject(error);
163
- });
164
- child.on("close", (code) => {
165
- resolve({ stdout, stderr, exitCode: code ?? -1 });
166
- });
167
- child.stdin.end(input === undefined ? undefined : `${input}\n`);
168
- });
141
+ const result = await runCommand(command, args, { ...(input === undefined ? {} : { input }) });
169
142
  if (result.exitCode !== 0 && !allowFailure)
170
143
  throw new Error(commandFailureMessage(cloudflareOperation(args), result));
171
144
  return result;
172
145
  }
173
146
  async function runInteractive(command, args) {
174
- console.log(`\n> ${command} ${args.join(" ")}`);
175
- await new Promise((resolve, reject) => {
176
- const child = spawn(command, args, { stdio: "inherit" });
177
- child.on("error", (error) => {
178
- reject(error);
179
- });
180
- child.on("close", (code) => {
181
- if (code === 0)
182
- resolve();
183
- else
184
- reject(new Error(`${command} exited with status ${code ?? "unknown"}`));
185
- });
147
+ const result = await runCommand(command, args, {
148
+ forwardLimitBytes: 4000,
149
+ inheritStdin: true
186
150
  });
151
+ if (result.exitCode !== 0)
152
+ throw new Error(commandFailureMessage(cloudflareOperation(args), result));
187
153
  }
188
154
  export function initialConfig(options, account) {
189
155
  return `${JSON.stringify({
@@ -354,7 +320,7 @@ function bootstrapSecret() {
354
320
  const hash = createHash("sha256").update(raw, "utf8").digest("hex");
355
321
  return { raw, hash };
356
322
  }
357
- 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) {
358
324
  let finalDetail = "no response";
359
325
  for (let attempt = 0; attempt < attempts; attempt += 1) {
360
326
  try {
@@ -379,7 +345,7 @@ export async function verifyEndpoint(origin, path, expectedStatus, fetcher = fet
379
345
  finalDetail = error instanceof Error ? error.message : "Unknown request failure";
380
346
  }
381
347
  if (attempt + 1 < attempts)
382
- await sleeper(250 * 2 ** attempt);
348
+ await sleeper(deploymentVerifyDelay(attempt));
383
349
  }
384
350
  throw new Error(`Deployment ${path} check failed after ${attempts} attempts: ${finalDetail}`);
385
351
  }
@@ -504,7 +470,6 @@ async function finalizeDeployment(options) {
504
470
  const deploymentRecord = deploymentRecordFromConfig(finalConfig, WIKIMEMORY_VERSION, options.kvName);
505
471
  const recordPath = installedRecordPath(options.workerName);
506
472
  await writeDeploymentRecord(deploymentRecord, recordPath);
507
- console.log(`\nSaved deployment record: ${recordPath}`);
508
473
  console.log(`\n${handoff(origin, secret.raw, options.workerName)}`);
509
474
  }
510
475
  async function freshDeployment(options) {
@@ -602,7 +567,7 @@ export async function runSetup(args) {
602
567
  const entrypoint = process.argv[1];
603
568
  if (entrypoint !== undefined && import.meta.url === pathToFileURL(entrypoint).href) {
604
569
  runSetup(process.argv.slice(2)).catch((error) => {
605
- console.error(`\nSetup failed: ${error instanceof Error ? error.message : "Unknown error"}`);
570
+ console.error(`\nSetup failed: ${conciseError(error)}`);
606
571
  process.exitCode = 1;
607
572
  });
608
573
  }
@@ -45,5 +45,11 @@ export async function deploymentStatus(deployment) {
45
45
  }
46
46
  export async function runStatus(deployment) {
47
47
  const status = await deploymentStatus(deployment);
48
- console.log(`Wikimemory deployment: ${status.deployment}\n Account: ${status.accountId}\n Worker: ${status.workerName}\n D1: ${status.database}\n KV: ${status.kvNamespace}\n Origin: ${status.origin}\n Recorded version: ${status.recordedVersion}\n Running version: ${status.runningVersion}\n Schema: ${status.schemaVersion}`);
48
+ console.log(statusSummary(status));
49
+ }
50
+ export function statusSummary(status) {
51
+ const mismatch = status.recordedVersion === status.runningVersion
52
+ ? ""
53
+ : `\nLocal record: ${status.recordedVersion} (run wikimemory upgrade to reconcile)`;
54
+ return `Wikimemory ${status.deployment}: ready\nURL: ${status.origin}\nVersion: ${status.runningVersion}\nDatabase: up to date.${mismatch}`;
49
55
  }
@@ -0,0 +1,74 @@
1
+ import { spawn } from "node:child_process";
2
+ import process from "node:process";
3
+ import { stripVTControlCharacters } from "node:util";
4
+ export function boundedOutputChunk(chunk, remaining) {
5
+ const visible = chunk.slice(0, remaining);
6
+ return {
7
+ visible,
8
+ remaining: Math.max(0, remaining - visible.length),
9
+ truncated: visible.length < chunk.length
10
+ };
11
+ }
12
+ function forwardBounded(destination, chunk, state) {
13
+ const limited = boundedOutputChunk(chunk, state.remaining);
14
+ if (limited.visible !== "")
15
+ destination.write(limited.visible);
16
+ state.remaining = limited.remaining;
17
+ if (limited.truncated && !state.suppressed) {
18
+ destination.write("\n… additional command output suppressed.\n");
19
+ state.suppressed = true;
20
+ }
21
+ }
22
+ export async function runCommand(command, args, options = {}) {
23
+ return await new Promise((resolve, reject) => {
24
+ const child = spawn(command, args, {
25
+ stdio: [options.inheritStdin === true ? "inherit" : "pipe", "pipe", "pipe"]
26
+ });
27
+ if (child.stdout === null || child.stderr === null) {
28
+ child.kill();
29
+ reject(new Error("Subprocess output pipes were not created"));
30
+ return;
31
+ }
32
+ let stdout = "";
33
+ let stderr = "";
34
+ const stdoutForward = { remaining: options.forwardLimitBytes ?? 0, suppressed: false };
35
+ const stderrForward = { remaining: options.forwardLimitBytes ?? 0, suppressed: false };
36
+ child.stdout.setEncoding("utf8");
37
+ child.stderr.setEncoding("utf8");
38
+ child.stdout.on("data", (chunk) => {
39
+ stdout += chunk;
40
+ if (options.forwardLimitBytes !== undefined)
41
+ forwardBounded(process.stdout, chunk, stdoutForward);
42
+ });
43
+ child.stderr.on("data", (chunk) => {
44
+ stderr += chunk;
45
+ if (options.forwardLimitBytes !== undefined)
46
+ forwardBounded(process.stderr, chunk, stderrForward);
47
+ });
48
+ child.on("error", reject);
49
+ child.on("close", (code) => {
50
+ resolve({ stdout, stderr, exitCode: code ?? -1 });
51
+ });
52
+ if (child.stdin !== null)
53
+ child.stdin.end(options.input === undefined ? undefined : `${options.input}\n`);
54
+ });
55
+ }
56
+ export function conciseDiagnostic(result, maximumLength = 600) {
57
+ const raw = result.stderr.trim() || result.stdout.trim();
58
+ const plain = stripVTControlCharacters(raw).replaceAll(/\s+/gu, " ").trim();
59
+ if (plain === "")
60
+ return `process exited with status ${result.exitCode}`;
61
+ if (plain.length <= maximumLength)
62
+ return plain;
63
+ return `${plain.slice(0, Math.max(0, maximumLength - 3))}...`;
64
+ }
65
+ export function commandFailureMessage(operation, result) {
66
+ return `${operation} failed: ${conciseDiagnostic(result)}`;
67
+ }
68
+ export function conciseError(error, maximumLength = 600) {
69
+ const raw = error instanceof Error ? error.message : "Unknown error";
70
+ const singleLine = stripVTControlCharacters(raw).replaceAll(/\s+/gu, " ").trim();
71
+ if (singleLine.length <= maximumLength)
72
+ return singleLine;
73
+ return `${singleLine.slice(0, Math.max(0, maximumLength - 3))}...`;
74
+ }
@@ -1,10 +1,10 @@
1
- import { spawn } from "node:child_process";
2
1
  import { readFile, unlink, writeFile } from "node:fs/promises";
3
2
  import process from "node:process";
4
3
  import { createInterface } from "node:readline/promises";
5
4
  import { pathToFileURL } from "node:url";
6
5
  import { removePackagedDeploymentRecord, uninstallRuntime } from "./lifecycle-runtime.js";
7
6
  import { bindingProperty, configValue, deploymentListIndicatesExisting } from "./setup.js";
7
+ import { commandFailureMessage, conciseError, runCommand as executeCommand } from "./subprocess.js";
8
8
  const CONFIG_PATH = uninstallRuntime.config;
9
9
  const STATE_PATH = uninstallRuntime.installProgress;
10
10
  const UNINSTALL_STATE_PATH = uninstallRuntime.uninstallProgress;
@@ -54,30 +54,9 @@ export function clientRemovalInstructions(connectorName = "wikimemory") {
54
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
55
  }
56
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
- });
57
+ const result = await executeCommand("npx", args);
79
58
  if (result.exitCode !== 0 && !allowFailure)
80
- throw new Error(`npx exited with status ${result.exitCode}`);
59
+ throw new Error(commandFailureMessage("Cloudflare resource deletion", result));
81
60
  return result;
82
61
  }
83
62
  async function requireExactName(workerName, supplied) {
@@ -145,6 +124,7 @@ export async function runUninstall(args, runCommand = run) {
145
124
  const progress = await loadProgress(targets);
146
125
  await saveProgress(progress);
147
126
  if (!progress.workerDeleted) {
127
+ console.log(" Removing Worker…");
148
128
  const probe = await runCommand([
149
129
  "wrangler",
150
130
  "deployments",
@@ -172,6 +152,7 @@ export async function runUninstall(args, runCommand = run) {
172
152
  await saveProgress(progress);
173
153
  }
174
154
  if (!progress.kvDeleted) {
155
+ console.log(" Removing OAuth state…");
175
156
  await runCommand([
176
157
  "wrangler",
177
158
  "kv",
@@ -187,6 +168,7 @@ export async function runUninstall(args, runCommand = run) {
187
168
  await saveProgress(progress);
188
169
  }
189
170
  if (!progress.databaseDeleted) {
171
+ console.log(" Removing database…");
190
172
  await runCommand([
191
173
  "wrangler",
192
174
  "d1",
@@ -214,7 +196,7 @@ export async function runUninstall(args, runCommand = run) {
214
196
  const entrypoint = process.argv[1];
215
197
  if (entrypoint !== undefined && import.meta.url === pathToFileURL(entrypoint).href) {
216
198
  runUninstall(process.argv.slice(2)).catch((error) => {
217
- console.error(`\nUninstall failed: ${error instanceof Error ? error.message : "Unknown error"}`);
199
+ console.error(`\nUninstall failed: ${conciseError(error)}`);
218
200
  process.exitCode = 1;
219
201
  });
220
202
  }
@@ -6,7 +6,9 @@ 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";
11
+ import { commandFailureMessage, runCommand } from "./subprocess.js";
10
12
  const PACKAGE_ROOT = packageRoot();
11
13
  const DEPLOYMENT_NAME = /^[a-z0-9][a-z0-9-]{0,62}$/u;
12
14
  const MIGRATION_NAME = /^\d{4}_[a-z0-9_]+\.sql$/u;
@@ -134,28 +136,14 @@ function migrationBundle(sql, migration) {
134
136
  return `${sql.trimEnd()}\n\nINSERT INTO d1_migrations(name) VALUES ('${name}');\n`;
135
137
  }
136
138
  async function command(commandName, args) {
137
- const { spawn } = await import("node:child_process");
138
- return await new Promise((resolve, reject) => {
139
- const child = spawn(commandName, args, { stdio: ["ignore", "pipe", "pipe"] });
140
- let stdout = "";
141
- let stderr = "";
142
- child.stdout.setEncoding("utf8");
143
- child.stderr.setEncoding("utf8");
144
- child.stdout.on("data", (chunk) => {
145
- stdout += chunk;
146
- process.stdout.write(chunk);
147
- });
148
- child.stderr.on("data", (chunk) => {
149
- stderr += chunk;
150
- process.stderr.write(chunk);
151
- });
152
- child.on("error", (error) => {
153
- reject(error);
154
- });
155
- child.on("close", (code) => {
156
- resolve({ stdout, stderr, exitCode: code ?? -1 });
157
- });
158
- });
139
+ return await runCommand(commandName, args);
140
+ }
141
+ export function upgradeSummary(record, currentVersion, targetVersion, databaseUpdates) {
142
+ const updates = databaseUpdates === 0 ? "none" : `${databaseUpdates} required`;
143
+ return `Upgrade Wikimemory “${record.workerName}” at ${record.origin}\n Version: ${currentVersion} -> ${targetVersion}\n Database updates: ${updates}`;
144
+ }
145
+ export function readySummary(version, alreadyCurrent = false) {
146
+ return `Wikimemory ${version} is ${alreadyCurrent ? "already " : ""}ready.\nDatabase: up to date.`;
159
147
  }
160
148
  function parsedJson(stdout) {
161
149
  return JSON.parse(stdout);
@@ -186,7 +174,7 @@ async function responseJson(response, label) {
186
174
  throw new Error(`${label} returned non-JSON content`);
187
175
  }
188
176
  }
189
- export async function verifyRelease(origin, manifest, fetcher = fetch, sleeper = sleep, attempts = 6) {
177
+ export async function verifyRelease(origin, manifest, fetcher = fetch, sleeper = sleep, attempts = DEPLOYMENT_VERIFY_ATTEMPTS) {
190
178
  let finalError;
191
179
  for (let attempt = 0; attempt < attempts; attempt += 1) {
192
180
  try {
@@ -223,7 +211,7 @@ export async function verifyRelease(origin, manifest, fetcher = fetch, sleeper =
223
211
  catch (error) {
224
212
  finalError = error;
225
213
  if (attempt + 1 < attempts)
226
- await sleeper(250 * 2 ** attempt);
214
+ await sleeper(deploymentVerifyDelay(attempt));
227
215
  }
228
216
  }
229
217
  const detail = finalError instanceof Error ? finalError.message : "Unknown verification failure";
@@ -264,7 +252,7 @@ export async function runUpgrade(args) {
264
252
  command("npx", ["wrangler", "kv", "namespace", "list", ...common])
265
253
  ]);
266
254
  if ([whoami, deployments, d1, kv].some((result) => result.exitCode !== 0))
267
- throw new Error("Cloudflare target preflight failed");
255
+ throw new Error(commandFailureMessage("Cloudflare target preflight", [whoami, deployments, d1, kv].find((result) => result.exitCode !== 0) ?? whoami));
268
256
  const whoamiBody = z
269
257
  .object({ accounts: z.array(z.object({ id: z.string() })) })
270
258
  .parse(parsedJson(whoami.stdout));
@@ -292,7 +280,7 @@ export async function runUpgrade(args) {
292
280
  "SELECT name FROM d1_migrations ORDER BY id"
293
281
  ]);
294
282
  if (listed.exitCode !== 0)
295
- throw new Error("Could not read installed migrations");
283
+ throw new Error(commandFailureMessage("Database version check", listed));
296
284
  const query = z
297
285
  .array(z.object({ results: z.array(z.object({ name: z.string() })) }))
298
286
  .parse(parsedJson(listed.stdout));
@@ -308,10 +296,12 @@ export async function runUpgrade(args) {
308
296
  if (deploymentIsCurrent(current, manifest, pending)) {
309
297
  await verifyRelease(record.origin, manifest);
310
298
  await writeDeploymentRecord({ ...record, installedVersion: manifest.version }, recordPath);
311
- console.log(`\nWikimemory ${manifest.version} is already ready. Reconciled the local deployment record without redeploying.`);
299
+ console.log(`\n${readySummary(manifest.version, true)}`);
312
300
  return;
313
301
  }
314
- 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);
302
+ await confirmUpgrade(`\n${upgradeSummary(record, current, manifest.version, pending.length)}`, options.yes);
303
+ if (pending.length > 0)
304
+ console.log(" Updating database…");
315
305
  for (const migration of pending) {
316
306
  const bundlePath = join(temporary, migration.name);
317
307
  const sql = await readFile(join(PACKAGE_ROOT, "migrations", migration.name), "utf8");
@@ -327,14 +317,16 @@ export async function runUpgrade(args) {
327
317
  bundlePath
328
318
  ]);
329
319
  if (result.exitCode !== 0)
330
- throw new Error(`Migration failed: ${migration.name}`);
320
+ throw new Error(commandFailureMessage("Database update", result));
331
321
  }
322
+ console.log(" Deploying Wikimemory…");
332
323
  const deployed = await command("npx", ["wrangler", "deploy", "--strict", ...common]);
333
324
  if (deployed.exitCode !== 0)
334
- throw new Error("Worker deployment failed");
325
+ throw new Error(commandFailureMessage("Worker deployment", deployed));
326
+ console.log(" Verifying deployment…");
335
327
  await verifyRelease(record.origin, manifest);
336
328
  await writeDeploymentRecord({ ...record, installedVersion: manifest.version }, recordPath);
337
- console.log(`\nWikimemory ${manifest.version} is ready. Schema: ${manifest.schemaVersion}`);
329
+ console.log(`\n${readySummary(manifest.version)}`);
338
330
  }
339
331
  finally {
340
332
  await rm(temporary, { recursive: true, force: true });
@@ -1,2 +1,2 @@
1
- export const WIKIMEMORY_VERSION = "0.2.4";
1
+ export const WIKIMEMORY_VERSION = "0.2.6";
2
2
  export const LATEST_SCHEMA_VERSION = "0004_credential_bound_registration_tokens.sql";