kizlo 0.9.1 → 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,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)
@@ -1,9 +1,8 @@
1
- import { dockerAvailable } from "../wp/docker.js";
2
1
  import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, runCommandAsync } from "../utils.js";
3
2
  import { PRESETS, detectPreset, getPreset } from "../presets/index.js";
4
3
  import { fetchTemplate } from "../presets/source.js";
5
- import { adaptFile, changesFor, fileEntries, patchEntries, readManifest } from "../presets/template.js";
6
- import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
4
+ import { adaptFile, changesFor, fileEntries, isOlderVersion, minCliError, patchEntries, readManifest, resolveDependencies } from "../presets/template.js";
5
+ import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, 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 path from "node:path";
9
8
  import fs from "node:fs";
@@ -32,7 +31,6 @@ function detectAppDir(cwd, hasSrcDir) {
32
31
  return hasSrcDir ? "src/app" : "app";
33
32
  }
34
33
  async function collectInteractively(ctx) {
35
- const conn = await collectConnectionInteractively(ctx.preset);
36
34
  const dir = orCancel(await p.text({
37
35
  message: "Kizlo directory",
38
36
  initialValue: defaultDir(ctx.hasSrcDir),
@@ -49,7 +47,7 @@ async function collectInteractively(ctx) {
49
47
  })).trim();
50
48
  }
51
49
  return {
52
- ...conn,
50
+ ...await collectConnectionInteractively(ctx.preset),
53
51
  dir,
54
52
  alias
55
53
  };
@@ -60,7 +58,7 @@ async function collectInteractively(ctx) {
60
58
  * later. Never fails — `--yes` always scaffolds a fillable project.
61
59
  */
62
60
  function collectFromEnv(ctx) {
63
- const conn = collectConnectionFromEnv(ctx.preset);
61
+ const conn = collectConnectionFromEnv(ctx.envKeys);
64
62
  const dir = defaultDir(ctx.hasSrcDir);
65
63
  return {
66
64
  ...conn,
@@ -71,40 +69,23 @@ function collectFromEnv(ctx) {
71
69
  function readPkg(pkgPath) {
72
70
  return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
73
71
  }
74
- function kizloDep(pkg) {
75
- return pkg.dependencies?.kizlo ?? pkg.devDependencies?.kizlo;
76
- }
77
- /** A bare `x.y.z` from a version spec (range prefix and pre-release/build metadata dropped); undefined if none. */
78
- function coerceVersion(spec) {
79
- const match = /(\d+)\.(\d+)\.(\d+)/.exec(spec);
80
- return match ? [
81
- Number(match[1]),
82
- Number(match[2]),
83
- Number(match[3])
84
- ] : void 0;
85
- }
86
- /** Whether `have` is a strictly older release than `want`; false when either can't be compared. */
87
- function isOlder(have, want) {
88
- const a = coerceVersion(have);
89
- const b = coerceVersion(want);
90
- if (!a || !b) return false;
91
- for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i];
92
- return false;
93
- }
94
72
  /**
95
- * The template is authoritative for the kizlo version: it declares `kizloVersion` in its manifest
96
- * (stamped at release), falling back to the running CLI's version in-repo before the first stamp.
97
- * When the project's kizlo is older, upgrade the project up to it and say so — a deliberate pin is
98
- * never changed silently. Never downgrades.
73
+ * The template is authoritative for the pinned versions: it declares `dependencies`/`devDependencies`
74
+ * in its manifest (stamped at release), falling back to the running CLI's version for `kizlo` in-repo
75
+ * before the first stamp. For each declared package the project already has at an older release, upgrade
76
+ * it up to the template's pin and say so — a deliberate pin is never changed silently. Never downgrades,
77
+ * and never adds a package the project doesn't already have (a missing `kizlo` is installed separately).
99
78
  */
100
- async function alignKizloVersion(cwd, pm, pkg, manifest) {
101
- const want = manifest.kizloVersion ?? `^${getVersion()}`;
102
- const have = kizloDep(pkg);
103
- if (!have || !isOlder(have, want)) return;
104
- const s = p.spinner();
105
- s.start(`Upgrading kizlo from ${have} to ${want}`);
106
- const ok = await runCommandAsync(addDependencyArgs(pm, `kizlo@${want}`), cwd, "ignore");
107
- s.stop(ok ? `Upgraded kizlo to ${want}` : `Could not upgrade kizlo automatically — install kizlo@${want} yourself`);
79
+ async function alignDependencies(cwd, pm, pkg, manifest) {
80
+ const { dependencies, devDependencies } = resolveDependencies(manifest);
81
+ for (const [name, want] of [...Object.entries(dependencies), ...Object.entries(devDependencies)]) {
82
+ const have = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
83
+ if (!have || !isOlderVersion(have, want)) continue;
84
+ const s = p.spinner();
85
+ s.start(`Upgrading ${name} from ${have} to ${want}`);
86
+ const ok = await runCommandAsync(addDependencyArgs(pm, `${name}@${want}`), cwd, "ignore");
87
+ s.stop(ok ? `Upgraded ${name} to ${want}` : `Could not upgrade ${name} automatically — install ${name}@${want} yourself`);
88
+ }
108
89
  }
109
90
  const init = defineCommand({
110
91
  meta: {
@@ -178,76 +159,85 @@ const init = defineCommand({
178
159
  process.exit(1);
179
160
  }
180
161
  if (yes) loadEnvFiles(cwd);
181
- const setup = yes ? collectFromEnv({
182
- cwd,
183
- hasSrcDir,
184
- preset
185
- }) : await collectInteractively({
186
- cwd,
187
- hasSrcDir,
188
- preset
189
- });
190
- if (args.alias !== void 0) setup.alias = String(args.alias).trim();
191
- setup.alias = aliasWithSlash(setup.alias);
192
- if (setup.mode === "local" && !await dockerAvailable()) {
193
- p.cancel("Docker isn't available — start Docker (or install it) and re-run, or choose “Use my own WordPress”.");
194
- process.exit(1);
195
- }
196
- if (!hasKizlo) {
197
- const spec = `kizlo@^${getVersion()}`;
198
- const s = p.spinner();
199
- s.start(`Installing kizlo with ${pm}`);
200
- const ok = await runCommandAsync(addDependencyArgs(pm, spec), cwd, "ignore");
201
- s.stop(ok ? "Installed kizlo" : "Could not install kizlo automatically");
202
- if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, spec).join(" ")}`);
203
- }
204
- if (preset.apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, preset.apiPath);
205
- await setupLocalWordPress(cwd, setup);
206
- await writeEnv(cwd, preset, setup, {
207
- force,
208
- yes
209
- });
210
- await syncRemote(setup);
211
- const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
212
- const serverDirRel = path.join(dirRel, "server");
213
- const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
214
- const scaffold = buildScaffoldContext(cwd, {
215
- dirRel,
216
- appDir: detectAppDir(cwd, hasSrcDir),
217
- alias: setup.alias,
218
- clientUrl
219
- });
220
- const files = [{
221
- label: "Kizlo config",
222
- relPath: "kizlo.config.ts",
223
- contents: kizloConfigTemplate(dirRel, setup.alias, setup.devPath)
224
- }];
225
162
  let manifest;
163
+ let fetched;
226
164
  if (preset.template) {
227
- const fetched = await fetchTemplate(preset.template);
228
- try {
229
- manifest = readManifest(fetched.dir);
230
- await alignKizloVersion(cwd, pm, pkg, manifest);
231
- for (const entry of fileEntries(changesFor(manifest, "init"))) files.push(adaptFile(fetched.dir, entry, manifest.conventions, scaffold));
232
- } finally {
165
+ fetched = await fetchTemplate(preset.template);
166
+ manifest = readManifest(fetched.dir);
167
+ const minErr = minCliError(manifest);
168
+ if (minErr) {
233
169
  fetched.cleanup();
170
+ p.cancel(minErr);
171
+ process.exit(1);
234
172
  }
235
- } else if (preset.scaffolds) files.push(...preset.scaffolds(scaffold));
236
- const scaffolded = [];
237
- for (const file of files) scaffolded.push({
238
- file,
239
- result: await scaffoldFile(cwd, file, {
173
+ }
174
+ const envKeys = resolveEnvKeys(preset, manifest);
175
+ try {
176
+ const setup = yes ? collectFromEnv({
177
+ cwd,
178
+ hasSrcDir,
179
+ preset,
180
+ envKeys
181
+ }) : await collectInteractively({
182
+ cwd,
183
+ hasSrcDir,
184
+ preset
185
+ });
186
+ if (args.alias !== void 0) setup.alias = String(args.alias).trim();
187
+ setup.alias = aliasWithSlash(setup.alias);
188
+ if (!hasKizlo) {
189
+ const spec = `kizlo@${manifest?.dependencies?.kizlo ?? `^${getVersion()}`}`;
190
+ const s = p.spinner();
191
+ s.start(`Installing kizlo with ${pm}`);
192
+ const ok = await runCommandAsync(addDependencyArgs(pm, spec), cwd, "ignore");
193
+ s.stop(ok ? "Installed kizlo" : "Could not install kizlo automatically");
194
+ if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, spec).join(" ")}`);
195
+ }
196
+ if (preset.apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, preset.apiPath);
197
+ await setupLocalWordPress(cwd, setup);
198
+ await writeEnv(cwd, envKeys, setup, {
240
199
  force,
241
- yes
242
- })
243
- });
244
- writeGeneratedContract(cwd, serverDirRel);
245
- const gitignore = ensureGitignored(cwd, ".env");
246
- for (const { file, result } of scaffolded) reportScaffold(file, result, yes);
247
- if (manifest) applyLayoutPatches(cwd, patchEntries(changesFor(manifest, "init")), manifest.conventions, scaffold);
248
- if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
249
- nextStepsNote(setup);
250
- p.outro("Kizlo is ready 🎉");
200
+ yes,
201
+ exampleTemplate: fetched ? readEnvExample(fetched.dir) : void 0
202
+ });
203
+ await syncRemote(setup);
204
+ const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
205
+ const serverDirRel = path.join(dirRel, "server");
206
+ const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
207
+ const scaffold = buildScaffoldContext(cwd, {
208
+ dirRel,
209
+ appDir: detectAppDir(cwd, hasSrcDir),
210
+ alias: setup.alias,
211
+ clientUrl
212
+ });
213
+ const files = [{
214
+ label: "Kizlo config",
215
+ relPath: "kizlo.config.ts",
216
+ contents: kizloConfigTemplate(dirRel, setup.alias, setup.mode === "local")
217
+ }];
218
+ if (manifest && fetched) {
219
+ await alignDependencies(cwd, pm, pkg, manifest);
220
+ for (const entry of fileEntries(changesFor(manifest, "init"))) files.push(adaptFile(fetched.dir, entry, manifest.conventions, scaffold));
221
+ } else if (preset.scaffolds) files.push(...preset.scaffolds(scaffold));
222
+ const scaffolded = [];
223
+ for (const file of files) scaffolded.push({
224
+ file,
225
+ result: await scaffoldFile(cwd, file, {
226
+ force,
227
+ yes
228
+ })
229
+ });
230
+ writeGeneratedContract(cwd, serverDirRel);
231
+ const gitignore = ensureGitignored(cwd, ".env");
232
+ ensureGitignored(cwd, ".kizlo/");
233
+ for (const { file, result } of scaffolded) reportScaffold(file, result, yes);
234
+ if (manifest) applyLayoutPatches(cwd, patchEntries(changesFor(manifest, "init")), manifest.conventions, scaffold);
235
+ if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
236
+ nextStepsNote(setup);
237
+ p.outro("Kizlo is ready 🎉");
238
+ } finally {
239
+ fetched?.cleanup();
240
+ }
251
241
  }
252
242
  });
253
243