kizlo 0.7.0 → 0.8.1

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 (85) hide show
  1. package/dist/category/error.js +14 -0
  2. package/dist/category/index.js +77 -0
  3. package/dist/category/schema.js +40 -0
  4. package/dist/category/utils.js +20 -0
  5. package/dist/cli/banner.js +1 -1
  6. package/dist/cli/commands/_setup.js +342 -0
  7. package/dist/cli/commands/create.js +152 -0
  8. package/dist/cli/commands/dev.js +1 -1
  9. package/dist/cli/commands/init.js +53 -300
  10. package/dist/cli/index.js +2 -1
  11. package/dist/cli/presets/generated/templates.js +59 -0
  12. package/dist/cli/presets/nextjs.js +3 -95
  13. package/dist/cli/presets/patch.js +133 -0
  14. package/dist/cli/presets/template.js +36 -0
  15. package/dist/comment/index.js +4 -4
  16. package/dist/context.d.ts +25 -8
  17. package/dist/context.js +15 -5
  18. package/dist/email/service.d.ts +4 -3
  19. package/dist/email/service.js +4 -4
  20. package/dist/index.d.ts +15 -11
  21. package/dist/index.js +10 -4
  22. package/dist/integrations/nextjs/metadata.js +6 -3
  23. package/dist/integrations/nextjs/sitemap.d.ts +8 -7
  24. package/dist/integrations/nextjs/sitemap.js +3 -3
  25. package/dist/menu/index.js +19 -14
  26. package/dist/menu/schema.d.ts +14 -0
  27. package/dist/menu/schema.js +35 -40
  28. package/dist/menu/utils.js +23 -14
  29. package/dist/nextjs/server.d.ts +2 -1
  30. package/dist/nextjs/server.js +2 -1
  31. package/dist/package.js +1 -1
  32. package/dist/page/error.js +20 -0
  33. package/dist/page/index.js +128 -0
  34. package/dist/page/schema.js +75 -0
  35. package/dist/page/utils.js +43 -0
  36. package/dist/post/error.js +4 -14
  37. package/dist/post/index.js +17 -9
  38. package/dist/post/schema.js +37 -66
  39. package/dist/post/utils.js +27 -24
  40. package/dist/router.d.ts +1006 -185
  41. package/dist/router.js +6 -0
  42. package/dist/seo/index.js +7 -44
  43. package/dist/seo/schema.d.ts +10 -6
  44. package/dist/seo/schema.js +7 -5
  45. package/dist/seo/sitemap.d.ts +9 -1
  46. package/dist/seo/sitemap.js +17 -1
  47. package/dist/seo/types.d.ts +17 -20
  48. package/dist/seo/utils.js +9 -7
  49. package/dist/settings/index.js +27 -24
  50. package/dist/settings/service.d.ts +14 -13
  51. package/dist/settings/service.interface.d.ts +2 -2
  52. package/dist/settings/service.js +9 -9
  53. package/dist/shared/middleware.d.ts +4 -4
  54. package/dist/shared/procedure.d.ts +6 -7
  55. package/dist/tag/error.js +14 -0
  56. package/dist/tag/index.js +76 -0
  57. package/dist/tag/schema.js +38 -0
  58. package/dist/tag/utils.js +19 -0
  59. package/dist/taxonomy/service.js +48 -0
  60. package/dist/test/fixtures.d.ts +1 -1
  61. package/dist/test/fixtures.js +1 -1
  62. package/dist/webhook/index.d.ts +2 -2
  63. package/dist/wordpress/category/service.d.ts +28 -0
  64. package/dist/wordpress/category/service.js +55 -0
  65. package/dist/wordpress/category/types.d.ts +120 -0
  66. package/dist/wordpress/category/types.js +15 -0
  67. package/dist/wordpress/comment/service.d.ts +1 -1
  68. package/dist/wordpress/index.d.ts +9 -4
  69. package/dist/wordpress/menu/items/types.d.ts +7 -4
  70. package/dist/wordpress/menu/items/types.js +22 -0
  71. package/dist/wordpress/page/service.d.ts +28 -0
  72. package/dist/wordpress/page/service.js +55 -0
  73. package/dist/wordpress/page/types.d.ts +270 -0
  74. package/dist/wordpress/page/types.js +27 -0
  75. package/dist/wordpress/post/types.d.ts +9 -5
  76. package/dist/wordpress/post/types.js +41 -0
  77. package/dist/wordpress/service.d.ts +7 -1
  78. package/dist/wordpress/service.js +6 -0
  79. package/dist/wordpress/tag/service.d.ts +28 -0
  80. package/dist/wordpress/tag/service.js +55 -0
  81. package/dist/wordpress/tag/types.d.ts +113 -0
  82. package/dist/wordpress/tag/types.js +15 -0
  83. package/package.json +5 -3
  84. package/dist/service.d.ts +0 -19
  85. package/dist/service.js +0 -20
@@ -1,65 +1,16 @@
1
- import { composeStop, createStack, dockerAvailable } from "../wp/docker.js";
2
- import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, envGroups, envKeysPresent, getVersion, loadEnvFiles, mergeEnv, pickStackPort, resolveModuleImport, runCommand, writeFileIfAbsent } from "../utils.js";
1
+ import { dockerAvailable } from "../wp/docker.js";
2
+ import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, resolveModuleImport, runCommand, writeFileIfAbsent } from "../utils.js";
3
3
  import { printBanner } from "../banner.js";
4
- import { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveStackName } from "../daemon/config.js";
5
- import { CONTRACT_BARREL } from "../daemon/generate.js";
6
- import { createAdminAppPassword } from "../wp/bootstrap.js";
7
- import { bootstrapDev } from "../wp/dev.js";
8
- import { removeProjectContainers } from "../wp/session.js";
9
- import { syncSiteSettings } from "../wp/settings.js";
10
- import { devStack } from "../wp/stack.js";
11
4
  import { PRESETS, detectPreset, getPreset } from "../presets/index.js";
12
- import z from "zod/v4";
5
+ import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
6
+ import { CONTRACT_BARREL } from "../daemon/generate.js";
7
+ import { applyPatchToSource, patchChanged, resolvePatch, resolvePatchTargetPath, templatePatches } from "../presets/patch.js";
13
8
  import path from "node:path";
14
9
  import fs from "node:fs";
15
10
  import { defineCommand } from "citty";
16
11
  import * as p from "@clack/prompts";
17
- import { randomBytes } from "node:crypto";
18
12
 
19
13
  //#region src/cli/commands/init.ts
20
- /** Production connection keys — what a real deploy needs, and what `.env.example` always lists. */
21
- const PROD_WP_ENV_KEYS = [
22
- "KIZLO_SITE_SECRET",
23
- "KIZLO_WORDPRESS_URL",
24
- "KIZLO_WORDPRESS_USERNAME",
25
- "KIZLO_WORDPRESS_APPLICATION_PASSWORD"
26
- ];
27
- /**
28
- * The `.env` keys init manages and the values to write, branched on the connection mode. A local
29
- * dev stack writes the `KIZLO_DEV_WORDPRESS_*` / `KIZLO_DEV_SITE_SECRET` set plus `KIZLO_TARGET=dev`, so it
30
- * never touches the production keys (a user can point those at a real site). A remote site writes the
31
- * bare production keys exactly as before — no `KIZLO_TARGET`, since `"production"` is the default target.
32
- */
33
- function managedEnv(preset, setup) {
34
- if (setup.mode === "local") return {
35
- keys: [
36
- preset.baseUrlEnvKey,
37
- "KIZLO_TARGET",
38
- "KIZLO_DEV_SITE_SECRET",
39
- "KIZLO_DEV_WORDPRESS_URL",
40
- "KIZLO_DEV_WORDPRESS_USERNAME",
41
- "KIZLO_DEV_WORDPRESS_APPLICATION_PASSWORD"
42
- ],
43
- values: {
44
- [preset.baseUrlEnvKey]: setup.baseUrl,
45
- KIZLO_TARGET: "dev",
46
- KIZLO_DEV_SITE_SECRET: setup.siteSecret,
47
- KIZLO_DEV_WORDPRESS_URL: setup.wpUrl,
48
- KIZLO_DEV_WORDPRESS_USERNAME: setup.wpUsername,
49
- KIZLO_DEV_WORDPRESS_APPLICATION_PASSWORD: setup.wpPassword
50
- }
51
- };
52
- return {
53
- keys: [preset.baseUrlEnvKey, ...PROD_WP_ENV_KEYS],
54
- values: {
55
- [preset.baseUrlEnvKey]: setup.baseUrl,
56
- KIZLO_SITE_SECRET: setup.siteSecret,
57
- KIZLO_WORDPRESS_URL: setup.wpUrl,
58
- KIZLO_WORDPRESS_USERNAME: setup.wpUsername,
59
- KIZLO_WORDPRESS_APPLICATION_PASSWORD: setup.wpPassword
60
- }
61
- };
62
- }
63
14
  function defaultDir(hasSrcDir) {
64
15
  return hasSrcDir ? "src/lib/kizlo" : "lib/kizlo";
65
16
  }
@@ -75,18 +26,6 @@ export default defineConfig({
75
26
  })
76
27
  `;
77
28
  }
78
- /** Appends the API path to the base URL so the client and route handler agree. */
79
- function withApiPath(baseUrl, apiPath) {
80
- try {
81
- const url = new URL(baseUrl);
82
- const current = url.pathname.replace(/\/+$/, "");
83
- if (current.endsWith(apiPath)) return url.toString().replace(/\/+$/, "");
84
- url.pathname = `${current}${apiPath}`;
85
- return url.toString().replace(/\/+$/, "");
86
- } catch {
87
- return baseUrl;
88
- }
89
- }
90
29
  /** Whether two URLs share an origin (scheme + host + port); false when either can't be parsed. */
91
30
  function sameOrigin(a, b) {
92
31
  try {
@@ -100,22 +39,6 @@ function detectAppDir(cwd, hasSrcDir) {
100
39
  if (fs.existsSync(path.join(cwd, "app"))) return "app";
101
40
  return hasSrcDir ? "src/app" : "app";
102
41
  }
103
- const requiredString = z.string().trim().min(1, "Required");
104
- const urlString = requiredString.pipe(z.url("Must be a valid URL (e.g. https://example.com)"));
105
- const dirPath = requiredString.refine((value) => !value.endsWith(".ts"), "Enter a directory, not a file");
106
- function validate(schema) {
107
- return (value) => {
108
- const result = schema.safeParse(value ?? "");
109
- return result.success ? void 0 : result.error.issues[0]?.message;
110
- };
111
- }
112
- function orCancel(value) {
113
- if (p.isCancel(value)) {
114
- p.cancel("Setup cancelled.");
115
- process.exit(0);
116
- }
117
- return value;
118
- }
119
42
  /**
120
43
  * The single overwrite policy for every scaffolded file: create it when absent, overwrite on
121
44
  * `--force`, and otherwise ask before clobbering an existing file (keeping it when the user
@@ -146,76 +69,7 @@ function reportScaffold(file, result, yes) {
146
69
  else p.log.success(`${result === "overwritten" ? "Overwrote" : "Created"} ${file.label} (${file.relPath})`);
147
70
  }
148
71
  async function collectInteractively(ctx) {
149
- let siteUrl;
150
- let baseUrl;
151
- if (ctx.preset.apiPath) baseUrl = orCancel(await p.text({
152
- message: "Public app URL",
153
- placeholder: "https://your-app.com",
154
- validate: validate(urlString)
155
- }));
156
- else {
157
- siteUrl = orCancel(await p.text({
158
- message: "Public site URL",
159
- placeholder: "https://your-app.com",
160
- validate: validate(urlString)
161
- }));
162
- baseUrl = orCancel(await p.text({
163
- message: "Kizlo backend URL (where the handler is mounted)",
164
- placeholder: "https://your-app.com/kizlo",
165
- validate: validate(urlString)
166
- }));
167
- }
168
- const siteSecret = orCancel(await p.select({
169
- message: "Site secret (webhook signing key)",
170
- initialValue: "generate",
171
- options: [{
172
- value: "generate",
173
- label: "Generate a secure secret automatically",
174
- hint: "recommended"
175
- }, {
176
- value: "enter",
177
- label: "Enter my own"
178
- }]
179
- })) === "enter" ? orCancel(await p.password({
180
- message: "Enter the site secret",
181
- validate: validate(requiredString)
182
- })) : randomBytes(32).toString("hex");
183
- const mode = orCancel(await p.select({
184
- message: "WordPress connection",
185
- initialValue: "local",
186
- options: [{
187
- value: "local",
188
- label: "Set up a local dev environment",
189
- hint: "runs WordPress in Docker"
190
- }, {
191
- value: "remote",
192
- label: "Use my own WordPress",
193
- hint: "connect to an existing site"
194
- }]
195
- }));
196
- let wpUrl = "";
197
- let wpUsername = "";
198
- let wpPassword = "";
199
- let devPath;
200
- if (mode === "remote") {
201
- wpUrl = orCancel(await p.text({
202
- message: "WordPress URL",
203
- placeholder: "https://wp.your-app.com",
204
- validate: validate(urlString)
205
- }));
206
- wpUsername = orCancel(await p.text({
207
- message: "WordPress username",
208
- validate: validate(requiredString)
209
- }));
210
- wpPassword = orCancel(await p.password({
211
- message: "WordPress application password",
212
- validate: validate(requiredString)
213
- }));
214
- } else devPath = orCancel(await p.text({
215
- message: "Local WordPress folder",
216
- initialValue: "wordpress",
217
- validate: validate(dirPath)
218
- }));
72
+ const conn = await collectConnectionInteractively(ctx.preset);
219
73
  const dir = orCancel(await p.text({
220
74
  message: "Kizlo directory",
221
75
  initialValue: defaultDir(ctx.hasSrcDir),
@@ -232,14 +86,7 @@ async function collectInteractively(ctx) {
232
86
  })).trim();
233
87
  }
234
88
  return {
235
- mode,
236
- baseUrl,
237
- siteUrl,
238
- siteSecret,
239
- wpUrl,
240
- wpUsername,
241
- wpPassword,
242
- devPath,
89
+ ...conn,
243
90
  dir,
244
91
  alias
245
92
  };
@@ -250,14 +97,10 @@ async function collectInteractively(ctx) {
250
97
  * later. Never fails — `--yes` always scaffolds a fillable project.
251
98
  */
252
99
  function collectFromEnv(ctx) {
100
+ const conn = collectConnectionFromEnv(ctx.preset);
253
101
  const dir = defaultDir(ctx.hasSrcDir);
254
102
  return {
255
- mode: "remote",
256
- baseUrl: process.env[ctx.preset.baseUrlEnvKey]?.trim() ?? "",
257
- siteSecret: process.env.KIZLO_SITE_SECRET?.trim() || randomBytes(32).toString("hex"),
258
- wpUrl: process.env.KIZLO_WORDPRESS_URL?.trim() ?? "",
259
- wpUsername: process.env.KIZLO_WORDPRESS_USERNAME?.trim() ?? "",
260
- wpPassword: process.env.KIZLO_WORDPRESS_APPLICATION_PASSWORD?.trim() ?? "",
103
+ ...conn,
261
104
  dir,
262
105
  alias: ctx.preset.apiPath ? aliasWithSlash(detectImportAlias(ctx.cwd, path.join(dir, "server"))?.prefix) : ""
263
106
  };
@@ -265,66 +108,6 @@ function collectFromEnv(ctx) {
265
108
  function readPkg(pkgPath) {
266
109
  return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
267
110
  }
268
- /** Build a {@link ResolvedDevConfig} from the chosen install folder, matching `resolveDevConfig`'s
269
- * defaults — built directly so init never has to round-trip through the config file it's writing. */
270
- function devConfigFor(cwd, devPath) {
271
- return {
272
- configDir: cwd,
273
- project: `${resolveStackName(cwd)}-dev`,
274
- port: DEFAULT_DEV_PORT,
275
- portExplicit: false,
276
- dbPort: DEFAULT_DEV_DB_PORT,
277
- dbPortExplicit: false,
278
- fixtures: [],
279
- wordpressPath: devPath,
280
- wordpressDir: path.resolve(cwd, devPath)
281
- };
282
- }
283
- /**
284
- * Boot the local dev stack once to produce working credentials, then stop it (volumes
285
- * persist, so a later `kizlo dev` resumes instantly). The dev stack mints no application
286
- * password — that's a test-stack concern — so we create one here for REST auth in `.env`.
287
- * While the stack is still up, push the dev site settings (`siteSecret` plus the Kizlo server's
288
- * `url`/`backend_url`, derived from `baseUrl`) into the plugin so webhook signing and event delivery
289
- * work.
290
- */
291
- async function provisionLocalStack(cfg, siteSecret, baseUrl) {
292
- await removeProjectContainers(cfg.project);
293
- const port = await pickStackPort(cfg.port, {
294
- fixed: cfg.portExplicit,
295
- configKey: "dev.port"
296
- });
297
- const dbPort = await pickStackPort(cfg.dbPort, {
298
- fixed: cfg.dbPortExplicit,
299
- host: "127.0.0.1",
300
- configKey: "dev.dbPort"
301
- });
302
- const ready = {
303
- ...cfg,
304
- port,
305
- dbPort
306
- };
307
- createStack(devStack(ready));
308
- const info = await bootstrapDev(ready);
309
- const appPassword = info.appPassword ?? await createAdminAppPassword("kizlo");
310
- const sync = await syncSiteSettings({
311
- url: info.url,
312
- username: info.username,
313
- password: appPassword
314
- }, {
315
- secret: siteSecret,
316
- backendUrl: baseUrl,
317
- containerized: true
318
- });
319
- await composeStop();
320
- return {
321
- url: info.url,
322
- username: info.username,
323
- appPassword,
324
- adminPassword: info.secrets?.password,
325
- secretSyncError: sync.ok ? void 0 : sync.error
326
- };
327
- }
328
111
  const init = defineCommand({
329
112
  meta: {
330
113
  name: "init",
@@ -409,63 +192,17 @@ const init = defineCommand({
409
192
  if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, "kizlo@latest").join(" ")}`);
410
193
  }
411
194
  if (preset.apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, preset.apiPath);
412
- let adminPassword;
413
- if (setup.mode === "local" && setup.devPath) {
414
- ensureGitignored(cwd, setup.devPath);
415
- const s = p.spinner();
416
- s.start("Setting up local WordPress (first run downloads images, this can take a while)");
417
- try {
418
- const local = await provisionLocalStack(devConfigFor(cwd, setup.devPath), setup.siteSecret, setup.baseUrl);
419
- setup.wpUrl = local.url;
420
- setup.wpUsername = local.username;
421
- setup.wpPassword = local.appPassword;
422
- adminPassword = local.adminPassword;
423
- s.stop("Local WordPress ready");
424
- if (local.secretSyncError) p.log.warn(`Could not sync KIZLO_DEV_SITE_SECRET to the local plugin (${local.secretSyncError})`);
425
- } catch (error) {
426
- s.stop("Local WordPress setup failed");
427
- p.cancel(error instanceof Error ? error.message : String(error));
428
- process.exit(1);
429
- }
430
- }
431
- const { keys, values: envValues } = managedEnv(preset, setup);
432
- const envPath = path.join(cwd, ".env");
433
- const envExisted = fs.existsSync(envPath);
434
- const existingEnv = envExisted ? fs.readFileSync(envPath, "utf8") : "";
435
- const conflicts = envKeysPresent(existingEnv, keys);
436
- let overwriteKeys = new Set(keys);
437
- if (conflicts.length && !force) if (yes) {
438
- overwriteKeys = /* @__PURE__ */ new Set();
439
- p.log.info("Keeping existing .env values (pass --force to overwrite)");
440
- } else {
441
- p.log.warn("Some environment variables already exist in .env");
442
- if (!orCancel(await p.confirm({
443
- message: "Overwrite their existing values?",
444
- initialValue: true
445
- }))) overwriteKeys = /* @__PURE__ */ new Set();
446
- }
447
- const merge = mergeEnv(existingEnv, envValues, overwriteKeys, envGroups(preset.baseUrlEnvKey));
448
- fs.writeFileSync(envPath, merge.content);
449
- if (setup.mode === "remote" && setup.wpUrl && setup.wpUsername && setup.wpPassword) {
450
- const sync = await syncSiteSettings({
451
- url: setup.wpUrl,
452
- username: setup.wpUsername,
453
- password: setup.wpPassword
454
- }, {
455
- secret: setup.siteSecret,
456
- siteUrl: setup.siteUrl,
457
- backendUrl: setup.baseUrl
458
- });
459
- 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.`);
460
- }
461
- const exampleKeys = [preset.baseUrlEnvKey, ...PROD_WP_ENV_KEYS];
462
- const { content: exampleEnv } = mergeEnv("", Object.fromEntries(exampleKeys.map((key) => [key, ""])), new Set(exampleKeys), envGroups(preset.baseUrlEnvKey));
463
- const exampleBody = `${exampleEnv}\n# Point the app at a local dev stack (managed by \`kizlo dev\`) instead of the keys above:\n# KIZLO_TARGET=dev\n`;
464
- const exampleCreated = writeFileIfAbsent(path.join(cwd, ".env.example"), exampleBody);
195
+ await setupLocalWordPress(cwd, setup);
196
+ await writeEnv(cwd, preset, setup, {
197
+ force,
198
+ yes
199
+ });
200
+ await syncRemote(setup);
465
201
  const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
466
202
  const serverDirRel = path.join(dirRel, "server");
467
203
  const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
468
204
  const scaffold = {
205
+ kizloDir: dirRel,
469
206
  serverDirName: path.basename(serverDirRel),
470
207
  serverEntryPath: path.join(serverDirRel, "index.ts"),
471
208
  clientPath: path.join(dirRel, "client.ts"),
@@ -490,29 +227,45 @@ const init = defineCommand({
490
227
  writeFileIfAbsent(path.join(cwd, generatedDirRel, "contract.json"), "{}\n");
491
228
  writeFileIfAbsent(path.join(cwd, generatedDirRel, "index.ts"), CONTRACT_BARREL);
492
229
  const gitignore = ensureGitignored(cwd, ".env");
493
- if (!envExisted) p.log.success("Created .env");
494
- else if (merge.updated.length || merge.added.length) p.log.success("Updated .env");
495
- else p.log.info("Left .env unchanged");
496
- p.log.success(exampleCreated ? "Created .env.example" : "Skipped .env.example (exists)");
497
230
  for (const { file, result } of scaffolded) reportScaffold(file, result, yes);
498
- if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
499
- if (setup.mode === "local") {
500
- p.log.success(`Local WordPress configured (${setup.devPath}) — .env points at ${setup.wpUrl}`);
501
- if (adminPassword) p.log.warn(`wp-admin login: ${setup.wpUsername} / ${adminPassword} — save it, it's shown only once`);
231
+ for (const patch of templatePatches(preset.id)) {
232
+ const resolved = resolvePatch(patch, scaffold);
233
+ const abs = resolvePatchTargetPath(cwd, resolved.relPath);
234
+ if (!abs) {
235
+ p.log.info(`Skipped ${resolved.label} wiring — ${resolved.relPath} not found (add it yourself)`);
236
+ continue;
237
+ }
238
+ const relTarget = path.relative(cwd, abs);
239
+ const src = fs.readFileSync(abs, "utf8");
240
+ let text;
241
+ let changes;
242
+ try {
243
+ ({text, changes} = applyPatchToSource(src, resolved, { replaceConflicts: force }));
244
+ } catch (err) {
245
+ p.log.error(`Could not parse ${resolved.label} (${relTarget}) to wire Kizlo into it: ${err instanceof Error ? err.message : String(err)}`);
246
+ throw err;
247
+ }
248
+ if (!force && !yes && changes.keptExports.length) {
249
+ p.log.warn(`${resolved.label} (${relTarget}) already defines ${changes.keptExports.join(", ")}`);
250
+ if (orCancel(await p.confirm({
251
+ message: "Replace with Kizlo's version?",
252
+ initialValue: true
253
+ }))) ({text, changes} = applyPatchToSource(src, resolved, { replaceConflicts: true }));
254
+ }
255
+ if (patchChanged(changes)) {
256
+ fs.writeFileSync(abs, text);
257
+ const parts = [
258
+ ...changes.replacedExports.length ? [`replaced ${changes.replacedExports.join(", ")}`] : [],
259
+ ...changes.addedExports.length ? [`added ${changes.addedExports.join(", ")}`] : [],
260
+ ...changes.addedImports.length ? ["imports"] : []
261
+ ];
262
+ p.log.success(`Wired Kizlo into your ${resolved.label} (${relTarget}) — ${parts.join(", ")}`);
263
+ }
264
+ if (changes.keptExports.length) p.log.info(`Kept your existing ${changes.keptExports.join(", ")} in ${resolved.label} — pass --force to replace`);
265
+ else if (!patchChanged(changes)) p.log.info(`Your ${resolved.label} is already wired (${relTarget})`);
502
266
  }
503
- p.note([
504
- ...setup.mode === "local" ? [
505
- `Start your local WordPress dev stack (also watches your extensions):`,
506
- ` npx kizlo dev`,
507
- ``
508
- ] : [
509
- `Watch your extensions and regenerate the contract during development:`,
510
- ` npx kizlo watch`,
511
- ``
512
- ],
513
- `Generate the contract once for production builds:`,
514
- ` npx kizlo generate`
515
- ].join("\n"), "Next steps");
267
+ if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
268
+ nextStepsNote(setup.mode);
516
269
  p.outro("Kizlo is ready 🎉");
517
270
  }
518
271
  });
package/dist/cli/index.js CHANGED
@@ -7,9 +7,10 @@ runMain(defineCommand({
7
7
  meta: {
8
8
  name: "kizlo",
9
9
  version: getVersion(),
10
- description: "Kizlo CLI — headless WordPress toolkit"
10
+ description: "Kizlo CLI — headless WordPress framework"
11
11
  },
12
12
  subCommands: {
13
+ create: () => import("./commands/create.js").then((m) => m.create),
13
14
  init: () => import("./commands/init.js").then((m) => m.init),
14
15
  watch: () => import("./commands/watch.js").then((m) => m.watch),
15
16
  generate: () => import("./commands/generate.js").then((m) => m.generate),
@@ -0,0 +1,59 @@
1
+ //#region src/cli/presets/generated/templates.ts
2
+ const TEMPLATE_SUBSETS = { "nextjs": [
3
+ {
4
+ "role": "server-entry",
5
+ "tokenizedPath": "{{kizloDir}}/server/index.ts",
6
+ "body": "import { createKizlo } from \"kizlo/nextjs/server\"\n\nexport const { router, client, context, handler } = createKizlo()\n"
7
+ },
8
+ {
9
+ "role": "client",
10
+ "tokenizedPath": "{{kizloDir}}/client.ts",
11
+ "body": "import { createKizloClient } from \"kizlo/nextjs\"\nimport { contract } from \"./server/generated\"\n\nexport const client = createKizloClient(contract)\n"
12
+ },
13
+ {
14
+ "role": "api-route",
15
+ "tokenizedPath": "{{appDir}}/api/kizlo/[[...rest]]/route.ts",
16
+ "body": "import { handler } from \"{{serverImport}}\"\n\nexport { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE, handler as OPTIONS }\n"
17
+ },
18
+ {
19
+ "role": "robots",
20
+ "tokenizedPath": "{{appDir}}/robots.txt/route.ts",
21
+ "body": "import { createRobotsRoute } from \"kizlo/nextjs/server\"\nimport { client } from \"{{serverImport}}\"\n\n// Run on the edge: cheaper and faster than Node for a response this small.\nexport const runtime = \"edge\"\n\n// Vercel treats special metadata files like robots.txt and sitemap.xml as completely static\n// files at build time, bypassing normal Incremental Static Regeneration (ISR) and on-demand\n// revalidatePath rules.\n//\n// While your revalidation logic executes perfectly in a local Node.js server, Vercel uploads\n// these routes directly to its Edge CDN as immutable assets. Subsequent on-demand revalidation\n// triggers will successfully clear the Data Cache but fail to purge the CDN-level Edge cache\n// for that file.\n//\n// force-dynamic opts out of that static treatment so CMS edits show up; the WordPress call is\n// cached, so requests stay cheap.\nexport const dynamic = \"force-dynamic\"\n\nexport const GET = createRobotsRoute(client)\n"
22
+ },
23
+ {
24
+ "role": "sitemap",
25
+ "tokenizedPath": "{{appDir}}/sitemaps/[sitemap]/route.ts",
26
+ "body": "import { createSitemapRoute } from \"kizlo/nextjs/server\"\nimport { client } from \"{{serverImport}}\"\n\n// Run on the edge: cheaper and faster than Node for a response this small.\nexport const runtime = \"edge\"\n\n// Like robots.txt, Vercel bakes sitemap routes into immutable Edge CDN assets at build time,\n// bypassing ISR and on-demand revalidatePath. The index gets frozen with whatever WordPress\n// returned at build (often nothing, since the self-referential backend isn't reachable yet),\n// and revalidation can't purge that CDN copy.\n//\n// force-dynamic opts out of that static treatment. createSitemapRoute caches the WordPress calls\n// per sitemap and refreshes them on content changes (via revalidateTag), so requests stay cheap.\nexport const dynamic = \"force-dynamic\"\n\nexport const GET = createSitemapRoute(client)\n"
27
+ },
28
+ {
29
+ "role": "sitemap-redirect",
30
+ "tokenizedPath": "{{appDir}}/sitemap.xml/route.ts",
31
+ "body": "import { createSitemapRedirectRoute } from \"kizlo/nextjs/server\"\n\n// Many crawlers ignore robots.txt and probe the well-known /sitemap.xml directly. This route\n// permanently redirects (308) to the generated index at /sitemaps/index.xml. The response is a\n// fixed constant with no WordPress call or request input, so Next statically generates it at\n// build time (no runtime/edge function invoked per request).\nexport const GET = createSitemapRedirectRoute()\n"
32
+ },
33
+ {
34
+ "role": "manifest",
35
+ "tokenizedPath": "{{appDir}}/site.webmanifest/route.ts",
36
+ "body": "import { createManifestRoute } from \"kizlo/nextjs/server\"\nimport { client } from \"{{serverImport}}\"\n\n// Run on the edge: cheaper and faster than Node for a response this small.\nexport const runtime = \"edge\"\n\n// Like robots.txt, Vercel bakes this route into an immutable Edge CDN asset at build time,\n// bypassing ISR and on-demand revalidatePath. force-dynamic opts out of that static treatment\n// so brand-settings edits show up; createManifestRoute caches the WordPress call, so requests\n// stay cheap.\nexport const dynamic = \"force-dynamic\"\n\nexport const GET = createManifestRoute(client)\n"
37
+ }
38
+ ] };
39
+ const TEMPLATE_PATCHES = { "nextjs": [{
40
+ "label": "root layout",
41
+ "tokenizedFile": "{{appDir}}/layout.tsx",
42
+ "imports": [{
43
+ "module": "kizlo/nextjs/server",
44
+ "names": ["createRootMetadata", "createRootViewport"]
45
+ }, {
46
+ "module": "{{serverImport}}",
47
+ "names": ["client"]
48
+ }],
49
+ "exports": [{
50
+ "name": "generateMetadata",
51
+ "value": "createRootMetadata(client)"
52
+ }, {
53
+ "name": "generateViewport",
54
+ "value": "createRootViewport(client)"
55
+ }]
56
+ }] };
57
+
58
+ //#endregion
59
+ export { TEMPLATE_PATCHES, TEMPLATE_SUBSETS };
@@ -1,3 +1,5 @@
1
+ import { materializeTemplate } from "./template.js";
2
+
1
3
  //#region src/cli/presets/nextjs.ts
2
4
  const nextjs = {
3
5
  id: "nextjs",
@@ -11,101 +13,7 @@ const nextjs = {
11
13
  baseUrlEnvKey: "NEXT_PUBLIC_KIZLO_BACKEND_URL",
12
14
  apiPath: "/api/kizlo",
13
15
  scaffolds(ctx) {
14
- const apiDir = `${ctx.appDir}/api/kizlo/[[...rest]]`;
15
- const robotsDir = `${ctx.appDir}/robots.txt`;
16
- const sitemapDir = `${ctx.appDir}/sitemaps/[sitemap]`;
17
- const manifestDir = `${ctx.appDir}/site.webmanifest`;
18
- return [
19
- {
20
- label: "Kizlo server instance",
21
- relPath: ctx.serverEntryPath,
22
- contents: `import { createKizlo } from "kizlo/nextjs/server"
23
-
24
- export const { router, client, context, handler } = createKizlo()
25
- `
26
- },
27
- {
28
- label: "Browser client",
29
- relPath: ctx.clientPath,
30
- contents: `import { createKizloClient } from "kizlo/nextjs"
31
- import { contract } from "./${ctx.serverDirName}/generated"
32
-
33
- export const client = createKizloClient(contract)
34
- `
35
- },
36
- {
37
- label: "API route",
38
- relPath: `${apiDir}/route.ts`,
39
- contents: `import { handler } from "${ctx.serverImport(apiDir)}"
40
-
41
- export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE, handler as OPTIONS }
42
- `
43
- },
44
- {
45
- label: "robots.txt route",
46
- relPath: `${robotsDir}/route.ts`,
47
- contents: `import { createRobotsRoute } from "kizlo/nextjs/server"
48
- import { client } from "${ctx.serverImport(robotsDir)}"
49
-
50
- // Run on the edge: cheaper and faster than Node for a response this small.
51
- export const runtime = "edge"
52
-
53
- // Vercel treats special metadata files like robots.txt and sitemap.xml as completely static
54
- // files at build time, bypassing normal Incremental Static Regeneration (ISR) and on-demand
55
- // revalidatePath rules.
56
- //
57
- // While your revalidation logic executes perfectly in a local Node.js server, Vercel uploads
58
- // these routes directly to its Edge CDN as immutable assets. Subsequent on-demand revalidation
59
- // triggers will successfully clear the Data Cache but fail to purge the CDN-level Edge cache
60
- // for that file.
61
- //
62
- // force-dynamic opts out of that static treatment so CMS edits show up; the WordPress call is
63
- // cached, so requests stay cheap.
64
- export const dynamic = "force-dynamic"
65
-
66
- export const GET = createRobotsRoute(client)
67
- `
68
- },
69
- {
70
- label: "sitemap route",
71
- relPath: `${sitemapDir}/route.ts`,
72
- contents: `import { createSitemapRoute } from "kizlo/nextjs/server"
73
- import { client } from "${ctx.serverImport(sitemapDir)}"
74
-
75
- // Run on the edge: cheaper and faster than Node for a response this small.
76
- export const runtime = "edge"
77
-
78
- // Like robots.txt, Vercel bakes sitemap routes into immutable Edge CDN assets at build time,
79
- // bypassing ISR and on-demand revalidatePath. The index gets frozen with whatever WordPress
80
- // returned at build (often nothing, since the self-referential backend isn't reachable yet),
81
- // and revalidation can't purge that CDN copy.
82
- //
83
- // force-dynamic opts out of that static treatment. createSitemapRoute caches the WordPress calls
84
- // per sitemap and refreshes them on content changes (via revalidateTag), so requests stay cheap.
85
- export const dynamic = "force-dynamic"
86
-
87
- export const GET = createSitemapRoute(client)
88
- `
89
- },
90
- {
91
- label: "web manifest route",
92
- relPath: `${manifestDir}/route.ts`,
93
- contents: `import { createManifestRoute } from "kizlo/nextjs/server"
94
- import { client } from "${ctx.serverImport(manifestDir)}"
95
-
96
- // Run on the edge: cheaper and faster than Node for a response this small.
97
- export const runtime = "edge"
98
-
99
- // Like robots.txt, Vercel bakes this route into an immutable Edge CDN asset at build time,
100
- // bypassing ISR and on-demand revalidatePath. force-dynamic opts out of that static treatment
101
- // so brand-settings edits show up; createManifestRoute caches the WordPress call, so requests
102
- // stay cheap.
103
- export const dynamic = "force-dynamic"
104
-
105
- export const GET = createManifestRoute(client)
106
- `
107
- }
108
- ];
16
+ return materializeTemplate("nextjs", ctx);
109
17
  }
110
18
  };
111
19