kizlo 0.9.1 → 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.
Files changed (49) hide show
  1. package/README.md +4 -4
  2. package/dist/cli/commands/_setup.js +132 -79
  3. package/dist/cli/commands/_wiring.js +5 -4
  4. package/dist/cli/commands/create.js +35 -28
  5. package/dist/cli/commands/dev.js +172 -87
  6. package/dist/cli/commands/init.js +97 -103
  7. package/dist/cli/commands/test.js +34 -34
  8. package/dist/cli/daemon/config.js +18 -24
  9. package/dist/cli/daemon/logger.js +1 -1
  10. package/dist/cli/daemon/reload.js +66 -0
  11. package/dist/cli/daemon/watch.js +0 -1
  12. package/dist/cli/presets/base.js +4 -2
  13. package/dist/cli/presets/nextjs.js +0 -1
  14. package/dist/cli/presets/template.js +104 -18
  15. package/dist/cli/utils.js +41 -19
  16. package/dist/cli/wp/compose/override.js +3 -3
  17. package/dist/cli/wp/compose/override.test.ts +2 -2
  18. package/dist/cli/wp/compose/override.ts +3 -3
  19. package/dist/cli/wp/constants.d.ts +10 -4
  20. package/dist/cli/wp/constants.js +10 -4
  21. package/dist/cli/wp/dev.js +13 -58
  22. package/dist/cli/wp/docker.d.ts +16 -4
  23. package/dist/cli/wp/docker.js +20 -7
  24. package/dist/cli/wp/settings.js +9 -3
  25. package/dist/cli/wp/setup.js +1 -1
  26. package/dist/cli/wp/stack.js +4 -4
  27. package/dist/comment/schema.js +2 -2
  28. package/dist/config.d.ts +20 -23
  29. package/dist/context.d.ts +9 -0
  30. package/dist/context.js +20 -2
  31. package/dist/index.d.ts +3 -3
  32. package/dist/index.js +2 -2
  33. package/dist/integrations/nextjs/kizlo.server.js +1 -1
  34. package/dist/integrations/nextjs/utils.js +2 -2
  35. package/dist/kizlo.d.ts +16 -16
  36. package/dist/kizlo.js +19 -15
  37. package/dist/package.js +1 -1
  38. package/dist/page/schema.js +2 -2
  39. package/dist/post/schema.js +2 -2
  40. package/dist/router.d.ts +9 -9
  41. package/dist/shared/schema.d.ts +1 -2
  42. package/dist/shared/schema.js +2 -2
  43. package/dist/test/server.js +1 -1
  44. package/dist/test.d.ts +3 -3
  45. package/dist/test.js +3 -3
  46. package/dist/wordpress/service.d.ts +4 -0
  47. package/dist/wordpress/service.js +5 -0
  48. package/package.json +2 -2
  49. package/dist/cli/wp/byo.js +0 -113
@@ -1,11 +1,14 @@
1
- import { createStack } from "../wp/docker.js";
2
- import { ensureGitignored, envGroups, groupDefault, mergeEnv, pickStackPort, withSpinner } from "../utils.js";
3
- import { hasDevStack, resolveDevConfig } from "../daemon/config.js";
1
+ import { createStack, dockerHint, dockerStatus } from "../wp/docker.js";
2
+ import { findConfigDir } from "../wp/utils.js";
3
+ import { log } from "../daemon/logger.js";
4
+ import { DEFAULT_ENV_KEYS, ensureGitignored, envGroups, groupDefault, mergeEnv, pickStackPort, withSpinner } from "../utils.js";
5
+ import { resolveDevConfig, usesLocalWordPress } from "../daemon/config.js";
4
6
  import { bootstrapDev } from "../wp/dev.js";
5
7
  import { reapOrphans, registerSession, removeProjectContainers, spawnWatchdog, unregisterSession } from "../wp/session.js";
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";
@@ -21,15 +24,13 @@ async function resolve$1(cwd) {
21
24
  };
22
25
  }
23
26
  /**
24
- * The stack lifecycle subcommands (`stop`/`down`/`reset`) only act on a local WordPress
25
- * Docker stack. A bring-your-own-WordPress project (no `dev.path`) has no stack to manage,
26
- * so — rather than fall through to `resolveDevConfig`'s hard "`dev.path` is required" error —
27
- * print a plain note explaining there's nothing to do and exit cleanly. Returns true when a
28
- * local stack is configured and the caller may proceed.
27
+ * The lifecycle subcommands (`stop`/`reset`) only act on local WordPress. A project pointing at its
28
+ * own WordPress (no `dev.local`) has nothing to manage, so print a plain note and exit cleanly. Returns
29
+ * true when local WordPress is configured and the caller may proceed.
29
30
  */
30
- async function requireDevStack(cwd) {
31
- if (await hasDevStack(cwd)) return true;
32
- note("No local WordPress stack is configured (`dev.path` is unset) — nothing to manage here.");
31
+ async function requireLocalWordPress(cwd) {
32
+ if (await usesLocalWordPress(cwd)) return true;
33
+ note("This project doesn't run local WordPress — set `dev.local: true` in kizlo.config.ts to add it.");
33
34
  return false;
34
35
  }
35
36
  /** A dim, untagged one-liner (no consola timestamp) for the clean dev output. */
@@ -39,7 +40,7 @@ function note(text) {
39
40
  }
40
41
  /**
41
42
  * Print the connection summary as an aligned, Next.js-style block. Shows the wp-admin URL on
42
- * the loopback ("Local") and — when the stack is installed on a LAN address rather than
43
+ * the loopback ("Local") and — when local WordPress is installed on a LAN address rather than
43
44
  * localhost — on the network, so it can be opened from other devices. On a fresh install, also
44
45
  * show the one-time wp-admin password (it's never stored, so this is the only chance to see it).
45
46
  */
@@ -52,57 +53,56 @@ function printSummary(info) {
52
53
  const width = Math.max(...rows.map(([label]) => label.length)) + 1;
53
54
  const lines = ["", ...rows.map(([label, value]) => ` ${dim}- ${`${label}:`.padEnd(width)}${reset} ${cyan}${value}${reset}`)];
54
55
  if (info.secrets) lines.push("", ` ${dim}admin login:${reset} ${info.username} / ${info.secrets.password}`, ` ${dim}save the password now — it's shown only once${reset}`);
55
- else if (info.imported) lines.push("", ` ${dim}log in with your imported site's own credentials${reset}`);
56
56
  if (info.seeded > 0) lines.push(` ${dim}seeded ${info.seeded} fixture${info.seeded === 1 ? "" : "s"}${reset}`);
57
57
  process.stdout.write(`${lines.join("\n")}\n`);
58
58
  }
59
59
  /**
60
- * Dev connection keys a fresh dev install must rewrite (the rest of `.env` is untouched). The
61
- * production `KIZLO_WORDPRESS_*` / `KIZLO_SITE_SECRET` keys live under their own names and are never
62
- * touched by `kizlo dev`, so pointing them at a real site survives booting the local stack.
60
+ * Local-WordPress connection keys a fresh install must rewrite (the rest of `.env` is untouched). The
61
+ * remote `KIZLO_WP_*` / `KIZLO_WP_SECRET` keys live under their own names and are never
62
+ * touched by `kizlo dev`, so pointing them at a real site survives booting local WordPress.
63
63
  */
64
64
  const WP_ENV_KEYS = [
65
- "KIZLO_DEV_WORDPRESS_URL",
66
- "KIZLO_DEV_WORDPRESS_USERNAME",
67
- "KIZLO_DEV_WORDPRESS_APPLICATION_PASSWORD"
65
+ "KIZLO_LOCAL_WP_URL",
66
+ "KIZLO_LOCAL_WP_USERNAME",
67
+ "KIZLO_LOCAL_WP_APP_PASSWORD"
68
68
  ];
69
69
  /** Read a key's value from raw `.env` content (first match, trimmed); undefined when absent or empty. */
70
70
  function readEnvValue(content, key) {
71
71
  return content.match(new RegExp(`^\\s*${key}\\s*=\\s*(.*)$`, "m"))?.[1]?.trim() || void 0;
72
72
  }
73
73
  /**
74
- * The Kizlo backend URL from `.env` — the one key ending in `KIZLO_BACKEND_URL` (`KIZLO_BACKEND_URL`,
75
- * or a framework-prefixed `NEXT_PUBLIC_KIZLO_BACKEND_URL`). It's the handler mount the plugin delivers
74
+ * The Kizlo backend URL from `.env` — the one key ending in `KIZLO_API_URL` (`KIZLO_API_URL`,
75
+ * or a framework-prefixed `NEXT_PUBLIC_KIZLO_API_URL`). It's the handler mount the plugin delivers
76
76
  * webhook events to; its origin is the site's canonical URL. User-managed (init writes it), so dev
77
77
  * only reads it. Undefined before `kizlo init` has populated `.env`.
78
78
  */
79
79
  function readBaseUrl(content) {
80
- return content.match(/^\s*[A-Z_]*KIZLO_BACKEND_URL\s*=\s*(.*)$/m)?.[1]?.trim() || void 0;
80
+ return content.match(/^\s*[A-Z_]*KIZLO_API_URL\s*=\s*(.*)$/m)?.[1]?.trim() || void 0;
81
81
  }
82
82
  /**
83
- * Rewrite the dev WordPress connection in `.env` after a fresh install. A reset (or first boot)
83
+ * Rewrite the local WordPress connection in `.env` after a fresh install. A reset (or first boot)
84
84
  * wipes the database, so the previous application password — and possibly the port — no longer
85
85
  * match; without this the app keeps the stale credentials and REST auth fails. Only the three
86
86
  * connection keys are overwritten; everything else in `.env` is preserved, and the file is created
87
87
  * if absent. Gated by the caller on a minted `appPassword`, so a warm resume never churns it.
88
88
  *
89
- * `KIZLO_TARGET=dev` and `KIZLO_DEV_SITE_SECRET` are written too but kept out of the overwrite set, so a
90
- * value the user already set is preserved — booting the dev stack only fills them when absent. The
91
- * resolved dev secret (the existing one, or a freshly minted one) and the Kizlo server base URL are
92
- * returned so the caller can sync them into the plugin.
89
+ * `KIZLO_CONNECT=local` and `KIZLO_LOCAL_WP_SECRET` are written too but kept out of the overwrite set,
90
+ * so a value the user already set is preserved — booting local WordPress only fills them when absent.
91
+ * The resolved local secret (the existing one, or a freshly minted one) and the Kizlo server base URL
92
+ * are returned so the caller can sync them into the plugin.
93
93
  */
94
94
  function updateWpEnv(cfg, info) {
95
95
  if (!info.appPassword) return void 0;
96
96
  const envPath = join(cfg.configDir, ".env");
97
97
  const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
98
- const siteSecret = readEnvValue(existing, "KIZLO_DEV_SITE_SECRET") ?? randomBytes(32).toString("hex");
98
+ const siteSecret = readEnvValue(existing, "KIZLO_LOCAL_WP_SECRET") ?? randomBytes(32).toString("hex");
99
99
  const { content } = mergeEnv(existing, {
100
- KIZLO_DEV_WORDPRESS_URL: info.url,
101
- KIZLO_DEV_WORDPRESS_USERNAME: info.username,
102
- KIZLO_DEV_WORDPRESS_APPLICATION_PASSWORD: info.appPassword,
103
- KIZLO_DEV_SITE_SECRET: siteSecret,
104
- KIZLO_TARGET: "dev"
105
- }, new Set(WP_ENV_KEYS), envGroups("KIZLO_BACKEND_URL"));
100
+ KIZLO_LOCAL_WP_URL: info.url,
101
+ KIZLO_LOCAL_WP_USERNAME: info.username,
102
+ KIZLO_LOCAL_WP_APP_PASSWORD: info.appPassword,
103
+ KIZLO_LOCAL_WP_SECRET: siteSecret,
104
+ KIZLO_CONNECT: "local"
105
+ }, new Set(WP_ENV_KEYS), envGroups(DEFAULT_ENV_KEYS));
106
106
  writeFileSync(envPath, content);
107
107
  return {
108
108
  siteSecret,
@@ -110,19 +110,19 @@ function updateWpEnv(cfg, info) {
110
110
  };
111
111
  }
112
112
  /**
113
- * Re-point `KIZLO_DEV_WORDPRESS_URL` at the current dev URL on a warm resume, so a LAN address that
113
+ * Re-point `KIZLO_LOCAL_WP_URL` at the current URL on a warm resume, so a LAN address that
114
114
  * changed since the last session (a new DHCP lease, a different network) doesn't leave the app talking
115
115
  * to a dead IP. Only this one key is rewritten — the credentials and secret from the fresh install are
116
116
  * preserved — and it's a no-op when `.env` is absent (nothing provisioned yet) or already current.
117
117
  * The full fresh-install write goes through {@link updateWpEnv} instead; this covers the resume path it
118
118
  * skips. Returns true when it changed the file. The app still needs a restart to read the new value.
119
119
  */
120
- function syncDevUrl(cfg, url) {
120
+ function syncLocalUrl(cfg, url) {
121
121
  const envPath = join(cfg.configDir, ".env");
122
122
  if (!existsSync(envPath)) return false;
123
123
  const existing = readFileSync(envPath, "utf8");
124
- if (readEnvValue(existing, "KIZLO_DEV_WORDPRESS_URL") === url) return false;
125
- const { content } = mergeEnv(existing, { KIZLO_DEV_WORDPRESS_URL: url }, new Set(["KIZLO_DEV_WORDPRESS_URL"]), envGroups("KIZLO_BACKEND_URL"));
124
+ if (readEnvValue(existing, "KIZLO_LOCAL_WP_URL") === url) return false;
125
+ const { content } = mergeEnv(existing, { KIZLO_LOCAL_WP_URL: url }, new Set(["KIZLO_LOCAL_WP_URL"]), envGroups(DEFAULT_ENV_KEYS));
126
126
  writeFileSync(envPath, content);
127
127
  return true;
128
128
  }
@@ -134,7 +134,7 @@ function syncDevUrl(cfg, url) {
134
134
  * `next dev` child server — nothing stops them automatically when we die. A dying process
135
135
  * also can't be trusted to finish async work (a closed terminal breaks its stdout
136
136
  * mid-write), so this process does no docker work on the way out. It only needs to
137
- * *exit*; the watchdog, watching our PID from its own session, stops the stack the moment
137
+ * *exit*; the watchdog, watching our PID from its own session, stops it the moment
138
138
  * we're gone — by Ctrl+C, SIGTERM, a closed tab, SIGKILL, or a crash alike.
139
139
  *
140
140
  * We trigger that exit two ways, because neither alone is enough: signal handlers (for
@@ -150,7 +150,7 @@ function armForegroundTeardown(cfg) {
150
150
  exiting = true;
151
151
  unregisterSession(cfg.project);
152
152
  try {
153
- note(`Stopping dev stack (${reason})…`);
153
+ note(`Stopping local WordPress (${reason})…`);
154
154
  } catch {}
155
155
  process.exit(0);
156
156
  };
@@ -169,13 +169,13 @@ function armForegroundTeardown(cfg) {
169
169
  }
170
170
  }
171
171
  /**
172
- * Boot the stack and run in the foreground until exit — the path behind bare
173
- * `kizlo dev`. Arms teardown first (so a mid-startup cancel still
174
- * stops partial containers), shows a timed spinner, prints the summary + stop hint, then
175
- * parks. A referenced timer holds the event loop open — an unresolved promise alone would
176
- * 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.
177
177
  */
178
- async function startForeground(cfg) {
178
+ async function prepareStack(cfg) {
179
179
  await removeProjectContainers(cfg.project);
180
180
  const port = await pickStackPort(cfg.port, {
181
181
  fixed: cfg.portExplicit,
@@ -194,11 +194,17 @@ async function startForeground(cfg) {
194
194
  createStack(devStack(ready));
195
195
  if (port !== cfg.port) note(`Port ${cfg.port} is in use — serving on ${port} instead.`);
196
196
  if (dbPort !== cfg.dbPort) note(`Database port ${cfg.dbPort} is in use — using ${dbPort} instead.`);
197
- armForegroundTeardown(ready);
198
- const start = Date.now();
199
- const creds = await withSpinner("Starting WordPress dev stack", () => 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) {
200
206
  if (creds.appPassword) {
201
- const env = updateWpEnv(ready, creds);
207
+ const env = updateWpEnv(cfg, creds);
202
208
  note("Updated .env with the new WordPress credentials");
203
209
  if (env) {
204
210
  const sync = await syncSiteSettings({
@@ -212,76 +218,155 @@ async function startForeground(cfg) {
212
218
  });
213
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.`);
214
220
  }
215
- } else if (syncDevUrl(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");
216
222
  printSummary(creds);
217
223
  const { green, dim, bold, reset } = palette();
218
224
  const ms = Date.now() - start;
219
225
  const took = ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
220
- process.stdout.write(`\n ${dim}Press ${reset}${bold}Ctrl+C${reset}${dim} (or close the terminal) to stop the development stack${reset}\n`);
221
- process.stdout.write(`\n ${green}✓${reset} Ready in ${took}\n\n`);
222
- const stopWatcher = await startWatcher(ready.configDir);
223
- 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
+ });
224
294
  setInterval(() => {}, 1 << 30);
225
295
  await new Promise(() => {});
226
296
  }
227
297
  /**
228
- * Run the contract watcher in the foreground until exit — the path bare `kizlo dev` takes when no
229
- * local dev stack is configured (a bring-your-own-WordPress project). Generates once, then watches
230
- * the server files and regenerates on save. Returns immediately when another watcher already holds
231
- * the lock; otherwise the persistent watcher keeps the process alive on its own.
298
+ * Run the contract watcher in the foreground until exit — the path bare `kizlo dev` takes when local
299
+ * WordPress isn't configured (a project pointing at its own WordPress). Generates once, then watches
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.
232
304
  */
233
305
  async function watchOnly(cwd) {
234
- const stop = await startWatcher(cwd);
306
+ let stop = await startWatcher(cwd);
235
307
  if (!stop) return;
236
- 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);
237
327
  for (const signal of [
238
328
  "SIGINT",
239
329
  "SIGTERM",
240
330
  "SIGHUP"
241
331
  ]) process.on(signal, () => {
242
- stop();
332
+ shutdown();
243
333
  process.exit(0);
244
334
  });
245
335
  }
246
336
  /**
247
- * Start (or resume) the dev stack. Backs bare `kizlo dev`. Always runs in the foreground
248
- * and stops the stack on exit, so it never outlives the session — the subcommands
249
- * (`stop`/`down`/`reset`) manage everything else. Without a local dev stack configured
250
- * (`dev.path` unset), there's nothing to boot, so it runs the contract watcher alone.
337
+ * Start (or resume) local WordPress. Backs bare `kizlo dev`. Always runs in the foreground and stops
338
+ * it on exit, so it never outlives the session — the subcommands (`stop`/`reset`) manage everything
339
+ * else. Without local WordPress configured there's nothing to boot, so it runs the contract watcher
340
+ * alone.
251
341
  */
252
342
  async function bringUp() {
253
343
  const cwd = process.cwd();
254
- if (!await hasDevStack(cwd)) {
344
+ if (!await usesLocalWordPress(cwd)) {
345
+ note("This project doesn't run local WordPress — running the contract watcher only. Set `dev.local` in kizlo.config.ts to add it.");
255
346
  await watchOnly(cwd);
256
347
  return;
257
348
  }
349
+ const status = await dockerStatus();
350
+ if (status !== "running") {
351
+ note(dockerHint(status));
352
+ process.exit(1);
353
+ }
258
354
  const reaped = await reapOrphans();
259
- if (reaped.length) note(`Stopped ${reaped.length} orphaned dev stack${reaped.length === 1 ? "" : "s"}: ${reaped.join(", ")}`);
355
+ if (reaped.length) note(`Stopped ${reaped.length} orphaned local WordPress${reaped.length === 1 ? "" : "s"}: ${reaped.join(", ")}`);
260
356
  const cfg = await resolveDevConfig(cwd);
261
- if (!cfg.wordpressPath.startsWith("..")) ensureGitignored(cfg.configDir, cfg.wordpressPath);
357
+ ensureGitignored(cfg.configDir, ".kizlo/");
262
358
  await startForeground(cfg);
263
359
  }
264
360
  const subCommands = {
265
361
  stop: defineCommand({
266
362
  meta: {
267
363
  name: "stop",
268
- description: "Stop the dev stack, keeping the database volume"
269
- },
270
- async run() {
271
- if (!await requireDevStack(process.cwd())) return;
272
- const { stack } = await resolve$1(process.cwd());
273
- await withSpinner("Stopping WordPress dev stack", () => stack.composeStop(), "Stack stopped (volumes kept)");
274
- }
275
- }),
276
- down: defineCommand({
277
- meta: {
278
- name: "down",
279
- description: "Stop and remove the dev stack containers (keeps the database and files)"
364
+ description: "Stop local WordPress, keeping the database volume"
280
365
  },
281
366
  async run() {
282
- if (!await requireDevStack(process.cwd())) return;
367
+ if (!await requireLocalWordPress(process.cwd())) return;
283
368
  const { stack } = await resolve$1(process.cwd());
284
- await withSpinner("Removing WordPress dev stack", () => stack.composeDown(), "Stack removed (database and files kept)");
369
+ await withSpinner("Stopping local WordPress", () => stack.composeStop(), "Local WordPress stopped (volumes kept)");
285
370
  }
286
371
  }),
287
372
  reset: defineCommand({
@@ -290,21 +375,21 @@ const subCommands = {
290
375
  description: "Wipe the database and the install folder so the next `kizlo dev` rebuilds fresh"
291
376
  },
292
377
  async run() {
293
- if (!await requireDevStack(process.cwd())) return;
378
+ if (!await requireLocalWordPress(process.cwd())) return;
294
379
  const { cfg, stack } = await resolve$1(process.cwd());
295
- await withSpinner("Wiping WordPress dev stack", () => stack.composeDown({ volumes: true }), "Local WordPress reset successfully");
380
+ await withSpinner("Wiping local WordPress", () => stack.composeDown({ volumes: true }), "Local WordPress reset successfully");
296
381
  rmSync(cfg.wordpressDir, {
297
382
  recursive: true,
298
383
  force: true
299
384
  });
300
- note("Run `kizlo dev` to rebuild a fresh stack.");
385
+ note("Run `kizlo dev` to rebuild fresh.");
301
386
  }
302
387
  })
303
388
  };
304
389
  const dev = defineCommand({
305
390
  meta: {
306
391
  name: "dev",
307
- description: "Run the dev stack and contract watcher (stop | down | reset)"
392
+ description: "Run local WordPress and the contract watcher (stop | reset)"
308
393
  },
309
394
  subCommands,
310
395
  run: groupDefault(Object.keys(subCommands), bringUp)