kizlo 0.8.4 → 0.9.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.
package/LICENSE CHANGED
@@ -1,21 +1,201 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Kizlo
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Kizlo
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -1,13 +1,4 @@
1
1
  //#region src/cli/banner.ts
2
- const KIZLO_CLI_WORD_MARK = [
3
- "██╗ ██╗██╗███████╗██╗ ██████╗ ",
4
- "██║ ██╔╝██║╚══███╔╝██║ ██╔═══██╗",
5
- "█████╔╝ ██║ ███╔╝ ██║ ██║ ██║",
6
- "██╔═██╗ ██║ ███╔╝ ██║ ██║ ██║",
7
- "██║ ██╗██║███████╗███████╗╚██████╔╝",
8
- "╚═╝ ╚═╝╚═╝╚══════╝╚══════╝ ╚═════╝ "
9
- ];
10
- const KIZLO_TAGLINE = "Headless WordPress framework for TypeScript";
11
2
  function colorSupported() {
12
3
  return Boolean(process.stdout.isTTY) && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb";
13
4
  }
@@ -22,16 +13,6 @@ function palette() {
22
13
  reset: on ? "\x1B[0m" : ""
23
14
  };
24
15
  }
25
- function printBanner(version) {
26
- const color = colorSupported();
27
- const cyan = color ? "\x1B[38;5;44m" : "";
28
- const bold = color ? "\x1B[1m" : "";
29
- const dim = color ? "\x1B[2m" : "";
30
- const reset = color ? "\x1B[0m" : "";
31
- const art = KIZLO_CLI_WORD_MARK.map((line) => `${cyan}${line}${reset}`).join("\n");
32
- process.stdout.write(`\n${art}\n\n`);
33
- process.stdout.write(`${dim}${KIZLO_TAGLINE}${reset} ${bold}v${version}${reset}\n\n`);
34
- }
35
16
 
36
17
  //#endregion
37
- export { palette, printBanner };
18
+ export { palette };
@@ -11,6 +11,7 @@ import path from "node:path";
11
11
  import fs from "node:fs";
12
12
  import * as p from "@clack/prompts";
13
13
  import { randomBytes } from "node:crypto";
14
+ import getPort, { portNumbers } from "get-port";
14
15
 
15
16
  //#region src/cli/commands/_setup.ts
16
17
  /** Production connection keys — what a real deploy needs, and what `.env.example` always lists. */
@@ -84,15 +85,34 @@ function withApiPath(baseUrl, apiPath) {
84
85
  return baseUrl;
85
86
  }
86
87
  }
88
+ /** The conventional starting port a scaffolded app's dev server serves from. */
89
+ const DEFAULT_APP_PORT = 3e3;
90
+ /**
91
+ * Pick a free port for a freshly scaffolded app, preferring {@link DEFAULT_APP_PORT} and stepping
92
+ * upward when it's taken. `create` builds the app URL from this and writes it into `.env`, which is
93
+ * synced to WordPress as the event/webhook delivery target — so defaulting it to a port already in use
94
+ * would hand WordPress an unreachable URL, and it fails *silently* there (events just never arrive).
95
+ * `get-port` probes every loopback family (including the IPv6 `::1` a dev server often binds on macOS),
96
+ * so it never returns a port that's really in use. `init` doesn't use this — an existing project owns
97
+ * its own port, so it asks the user instead.
98
+ */
99
+ function pickAppPort() {
100
+ return getPort({ port: portNumbers(DEFAULT_APP_PORT, DEFAULT_APP_PORT + 100) });
101
+ }
87
102
  /**
88
103
  * Collect the WordPress connection interactively: the backend/site URLs, the webhook signing
89
104
  * secret, and either the local-dev folder or a remote site's credentials. The caller layers any
90
105
  * framework-specific prompts (directory, import alias) on top of what this returns.
106
+ *
107
+ * When `baseUrl` is supplied the public-URL prompt is skipped and that value is used verbatim —
108
+ * `create` scaffolds a brand-new app that has no public URL yet, so it defaults to the local dev
109
+ * origin and lets the user fill in production later.
91
110
  */
92
- async function collectConnectionInteractively(preset) {
111
+ async function collectConnectionInteractively(preset, opts = {}) {
93
112
  let siteUrl;
94
113
  let baseUrl;
95
- if (preset.apiPath) baseUrl = orCancel(await p.text({
114
+ if (opts.baseUrl !== void 0) baseUrl = opts.baseUrl;
115
+ else if (preset.apiPath) baseUrl = orCancel(await p.text({
96
116
  message: "Public app URL",
97
117
  placeholder: "https://your-app.com",
98
118
  validate: validate(urlString)
@@ -260,10 +280,9 @@ async function setupLocalWordPress(cwd, conn) {
260
280
  conn.wpUrl = local.url;
261
281
  conn.wpUsername = local.username;
262
282
  conn.wpPassword = local.appPassword;
283
+ conn.adminPassword = local.adminPassword;
263
284
  s.stop("Local WordPress ready");
264
285
  if (local.secretSyncError) p.log.warn(`Could not sync KIZLO_DEV_SITE_SECRET to the local plugin (${local.secretSyncError})`);
265
- p.log.success(`Local WordPress configured (${conn.devPath}) — .env points at ${conn.wpUrl}`);
266
- if (local.adminPassword) p.log.warn(`wp-admin login: ${conn.wpUsername} / ${local.adminPassword} — save it, it's shown only once`);
267
286
  } catch (error) {
268
287
  s.stop("Local WordPress setup failed");
269
288
  p.cancel(error instanceof Error ? error.message : String(error));
@@ -321,26 +340,23 @@ async function syncRemote(conn) {
321
340
  });
322
341
  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.`);
323
342
  }
324
- /** The "Next steps" lines, branched on whether a local dev stack was set up. */
325
- function nextStepsLines(mode, prefix = "") {
343
+ /** The "Next steps" lines. `kizlo dev` is the single entry point for development. */
344
+ function nextStepsLines(conn, prefix = "") {
345
+ const login = conn.mode === "local" && conn.adminPassword ? [
346
+ ``,
347
+ `Log in to wp-admin (${conn.wpUrl}/wp-admin):`,
348
+ ` ${conn.wpUsername} / ${conn.adminPassword} — save it, it's shown only once`
349
+ ] : [];
326
350
  return [
327
- ...mode === "local" ? [
328
- `Start your local WordPress dev stack:`,
329
- ` ${prefix}npx kizlo dev`,
330
- ``
331
- ] : [
332
- `Watch your extensions and regenerate the contract during development:`,
333
- ` ${prefix}npx kizlo watch`,
334
- ``
335
- ],
336
- `Generate the contract once for production builds:`,
337
- ` ${prefix}npx kizlo generate`
351
+ `Start developing:`,
352
+ ` ${prefix}npx kizlo dev`,
353
+ ...login
338
354
  ];
339
355
  }
340
- /** The shared "Next steps" note, branched on whether a local dev stack was set up. */
341
- function nextStepsNote(mode, prefix = "") {
342
- p.note(nextStepsLines(mode, prefix).join("\n"), "Next steps");
356
+ /** The shared "Next steps" note. */
357
+ function nextStepsNote(conn, prefix = "") {
358
+ p.note(nextStepsLines(conn, prefix).join("\n"), "Next steps");
343
359
  }
344
360
 
345
361
  //#endregion
346
- export { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsLines, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv };
362
+ export { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsLines, nextStepsNote, orCancel, pickAppPort, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv };
@@ -1,6 +1,6 @@
1
1
  import { resolveModuleImport, writeFileIfAbsent } from "../utils.js";
2
2
  import { applyPatchToSource, patchChanged, renderPatchCode } from "../presets/patch.js";
3
- import { findRootLayout, patchEntries, resolvePatch } from "../presets/template.js";
3
+ import { findRootLayout, resolvePatch } from "../presets/template.js";
4
4
  import { orCancel } from "./_setup.js";
5
5
  import { CONTRACT_BARREL } from "../daemon/generate.js";
6
6
  import path from "node:path";
@@ -19,7 +19,7 @@ export default defineConfig({
19
19
  `;
20
20
  }
21
21
  /**
22
- * Build the {@link ScaffoldContext} that adapts a template's tokenized paths and imports to a real
22
+ * Build the {@link ScaffoldContext} that adapts a template's conventions (paths and imports) to a real
23
23
  * project. `dirRel` is the Kizlo home directory, `appDir` the App Router directory, and `alias` the
24
24
  * import-alias prefix (empty for relative imports). The server-entry import is resolved per calling
25
25
  * file so each scaffolded file references the server through the right specifier.
@@ -65,7 +65,7 @@ function reportScaffold(file, result, yes) {
65
65
  if (result === "kept") p.log.info(`Kept existing ${file.label} (${file.relPath})${yes ? " — pass --force to overwrite" : ""}`);
66
66
  else p.log.success(`${result === "overwritten" ? "Overwrote" : "Created"} ${file.label} (${file.relPath})`);
67
67
  }
68
- /** Seed the generated-contract directory so imports resolve before the first `kizlo watch`/`generate`. */
68
+ /** Seed the generated-contract directory so imports resolve before the first `kizlo dev`/`generate`. */
69
69
  function writeGeneratedContract(cwd, serverDirRel) {
70
70
  const generatedDirRel = path.join(serverDirRel, "generated");
71
71
  writeFileIfAbsent(path.join(cwd, generatedDirRel, "contract.json"), "{}\n");
@@ -78,10 +78,10 @@ function writeGeneratedContract(cwd, serverDirRel) {
78
78
  * rather than written to a guessed-at file. A confident apply is an idempotent upsert: it replaces
79
79
  * our exports if present, adds them if not.
80
80
  */
81
- function applyLayoutPatches(cwd, manifest, scaffold) {
81
+ function applyLayoutPatches(cwd, patches, conventions, scaffold) {
82
82
  const manualSteps = [];
83
- for (const entry of patchEntries(manifest)) {
84
- const resolved = resolvePatch(entry, manifest.tokens, scaffold);
83
+ for (const entry of patches) {
84
+ const resolved = resolvePatch(entry, conventions, scaffold);
85
85
  const target = findRootLayout(cwd, scaffold.appDir, resolved.relPath);
86
86
  if (!target) {
87
87
  manualSteps.push(resolved);
@@ -101,12 +101,7 @@ function applyLayoutPatches(cwd, manifest, scaffold) {
101
101
  const { text, changes } = applied;
102
102
  if (patchChanged(changes)) {
103
103
  fs.writeFileSync(target, text);
104
- const parts = [
105
- ...changes.replacedExports.length ? [`replaced ${changes.replacedExports.join(", ")}`] : [],
106
- ...changes.addedExports.length ? [`added ${changes.addedExports.join(", ")}`] : [],
107
- ...changes.addedImports.length ? ["imports"] : []
108
- ];
109
- p.log.success(`Wired Kizlo into your ${resolved.label} (${relTarget}) — ${parts.join(", ")}`);
104
+ p.log.success(`Wired Kizlo into your ${resolved.label} (${relTarget})`);
110
105
  } else p.log.info(`Your ${resolved.label} is already wired (${relTarget})`);
111
106
  }
112
107
  for (const resolved of manualSteps) p.note(renderPatchCode(resolved), `Add these to your ${resolved.label} (the layout that renders <html>)`);
@@ -1,10 +1,9 @@
1
1
  import { dockerAvailable } from "../wp/docker.js";
2
2
  import { availablePackageManagers, detectInvokingPackageManager, ensureGitignored, frameworkCreateArgs, getVersion, installArgs, runCommandAsync, runCommandCaptured } from "../utils.js";
3
- import { printBanner } from "../banner.js";
4
3
  import { getPreset } from "../presets/index.js";
5
4
  import { fetchTemplate } from "../presets/source.js";
6
- import { adaptOwnFile, ownEntries, readManifest } from "../presets/template.js";
7
- import { collectConnectionInteractively, nextStepsLines, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.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";
8
7
  import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
9
8
  import z from "zod/v4";
10
9
  import path from "node:path";
@@ -58,14 +57,16 @@ function bootstrapArgs(manifest, pm, name) {
58
57
  * Layer Kizlo's wiring onto a freshly bootstrapped app in `dir`, from an already-fetched template
59
58
  * directory + manifest. The framework CLI has produced the base project (package.json, config,
60
59
  * tsconfig, the root layout); this drives the manifest — the same engine `init` uses — to record the
61
- * `kizlo` dependency, write `kizlo.config.ts`, scaffold the manifest's own files (wiring plus the demo
62
- * starter pages), seed the generated contract, ignore `.env`, and patch the root layout's SEO exports.
63
- * The target directory layout comes straight from the manifest tokens, which the bootstrap flags are
64
- * chosen to match, so files land where the manifest expects. Fresh files are written with `force`,
65
- * silently replacing the framework defaults.
60
+ * `kizlo` dependency, write `kizlo.config.ts`, scaffold the manifest's files (the `base` plumbing and
61
+ * 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`
63
+ * writes it whole (already SEO-wired) rather than patching — `applyLayoutPatches` runs over `create`'s
64
+ * patches, which are none today. The target directory layout comes straight from the manifest's
65
+ * conventions, which the bootstrap flags are chosen to match, so files land where they expect. Fresh
66
+ * files are written with `force`, silently replacing the framework defaults.
66
67
  */
67
68
  async function applyManifestWiring(dir, templateDir, manifest, opts) {
68
- const { kizloDir, appDir, alias } = manifest.tokens;
69
+ const { kizloDir, appDir, alias } = manifest.conventions;
69
70
  const scaffold = buildScaffoldContext(dir, {
70
71
  dirRel: kizloDir,
71
72
  appDir,
@@ -73,18 +74,19 @@ async function applyManifestWiring(dir, templateDir, manifest, opts) {
73
74
  clientUrl: void 0
74
75
  });
75
76
  recordKizloDependency(dir, manifest);
77
+ const changes = changesFor(manifest, "create").filter((change) => opts.includeExamples || !isExample(change));
76
78
  const files = [{
77
79
  label: "Kizlo config",
78
80
  relPath: "kizlo.config.ts",
79
81
  contents: kizloConfigTemplate(kizloDir, alias, opts.devPath)
80
- }, ...ownEntries(manifest, { includeStarter: opts.includeStarter }).map((entry) => adaptOwnFile(templateDir, entry, manifest.tokens, scaffold))];
82
+ }, ...fileEntries(changes).map((entry) => adaptFile(templateDir, entry, manifest.conventions, scaffold))];
81
83
  for (const file of files) reportScaffold(file, await scaffoldFile(dir, file, {
82
84
  force: true,
83
85
  yes: false
84
86
  }), false);
85
87
  writeGeneratedContract(dir, path.join(kizloDir, "server"));
86
88
  ensureGitignored(dir, ".env");
87
- applyLayoutPatches(dir, manifest, scaffold);
89
+ applyLayoutPatches(dir, patchEntries(changes), manifest.conventions, scaffold);
88
90
  }
89
91
  const create = defineCommand({
90
92
  meta: {
@@ -105,8 +107,7 @@ const create = defineCommand({
105
107
  },
106
108
  async run({ args }) {
107
109
  const cwd = process.cwd();
108
- printBanner(getVersion());
109
- p.intro("kizlo create");
110
+ p.intro(`Let's create a new Kizlo application`);
110
111
  const requested = args.template;
111
112
  let template;
112
113
  if (requested) {
@@ -155,11 +156,11 @@ const create = defineCommand({
155
156
  })),
156
157
  initialValue: defaultPm
157
158
  }));
158
- const includeStarter = orCancel(await p.confirm({
159
+ const includeExamples = orCancel(await p.confirm({
159
160
  message: "Add example pages?",
160
161
  initialValue: true
161
162
  }));
162
- const conn = await collectConnectionInteractively(preset);
163
+ const conn = await collectConnectionInteractively(preset, { baseUrl: `http://localhost:${await pickAppPort()}` });
163
164
  if (preset.apiPath && conn.baseUrl) conn.baseUrl = withApiPath(conn.baseUrl, preset.apiPath);
164
165
  if (conn.mode === "local" && !await dockerAvailable()) {
165
166
  p.cancel("Docker isn't available — start Docker (or install it) and re-run, or choose “Use my own WordPress”.");
@@ -189,7 +190,7 @@ const create = defineCommand({
189
190
  try {
190
191
  await applyManifestWiring(dir, fetched.dir, manifest, {
191
192
  devPath: conn.devPath,
192
- includeStarter
193
+ includeExamples
193
194
  });
194
195
  } catch (error) {
195
196
  fail(error instanceof Error ? error.message : String(error));
@@ -216,7 +217,7 @@ const create = defineCommand({
216
217
  `cd ${name}`,
217
218
  ...depsInstalled ? [] : [`${pm} install`],
218
219
  ``,
219
- ...nextStepsLines(conn.mode)
220
+ ...nextStepsLines(conn)
220
221
  ].join("\n"), "Next steps");
221
222
  p.outro("Kizlo is ready 🎉");
222
223
  }
@@ -1,11 +1,11 @@
1
1
  import { createStack } from "../wp/docker.js";
2
- import { ensureGitignored, envGroups, getVersion, groupDefault, mergeEnv, pickStackPort, withSpinner } from "../utils.js";
3
- import { palette, printBanner } from "../banner.js";
4
- import { resolveDevConfig } from "../daemon/config.js";
2
+ import { ensureGitignored, envGroups, groupDefault, mergeEnv, pickStackPort, withSpinner } from "../utils.js";
3
+ import { hasDevStack, resolveDevConfig } from "../daemon/config.js";
5
4
  import { bootstrapDev } from "../wp/dev.js";
6
5
  import { reapOrphans, registerSession, removeProjectContainers, spawnWatchdog, unregisterSession } from "../wp/session.js";
7
6
  import { syncSiteSettings } from "../wp/settings.js";
8
7
  import { devStack } from "../wp/stack.js";
8
+ import { palette } from "../banner.js";
9
9
  import { startWatcher } from "../daemon/watch.js";
10
10
  import { join } from "node:path";
11
11
  import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -20,6 +20,18 @@ async function resolve$1(cwd) {
20
20
  stack: createStack(devStack(cfg))
21
21
  };
22
22
  }
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.
29
+ */
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.");
33
+ return false;
34
+ }
23
35
  /** A dim, untagged one-liner (no consola timestamp) for the clean dev output. */
24
36
  function note(text) {
25
37
  const { dim, reset } = palette();
@@ -157,8 +169,8 @@ function armForegroundTeardown(cfg) {
157
169
  }
158
170
  }
159
171
  /**
160
- * Boot the stack and run in the foreground until exit — the shared path behind bare
161
- * `kizlo dev` and `kizlo dev reset`. Arms teardown first (so a mid-startup cancel still
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
162
174
  * stops partial containers), shows a timed spinner, prints the summary + stop hint, then
163
175
  * parks. A referenced timer holds the event loop open — an unresolved promise alone would
164
176
  * not, so without it Node would empty its loop and exit straight after printing.
@@ -213,15 +225,39 @@ async function startForeground(cfg) {
213
225
  await new Promise(() => {});
214
226
  }
215
227
  /**
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.
232
+ */
233
+ async function watchOnly(cwd) {
234
+ const stop = await startWatcher(cwd);
235
+ if (!stop) return;
236
+ process.on("exit", stop);
237
+ for (const signal of [
238
+ "SIGINT",
239
+ "SIGTERM",
240
+ "SIGHUP"
241
+ ]) process.on(signal, () => {
242
+ stop();
243
+ process.exit(0);
244
+ });
245
+ }
246
+ /**
216
247
  * Start (or resume) the dev stack. Backs bare `kizlo dev`. Always runs in the foreground
217
248
  * and stops the stack on exit, so it never outlives the session — the subcommands
218
- * (`stop`/`down`/`reset`) manage everything else.
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.
219
251
  */
220
252
  async function bringUp() {
221
- printBanner(getVersion());
253
+ const cwd = process.cwd();
254
+ if (!await hasDevStack(cwd)) {
255
+ await watchOnly(cwd);
256
+ return;
257
+ }
222
258
  const reaped = await reapOrphans();
223
259
  if (reaped.length) note(`Stopped ${reaped.length} orphaned dev stack${reaped.length === 1 ? "" : "s"}: ${reaped.join(", ")}`);
224
- const cfg = await resolveDevConfig(process.cwd());
260
+ const cfg = await resolveDevConfig(cwd);
225
261
  if (!cfg.wordpressPath.startsWith("..")) ensureGitignored(cfg.configDir, cfg.wordpressPath);
226
262
  await startForeground(cfg);
227
263
  }
@@ -232,6 +268,7 @@ const subCommands = {
232
268
  description: "Stop the dev stack, keeping the database volume"
233
269
  },
234
270
  async run() {
271
+ if (!await requireDevStack(process.cwd())) return;
235
272
  const { stack } = await resolve$1(process.cwd());
236
273
  await withSpinner("Stopping WordPress dev stack", () => stack.composeStop(), "Stack stopped (volumes kept)");
237
274
  }
@@ -242,6 +279,7 @@ const subCommands = {
242
279
  description: "Stop and remove the dev stack containers (keeps the database and files)"
243
280
  },
244
281
  async run() {
282
+ if (!await requireDevStack(process.cwd())) return;
245
283
  const { stack } = await resolve$1(process.cwd());
246
284
  await withSpinner("Removing WordPress dev stack", () => stack.composeDown(), "Stack removed (database and files kept)");
247
285
  }
@@ -249,24 +287,24 @@ const subCommands = {
249
287
  reset: defineCommand({
250
288
  meta: {
251
289
  name: "reset",
252
- description: "Wipe the database and the install folder, then rebuild fresh"
290
+ description: "Wipe the database and the install folder so the next `kizlo dev` rebuilds fresh"
253
291
  },
254
292
  async run() {
255
- printBanner(getVersion());
293
+ if (!await requireDevStack(process.cwd())) return;
256
294
  const { cfg, stack } = await resolve$1(process.cwd());
257
- await withSpinner("Wiping WordPress dev stack", () => stack.composeDown({ volumes: true }), "Stack wiped");
295
+ await withSpinner("Wiping WordPress dev stack", () => stack.composeDown({ volumes: true }), "Local WordPress reset successfully");
258
296
  rmSync(cfg.wordpressDir, {
259
297
  recursive: true,
260
298
  force: true
261
299
  });
262
- await startForeground(cfg);
300
+ note("Run `kizlo dev` to rebuild a fresh stack.");
263
301
  }
264
302
  })
265
303
  };
266
304
  const dev = defineCommand({
267
305
  meta: {
268
306
  name: "dev",
269
- description: "Manage the local WordPress dev stack (stop | down | reset)"
307
+ description: "Run the dev stack and contract watcher (stop | down | reset)"
270
308
  },
271
309
  subCommands,
272
310
  run: groupDefault(Object.keys(subCommands), bringUp)
@@ -1,9 +1,8 @@
1
1
  import { dockerAvailable } from "../wp/docker.js";
2
2
  import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, runCommandAsync } from "../utils.js";
3
- import { printBanner } from "../banner.js";
4
3
  import { PRESETS, detectPreset, getPreset } from "../presets/index.js";
5
4
  import { fetchTemplate } from "../presets/source.js";
6
- import { adaptOwnFile, ownEntries, readManifest } from "../presets/template.js";
5
+ import { adaptFile, changesFor, fileEntries, patchEntries, readManifest } from "../presets/template.js";
7
6
  import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
8
7
  import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
9
8
  import path from "node:path";
@@ -139,10 +138,17 @@ const init = defineCommand({
139
138
  const force = Boolean(args.force);
140
139
  const cwd = process.cwd();
141
140
  const pkgPath = path.join(cwd, "package.json");
142
- if (!yes) printBanner(getVersion());
143
- p.intro("kizlo init");
141
+ p.intro(`Let's configure Kizlo in your existing application`);
144
142
  if (!fs.existsSync(pkgPath)) {
145
- p.cancel("No package.json found — run `kizlo init` inside a project.");
143
+ p.note([
144
+ "`kizlo init` adds Kizlo to an existing app, but this directory has no package.json.",
145
+ "",
146
+ "Start a new project (scaffolds an app with Kizlo already wired):",
147
+ " npx kizlo@latest create",
148
+ "",
149
+ "Already have an app? cd into it, then run `kizlo init` again."
150
+ ].join("\n"), "Nothing to configure here");
151
+ p.cancel("No package.json found in this directory.");
146
152
  process.exit(1);
147
153
  }
148
154
  const pkg = readPkg(pkgPath);
@@ -183,10 +189,6 @@ const init = defineCommand({
183
189
  });
184
190
  if (args.alias !== void 0) setup.alias = String(args.alias).trim();
185
191
  setup.alias = aliasWithSlash(setup.alias);
186
- const includeStarter = preset.template ? yes || orCancel(await p.confirm({
187
- message: "Add example pages?",
188
- initialValue: true
189
- })) : false;
190
192
  if (setup.mode === "local" && !await dockerAvailable()) {
191
193
  p.cancel("Docker isn't available — start Docker (or install it) and re-run, or choose “Use my own WordPress”.");
192
194
  process.exit(1);
@@ -226,7 +228,7 @@ const init = defineCommand({
226
228
  try {
227
229
  manifest = readManifest(fetched.dir);
228
230
  await alignKizloVersion(cwd, pm, pkg, manifest);
229
- for (const entry of ownEntries(manifest, { includeStarter })) files.push(adaptOwnFile(fetched.dir, entry, manifest.tokens, scaffold));
231
+ for (const entry of fileEntries(changesFor(manifest, "init"))) files.push(adaptFile(fetched.dir, entry, manifest.conventions, scaffold));
230
232
  } finally {
231
233
  fetched.cleanup();
232
234
  }
@@ -242,9 +244,9 @@ const init = defineCommand({
242
244
  writeGeneratedContract(cwd, serverDirRel);
243
245
  const gitignore = ensureGitignored(cwd, ".env");
244
246
  for (const { file, result } of scaffolded) reportScaffold(file, result, yes);
245
- if (manifest) applyLayoutPatches(cwd, manifest, scaffold);
247
+ if (manifest) applyLayoutPatches(cwd, patchEntries(changesFor(manifest, "init")), manifest.conventions, scaffold);
246
248
  if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
247
- nextStepsNote(setup.mode);
249
+ nextStepsNote(setup);
248
250
  p.outro("Kizlo is ready 🎉");
249
251
  }
250
252
  });
@@ -118,10 +118,20 @@ async function resolveTestConfig(cwd) {
118
118
  };
119
119
  }
120
120
  /**
121
+ * Whether a local WordPress dev stack is configured (`dev.path` is set). When false, `kizlo dev`
122
+ * has no stack to boot and runs the contract watcher alone — the path a bring-your-own-WordPress
123
+ * project takes. Loads `kizlo.config.*` from the config dir; a missing config counts as no stack.
124
+ */
125
+ async function hasDevStack(cwd) {
126
+ const fileConfig = await loadConfigFile(findConfigDir(cwd));
127
+ return Boolean(fileConfig?.dev?.path);
128
+ }
129
+ /**
121
130
  * Resolve the `dev` block from `kizlo.config.*` into concrete values for the `dev`
122
131
  * command, applying defaults (port 8080). `dev.path` is required — the whole
123
132
  * WordPress install lives there, so we make the user choose a real folder rather
124
- * than hide it under a default.
133
+ * than hide it under a default. Callers gate on {@link hasDevStack} first, since a
134
+ * project without a local stack runs the watcher alone rather than reaching here.
125
135
  */
126
136
  async function resolveDevConfig(cwd) {
127
137
  const configDir = findConfigDir(cwd);
@@ -150,4 +160,4 @@ async function resolveDevConfig(cwd) {
150
160
  }
151
161
 
152
162
  //#endregion
153
- export { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveConfig, resolveDevConfig, resolveStackName, resolveTestConfig };
163
+ export { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, hasDevStack, resolveConfig, resolveDevConfig, resolveStackName, resolveTestConfig };
@@ -37,10 +37,10 @@ async function watch(cfg) {
37
37
  /**
38
38
  * Acquire the single-instance lock, generate the contract once, and start the file
39
39
  * watcher. Returns a synchronous `stop()` that closes the watcher and releases the
40
- * lock — or `undefined` when another watcher already holds the lock (a standalone
41
- * `kizlo watch`, or a framework dev script), in which case the caller carries on
42
- * without watching. Shared by the standalone `kizlo watch` command and the
43
- * folded-in watcher of `kizlo dev`, so a single terminal covers both.
40
+ * lock — or `undefined` when another watcher already holds the lock (a framework dev
41
+ * script, or a second `kizlo dev`), in which case the caller carries on without
42
+ * watching. Used by `kizlo dev`, both when it boots a local stack and when it runs
43
+ * the watcher alone, so a single terminal covers the whole dev loop.
44
44
  */
45
45
  async function startWatcher(cwd, opts) {
46
46
  const lock = lockPath(cwd);
package/dist/cli/index.js CHANGED
@@ -12,7 +12,6 @@ runMain(defineCommand({
12
12
  subCommands: {
13
13
  create: () => import("./commands/create.js").then((m) => m.create),
14
14
  init: () => import("./commands/init.js").then((m) => m.init),
15
- watch: () => import("./commands/watch.js").then((m) => m.watch),
16
15
  generate: () => import("./commands/generate.js").then((m) => m.generate),
17
16
  dev: () => import("./commands/dev.js").then((m) => m.dev),
18
17
  test: () => import("./commands/test.js").then((m) => m.test)
@@ -5,28 +5,39 @@ import fs from "node:fs";
5
5
 
6
6
  //#region src/cli/presets/template.ts
7
7
  /**
8
- * The template's `template.json`. It is the single declaration of which files Kizlo lays onto a
9
- * project and how each is applied — the same engine drives both `create` (onto a fresh app the
10
- * framework CLI just bootstrapped) and `init` (onto the user's existing project). Every entry is one
11
- * of a small closed set — deliberately no "patch an arbitrary source file" kind, so a dangerous
12
- * strategy is unrepresentable:
8
+ * The template's `template.json`. It is the single declaration of what Kizlo lays onto a project and
9
+ * how — the same engine drives both `create` (onto a fresh app the framework CLI just bootstrapped)
10
+ * and `init` (onto the user's existing project). Changes are grouped into three sections so a command
11
+ * only ever applies what is safe for it:
13
12
  *
14
- * - `own` — a whole file Kizlo writes, adapted to the project's directories and written through the
15
- * overwrite policy. `wiring`-scope files (the default) are laid down by both commands; `starter`
16
- * demo files are laid down only by `create` (see {@link ownEntries}).
17
- * - `patch` — a partial injection into a file the framework owns (the root layout's SEO exports).
13
+ * - `base` — applied by both commands: the Kizlo plumbing (server entry, client, routes). Every
14
+ * change here must be safe on an existing project, so it is limited to files Kizlo owns, which
15
+ * never collide with the user's pages.
16
+ * - `create` — applied only by `create`, onto a freshly bootstrapped app it may freely overwrite:
17
+ * the root layout (written whole, already SEO-wired) and its styles as core wiring, plus the demo
18
+ * pages flagged `example`, which are written only when the user asks for example pages.
19
+ * - `init` — applied only by `init`, onto the user's existing project. The root-layout SEO wiring
20
+ * lands here as a `patch` so it merges into the layout the user already owns instead of replacing it.
21
+ *
22
+ * Each section is a list of changes, every one a member of a small closed set — deliberately no
23
+ * "patch an arbitrary source file" kind, so a dangerous strategy is unrepresentable:
24
+ *
25
+ * - `file` — a whole file Kizlo writes, adapted to the project's directories and written through the
26
+ * overwrite policy.
27
+ * - `patch` — a partial injection into a file the framework owns (the root layout's SEO exports):
28
+ * it adds or replaces individual modules (imports/exports) inside the file, never the whole file.
18
29
  * Both commands try a confident apply, otherwise print the payload with placement instructions.
19
30
  */
20
- const tokensSchema = z.object({
31
+ const conventionsSchema = z.object({
21
32
  kizloDir: z.string(),
22
33
  appDir: z.string(),
23
34
  alias: z.string()
24
35
  });
25
- const ownSchema = z.object({
26
- kind: z.literal("own"),
36
+ const fileSchema = z.object({
37
+ kind: z.literal("file"),
27
38
  role: z.string(),
28
39
  path: z.string(),
29
- scope: z.enum(["wiring", "starter"]).optional()
40
+ example: z.boolean().optional()
30
41
  });
31
42
  const patchSchema = z.object({
32
43
  kind: z.literal("patch"),
@@ -46,12 +57,15 @@ const bootstrapSchema = z.object({
46
57
  initializer: z.string(),
47
58
  flags: z.array(z.string()).default([])
48
59
  });
60
+ const changeSchema = z.discriminatedUnion("kind", [fileSchema, patchSchema]);
49
61
  const manifestSchema = z.object({
50
62
  framework: z.string(),
51
63
  kizloVersion: z.string().optional(),
52
64
  bootstrap: bootstrapSchema.optional(),
53
- tokens: tokensSchema,
54
- files: z.array(z.discriminatedUnion("kind", [ownSchema, patchSchema]))
65
+ conventions: conventionsSchema,
66
+ base: z.array(changeSchema).default([]),
67
+ create: z.array(changeSchema).default([]),
68
+ init: z.array(changeSchema).default([])
55
69
  });
56
70
  /** Role → the human label init uses in prompts and logs. */
57
71
  const ROLE_LABELS = {
@@ -62,6 +76,7 @@ const ROLE_LABELS = {
62
76
  sitemap: "sitemap route",
63
77
  "sitemap-redirect": "sitemap.xml redirect route",
64
78
  manifest: "web manifest route",
79
+ "root-layout": "root layout",
65
80
  "home-page": "home page",
66
81
  "blog-post": "blog post page",
67
82
  styles: "global styles"
@@ -73,55 +88,64 @@ function readManifest(templateDir) {
73
88
  return manifestSchema.parse(JSON.parse(fs.readFileSync(manifestPath, "utf8")));
74
89
  }
75
90
  /**
76
- * The `own` files to scaffold. By default only `wiring` files (the Kizlo plumbing) — this is what
77
- * `init` lays onto an existing project, so it never touches the user's own pages. With
78
- * `includeStarter`, demo `starter` files are added too, which is what `create` lays onto a fresh app.
91
+ * The changes a command applies: the shared `base` set plus the command's own section. `create` gets
92
+ * `base + create` (plumbing plus the demo pages); `init` gets `base + init` (plumbing plus any
93
+ * init-only surgical patches). `base` is applied first so a later section can override it.
79
94
  */
80
- function ownEntries(manifest, opts = {}) {
81
- return manifest.files.filter((file) => file.kind === "own" && (opts.includeStarter || file.scope !== "starter"));
95
+ function changesFor(manifest, command) {
96
+ return [...manifest.base, ...manifest[command]];
97
+ }
98
+ /** The whole-file changes in a resolved change list. */
99
+ function fileEntries(changes) {
100
+ return changes.filter((change) => change.kind === "file");
101
+ }
102
+ /** The partial-injection changes in a resolved change list. */
103
+ function patchEntries(changes) {
104
+ return changes.filter((change) => change.kind === "patch");
82
105
  }
83
- function patchEntries(manifest) {
84
- return manifest.files.filter((file) => file.kind === "patch");
106
+ /** Whether a change is opt-in demo content — written by `create` only when example pages are requested. */
107
+ function isExample(change) {
108
+ return change.kind === "file" && change.example === true;
85
109
  }
86
110
  /** The specifier the template's files use to reach the server entry, e.g. `@/lib/kizlo/server`. */
87
- function serverImportSpecifier(tokens) {
88
- const withoutSrc = tokens.kizloDir.replace(/^src\//, "");
89
- return `${tokens.alias.replace(/\/+$/, "")}/${withoutSrc}/server`;
111
+ function serverImportSpecifier(conventions) {
112
+ const withoutSrc = conventions.kizloDir.replace(/^src\//, "");
113
+ return `${conventions.alias.replace(/\/+$/, "")}/${withoutSrc}/server`;
90
114
  }
91
115
  /**
92
116
  * Adapt a template path prefix (`src/app/...`, `src/lib/kizlo/...`) to the project's real directories.
93
- * Only the leading token dir is swapped; the rest of the path is preserved verbatim.
117
+ * Only the leading convention dir is swapped; the rest of the path is preserved verbatim.
94
118
  */
95
- function adaptTemplatePath(relPath, tokens, ctx) {
119
+ function adaptTemplatePath(relPath, conventions, ctx) {
96
120
  const normalized = relPath.split(path.sep).join("/");
97
- if (normalized === tokens.appDir || normalized.startsWith(`${tokens.appDir}/`)) return `${ctx.appDir}${normalized.slice(tokens.appDir.length)}`;
98
- if (normalized === tokens.kizloDir || normalized.startsWith(`${tokens.kizloDir}/`)) return `${ctx.kizloDir}${normalized.slice(tokens.kizloDir.length)}`;
121
+ if (normalized === conventions.appDir || normalized.startsWith(`${conventions.appDir}/`)) return `${ctx.appDir}${normalized.slice(conventions.appDir.length)}`;
122
+ if (normalized === conventions.kizloDir || normalized.startsWith(`${conventions.kizloDir}/`)) return `${ctx.kizloDir}${normalized.slice(conventions.kizloDir.length)}`;
99
123
  return normalized;
100
124
  }
101
125
  /**
102
- * Turn an `own` manifest entry into the {@link ScaffoldFile} init writes: read the template's real
126
+ * Turn a `file` manifest entry into the {@link ScaffoldFile} init writes: read the template's real
103
127
  * file, rewrite its path prefix to the project's directories, and swap the server-entry import
104
128
  * specifier for the one resolved at the file's new location (tsconfig alias or relative). The swap is
105
129
  * surgical — it replaces the exact specifier the template uses, not a loose directory substring — so
106
130
  * an unrelated occurrence can't be corrupted. This is the job the old build-time extractor did; it
107
131
  * moves to runtime and stays as precise.
108
132
  */
109
- function adaptOwnFile(templateDir, entry, tokens, ctx) {
133
+ function adaptFile(templateDir, entry, conventions, ctx) {
110
134
  const abs = path.join(templateDir, entry.path);
111
135
  if (!fs.existsSync(abs)) throw new Error(`Template file "${entry.path}" is listed in the manifest but does not exist in the template.`);
112
136
  const body = fs.readFileSync(abs, "utf8");
113
- const relPath = adaptTemplatePath(entry.path, tokens, ctx);
137
+ const relPath = adaptTemplatePath(entry.path, conventions, ctx);
114
138
  const fromDir = path.posix.dirname(relPath);
115
- const contents = body.split(serverImportSpecifier(tokens)).join(ctx.serverImport(fromDir));
139
+ const contents = body.split(serverImportSpecifier(conventions)).join(ctx.serverImport(fromDir));
116
140
  return {
117
141
  label: ROLE_LABELS[entry.role] ?? entry.role,
118
142
  relPath,
119
143
  contents
120
144
  };
121
145
  }
122
- /** Substitute the path/import tokens for a patch against the project's real directories. */
123
- function resolvePatch(entry, tokens, ctx) {
124
- const relPath = adaptTemplatePath(entry.path, tokens, ctx);
146
+ /** Adapt a patch's path prefix and server-import specifier to the project's real directories. */
147
+ function resolvePatch(entry, conventions, ctx) {
148
+ const relPath = adaptTemplatePath(entry.path, conventions, ctx);
125
149
  const fromDir = path.posix.dirname(relPath);
126
150
  const imports = entry.imports.map((imp) => ({
127
151
  module: imp.module.replaceAll("{{serverImport}}", ctx.serverImport(fromDir)),
@@ -169,4 +193,4 @@ function rendersHtml(file) {
169
193
  }
170
194
 
171
195
  //#endregion
172
- export { adaptOwnFile, findRootLayout, ownEntries, patchEntries, readManifest, resolvePatch };
196
+ export { adaptFile, changesFor, fileEntries, findRootLayout, isExample, patchEntries, readManifest, resolvePatch };
package/dist/cli/utils.js CHANGED
@@ -87,7 +87,7 @@ function resolveModuleImport(cwd, targetRel, fromDirRel, alias) {
87
87
  }
88
88
  /**
89
89
  * Loads the project's `.env` files into `process.env`. The single place env
90
- * values come from — shared by `init --yes` and the `kizlo watch`/`generate`
90
+ * values come from — shared by `init --yes` and the `kizlo dev`/`generate`
91
91
  * daemon so they always read the same source.
92
92
  */
93
93
  function loadEnvFiles(cwd) {
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.8.4";
2
+ var version = "0.9.1";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/dist/router.d.ts CHANGED
@@ -764,7 +764,7 @@ declare const ROUTER_MAP: {
764
764
  exclude?: string | number | (string | number)[] | undefined;
765
765
  include?: string | number | (string | number)[] | undefined;
766
766
  order?: "asc" | "desc" | undefined;
767
- orderBy?: "id" | "slug" | "name" | "description" | "include" | "include_slugs" | "term_group" | "count" | undefined;
767
+ orderBy?: "name" | "id" | "slug" | "description" | "include" | "include_slugs" | "term_group" | "count" | undefined;
768
768
  hideEmpty?: string | boolean | undefined;
769
769
  parent?: string | number | undefined;
770
770
  post?: string | number | undefined;
@@ -976,7 +976,7 @@ declare const ROUTER_MAP: {
976
976
  exclude?: string | number | (string | number)[] | undefined;
977
977
  include?: string | number | (string | number)[] | undefined;
978
978
  order?: "asc" | "desc" | undefined;
979
- orderBy?: "id" | "slug" | "name" | "description" | "include" | "include_slugs" | "term_group" | "count" | undefined;
979
+ orderBy?: "name" | "id" | "slug" | "description" | "include" | "include_slugs" | "term_group" | "count" | undefined;
980
980
  hideEmpty?: string | boolean | undefined;
981
981
  post?: string | number | undefined;
982
982
  slug?: string | string[] | undefined;
@@ -1570,9 +1570,9 @@ declare const ROUTER_MAP: {
1570
1570
  site: {
1571
1571
  update: Procedure<"internal", {
1572
1572
  url?: string | null | undefined;
1573
- name?: string | null | undefined;
1574
1573
  backend_url?: string | null | undefined;
1575
1574
  secret?: string | null | undefined;
1575
+ name?: string | null | undefined;
1576
1576
  alternate_name?: string | null | undefined;
1577
1577
  tagline?: string | null | undefined;
1578
1578
  title_separator?: string | undefined;
@@ -1645,16 +1645,16 @@ declare const ROUTER_MAP: {
1645
1645
  update: Procedure<"internal", {
1646
1646
  key: string;
1647
1647
  data: {
1648
- webpage_type?: string | undefined;
1649
- article_type?: string | null | undefined;
1650
1648
  title_structure?: string | null | undefined;
1651
1649
  description_structure?: string | null | undefined;
1652
1650
  search_engine_visibility?: boolean | null | undefined;
1653
- breadcrumbs?: (string | number)[] | undefined;
1654
- pathname_structure?: string | null | undefined;
1651
+ webpage_type?: string | undefined;
1652
+ article_type?: string | null | undefined;
1655
1653
  seo_enabled?: boolean | undefined;
1654
+ pathname_structure?: string | null | undefined;
1656
1655
  comment_action_structure?: string | null | undefined;
1657
1656
  rest_api_enabled?: boolean | undefined;
1657
+ breadcrumbs?: (string | number)[] | undefined;
1658
1658
  };
1659
1659
  }, _kizlo_shared0.PostTypeSettings, DefinedErrorMapLike>;
1660
1660
  };
@@ -1665,10 +1665,10 @@ declare const ROUTER_MAP: {
1665
1665
  title_structure?: string | null | undefined;
1666
1666
  description_structure?: string | null | undefined;
1667
1667
  search_engine_visibility?: boolean | null | undefined;
1668
- breadcrumbs?: (string | number)[] | undefined;
1669
- pathname_structure?: string | null | undefined;
1670
1668
  seo_enabled?: boolean | undefined;
1669
+ pathname_structure?: string | null | undefined;
1671
1670
  rest_api_enabled?: boolean | undefined;
1671
+ breadcrumbs?: (string | number)[] | undefined;
1672
1672
  };
1673
1673
  }, _kizlo_shared0.TaxonomySettings, DefinedErrorMapLike>;
1674
1674
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kizlo",
3
- "version": "0.8.4",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
5
  "description": "A headless WordPress framework for TypeScript.",
6
6
  "keywords": [
@@ -14,7 +14,7 @@
14
14
  "nextjs",
15
15
  "sdk"
16
16
  ],
17
- "license": "MIT",
17
+ "license": "Apache-2.0",
18
18
  "author": "Kizlo",
19
19
  "homepage": "https://github.com/kizlo-io/kizlo/tree/main/packages/kizlo#readme",
20
20
  "bugs": "https://github.com/kizlo-io/kizlo/issues",
@@ -76,7 +76,7 @@
76
76
  },
77
77
  "dependencies": {
78
78
  "@clack/prompts": "^1.5.1",
79
- "@kizlo/shared": "0.5.0",
79
+ "@kizlo/shared": "0.5.1",
80
80
  "@orpc/client": "^1.14.3",
81
81
  "@orpc/contract": "^1.14.3",
82
82
  "@orpc/openapi": "^1.14.3",
@@ -86,6 +86,7 @@
86
86
  "citty": "^0.1.6",
87
87
  "consola": "^3.2.3",
88
88
  "cookie": "^1.0.2",
89
+ "get-port": "^7.2.0",
89
90
  "giget": "^3.0.0",
90
91
  "jiti": "^2.7.0",
91
92
  "magicast": "^0.5.3",
@@ -1,35 +0,0 @@
1
- import { getVersion } from "../utils.js";
2
- import { printBanner } from "../banner.js";
3
- import { startWatcher } from "../daemon/watch.js";
4
- import { defineCommand } from "citty";
5
-
6
- //#region src/cli/commands/watch.ts
7
- const args = { dir: {
8
- type: "string",
9
- description: "Override the Kizlo directory (defaults to kizlo.config.ts)"
10
- } };
11
- async function run({ args: args$1 }) {
12
- printBanner(getVersion());
13
- const stop = await startWatcher(process.cwd(), { dir: args$1.dir });
14
- if (!stop) return;
15
- process.on("exit", stop);
16
- for (const signal of [
17
- "SIGINT",
18
- "SIGTERM",
19
- "SIGHUP"
20
- ]) process.on(signal, () => {
21
- stop();
22
- process.exit(0);
23
- });
24
- }
25
- const watch = defineCommand({
26
- meta: {
27
- name: "watch",
28
- description: "Watch your extensions and regenerate the Kizlo contract"
29
- },
30
- args,
31
- run
32
- });
33
-
34
- //#endregion
35
- export { watch };