kizlo 0.8.2 → 0.8.3

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.
@@ -321,11 +321,11 @@ async function syncRemote(conn) {
321
321
  });
322
322
  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
323
  }
324
- /** The shared "Next steps" note, branched on whether a local dev stack was set up. */
325
- function nextStepsNote(mode, prefix = "") {
326
- p.note([
324
+ /** The "Next steps" lines, branched on whether a local dev stack was set up. */
325
+ function nextStepsLines(mode, prefix = "") {
326
+ return [
327
327
  ...mode === "local" ? [
328
- `Start your local WordPress dev stack (also watches your extensions):`,
328
+ `Start your local WordPress dev stack:`,
329
329
  ` ${prefix}npx kizlo dev`,
330
330
  ``
331
331
  ] : [
@@ -335,8 +335,12 @@ function nextStepsNote(mode, prefix = "") {
335
335
  ],
336
336
  `Generate the contract once for production builds:`,
337
337
  ` ${prefix}npx kizlo generate`
338
- ].join("\n"), "Next steps");
338
+ ];
339
+ }
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");
339
343
  }
340
344
 
341
345
  //#endregion
342
- export { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv };
346
+ export { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsLines, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv };
@@ -0,0 +1,116 @@
1
+ import { resolveModuleImport, writeFileIfAbsent } from "../utils.js";
2
+ import { applyPatchToSource, patchChanged, renderPatchCode } from "../presets/patch.js";
3
+ import { findRootLayout, patchEntries, resolvePatch } from "../presets/template.js";
4
+ import { orCancel } from "./_setup.js";
5
+ import { CONTRACT_BARREL } from "../daemon/generate.js";
6
+ import path from "node:path";
7
+ import fs from "node:fs";
8
+ import * as p from "@clack/prompts";
9
+
10
+ //#region src/cli/commands/_wiring.ts
11
+ /** The kizlo.config.ts a scaffolded project gets: the Kizlo directory, optional import alias, and —
12
+ * for a local dev setup — the WordPress install folder so `kizlo dev` knows where it lives. */
13
+ function kizloConfigTemplate(dir, alias, devPath) {
14
+ return `import { defineConfig } from "kizlo/config"
15
+
16
+ export default defineConfig({
17
+ dir: "${dir}",${alias ? `\n\talias: "${alias}",` : ""}${devPath ? `\n\tdev: { path: "${devPath}" },` : ""}
18
+ })
19
+ `;
20
+ }
21
+ /**
22
+ * Build the {@link ScaffoldContext} that adapts a template's tokenized paths and imports to a real
23
+ * project. `dirRel` is the Kizlo home directory, `appDir` the App Router directory, and `alias` the
24
+ * import-alias prefix (empty for relative imports). The server-entry import is resolved per calling
25
+ * file so each scaffolded file references the server through the right specifier.
26
+ */
27
+ function buildScaffoldContext(cwd, { dirRel, appDir, alias, clientUrl }) {
28
+ const serverDirRel = path.join(dirRel, "server");
29
+ return {
30
+ kizloDir: dirRel,
31
+ serverDirName: path.basename(serverDirRel),
32
+ serverEntryPath: path.join(serverDirRel, "index.ts"),
33
+ clientPath: path.join(dirRel, "client.ts"),
34
+ appDir,
35
+ serverImport: (fromDir) => resolveModuleImport(cwd, serverDirRel, fromDir, alias),
36
+ clientUrl
37
+ };
38
+ }
39
+ /**
40
+ * The single overwrite policy for every scaffolded file: create it when absent, overwrite on
41
+ * `--force`, and otherwise ask before clobbering an existing file (keeping it when the user
42
+ * declines or when running non-interactively with `--yes`). Every scaffolded file routes through
43
+ * here rather than deciding for itself, so new files inherit the same behavior for free.
44
+ */
45
+ async function scaffoldFile(cwd, file, opts) {
46
+ const absPath = path.join(cwd, file.relPath);
47
+ const existed = fs.existsSync(absPath);
48
+ if (existed) {
49
+ let overwrite = opts.force;
50
+ if (!opts.force && !opts.yes) {
51
+ p.log.warn(`${file.label} already exists at ${file.relPath}`);
52
+ overwrite = orCancel(await p.confirm({
53
+ message: "Overwrite it?",
54
+ initialValue: true
55
+ }));
56
+ }
57
+ if (!overwrite) return "kept";
58
+ }
59
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
60
+ fs.writeFileSync(absPath, file.contents);
61
+ return existed ? "overwritten" : "created";
62
+ }
63
+ /** Report a {@link scaffoldFile} outcome in the setup commands' usual voice. */
64
+ function reportScaffold(file, result, yes) {
65
+ if (result === "kept") p.log.info(`Kept existing ${file.label} (${file.relPath})${yes ? " — pass --force to overwrite" : ""}`);
66
+ else p.log.success(`${result === "overwritten" ? "Overwrote" : "Created"} ${file.label} (${file.relPath})`);
67
+ }
68
+ /** Seed the generated-contract directory so imports resolve before the first `kizlo watch`/`generate`. */
69
+ function writeGeneratedContract(cwd, serverDirRel) {
70
+ const generatedDirRel = path.join(serverDirRel, "generated");
71
+ writeFileIfAbsent(path.join(cwd, generatedDirRel, "contract.json"), "{}\n");
72
+ writeFileIfAbsent(path.join(cwd, generatedDirRel, "index.ts"), CONTRACT_BARREL);
73
+ }
74
+ /**
75
+ * Merge Kizlo wiring into files the project already owns (the root layout's SEO exports). Never
76
+ * aborts: the target is resolved by identity (the layout that renders `<html>`), and on any doubt —
77
+ * not found, not parseable — the resolved payload is printed at the end with placement instructions
78
+ * rather than written to a guessed-at file. A confident apply is an idempotent upsert: it replaces
79
+ * our exports if present, adds them if not.
80
+ */
81
+ function applyLayoutPatches(cwd, manifest, scaffold) {
82
+ const manualSteps = [];
83
+ for (const entry of patchEntries(manifest)) {
84
+ const resolved = resolvePatch(entry, manifest.tokens, scaffold);
85
+ const target = findRootLayout(cwd, scaffold.appDir, resolved.relPath);
86
+ if (!target) {
87
+ manualSteps.push(resolved);
88
+ p.log.info(`Couldn't find your ${resolved.label} to wire Kizlo into — see the code to add below`);
89
+ continue;
90
+ }
91
+ const relTarget = path.relative(cwd, target);
92
+ const src = fs.readFileSync(target, "utf8");
93
+ let applied;
94
+ try {
95
+ applied = applyPatchToSource(src, resolved);
96
+ } catch {
97
+ manualSteps.push(resolved);
98
+ p.log.warn(`Couldn't parse your ${resolved.label} (${relTarget}) — see the code to add below`);
99
+ continue;
100
+ }
101
+ const { text, changes } = applied;
102
+ if (patchChanged(changes)) {
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(", ")}`);
110
+ } else p.log.info(`Your ${resolved.label} is already wired (${relTarget})`);
111
+ }
112
+ for (const resolved of manualSteps) p.note(renderPatchCode(resolved), `Add these to your ${resolved.label} (the layout that renders <html>)`);
113
+ }
114
+
115
+ //#endregion
116
+ export { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract };
@@ -1,10 +1,11 @@
1
1
  import { dockerAvailable } from "../wp/docker.js";
2
- import { detectPackageManager, getVersion } from "../utils.js";
2
+ import { availablePackageManagers, detectInvokingPackageManager, ensureGitignored, frameworkCreateArgs, getVersion, installArgs, runCommandAsync, runCommandCaptured } from "../utils.js";
3
3
  import { printBanner } from "../banner.js";
4
4
  import { getPreset } from "../presets/index.js";
5
5
  import { fetchTemplate } from "../presets/source.js";
6
- import { readManifest } from "../presets/template.js";
7
- import { collectConnectionInteractively, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
6
+ import { adaptOwnFile, ownEntries, readManifest } from "../presets/template.js";
7
+ import { collectConnectionInteractively, nextStepsLines, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
8
+ import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
8
9
  import z from "zod/v4";
9
10
  import path from "node:path";
10
11
  import fs from "node:fs";
@@ -14,54 +15,77 @@ import * as p from "@clack/prompts";
14
15
  //#region src/cli/commands/create.ts
15
16
  /**
16
17
  * The templates `create` can scaffold from. Each id is both the `templates/<id>` folder and the
17
- * preset that drives its WordPress setup.
18
+ * preset that drives its WordPress setup and framework bootstrap.
18
19
  */
19
20
  const TEMPLATES = ["nextjs"];
20
21
  function isTemplate(value) {
21
22
  return TEMPLATES.includes(value);
22
23
  }
23
24
  const projectName = validate(z.string().trim().min(1, "Required").refine((value) => !value.startsWith(".") && !value.includes("/") && !value.includes("\\"), "Enter a folder name (letters, numbers, dashes)"));
24
- /** Directory entries never copied into a scaffolded project — the internal manifest and build junk. */
25
- const SKIP_COPY = new Set([
26
- "template.json",
27
- "node_modules",
28
- ".next",
29
- ".turbo",
30
- ".git"
31
- ]);
25
+ /** Package managers `create` can wire the getting-started steps for, in display order. */
26
+ const PACKAGE_MANAGERS = [
27
+ "pnpm",
28
+ "npm",
29
+ "yarn",
30
+ "bun"
31
+ ];
32
32
  /**
33
- * Copy the template into `dir`, then make it a standalone project: name the package after the
34
- * project folder and resolve the monorepo-only `workspace:*` Kizlo dependency to the version the
35
- * template declares (`kizloVersion` in its manifest, stamped at release), falling back to the running
36
- * CLI's version. The version the project runs is the template's to decide, not a moving `latest` tag.
37
- * The template's `template.json` is an internal input, so it's left out of the copy — as is
38
- * local build junk (`node_modules`, `.next`, …) that only exists on the `KIZLO_TEMPLATE_LOCAL_DIR` path.
33
+ * Pin the `kizlo` dependency in the freshly bootstrapped `package.json`. The template is
34
+ * authoritative for the version (`kizloVersion` in its manifest, stamped at release), falling back to
35
+ * the running CLI's version in-repo before the first stamp. The dependency is only recorded, not
36
+ * installed — the getting-started note tells the user to run install.
39
37
  */
40
- async function scaffoldTemplate(template, dir, name) {
41
- const { dir: src, cleanup } = await fetchTemplate(template);
42
- let kizloVersion;
43
- try {
44
- kizloVersion = readManifest(src).kizloVersion ?? `^${getVersion()}`;
45
- fs.cpSync(src, dir, {
46
- recursive: true,
47
- filter: (from) => !SKIP_COPY.has(path.basename(from))
48
- });
49
- } finally {
50
- cleanup();
51
- }
38
+ function recordKizloDependency(dir, manifest) {
52
39
  const pkgPath = path.join(dir, "package.json");
53
- if (!fs.existsSync(pkgPath)) {
54
- fs.rmSync(dir, {
55
- recursive: true,
56
- force: true
57
- });
58
- throw new Error(`Template "${template}" wasn't found. Check the template name and your network connection, then try again.`);
59
- }
60
40
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
61
- pkg.name = name;
62
- if (pkg.dependencies?.kizlo === "workspace:*") pkg.dependencies.kizlo = kizloVersion;
41
+ pkg.dependencies ??= {};
42
+ pkg.dependencies.kizlo = manifest.kizloVersion ?? `^${getVersion()}`;
63
43
  fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
64
44
  }
45
+ /**
46
+ * The argv `create` runs to bootstrap a fresh base app with the framework's own official CLI, built
47
+ * from the template manifest's `bootstrap` (initializer + flags) — the template is the single source
48
+ * of truth for the framework. The CLI owns only the package-manager mechanics: {@link frameworkCreateArgs}
49
+ * assembles the `<pm> create …` invocation, and the `{{pm}}` token in the flags is substituted with the
50
+ * chosen manager (e.g. `--use-{{pm}}` → `--use-pnpm`).
51
+ */
52
+ function bootstrapArgs(manifest, pm, name) {
53
+ if (!manifest.bootstrap) return void 0;
54
+ const flags = manifest.bootstrap.flags.map((flag) => flag.replaceAll("{{pm}}", pm));
55
+ return frameworkCreateArgs(pm, manifest.bootstrap.initializer, name, flags);
56
+ }
57
+ /**
58
+ * Layer Kizlo's wiring onto a freshly bootstrapped app in `dir`, from an already-fetched template
59
+ * directory + manifest. The framework CLI has produced the base project (package.json, config,
60
+ * 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.
66
+ */
67
+ async function applyManifestWiring(dir, templateDir, manifest, opts) {
68
+ const { kizloDir, appDir, alias } = manifest.tokens;
69
+ const scaffold = buildScaffoldContext(dir, {
70
+ dirRel: kizloDir,
71
+ appDir,
72
+ alias,
73
+ clientUrl: void 0
74
+ });
75
+ recordKizloDependency(dir, manifest);
76
+ const files = [{
77
+ label: "Kizlo config",
78
+ relPath: "kizlo.config.ts",
79
+ contents: kizloConfigTemplate(kizloDir, alias, opts.devPath)
80
+ }, ...ownEntries(manifest, { includeStarter: true }).map((entry) => adaptOwnFile(templateDir, entry, manifest.tokens, scaffold))];
81
+ for (const file of files) reportScaffold(file, await scaffoldFile(dir, file, {
82
+ force: true,
83
+ yes: false
84
+ }), false);
85
+ writeGeneratedContract(dir, path.join(kizloDir, "server"));
86
+ ensureGitignored(dir, ".env");
87
+ applyLayoutPatches(dir, manifest, scaffold);
88
+ }
65
89
  const create = defineCommand({
66
90
  meta: {
67
91
  name: "create",
@@ -120,21 +144,60 @@ const create = defineCommand({
120
144
  p.cancel(`No preset for template "${template}".`);
121
145
  process.exit(1);
122
146
  }
147
+ const installed = availablePackageManagers(PACKAGE_MANAGERS);
148
+ const invokingPm = detectInvokingPackageManager();
149
+ const defaultPm = invokingPm && installed.includes(invokingPm) ? invokingPm : installed[0];
150
+ const pm = orCancel(await p.select({
151
+ message: "Package manager",
152
+ options: installed.map((id) => ({
153
+ value: id,
154
+ label: id
155
+ })),
156
+ initialValue: defaultPm
157
+ }));
123
158
  const conn = await collectConnectionInteractively(preset);
124
159
  if (preset.apiPath && conn.baseUrl) conn.baseUrl = withApiPath(conn.baseUrl, preset.apiPath);
125
160
  if (conn.mode === "local" && !await dockerAvailable()) {
126
161
  p.cancel("Docker isn't available — start Docker (or install it) and re-run, or choose “Use my own WordPress”.");
127
162
  process.exit(1);
128
163
  }
164
+ const fetched = await fetchTemplate(template);
165
+ const fail = (message) => {
166
+ fetched.cleanup();
167
+ p.cancel(message);
168
+ process.exit(1);
169
+ };
170
+ const manifest = readManifest(fetched.dir);
171
+ const bootstrap = bootstrapArgs(manifest, pm, name);
172
+ if (!bootstrap) {
173
+ fetched.cleanup();
174
+ p.cancel(`Template "${template}" can't be scaffolded — its manifest declares no framework bootstrap.`);
175
+ process.exit(1);
176
+ }
129
177
  const s = p.spinner();
130
- s.start(`Creating ${name} from the ${template} template`);
178
+ s.start(`Creating ${name} with the ${preset.label} CLI`);
179
+ const scaffold = runCommandCaptured(bootstrap, cwd);
180
+ s.stop(scaffold.ok ? `Created ${name} with the ${preset.label} CLI` : `${preset.label} setup failed`);
181
+ if (!scaffold.ok) {
182
+ if (scaffold.output) p.log.error(scaffold.output);
183
+ fail(`${preset.label} setup failed — see the output above and try again.`);
184
+ }
131
185
  try {
132
- await scaffoldTemplate(template, dir, name);
133
- s.stop(`Created ${name}`);
186
+ await applyManifestWiring(dir, fetched.dir, manifest, { devPath: conn.devPath });
134
187
  } catch (error) {
135
- s.stop("Could not create the project");
136
- p.cancel(error instanceof Error ? error.message : String(error));
137
- process.exit(1);
188
+ fail(error instanceof Error ? error.message : String(error));
189
+ }
190
+ fetched.cleanup();
191
+ let depsInstalled = false;
192
+ if (orCancel(await p.confirm({
193
+ message: "Install dependencies now?",
194
+ initialValue: true
195
+ }))) {
196
+ const is = p.spinner();
197
+ is.start(`Installing dependencies with ${pm}`);
198
+ depsInstalled = await runCommandAsync(installArgs(pm), dir, "ignore");
199
+ is.stop(depsInstalled ? "Installed dependencies" : "Could not install dependencies");
200
+ if (!depsInstalled) p.log.warn(`Install them yourself: cd ${name} && ${installArgs(pm).join(" ")}`);
138
201
  }
139
202
  await setupLocalWordPress(dir, conn);
140
203
  await writeEnv(dir, preset, conn, {
@@ -142,9 +205,12 @@ const create = defineCommand({
142
205
  yes: false
143
206
  });
144
207
  await syncRemote(conn);
145
- const pm = detectPackageManager(dir);
146
- p.note([`cd ${name}`, `${pm} install`].join("\n"), "Get started");
147
- nextStepsNote(conn.mode);
208
+ p.note([
209
+ `cd ${name}`,
210
+ ...depsInstalled ? [] : [`${pm} install`],
211
+ ``,
212
+ ...nextStepsLines(conn.mode)
213
+ ].join("\n"), "Next steps");
148
214
  p.outro("Kizlo is ready 🎉");
149
215
  }
150
216
  });
@@ -1,12 +1,11 @@
1
1
  import { dockerAvailable } from "../wp/docker.js";
2
- import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, resolveModuleImport, runCommand, writeFileIfAbsent } from "../utils.js";
2
+ import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, runCommand } from "../utils.js";
3
3
  import { printBanner } from "../banner.js";
4
4
  import { PRESETS, detectPreset, getPreset } from "../presets/index.js";
5
5
  import { fetchTemplate } from "../presets/source.js";
6
- import { applyPatchToSource, patchChanged, renderPatchCode } from "../presets/patch.js";
7
- import { adaptOwnFile, findRootLayout, ownEntries, patchEntries, readManifest, resolvePatch } from "../presets/template.js";
6
+ import { adaptOwnFile, ownEntries, readManifest } from "../presets/template.js";
8
7
  import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
9
- import { CONTRACT_BARREL } from "../daemon/generate.js";
8
+ import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
10
9
  import path from "node:path";
11
10
  import fs from "node:fs";
12
11
  import { defineCommand } from "citty";
@@ -20,14 +19,6 @@ function defaultDir(hasSrcDir) {
20
19
  function aliasWithSlash(alias) {
21
20
  return alias ? `${alias.replace(/\/+$/, "")}/` : "";
22
21
  }
23
- function kizloConfigTemplate(dir, alias, devPath) {
24
- return `import { defineConfig } from "kizlo/config"
25
-
26
- export default defineConfig({
27
- dir: "${dir}",${alias ? `\n\talias: "${alias}",` : ""}${devPath ? `\n\tdev: { path: "${devPath}" },` : ""}
28
- })
29
- `;
30
- }
31
22
  /** Whether two URLs share an origin (scheme + host + port); false when either can't be parsed. */
32
23
  function sameOrigin(a, b) {
33
24
  try {
@@ -41,35 +32,6 @@ function detectAppDir(cwd, hasSrcDir) {
41
32
  if (fs.existsSync(path.join(cwd, "app"))) return "app";
42
33
  return hasSrcDir ? "src/app" : "app";
43
34
  }
44
- /**
45
- * The single overwrite policy for every scaffolded file: create it when absent, overwrite on
46
- * `--force`, and otherwise ask before clobbering an existing file (keeping it when the user
47
- * declines or when running non-interactively with `--yes`). Every scaffolded file routes through
48
- * here rather than deciding for itself, so new files inherit the same behavior for free.
49
- */
50
- async function scaffoldFile(cwd, file, opts) {
51
- const absPath = path.join(cwd, file.relPath);
52
- const existed = fs.existsSync(absPath);
53
- if (existed) {
54
- let overwrite = opts.force;
55
- if (!opts.force && !opts.yes) {
56
- p.log.warn(`${file.label} already exists at ${file.relPath}`);
57
- overwrite = orCancel(await p.confirm({
58
- message: "Overwrite it?",
59
- initialValue: true
60
- }));
61
- }
62
- if (!overwrite) return "kept";
63
- }
64
- fs.mkdirSync(path.dirname(absPath), { recursive: true });
65
- fs.writeFileSync(absPath, file.contents);
66
- return existed ? "overwritten" : "created";
67
- }
68
- /** Report a {@link scaffoldFile} outcome in init's usual voice. */
69
- function reportScaffold(file, result, yes) {
70
- if (result === "kept") p.log.info(`Kept existing ${file.label} (${file.relPath})${yes ? " — pass --force to overwrite" : ""}`);
71
- else p.log.success(`${result === "overwritten" ? "Overwrote" : "Created"} ${file.label} (${file.relPath})`);
72
- }
73
35
  async function collectInteractively(ctx) {
74
36
  const conn = await collectConnectionInteractively(ctx.preset);
75
37
  const dir = orCancel(await p.text({
@@ -243,15 +205,12 @@ const init = defineCommand({
243
205
  const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
244
206
  const serverDirRel = path.join(dirRel, "server");
245
207
  const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
246
- const scaffold = {
247
- kizloDir: dirRel,
248
- serverDirName: path.basename(serverDirRel),
249
- serverEntryPath: path.join(serverDirRel, "index.ts"),
250
- clientPath: path.join(dirRel, "client.ts"),
208
+ const scaffold = buildScaffoldContext(cwd, {
209
+ dirRel,
251
210
  appDir: detectAppDir(cwd, hasSrcDir),
252
- serverImport: (fromDir) => resolveModuleImport(cwd, serverDirRel, fromDir, setup.alias),
211
+ alias: setup.alias,
253
212
  clientUrl
254
- };
213
+ });
255
214
  const files = [{
256
215
  label: "Kizlo config",
257
216
  relPath: "kizlo.config.ts",
@@ -276,42 +235,10 @@ const init = defineCommand({
276
235
  yes
277
236
  })
278
237
  });
279
- const generatedDirRel = path.join(serverDirRel, "generated");
280
- writeFileIfAbsent(path.join(cwd, generatedDirRel, "contract.json"), "{}\n");
281
- writeFileIfAbsent(path.join(cwd, generatedDirRel, "index.ts"), CONTRACT_BARREL);
238
+ writeGeneratedContract(cwd, serverDirRel);
282
239
  const gitignore = ensureGitignored(cwd, ".env");
283
240
  for (const { file, result } of scaffolded) reportScaffold(file, result, yes);
284
- const manualSteps = [];
285
- for (const entry of manifest ? patchEntries(manifest) : []) {
286
- const resolved = resolvePatch(entry, manifest.tokens, scaffold);
287
- const target = findRootLayout(cwd, scaffold.appDir, resolved.relPath);
288
- if (!target) {
289
- manualSteps.push(resolved);
290
- p.log.info(`Couldn't find your ${resolved.label} to wire Kizlo into — see the code to add below`);
291
- continue;
292
- }
293
- const relTarget = path.relative(cwd, target);
294
- const src = fs.readFileSync(target, "utf8");
295
- let applied;
296
- try {
297
- applied = applyPatchToSource(src, resolved);
298
- } catch {
299
- manualSteps.push(resolved);
300
- p.log.warn(`Couldn't parse your ${resolved.label} (${relTarget}) — see the code to add below`);
301
- continue;
302
- }
303
- const { text, changes } = applied;
304
- if (patchChanged(changes)) {
305
- fs.writeFileSync(target, text);
306
- const parts = [
307
- ...changes.replacedExports.length ? [`replaced ${changes.replacedExports.join(", ")}`] : [],
308
- ...changes.addedExports.length ? [`added ${changes.addedExports.join(", ")}`] : [],
309
- ...changes.addedImports.length ? ["imports"] : []
310
- ];
311
- p.log.success(`Wired Kizlo into your ${resolved.label} (${relTarget}) — ${parts.join(", ")}`);
312
- } else p.log.info(`Your ${resolved.label} is already wired (${relTarget})`);
313
- }
314
- for (const resolved of manualSteps) p.note(renderPatchCode(resolved), `Add these to your ${resolved.label} (the layout that renders <html>)`);
241
+ if (manifest) applyLayoutPatches(cwd, manifest, scaffold);
315
242
  if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
316
243
  nextStepsNote(setup.mode);
317
244
  p.outro("Kizlo is ready 🎉");
@@ -5,15 +5,17 @@ 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 are Kizlo wiring
9
- * and how each is applied. Every entry is one of a small closed set — deliberately no "patch an
10
- * arbitrary source file" kind, so a dangerous strategy is unrepresentable:
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:
11
13
  *
12
- * - `own` — a whole file Kizlo writes. Its path is authoritative; `create` copies it and `init`
13
- * adapts + writes it through the overwrite policy.
14
- * - `patch` — a partial injection into a file the user owns (the root layout's SEO exports). `init`
15
- * tries a confident apply, otherwise prints the payload with placement instructions. `create` never
16
- * patches — its own template files already carry the wiring.
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).
18
+ * Both commands try a confident apply, otherwise print the payload with placement instructions.
17
19
  */
18
20
  const tokensSchema = z.object({
19
21
  kizloDir: z.string(),
@@ -23,7 +25,8 @@ const tokensSchema = z.object({
23
25
  const ownSchema = z.object({
24
26
  kind: z.literal("own"),
25
27
  role: z.string(),
26
- path: z.string()
28
+ path: z.string(),
29
+ scope: z.enum(["wiring", "starter"]).optional()
27
30
  });
28
31
  const patchSchema = z.object({
29
32
  kind: z.literal("patch"),
@@ -39,9 +42,14 @@ const patchSchema = z.object({
39
42
  value: z.string()
40
43
  }))
41
44
  });
45
+ const bootstrapSchema = z.object({
46
+ initializer: z.string(),
47
+ flags: z.array(z.string()).default([])
48
+ });
42
49
  const manifestSchema = z.object({
43
50
  framework: z.string(),
44
51
  kizloVersion: z.string().optional(),
52
+ bootstrap: bootstrapSchema.optional(),
45
53
  tokens: tokensSchema,
46
54
  files: z.array(z.discriminatedUnion("kind", [ownSchema, patchSchema]))
47
55
  });
@@ -53,7 +61,10 @@ const ROLE_LABELS = {
53
61
  robots: "robots.txt route",
54
62
  sitemap: "sitemap route",
55
63
  "sitemap-redirect": "sitemap.xml redirect route",
56
- manifest: "web manifest route"
64
+ manifest: "web manifest route",
65
+ "home-page": "home page",
66
+ "blog-post": "blog post page",
67
+ styles: "global styles"
57
68
  };
58
69
  /** Read and validate the template's manifest from a fetched template directory. */
59
70
  function readManifest(templateDir) {
@@ -61,8 +72,13 @@ function readManifest(templateDir) {
61
72
  if (!fs.existsSync(manifestPath)) throw new Error(`Template manifest not found at ${manifestPath}`);
62
73
  return manifestSchema.parse(JSON.parse(fs.readFileSync(manifestPath, "utf8")));
63
74
  }
64
- function ownEntries(manifest) {
65
- return manifest.files.filter((file) => file.kind === "own");
75
+ /**
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.
79
+ */
80
+ function ownEntries(manifest, opts = {}) {
81
+ return manifest.files.filter((file) => file.kind === "own" && (opts.includeStarter || file.scope !== "starter"));
66
82
  }
67
83
  function patchEntries(manifest) {
68
84
  return manifest.files.filter((file) => file.kind === "patch");
package/dist/cli/utils.js CHANGED
@@ -2,7 +2,7 @@ import { log } from "./daemon/logger.js";
2
2
  import { PortInUseError, resolveHostPort } from "./wp/ports.js";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { spawnSync } from "node:child_process";
5
+ import { spawn, spawnSync } from "node:child_process";
6
6
  import fs from "node:fs";
7
7
  import * as p from "@clack/prompts";
8
8
 
@@ -107,6 +107,31 @@ function detectPackageManager(cwd) {
107
107
  if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) return "bun";
108
108
  return "npm";
109
109
  }
110
+ /**
111
+ * The package manager that invoked the current process, read from `npm_config_user_agent`
112
+ * (e.g. `pnpm/9.0.0 npm/? node/v20`). Used to pre-select the most likely choice when a fresh
113
+ * project has no lockfile to detect from. Returns undefined when the agent is absent or unknown.
114
+ */
115
+ function detectInvokingPackageManager() {
116
+ const name = process.env.npm_config_user_agent?.split("/")[0];
117
+ return name === "pnpm" || name === "yarn" || name === "bun" || name === "npm" ? name : void 0;
118
+ }
119
+ /**
120
+ * Whether `command` resolves to a runnable executable, probed with `--version`. Used to hide or
121
+ * disable package-manager choices the user can't actually run. A non-zero exit or a spawn error
122
+ * (the binary isn't on PATH) both count as unavailable.
123
+ */
124
+ function isCommandAvailable(command) {
125
+ const result = spawnSync(command, ["--version"], {
126
+ stdio: "ignore",
127
+ shell: process.platform === "win32"
128
+ });
129
+ return !result.error && result.status === 0;
130
+ }
131
+ /** The package managers currently installed on the host, in the given display order. */
132
+ function availablePackageManagers(order) {
133
+ return order.filter((pm) => isCommandAvailable(pm));
134
+ }
110
135
  function addDependencyArgs(pm, pkg) {
111
136
  if (pm === "npm") return [
112
137
  "npm",
@@ -119,6 +144,42 @@ function addDependencyArgs(pm, pkg) {
119
144
  pkg
120
145
  ];
121
146
  }
147
+ /** The argv to install a project's dependencies with `pm` — every supported manager spells it `<pm> install`. */
148
+ function installArgs(pm) {
149
+ return [pm, "install"];
150
+ }
151
+ /**
152
+ * The argv to run a framework's `create-*` initializer through a package manager, e.g.
153
+ * `pnpm create next-app@latest my-app --ts …`. The mechanics differ per manager: npm forwards
154
+ * flags to the initializer only after a `--` separator, and `yarn create` resolves the latest
155
+ * version itself so the `@latest` tag is dropped. `flags` are the initializer's own options.
156
+ */
157
+ function frameworkCreateArgs(pm, initializer, name, flags) {
158
+ switch (pm) {
159
+ case "npm": return [
160
+ "npm",
161
+ "create",
162
+ initializer,
163
+ name,
164
+ "--",
165
+ ...flags
166
+ ];
167
+ case "yarn": return [
168
+ "yarn",
169
+ "create",
170
+ initializer.replace(/@latest$/, ""),
171
+ name,
172
+ ...flags
173
+ ];
174
+ default: return [
175
+ pm,
176
+ "create",
177
+ initializer,
178
+ name,
179
+ ...flags
180
+ ];
181
+ }
182
+ }
122
183
  function runCommand(args, cwd, stdio = "inherit") {
123
184
  const [command, ...rest] = args;
124
185
  if (!command) return false;
@@ -128,6 +189,45 @@ function runCommand(args, cwd, stdio = "inherit") {
128
189
  shell: process.platform === "win32"
129
190
  }).status === 0;
130
191
  }
192
+ /**
193
+ * Async sibling of {@link runCommand}: runs a command without blocking the event loop, so a clack
194
+ * spinner can keep animating while it works (a synchronous `spawnSync` would freeze it on the first
195
+ * frame). Resolves whether it exited cleanly; a spawn error (binary off PATH) resolves false.
196
+ */
197
+ function runCommandAsync(args, cwd, stdio = "inherit") {
198
+ const [command, ...rest] = args;
199
+ if (!command) return Promise.resolve(false);
200
+ return new Promise((resolve$1) => {
201
+ const child = spawn(command, rest, {
202
+ cwd,
203
+ stdio,
204
+ shell: process.platform === "win32"
205
+ });
206
+ child.on("error", () => resolve$1(false));
207
+ child.on("close", (code) => resolve$1(code === 0));
208
+ });
209
+ }
210
+ /**
211
+ * Run a command capturing its combined output instead of streaming it, for a step that should stay
212
+ * quiet on success but whose logs are worth surfacing when it fails (e.g. the framework's scaffolder
213
+ * behind a spinner). Returns whether it succeeded and the trimmed stdout+stderr.
214
+ */
215
+ function runCommandCaptured(args, cwd) {
216
+ const [command, ...rest] = args;
217
+ if (!command) return {
218
+ ok: false,
219
+ output: ""
220
+ };
221
+ const result = spawnSync(command, rest, {
222
+ cwd,
223
+ encoding: "utf8",
224
+ shell: process.platform === "win32"
225
+ });
226
+ return {
227
+ ok: result.status === 0,
228
+ output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
229
+ };
230
+ }
131
231
  function writeFileIfAbsent(filePath, contents) {
132
232
  if (fs.existsSync(filePath)) return false;
133
233
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
@@ -263,4 +363,4 @@ function groupDefault(subCommandNames, run) {
263
363
  }
264
364
 
265
365
  //#endregion
266
- export { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, envGroups, envKeysPresent, getVersion, groupDefault, loadEnvFiles, mergeEnv, pickStackPort, resolveModuleImport, runCommand, withSpinner, writeFileIfAbsent };
366
+ export { addDependencyArgs, availablePackageManagers, detectImportAlias, detectInvokingPackageManager, detectPackageManager, ensureGitignored, envGroups, envKeysPresent, frameworkCreateArgs, getVersion, groupDefault, installArgs, loadEnvFiles, mergeEnv, pickStackPort, resolveModuleImport, runCommand, runCommandAsync, runCommandCaptured, withSpinner, writeFileIfAbsent };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.8.2";
2
+ var version = "0.8.3";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/dist/router.d.ts CHANGED
@@ -179,7 +179,7 @@ declare const ROUTER_MAP: {
179
179
  include?: string | number | (string | number)[] | undefined;
180
180
  offset?: string | number | undefined;
181
181
  order?: "asc" | "desc" | undefined;
182
- orderby?: "id" | "date" | "slug" | "parent" | "title" | "author" | "include" | "modified" | "relevance" | "include_slugs" | undefined;
182
+ orderby?: "date" | "id" | "slug" | "parent" | "title" | "author" | "include" | "modified" | "relevance" | "include_slugs" | undefined;
183
183
  searchColumns?: ("post_title" | "post_content" | "post_excerpt")[] | undefined;
184
184
  slug?: string | string[] | undefined;
185
185
  taxRelation?: "AND" | "OR" | undefined;
@@ -507,7 +507,7 @@ declare const ROUTER_MAP: {
507
507
  include?: string | number | (string | number)[] | undefined;
508
508
  offset?: string | number | undefined;
509
509
  order?: "asc" | "desc" | undefined;
510
- orderby?: "id" | "date" | "slug" | "parent" | "title" | "author" | "include" | "modified" | "relevance" | "include_slugs" | "menu_order" | undefined;
510
+ orderby?: "date" | "id" | "slug" | "parent" | "title" | "author" | "include" | "modified" | "relevance" | "include_slugs" | "menu_order" | undefined;
511
511
  searchColumns?: ("post_title" | "post_content" | "post_excerpt")[] | undefined;
512
512
  slug?: string | string[] | undefined;
513
513
  parent?: string | number | (string | number)[] | undefined;
@@ -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?: "count" | "id" | "slug" | "name" | "description" | "include" | "include_slugs" | "term_group" | 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?: "count" | "id" | "slug" | "name" | "description" | "include" | "include_slugs" | "term_group" | undefined;
980
980
  hideEmpty?: string | boolean | undefined;
981
981
  post?: string | number | undefined;
982
982
  slug?: string | string[] | undefined;
@@ -1135,7 +1135,7 @@ declare const ROUTER_MAP: {
1135
1135
  sitemaps: {
1136
1136
  list: Procedure<"internal", {}, {
1137
1137
  key: string;
1138
- type: "author" | "post_type" | "taxonomy";
1138
+ type: "post_type" | "taxonomy" | "author";
1139
1139
  pages: number;
1140
1140
  lastmod: string;
1141
1141
  }[], DefinedErrorMapLike>;
@@ -1143,7 +1143,7 @@ declare const ROUTER_MAP: {
1143
1143
  origin: string;
1144
1144
  sitemaps: {
1145
1145
  key: string;
1146
- type: "author" | "post_type" | "taxonomy";
1146
+ type: "post_type" | "taxonomy" | "author";
1147
1147
  pages: number;
1148
1148
  lastmod: string;
1149
1149
  }[];
@@ -1186,7 +1186,7 @@ declare const ROUTER_MAP: {
1186
1186
  include?: string | number | (string | number)[] | undefined;
1187
1187
  offset?: string | number | undefined;
1188
1188
  order?: "asc" | "desc" | undefined;
1189
- orderby?: "id" | "date" | "slug" | "parent" | "title" | "author" | "include" | "relevance" | "include_slugs" | "menu_order" | undefined;
1189
+ orderby?: "date" | "id" | "slug" | "parent" | "title" | "author" | "include" | "relevance" | "include_slugs" | "menu_order" | undefined;
1190
1190
  searchColumns?: ("post_title" | "post_content" | "post_excerpt")[] | undefined;
1191
1191
  slug?: string | string[] | undefined;
1192
1192
  taxRelation?: "AND" | "OR" | undefined;
@@ -1265,7 +1265,7 @@ declare const ROUTER_MAP: {
1265
1265
  include?: string | number | (string | number)[] | undefined;
1266
1266
  offset?: string | number | undefined;
1267
1267
  order?: "asc" | "desc" | undefined;
1268
- orderby?: "id" | "date" | "slug" | "parent" | "title" | "author" | "include" | "relevance" | "include_slugs" | "menu_order" | undefined;
1268
+ orderby?: "date" | "id" | "slug" | "parent" | "title" | "author" | "include" | "relevance" | "include_slugs" | "menu_order" | undefined;
1269
1269
  searchColumns?: ("post_title" | "post_content" | "post_excerpt")[] | undefined;
1270
1270
  slug?: string | string[] | undefined;
1271
1271
  taxRelation?: "AND" | "OR" | undefined;
@@ -1414,7 +1414,7 @@ declare const ROUTER_MAP: {
1414
1414
  include?: string | number | (string | number)[] | undefined;
1415
1415
  offset?: string | number | undefined;
1416
1416
  order?: "asc" | "desc" | undefined;
1417
- orderby?: "id" | "date" | "parent" | "post" | "include" | undefined;
1417
+ orderby?: "date" | "id" | "parent" | "post" | "include" | undefined;
1418
1418
  parent?: string | number | (string | number)[] | undefined;
1419
1419
  parentExclude?: string | number | (string | number)[] | undefined;
1420
1420
  post?: string | number | (string | number)[] | undefined;
@@ -108,17 +108,17 @@ declare const Seo: z.ZodObject<{
108
108
  type Seo = z.infer<typeof Seo>;
109
109
  declare const CONTENT_TYPES: readonly ["post_type", "taxonomy", "author"];
110
110
  declare const ContentType: z.ZodEnum<{
111
- author: "author";
112
111
  post_type: "post_type";
113
112
  taxonomy: "taxonomy";
113
+ author: "author";
114
114
  }>;
115
115
  type ContentType = z.infer<typeof ContentType>;
116
116
  declare const Sitemap: z.ZodObject<{
117
117
  key: z.ZodString;
118
118
  type: z.ZodEnum<{
119
- author: "author";
120
119
  post_type: "post_type";
121
120
  taxonomy: "taxonomy";
121
+ author: "author";
122
122
  }>;
123
123
  pages: z.ZodNumber;
124
124
  lastmod: z.ZodString;
@@ -127,9 +127,9 @@ type Sitemap = z.infer<typeof Sitemap>;
127
127
  declare const SitemapList: z.ZodArray<z.ZodObject<{
128
128
  key: z.ZodString;
129
129
  type: z.ZodEnum<{
130
- author: "author";
131
130
  post_type: "post_type";
132
131
  taxonomy: "taxonomy";
132
+ author: "author";
133
133
  }>;
134
134
  pages: z.ZodNumber;
135
135
  lastmod: z.ZodString;
@@ -140,9 +140,9 @@ declare const SitemapIndex: z.ZodObject<{
140
140
  sitemaps: z.ZodArray<z.ZodObject<{
141
141
  key: z.ZodString;
142
142
  type: z.ZodEnum<{
143
- author: "author";
144
143
  post_type: "post_type";
145
144
  taxonomy: "taxonomy";
145
+ author: "author";
146
146
  }>;
147
147
  pages: z.ZodNumber;
148
148
  lastmod: z.ZodString;
@@ -174,9 +174,9 @@ declare const SitemapUrlList: z.ZodArray<z.ZodObject<{
174
174
  type SitemapUrlList = z.infer<typeof SitemapUrlList>;
175
175
  declare const ListSitemapUrlInputBase: z.ZodObject<{
176
176
  type: z.ZodEnum<{
177
- author: "author";
178
177
  post_type: "post_type";
179
178
  taxonomy: "taxonomy";
179
+ author: "author";
180
180
  }>;
181
181
  page: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodCoercedNumber<string | number>>>;
182
182
  }, z.core.$strip>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kizlo",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "type": "module",
5
5
  "description": "A headless WordPress framework for TypeScript.",
6
6
  "keywords": [