wikimemory 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/npm-cli/scripts/cli.js +2 -1
- package/dist/npm-cli/scripts/client-tools.js +11 -13
- package/dist/npm-cli/scripts/dev.js +7 -12
- package/dist/npm-cli/scripts/passkeys.js +2 -1
- package/dist/npm-cli/scripts/setup.js +9 -45
- package/dist/npm-cli/scripts/status.js +7 -1
- package/dist/npm-cli/scripts/subprocess.js +74 -0
- package/dist/npm-cli/scripts/uninstall.js +7 -25
- package/dist/npm-cli/scripts/upgrade.js +86 -55
- package/dist/npm-cli/src/version.js +1 -1
- package/package.json +1 -1
- package/release-manifest.json +1 -1
- package/src/version.ts +1 -1
|
@@ -5,6 +5,7 @@ import { WIKIMEMORY_VERSION } from "../src/version.js";
|
|
|
5
5
|
import { deploymentArguments, installArguments } from "./cli-options.js";
|
|
6
6
|
import { deploymentPaths } 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.`;
|
|
@@ -74,6 +75,6 @@ async function main() {
|
|
|
74
75
|
throw new Error(`Unknown command: ${command}`);
|
|
75
76
|
}
|
|
76
77
|
main().catch((error) => {
|
|
77
|
-
console.error(`Wikimemory failed: ${error
|
|
78
|
+
console.error(`Wikimemory failed: ${conciseError(error)}`);
|
|
78
79
|
process.exitCode = 1;
|
|
79
80
|
});
|
|
@@ -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
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
|
31
|
-
|
|
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
|
|
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";
|
|
@@ -11,6 +10,8 @@ import { z } from "zod";
|
|
|
11
10
|
import { WIKIMEMORY_VERSION } from "../src/version.js";
|
|
12
11
|
import { deploymentRecordFromConfig, writeDeploymentRecord } from "./deployment-record.js";
|
|
13
12
|
import { installedRecordPath, packageRoot, setupRuntime } from "./lifecycle-runtime.js";
|
|
13
|
+
import { commandFailureMessage, conciseError, runCommand } from "./subprocess.js";
|
|
14
|
+
export { commandFailureMessage } from "./subprocess.js";
|
|
14
15
|
const PACKAGE_ROOT = packageRoot;
|
|
15
16
|
const CONFIG_PATH = setupRuntime.config;
|
|
16
17
|
const STATE_PATH = setupRuntime.progress;
|
|
@@ -55,16 +56,6 @@ function cloudflareOperation(args) {
|
|
|
55
56
|
return "KV namespace creation";
|
|
56
57
|
return `Cloudflare ${command}`;
|
|
57
58
|
}
|
|
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
59
|
async function sleep(milliseconds) {
|
|
69
60
|
await new Promise((resolve) => {
|
|
70
61
|
setTimeout(resolve, milliseconds);
|
|
@@ -146,44 +137,18 @@ async function exists(path) {
|
|
|
146
137
|
}
|
|
147
138
|
}
|
|
148
139
|
async function run(command, args, input, allowFailure = false) {
|
|
149
|
-
const result = await
|
|
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
|
-
});
|
|
140
|
+
const result = await runCommand(command, args, { ...(input === undefined ? {} : { input }) });
|
|
169
141
|
if (result.exitCode !== 0 && !allowFailure)
|
|
170
142
|
throw new Error(commandFailureMessage(cloudflareOperation(args), result));
|
|
171
143
|
return result;
|
|
172
144
|
}
|
|
173
145
|
async function runInteractive(command, args) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
});
|
|
146
|
+
const result = await runCommand(command, args, {
|
|
147
|
+
forwardLimitBytes: 4000,
|
|
148
|
+
inheritStdin: true
|
|
186
149
|
});
|
|
150
|
+
if (result.exitCode !== 0)
|
|
151
|
+
throw new Error(commandFailureMessage(cloudflareOperation(args), result));
|
|
187
152
|
}
|
|
188
153
|
export function initialConfig(options, account) {
|
|
189
154
|
return `${JSON.stringify({
|
|
@@ -504,7 +469,6 @@ async function finalizeDeployment(options) {
|
|
|
504
469
|
const deploymentRecord = deploymentRecordFromConfig(finalConfig, WIKIMEMORY_VERSION, options.kvName);
|
|
505
470
|
const recordPath = installedRecordPath(options.workerName);
|
|
506
471
|
await writeDeploymentRecord(deploymentRecord, recordPath);
|
|
507
|
-
console.log(`\nSaved deployment record: ${recordPath}`);
|
|
508
472
|
console.log(`\n${handoff(origin, secret.raw, options.workerName)}`);
|
|
509
473
|
}
|
|
510
474
|
async function freshDeployment(options) {
|
|
@@ -602,7 +566,7 @@ export async function runSetup(args) {
|
|
|
602
566
|
const entrypoint = process.argv[1];
|
|
603
567
|
if (entrypoint !== undefined && import.meta.url === pathToFileURL(entrypoint).href) {
|
|
604
568
|
runSetup(process.argv.slice(2)).catch((error) => {
|
|
605
|
-
console.error(`\nSetup failed: ${error
|
|
569
|
+
console.error(`\nSetup failed: ${conciseError(error)}`);
|
|
606
570
|
process.exitCode = 1;
|
|
607
571
|
});
|
|
608
572
|
}
|
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
199
|
+
console.error(`\nUninstall failed: ${conciseError(error)}`);
|
|
218
200
|
process.exitCode = 1;
|
|
219
201
|
});
|
|
220
202
|
}
|
|
@@ -7,6 +7,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { deploymentRecordPath, readDeploymentRecord, writeDeploymentRecord } from "./deployment-record.js";
|
|
9
9
|
import { packageRoot } from "./package-root.js";
|
|
10
|
+
import { commandFailureMessage, runCommand } from "./subprocess.js";
|
|
10
11
|
const PACKAGE_ROOT = packageRoot();
|
|
11
12
|
const DEPLOYMENT_NAME = /^[a-z0-9][a-z0-9-]{0,62}$/u;
|
|
12
13
|
const MIGRATION_NAME = /^\d{4}_[a-z0-9_]+\.sql$/u;
|
|
@@ -79,6 +80,10 @@ export function compareSemanticVersions(left, right) {
|
|
|
79
80
|
}
|
|
80
81
|
return 0;
|
|
81
82
|
}
|
|
83
|
+
export function deploymentIsCurrent(currentVersion, manifest, pendingMigrations) {
|
|
84
|
+
return (compareSemanticVersions(currentVersion, manifest.version) === 0 &&
|
|
85
|
+
pendingMigrations.length === 0);
|
|
86
|
+
}
|
|
82
87
|
export function productionUpgradeConfig(record, packageRoot) {
|
|
83
88
|
return `${JSON.stringify({
|
|
84
89
|
$schema: join(packageRoot, "node_modules", "wrangler", "config-schema.json"),
|
|
@@ -130,28 +135,14 @@ function migrationBundle(sql, migration) {
|
|
|
130
135
|
return `${sql.trimEnd()}\n\nINSERT INTO d1_migrations(name) VALUES ('${name}');\n`;
|
|
131
136
|
}
|
|
132
137
|
async function command(commandName, args) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
stdout += chunk;
|
|
142
|
-
process.stdout.write(chunk);
|
|
143
|
-
});
|
|
144
|
-
child.stderr.on("data", (chunk) => {
|
|
145
|
-
stderr += chunk;
|
|
146
|
-
process.stderr.write(chunk);
|
|
147
|
-
});
|
|
148
|
-
child.on("error", (error) => {
|
|
149
|
-
reject(error);
|
|
150
|
-
});
|
|
151
|
-
child.on("close", (code) => {
|
|
152
|
-
resolve({ stdout, stderr, exitCode: code ?? -1 });
|
|
153
|
-
});
|
|
154
|
-
});
|
|
138
|
+
return await runCommand(commandName, args);
|
|
139
|
+
}
|
|
140
|
+
export function upgradeSummary(record, currentVersion, targetVersion, databaseUpdates) {
|
|
141
|
+
const updates = databaseUpdates === 0 ? "none" : `${databaseUpdates} required`;
|
|
142
|
+
return `Upgrade Wikimemory “${record.workerName}” at ${record.origin}\n Version: ${currentVersion} -> ${targetVersion}\n Database updates: ${updates}`;
|
|
143
|
+
}
|
|
144
|
+
export function readySummary(version, alreadyCurrent = false) {
|
|
145
|
+
return `Wikimemory ${version} is ${alreadyCurrent ? "already " : ""}ready.\nDatabase: up to date.`;
|
|
155
146
|
}
|
|
156
147
|
function parsedJson(stdout) {
|
|
157
148
|
return JSON.parse(stdout);
|
|
@@ -166,34 +157,64 @@ async function confirmUpgrade(summary, automatic) {
|
|
|
166
157
|
if (answer !== "y" && answer !== "yes")
|
|
167
158
|
throw new Error("Cancelled");
|
|
168
159
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
.object({ status: z.literal("ok"), service: z.literal("wikimemory"), version: z.string() })
|
|
173
|
-
.parse(await health.json());
|
|
174
|
-
if (!health.ok || healthBody.version !== manifest.version)
|
|
175
|
-
throw new Error("Deployed Worker version verification failed");
|
|
176
|
-
const ready = await fetch(`${origin}/ready`, { redirect: "error" });
|
|
177
|
-
const readyBody = z
|
|
178
|
-
.object({
|
|
179
|
-
status: z.literal("ready"),
|
|
180
|
-
service: z.literal("wikimemory"),
|
|
181
|
-
version: z.string(),
|
|
182
|
-
schemaVersion: z.string()
|
|
183
|
-
})
|
|
184
|
-
.parse(await ready.json());
|
|
185
|
-
if (!ready.ok ||
|
|
186
|
-
readyBody.version !== manifest.version ||
|
|
187
|
-
readyBody.schemaVersion !== manifest.schemaVersion)
|
|
188
|
-
throw new Error("Deployed schema version verification failed");
|
|
189
|
-
const discovery = await fetch(`${origin}/.well-known/oauth-protected-resource/mcp`, {
|
|
190
|
-
redirect: "error"
|
|
160
|
+
function sleep(milliseconds) {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
setTimeout(resolve, milliseconds);
|
|
191
163
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
if (!
|
|
196
|
-
throw new Error(
|
|
164
|
+
}
|
|
165
|
+
async function responseJson(response, label) {
|
|
166
|
+
const text = await response.text();
|
|
167
|
+
if (!response.ok)
|
|
168
|
+
throw new Error(`${label} returned HTTP ${response.status}: ${text.slice(0, 300)}`);
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(text);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
throw new Error(`${label} returned non-JSON content`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export async function verifyRelease(origin, manifest, fetcher = fetch, sleeper = sleep, attempts = 6) {
|
|
177
|
+
let finalError;
|
|
178
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
179
|
+
try {
|
|
180
|
+
const health = await fetcher(`${origin}/health`, { redirect: "error" });
|
|
181
|
+
const healthBody = z
|
|
182
|
+
.object({ status: z.literal("ok"), service: z.literal("wikimemory"), version: z.string() })
|
|
183
|
+
.parse(await responseJson(health, "Health endpoint"));
|
|
184
|
+
if (healthBody.version !== manifest.version) {
|
|
185
|
+
throw new Error(`Health endpoint reports version ${healthBody.version}; expected ${manifest.version}`);
|
|
186
|
+
}
|
|
187
|
+
const ready = await fetcher(`${origin}/ready`, { redirect: "error" });
|
|
188
|
+
const readyBody = z
|
|
189
|
+
.object({
|
|
190
|
+
status: z.literal("ready"),
|
|
191
|
+
service: z.literal("wikimemory"),
|
|
192
|
+
version: z.string(),
|
|
193
|
+
schemaVersion: z.string()
|
|
194
|
+
})
|
|
195
|
+
.parse(await responseJson(ready, "Readiness endpoint"));
|
|
196
|
+
if (readyBody.version !== manifest.version ||
|
|
197
|
+
readyBody.schemaVersion !== manifest.schemaVersion) {
|
|
198
|
+
throw new Error(`Readiness endpoint reports version ${readyBody.version} and schema ${readyBody.schemaVersion}; expected ${manifest.version} and ${manifest.schemaVersion}`);
|
|
199
|
+
}
|
|
200
|
+
const discovery = await fetcher(`${origin}/.well-known/oauth-protected-resource/mcp`, {
|
|
201
|
+
redirect: "error"
|
|
202
|
+
});
|
|
203
|
+
if (!discovery.ok)
|
|
204
|
+
throw new Error(`OAuth protected-resource discovery returned HTTP ${discovery.status}`);
|
|
205
|
+
const app = await fetcher(`${origin}/app`, { redirect: "error" });
|
|
206
|
+
if (!app.ok || !(await app.text()).includes('<div id="root"></div>'))
|
|
207
|
+
throw new Error("React application verification failed");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
finalError = error;
|
|
212
|
+
if (attempt + 1 < attempts)
|
|
213
|
+
await sleeper(250 * 2 ** attempt);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const detail = finalError instanceof Error ? finalError.message : "Unknown verification failure";
|
|
217
|
+
throw new Error(`Deployed release verification failed after ${attempts} attempts: ${detail}`);
|
|
197
218
|
}
|
|
198
219
|
export async function runUpgrade(args) {
|
|
199
220
|
const options = parseUpgradeOptions(args);
|
|
@@ -230,7 +251,7 @@ export async function runUpgrade(args) {
|
|
|
230
251
|
command("npx", ["wrangler", "kv", "namespace", "list", ...common])
|
|
231
252
|
]);
|
|
232
253
|
if ([whoami, deployments, d1, kv].some((result) => result.exitCode !== 0))
|
|
233
|
-
throw new Error("Cloudflare target preflight
|
|
254
|
+
throw new Error(commandFailureMessage("Cloudflare target preflight", [whoami, deployments, d1, kv].find((result) => result.exitCode !== 0) ?? whoami));
|
|
234
255
|
const whoamiBody = z
|
|
235
256
|
.object({ accounts: z.array(z.object({ id: z.string() })) })
|
|
236
257
|
.parse(parsedJson(whoami.stdout));
|
|
@@ -258,7 +279,7 @@ export async function runUpgrade(args) {
|
|
|
258
279
|
"SELECT name FROM d1_migrations ORDER BY id"
|
|
259
280
|
]);
|
|
260
281
|
if (listed.exitCode !== 0)
|
|
261
|
-
throw new Error("
|
|
282
|
+
throw new Error(commandFailureMessage("Database version check", listed));
|
|
262
283
|
const query = z
|
|
263
284
|
.array(z.object({ results: z.array(z.object({ name: z.string() })) }))
|
|
264
285
|
.parse(parsedJson(listed.stdout));
|
|
@@ -271,7 +292,15 @@ export async function runUpgrade(args) {
|
|
|
271
292
|
throw new Error("Current deployment health check failed");
|
|
272
293
|
if (compareSemanticVersions(current, manifest.version) > 0)
|
|
273
294
|
throw new Error(`Refusing to downgrade Wikimemory from ${current} to ${manifest.version}`);
|
|
274
|
-
|
|
295
|
+
if (deploymentIsCurrent(current, manifest, pending)) {
|
|
296
|
+
await verifyRelease(record.origin, manifest);
|
|
297
|
+
await writeDeploymentRecord({ ...record, installedVersion: manifest.version }, recordPath);
|
|
298
|
+
console.log(`\n${readySummary(manifest.version, true)}`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
await confirmUpgrade(`\n${upgradeSummary(record, current, manifest.version, pending.length)}`, options.yes);
|
|
302
|
+
if (pending.length > 0)
|
|
303
|
+
console.log(" Updating database…");
|
|
275
304
|
for (const migration of pending) {
|
|
276
305
|
const bundlePath = join(temporary, migration.name);
|
|
277
306
|
const sql = await readFile(join(PACKAGE_ROOT, "migrations", migration.name), "utf8");
|
|
@@ -287,14 +316,16 @@ export async function runUpgrade(args) {
|
|
|
287
316
|
bundlePath
|
|
288
317
|
]);
|
|
289
318
|
if (result.exitCode !== 0)
|
|
290
|
-
throw new Error(
|
|
319
|
+
throw new Error(commandFailureMessage("Database update", result));
|
|
291
320
|
}
|
|
321
|
+
console.log(" Deploying Wikimemory…");
|
|
292
322
|
const deployed = await command("npx", ["wrangler", "deploy", "--strict", ...common]);
|
|
293
323
|
if (deployed.exitCode !== 0)
|
|
294
|
-
throw new Error("Worker deployment
|
|
324
|
+
throw new Error(commandFailureMessage("Worker deployment", deployed));
|
|
325
|
+
console.log(" Verifying deployment…");
|
|
295
326
|
await verifyRelease(record.origin, manifest);
|
|
296
327
|
await writeDeploymentRecord({ ...record, installedVersion: manifest.version }, recordPath);
|
|
297
|
-
console.log(`\
|
|
328
|
+
console.log(`\n${readySummary(manifest.version)}`);
|
|
298
329
|
}
|
|
299
330
|
finally {
|
|
300
331
|
await rm(temporary, { recursive: true, force: true });
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const WIKIMEMORY_VERSION = "0.2.
|
|
1
|
+
export const WIKIMEMORY_VERSION = "0.2.5";
|
|
2
2
|
export const LATEST_SCHEMA_VERSION = "0004_credential_bound_registration_tokens.sql";
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const WIKIMEMORY_VERSION = "0.2.
|
|
1
|
+
export const WIKIMEMORY_VERSION = "0.2.5";
|
|
2
2
|
export const LATEST_SCHEMA_VERSION = "0004_credential_bound_registration_tokens.sql";
|