hollware 0.11.0 → 1.0.0-rc.1
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/{args-CljWh-kZ.mjs → args-D42wi6n2.mjs} +1 -1
- package/dist/{d1-BGChD8_u.mjs → d1-BvAz-iyW.mjs} +5 -70
- package/dist/{deploy-8_Nvbr3b.mjs → deploy-DsHNryRG.mjs} +6 -6
- package/dist/dev--aG_y4gx.mjs +182 -0
- package/dist/{doctor-BT_bTF1M.mjs → doctor-fpXiQgnU.mjs} +35 -17
- package/dist/errors-DJX_jiSP.mjs +18 -0
- package/dist/{export-DprcsjU_.mjs → export-Ims-m9Rl.mjs} +4 -3
- package/dist/{format-BpRYEsGB.mjs → format-Dvk_h5ys.mjs} +1 -18
- package/dist/{import-D9LRrYtb.mjs → import-CR4G8q5W.mjs} +7 -5
- package/dist/import-D4_IhUC7.mjs +1 -0
- package/dist/index.d.mts +38 -0
- package/dist/index.mjs +111 -12
- package/dist/{init-cS_QgC4n.mjs → init-BozoqmFw.mjs} +84 -14
- package/dist/{invite-BvW1xuxU.mjs → invite-tg1_MWv-.mjs} +21 -8
- package/dist/jsonc-CA2ezIB6.mjs +68 -0
- package/dist/mcp-config-BU2YmmGE.mjs +46 -0
- package/dist/mcp-config-CDOUrEpU.mjs +52 -0
- package/dist/migrate-B2GrwRh4.mjs +1 -0
- package/dist/{migrate-BqdgcoRJ.mjs → migrate-D2QcagMk.mjs} +4 -4
- package/dist/{public-url-B2BXf_pj.mjs → public-url-d_yOsEMO.mjs} +2 -2
- package/dist/{token-CVPHV1K-.mjs → token-B0UKrZBf.mjs} +39 -8
- package/dist/token-BG3q6bWZ.mjs +1 -0
- package/dist/token-output-DTn4X40H.mjs +68 -0
- package/dist/{wrangler-BLIfwbcu.mjs → wrangler-DO3h-eNH.mjs} +8 -4
- package/package.json +2 -2
- package/dist/entropy-5uxirwFD.mjs +0 -27
- package/dist/import-JNLtBmyD.mjs +0 -1
- package/dist/migrate-BleH6hXz.mjs +0 -1
- package/dist/token-B0hS__kw.mjs +0 -1
|
@@ -1,76 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { n as UsageError, t as CliError } from "./errors-DJX_jiSP.mjs";
|
|
2
|
+
import { t as printable } from "./format-Dvk_h5ys.mjs";
|
|
3
|
+
import { n as failureReason } from "./wrangler-DO3h-eNH.mjs";
|
|
4
|
+
import { t as parseJsonc } from "./jsonc-CA2ezIB6.mjs";
|
|
3
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
6
|
import { join } from "node:path";
|
|
5
7
|
import * as z from "zod";
|
|
6
8
|
import { SCOPES, SITE_KEY_MAX_LENGTH, isId, isScope, isSiteKey } from "@hollware/core";
|
|
7
|
-
//#region src/jsonc.ts
|
|
8
|
-
/**
|
|
9
|
-
* Reads JSON with comments, the format of `wrangler.jsonc`: `//` and `/* *\/` comments and a
|
|
10
|
-
* comma before a closing bracket are dropped, and a comment marker inside a string is left
|
|
11
|
-
* alone. It does nothing else: whatever `JSON.parse` rejects after that is an error.
|
|
12
|
-
*/
|
|
13
|
-
function parseJsonc(text) {
|
|
14
|
-
return JSON.parse(stripJsonc(text));
|
|
15
|
-
}
|
|
16
|
-
function stripJsonc(text) {
|
|
17
|
-
const source = text.startsWith("") ? text.slice(1) : text;
|
|
18
|
-
let out = "";
|
|
19
|
-
let mode = "code";
|
|
20
|
-
for (let i = 0; i < source.length; i += 1) {
|
|
21
|
-
const char = source.charAt(i);
|
|
22
|
-
const next = source.charAt(i + 1);
|
|
23
|
-
if (mode === "string") {
|
|
24
|
-
out += char;
|
|
25
|
-
if (char === "\\") {
|
|
26
|
-
out += next;
|
|
27
|
-
i += 1;
|
|
28
|
-
} else if (char === "\"") mode = "code";
|
|
29
|
-
} else if (mode === "line-comment") {
|
|
30
|
-
if (char === "\n") {
|
|
31
|
-
out += char;
|
|
32
|
-
mode = "code";
|
|
33
|
-
}
|
|
34
|
-
} else if (mode === "block-comment") {
|
|
35
|
-
if (char === "*" && next === "/") {
|
|
36
|
-
i += 1;
|
|
37
|
-
mode = "code";
|
|
38
|
-
} else if (char === "\n") out += char;
|
|
39
|
-
} else if (char === "\"") {
|
|
40
|
-
out += char;
|
|
41
|
-
mode = "string";
|
|
42
|
-
} else if (char === "/" && next === "/") {
|
|
43
|
-
i += 1;
|
|
44
|
-
mode = "line-comment";
|
|
45
|
-
} else if (char === "/" && next === "*") {
|
|
46
|
-
i += 1;
|
|
47
|
-
mode = "block-comment";
|
|
48
|
-
} else out += char;
|
|
49
|
-
}
|
|
50
|
-
if (mode === "block-comment") throw new SyntaxError("a /* comment is never closed");
|
|
51
|
-
return dropTrailingCommas(out);
|
|
52
|
-
}
|
|
53
|
-
/** Drops a comma that only whitespace separates from a closing bracket, outside strings. */
|
|
54
|
-
function dropTrailingCommas(code) {
|
|
55
|
-
let out = "";
|
|
56
|
-
let inString = false;
|
|
57
|
-
for (let i = 0; i < code.length; i += 1) {
|
|
58
|
-
const char = code.charAt(i);
|
|
59
|
-
if (inString) {
|
|
60
|
-
out += char;
|
|
61
|
-
if (char === "\\") {
|
|
62
|
-
out += code.charAt(i + 1);
|
|
63
|
-
i += 1;
|
|
64
|
-
} else if (char === "\"") inString = false;
|
|
65
|
-
} else if (char === "\"") {
|
|
66
|
-
out += char;
|
|
67
|
-
inString = true;
|
|
68
|
-
} else if (char === "," && /^\s*[}\]]/.test(code.slice(i + 1))) continue;
|
|
69
|
-
else out += char;
|
|
70
|
-
}
|
|
71
|
-
return out;
|
|
72
|
-
}
|
|
73
|
-
//#endregion
|
|
74
9
|
//#region src/sql.ts
|
|
75
10
|
/**
|
|
76
11
|
* Everything the CLI says to the database. A value a person types is checked first and then
|
|
@@ -322,4 +257,4 @@ function jsonOf(stdout) {
|
|
|
322
257
|
} catch {}
|
|
323
258
|
}
|
|
324
259
|
//#endregion
|
|
325
|
-
export {
|
|
260
|
+
export { parseSiteKey as _, runFile as a, sitesAndOwnerQuery as b, insertInvitation as c, ownerOfSiteQuery as d, ownerQuery as f, parseScopes as g, parseOwnerName as h, resolveDatabase as i, insertToken as l, parseLabel as m, describeDatabase as n, anotherSiteScript as o, parseExpiresInDays as p, query as r, initScript as s, chooseTarget as t, listTokens as u, parseTokenId as v, sitesQuery as x, revokeToken as y };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { t as parseFlags } from "./args-
|
|
4
|
-
import { f as ownerQuery, i as resolveDatabase, r as query } from "./d1-
|
|
5
|
-
import { t as publicUrlOf } from "./public-url-
|
|
6
|
-
import { i as migrate } from "./migrate-
|
|
1
|
+
import { t as CliError } from "./errors-DJX_jiSP.mjs";
|
|
2
|
+
import { i as stripAnsi, n as failureReason } from "./wrangler-DO3h-eNH.mjs";
|
|
3
|
+
import { t as parseFlags } from "./args-D42wi6n2.mjs";
|
|
4
|
+
import { f as ownerQuery, i as resolveDatabase, r as query } from "./d1-BvAz-iyW.mjs";
|
|
5
|
+
import { t as publicUrlOf } from "./public-url-d_yOsEMO.mjs";
|
|
6
|
+
import { i as migrate } from "./migrate-D2QcagMk.mjs";
|
|
7
7
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
//#region src/commands/deploy.ts
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { n as UsageError, t as CliError } from "./errors-DJX_jiSP.mjs";
|
|
2
|
+
import { r as findBin } from "./wrangler-DO3h-eNH.mjs";
|
|
3
|
+
import { t as parseFlags } from "./args-D42wi6n2.mjs";
|
|
4
|
+
import { t as publicUrlOf } from "./public-url-d_yOsEMO.mjs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
//#region src/commands/dev.ts
|
|
8
|
+
const DEV_USAGE = [
|
|
9
|
+
"Usage: hollware dev [--url <url>] [--timeout <seconds>] [--json]",
|
|
10
|
+
" hollware dev --detach [--url <url>] [--timeout <seconds>] [--json]",
|
|
11
|
+
" hollware dev --stop [--json]",
|
|
12
|
+
"",
|
|
13
|
+
"Starts the project's local server, the way its \"dev\" script does (the project's own vp dev),",
|
|
14
|
+
"waits until GET <url>/health answers, and prints \"Hollware is ready at <url>\". It stays in the",
|
|
15
|
+
"foreground until SIGINT or SIGTERM, then stops the server and everything it started.",
|
|
16
|
+
"With --detach it starts the server in the background, writes its process id and URL to",
|
|
17
|
+
`.wrangler/hollware-dev.json, prints the ready line and exits 0. "hollware dev --stop" stops what`,
|
|
18
|
+
"that file names.",
|
|
19
|
+
"",
|
|
20
|
+
"Options:",
|
|
21
|
+
" --url <url> Where the server listens (default: HOLLWARE_PUBLIC_URL of wrangler.jsonc,",
|
|
22
|
+
" else http://localhost:5199)",
|
|
23
|
+
" --timeout <seconds> How long to wait for /health, 1 to 600 (default: 60)",
|
|
24
|
+
" --detach Start it in the background and exit",
|
|
25
|
+
" --stop Stop the server that --detach started",
|
|
26
|
+
" --json One JSON object on stdout instead of the line",
|
|
27
|
+
" --help Show this help"
|
|
28
|
+
].join("\n");
|
|
29
|
+
/** The file that names the server `--detach` started, relative to the project. */
|
|
30
|
+
const DEV_STATE_FILE = join(".wrangler", "hollware-dev.json");
|
|
31
|
+
const DEFAULT_URL = "http://localhost:5199";
|
|
32
|
+
const POLL_MS = 250;
|
|
33
|
+
const GRACE_MS = 5e3;
|
|
34
|
+
async function dev(argv, io) {
|
|
35
|
+
const { values } = parseFlags({
|
|
36
|
+
args: [...argv],
|
|
37
|
+
allowPositionals: false,
|
|
38
|
+
options: {
|
|
39
|
+
url: { type: "string" },
|
|
40
|
+
timeout: { type: "string" },
|
|
41
|
+
detach: { type: "boolean" },
|
|
42
|
+
stop: { type: "boolean" },
|
|
43
|
+
json: { type: "boolean" },
|
|
44
|
+
help: { type: "boolean" }
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
if (values.help === true) {
|
|
48
|
+
io.out(DEV_USAGE);
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
const host = io.dev;
|
|
52
|
+
if (host === void 0) throw new CliError("this environment cannot run a local server.");
|
|
53
|
+
const json = values.json === true;
|
|
54
|
+
if (values.stop === true) {
|
|
55
|
+
if (values.detach === true || values.url !== void 0 || values.timeout !== void 0) throw new UsageError("--stop goes alone: it stops what --detach started.");
|
|
56
|
+
return stop(io, host, json);
|
|
57
|
+
}
|
|
58
|
+
const timeout = timeoutOf(values.timeout);
|
|
59
|
+
const url = (publicUrlOf(io.cwd, values.url) ?? DEFAULT_URL).replace(/\/$/, "");
|
|
60
|
+
const bin = findBin(io.cwd, "vp");
|
|
61
|
+
if (bin === void 0) throw new CliError(`vp is not installed in ${io.cwd} or any directory above it. Install the dependencies of the project (for example "pnpm install") and run hollware dev from the project directory.`);
|
|
62
|
+
const detach = values.detach === true;
|
|
63
|
+
const state = readState(io.cwd);
|
|
64
|
+
if (state !== void 0 && host.isAlive(state.pid)) throw new CliError(`a server started by --detach runs already (process ${state.pid}, ${state.url}). Stop it with "hollware dev --stop".`);
|
|
65
|
+
const child = host.spawn(bin, ["dev"], io.cwd, { detached: detach });
|
|
66
|
+
let interrupted = false;
|
|
67
|
+
let signalled = () => void 0;
|
|
68
|
+
const asked = new Promise((resolve) => {
|
|
69
|
+
signalled = () => resolve("signal");
|
|
70
|
+
});
|
|
71
|
+
const off = host.onSignal(() => {
|
|
72
|
+
interrupted = true;
|
|
73
|
+
signalled();
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
const ready = await waitUntilHealthy(host, child, url, timeout, () => interrupted);
|
|
77
|
+
if (ready !== "ready") {
|
|
78
|
+
await terminate(host, child);
|
|
79
|
+
if (ready === "interrupted") return 0;
|
|
80
|
+
throw new CliError(ready === "exited" ? `the dev server stopped before it was ready.${detail(child)}` : `the dev server did not answer ${url}/health within ${timeout} seconds.${detail(child)}`);
|
|
81
|
+
}
|
|
82
|
+
announce(io, json, {
|
|
83
|
+
ready: true,
|
|
84
|
+
url,
|
|
85
|
+
pid: child.pid,
|
|
86
|
+
detached: detach
|
|
87
|
+
});
|
|
88
|
+
if (detach) {
|
|
89
|
+
const file = join(io.cwd, DEV_STATE_FILE);
|
|
90
|
+
mkdirSync(join(io.cwd, ".wrangler"), { recursive: true });
|
|
91
|
+
const record = {
|
|
92
|
+
pid: child.pid,
|
|
93
|
+
url,
|
|
94
|
+
startedAt: io.now().toISOString()
|
|
95
|
+
};
|
|
96
|
+
writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
if (await Promise.race([asked, child.exited.then(() => "exited")]) === "exited") throw new CliError(`the dev server stopped by itself.${detail(child)}`);
|
|
100
|
+
await terminate(host, child);
|
|
101
|
+
return 0;
|
|
102
|
+
} finally {
|
|
103
|
+
off();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function stop(io, host, json) {
|
|
107
|
+
const state = readState(io.cwd);
|
|
108
|
+
if (state === void 0) {
|
|
109
|
+
if (json) io.out(JSON.stringify({ stopped: false }));
|
|
110
|
+
else io.out(`No detached server to stop: ${DEV_STATE_FILE} does not exist.`);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
const wasAlive = host.isAlive(state.pid);
|
|
114
|
+
if (wasAlive) {
|
|
115
|
+
host.killGroup(state.pid, "SIGTERM");
|
|
116
|
+
const until = GRACE_MS / POLL_MS;
|
|
117
|
+
for (let waited = 0; host.isAlive(state.pid) && waited < until; waited += 1) await host.sleep(POLL_MS);
|
|
118
|
+
host.killGroup(state.pid, "SIGKILL");
|
|
119
|
+
}
|
|
120
|
+
rmSync(join(io.cwd, DEV_STATE_FILE), { force: true });
|
|
121
|
+
if (json) io.out(JSON.stringify({
|
|
122
|
+
stopped: wasAlive,
|
|
123
|
+
pid: state.pid,
|
|
124
|
+
url: state.url
|
|
125
|
+
}));
|
|
126
|
+
else io.out(wasAlive ? `Stopped the server at ${state.url} (process ${state.pid}).` : `The server at ${state.url} (process ${state.pid}) was not running; removed ${DEV_STATE_FILE}.`);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
function announce(io, json, what) {
|
|
130
|
+
io.out(json ? JSON.stringify(what) : `Hollware is ready at ${what.url}`);
|
|
131
|
+
}
|
|
132
|
+
function timeoutOf(value) {
|
|
133
|
+
if (value === void 0) return 60;
|
|
134
|
+
const seconds = /^\d+$/.test(value) ? Number(value) : NaN;
|
|
135
|
+
if (!(seconds >= 1 && seconds <= 600)) throw new UsageError(`--timeout is a whole number of seconds from 1 to 600, not "${value}".`);
|
|
136
|
+
return seconds;
|
|
137
|
+
}
|
|
138
|
+
/** Polls /health until it answers, the server ends, a signal comes or the time is up. */
|
|
139
|
+
async function waitUntilHealthy(host, child, url, seconds, interrupted) {
|
|
140
|
+
let ended = false;
|
|
141
|
+
child.exited.then(() => {
|
|
142
|
+
ended = true;
|
|
143
|
+
});
|
|
144
|
+
for (let waited = 0; waited <= seconds * 1e3; waited += POLL_MS) {
|
|
145
|
+
if (interrupted()) return "interrupted";
|
|
146
|
+
if (ended) return "exited";
|
|
147
|
+
if (await host.healthy(url)) return "ready";
|
|
148
|
+
await host.sleep(POLL_MS);
|
|
149
|
+
}
|
|
150
|
+
return "timeout";
|
|
151
|
+
}
|
|
152
|
+
/** Stops the server: SIGTERM to its group, SIGKILL when it has not ended after a grace period. */
|
|
153
|
+
async function terminate(host, child) {
|
|
154
|
+
host.killGroup(child.pid, "SIGTERM");
|
|
155
|
+
const clear = new AbortController();
|
|
156
|
+
const ended = await Promise.race([child.exited.then(() => true), host.sleep(GRACE_MS, clear.signal).then(() => false)]);
|
|
157
|
+
clear.abort();
|
|
158
|
+
if (!ended) {
|
|
159
|
+
host.killGroup(child.pid, "SIGKILL");
|
|
160
|
+
await child.exited;
|
|
161
|
+
}
|
|
162
|
+
await host.sleep(POLL_MS);
|
|
163
|
+
host.killGroup(child.pid, "SIGKILL");
|
|
164
|
+
}
|
|
165
|
+
function detail(child) {
|
|
166
|
+
const tail = child.tail().trim();
|
|
167
|
+
return tail === "" ? "" : `\n${tail}`;
|
|
168
|
+
}
|
|
169
|
+
function readState(cwd) {
|
|
170
|
+
const file = join(cwd, DEV_STATE_FILE);
|
|
171
|
+
if (!existsSync(file)) return void 0;
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
174
|
+
if (typeof parsed === "object" && parsed !== null && "pid" in parsed && "url" in parsed && typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 1 && typeof parsed.url === "string") return {
|
|
175
|
+
pid: parsed.pid,
|
|
176
|
+
url: parsed.url,
|
|
177
|
+
startedAt: "startedAt" in parsed && typeof parsed.startedAt === "string" ? parsed.startedAt : ""
|
|
178
|
+
};
|
|
179
|
+
} catch {}
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
export { dev };
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as failureReason } from "./wrangler-
|
|
3
|
-
import { t as parseFlags } from "./args-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { t as CliError } from "./errors-DJX_jiSP.mjs";
|
|
2
|
+
import { n as failureReason } from "./wrangler-DO3h-eNH.mjs";
|
|
3
|
+
import { t as parseFlags } from "./args-D42wi6n2.mjs";
|
|
4
|
+
import { t as parseJsonc } from "./jsonc-CA2ezIB6.mjs";
|
|
5
|
+
import { f as ownerQuery, r as query, t as chooseTarget } from "./d1-BvAz-iyW.mjs";
|
|
6
|
+
import { a as parsePendingMigrations } from "./migrate-D2QcagMk.mjs";
|
|
6
7
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
8
|
import { join } from "node:path";
|
|
8
9
|
//#region src/commands/doctor.ts
|
|
9
10
|
const DOCTOR_USAGE = [
|
|
10
|
-
"Usage: hollware doctor [--local | --remote]",
|
|
11
|
+
"Usage: hollware doctor [--local | --remote] [--json]",
|
|
11
12
|
"",
|
|
12
13
|
"Checks the project in the current directory: its wrangler.jsonc, the bindings and flags",
|
|
13
14
|
"Hollware needs, the built files of the admin, the migrations of the database and its owner.",
|
|
@@ -16,6 +17,8 @@ const DOCTOR_USAGE = [
|
|
|
16
17
|
"Options:",
|
|
17
18
|
" --local Check the local database (the default)",
|
|
18
19
|
" --remote Check the database in Cloudflare",
|
|
20
|
+
" --json One JSON object on stdout: { ok, checks: [{ name, ok, message, fix }] }; the exit code",
|
|
21
|
+
" is the same (fix is \"\" for a check that passed)",
|
|
19
22
|
" --help Show this help"
|
|
20
23
|
].join("\n");
|
|
21
24
|
/** The flags of `compatibility_flags` that Hollware runs with (ADR-018, ADR-026). */
|
|
@@ -24,11 +27,13 @@ const REQUIRED_FLAGS = [
|
|
|
24
27
|
"no_nodejs_compat_v2",
|
|
25
28
|
"global_fetch_strictly_public"
|
|
26
29
|
];
|
|
27
|
-
const pass = (what) => ({
|
|
30
|
+
const pass = (name, what) => ({
|
|
31
|
+
name,
|
|
28
32
|
ok: true,
|
|
29
33
|
what
|
|
30
34
|
});
|
|
31
|
-
const fail = (what, fix) => ({
|
|
35
|
+
const fail = (name, what, fix) => ({
|
|
36
|
+
name,
|
|
32
37
|
ok: false,
|
|
33
38
|
what,
|
|
34
39
|
fix
|
|
@@ -44,20 +49,20 @@ function checkConfig(cwd, config) {
|
|
|
44
49
|
const findings = [];
|
|
45
50
|
const flags = Array.isArray(config["compatibility_flags"]) ? config["compatibility_flags"] : [];
|
|
46
51
|
const missing = REQUIRED_FLAGS.filter((flag) => !flags.includes(flag));
|
|
47
|
-
findings.push(missing.length === 0 ? pass(`compatibility_flags has ${REQUIRED_FLAGS.join(", ")}`) : fail(`compatibility_flags lacks ${missing.join(", ")}`, `Add ${missing.map((flag) => `"${flag}"`).join(", ")} to compatibility_flags in wrangler.jsonc.`));
|
|
52
|
+
findings.push(missing.length === 0 ? pass("compatibility-flags", `compatibility_flags has ${REQUIRED_FLAGS.join(", ")}`) : fail("compatibility-flags", `compatibility_flags lacks ${missing.join(", ")}`, `Add ${missing.map((flag) => `"${flag}"`).join(", ")} to compatibility_flags in wrangler.jsonc.`));
|
|
48
53
|
const url = (isRecord(config["vars"]) ? config["vars"] : {})["HOLLWARE_PUBLIC_URL"];
|
|
49
54
|
const urlOk = typeof url === "string" && URL.canParse(url) && /^https?:$/.test(new URL(url).protocol);
|
|
50
|
-
findings.push(urlOk ? pass(`HOLLWARE_PUBLIC_URL is ${new URL(url).origin}`) : fail("HOLLWARE_PUBLIC_URL is missing or is no http or https URL", "Set vars.HOLLWARE_PUBLIC_URL in wrangler.jsonc to the public URL of the Worker, such as https://cms.example.com."));
|
|
55
|
+
findings.push(urlOk ? pass("public-url", `HOLLWARE_PUBLIC_URL is ${new URL(url).origin}`) : fail("public-url", "HOLLWARE_PUBLIC_URL is missing or is no http or https URL", "Set vars.HOLLWARE_PUBLIC_URL in wrangler.jsonc to the public URL of the Worker, such as https://cms.example.com."));
|
|
51
56
|
const db = records(config["d1_databases"]).find((entry) => entry["binding"] === "DB");
|
|
52
57
|
const migrationsDir = db !== void 0 && typeof db["migrations_dir"] === "string" ? join(cwd, db["migrations_dir"]) : void 0;
|
|
53
58
|
const migrations = migrationsDir !== void 0 && existsSync(migrationsDir) ? readdirSync(migrationsDir).filter((name) => name.endsWith(".sql")) : [];
|
|
54
|
-
findings.push(db === void 0 ? fail(`there is no D1 binding "DB"`, "Add it to d1_databases in wrangler.jsonc, as the starter has it.") : migrations.length === 0 ? fail(`the D1 binding "DB" has no migrations in its migrations_dir`, "Set migrations_dir to \"node_modules/@hollware/server/migrations\", and install the dependencies.") : pass(`the D1 binding "DB" has ${migrations.length} migrations`));
|
|
59
|
+
findings.push(db === void 0 ? fail("d1-binding", `there is no D1 binding "DB"`, "Add it to d1_databases in wrangler.jsonc, as the starter has it.") : migrations.length === 0 ? fail("d1-binding", `the D1 binding "DB" has no migrations in its migrations_dir`, "Set migrations_dir to \"node_modules/@hollware/server/migrations\", and install the dependencies.") : pass("d1-binding", `the D1 binding "DB" has ${migrations.length} migrations`));
|
|
55
60
|
const kv = records(config["kv_namespaces"]).some((entry) => entry["binding"] === "OAUTH_KV");
|
|
56
|
-
findings.push(kv ? pass("the KV binding \"OAUTH_KV\" is there") : fail("there is no KV binding \"OAUTH_KV\", which the OAuth server needs", "Add { \"binding\": \"OAUTH_KV\" } to kv_namespaces in wrangler.jsonc."));
|
|
61
|
+
findings.push(kv ? pass("kv-binding", "the KV binding \"OAUTH_KV\" is there") : fail("kv-binding", "there is no KV binding \"OAUTH_KV\", which the OAuth server needs", "Add { \"binding\": \"OAUTH_KV\" } to kv_namespaces in wrangler.jsonc."));
|
|
57
62
|
const assets = isRecord(config["assets"]) ? config["assets"] : void 0;
|
|
58
63
|
const directory = typeof assets?.["directory"] === "string" ? assets["directory"] : void 0;
|
|
59
64
|
const admin = directory !== void 0 && existsSync(join(cwd, directory, "admin"));
|
|
60
|
-
findings.push(admin ? pass("the admin is built, in the static assets") : fail("the static assets do not hold the admin", "Install the dependencies; assets.directory of wrangler.jsonc must be \"node_modules/@hollware/admin/dist/client\"."));
|
|
65
|
+
findings.push(admin ? pass("admin-assets", "the admin is built, in the static assets") : fail("admin-assets", "the static assets do not hold the admin", "Install the dependencies; assets.directory of wrangler.jsonc must be \"node_modules/@hollware/admin/dist/client\"."));
|
|
61
66
|
return findings;
|
|
62
67
|
}
|
|
63
68
|
/** What the database says: the migrations it lacks and whether it has an owner. */
|
|
@@ -69,11 +74,11 @@ async function checkDatabase(io, target) {
|
|
|
69
74
|
"DB",
|
|
70
75
|
`--${target}`
|
|
71
76
|
]);
|
|
72
|
-
if (listed.code !== 0) return [fail(`wrangler could not list the migrations of the ${target} database: ${failureReason(listed)}`, target === "remote" ? "Run \"wrangler login\", then \"hollware deploy\"." : "Check wrangler.jsonc.")];
|
|
77
|
+
if (listed.code !== 0) return [fail("migrations-list", `wrangler could not list the migrations of the ${target} database: ${failureReason(listed)}`, target === "remote" ? "Run \"wrangler login\", then \"hollware deploy\"." : "Check wrangler.jsonc.")];
|
|
73
78
|
const pending = parsePendingMigrations(listed.stdout);
|
|
74
|
-
if (pending.length > 0) return [fail(`the ${target} database lacks ${pending.length} ${pending.length === 1 ? "migration" : "migrations"}`, `Run "hollware migrate --${target}".`)];
|
|
79
|
+
if (pending.length > 0) return [fail("migrations", `the ${target} database lacks ${pending.length} ${pending.length === 1 ? "migration" : "migrations"}`, `Run "hollware migrate --${target}".`)];
|
|
75
80
|
const [owners = []] = await query(io.wrangler, "DB", target, ownerQuery());
|
|
76
|
-
return [pass(`the ${target} database has every migration`), owners.length > 0 ? pass(`the ${target} database has an owner`) : fail(`the ${target} database has no owner yet`, `Run "hollware init --site <key> --${target}", with the key your site is to have, such as my-site.`)];
|
|
81
|
+
return [pass("migrations", `the ${target} database has every migration`), owners.length > 0 ? pass("owner", `the ${target} database has an owner`) : fail("owner", `the ${target} database has no owner yet`, `Run "hollware init --site <key> --${target}", with the key your site is to have, such as my-site.`)];
|
|
77
82
|
}
|
|
78
83
|
async function doctor(argv, io) {
|
|
79
84
|
const { values } = parseFlags({
|
|
@@ -82,6 +87,7 @@ async function doctor(argv, io) {
|
|
|
82
87
|
options: {
|
|
83
88
|
local: { type: "boolean" },
|
|
84
89
|
remote: { type: "boolean" },
|
|
90
|
+
json: { type: "boolean" },
|
|
85
91
|
help: { type: "boolean" }
|
|
86
92
|
}
|
|
87
93
|
});
|
|
@@ -100,8 +106,20 @@ async function doctor(argv, io) {
|
|
|
100
106
|
}
|
|
101
107
|
if (!isRecord(config)) throw new CliError("wrangler.jsonc holds no object.");
|
|
102
108
|
const findings = [...checkConfig(io.cwd, config), ...await checkDatabase(io, target)];
|
|
103
|
-
io.out(findings.map((finding) => finding.ok ? `ok ${finding.what}` : `FAIL ${finding.what}\n ${finding.fix ?? ""}`).join("\n"));
|
|
104
109
|
const failed = findings.filter((finding) => !finding.ok).length;
|
|
110
|
+
if (values.json === true) {
|
|
111
|
+
io.out(JSON.stringify({
|
|
112
|
+
ok: failed === 0,
|
|
113
|
+
checks: findings.map((finding) => ({
|
|
114
|
+
name: finding.name,
|
|
115
|
+
ok: finding.ok,
|
|
116
|
+
message: finding.what,
|
|
117
|
+
fix: finding.fix ?? ""
|
|
118
|
+
}))
|
|
119
|
+
}));
|
|
120
|
+
return failed === 0 ? 0 : 1;
|
|
121
|
+
}
|
|
122
|
+
io.out(findings.map((finding) => finding.ok ? `ok ${finding.what}` : `FAIL ${finding.what}\n ${finding.fix ?? ""}`).join("\n"));
|
|
105
123
|
if (failed === 0) {
|
|
106
124
|
io.out("Everything Hollware needs is in place.");
|
|
107
125
|
return 0;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* An error the person at the keyboard can act on. `main` prints its message on stderr, behind
|
|
4
|
+
* the name of the command, and exits with code 1. Anything that is not a `CliError` is a bug or
|
|
5
|
+
* a broken install, and escapes with its stack trace.
|
|
6
|
+
*/
|
|
7
|
+
var CliError = class extends Error {
|
|
8
|
+
name = "CliError";
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* A mistake in the arguments: an unknown option, a missing or a malformed value. `main` adds a
|
|
12
|
+
* pointer to the help of the command after the message.
|
|
13
|
+
*/
|
|
14
|
+
var UsageError = class extends CliError {
|
|
15
|
+
name = "UsageError";
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { UsageError as n, CliError as t };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as UsageError, t as CliError } from "./errors-DJX_jiSP.mjs";
|
|
2
|
+
import { t as printable } from "./format-Dvk_h5ys.mjs";
|
|
2
3
|
import { t as readVersion } from "./version-BMhdZaNo.mjs";
|
|
3
|
-
import { t as parseFlags } from "./args-
|
|
4
|
-
import { a as redact, c as openSession, i as readToken, o as withoutToken, r as mcpEndpoint, s as ToolFailure } from "./import-
|
|
4
|
+
import { t as parseFlags } from "./args-D42wi6n2.mjs";
|
|
5
|
+
import { a as redact, c as openSession, i as readToken, o as withoutToken, r as mcpEndpoint, s as ToolFailure } from "./import-CR4G8q5W.mjs";
|
|
5
6
|
import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { dirname, join, resolve } from "node:path";
|
|
7
8
|
import { describeSite, getEntry, listEntries, schemas } from "@hollware/core";
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
//#region src/errors.ts
|
|
2
|
-
/**
|
|
3
|
-
* An error the person at the keyboard can act on. `main` prints its message on stderr, behind
|
|
4
|
-
* the name of the command, and exits with code 1. Anything that is not a `CliError` is a bug or
|
|
5
|
-
* a broken install, and escapes with its stack trace.
|
|
6
|
-
*/
|
|
7
|
-
var CliError = class extends Error {
|
|
8
|
-
name = "CliError";
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* A mistake in the arguments: an unknown option, a missing or a malformed value. `main` adds a
|
|
12
|
-
* pointer to the help of the command after the message.
|
|
13
|
-
*/
|
|
14
|
-
var UsageError = class extends CliError {
|
|
15
|
-
name = "UsageError";
|
|
16
|
-
};
|
|
17
|
-
//#endregion
|
|
18
1
|
//#region src/format.ts
|
|
19
2
|
/**
|
|
20
3
|
* Text for the terminal: a value that came from the database or from a server is made safe to
|
|
@@ -35,4 +18,4 @@ function table(header, rows) {
|
|
|
35
18
|
}).join(" ")).join("\n");
|
|
36
19
|
}
|
|
37
20
|
//#endregion
|
|
38
|
-
export {
|
|
21
|
+
export { table as n, printable as t };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as UsageError, t as CliError } from "./errors-DJX_jiSP.mjs";
|
|
2
|
+
import { t as printable } from "./format-Dvk_h5ys.mjs";
|
|
2
3
|
import { t as readVersion } from "./version-BMhdZaNo.mjs";
|
|
3
|
-
import { t as parseFlags } from "./args-
|
|
4
|
+
import { t as parseFlags } from "./args-D42wi6n2.mjs";
|
|
4
5
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
5
6
|
import { join, resolve } from "node:path";
|
|
6
7
|
import { REFUSAL_CONTRACT_IDS, createDraft, getEntry, isPersonalToken, publish, schemas, updateDraft, validate } from "@hollware/core";
|
|
@@ -379,7 +380,7 @@ async function importFiles({ io, token, endpoint, directory, corpus, publish: pu
|
|
|
379
380
|
return 0;
|
|
380
381
|
}
|
|
381
382
|
out(`Validated ${count(checks.length, "file")}: none would be refused.`);
|
|
382
|
-
const drafts = await writeDrafts(session, corpus.files, out);
|
|
383
|
+
const drafts = await writeDrafts(session, corpus.files, out, checks);
|
|
383
384
|
out(`Drafts: ${drafts.created} created, ${drafts.updated} updated, ${drafts.unchanged} unchanged${drafts.failed === 0 ? "" : `, ${drafts.failed} failed`}.`);
|
|
384
385
|
if (drafts.failed > 0) throw new CliError(`${count(drafts.failed, "file")} could not be imported (see above).${publishToo ? " Nothing was published." : ""} Fix ${drafts.failed === 1 ? "it" : "them"} and run the import again: what is already there stays unchanged.`);
|
|
385
386
|
if (!publishToo) return 0;
|
|
@@ -498,7 +499,7 @@ function violationLine(violation) {
|
|
|
498
499
|
* does is updated at the version just read, which Hollware answers with `unchanged` when the
|
|
499
500
|
* Markdown is the same. A failure of one file does not stop the others.
|
|
500
501
|
*/
|
|
501
|
-
async function writeDrafts(session, files, out) {
|
|
502
|
+
async function writeDrafts(session, files, out, checks = []) {
|
|
502
503
|
const drafts = {
|
|
503
504
|
created: 0,
|
|
504
505
|
updated: 0,
|
|
@@ -506,6 +507,7 @@ async function writeDrafts(session, files, out) {
|
|
|
506
507
|
failed: 0,
|
|
507
508
|
entries: []
|
|
508
509
|
};
|
|
510
|
+
const fresh = new Map(checks.map((check) => [check.file, check.verdict]));
|
|
509
511
|
for (const file of files) {
|
|
510
512
|
const label = printable(file.path.replace(/\.md$/, ""));
|
|
511
513
|
try {
|
|
@@ -545,7 +547,7 @@ async function writeDrafts(session, files, out) {
|
|
|
545
547
|
file,
|
|
546
548
|
summary: entry
|
|
547
549
|
});
|
|
548
|
-
out(draftLine(kind, label, entry, updated.verdict));
|
|
550
|
+
out(draftLine(kind, label, entry, kind === "unchanged" ? fresh.get(file) ?? updated.verdict : updated.verdict));
|
|
549
551
|
continue;
|
|
550
552
|
}
|
|
551
553
|
throw new ToolFailure(`update_draft ${updated.outcome}${refusalReasons(updated.verdict)}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { n as importContent } from "./import-CR4G8q5W.mjs";
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
//#region src/dev-host.d.ts
|
|
2
|
+
/** How a child process ended. */
|
|
3
|
+
interface DevExit {
|
|
4
|
+
readonly code: number | null;
|
|
5
|
+
readonly signal: string | null;
|
|
6
|
+
}
|
|
7
|
+
/** A process the local server runs in, started as the leader of a group of its own. */
|
|
8
|
+
interface DevChild {
|
|
9
|
+
readonly pid: number;
|
|
10
|
+
/** Settles when the process ends. */
|
|
11
|
+
readonly exited: Promise<DevExit>;
|
|
12
|
+
/** The last lines it printed (stdout and stderr), for the message of a failure. Empty when detached. */
|
|
13
|
+
readonly tail: () => string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* What `hollware dev` takes from the machine, so that a test hands in a fake: processes (each the
|
|
17
|
+
* leader of its own group, so that the server and what it started stop together), the signals
|
|
18
|
+
* that stop it, the health check, and time. The entry point of `index.ts` builds the real one.
|
|
19
|
+
*/
|
|
20
|
+
interface DevHost {
|
|
21
|
+
/** Starts `bin` with `args` in `cwd`. Detached: the process outlives this one, in a group of its own. */
|
|
22
|
+
spawn: (bin: string, args: readonly string[], cwd: string, options: {
|
|
23
|
+
detached: boolean;
|
|
24
|
+
}) => DevChild;
|
|
25
|
+
/** Sends the signal to the whole group of the process. Never throws when the group is gone. */
|
|
26
|
+
killGroup: (pid: number, signal: 'SIGTERM' | 'SIGKILL') => void;
|
|
27
|
+
/** Whether a process with this id exists. */
|
|
28
|
+
isAlive: (pid: number) => boolean;
|
|
29
|
+
/** Calls `callback` on SIGINT or SIGTERM, once. Returns a function that stops listening. */
|
|
30
|
+
onSignal: (callback: () => void) => () => void;
|
|
31
|
+
/** True when `GET <url>/health` answers 200. Never throws. */
|
|
32
|
+
healthy: (url: string) => Promise<boolean>;
|
|
33
|
+
/** Resolves after `ms` milliseconds, or at once when `cancel` aborts (and the timer is cleared). */
|
|
34
|
+
sleep: (ms: number, cancel?: AbortSignal) => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
1
37
|
//#region src/wrangler.d.ts
|
|
2
38
|
/** What a run of wrangler left behind. */
|
|
3
39
|
interface WranglerResult {
|
|
@@ -38,6 +74,8 @@ interface Io {
|
|
|
38
74
|
wrangler: RunWrangler;
|
|
39
75
|
/** A new empty directory for temporary files. Whoever asks for it removes it. */
|
|
40
76
|
tempDir: () => string;
|
|
77
|
+
/** What `hollware dev` needs of the machine: processes, signals and the health check. */
|
|
78
|
+
dev?: DevHost;
|
|
41
79
|
}
|
|
42
80
|
//#endregion
|
|
43
81
|
//#region src/index.d.ts
|