create-hollware 1.0.0-rc.0 → 1.0.0-rc.2
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/index.mjs +94 -19
- package/package.json +1 -1
- package/template/AGENTS.md +5 -0
- package/template/package.json +1 -0
- package/template/test/worker.workerd.test.ts +1 -0
- package/template/vite.config.ts +25 -3
- package/template/vitest.config.ts +4 -1
- package/template/worker-configuration.d.ts +3 -1
- package/template/wrangler.jsonc +5 -0
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { cpSync, existsSync, readFileSync, readdirSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { appendFileSync, cpSync, existsSync, readFileSync, readdirSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
-
import { basename, join, resolve } from "node:path";
|
|
5
|
+
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
7
|
//#region src/scaffold.ts
|
|
8
8
|
const USAGE = [
|
|
@@ -21,6 +21,12 @@ const USAGE = [
|
|
|
21
21
|
" Whether the activity log keeps a keyed hash of each address (default: off)",
|
|
22
22
|
" --audit-retention-days <n>",
|
|
23
23
|
" Days each line of the activity log is kept, 30 to 3650 (default: forever)",
|
|
24
|
+
" --token-file <path>",
|
|
25
|
+
" Write the first token to this file (mode 0600) and print its path, not the token;",
|
|
26
|
+
" a file that exists is refused. Inside the project, it goes into .gitignore",
|
|
27
|
+
" --json One JSON object on stdout and nothing else: dir, then what \"hollware init\" says",
|
|
28
|
+
" (site, ownerId, tokenId, token or tokenFile, expiresAt, setupUrl, mcpUrl,",
|
|
29
|
+
" readApiUrl), the commands to start it and the MCP configuration",
|
|
24
30
|
" --pm <npm|pnpm> The package manager that installs (default: npm)",
|
|
25
31
|
" --yes Take the defaults: <dir> is my-hollware when it is not given",
|
|
26
32
|
" --no-install Do not install, migrate or make the site",
|
|
@@ -42,7 +48,6 @@ const WORKER_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
|
42
48
|
const SITE_KEY = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
43
49
|
/** What `@hollware/*` and `hollware` say in the template's package.json. */
|
|
44
50
|
const VERSION_PLACEHOLDER = "HOLLWARE_VERSION";
|
|
45
|
-
const TOKEN = /hw_pat_[0-9a-hjkmnp-tv-z]{52}/;
|
|
46
51
|
/** The choices of a run, from its arguments. */
|
|
47
52
|
function readChoices(argv) {
|
|
48
53
|
const { values, positionals } = (() => {
|
|
@@ -60,7 +65,9 @@ function readChoices(argv) {
|
|
|
60
65
|
pm: { type: "string" },
|
|
61
66
|
yes: { type: "boolean" },
|
|
62
67
|
"no-install": { type: "boolean" },
|
|
63
|
-
"no-git": { type: "boolean" }
|
|
68
|
+
"no-git": { type: "boolean" },
|
|
69
|
+
"token-file": { type: "string" },
|
|
70
|
+
json: { type: "boolean" }
|
|
64
71
|
}
|
|
65
72
|
});
|
|
66
73
|
} catch (error) {
|
|
@@ -84,6 +91,8 @@ function readChoices(argv) {
|
|
|
84
91
|
pm,
|
|
85
92
|
install: values["no-install"] !== true,
|
|
86
93
|
git: values["no-git"] !== true,
|
|
94
|
+
tokenFile: values["token-file"],
|
|
95
|
+
json: values.json === true,
|
|
87
96
|
initFlags: [
|
|
88
97
|
...values.preset === void 0 ? [] : ["--preset", values.preset],
|
|
89
98
|
...values.locales === void 0 ? [] : ["--locales", values.locales],
|
|
@@ -105,6 +114,7 @@ function writeProject(io, target, choices) {
|
|
|
105
114
|
replaceIn(join(target, "wrangler.jsonc"), [
|
|
106
115
|
["\"name\": \"my-hollware\"", `"name": "${choices.name}"`],
|
|
107
116
|
["\"database_name\": \"my-hollware\"", `"database_name": "${choices.name}"`],
|
|
117
|
+
["\"bucket_name\": \"my-hollware-media\"", `"bucket_name": "${choices.name}-media"`],
|
|
108
118
|
["hollware-events", `${choices.name}-events`]
|
|
109
119
|
]);
|
|
110
120
|
for (const file of ["README.md"]) replaceIn(join(target, file), [["my-site", choices.site]]);
|
|
@@ -127,44 +137,109 @@ async function step(io, what, command, args, cwd) {
|
|
|
127
137
|
}
|
|
128
138
|
return result;
|
|
129
139
|
}
|
|
140
|
+
/** The environment variable that holds the token in an MCP client's configuration. */
|
|
141
|
+
const TOKEN_VARIABLE = "HOLLWARE_TOKEN";
|
|
142
|
+
/** The `.mcp.json` block of a Hollware: the URL of /mcp, and a header that names HOLLWARE_TOKEN. */
|
|
143
|
+
function mcpJson(url) {
|
|
144
|
+
return JSON.stringify({ mcpServers: { hollware: {
|
|
145
|
+
type: "http",
|
|
146
|
+
url: `${url}/mcp`,
|
|
147
|
+
headers: { Authorization: `Bearer \${${TOKEN_VARIABLE}}` }
|
|
148
|
+
} } }, null, 2);
|
|
149
|
+
}
|
|
150
|
+
/** The `claude mcp add` line that names HOLLWARE_TOKEN instead of the token. */
|
|
151
|
+
function claudeAddLine(url) {
|
|
152
|
+
return `claude mcp add --transport http hollware ${url}/mcp --header 'Authorization: Bearer \${${TOKEN_VARIABLE}}'`;
|
|
153
|
+
}
|
|
154
|
+
/** What `hollware init --json` prints, as far as this package reads it. */
|
|
155
|
+
function readInitJson(stdout) {
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(stdout);
|
|
158
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return Object.fromEntries(Object.entries(parsed));
|
|
159
|
+
} catch {}
|
|
160
|
+
throw new CreateError(`"hollware init --json" did not answer with a JSON object:\n${stdout}`);
|
|
161
|
+
}
|
|
130
162
|
/** Makes the project, and everything it needs to run locally. */
|
|
131
163
|
async function create(argv, io) {
|
|
132
164
|
const choices = readChoices(argv);
|
|
133
165
|
const target = resolve(io.cwd, choices.dir);
|
|
134
166
|
if (existsSync(target) && readdirSync(target).length > 0) throw new CreateError(`${target} is not empty: choose a new directory.`);
|
|
167
|
+
const tokenFile = choices.tokenFile === void 0 ? void 0 : resolve(io.cwd, choices.tokenFile);
|
|
168
|
+
if (tokenFile !== void 0 && existsSync(tokenFile)) throw new CreateError(`${tokenFile} exists already, and a token is never written over a file: choose another --token-file.`);
|
|
169
|
+
const say = (message) => {
|
|
170
|
+
if (!choices.json) io.out(message);
|
|
171
|
+
};
|
|
135
172
|
writeProject(io, target, choices);
|
|
136
|
-
|
|
173
|
+
say(`Created a Hollware project in ${target}.`);
|
|
174
|
+
if (tokenFile !== void 0) {
|
|
175
|
+
const inside = relative(target, tokenFile);
|
|
176
|
+
if (inside !== "" && !inside.startsWith("..") && !isAbsolute(inside)) appendFileSync(join(target, ".gitignore"), `\n# The token of "hollware init": never commit it.\n/${inside}\n`);
|
|
177
|
+
}
|
|
137
178
|
if (choices.git) await step(io, "git init", "git", ["init", "--quiet"], target);
|
|
179
|
+
const initArgs = [
|
|
180
|
+
"init",
|
|
181
|
+
"--site",
|
|
182
|
+
choices.site,
|
|
183
|
+
...choices.initFlags,
|
|
184
|
+
...tokenFile === void 0 ? [] : ["--token-file", tokenFile]
|
|
185
|
+
];
|
|
138
186
|
if (!choices.install) {
|
|
187
|
+
const next = [
|
|
188
|
+
`cd ${choices.dir}`,
|
|
189
|
+
`${choices.pm} install`,
|
|
190
|
+
`${choices.pm} exec vp fmt`,
|
|
191
|
+
`${choices.pm} exec hollware migrate --local`,
|
|
192
|
+
`${choices.pm} exec hollware ${initArgs.join(" ")} --local`
|
|
193
|
+
];
|
|
194
|
+
if (choices.json) {
|
|
195
|
+
io.out(JSON.stringify({
|
|
196
|
+
dir: target,
|
|
197
|
+
installed: false,
|
|
198
|
+
next
|
|
199
|
+
}));
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
139
202
|
io.out([
|
|
140
203
|
"",
|
|
141
204
|
"Next:",
|
|
142
|
-
`
|
|
143
|
-
` ${choices.pm} install`,
|
|
144
|
-
` ${choices.pm} exec hollware migrate --local`,
|
|
145
|
-
` ${choices.pm} exec hollware init --site ${[choices.site, ...choices.initFlags].join(" ")} --local`
|
|
205
|
+
...next.map((line) => ` ${line}`)
|
|
146
206
|
].join("\n"));
|
|
147
207
|
return 0;
|
|
148
208
|
}
|
|
149
|
-
|
|
209
|
+
say(`Installing with ${choices.pm} ...`);
|
|
150
210
|
await step(io, "The install", choices.pm, ["install"], target);
|
|
211
|
+
await step(io, "The formatting", join(target, "node_modules", ".bin", "vp"), ["fmt"], target);
|
|
151
212
|
const hollware = join(target, "node_modules", ".bin", "hollware");
|
|
152
213
|
await step(io, "The migrations", hollware, ["migrate", "--local"], target);
|
|
153
214
|
const init = await step(io, "Making the site", hollware, [
|
|
154
|
-
|
|
155
|
-
"--
|
|
156
|
-
choices.
|
|
157
|
-
...choices.initFlags,
|
|
158
|
-
"--local"
|
|
215
|
+
...initArgs,
|
|
216
|
+
"--local",
|
|
217
|
+
...choices.json ? ["--json"] : []
|
|
159
218
|
], target);
|
|
219
|
+
const dev = `${choices.pm} exec hollware dev`;
|
|
220
|
+
if (choices.json) {
|
|
221
|
+
const made = readInitJson(init.stdout.trim());
|
|
222
|
+
const url = typeof made["mcpUrl"] === "string" ? made["mcpUrl"].replace(/\/mcp$/, "") : void 0;
|
|
223
|
+
io.out(JSON.stringify({
|
|
224
|
+
dir: target,
|
|
225
|
+
installed: true,
|
|
226
|
+
...made,
|
|
227
|
+
devCommand: dev,
|
|
228
|
+
adminUrl: url === void 0 ? null : `${url}/admin/`,
|
|
229
|
+
mcpConfig: url === void 0 ? null : {
|
|
230
|
+
variable: TOKEN_VARIABLE,
|
|
231
|
+
claudeCode: claudeAddLine(url),
|
|
232
|
+
json: JSON.parse(mcpJson(url))
|
|
233
|
+
}
|
|
234
|
+
}));
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
160
237
|
io.out(init.stdout.trim());
|
|
161
|
-
const token = TOKEN.exec(init.stdout)?.[0] ?? "<the token above>";
|
|
162
238
|
io.out([
|
|
163
239
|
"",
|
|
164
|
-
"Start it
|
|
240
|
+
"Start it:",
|
|
165
241
|
` cd ${choices.dir}`,
|
|
166
|
-
` ${
|
|
167
|
-
` claude mcp add --transport http hollware http://localhost:5199/mcp --header "Authorization: Bearer ${token}"`,
|
|
242
|
+
` ${dev}`,
|
|
168
243
|
"",
|
|
169
244
|
"The admin is at http://localhost:5199/admin/. To deploy, log in to Cloudflare and run:",
|
|
170
245
|
` ${choices.pm} exec wrangler login`,
|
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -11,5 +11,10 @@ of the sites live in D1; this repository holds only the Worker.
|
|
|
11
11
|
- Content is never edited in files here. Write it through the MCP tools (`create_draft`,
|
|
12
12
|
`update_draft`, `validate`, `publish`) of the Hollware at `HOLLWARE_PUBLIC_URL` + `/mcp`.
|
|
13
13
|
- Run `pnpm test` and `pnpm exec wrangler types --check` after a change of `wrangler.jsonc`, and `pnpm exec hollware doctor` when something does not work.
|
|
14
|
+
- To update Hollware, move `hollware`, `@hollware/core`, `@hollware/server` and `@hollware/admin`
|
|
15
|
+
to one new version together, as exact versions
|
|
16
|
+
(`pnpm add -E hollware@X @hollware/core@X @hollware/server@X @hollware/admin@X`), read its
|
|
17
|
+
release notes, run `pnpm test`, then `pnpm exec hollware deploy`: it deploys the Worker and applies
|
|
18
|
+
the new migrations to the remote database. Migrations only move forward; content stays as it is.
|
|
14
19
|
- Never commit a token. `hollware init` and `hollware token create` print one once.
|
|
15
20
|
- Docs: https://hollware.com
|
package/template/package.json
CHANGED
package/template/vite.config.ts
CHANGED
|
@@ -2,8 +2,20 @@ import { cloudflare } from '@cloudflare/vite-plugin'
|
|
|
2
2
|
import { adminAssets } from '@hollware/admin'
|
|
3
3
|
import { defineConfig } from 'vite-plus'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/** Where `vp dev` serves Hollware: the URL of HOLLWARE_PUBLIC_URL before the first deploy. */
|
|
6
|
+
const LOCAL_URL = 'http://localhost:5199'
|
|
7
|
+
|
|
8
|
+
export default defineConfig(({ command }) => ({
|
|
9
|
+
plugins: [
|
|
10
|
+
cloudflare({
|
|
11
|
+
// `hollware deploy` writes the public URL into wrangler.jsonc, and the Worker answers only
|
|
12
|
+
// the Host of HOLLWARE_PUBLIC_URL: in development it is this machine's, whatever is deployed.
|
|
13
|
+
config:
|
|
14
|
+
command === 'serve'
|
|
15
|
+
? (config) => ({ vars: { ...config.vars, HOLLWARE_PUBLIC_URL: LOCAL_URL } })
|
|
16
|
+
: undefined,
|
|
17
|
+
}),
|
|
18
|
+
],
|
|
7
19
|
// The admin's built files: served at /admin/ in development, and copied into the build, where
|
|
8
20
|
// the Worker's static assets hold them.
|
|
9
21
|
publicDir: adminAssets(),
|
|
@@ -15,4 +27,14 @@ export default defineConfig({
|
|
|
15
27
|
// What you deploy is what the size budget of Hollware measures: minified.
|
|
16
28
|
minify: true,
|
|
17
29
|
},
|
|
18
|
-
|
|
30
|
+
// `vp check` formats and lints the project as Hollware writes it. wrangler writes
|
|
31
|
+
// worker-configuration.d.ts, so neither touches it.
|
|
32
|
+
fmt: {
|
|
33
|
+
semi: false,
|
|
34
|
+
singleQuote: true,
|
|
35
|
+
ignorePatterns: ['dist/**', '.wrangler/**', 'worker-configuration.d.ts'],
|
|
36
|
+
},
|
|
37
|
+
lint: {
|
|
38
|
+
ignorePatterns: ['dist/**', '.wrangler/**', 'worker-configuration.d.ts'],
|
|
39
|
+
},
|
|
40
|
+
}))
|
|
@@ -17,8 +17,11 @@ export default defineConfig({
|
|
|
17
17
|
cloudflareTest(async () => ({
|
|
18
18
|
wrangler: { configPath: './wrangler.jsonc' },
|
|
19
19
|
miniflare: {
|
|
20
|
-
// Read here in Node and applied inside workerd by the setup file.
|
|
21
20
|
bindings: {
|
|
21
|
+
// The tests reach the Worker at localhost, whatever public URL `hollware deploy` wrote
|
|
22
|
+
// into wrangler.jsonc: it answers only the Host of HOLLWARE_PUBLIC_URL.
|
|
23
|
+
HOLLWARE_PUBLIC_URL: 'http://localhost:5199',
|
|
24
|
+
// Read here in Node and applied inside workerd by the setup file.
|
|
22
25
|
TEST_MIGRATIONS: await readD1Migrations({
|
|
23
26
|
projectPath: import.meta.dirname,
|
|
24
27
|
migrationsDir,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
|
-
// Generated by Wrangler by running `wrangler types` (hash:
|
|
2
|
+
// Generated by Wrangler by running `wrangler types` (hash: f943445b38678ab7934813fd9b1a910b)
|
|
3
3
|
// Runtime types generated with workerd@1.20261012.1 2026-10-06 global_fetch_strictly_public,no_nodejs_compat,no_nodejs_compat_v2
|
|
4
4
|
interface __BaseEnv_Env {
|
|
5
5
|
OAUTH_KV: KVNamespace;
|
|
6
|
+
MEDIA: R2Bucket;
|
|
6
7
|
DB: D1Database;
|
|
7
8
|
EVENTS: Queue;
|
|
8
9
|
HOLLWARE_RATE_LIMIT: RateLimit;
|
|
10
|
+
IMAGES: ImagesBinding;
|
|
9
11
|
ASSETS: Fetcher;
|
|
10
12
|
HOLLWARE_PUBLIC_URL: "http://localhost:5199";
|
|
11
13
|
}
|
package/template/wrangler.jsonc
CHANGED
|
@@ -59,4 +59,9 @@
|
|
|
59
59
|
"migrations_dir": "node_modules/@hollware/server/migrations",
|
|
60
60
|
},
|
|
61
61
|
],
|
|
62
|
+
// The images of the sites (ADR-038): the bucket that keeps them, which the first deploy creates,
|
|
63
|
+
// and the Images binding that converts each once when it arrives (to WebP by default). Without
|
|
64
|
+
// the binding, images are kept as they come; without the bucket, upload_asset is unavailable.
|
|
65
|
+
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "my-hollware-media" }],
|
|
66
|
+
"images": { "binding": "IMAGES" },
|
|
62
67
|
}
|