kizlo 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/astro/server.d.ts +9 -0
  2. package/dist/astro/server.js +10 -0
  3. package/dist/astro.d.ts +2 -0
  4. package/dist/astro.js +3 -0
  5. package/dist/cli/commands/_setup.js +8 -8
  6. package/dist/cli/commands/create.js +98 -68
  7. package/dist/cli/commands/generate.js +4 -0
  8. package/dist/cli/commands/init.js +109 -59
  9. package/dist/cli/daemon/config.js +12 -6
  10. package/dist/cli/daemon/generate.js +2 -3
  11. package/dist/cli/daemon/jiti.js +101 -0
  12. package/dist/cli/daemon/watch.js +7 -3
  13. package/dist/cli/presets/source.js +122 -24
  14. package/dist/cli/presets/template.js +47 -1
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +2 -2
  17. package/dist/integrations/astro/kizlo.client.d.ts +15 -0
  18. package/dist/integrations/astro/kizlo.client.js +13 -0
  19. package/dist/integrations/astro/kizlo.server.d.ts +10 -0
  20. package/dist/integrations/astro/kizlo.server.js +12 -0
  21. package/dist/integrations/astro/manifest.d.ts +9 -0
  22. package/dist/integrations/astro/manifest.js +11 -0
  23. package/dist/integrations/astro/metadata.d.ts +64 -0
  24. package/dist/integrations/astro/metadata.js +71 -0
  25. package/dist/integrations/astro/robots.d.ts +8 -0
  26. package/dist/integrations/astro/robots.js +11 -0
  27. package/dist/integrations/astro/sitemap.d.ts +16 -0
  28. package/dist/integrations/astro/sitemap.js +20 -0
  29. package/dist/integrations/astro/utils.d.ts +19 -0
  30. package/dist/integrations/astro/utils.js +17 -0
  31. package/dist/integrations/nextjs/metadata.d.ts +1 -0
  32. package/dist/integrations/nextjs/metadata.js +1 -0
  33. package/dist/kizlo.d.ts +8 -1
  34. package/dist/kizlo.js +16 -13
  35. package/dist/nextjs/server.d.ts +2 -1
  36. package/dist/nextjs/server.js +2 -1
  37. package/dist/package.js +1 -1
  38. package/dist/seo/utils.d.ts +8 -2
  39. package/dist/seo/utils.js +9 -1
  40. package/dist/shared/constants.js +9 -1
  41. package/package.json +17 -1
  42. package/dist/cli/presets/base.js +0 -40
  43. package/dist/cli/presets/index.js +0 -16
  44. package/dist/cli/presets/nextjs.js +0 -16
@@ -0,0 +1,9 @@
1
+ import { WEB_MANIFEST_ROUTE } from "../brand/manifest.js";
2
+ import { renderJsonLd } from "../seo/utils.js";
3
+ import { KizloServerOptions, createKizlo } from "../integrations/astro/kizlo.server.js";
4
+ import { AstroRoute, AstroRouteContext, getServerBaseUrl } from "../integrations/astro/utils.js";
5
+ import { createManifestRoute } from "../integrations/astro/manifest.js";
6
+ import { PageHead, RootHead, ThemeColor, resolvePageHead, resolveRootHead } from "../integrations/astro/metadata.js";
7
+ import { createRobotsRoute } from "../integrations/astro/robots.js";
8
+ import { createSitemapRedirectRoute, createSitemapRoute } from "../integrations/astro/sitemap.js";
9
+ export { AstroRoute, AstroRouteContext, KizloServerOptions, PageHead, RootHead, ThemeColor, WEB_MANIFEST_ROUTE, createKizlo, createManifestRoute, createRobotsRoute, createSitemapRedirectRoute, createSitemapRoute, getServerBaseUrl, renderJsonLd, resolvePageHead, resolveRootHead };
@@ -0,0 +1,10 @@
1
+ import { WEB_MANIFEST_ROUTE } from "../brand/manifest.js";
2
+ import { renderJsonLd } from "../seo/utils.js";
3
+ import { getServerBaseUrl } from "../integrations/astro/utils.js";
4
+ import { createKizlo } from "../integrations/astro/kizlo.server.js";
5
+ import { createManifestRoute } from "../integrations/astro/manifest.js";
6
+ import { resolvePageHead, resolveRootHead } from "../integrations/astro/metadata.js";
7
+ import { createRobotsRoute } from "../integrations/astro/robots.js";
8
+ import { createSitemapRedirectRoute, createSitemapRoute } from "../integrations/astro/sitemap.js";
9
+
10
+ export { WEB_MANIFEST_ROUTE, createKizlo, createManifestRoute, createRobotsRoute, createSitemapRedirectRoute, createSitemapRoute, getServerBaseUrl, renderJsonLd, resolvePageHead, resolveRootHead };
@@ -0,0 +1,2 @@
1
+ import { KizloClientOptions, createKizloClient } from "./integrations/astro/kizlo.client.js";
2
+ export { KizloClientOptions, createKizloClient };
package/dist/astro.js ADDED
@@ -0,0 +1,3 @@
1
+ import { createKizloClient } from "./integrations/astro/kizlo.client.js";
2
+
3
+ export { createKizloClient };
@@ -17,13 +17,13 @@ import getPort, { portNumbers } from "get-port";
17
17
 
18
18
  //#region src/cli/commands/_setup.ts
19
19
  /**
20
- * The resolved `.env` key names the scaffold writes: the template's declared `env` when it has one,
21
- * else the preset's built-in `envKeys` (the generic `base` preset), else the {@link DEFAULT_ENV_KEYS}
22
- * fallback for an in-repo template before the first stamped release. Threaded into `managedEnv`,
23
- * `writeEnv`, and `collectConnectionFromEnv` so the scaffold writes the names the pinned runtime reads.
20
+ * The resolved `.env` key names the scaffold writes: the template's declared `env` when it has one, else
21
+ * the {@link DEFAULT_ENV_KEYS} fallback — for an in-repo template before the first stamped release, and
22
+ * for the base no-template fallback (which has no manifest). Threaded into `managedEnv`, `writeEnv`, and
23
+ * `collectConnectionFromEnv` so the scaffold writes the names the pinned runtime reads.
24
24
  */
25
- function resolveEnvKeys(preset, manifest) {
26
- return manifest?.env ?? preset.envKeys ?? DEFAULT_ENV_KEYS;
25
+ function resolveEnvKeys(manifest) {
26
+ return manifest?.env ?? DEFAULT_ENV_KEYS;
27
27
  }
28
28
  /** Remote connection keys — what a real deploy needs, and what `.env.example` always lists. */
29
29
  function remoteKeys(envKeys) {
@@ -135,11 +135,11 @@ function pickAppPort() {
135
135
  * `create` scaffolds a brand-new app that has no public URL yet, so it defaults to the local dev
136
136
  * origin and lets the user fill in production later.
137
137
  */
138
- async function collectConnectionInteractively(preset, opts = {}) {
138
+ async function collectConnectionInteractively(apiPath, opts = {}) {
139
139
  let siteUrl;
140
140
  let baseUrl;
141
141
  if (opts.baseUrl !== void 0) baseUrl = opts.baseUrl;
142
- else if (preset.apiPath) baseUrl = orCancel(await p.text({
142
+ else if (apiPath) baseUrl = orCancel(await p.text({
143
143
  message: "Public app URL",
144
144
  placeholder: "https://your-app.com",
145
145
  validate: validate(urlString)
@@ -1,6 +1,5 @@
1
1
  import { availablePackageManagers, detectInvokingPackageManager, ensureGitignored, frameworkCreateArgs, installArgs, runCommandAsync, runCommandCaptured } from "../utils.js";
2
- import { getPreset } from "../presets/index.js";
3
- import { fetchTemplate } from "../presets/source.js";
2
+ import { fetchRegistry, listTemplates, resolveRegistry } from "../presets/source.js";
4
3
  import { adaptFile, changesFor, fileEntries, minCliError, patchEntries, readManifest, resolveDependencies } from "../presets/template.js";
5
4
  import { collectConnectionInteractively, nextStepsLines, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
6
5
  import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
@@ -11,15 +10,30 @@ import { defineCommand } from "citty";
11
10
  import * as p from "@clack/prompts";
12
11
 
13
12
  //#region src/cli/commands/create.ts
13
+ /** The final path segment of a project name — the app folder itself; everything before it is where it lands. */
14
+ function folderSegment(value) {
15
+ const segments = value.replace(/\\/g, "/").split("/").filter(Boolean);
16
+ return segments[segments.length - 1] ?? "";
17
+ }
14
18
  /**
15
- * The templates `create` can scaffold from. Each id is both the `templates/<id>` folder and the
16
- * preset that drives its WordPress setup and framework bootstrap.
19
+ * Tidy the entered name/path for both the target dir and the display: trim, drop a leading `./`, and drop a
20
+ * trailing separator, so `cd`, the spinner, and the warning lines read as `templates/my-app`, not
21
+ * `./templates/my-app/`. Preserves `../` and absolute paths (only a leading `./` is redundant).
17
22
  */
18
- const TEMPLATES = ["nextjs"];
19
- function isTemplate(value) {
20
- return TEMPLATES.includes(value);
23
+ function normalizeProjectName(value) {
24
+ return value.trim().replace(/^\.\/+/, "").replace(/[/\\]+$/, "");
21
25
  }
22
- const projectName = validate(z.string().trim().min(1, "Required").refine((value) => !value.startsWith(".") && !value.includes("/") && !value.includes("\\"), "Enter a folder name (letters, numbers, dashes)"));
26
+ /**
27
+ * A project name may be a bare folder (`my-app`) or a path to scaffold somewhere other than the current
28
+ * dir — relative (`apps/my-app`, `../my-app`) or absolute (`/srv/my-app`). Only the last segment is the
29
+ * app folder, so that's what's validated as a name; the parent dirs (which may not exist yet) are created
30
+ * by the framework's own CLI. `.`/`..` as the final segment, or a name with no real segment, is rejected.
31
+ */
32
+ const projectNameSchema = z.string().trim().min(1, "Required").refine((value) => {
33
+ const base = folderSegment(value);
34
+ return base.length > 0 && base !== "." && base !== ".." && /^[A-Za-z0-9._-]+$/.test(base);
35
+ }, "The app folder (last path segment) can only contain letters, numbers, dots, and dashes");
36
+ const projectName = validate(projectNameSchema);
23
37
  /** Package managers `create` can wire the getting-started steps for, in display order. */
24
38
  const PACKAGE_MANAGERS = [
25
39
  "pnpm",
@@ -37,17 +51,25 @@ function recordDependencies(dir, manifest) {
37
51
  const pkgPath = path.join(dir, "package.json");
38
52
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
39
53
  const { dependencies, devDependencies } = resolveDependencies(manifest);
40
- pkg.dependencies = {
54
+ pkg.dependencies = sortDependencies({
41
55
  ...pkg.dependencies,
42
56
  ...dependencies
43
- };
44
- if (Object.keys(devDependencies).length) pkg.devDependencies = {
57
+ });
58
+ if (Object.keys(devDependencies).length) pkg.devDependencies = sortDependencies({
45
59
  ...pkg.devDependencies,
46
60
  ...devDependencies
47
- };
61
+ });
48
62
  fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
49
63
  }
50
64
  /**
65
+ * Alphabetize a dependency map by package name. The framework CLI writes its deps sorted, but merging
66
+ * Kizlo's pins on top appends them out of order — sherif flags unsorted dependency keys, so re-sort the
67
+ * whole map before writing.
68
+ */
69
+ function sortDependencies(deps) {
70
+ return Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
71
+ }
72
+ /**
51
73
  * The argv `create` runs to bootstrap a fresh base app with the framework's own official CLI, built
52
74
  * from the template manifest's `bootstrap` (initializer + flags) — the template is the single source
53
75
  * of truth for the framework. The CLI owns only the package-manager mechanics: {@link frameworkCreateArgs}
@@ -104,54 +126,75 @@ const create = defineCommand({
104
126
  template: {
105
127
  type: "positional",
106
128
  required: false,
107
- description: `Template to scaffold from (${TEMPLATES.join(", ")})`
129
+ description: "Template to scaffold from (discovered from the registry)"
108
130
  },
109
131
  name: {
110
132
  type: "positional",
111
133
  required: false,
112
- description: "Project folder to create"
134
+ description: "Project folder to create — a bare name or a path (e.g. apps/my-app, ../my-app, /srv/my-app)"
135
+ },
136
+ source: {
137
+ type: "string",
138
+ description: "Where to scaffold from — a local dir or giget source, either a registry of templates or a single template (default: Kizlo's GitHub templates)"
113
139
  }
114
140
  },
115
141
  async run({ args }) {
116
142
  const cwd = process.cwd();
117
143
  p.intro(`Let's create a new Kizlo application`);
144
+ const registry = resolveRegistry(args.source ? String(args.source) : void 0);
145
+ let fetchedRegistry;
146
+ try {
147
+ fetchedRegistry = await fetchRegistry(registry);
148
+ } catch (error) {
149
+ p.cancel(error instanceof Error ? error.message : String(error));
150
+ process.exit(1);
151
+ }
152
+ const templates = listTemplates(fetchedRegistry.dir);
153
+ if (templates.length === 0) {
154
+ fetchedRegistry.cleanup();
155
+ p.cancel(`No templates found in the registry (${registry.source}). Each template needs a template.json.`);
156
+ process.exit(1);
157
+ }
158
+ const cancelFrom = (message) => {
159
+ fetchedRegistry.cleanup();
160
+ p.cancel(message);
161
+ process.exit(1);
162
+ };
118
163
  const requested = args.template;
119
- let template;
164
+ let selected;
120
165
  if (requested) {
121
- if (!isTemplate(requested)) {
122
- p.cancel(`Unknown template "${requested}". Available: ${TEMPLATES.join(", ")}`);
123
- process.exit(1);
124
- }
125
- template = requested;
126
- } else template = orCancel(await p.select({
127
- message: "Template",
128
- options: TEMPLATES.map((id) => ({
129
- value: id,
130
- label: id
131
- }))
132
- }));
133
- const name = args.name ? String(args.name).trim() : orCancel(await p.text({
134
- message: "Project name",
166
+ selected = templates.find((entry) => entry.id === requested);
167
+ if (!selected) return cancelFrom(`Unknown template "${requested}". Available: ${templates.map((entry) => entry.id).join(", ")}`);
168
+ } else {
169
+ const id = orCancel(await p.select({
170
+ message: "Template",
171
+ options: templates.map((entry) => ({
172
+ value: entry.id,
173
+ label: entry.label
174
+ }))
175
+ }));
176
+ selected = templates.find((entry) => entry.id === id);
177
+ }
178
+ if (!selected) return cancelFrom("No template selected.");
179
+ const template = selected.id;
180
+ const templateDir = selected.dir;
181
+ const label = selected.label;
182
+ const fail = cancelFrom;
183
+ const manifest = readManifest(templateDir);
184
+ const minErr = minCliError(manifest);
185
+ if (minErr) return fail(minErr);
186
+ const name = normalizeProjectName(args.name ? String(args.name) : orCancel(await p.text({
187
+ message: "What is your project named?",
135
188
  placeholder: "my-app",
136
189
  validate: projectName
137
- }));
138
- if (!args.name) {
139
- const check = projectName(name);
140
- if (check) {
141
- p.cancel(check);
142
- process.exit(1);
143
- }
144
- }
145
- const dir = path.resolve(cwd, name);
146
- if (fs.existsSync(dir)) {
147
- p.cancel(`${name} already exists — pick a different name or remove it.`);
148
- process.exit(1);
149
- }
150
- const preset = getPreset(template);
151
- if (!preset) {
152
- p.cancel(`No preset for template "${template}".`);
190
+ })));
191
+ const invalid = projectName(name);
192
+ if (invalid) {
193
+ p.cancel(invalid);
153
194
  process.exit(1);
154
195
  }
196
+ const dir = path.resolve(cwd, name);
197
+ if (fs.existsSync(dir)) return cancelFrom(`${name} already exists — pick a different name or remove it.`);
155
198
  const installed = availablePackageManagers(PACKAGE_MANAGERS);
156
199
  const invokingPm = detectInvokingPackageManager();
157
200
  const defaultPm = invokingPm && installed.includes(invokingPm) ? invokingPm : installed[0];
@@ -163,45 +206,32 @@ const create = defineCommand({
163
206
  })),
164
207
  initialValue: defaultPm
165
208
  }));
166
- const conn = await collectConnectionInteractively(preset, { baseUrl: `http://localhost:${await pickAppPort()}` });
167
- if (preset.apiPath && conn.baseUrl) conn.baseUrl = withApiPath(conn.baseUrl, preset.apiPath);
209
+ const conn = await collectConnectionInteractively(manifest.apiPath, { baseUrl: `http://localhost:${await pickAppPort()}` });
210
+ if (manifest.apiPath && conn.baseUrl) conn.baseUrl = withApiPath(conn.baseUrl, manifest.apiPath);
168
211
  const includeExamples = orCancel(await p.confirm({
169
212
  message: "Add examples?",
170
213
  initialValue: true
171
214
  }));
172
- const fetched = await fetchTemplate(template);
173
- const fail = (message) => {
174
- fetched.cleanup();
175
- p.cancel(message);
176
- process.exit(1);
177
- };
178
- const manifest = readManifest(fetched.dir);
179
- const minErr = minCliError(manifest);
180
- if (minErr) fail(minErr);
181
215
  const bootstrap = bootstrapArgs(manifest, pm, name);
182
- if (!bootstrap) {
183
- fetched.cleanup();
184
- p.cancel(`Template "${template}" can't be scaffolded — its manifest declares no framework bootstrap.`);
185
- process.exit(1);
186
- }
216
+ if (!bootstrap) return fail(`Template "${template}" can't be scaffolded — its manifest declares no framework bootstrap.`);
187
217
  const s = p.spinner();
188
- s.start(`Creating ${name} with the ${preset.label} CLI`);
218
+ s.start(`Creating ${name} with the ${label} CLI`);
189
219
  const scaffold = runCommandCaptured(bootstrap, cwd);
190
- s.stop(scaffold.ok ? `Created ${name} with the ${preset.label} CLI` : `${preset.label} setup failed`);
220
+ s.stop(scaffold.ok ? `Created ${name} with the ${label} CLI` : `${label} setup failed`);
191
221
  if (!scaffold.ok) {
192
222
  if (scaffold.output) p.log.error(scaffold.output);
193
- fail(`${preset.label} setup failed — see the output above and try again.`);
223
+ fail(`${label} setup failed — see the output above and try again.`);
194
224
  }
195
- const exampleTemplate = readEnvExample(fetched.dir);
225
+ const exampleTemplate = readEnvExample(templateDir);
196
226
  try {
197
- await applyManifestWiring(dir, fetched.dir, manifest, {
227
+ await applyManifestWiring(dir, templateDir, manifest, {
198
228
  includeExamples,
199
229
  localDev: conn.mode === "local"
200
230
  });
201
231
  } catch (error) {
202
232
  fail(error instanceof Error ? error.message : String(error));
203
233
  }
204
- fetched.cleanup();
234
+ fetchedRegistry.cleanup();
205
235
  let depsInstalled = false;
206
236
  if (orCancel(await p.confirm({
207
237
  message: "Install dependencies now?",
@@ -214,7 +244,7 @@ const create = defineCommand({
214
244
  if (!depsInstalled) p.log.warn(`Install them yourself: cd ${name} && ${installArgs(pm).join(" ")}`);
215
245
  }
216
246
  await setupLocalWordPress(dir, conn);
217
- await writeEnv(dir, resolveEnvKeys(preset, manifest), conn, {
247
+ await writeEnv(dir, resolveEnvKeys(manifest), conn, {
218
248
  force: true,
219
249
  yes: false,
220
250
  exampleTemplate
@@ -15,6 +15,10 @@ const generate = defineCommand({
15
15
  } },
16
16
  async run({ args }) {
17
17
  const cfg = await resolveConfig(process.cwd(), { dir: args.dir });
18
+ if (!cfg) {
19
+ log.info("No Kizlo server directory configured, nothing to generate. Set `dir` in kizlo.config.ts or pass --dir.");
20
+ return;
21
+ }
18
22
  let ok;
19
23
  try {
20
24
  ok = await generateOnce(cfg);
@@ -1,7 +1,6 @@
1
1
  import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, runCommandAsync } from "../utils.js";
2
- import { PRESETS, detectPreset, getPreset } from "../presets/index.js";
3
- import { fetchTemplate } from "../presets/source.js";
4
- import { adaptFile, changesFor, fileEntries, isOlderVersion, minCliError, patchEntries, readManifest, resolveDependencies } from "../presets/template.js";
2
+ import { DEFAULT_REGISTRY, detectTemplates, fetchRegistry, isSingleTemplate, listTemplates, resolveRegistry } from "../presets/source.js";
3
+ import { adaptFile, changesFor, fileEntries, isOlderVersion, minCliError, patchEntries, readManifest, renderNote, resolveDependencies } from "../presets/template.js";
5
4
  import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, readEnvExample, resolveEnvKeys, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
6
5
  import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
7
6
  import path from "node:path";
@@ -25,7 +24,20 @@ function sameOrigin(a, b) {
25
24
  return false;
26
25
  }
27
26
  }
28
- function detectAppDir(cwd, hasSrcDir) {
27
+ /**
28
+ * The framework's route directory in the user's project. When a template is present its
29
+ * `conventions.appDir` (e.g. `src/app` for Next, `src/pages` for Astro) is the expected layout; we
30
+ * probe that path and its no-`src` variant so a project that keeps routes at the top level is still
31
+ * found, falling back to whether the project has a `src` dir. With no known template appDir we keep the
32
+ * App Router default.
33
+ */
34
+ function detectAppDir(cwd, hasSrcDir, templateAppDir) {
35
+ if (templateAppDir) {
36
+ const withoutSrc = templateAppDir.replace(/^src\//, "");
37
+ if (fs.existsSync(path.join(cwd, templateAppDir))) return templateAppDir;
38
+ if (fs.existsSync(path.join(cwd, withoutSrc))) return withoutSrc;
39
+ return hasSrcDir ? templateAppDir : withoutSrc;
40
+ }
29
41
  if (fs.existsSync(path.join(cwd, "src", "app"))) return "src/app";
30
42
  if (fs.existsSync(path.join(cwd, "app"))) return "app";
31
43
  return hasSrcDir ? "src/app" : "app";
@@ -37,7 +49,7 @@ async function collectInteractively(ctx) {
37
49
  validate: validate(dirPath)
38
50
  }));
39
51
  let alias = "";
40
- if (ctx.preset.apiPath) {
52
+ if (ctx.apiPath) {
41
53
  const serverDir = path.join(dir.replace(/^\.\//, "").replace(/\/+$/, ""), "server");
42
54
  const detected = detectImportAlias(ctx.cwd, serverDir)?.prefix;
43
55
  alias = orCancel(await p.text({
@@ -47,7 +59,7 @@ async function collectInteractively(ctx) {
47
59
  })).trim();
48
60
  }
49
61
  return {
50
- ...await collectConnectionInteractively(ctx.preset),
62
+ ...await collectConnectionInteractively(ctx.apiPath),
51
63
  dir,
52
64
  alias
53
65
  };
@@ -63,7 +75,7 @@ function collectFromEnv(ctx) {
63
75
  return {
64
76
  ...conn,
65
77
  dir,
66
- alias: ctx.preset.apiPath ? aliasWithSlash(detectImportAlias(ctx.cwd, path.join(dir, "server"))?.prefix) : ""
78
+ alias: ctx.apiPath ? aliasWithSlash(detectImportAlias(ctx.cwd, path.join(dir, "server"))?.prefix) : ""
67
79
  };
68
80
  }
69
81
  function readPkg(pkgPath) {
@@ -105,13 +117,13 @@ const init = defineCommand({
105
117
  description: "Overwrite existing .env values and server entry without asking",
106
118
  default: false
107
119
  },
108
- preset: {
109
- type: "string",
110
- description: `Force a setup preset (${PRESETS.map((preset) => preset.id).join(", ")})`
111
- },
112
120
  alias: {
113
121
  type: "string",
114
122
  description: "Import alias prefix for generated imports (e.g. @); blank for relative"
123
+ },
124
+ source: {
125
+ type: "string",
126
+ description: "Where to wire from — a local dir or giget source, either a registry of templates or a single template (default: Kizlo's GitHub templates)"
115
127
  }
116
128
  },
117
129
  async run({ args }) {
@@ -136,73 +148,109 @@ const init = defineCommand({
136
148
  const pm = detectPackageManager(cwd);
137
149
  const hasKizlo = Boolean(pkg.dependencies?.kizlo) || Boolean(pkg.devDependencies?.kizlo);
138
150
  const hasSrcDir = fs.existsSync(path.join(cwd, "src"));
139
- const initCtx = {
140
- cwd,
141
- pkg,
142
- pm,
143
- hasSrcDir
144
- };
145
- let preset;
146
- if (args.preset) {
147
- const chosen = getPreset(String(args.preset));
148
- if (!chosen) {
149
- p.cancel(`Unknown preset "${args.preset}". Available: ${PRESETS.map((pr) => pr.id).join(", ")}`);
150
- process.exit(1);
151
- }
152
- preset = chosen;
153
- } else {
154
- preset = detectPreset(initCtx);
155
- if (preset.id !== "base") p.log.success(`${preset.label} detected`);
156
- }
157
- if (preset.template === "nextjs" && !fs.existsSync(path.join(cwd, "app")) && !fs.existsSync(path.join(cwd, "src/app"))) {
158
- p.cancel("Kizlo needs the Next.js App Router — no `app` or `src/app` directory found. The Pages Router isn't supported.");
159
- process.exit(1);
160
- }
161
151
  if (yes) loadEnvFiles(cwd);
162
- let manifest;
163
- let fetched;
164
- if (preset.template) {
165
- fetched = await fetchTemplate(preset.template);
166
- manifest = readManifest(fetched.dir);
167
- const minErr = minCliError(manifest);
168
- if (minErr) {
169
- fetched.cleanup();
170
- p.cancel(minErr);
171
- process.exit(1);
172
- }
152
+ const registry = resolveRegistry(args.source ? String(args.source) : void 0);
153
+ let fetchedRegistry;
154
+ try {
155
+ fetchedRegistry = await fetchRegistry(registry);
156
+ } catch (error) {
157
+ p.cancel(error instanceof Error ? error.message : String(error));
158
+ process.exit(1);
173
159
  }
174
- const envKeys = resolveEnvKeys(preset, manifest);
160
+ const cancelFrom = (message) => {
161
+ fetchedRegistry.cleanup();
162
+ p.cancel(message);
163
+ process.exit(1);
164
+ };
175
165
  try {
166
+ const entries = listTemplates(fetchedRegistry.dir);
167
+ if (entries.length === 0) cancelFrom(`No templates found in ${registry.source}. Each template needs a template.json.`);
168
+ const projectDeps = {
169
+ ...pkg.dependencies,
170
+ ...pkg.devDependencies
171
+ };
172
+ let entry;
173
+ if (isSingleTemplate(fetchedRegistry.dir)) {
174
+ const [only] = entries;
175
+ entry = only;
176
+ if (only && detectTemplates(entries, projectDeps).length === 0) {
177
+ p.log.warn(`The ${only.label} template doesn't match this project's dependencies — detection may be off on either side.`);
178
+ if (!(yes || orCancel(await p.confirm({
179
+ message: `Apply ${only.label} anyway?`,
180
+ initialValue: false
181
+ })))) cancelFrom("Cancelled. Point --source at a template that matches this project, or omit it to auto-detect.");
182
+ }
183
+ } else {
184
+ const matched = detectTemplates(entries, projectDeps);
185
+ const [first] = matched;
186
+ if (matched.length === 0) {
187
+ p.log.warn(`Couldn't find a template matching this project's framework ${registry.source === DEFAULT_REGISTRY ? "in Kizlo templates" : `at your provided source ${registry.source}`}.`);
188
+ if (yes) cancelFrom(`Re-run with --source pointing at the single template you want, or run without --yes to pick one manually.`);
189
+ if (!orCancel(await p.confirm({
190
+ message: "Pick a template manually anyway?",
191
+ initialValue: false
192
+ }))) cancelFrom("Cancelled.");
193
+ const id = orCancel(await p.select({
194
+ message: "Choose a template to apply",
195
+ options: entries.map((e) => ({
196
+ value: e.id,
197
+ label: e.label
198
+ }))
199
+ }));
200
+ entry = entries.find((e) => e.id === id);
201
+ } else if (matched.length === 1 && first) {
202
+ entry = first;
203
+ p.log.success(`${first.label} detected`);
204
+ } else {
205
+ const ids = matched.map((e) => e.id).join(", ");
206
+ if (yes) cancelFrom(`Multiple templates match this project (${ids}). Re-run with --source pointing at the single template you want.`);
207
+ const id = orCancel(await p.select({
208
+ message: "Multiple templates match this project — pick one",
209
+ options: matched.map((e) => ({
210
+ value: e.id,
211
+ label: e.label
212
+ }))
213
+ }));
214
+ entry = matched.find((e) => e.id === id);
215
+ }
216
+ }
217
+ if (!entry) return cancelFrom("No template selected.");
218
+ const manifest = readManifest(entry.dir);
219
+ const minErr = minCliError(manifest);
220
+ if (minErr) cancelFrom(minErr);
221
+ if (manifest.requires && !manifest.requires.anyDir.some((rel) => fs.existsSync(path.join(cwd, rel)))) cancelFrom(manifest.requires.message);
222
+ const apiPath = manifest.apiPath;
223
+ const envKeys = resolveEnvKeys(manifest);
176
224
  const setup = yes ? collectFromEnv({
177
225
  cwd,
178
226
  hasSrcDir,
179
- preset,
227
+ apiPath,
180
228
  envKeys
181
229
  }) : await collectInteractively({
182
230
  cwd,
183
231
  hasSrcDir,
184
- preset
232
+ apiPath
185
233
  });
186
234
  if (args.alias !== void 0) setup.alias = String(args.alias).trim();
187
235
  setup.alias = aliasWithSlash(setup.alias);
188
- const includeExamples = manifest && fetched ? yes ? true : orCancel(await p.confirm({
236
+ const includeExamples = yes ? true : orCancel(await p.confirm({
189
237
  message: "Add example pages?",
190
238
  initialValue: true
191
- })) : false;
239
+ }));
192
240
  if (!hasKizlo) {
193
- const spec = `kizlo@${manifest?.dependencies?.kizlo ?? `^${getVersion()}`}`;
241
+ const spec = `kizlo@${manifest.dependencies?.kizlo ?? `^${getVersion()}`}`;
194
242
  const s = p.spinner();
195
243
  s.start(`Installing kizlo with ${pm}`);
196
244
  const ok = await runCommandAsync(addDependencyArgs(pm, spec), cwd, "ignore");
197
245
  s.stop(ok ? "Installed kizlo" : "Could not install kizlo automatically");
198
246
  if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, spec).join(" ")}`);
199
247
  }
200
- if (preset.apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, preset.apiPath);
248
+ if (apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, apiPath);
201
249
  await setupLocalWordPress(cwd, setup);
202
250
  await writeEnv(cwd, envKeys, setup, {
203
251
  force,
204
252
  yes,
205
- exampleTemplate: fetched ? readEnvExample(fetched.dir) : void 0
253
+ exampleTemplate: readEnvExample(entry.dir)
206
254
  });
207
255
  await syncRemote(setup);
208
256
  const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
@@ -210,7 +258,7 @@ const init = defineCommand({
210
258
  const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
211
259
  const scaffold = buildScaffoldContext(cwd, {
212
260
  dirRel,
213
- appDir: detectAppDir(cwd, hasSrcDir),
261
+ appDir: detectAppDir(cwd, hasSrcDir, manifest.conventions.appDir),
214
262
  alias: setup.alias,
215
263
  clientUrl
216
264
  });
@@ -219,10 +267,8 @@ const init = defineCommand({
219
267
  relPath: "kizlo.config.ts",
220
268
  contents: kizloConfigTemplate(dirRel, setup.alias, setup.mode === "local")
221
269
  }];
222
- if (manifest && fetched) {
223
- await alignDependencies(cwd, pm, pkg, manifest);
224
- for (const entry of fileEntries(changesFor(manifest, "init", { includeExamples }))) files.push(adaptFile(fetched.dir, entry, manifest.conventions, scaffold));
225
- } else if (preset.scaffolds) files.push(...preset.scaffolds(scaffold));
270
+ await alignDependencies(cwd, pm, pkg, manifest);
271
+ for (const fileEntry of fileEntries(changesFor(manifest, "init", { includeExamples }))) files.push(adaptFile(entry.dir, fileEntry, manifest.conventions, scaffold));
226
272
  const scaffolded = [];
227
273
  for (const file of files) scaffolded.push({
228
274
  file,
@@ -235,12 +281,16 @@ const init = defineCommand({
235
281
  const gitignore = ensureGitignored(cwd, ".env");
236
282
  ensureGitignored(cwd, ".kizlo/");
237
283
  for (const { file, result } of scaffolded) reportScaffold(file, result, yes);
238
- if (manifest) applyLayoutPatches(cwd, patchEntries(changesFor(manifest, "init")), manifest.conventions, scaffold);
284
+ applyLayoutPatches(cwd, patchEntries(changesFor(manifest, "init")), manifest.conventions, scaffold);
285
+ for (const note of manifest.notes ?? []) {
286
+ const rendered = renderNote(note, setup.alias);
287
+ p.note(rendered.body, rendered.title);
288
+ }
239
289
  if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
240
290
  nextStepsNote(setup);
241
291
  p.outro("Kizlo is ready 🎉");
242
292
  } finally {
243
- fetched?.cleanup();
293
+ fetchedRegistry.cleanup();
244
294
  }
245
295
  }
246
296
  });
@@ -2,10 +2,10 @@ import { LOCAL_DIR_REL } from "../wp/constants.js";
2
2
  import { credentialsPath, findConfigDir } from "../wp/utils.js";
3
3
  import { log } from "./logger.js";
4
4
  import { detectPackageManager } from "../utils.js";
5
+ import { importIgnoringVirtualModules } from "./jiti.js";
5
6
  import z from "zod/v4";
6
7
  import path from "node:path";
7
8
  import fs from "node:fs";
8
- import { createJiti } from "jiti";
9
9
 
10
10
  //#region src/cli/daemon/config.ts
11
11
  const fixtureSchema = z.custom((value) => typeof value === "object" && value !== null && typeof value.name === "string", { message: "must be a fixture object with a string `name` (use defineFixture)" });
@@ -41,15 +41,12 @@ const CONFIG_FILES = [
41
41
  const DEFAULT_DEV_PORT = 8080;
42
42
  const DEFAULT_DEV_DB_PORT = 3307;
43
43
  const DEFAULT_TEST_PORT = 8889;
44
- function defaultDir(cwd) {
45
- return fs.existsSync(path.join(cwd, "src")) ? "src/lib/kizlo" : "lib/kizlo";
46
- }
47
44
  async function loadConfigFile(cwd) {
48
45
  const file = CONFIG_FILES.map((name) => path.join(cwd, name)).find((p) => fs.existsSync(p));
49
46
  if (!file) return void 0;
50
47
  let raw;
51
48
  try {
52
- const mod = await createJiti(cwd, { moduleCache: false }).import(file);
49
+ const mod = await importIgnoringVirtualModules(cwd, file);
53
50
  raw = mod.default ?? mod;
54
51
  } catch (error) {
55
52
  log.error(`Could not load ${path.basename(file)}:`, error);
@@ -62,9 +59,18 @@ async function loadConfigFile(cwd) {
62
59
  }
63
60
  return result.data;
64
61
  }
62
+ /**
63
+ * Resolve the Kizlo server layout from an explicit `dir` — the `--dir` flag or `dir` in
64
+ * `kizlo.config.*`. Returns `undefined` when neither is set: there's no Kizlo server to
65
+ * generate a contract from, so callers skip generation and (for `dev`) run local WordPress
66
+ * alone. There's intentionally no default path — a missing `dir` means "no server", not
67
+ * "look under `lib/kizlo`".
68
+ */
65
69
  async function resolveConfig(cwd, flags) {
66
70
  const fileConfig = await loadConfigFile(cwd);
67
- const dir = (flags?.dir ?? fileConfig?.dir ?? defaultDir(cwd)).replace(/^\.\//, "").replace(/\/+$/, "");
71
+ const raw = flags?.dir ?? fileConfig?.dir;
72
+ if (!raw) return void 0;
73
+ const dir = raw.replace(/^\.\//, "").replace(/\/+$/, "");
68
74
  const serverDir = path.join(dir, "server");
69
75
  const generatedDir = path.join(serverDir, "generated");
70
76
  return {