kizlo 0.10.0 → 0.11.0

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/README.md CHANGED
@@ -23,16 +23,16 @@ Kizlo is an open-source framework for building content-driven applications with
23
23
 
24
24
  ## Quick start
25
25
 
26
- Set up Kizlo in your project:
26
+ Scaffold a new project with Kizlo already wired:
27
27
 
28
28
  ```bash
29
- npx kizlo@latest init
29
+ npx kizlo@latest create
30
30
  ```
31
31
 
32
- Start the local WordPress stack:
32
+ Or add Kizlo to an existing project:
33
33
 
34
34
  ```bash
35
- npx kizlo dev
35
+ npx kizlo@latest init
36
36
  ```
37
37
 
38
38
  See the [docs](https://kizlo.io/docs) for the full guide.
@@ -7,6 +7,7 @@ import { bootstrapDev } from "../wp/dev.js";
7
7
  import { removeProjectContainers } from "../wp/session.js";
8
8
  import { syncSiteSettings } from "../wp/settings.js";
9
9
  import { devStack } from "../wp/stack.js";
10
+ import { isPluginVersionSupported, pluginUpdateMessage } from "@kizlo/shared";
10
11
  import z from "zod/v4";
11
12
  import path from "node:path";
12
13
  import fs from "node:fs";
@@ -80,7 +81,11 @@ function managedEnv(envKeys, conn) {
80
81
  };
81
82
  }
82
83
  const requiredString = z.string().trim().min(1, "Required");
83
- const urlString = requiredString.pipe(z.url("Must be a valid URL (e.g. https://example.com)"));
84
+ const urlString = requiredString.pipe(z.url({
85
+ protocol: /^https?$/,
86
+ hostname: /^(localhost|(\d{1,3}\.){3}\d{1,3}|([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})$/,
87
+ error: "Must be a valid http(s) URL with a real host (e.g. https://example.com)"
88
+ }));
84
89
  const dirPath = requiredString.refine((value) => !value.endsWith(".ts"), "Enter a directory, not a file");
85
90
  function validate(schema) {
86
91
  return (value) => {
@@ -382,7 +387,11 @@ async function syncRemote(conn) {
382
387
  siteUrl: conn.siteUrl,
383
388
  backendUrl: conn.baseUrl
384
389
  });
385
- if (!sync.ok) p.log.warn(`Could not sync the site settings to WordPress (${sync.error}) — make sure the kizlo plugin is active, then set them from the Kizlo settings.`);
390
+ if (!sync.ok) {
391
+ p.log.warn(`Could not sync the site settings to WordPress (${sync.error}) — make sure the kizlo plugin is active, then set them from the Kizlo settings.`);
392
+ return;
393
+ }
394
+ if (!isPluginVersionSupported(sync.pluginVersion)) p.log.warn(pluginUpdateMessage(sync.pluginVersion));
386
395
  }
387
396
  /** The "Next steps" lines. `kizlo dev` is the single entry point for development. */
388
397
  function nextStepsLines(conn, prefix = "") {
@@ -1,7 +1,7 @@
1
1
  import { availablePackageManagers, detectInvokingPackageManager, ensureGitignored, frameworkCreateArgs, installArgs, runCommandAsync, runCommandCaptured } from "../utils.js";
2
2
  import { getPreset } from "../presets/index.js";
3
3
  import { fetchTemplate } from "../presets/source.js";
4
- import { adaptFile, changesFor, fileEntries, isExample, minCliError, patchEntries, readManifest, resolveDependencies } from "../presets/template.js";
4
+ import { adaptFile, changesFor, fileEntries, minCliError, patchEntries, readManifest, resolveDependencies } from "../presets/template.js";
5
5
  import { collectConnectionInteractively, nextStepsLines, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
6
6
  import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
7
7
  import z from "zod/v4";
@@ -63,8 +63,8 @@ function bootstrapArgs(manifest, pm, name) {
63
63
  * Layer Kizlo's wiring onto a freshly bootstrapped app in `dir`, from an already-fetched template
64
64
  * directory + manifest. The framework CLI has produced the base project (package.json, config,
65
65
  * tsconfig, the root layout); this drives the manifest — the same engine `init` uses — to record the
66
- * `kizlo` dependency, write `kizlo.config.ts`, scaffold the manifest's files (the `base` plumbing and
67
- * the `create` layout/styles always, the `example`-flagged demo pages only when `includeExamples`),
66
+ * `kizlo` dependency, write `kizlo.config.ts`, scaffold the manifest's files (core wiring from `base`
67
+ * and `create` always, the `example`-flagged demo pages only when `includeExamples`),
68
68
  * seed the generated contract, and ignore `.env` and the `.kizlo/` working dir (which holds the local WordPress install). On a fresh app Kizlo owns the layout, so `create`
69
69
  * writes it whole (already SEO-wired) rather than patching — `applyLayoutPatches` runs over `create`'s
70
70
  * patches, which are none today. The target directory layout comes straight from the manifest's
@@ -80,7 +80,7 @@ async function applyManifestWiring(dir, templateDir, manifest, opts) {
80
80
  clientUrl: void 0
81
81
  });
82
82
  recordDependencies(dir, manifest);
83
- const changes = changesFor(manifest, "create").filter((change) => opts.includeExamples || !isExample(change));
83
+ const changes = changesFor(manifest, "create", { includeExamples: opts.includeExamples });
84
84
  const files = [{
85
85
  label: "Kizlo config",
86
86
  relPath: "kizlo.config.ts",
@@ -1,4 +1,6 @@
1
1
  import { createStack, dockerHint, dockerStatus } from "../wp/docker.js";
2
+ import { findConfigDir } from "../wp/utils.js";
3
+ import { log } from "../daemon/logger.js";
2
4
  import { DEFAULT_ENV_KEYS, ensureGitignored, envGroups, groupDefault, mergeEnv, pickStackPort, withSpinner } from "../utils.js";
3
5
  import { resolveDevConfig, usesLocalWordPress } from "../daemon/config.js";
4
6
  import { bootstrapDev } from "../wp/dev.js";
@@ -6,6 +8,7 @@ import { reapOrphans, registerSession, removeProjectContainers, spawnWatchdog, u
6
8
  import { syncSiteSettings } from "../wp/settings.js";
7
9
  import { devStack } from "../wp/stack.js";
8
10
  import { palette } from "../banner.js";
11
+ import { watchReload } from "../daemon/reload.js";
9
12
  import { startWatcher } from "../daemon/watch.js";
10
13
  import { join } from "node:path";
11
14
  import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -166,13 +169,13 @@ function armForegroundTeardown(cfg) {
166
169
  }
167
170
  }
168
171
  /**
169
- * Boot local WordPress and run in the foreground until exit — the path behind bare
170
- * `kizlo dev`. Arms teardown first (so a mid-startup cancel still
171
- * stops partial containers), shows a timed spinner, prints the summary + stop hint, then
172
- * parks. A referenced timer holds the event loop open — an unresolved promise alone would
173
- * not, so without it Node would empty its loop and exit straight after printing.
172
+ * Bring the dev stack's containers up for `cfg`: clear any stale containers, resolve the published
173
+ * ports (stepping off a collision unless the port is pinned in config), write the compose files, and
174
+ * note any port that moved. Returns `cfg` with the resolved ports — the shape the rest of the boot
175
+ * and the teardown work against. Shared by the first boot and every reload reboot, so a changed
176
+ * `dev.port` in `kizlo.config.ts` is re-resolved the same way each time.
174
177
  */
175
- async function startForeground(cfg) {
178
+ async function prepareStack(cfg) {
176
179
  await removeProjectContainers(cfg.project);
177
180
  const port = await pickStackPort(cfg.port, {
178
181
  fixed: cfg.portExplicit,
@@ -191,11 +194,17 @@ async function startForeground(cfg) {
191
194
  createStack(devStack(ready));
192
195
  if (port !== cfg.port) note(`Port ${cfg.port} is in use — serving on ${port} instead.`);
193
196
  if (dbPort !== cfg.dbPort) note(`Database port ${cfg.dbPort} is in use — using ${dbPort} instead.`);
194
- armForegroundTeardown(ready);
195
- const start = Date.now();
196
- const creds = await withSpinner("Starting local WordPress", () => bootstrapDev(ready));
197
+ return ready;
198
+ }
199
+ /**
200
+ * After a boot, sync the fresh credentials (or the current LAN URL) into `.env` and the plugin, print
201
+ * the connection summary, and print the timed ready line. `reloaded` swaps the wording and drops the
202
+ * one-time Ctrl+C hint. `start` is when the boot began, so the timing spans the whole reboot including
203
+ * the spinner. The `.env` writes here are why the caller pauses the reload watcher around a reboot.
204
+ */
205
+ async function report(cfg, creds, reloaded, start) {
197
206
  if (creds.appPassword) {
198
- const env = updateWpEnv(ready, creds);
207
+ const env = updateWpEnv(cfg, creds);
199
208
  note("Updated .env with the new WordPress credentials");
200
209
  if (env) {
201
210
  const sync = await syncSiteSettings({
@@ -209,34 +218,118 @@ async function startForeground(cfg) {
209
218
  });
210
219
  if (!sync.ok) note(`Could not sync the site settings to WordPress (${sync.error}) — make sure the kizlo plugin is active, then set them from the Kizlo settings.`);
211
220
  }
212
- } else if (syncLocalUrl(ready, creds.url)) note("Updated .env WordPress URL to the current network address — restart your app to pick it up");
221
+ } else if (syncLocalUrl(cfg, creds.url)) note("Updated .env WordPress URL to the current network address — restart your app to pick it up");
213
222
  printSummary(creds);
214
223
  const { green, dim, bold, reset } = palette();
215
224
  const ms = Date.now() - start;
216
225
  const took = ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
217
- process.stdout.write(`\n ${dim}Press ${reset}${bold}Ctrl+C${reset}${dim} (or close the terminal) to stop local WordPress${reset}\n`);
218
- process.stdout.write(`\n ${green}✓${reset} Ready in ${took}\n\n`);
219
- const stopWatcher = await startWatcher(ready.configDir);
220
- if (stopWatcher) process.on("exit", stopWatcher);
226
+ if (!reloaded) process.stdout.write(`\n ${dim}Press ${reset}${bold}Ctrl+C${reset}${dim} (or close the terminal) to stop local WordPress${reset}\n`);
227
+ process.stdout.write(`\n ${green}✓${reset} ${reloaded ? "Reloaded" : "Ready"} in ${took}\n\n`);
228
+ }
229
+ /**
230
+ * Boot local WordPress for an already-prepared `cfg` (ports resolved, teardown armed) behind a spinner,
231
+ * then report. The first-boot path only — reboots go through {@link rebootStack}, which also folds the
232
+ * prepare step under the spinner.
233
+ */
234
+ async function bootAndReport(cfg) {
235
+ const start = Date.now();
236
+ await report(cfg, await withSpinner("Starting local WordPress", () => bootstrapDev(cfg)), false, start);
237
+ }
238
+ /**
239
+ * Reboot the whole stack in place after a config/env change: re-resolve the config, recreate the
240
+ * containers, and boot WordPress — all behind a single spinner labelled with the file that changed, so
241
+ * there's no loaderless gap between detecting the change and the boot. Teardown is already armed from
242
+ * the first boot, so this never re-arms. Returns the freshly resolved config (ports may have moved) so
243
+ * the caller can re-target its watchers.
244
+ */
245
+ async function rebootStack(cwd, label) {
246
+ const start = Date.now();
247
+ const { ready, creds } = await withSpinner(label, async () => {
248
+ const ready$1 = await prepareStack(await resolveDevConfig(cwd));
249
+ return {
250
+ ready: ready$1,
251
+ creds: await bootstrapDev(ready$1)
252
+ };
253
+ });
254
+ await report(ready, creds, true, start);
255
+ return ready;
256
+ }
257
+ /**
258
+ * Boot local WordPress and run in the foreground until exit — the path behind bare `kizlo dev`. Arms
259
+ * teardown first (so a mid-startup cancel still stops partial containers), boots and prints the
260
+ * summary, then parks. While parked it watches `.env` and `kizlo.config.*`: a change reboots the whole
261
+ * stack in place so a new port, connection, or fixture takes effect without rerunning the command. The
262
+ * reboot runs in the same process — the watchdog and session keep watching this PID throughout — and is
263
+ * guarded so the credential/URL writes it makes to `.env` don't trigger another reboot. A referenced
264
+ * timer holds the event loop open — an unresolved promise alone would not, so without it Node would
265
+ * empty its loop and exit straight after printing.
266
+ */
267
+ async function startForeground(cfg) {
268
+ const cwd = process.cwd();
269
+ let ready = await prepareStack(cfg);
270
+ armForegroundTeardown(ready);
271
+ await bootAndReport(ready);
272
+ let stopContract = await startWatcher(ready.configDir);
273
+ let reloading = false;
274
+ const reload = async (changed) => {
275
+ if (reloading) return;
276
+ reloading = true;
277
+ try {
278
+ stopContract?.();
279
+ ready = await rebootStack(cwd, `${changed} changed — restarting the session`);
280
+ stopContract = await startWatcher(ready.configDir);
281
+ } catch (error) {
282
+ log.error("Failed to reload the dev session:", error);
283
+ } finally {
284
+ setTimeout(() => {
285
+ reloading = false;
286
+ }, 500);
287
+ }
288
+ };
289
+ const stopReload = watchReload(ready.configDir, (changed) => void reload(changed));
290
+ process.on("exit", () => {
291
+ stopContract?.();
292
+ stopReload();
293
+ });
221
294
  setInterval(() => {}, 1 << 30);
222
295
  await new Promise(() => {});
223
296
  }
224
297
  /**
225
298
  * Run the contract watcher in the foreground until exit — the path bare `kizlo dev` takes when local
226
299
  * WordPress isn't configured (a project pointing at its own WordPress). Generates once, then watches
227
- * the server files and regenerates on save. Returns immediately when another watcher already holds
228
- * the lock; otherwise the persistent watcher keeps the process alive on its own.
300
+ * the server files and regenerates on save. It also watches `.env` and `kizlo.config.*` and restarts
301
+ * the watcher on a change, so a new server dir or env value is picked up the same way the local path
302
+ * reboots its stack. Returns immediately when another watcher already holds the lock; otherwise the
303
+ * persistent watcher keeps the process alive on its own.
229
304
  */
230
305
  async function watchOnly(cwd) {
231
- const stop = await startWatcher(cwd);
306
+ let stop = await startWatcher(cwd);
232
307
  if (!stop) return;
233
- process.on("exit", stop);
308
+ let reloading = false;
309
+ const reload = async () => {
310
+ if (reloading) return;
311
+ reloading = true;
312
+ try {
313
+ stop?.();
314
+ stop = await startWatcher(cwd);
315
+ } finally {
316
+ setTimeout(() => {
317
+ reloading = false;
318
+ }, 300);
319
+ }
320
+ };
321
+ const stopReload = watchReload(findConfigDir(cwd), () => void reload());
322
+ const shutdown = () => {
323
+ stop?.();
324
+ stopReload();
325
+ };
326
+ process.on("exit", shutdown);
234
327
  for (const signal of [
235
328
  "SIGINT",
236
329
  "SIGTERM",
237
330
  "SIGHUP"
238
331
  ]) process.on(signal, () => {
239
- stop();
332
+ shutdown();
240
333
  process.exit(0);
241
334
  });
242
335
  }
@@ -185,6 +185,10 @@ const init = defineCommand({
185
185
  });
186
186
  if (args.alias !== void 0) setup.alias = String(args.alias).trim();
187
187
  setup.alias = aliasWithSlash(setup.alias);
188
+ const includeExamples = manifest && fetched ? yes ? true : orCancel(await p.confirm({
189
+ message: "Add example pages?",
190
+ initialValue: true
191
+ })) : false;
188
192
  if (!hasKizlo) {
189
193
  const spec = `kizlo@${manifest?.dependencies?.kizlo ?? `^${getVersion()}`}`;
190
194
  const s = p.spinner();
@@ -217,7 +221,7 @@ const init = defineCommand({
217
221
  }];
218
222
  if (manifest && fetched) {
219
223
  await alignDependencies(cwd, pm, pkg, manifest);
220
- for (const entry of fileEntries(changesFor(manifest, "init"))) files.push(adaptFile(fetched.dir, entry, manifest.conventions, scaffold));
224
+ for (const entry of fileEntries(changesFor(manifest, "init", { includeExamples }))) files.push(adaptFile(fetched.dir, entry, manifest.conventions, scaffold));
221
225
  } else if (preset.scaffolds) files.push(...preset.scaffolds(scaffold));
222
226
  const scaffolded = [];
223
227
  for (const file of files) scaffolded.push({
@@ -154,4 +154,4 @@ async function resolveDevConfig(cwd) {
154
154
  }
155
155
 
156
156
  //#endregion
157
- export { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveConfig, resolveDevConfig, resolveStackName, resolveTestConfig, usesLocalWordPress };
157
+ export { CONFIG_FILES, DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveConfig, resolveDevConfig, resolveStackName, resolveTestConfig, usesLocalWordPress };
@@ -2,7 +2,7 @@ import { consola } from "consola";
2
2
 
3
3
  //#region src/cli/daemon/logger.ts
4
4
  /** Tagged logger for the daemon — every line is prefixed with `kizlo`. */
5
- const log = consola.withTag("kizlo");
5
+ const log = consola;
6
6
  log.options.formatOptions.date = false;
7
7
 
8
8
  //#endregion
@@ -0,0 +1,66 @@
1
+ import { CONFIG_FILES } from "./config.js";
2
+ import path from "node:path";
3
+ import { readFileSync } from "node:fs";
4
+ import { createHash } from "node:crypto";
5
+ import { FSWatcher } from "chokidar";
6
+
7
+ //#region src/cli/daemon/reload.ts
8
+ /** Env files `loadEnvFiles` reads, in the same order — the runtime values a reload must pick up. */
9
+ const ENV_FILES = [".env", ".env.local"];
10
+ /** A combined hash of the watched files' contents; a missing file counts as empty. */
11
+ function fingerprint(files) {
12
+ const hash = createHash("sha1");
13
+ for (const file of files) {
14
+ hash.update(file);
15
+ hash.update("\0");
16
+ try {
17
+ hash.update(readFileSync(file));
18
+ } catch {}
19
+ hash.update("\0");
20
+ }
21
+ return hash.digest("hex");
22
+ }
23
+ /**
24
+ * Watch the project's `.env` files and `kizlo.config.*` and call `onReload` with the changed file's
25
+ * basename when their *contents* change. `kizlo dev` uses this to restart its whole session — rebooting
26
+ * local WordPress and regenerating the contract — so a changed port, connection, or fixture takes
27
+ * effect without stopping and rerunning the command by hand. Debounced so an editor's multi-write save
28
+ * fires once, and gated on a content hash so a no-op save (`touch`, or writing the file back unchanged)
29
+ * is ignored rather than triggering a pointless reboot. Returns a synchronous `stop()` that closes the
30
+ * watcher.
31
+ *
32
+ * `dev` rewrites `.env` itself while booting (fresh credentials, the current LAN URL); the caller
33
+ * pauses its reload handler around a reboot so that self-write doesn't trigger another reload. The hash
34
+ * is still refreshed for it here, so once the reboot settles the new content is the baseline and won't
35
+ * re-fire.
36
+ */
37
+ function watchReload(configDir, onReload) {
38
+ const files = [...ENV_FILES, ...CONFIG_FILES].map((name) => path.join(configDir, name));
39
+ const watcher = new FSWatcher({
40
+ persistent: true,
41
+ ignoreInitial: true
42
+ });
43
+ let last = fingerprint(files);
44
+ let timer;
45
+ const schedule = (changed) => {
46
+ clearTimeout(timer);
47
+ timer = setTimeout(() => {
48
+ const next = fingerprint(files);
49
+ if (next === last) return;
50
+ last = next;
51
+ onReload(path.basename(changed));
52
+ }, 300);
53
+ };
54
+ watcher.add(files);
55
+ watcher.on("all", (_event, changed) => schedule(changed));
56
+ let stopped = false;
57
+ return () => {
58
+ if (stopped) return;
59
+ stopped = true;
60
+ clearTimeout(timer);
61
+ watcher.close();
62
+ };
63
+ }
64
+
65
+ //#endregion
66
+ export { watchReload };
@@ -30,7 +30,6 @@ async function watch(cfg) {
30
30
  });
31
31
  const onChange = debounce(() => regenerate(cfg), 300);
32
32
  watcher.add(path.resolve(cfg.cwd, cfg.serverDir));
33
- watcher.on("ready", () => log.start("Watching for contract changes..."));
34
33
  watcher.on("all", () => void onChange());
35
34
  return watcher;
36
35
  }
@@ -8,17 +8,25 @@ import fs from "node:fs";
8
8
  /**
9
9
  * The template's `template.json`. It is the single declaration of what Kizlo lays onto a project and
10
10
  * how — the same engine drives both `create` (onto a fresh app the framework CLI just bootstrapped)
11
- * and `init` (onto the user's existing project). Changes are grouped into three sections so a command
12
- * only ever applies what is safe for it:
11
+ * and `init` (onto the user's existing project). Changes are grouped into three sections by the one
12
+ * question that decides whether a command may safely apply them — *where is this safe to land?* — and
13
+ * no command ever reads another command's section:
13
14
  *
14
- * - `base` — applied by both commands: the Kizlo plumbing (server entry, client, routes). Every
15
- * change here must be safe on an existing project, so it is limited to files Kizlo owns, which
16
- * never collide with the user's pages.
17
- * - `create` — applied only by `create`, onto a freshly bootstrapped app it may freely overwrite:
18
- * the root layout (written whole, already SEO-wired) and its styles as core wiring, plus the demo
19
- * pages flagged `example`, which are written only when the user asks for example pages.
20
- * - `init` — applied only by `init`, onto the user's existing project. The root-layout SEO wiring
21
- * lands here as a `patch` so it merges into the layout the user already owns instead of replacing it.
15
+ * - `base` — safe on any project: additive, Kizlo-owned paths that never collide with the user's own
16
+ * files (the server entry, client, routes, and additive demo pages like a new blog route). Applied
17
+ * by both commands.
18
+ * - `create` — safe only on a freshly bootstrapped app `create` may freely overwrite: the files the
19
+ * framework CLI already scaffolded, like the root layout (written whole, SEO-wired) and the homepage
20
+ * showcase. Applied only by `create`, never `init` — overwriting these on a real project would
21
+ * clobber the user's work.
22
+ * - `init` — the non-destructive counterpart for the user's existing project: the root-layout SEO
23
+ * wiring lands here as a `patch` that merges into the layout the user already owns.
24
+ *
25
+ * Cutting across all three is the orthogonal `example` flag: opt-in demo content, applied only when the
26
+ * user answers yes to "Add example pages?". A cross-cutting example (e.g. a blog) files each of its
27
+ * pieces in the section that matches its safety — the additive new route in `base`, the homepage
28
+ * showcase that overwrites an app-owned file in `create` — so `init` picks up only the additive half and
29
+ * never touches a file the user owns, while `create` gets the whole demo. See {@link changesFor}.
22
30
  *
23
31
  * Each section is a list of changes, every one a member of a small closed set — deliberately no
24
32
  * "patch an arbitrary source file" kind, so a dangerous strategy is unrepresentable:
@@ -162,12 +170,16 @@ function minCliError(manifest) {
162
170
  return `This template needs the kizlo CLI >= ${manifest.minCli} (you have ${running}). Run \`npx kizlo@latest\` and try again.`;
163
171
  }
164
172
  /**
165
- * The changes a command applies: the shared `base` set plus the command's own section. `create` gets
166
- * `base + create` (plumbing plus the demo pages); `init` gets `base + init` (plumbing plus any
167
- * init-only surgical patches). `base` is applied first so a later section can override it.
173
+ * The changes a command applies: the shared `base` set plus the command's own section (`base` first, so
174
+ * a later section can override it). Section placement is the *safety* axis — `base` is additive and safe
175
+ * on any project, `create`/`init` carry the fresh-app-vs-existing-project halves — while the `example`
176
+ * flag is the orthogonal *opt-in* axis. Example changes are dropped unless `includeExamples`, so the same
177
+ * call serves both "core wiring only" and "plus the demo pages", and each command only ever sees its own
178
+ * section: init's opt-in examples are the additive ones in `base`, never create's homepage overwrite.
168
179
  */
169
- function changesFor(manifest, command) {
170
- return [...manifest.base, ...manifest[command]];
180
+ function changesFor(manifest, command, opts = {}) {
181
+ const changes = [...manifest.base, ...manifest[command]];
182
+ return opts.includeExamples ? changes : changes.filter((change) => !isExample(change));
171
183
  }
172
184
  /** The whole-file changes in a resolved change list. */
173
185
  function fileEntries(changes) {
@@ -177,7 +189,7 @@ function fileEntries(changes) {
177
189
  function patchEntries(changes) {
178
190
  return changes.filter((change) => change.kind === "patch");
179
191
  }
180
- /** Whether a change is opt-in demo content — written by `create` only when example pages are requested. */
192
+ /** Whether a change is opt-in demo content — applied only when example pages are requested. */
181
193
  function isExample(change) {
182
194
  return change.kind === "file" && change.example === true;
183
195
  }
@@ -267,4 +279,4 @@ function rendersHtml(file) {
267
279
  }
268
280
 
269
281
  //#endregion
270
- export { adaptFile, changesFor, fileEntries, findRootLayout, isExample, isOlderVersion, minCliError, patchEntries, readManifest, resolveDependencies, resolvePatch };
282
+ export { adaptFile, changesFor, fileEntries, findRootLayout, isOlderVersion, minCliError, patchEntries, readManifest, resolveDependencies, resolvePatch };
@@ -1,5 +1,6 @@
1
1
  import { WP_KIZLO_BASE } from "../../wordpress/constants.js";
2
2
  import { WordPressService } from "../../wordpress/service.js";
3
+ import { PLUGIN_VERSION_HEADER } from "@kizlo/shared";
3
4
 
4
5
  //#region src/cli/wp/settings.ts
5
6
  /** Loopback hosts that, from inside the dev Docker stack, must be rewritten to reach the host. */
@@ -52,11 +53,16 @@ async function syncSiteSettings(credentials, settings) {
52
53
  base: WP_KIZLO_BASE,
53
54
  body
54
55
  });
56
+ const pluginVersion = response.headers.get(PLUGIN_VERSION_HEADER);
55
57
  if (response.error) return {
56
58
  ok: false,
57
- error: describeError(response.status, response.error)
59
+ error: describeError(response.status, response.error),
60
+ pluginVersion
61
+ };
62
+ return {
63
+ ok: true,
64
+ pluginVersion
58
65
  };
59
- return { ok: true };
60
66
  }
61
67
  /**
62
68
  * A short, log-friendly summary of a failed request. A transport failure has no HTTP status (`0`), so
@@ -1,5 +1,5 @@
1
- import { ListMetadata, ListOrder, Media } from "../shared/schema.js";
2
- import { NumberLike, arrayable } from "@kizlo/shared";
1
+ import { ListMetadata, ListOrder } from "../shared/schema.js";
2
+ import { Media, NumberLike, arrayable } from "@kizlo/shared";
3
3
  import z from "zod/v4";
4
4
 
5
5
  //#region src/comment/schema.ts
package/dist/context.d.ts CHANGED
@@ -60,6 +60,15 @@ declare class Context {
60
60
  private readonly settings;
61
61
  private readonly email;
62
62
  constructor(config: ContextConfig);
63
+ /**
64
+ * Warn when the WordPress plugin is older than {@link isPluginVersionSupported} allows, read from the
65
+ * `X-Kizlo-Version` header the plugin stamps on every response. A version mismatch is a serious
66
+ * contract break, so this fires on every WordPress response rather than once — the header rides
67
+ * responses the runtime already makes, so it costs no extra request. Emitted through `console.warn`
68
+ * rather than the logger adapter on purpose: the adapter is optional (it defaults to a no-op), and
69
+ * this is a setup problem every user must see regardless of whether they've wired up logging.
70
+ */
71
+ private warnIfPluginOutdated;
63
72
  createRestContext(request: Request): {
64
73
  headers: Headers;
65
74
  request: Request;
package/dist/context.js CHANGED
@@ -5,7 +5,7 @@ import { CookiesStorage } from "./cookie.js";
5
5
  import { WordPressService } from "./wordpress/service.js";
6
6
  import { EmailService } from "./email/service.js";
7
7
  import { SettingsService } from "./settings/service.js";
8
- import { base64Decode, tryCatchSync } from "@kizlo/shared";
8
+ import { PLUGIN_VERSION_HEADER, base64Decode, isPluginVersionSupported, pluginUpdateMessage, tryCatchSync } from "@kizlo/shared";
9
9
  import { serialize } from "cookie";
10
10
 
11
11
  //#region src/context.ts
@@ -17,11 +17,29 @@ var Context = class {
17
17
  email;
18
18
  constructor(config) {
19
19
  this.config = config;
20
- this.wordpress = new WordPressService(config);
20
+ this.wordpress = new WordPressService({
21
+ ...config,
22
+ onResponse: (headers) => this.warnIfPluginOutdated(headers)
23
+ });
21
24
  this.settings = new SettingsService(this.wordpress);
22
25
  this.email = new EmailService(this.wordpress);
23
26
  this.logger = this.createLogger();
24
27
  }
28
+ /**
29
+ * Warn when the WordPress plugin is older than {@link isPluginVersionSupported} allows, read from the
30
+ * `X-Kizlo-Version` header the plugin stamps on every response. A version mismatch is a serious
31
+ * contract break, so this fires on every WordPress response rather than once — the header rides
32
+ * responses the runtime already makes, so it costs no extra request. Emitted through `console.warn`
33
+ * rather than the logger adapter on purpose: the adapter is optional (it defaults to a no-op), and
34
+ * this is a setup problem every user must see regardless of whether they've wired up logging.
35
+ */
36
+ warnIfPluginOutdated(headers) {
37
+ const installed = headers.get(PLUGIN_VERSION_HEADER);
38
+ if (isPluginVersionSupported(installed)) return;
39
+ const inner = ` ${pluginUpdateMessage(installed)} `;
40
+ const border = "-".repeat(inner.length + 2);
41
+ console.warn(`\n\x1b[1m\x1b[33m${border}\n|${inner}|\n${border}\x1b[0m\n`);
42
+ }
25
43
  createRestContext(request) {
26
44
  const headers = new Headers();
27
45
  return {
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ import "./wordpress/index.js";
22
22
  import { DevPluginSource, Fixture, FixtureCleanupFn, FixtureSeedFn, JsonValue, LocalPlugin, PluginSource, SeedContext, TestAdminUser, TestCredentials, TestUser, defineFixture, isLocalPlugin } from "./cli/wp/types.js";
23
23
  import { AuthAdapter, AuthGetUser, AuthUser, authMock, createAuthAdapter } from "./adapters/auth.js";
24
24
  import { CaptchaAdapter, altcha, arkose, createCaptchaAdapter, friendlyCaptcha, hcaptcha, recaptcha, turnstile } from "./adapters/captcha.js";
25
- import { CurrencyFormat, IdentifierInput, LIST_ORDER, ListMetadata, ListOrder, Media, PreviewTokenData, PreviewTokenPayload } from "./shared/schema.js";
25
+ import { CurrencyFormat, IdentifierInput, LIST_ORDER, ListMetadata, ListOrder, PreviewTokenData, PreviewTokenPayload } from "./shared/schema.js";
26
26
  import { AnyContext, AnyNestedClient, CookiesAdapter, CookiesDeleteAll, CookiesGetAll, CookiesSetAll, HTTPMethod, List, MaybeOptionalOptions, RedirectInput, RedirectStatus } from "./shared/types.js";
27
27
  import { createCookiesAdapter } from "./adapters/cookies.js";
28
28
  import { ConnInfo, GeoAdapter, GeoGetConnInfo, createGeoAdapter, geoMock } from "./adapters/geo.js";
@@ -52,4 +52,4 @@ import { ShaAlgorithm, compare, hash, hmac } from "./shared/crypto.js";
52
52
  import { Identifier, parseIdentifier } from "./shared/identifier.js";
53
53
  import { createOrpcRouter } from "./shared/router.js";
54
54
  import { deserializeCurrencyFormat, deserializeListMetadata, emptyList, serializeCurrencyFormat } from "./shared/serialize.js";
55
- export { AnyContext, AnyEventSchema, AnyEvents, AnyExtension, AnyKizloConfig, AnyMiddleware, AnyNestedClient, AnyProcedure, AnyProcedureRouter, AuthAdapter, AuthGetUser, AuthUser, AuthUserFn, AuthorsSettings, AuthorsSettingsInput, BaseHandlerOptions, BrandSettings, BrandSettingsInput, CART_HASH_HEADER_KEY, COMMON_ERRORS, CONTENT_TYPES, CaptchaAdapter, CategoryService, CommentService, CommonErrorCode, CommonErrorLiteral, CommonErrorMap, ConnInfo, ConnInfoFn, ConsoleLogAdapterOptions, ContentType, Context, ContextConfig, Contract, ContractProcedure, CookiesAdapter, CookiesDeleteAll, CookiesGetAll, CookiesSetAll, CookiesStorage, CrawlingSettings, CrawlingSettingsInput, CreateKizloOptions, CreateSitemapRouteOptions, CurrencyFormat, DefinedErrorMapLike, DevPluginSource, Environment, ErrorDefinition, EventHandler, EventHandlerFn, Extension, ExtensionInitFn, ExtensionInitOptions, ExtensionInitResult, ExtractExtensionRouter, ExtractMiddlewareContextOutput, ExtractParams, ExtractProcedureByScope, Fixture, FixtureCleanupFn, FixtureSeedFn, GeoAdapter, GeoGetConnInfo, HTTPMethod, IconDescriptor, Identifier, IdentifierInput, IdentitySettings, IdentitySettingsInput, InferEventUnion, InferExtensionRouter, InferMiddlewares, InvocationScope, JsonValue, Kizlo, KizloClient, KizloClientConfig, KizloConfig, KizloConnect, KizloError, KizloErrorOptions, LIST_ORDER, List, ListMetadata, ListOrder, ListSitemapUrlInput, ListSitemapUrlInputBase, LocalPlugin, LogLevel, LogPayload, Logger, LoggerAdapter, ManifestIcon, MaybeOptionalOptions, Media, MenuItemService, MenuService, Middleware, MiddlewareHandler, MiddlewareNextFn, MiddlewareOptions, MiddlewareResult, OpenapiPart, OrganizationFounder, OrganizationSettings, OrganizationSettingsInput, PageService, PersonSettings, PersonSettingsInput, PluginSource, PostService, PostStatusDefinition, PostTypeSettings, PostTypeSettingsInput, PreviewTokenData, PreviewTokenPayload, Procedure, ProcedureContext, ProcedureErrors, ProcedureHandler, ProcedureOptions, ROUTER_MAP, RedirectInput, RedirectStatus, ResolveParamsPart, ResolvedIcons, ResolvedSchemaInput, ResolvedSchemaOutput, Robots, RobotsRule, RootRouter, RouterDepthTuple, RouterMap, S2SClient, SAFE_REQUEST_TIMEOUT, SITEMAP_BASE, SITEMAP_INDEX_SLUG, SchemaIO, SeedContext, Seo, SeoHead, SeoSchema, ServiceAdapters, Settings, SettingsService, ShaAlgorithm, SiteSettings, SiteSettingsInput, Sitemap, SitemapIndex, SitemapIndexEntry, SitemapList, SitemapRouteExtra, SitemapUrl, SitemapUrlImage, SitemapUrlList, SocialProfile, TaxonomySettings, TaxonomySettingsInput, TestAdminUser, TestCredentials, TestUser, ThrowableErrorFn, ThrowableErrorMap, UNEXPECTED_BODY_SNIPPET_LENGTH, UploadsSettings, UploadsSettingsInput, UserService, VerifyCaptchaFn, VerifyPreviewTokenFn, WC_CORE_BASE, WC_STORE_BASE, WEB_MANIFEST_ROUTE, WPK_RobotRule, WPK_Robots, WPK_Seo, WPK_Sitemap, WPK_SitemapContentType, WPK_SitemapIndex, WPK_SitemapUrl, WPK_SitemapUrlImage, WP_AUTH_HEADER_KEY, WP_AUTH_TYPE, WP_CATEGORY_ORDER_BYS, WP_CORE_BASE, WP_Category, WP_CategoryCreateErrorCode, WP_CategoryCreateInput, WP_CategoryDeleteErrorCode, WP_CategoryDeleteInput, WP_CategoryDeleteResponse, WP_CategoryLinks, WP_CategoryListErrorCode, WP_CategoryListInput, WP_CategoryOrderBy, WP_CategoryRetrieveErrorCode, WP_CategoryRetrieveInput, WP_CategoryTaxonomy, WP_CategoryUpdateErrorCode, WP_CategoryUpdateInput, WP_Comment, WP_CommentAvatarUrls, WP_CommentContent, WP_CommentContentInput, WP_CommentCreateErrorCode, WP_CommentCreateInput, WP_CommentDeleteErrorCode, WP_CommentDeleteInput, WP_CommentDeleteResponse, WP_CommentLinks, WP_CommentListErrorCode, WP_CommentListInput, WP_CommentOrderBy, WP_CommentRetrieveErrorCode, WP_CommentRetrieveInput, WP_CommentStatus, WP_CommentStatusFilter, WP_CommentStatusInput, WP_CommentType, WP_CommentUpdateErrorCode, WP_CommentUpdateInput, WP_CommonErrorCode, WP_Context, WP_CurrencyFormat, WP_Error, WP_ErrorData, WP_KIZLO_BASE, WP_Link, WP_List, WP_ListMetadata, WP_ListOrder, WP_MENU_ITEM_ORDER_BYES, WP_MENU_ITEM_TARGETS, WP_MENU_ITEM_TAX_RELATIONS, WP_Menu, WP_MenuCreateErrorCode, WP_MenuCreateInput, WP_MenuDeleteErrorCode, WP_MenuDeleteInput, WP_MenuDeleteResponse, WP_MenuItem, WP_MenuItemCreateErrorCode, WP_MenuItemCreateInput, WP_MenuItemDeleteErrorCode, WP_MenuItemDeleteInput, WP_MenuItemDeleteResponse, WP_MenuItemLinks, WP_MenuItemListErrorCode, WP_MenuItemListInput, WP_MenuItemOrderBy, WP_MenuItemRetrieveErrorCode, WP_MenuItemRetrieveInput, WP_MenuItemStatus, WP_MenuItemStatusFilter, WP_MenuItemTarget, WP_MenuItemTaxRelation, WP_MenuItemTitle, WP_MenuItemTitleInput, WP_MenuItemType, WP_MenuItemUpdateErrorCode, WP_MenuItemUpdateInput, WP_MenuLinks, WP_MenuListErrorCode, WP_MenuListInput, WP_MenuOrderBy, WP_MenuRetrieveErrorCode, WP_MenuRetrieveInput, WP_MenuUpdateErrorCode, WP_MenuUpdateInput, WP_PAGE_ORDER_BYES, WP_PAGE_STATUSES, WP_POST_FORMATS, WP_POST_ORDER_BYES, WP_POST_STATUSES, WP_POST_TAX_RELATIONS, WP_Page, WP_PageCommentStatus, WP_PageContent, WP_PageContentInput, WP_PageCreateErrorCode, WP_PageCreateInput, WP_PageDeleteErrorCode, WP_PageDeleteInput, WP_PageDeleteResponse, WP_PageExcerpt, WP_PageExcerptInput, WP_PageGuid, WP_PageLinks, WP_PageListErrorCode, WP_PageListInput, WP_PageOrderBy, WP_PageRetrieveErrorCode, WP_PageRetrieveInput, WP_PageStatus, WP_PageStatusFilter, WP_PageTitle, WP_PageTitleInput, WP_PageUpdateErrorCode, WP_PageUpdateInput, WP_Post, WP_PostCommentStatus, WP_PostContent, WP_PostContentInput, WP_PostCreateErrorCode, WP_PostCreateInput, WP_PostDeleteErrorCode, WP_PostDeleteInput, WP_PostDeleteResponse, WP_PostExcerpt, WP_PostExcerptInput, WP_PostFormat, WP_PostGuid, WP_PostLinks, WP_PostListErrorCode, WP_PostListInput, WP_PostOrderBy, WP_PostRetrieveErrorCode, WP_PostRetrieveInput, WP_PostStatus, WP_PostStatusFilter, WP_PostTaxRelation, WP_PostTitle, WP_PostTitleInput, WP_PostUpdateErrorCode, WP_PostUpdateInput, WP_RequestInput, WP_Result, WP_TAG_ORDER_BYS, WP_Tag, WP_TagCreateErrorCode, WP_TagCreateInput, WP_TagDeleteErrorCode, WP_TagDeleteInput, WP_TagDeleteResponse, WP_TagLinks, WP_TagListErrorCode, WP_TagListInput, WP_TagOrderBy, WP_TagRetrieveErrorCode, WP_TagRetrieveInput, WP_TagTaxonomy, WP_TagUpdateErrorCode, WP_TagUpdateInput, WP_User, WP_UserAvatarUrls, WP_UserCapabilities, WP_UserCreateErrorCode, WP_UserCreateInput, WP_UserDeleteErrorCode, WP_UserDeleteInput, WP_UserDeleteMeErrorCode, WP_UserDeleteMeInput, WP_UserDeleteMeResponse, WP_UserDeleteResponse, WP_UserLinks, WP_UserListErrorCode, WP_UserListInput, WP_UserOrderBy, WP_UserRetrieveErrorCode, WP_UserRetrieveInput, WP_UserRetrieveMeErrorCode, WP_UserRetrieveMeInput, WP_UserUpdateErrorCode, WP_UserUpdateInput, WP_UserUpdateMeErrorCode, WP_UserUpdateMeInput, WP_UserWho, WebManifest, WebhookOptions, WebhookSettings, WebhookSettingsInput, WithCommonErrorMap, WordPressCredentials, WordPressService, altcha, arkose, authMock, buildProcedure, buildWebManifest, combineDetailed, compare, consoleLog, createAuthAdapter, createCaptchaAdapter, createCookiesAdapter, createEventHandler, createExtension, createGeoAdapter, createKizlo, createKizloClient, createLoggerAdapter, createManifestRoute, createMiddleware, createOrpcRouter, createProcedure, createSitemapRedirectRoute, createSitemapRoute, createThrowableErrorMap, createWebhookRouter, defineErrorMap, defineFixture, deserializeCurrencyFormat, deserializeListMetadata, deserializeSeo, emptyList, friendlyCaptcha, generateContract, geoMock, geoVercelNext, handleUnexpectedError, hash, hcaptcha, hmac, isContractProcedure, isKizloError, isLocalPlugin, isWordPressResourceError, noopAdapter, parseIdentifier, parseSitemapSlug, recaptcha, renderSitemapIndex, renderUrlSet, resolveIcons, resolveKizloConfig, restoreContract, schemaType, serializeCurrencyFormat, sitemapEntryPath, toKizloError, turnstile, xmlResponse };
55
+ export { AnyContext, AnyEventSchema, AnyEvents, AnyExtension, AnyKizloConfig, AnyMiddleware, AnyNestedClient, AnyProcedure, AnyProcedureRouter, AuthAdapter, AuthGetUser, AuthUser, AuthUserFn, AuthorsSettings, AuthorsSettingsInput, BaseHandlerOptions, BrandSettings, BrandSettingsInput, CART_HASH_HEADER_KEY, COMMON_ERRORS, CONTENT_TYPES, CaptchaAdapter, CategoryService, CommentService, CommonErrorCode, CommonErrorLiteral, CommonErrorMap, ConnInfo, ConnInfoFn, ConsoleLogAdapterOptions, ContentType, Context, ContextConfig, Contract, ContractProcedure, CookiesAdapter, CookiesDeleteAll, CookiesGetAll, CookiesSetAll, CookiesStorage, CrawlingSettings, CrawlingSettingsInput, CreateKizloOptions, CreateSitemapRouteOptions, CurrencyFormat, DefinedErrorMapLike, DevPluginSource, Environment, ErrorDefinition, EventHandler, EventHandlerFn, Extension, ExtensionInitFn, ExtensionInitOptions, ExtensionInitResult, ExtractExtensionRouter, ExtractMiddlewareContextOutput, ExtractParams, ExtractProcedureByScope, Fixture, FixtureCleanupFn, FixtureSeedFn, GeoAdapter, GeoGetConnInfo, HTTPMethod, IconDescriptor, Identifier, IdentifierInput, IdentitySettings, IdentitySettingsInput, InferEventUnion, InferExtensionRouter, InferMiddlewares, InvocationScope, JsonValue, Kizlo, KizloClient, KizloClientConfig, KizloConfig, KizloConnect, KizloError, KizloErrorOptions, LIST_ORDER, List, ListMetadata, ListOrder, ListSitemapUrlInput, ListSitemapUrlInputBase, LocalPlugin, LogLevel, LogPayload, Logger, LoggerAdapter, ManifestIcon, MaybeOptionalOptions, MenuItemService, MenuService, Middleware, MiddlewareHandler, MiddlewareNextFn, MiddlewareOptions, MiddlewareResult, OpenapiPart, OrganizationFounder, OrganizationSettings, OrganizationSettingsInput, PageService, PersonSettings, PersonSettingsInput, PluginSource, PostService, PostStatusDefinition, PostTypeSettings, PostTypeSettingsInput, PreviewTokenData, PreviewTokenPayload, Procedure, ProcedureContext, ProcedureErrors, ProcedureHandler, ProcedureOptions, ROUTER_MAP, RedirectInput, RedirectStatus, ResolveParamsPart, ResolvedIcons, ResolvedSchemaInput, ResolvedSchemaOutput, Robots, RobotsRule, RootRouter, RouterDepthTuple, RouterMap, S2SClient, SAFE_REQUEST_TIMEOUT, SITEMAP_BASE, SITEMAP_INDEX_SLUG, SchemaIO, SeedContext, Seo, SeoHead, SeoSchema, ServiceAdapters, Settings, SettingsService, ShaAlgorithm, SiteSettings, SiteSettingsInput, Sitemap, SitemapIndex, SitemapIndexEntry, SitemapList, SitemapRouteExtra, SitemapUrl, SitemapUrlImage, SitemapUrlList, SocialProfile, TaxonomySettings, TaxonomySettingsInput, TestAdminUser, TestCredentials, TestUser, ThrowableErrorFn, ThrowableErrorMap, UNEXPECTED_BODY_SNIPPET_LENGTH, UploadsSettings, UploadsSettingsInput, UserService, VerifyCaptchaFn, VerifyPreviewTokenFn, WC_CORE_BASE, WC_STORE_BASE, WEB_MANIFEST_ROUTE, WPK_RobotRule, WPK_Robots, WPK_Seo, WPK_Sitemap, WPK_SitemapContentType, WPK_SitemapIndex, WPK_SitemapUrl, WPK_SitemapUrlImage, WP_AUTH_HEADER_KEY, WP_AUTH_TYPE, WP_CATEGORY_ORDER_BYS, WP_CORE_BASE, WP_Category, WP_CategoryCreateErrorCode, WP_CategoryCreateInput, WP_CategoryDeleteErrorCode, WP_CategoryDeleteInput, WP_CategoryDeleteResponse, WP_CategoryLinks, WP_CategoryListErrorCode, WP_CategoryListInput, WP_CategoryOrderBy, WP_CategoryRetrieveErrorCode, WP_CategoryRetrieveInput, WP_CategoryTaxonomy, WP_CategoryUpdateErrorCode, WP_CategoryUpdateInput, WP_Comment, WP_CommentAvatarUrls, WP_CommentContent, WP_CommentContentInput, WP_CommentCreateErrorCode, WP_CommentCreateInput, WP_CommentDeleteErrorCode, WP_CommentDeleteInput, WP_CommentDeleteResponse, WP_CommentLinks, WP_CommentListErrorCode, WP_CommentListInput, WP_CommentOrderBy, WP_CommentRetrieveErrorCode, WP_CommentRetrieveInput, WP_CommentStatus, WP_CommentStatusFilter, WP_CommentStatusInput, WP_CommentType, WP_CommentUpdateErrorCode, WP_CommentUpdateInput, WP_CommonErrorCode, WP_Context, WP_CurrencyFormat, WP_Error, WP_ErrorData, WP_KIZLO_BASE, WP_Link, WP_List, WP_ListMetadata, WP_ListOrder, WP_MENU_ITEM_ORDER_BYES, WP_MENU_ITEM_TARGETS, WP_MENU_ITEM_TAX_RELATIONS, WP_Menu, WP_MenuCreateErrorCode, WP_MenuCreateInput, WP_MenuDeleteErrorCode, WP_MenuDeleteInput, WP_MenuDeleteResponse, WP_MenuItem, WP_MenuItemCreateErrorCode, WP_MenuItemCreateInput, WP_MenuItemDeleteErrorCode, WP_MenuItemDeleteInput, WP_MenuItemDeleteResponse, WP_MenuItemLinks, WP_MenuItemListErrorCode, WP_MenuItemListInput, WP_MenuItemOrderBy, WP_MenuItemRetrieveErrorCode, WP_MenuItemRetrieveInput, WP_MenuItemStatus, WP_MenuItemStatusFilter, WP_MenuItemTarget, WP_MenuItemTaxRelation, WP_MenuItemTitle, WP_MenuItemTitleInput, WP_MenuItemType, WP_MenuItemUpdateErrorCode, WP_MenuItemUpdateInput, WP_MenuLinks, WP_MenuListErrorCode, WP_MenuListInput, WP_MenuOrderBy, WP_MenuRetrieveErrorCode, WP_MenuRetrieveInput, WP_MenuUpdateErrorCode, WP_MenuUpdateInput, WP_PAGE_ORDER_BYES, WP_PAGE_STATUSES, WP_POST_FORMATS, WP_POST_ORDER_BYES, WP_POST_STATUSES, WP_POST_TAX_RELATIONS, WP_Page, WP_PageCommentStatus, WP_PageContent, WP_PageContentInput, WP_PageCreateErrorCode, WP_PageCreateInput, WP_PageDeleteErrorCode, WP_PageDeleteInput, WP_PageDeleteResponse, WP_PageExcerpt, WP_PageExcerptInput, WP_PageGuid, WP_PageLinks, WP_PageListErrorCode, WP_PageListInput, WP_PageOrderBy, WP_PageRetrieveErrorCode, WP_PageRetrieveInput, WP_PageStatus, WP_PageStatusFilter, WP_PageTitle, WP_PageTitleInput, WP_PageUpdateErrorCode, WP_PageUpdateInput, WP_Post, WP_PostCommentStatus, WP_PostContent, WP_PostContentInput, WP_PostCreateErrorCode, WP_PostCreateInput, WP_PostDeleteErrorCode, WP_PostDeleteInput, WP_PostDeleteResponse, WP_PostExcerpt, WP_PostExcerptInput, WP_PostFormat, WP_PostGuid, WP_PostLinks, WP_PostListErrorCode, WP_PostListInput, WP_PostOrderBy, WP_PostRetrieveErrorCode, WP_PostRetrieveInput, WP_PostStatus, WP_PostStatusFilter, WP_PostTaxRelation, WP_PostTitle, WP_PostTitleInput, WP_PostUpdateErrorCode, WP_PostUpdateInput, WP_RequestInput, WP_Result, WP_TAG_ORDER_BYS, WP_Tag, WP_TagCreateErrorCode, WP_TagCreateInput, WP_TagDeleteErrorCode, WP_TagDeleteInput, WP_TagDeleteResponse, WP_TagLinks, WP_TagListErrorCode, WP_TagListInput, WP_TagOrderBy, WP_TagRetrieveErrorCode, WP_TagRetrieveInput, WP_TagTaxonomy, WP_TagUpdateErrorCode, WP_TagUpdateInput, WP_User, WP_UserAvatarUrls, WP_UserCapabilities, WP_UserCreateErrorCode, WP_UserCreateInput, WP_UserDeleteErrorCode, WP_UserDeleteInput, WP_UserDeleteMeErrorCode, WP_UserDeleteMeInput, WP_UserDeleteMeResponse, WP_UserDeleteResponse, WP_UserLinks, WP_UserListErrorCode, WP_UserListInput, WP_UserOrderBy, WP_UserRetrieveErrorCode, WP_UserRetrieveInput, WP_UserRetrieveMeErrorCode, WP_UserRetrieveMeInput, WP_UserUpdateErrorCode, WP_UserUpdateInput, WP_UserUpdateMeErrorCode, WP_UserUpdateMeInput, WP_UserWho, WebManifest, WebhookOptions, WebhookSettings, WebhookSettingsInput, WithCommonErrorMap, WordPressCredentials, WordPressService, altcha, arkose, authMock, buildProcedure, buildWebManifest, combineDetailed, compare, consoleLog, createAuthAdapter, createCaptchaAdapter, createCookiesAdapter, createEventHandler, createExtension, createGeoAdapter, createKizlo, createKizloClient, createLoggerAdapter, createManifestRoute, createMiddleware, createOrpcRouter, createProcedure, createSitemapRedirectRoute, createSitemapRoute, createThrowableErrorMap, createWebhookRouter, defineErrorMap, defineFixture, deserializeCurrencyFormat, deserializeListMetadata, deserializeSeo, emptyList, friendlyCaptcha, generateContract, geoMock, geoVercelNext, handleUnexpectedError, hash, hcaptcha, hmac, isContractProcedure, isKizloError, isLocalPlugin, isWordPressResourceError, noopAdapter, parseIdentifier, parseSitemapSlug, recaptcha, renderSitemapIndex, renderUrlSet, resolveIcons, resolveKizloConfig, restoreContract, schemaType, serializeCurrencyFormat, sitemapEntryPath, toKizloError, turnstile, xmlResponse };
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { COMMON_ERRORS, KizloError, createThrowableErrorMap, defineErrorMap, isK
12
12
  import { buildProcedure, combineDetailed, createProcedure, handleUnexpectedError, schemaType } from "./shared/procedure.js";
13
13
  import { createOrpcRouter } from "./shared/router.js";
14
14
  import { generateContract, isContractProcedure, restoreContract } from "./shared/contract.js";
15
- import { CurrencyFormat, IdentifierInput, LIST_ORDER, ListMetadata, ListOrder, Media, PreviewTokenData, PreviewTokenPayload } from "./shared/schema.js";
15
+ import { CurrencyFormat, IdentifierInput, LIST_ORDER, ListMetadata, ListOrder, PreviewTokenData, PreviewTokenPayload } from "./shared/schema.js";
16
16
  import { KizloClient, createKizloClient } from "./client.js";
17
17
  import { CookiesStorage } from "./cookie.js";
18
18
  import { CART_HASH_HEADER_KEY, SAFE_REQUEST_TIMEOUT, UNEXPECTED_BODY_SNIPPET_LENGTH, WC_CORE_BASE, WC_STORE_BASE, WP_AUTH_HEADER_KEY, WP_AUTH_TYPE, WP_CORE_BASE, WP_KIZLO_BASE } from "./wordpress/constants.js";
@@ -44,4 +44,4 @@ import { SITEMAP_BASE, SITEMAP_INDEX_SLUG, createSitemapRedirectRoute, createSit
44
44
  import { createExtension } from "./shared/extension.js";
45
45
  import { createMiddleware } from "./shared/middleware.js";
46
46
 
47
- export { CART_HASH_HEADER_KEY, COMMON_ERRORS, CONTENT_TYPES, CategoryService, CommentService, ContentType, Context, CookiesStorage, CurrencyFormat, IdentifierInput, Kizlo, KizloClient, KizloError, LIST_ORDER, ListMetadata, ListOrder, ListSitemapUrlInput, ListSitemapUrlInputBase, Media, MenuItemService, MenuService, PageService, PostService, PreviewTokenData, PreviewTokenPayload, ROUTER_MAP, Robots, RobotsRule, SAFE_REQUEST_TIMEOUT, SITEMAP_BASE, SITEMAP_INDEX_SLUG, Seo, SeoHead, SeoSchema, SettingsService, Sitemap, SitemapIndex, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList, UNEXPECTED_BODY_SNIPPET_LENGTH, UserService, WC_CORE_BASE, WC_STORE_BASE, WEB_MANIFEST_ROUTE, WP_AUTH_HEADER_KEY, WP_AUTH_TYPE, WP_CATEGORY_ORDER_BYS, WP_CORE_BASE, WP_Error, WP_KIZLO_BASE, WP_MENU_ITEM_ORDER_BYES, WP_MENU_ITEM_TARGETS, WP_MENU_ITEM_TAX_RELATIONS, WP_PAGE_ORDER_BYES, WP_PAGE_STATUSES, WP_POST_FORMATS, WP_POST_ORDER_BYES, WP_POST_STATUSES, WP_POST_TAX_RELATIONS, WP_TAG_ORDER_BYS, WordPressService, altcha, arkose, authMock, buildProcedure, buildWebManifest, combineDetailed, compare, consoleLog, createAuthAdapter, createCaptchaAdapter, createCookiesAdapter, createEventHandler, createExtension, createGeoAdapter, createKizlo, createKizloClient, createLoggerAdapter, createManifestRoute, createMiddleware, createOrpcRouter, createProcedure, createSitemapRedirectRoute, createSitemapRoute, createThrowableErrorMap, createWebhookRouter, defineErrorMap, defineFixture, deserializeCurrencyFormat, deserializeListMetadata, deserializeSeo, emptyList, friendlyCaptcha, generateContract, geoMock, geoVercelNext, handleUnexpectedError, hash, hcaptcha, hmac, isContractProcedure, isKizloError, isLocalPlugin, isWordPressResourceError, noopAdapter, parseIdentifier, parseSitemapSlug, recaptcha, renderSitemapIndex, renderUrlSet, resolveIcons, resolveKizloConfig, restoreContract, schemaType, serializeCurrencyFormat, sitemapEntryPath, toKizloError, turnstile, xmlResponse };
47
+ export { CART_HASH_HEADER_KEY, COMMON_ERRORS, CONTENT_TYPES, CategoryService, CommentService, ContentType, Context, CookiesStorage, CurrencyFormat, IdentifierInput, Kizlo, KizloClient, KizloError, LIST_ORDER, ListMetadata, ListOrder, ListSitemapUrlInput, ListSitemapUrlInputBase, MenuItemService, MenuService, PageService, PostService, PreviewTokenData, PreviewTokenPayload, ROUTER_MAP, Robots, RobotsRule, SAFE_REQUEST_TIMEOUT, SITEMAP_BASE, SITEMAP_INDEX_SLUG, Seo, SeoHead, SeoSchema, SettingsService, Sitemap, SitemapIndex, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList, UNEXPECTED_BODY_SNIPPET_LENGTH, UserService, WC_CORE_BASE, WC_STORE_BASE, WEB_MANIFEST_ROUTE, WP_AUTH_HEADER_KEY, WP_AUTH_TYPE, WP_CATEGORY_ORDER_BYS, WP_CORE_BASE, WP_Error, WP_KIZLO_BASE, WP_MENU_ITEM_ORDER_BYES, WP_MENU_ITEM_TARGETS, WP_MENU_ITEM_TAX_RELATIONS, WP_PAGE_ORDER_BYES, WP_PAGE_STATUSES, WP_POST_FORMATS, WP_POST_ORDER_BYES, WP_POST_STATUSES, WP_POST_TAX_RELATIONS, WP_TAG_ORDER_BYS, WordPressService, altcha, arkose, authMock, buildProcedure, buildWebManifest, combineDetailed, compare, consoleLog, createAuthAdapter, createCaptchaAdapter, createCookiesAdapter, createEventHandler, createExtension, createGeoAdapter, createKizlo, createKizloClient, createLoggerAdapter, createManifestRoute, createMiddleware, createOrpcRouter, createProcedure, createSitemapRedirectRoute, createSitemapRoute, createThrowableErrorMap, createWebhookRouter, defineErrorMap, defineFixture, deserializeCurrencyFormat, deserializeListMetadata, deserializeSeo, emptyList, friendlyCaptcha, generateContract, geoMock, geoVercelNext, handleUnexpectedError, hash, hcaptcha, hmac, isContractProcedure, isKizloError, isLocalPlugin, isWordPressResourceError, noopAdapter, parseIdentifier, parseSitemapSlug, recaptcha, renderSitemapIndex, renderUrlSet, resolveIcons, resolveKizloConfig, restoreContract, schemaType, serializeCurrencyFormat, sitemapEntryPath, toKizloError, turnstile, xmlResponse };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.10.0";
2
+ var version = "0.11.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
@@ -1,7 +1,7 @@
1
- import { IdentifierInput, ListMetadata, ListOrder, Media } from "../shared/schema.js";
1
+ import { IdentifierInput, ListMetadata, ListOrder } from "../shared/schema.js";
2
2
  import { WP_PAGE_ORDER_BYES, WP_PAGE_STATUSES } from "../wordpress/page/types.js";
3
3
  import { Seo } from "../seo/schema.js";
4
- import { Metadata, NumberLike, arrayable, lenient } from "@kizlo/shared";
4
+ import { Media, Metadata, NumberLike, arrayable, lenient } from "@kizlo/shared";
5
5
  import z from "zod/v4";
6
6
 
7
7
  //#region src/page/schema.ts
@@ -1,7 +1,7 @@
1
- import { IdentifierInput, ListMetadata, ListOrder, Media } from "../shared/schema.js";
1
+ import { IdentifierInput, ListMetadata, ListOrder } from "../shared/schema.js";
2
2
  import { WP_POST_FORMATS, WP_POST_ORDER_BYES, WP_POST_STATUSES, WP_POST_TAX_RELATIONS } from "../wordpress/post/types.js";
3
3
  import { Seo } from "../seo/schema.js";
4
- import { BooleanLike, Metadata, NumberLike, arrayable, lenient } from "@kizlo/shared";
4
+ import { BooleanLike, Media, Metadata, NumberLike, arrayable, lenient } from "@kizlo/shared";
5
5
  import z from "zod/v4";
6
6
 
7
7
  //#region src/post/schema.ts
@@ -1,4 +1,3 @@
1
- import { Media } from "@kizlo/shared";
2
1
  import z from "zod/v4";
3
2
 
4
3
  //#region src/shared/schema.d.ts
@@ -46,4 +45,4 @@ declare const PreviewTokenData: z.ZodObject<{
46
45
  }, z.core.$strip>;
47
46
  type PreviewTokenData = z.infer<typeof PreviewTokenData>;
48
47
  //#endregion
49
- export { CurrencyFormat, IdentifierInput, LIST_ORDER, ListMetadata, ListOrder, Media, PreviewTokenData, PreviewTokenPayload };
48
+ export { CurrencyFormat, IdentifierInput, LIST_ORDER, ListMetadata, ListOrder, PreviewTokenData, PreviewTokenPayload };
@@ -1,4 +1,4 @@
1
- import { Media, NumberLike } from "@kizlo/shared";
1
+ import { NumberLike } from "@kizlo/shared";
2
2
  import z from "zod/v4";
3
3
 
4
4
  //#region src/shared/schema.ts
@@ -34,4 +34,4 @@ const PreviewTokenData = z.object({
34
34
  });
35
35
 
36
36
  //#endregion
37
- export { CurrencyFormat, IdentifierInput, LIST_ORDER, ListMetadata, ListOrder, Media, PreviewTokenData, PreviewTokenPayload };
37
+ export { CurrencyFormat, IdentifierInput, LIST_ORDER, ListMetadata, ListOrder, PreviewTokenData, PreviewTokenPayload };
@@ -20,9 +20,13 @@ declare class WordPressService {
20
20
  private readonly siteBase;
21
21
  private readonly authHeader;
22
22
  private readonly defaultTimeout;
23
+ /** Called with the response headers of every non-transport-error response, so callers can observe
24
+ * per-response signals (e.g. the plugin version header) without threading them through every method. */
25
+ private readonly onResponse?;
23
26
  constructor(context: {
24
27
  credentials: WordPressCredentials;
25
28
  timeout?: Duration;
29
+ onResponse?: (headers: Headers) => void;
26
30
  });
27
31
  post<TData, TCode extends string = never>(path: string, input: {
28
32
  body?: unknown;
@@ -22,6 +22,9 @@ var WordPressService = class {
22
22
  siteBase;
23
23
  authHeader;
24
24
  defaultTimeout;
25
+ /** Called with the response headers of every non-transport-error response, so callers can observe
26
+ * per-response signals (e.g. the plugin version header) without threading them through every method. */
27
+ onResponse;
25
28
  constructor(context) {
26
29
  const credentialBytes = new TextEncoder().encode(`${context.credentials.username}:${context.credentials.password}`);
27
30
  let binary = "";
@@ -29,6 +32,7 @@ var WordPressService = class {
29
32
  this.authHeader = `${WP_AUTH_TYPE} ${btoa(binary)}`;
30
33
  this.siteBase = context.credentials.url.endsWith("/") ? context.credentials.url : `${context.credentials.url}/`;
31
34
  this.defaultTimeout = context.timeout ?? SAFE_REQUEST_TIMEOUT;
35
+ this.onResponse = context.onResponse;
32
36
  }
33
37
  async post(path, input) {
34
38
  return this.request({
@@ -103,6 +107,7 @@ var WordPressService = class {
103
107
  message: transportErr.message
104
108
  })
105
109
  };
110
+ this.onResponse?.(response.headers);
106
111
  const [readErr, text] = await tryCatch(response.text());
107
112
  if (readErr) return {
108
113
  data: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kizlo",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "A headless WordPress framework for TypeScript.",
6
6
  "keywords": [
@@ -76,7 +76,7 @@
76
76
  },
77
77
  "dependencies": {
78
78
  "@clack/prompts": "^1.5.1",
79
- "@kizlo/shared": "0.5.1",
79
+ "@kizlo/shared": "0.6.0",
80
80
  "@orpc/client": "^1.14.3",
81
81
  "@orpc/contract": "^1.14.3",
82
82
  "@orpc/openapi": "^1.14.3",