kizlo 0.9.0 → 0.10.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.
@@ -1,9 +1,8 @@
1
- import { dockerAvailable } from "../wp/docker.js";
2
- import { availablePackageManagers, detectInvokingPackageManager, ensureGitignored, frameworkCreateArgs, getVersion, installArgs, runCommandAsync, runCommandCaptured } from "../utils.js";
1
+ import { availablePackageManagers, detectInvokingPackageManager, ensureGitignored, frameworkCreateArgs, installArgs, runCommandAsync, runCommandCaptured } from "../utils.js";
3
2
  import { getPreset } from "../presets/index.js";
4
3
  import { fetchTemplate } from "../presets/source.js";
5
- import { adaptFile, changesFor, fileEntries, isExample, patchEntries, readManifest } from "../presets/template.js";
6
- import { collectConnectionInteractively, nextStepsLines, orCancel, pickAppPort, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
4
+ import { adaptFile, changesFor, fileEntries, isExample, minCliError, patchEntries, readManifest, resolveDependencies } from "../presets/template.js";
5
+ import { collectConnectionInteractively, nextStepsLines, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
7
6
  import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
8
7
  import z from "zod/v4";
9
8
  import path from "node:path";
@@ -29,16 +28,23 @@ const PACKAGE_MANAGERS = [
29
28
  "bun"
30
29
  ];
31
30
  /**
32
- * Pin the `kizlo` dependency in the freshly bootstrapped `package.json`. The template is
33
- * authoritative for the version (`kizloVersion` in its manifest, stamped at release), falling back to
34
- * the running CLI's version in-repo before the first stamp. The dependency is only recorded, not
35
- * installed — the getting-started note tells the user to run install.
31
+ * Pin Kizlo's dependencies in the freshly bootstrapped `package.json`. The template is authoritative
32
+ * for the versions (its manifest's `dependencies`/`devDependencies`, stamped at release), falling back
33
+ * to the running CLI's version for `kizlo` in-repo before the first stamp. The deps are only recorded,
34
+ * not installed — the getting-started note tells the user to run install.
36
35
  */
37
- function recordKizloDependency(dir, manifest) {
36
+ function recordDependencies(dir, manifest) {
38
37
  const pkgPath = path.join(dir, "package.json");
39
38
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
40
- pkg.dependencies ??= {};
41
- pkg.dependencies.kizlo = manifest.kizloVersion ?? `^${getVersion()}`;
39
+ const { dependencies, devDependencies } = resolveDependencies(manifest);
40
+ pkg.dependencies = {
41
+ ...pkg.dependencies,
42
+ ...dependencies
43
+ };
44
+ if (Object.keys(devDependencies).length) pkg.devDependencies = {
45
+ ...pkg.devDependencies,
46
+ ...devDependencies
47
+ };
42
48
  fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
43
49
  }
44
50
  /**
@@ -59,7 +65,7 @@ function bootstrapArgs(manifest, pm, name) {
59
65
  * tsconfig, the root layout); this drives the manifest — the same engine `init` uses — to record the
60
66
  * `kizlo` dependency, write `kizlo.config.ts`, scaffold the manifest's files (the `base` plumbing and
61
67
  * the `create` layout/styles always, the `example`-flagged demo pages only when `includeExamples`),
62
- * seed the generated contract, and ignore `.env`. On a fresh app Kizlo owns the layout, so `create`
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`
63
69
  * writes it whole (already SEO-wired) rather than patching — `applyLayoutPatches` runs over `create`'s
64
70
  * patches, which are none today. The target directory layout comes straight from the manifest's
65
71
  * conventions, which the bootstrap flags are chosen to match, so files land where they expect. Fresh
@@ -73,12 +79,12 @@ async function applyManifestWiring(dir, templateDir, manifest, opts) {
73
79
  alias,
74
80
  clientUrl: void 0
75
81
  });
76
- recordKizloDependency(dir, manifest);
82
+ recordDependencies(dir, manifest);
77
83
  const changes = changesFor(manifest, "create").filter((change) => opts.includeExamples || !isExample(change));
78
84
  const files = [{
79
85
  label: "Kizlo config",
80
86
  relPath: "kizlo.config.ts",
81
- contents: kizloConfigTemplate(kizloDir, alias, opts.devPath)
87
+ contents: kizloConfigTemplate(kizloDir, alias, opts.localDev)
82
88
  }, ...fileEntries(changes).map((entry) => adaptFile(templateDir, entry, manifest.conventions, scaffold))];
83
89
  for (const file of files) reportScaffold(file, await scaffoldFile(dir, file, {
84
90
  force: true,
@@ -86,6 +92,7 @@ async function applyManifestWiring(dir, templateDir, manifest, opts) {
86
92
  }), false);
87
93
  writeGeneratedContract(dir, path.join(kizloDir, "server"));
88
94
  ensureGitignored(dir, ".env");
95
+ ensureGitignored(dir, ".kizlo/");
89
96
  applyLayoutPatches(dir, patchEntries(changes), manifest.conventions, scaffold);
90
97
  }
91
98
  const create = defineCommand({
@@ -156,16 +163,12 @@ const create = defineCommand({
156
163
  })),
157
164
  initialValue: defaultPm
158
165
  }));
166
+ const conn = await collectConnectionInteractively(preset, { baseUrl: `http://localhost:${await pickAppPort()}` });
167
+ if (preset.apiPath && conn.baseUrl) conn.baseUrl = withApiPath(conn.baseUrl, preset.apiPath);
159
168
  const includeExamples = orCancel(await p.confirm({
160
- message: "Add example pages?",
169
+ message: "Add examples?",
161
170
  initialValue: true
162
171
  }));
163
- const conn = await collectConnectionInteractively(preset, { baseUrl: `http://localhost:${await pickAppPort()}` });
164
- if (preset.apiPath && conn.baseUrl) conn.baseUrl = withApiPath(conn.baseUrl, preset.apiPath);
165
- if (conn.mode === "local" && !await dockerAvailable()) {
166
- p.cancel("Docker isn't available — start Docker (or install it) and re-run, or choose “Use my own WordPress”.");
167
- process.exit(1);
168
- }
169
172
  const fetched = await fetchTemplate(template);
170
173
  const fail = (message) => {
171
174
  fetched.cleanup();
@@ -173,6 +176,8 @@ const create = defineCommand({
173
176
  process.exit(1);
174
177
  };
175
178
  const manifest = readManifest(fetched.dir);
179
+ const minErr = minCliError(manifest);
180
+ if (minErr) fail(minErr);
176
181
  const bootstrap = bootstrapArgs(manifest, pm, name);
177
182
  if (!bootstrap) {
178
183
  fetched.cleanup();
@@ -187,10 +192,11 @@ const create = defineCommand({
187
192
  if (scaffold.output) p.log.error(scaffold.output);
188
193
  fail(`${preset.label} setup failed — see the output above and try again.`);
189
194
  }
195
+ const exampleTemplate = readEnvExample(fetched.dir);
190
196
  try {
191
197
  await applyManifestWiring(dir, fetched.dir, manifest, {
192
- devPath: conn.devPath,
193
- includeExamples
198
+ includeExamples,
199
+ localDev: conn.mode === "local"
194
200
  });
195
201
  } catch (error) {
196
202
  fail(error instanceof Error ? error.message : String(error));
@@ -208,9 +214,10 @@ const create = defineCommand({
208
214
  if (!depsInstalled) p.log.warn(`Install them yourself: cd ${name} && ${installArgs(pm).join(" ")}`);
209
215
  }
210
216
  await setupLocalWordPress(dir, conn);
211
- await writeEnv(dir, preset, conn, {
217
+ await writeEnv(dir, resolveEnvKeys(preset, manifest), conn, {
212
218
  force: true,
213
- yes: false
219
+ yes: false,
220
+ exampleTemplate
214
221
  });
215
222
  await syncRemote(conn);
216
223
  p.note([
@@ -1,6 +1,6 @@
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 { DEFAULT_ENV_KEYS, ensureGitignored, envGroups, groupDefault, mergeEnv, pickStackPort, withSpinner } from "../utils.js";
3
+ import { resolveDevConfig, usesLocalWordPress } from "../daemon/config.js";
4
4
  import { bootstrapDev } from "../wp/dev.js";
5
5
  import { reapOrphans, registerSession, removeProjectContainers, spawnWatchdog, unregisterSession } from "../wp/session.js";
6
6
  import { syncSiteSettings } from "../wp/settings.js";
@@ -21,15 +21,13 @@ async function resolve$1(cwd) {
21
21
  };
22
22
  }
23
23
  /**
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.
24
+ * The lifecycle subcommands (`stop`/`reset`) only act on local WordPress. A project pointing at its
25
+ * own WordPress (no `dev.local`) has nothing to manage, so print a plain note and exit cleanly. Returns
26
+ * true when local WordPress is configured and the caller may proceed.
29
27
  */
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.");
28
+ async function requireLocalWordPress(cwd) {
29
+ if (await usesLocalWordPress(cwd)) return true;
30
+ note("This project doesn't run local WordPress — set `dev.local: true` in kizlo.config.ts to add it.");
33
31
  return false;
34
32
  }
35
33
  /** A dim, untagged one-liner (no consola timestamp) for the clean dev output. */
@@ -39,7 +37,7 @@ function note(text) {
39
37
  }
40
38
  /**
41
39
  * 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
40
+ * the loopback ("Local") and — when local WordPress is installed on a LAN address rather than
43
41
  * localhost — on the network, so it can be opened from other devices. On a fresh install, also
44
42
  * show the one-time wp-admin password (it's never stored, so this is the only chance to see it).
45
43
  */
@@ -52,57 +50,56 @@ function printSummary(info) {
52
50
  const width = Math.max(...rows.map(([label]) => label.length)) + 1;
53
51
  const lines = ["", ...rows.map(([label, value]) => ` ${dim}- ${`${label}:`.padEnd(width)}${reset} ${cyan}${value}${reset}`)];
54
52
  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
53
  if (info.seeded > 0) lines.push(` ${dim}seeded ${info.seeded} fixture${info.seeded === 1 ? "" : "s"}${reset}`);
57
54
  process.stdout.write(`${lines.join("\n")}\n`);
58
55
  }
59
56
  /**
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.
57
+ * Local-WordPress connection keys a fresh install must rewrite (the rest of `.env` is untouched). The
58
+ * remote `KIZLO_WP_*` / `KIZLO_WP_SECRET` keys live under their own names and are never
59
+ * touched by `kizlo dev`, so pointing them at a real site survives booting local WordPress.
63
60
  */
64
61
  const WP_ENV_KEYS = [
65
- "KIZLO_DEV_WORDPRESS_URL",
66
- "KIZLO_DEV_WORDPRESS_USERNAME",
67
- "KIZLO_DEV_WORDPRESS_APPLICATION_PASSWORD"
62
+ "KIZLO_LOCAL_WP_URL",
63
+ "KIZLO_LOCAL_WP_USERNAME",
64
+ "KIZLO_LOCAL_WP_APP_PASSWORD"
68
65
  ];
69
66
  /** Read a key's value from raw `.env` content (first match, trimmed); undefined when absent or empty. */
70
67
  function readEnvValue(content, key) {
71
68
  return content.match(new RegExp(`^\\s*${key}\\s*=\\s*(.*)$`, "m"))?.[1]?.trim() || void 0;
72
69
  }
73
70
  /**
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
71
+ * The Kizlo backend URL from `.env` — the one key ending in `KIZLO_API_URL` (`KIZLO_API_URL`,
72
+ * or a framework-prefixed `NEXT_PUBLIC_KIZLO_API_URL`). It's the handler mount the plugin delivers
76
73
  * webhook events to; its origin is the site's canonical URL. User-managed (init writes it), so dev
77
74
  * only reads it. Undefined before `kizlo init` has populated `.env`.
78
75
  */
79
76
  function readBaseUrl(content) {
80
- return content.match(/^\s*[A-Z_]*KIZLO_BACKEND_URL\s*=\s*(.*)$/m)?.[1]?.trim() || void 0;
77
+ return content.match(/^\s*[A-Z_]*KIZLO_API_URL\s*=\s*(.*)$/m)?.[1]?.trim() || void 0;
81
78
  }
82
79
  /**
83
- * Rewrite the dev WordPress connection in `.env` after a fresh install. A reset (or first boot)
80
+ * Rewrite the local WordPress connection in `.env` after a fresh install. A reset (or first boot)
84
81
  * wipes the database, so the previous application password — and possibly the port — no longer
85
82
  * match; without this the app keeps the stale credentials and REST auth fails. Only the three
86
83
  * connection keys are overwritten; everything else in `.env` is preserved, and the file is created
87
84
  * if absent. Gated by the caller on a minted `appPassword`, so a warm resume never churns it.
88
85
  *
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.
86
+ * `KIZLO_CONNECT=local` and `KIZLO_LOCAL_WP_SECRET` are written too but kept out of the overwrite set,
87
+ * so a value the user already set is preserved — booting local WordPress only fills them when absent.
88
+ * The resolved local secret (the existing one, or a freshly minted one) and the Kizlo server base URL
89
+ * are returned so the caller can sync them into the plugin.
93
90
  */
94
91
  function updateWpEnv(cfg, info) {
95
92
  if (!info.appPassword) return void 0;
96
93
  const envPath = join(cfg.configDir, ".env");
97
94
  const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
98
- const siteSecret = readEnvValue(existing, "KIZLO_DEV_SITE_SECRET") ?? randomBytes(32).toString("hex");
95
+ const siteSecret = readEnvValue(existing, "KIZLO_LOCAL_WP_SECRET") ?? randomBytes(32).toString("hex");
99
96
  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"));
97
+ KIZLO_LOCAL_WP_URL: info.url,
98
+ KIZLO_LOCAL_WP_USERNAME: info.username,
99
+ KIZLO_LOCAL_WP_APP_PASSWORD: info.appPassword,
100
+ KIZLO_LOCAL_WP_SECRET: siteSecret,
101
+ KIZLO_CONNECT: "local"
102
+ }, new Set(WP_ENV_KEYS), envGroups(DEFAULT_ENV_KEYS));
106
103
  writeFileSync(envPath, content);
107
104
  return {
108
105
  siteSecret,
@@ -110,19 +107,19 @@ function updateWpEnv(cfg, info) {
110
107
  };
111
108
  }
112
109
  /**
113
- * Re-point `KIZLO_DEV_WORDPRESS_URL` at the current dev URL on a warm resume, so a LAN address that
110
+ * Re-point `KIZLO_LOCAL_WP_URL` at the current URL on a warm resume, so a LAN address that
114
111
  * changed since the last session (a new DHCP lease, a different network) doesn't leave the app talking
115
112
  * to a dead IP. Only this one key is rewritten — the credentials and secret from the fresh install are
116
113
  * preserved — and it's a no-op when `.env` is absent (nothing provisioned yet) or already current.
117
114
  * The full fresh-install write goes through {@link updateWpEnv} instead; this covers the resume path it
118
115
  * skips. Returns true when it changed the file. The app still needs a restart to read the new value.
119
116
  */
120
- function syncDevUrl(cfg, url) {
117
+ function syncLocalUrl(cfg, url) {
121
118
  const envPath = join(cfg.configDir, ".env");
122
119
  if (!existsSync(envPath)) return false;
123
120
  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"));
121
+ if (readEnvValue(existing, "KIZLO_LOCAL_WP_URL") === url) return false;
122
+ const { content } = mergeEnv(existing, { KIZLO_LOCAL_WP_URL: url }, new Set(["KIZLO_LOCAL_WP_URL"]), envGroups(DEFAULT_ENV_KEYS));
126
123
  writeFileSync(envPath, content);
127
124
  return true;
128
125
  }
@@ -134,7 +131,7 @@ function syncDevUrl(cfg, url) {
134
131
  * `next dev` child server — nothing stops them automatically when we die. A dying process
135
132
  * also can't be trusted to finish async work (a closed terminal breaks its stdout
136
133
  * 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
134
+ * *exit*; the watchdog, watching our PID from its own session, stops it the moment
138
135
  * we're gone — by Ctrl+C, SIGTERM, a closed tab, SIGKILL, or a crash alike.
139
136
  *
140
137
  * We trigger that exit two ways, because neither alone is enough: signal handlers (for
@@ -150,7 +147,7 @@ function armForegroundTeardown(cfg) {
150
147
  exiting = true;
151
148
  unregisterSession(cfg.project);
152
149
  try {
153
- note(`Stopping dev stack (${reason})…`);
150
+ note(`Stopping local WordPress (${reason})…`);
154
151
  } catch {}
155
152
  process.exit(0);
156
153
  };
@@ -169,7 +166,7 @@ function armForegroundTeardown(cfg) {
169
166
  }
170
167
  }
171
168
  /**
172
- * Boot the stack and run in the foreground until exit — the path behind bare
169
+ * Boot local WordPress and run in the foreground until exit — the path behind bare
173
170
  * `kizlo dev`. Arms teardown first (so a mid-startup cancel still
174
171
  * stops partial containers), shows a timed spinner, prints the summary + stop hint, then
175
172
  * parks. A referenced timer holds the event loop open — an unresolved promise alone would
@@ -196,7 +193,7 @@ async function startForeground(cfg) {
196
193
  if (dbPort !== cfg.dbPort) note(`Database port ${cfg.dbPort} is in use — using ${dbPort} instead.`);
197
194
  armForegroundTeardown(ready);
198
195
  const start = Date.now();
199
- const creds = await withSpinner("Starting WordPress dev stack", () => bootstrapDev(ready));
196
+ const creds = await withSpinner("Starting local WordPress", () => bootstrapDev(ready));
200
197
  if (creds.appPassword) {
201
198
  const env = updateWpEnv(ready, creds);
202
199
  note("Updated .env with the new WordPress credentials");
@@ -212,12 +209,12 @@ async function startForeground(cfg) {
212
209
  });
213
210
  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
211
  }
215
- } else if (syncDevUrl(ready, creds.url)) note("Updated .env WordPress URL to the current network address — restart your app to pick it up");
212
+ } else if (syncLocalUrl(ready, creds.url)) note("Updated .env WordPress URL to the current network address — restart your app to pick it up");
216
213
  printSummary(creds);
217
214
  const { green, dim, bold, reset } = palette();
218
215
  const ms = Date.now() - start;
219
216
  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`);
217
+ process.stdout.write(`\n ${dim}Press ${reset}${bold}Ctrl+C${reset}${dim} (or close the terminal) to stop local WordPress${reset}\n`);
221
218
  process.stdout.write(`\n ${green}✓${reset} Ready in ${took}\n\n`);
222
219
  const stopWatcher = await startWatcher(ready.configDir);
223
220
  if (stopWatcher) process.on("exit", stopWatcher);
@@ -225,8 +222,8 @@ async function startForeground(cfg) {
225
222
  await new Promise(() => {});
226
223
  }
227
224
  /**
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
225
+ * Run the contract watcher in the foreground until exit — the path bare `kizlo dev` takes when local
226
+ * WordPress isn't configured (a project pointing at its own WordPress). Generates once, then watches
230
227
  * the server files and regenerates on save. Returns immediately when another watcher already holds
231
228
  * the lock; otherwise the persistent watcher keeps the process alive on its own.
232
229
  */
@@ -244,44 +241,39 @@ async function watchOnly(cwd) {
244
241
  });
245
242
  }
246
243
  /**
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.
244
+ * Start (or resume) local WordPress. Backs bare `kizlo dev`. Always runs in the foreground and stops
245
+ * it on exit, so it never outlives the session — the subcommands (`stop`/`reset`) manage everything
246
+ * else. Without local WordPress configured there's nothing to boot, so it runs the contract watcher
247
+ * alone.
251
248
  */
252
249
  async function bringUp() {
253
250
  const cwd = process.cwd();
254
- if (!await hasDevStack(cwd)) {
251
+ if (!await usesLocalWordPress(cwd)) {
252
+ note("This project doesn't run local WordPress — running the contract watcher only. Set `dev.local` in kizlo.config.ts to add it.");
255
253
  await watchOnly(cwd);
256
254
  return;
257
255
  }
256
+ const status = await dockerStatus();
257
+ if (status !== "running") {
258
+ note(dockerHint(status));
259
+ process.exit(1);
260
+ }
258
261
  const reaped = await reapOrphans();
259
- if (reaped.length) note(`Stopped ${reaped.length} orphaned dev stack${reaped.length === 1 ? "" : "s"}: ${reaped.join(", ")}`);
262
+ if (reaped.length) note(`Stopped ${reaped.length} orphaned local WordPress${reaped.length === 1 ? "" : "s"}: ${reaped.join(", ")}`);
260
263
  const cfg = await resolveDevConfig(cwd);
261
- if (!cfg.wordpressPath.startsWith("..")) ensureGitignored(cfg.configDir, cfg.wordpressPath);
264
+ ensureGitignored(cfg.configDir, ".kizlo/");
262
265
  await startForeground(cfg);
263
266
  }
264
267
  const subCommands = {
265
268
  stop: defineCommand({
266
269
  meta: {
267
270
  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)"
271
+ description: "Stop local WordPress, keeping the database volume"
280
272
  },
281
273
  async run() {
282
- if (!await requireDevStack(process.cwd())) return;
274
+ if (!await requireLocalWordPress(process.cwd())) return;
283
275
  const { stack } = await resolve$1(process.cwd());
284
- await withSpinner("Removing WordPress dev stack", () => stack.composeDown(), "Stack removed (database and files kept)");
276
+ await withSpinner("Stopping local WordPress", () => stack.composeStop(), "Local WordPress stopped (volumes kept)");
285
277
  }
286
278
  }),
287
279
  reset: defineCommand({
@@ -290,21 +282,21 @@ const subCommands = {
290
282
  description: "Wipe the database and the install folder so the next `kizlo dev` rebuilds fresh"
291
283
  },
292
284
  async run() {
293
- if (!await requireDevStack(process.cwd())) return;
285
+ if (!await requireLocalWordPress(process.cwd())) return;
294
286
  const { cfg, stack } = await resolve$1(process.cwd());
295
- await withSpinner("Wiping WordPress dev stack", () => stack.composeDown({ volumes: true }), "Local WordPress reset successfully");
287
+ await withSpinner("Wiping local WordPress", () => stack.composeDown({ volumes: true }), "Local WordPress reset successfully");
296
288
  rmSync(cfg.wordpressDir, {
297
289
  recursive: true,
298
290
  force: true
299
291
  });
300
- note("Run `kizlo dev` to rebuild a fresh stack.");
292
+ note("Run `kizlo dev` to rebuild fresh.");
301
293
  }
302
294
  })
303
295
  };
304
296
  const dev = defineCommand({
305
297
  meta: {
306
298
  name: "dev",
307
- description: "Run the dev stack and contract watcher (stop | down | reset)"
299
+ description: "Run local WordPress and the contract watcher (stop | reset)"
308
300
  },
309
301
  subCommands,
310
302
  run: groupDefault(Object.keys(subCommands), bringUp)