kizlo 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { LOCAL_DIR_REL } from "../wp/constants.js";
2
2
  import { composeStop, createStack, dockerHint, dockerStatus } from "../wp/docker.js";
3
- import { DEFAULT_ENV_KEYS, ensureGitignored, envGroups, envKeysPresent, mergeEnv, pickStackPort, writeFileIfAbsent } from "../utils.js";
4
- import { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveStackName } from "../daemon/config.js";
3
+ import { DEFAULT_ENV_KEYS, availablePackageManagers, detectInvokingPackageManager, ensureGitignored, envGroups, envKeysPresent, mergeEnv, pickStackPort, writeFileIfAbsent } from "../utils.js";
4
+ import { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveStackName, stackProject } from "../daemon/config.js";
5
5
  import { createAdminAppPassword } from "../wp/bootstrap.js";
6
6
  import { bootstrapDev } from "../wp/dev.js";
7
7
  import { removeProjectContainers } from "../wp/session.js";
@@ -100,6 +100,32 @@ function orCancel(value) {
100
100
  }
101
101
  return value;
102
102
  }
103
+ /** Package managers the CLI can wire getting-started steps for, in display order. */
104
+ const PACKAGE_MANAGERS = [
105
+ "pnpm",
106
+ "npm",
107
+ "yarn",
108
+ "bun"
109
+ ];
110
+ /**
111
+ * Ask which package manager to use, offering only those installed on the host. The pre-selected default
112
+ * is the manager that invoked this CLI when it's available (a hint, not a decision — `npx` reports
113
+ * `npm`, so it can be wrong, which is exactly why we ask rather than commit to it), else the first
114
+ * installed. `create` asks up front; `init` asks only when it couldn't detect one from the project.
115
+ */
116
+ async function selectPackageManager(message) {
117
+ const installed = availablePackageManagers(PACKAGE_MANAGERS);
118
+ const invoking = detectInvokingPackageManager();
119
+ const initialValue = invoking && installed.includes(invoking) ? invoking : installed[0];
120
+ return orCancel(await p.select({
121
+ message,
122
+ options: installed.map((id) => ({
123
+ value: id,
124
+ label: id
125
+ })),
126
+ initialValue
127
+ }));
128
+ }
103
129
  /** Appends the API path to the base URL so the client and route handler agree. */
104
130
  function withApiPath(baseUrl, apiPath) {
105
131
  try {
@@ -174,15 +200,23 @@ async function collectConnectionInteractively(apiPath, opts = {}) {
174
200
  const mode = orCancel(await p.select({
175
201
  message: "WordPress connection",
176
202
  initialValue: "local",
177
- options: [{
178
- value: "local",
179
- label: "Set up local WordPress",
180
- hint: "runs in Docker, for dev and test"
181
- }, {
182
- value: "remote",
183
- label: "Use my own WordPress",
184
- hint: "connect to your existing WordPress"
185
- }]
203
+ options: [
204
+ {
205
+ value: "local",
206
+ label: "Set up local WordPress",
207
+ hint: "runs in Docker, for dev and test"
208
+ },
209
+ {
210
+ value: "remote",
211
+ label: "Use my own WordPress",
212
+ hint: "connect to your existing WordPress"
213
+ },
214
+ {
215
+ value: "skip",
216
+ label: "Skip for now",
217
+ hint: "fill in the connection in .env later"
218
+ }
219
+ ]
186
220
  }));
187
221
  if (mode === "local") {
188
222
  const status = await dockerStatus();
@@ -209,8 +243,9 @@ async function collectConnectionInteractively(apiPath, opts = {}) {
209
243
  validate: validate(requiredString)
210
244
  }));
211
245
  }
246
+ if (mode === "skip") p.log.info("Skipping WordPress setup — the connection keys are written to .env empty for you to fill in later.");
212
247
  return {
213
- mode,
248
+ mode: mode === "local" ? "local" : "remote",
214
249
  baseUrl,
215
250
  siteUrl,
216
251
  siteSecret,
@@ -238,7 +273,7 @@ function collectConnectionFromEnv(envKeys) {
238
273
  function devConfigFor(cwd) {
239
274
  return {
240
275
  configDir: cwd,
241
- project: `${resolveStackName(cwd)}-dev`,
276
+ project: stackProject(resolveStackName(cwd), "dev"),
242
277
  port: DEFAULT_DEV_PORT,
243
278
  portExplicit: false,
244
279
  dbPort: DEFAULT_DEV_DB_PORT,
@@ -412,4 +447,4 @@ function nextStepsNote(conn, prefix = "") {
412
447
  }
413
448
 
414
449
  //#endregion
415
- export { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsLines, nextStepsNote, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv };
450
+ export { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsLines, nextStepsNote, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, selectPackageManager, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv };
@@ -1,39 +1,44 @@
1
- import { resolveModuleImport, writeFileIfAbsent } from "../utils.js";
2
- import { applyPatchToSource, patchChanged, renderPatchCode } from "../presets/patch.js";
3
- import { findRootLayout, resolvePatch } from "../presets/template.js";
1
+ import { aliasWithSlash, resolveModuleImport, writeFileIfAbsent } from "../utils.js";
2
+ import { resolvePatch } from "../presets/template.js";
4
3
  import { orCancel } from "./_setup.js";
5
4
  import { CONTRACT_BARREL } from "../daemon/generate.js";
5
+ import { applyPatchToSource, patchChanged, renderPatchCode, resolvePatchTargetPath } from "../presets/patch.js";
6
6
  import path from "node:path";
7
7
  import fs from "node:fs";
8
8
  import * as p from "@clack/prompts";
9
9
 
10
10
  //#region src/cli/commands/_wiring.ts
11
- /** The kizlo.config.ts a scaffolded project gets: the Kizlo directory, an optional import alias, and —
11
+ /** The kizlo.config.ts a scaffolded project gets: the Kizlo directory, the import-alias preference, and —
12
12
  * when local WordPress is chosen — `dev.local` + `test.local` so `kizlo dev` and `kizlo test` boot the
13
- * fixed `.kizlo/local` install. */
13
+ * fixed `.kizlo/local` install. The alias is always written — `""` for relative imports included — so it
14
+ * records a made decision a later `kizlo init` reads back instead of prompting for it again. The alias
15
+ * is written in its canonical `@/` form (never a bare `@`) so it reads like the imports it produces. */
14
16
  function kizloConfigTemplate(dir, alias, localWordPress = false) {
15
17
  return `import { defineConfig } from "kizlo/config"
16
18
 
17
19
  export default defineConfig({
18
- dir: "${dir}",${alias ? `\n\talias: "${alias}",` : ""}${localWordPress ? `\n\tdev: { local: true },\n\ttest: { local: true },` : ""}
20
+ dir: "${dir}",${`\n\talias: "${aliasWithSlash(alias)}",`}${localWordPress ? `\n\tdev: { local: true },\n\ttest: { local: true },` : ""}
19
21
  })
20
22
  `;
21
23
  }
22
24
  /**
23
25
  * Build the {@link ScaffoldContext} that adapts a template's conventions (paths and imports) to a real
24
26
  * project. `dirRel` is the Kizlo home directory, `appDir` the App Router directory, and `alias` the
25
- * import-alias prefix (empty for relative imports). The server-entry import is resolved per calling
26
- * file so each scaffolded file references the server through the right specifier.
27
+ * import-alias prefix (empty for relative imports). Imports are resolved per calling file so each
28
+ * scaffolded file references its targets through the right specifier — `importFrom` for any project
29
+ * path (used to retarget every template-alias import), `serverImport` the server-entry shorthand.
27
30
  */
28
31
  function buildScaffoldContext(cwd, { dirRel, appDir, alias, clientUrl }) {
29
32
  const serverDirRel = path.join(dirRel, "server");
33
+ const importFrom = (targetRel, fromDir) => resolveModuleImport(cwd, targetRel, fromDir, alias);
30
34
  return {
31
35
  kizloDir: dirRel,
32
36
  serverDirName: path.basename(serverDirRel),
33
37
  serverEntryPath: path.join(serverDirRel, "index.ts"),
34
38
  clientPath: path.join(dirRel, "client.ts"),
35
39
  appDir,
36
- serverImport: (fromDir) => resolveModuleImport(cwd, serverDirRel, fromDir, alias),
40
+ serverImport: (fromDir) => importFrom(serverDirRel, fromDir),
41
+ importFrom,
37
42
  clientUrl
38
43
  };
39
44
  }
@@ -73,17 +78,24 @@ function writeGeneratedContract(cwd, serverDirRel) {
73
78
  writeFileIfAbsent(path.join(cwd, generatedDirRel, "index.ts"), CONTRACT_BARREL);
74
79
  }
75
80
  /**
76
- * Merge Kizlo wiring into files the project already owns (the root layout's SEO exports). Never
77
- * aborts: the target is resolved by identity (the layout that renders `<html>`), and on any doubt —
78
- * not found, not parseable — the resolved payload is printed at the end with placement instructions
79
- * rather than written to a guessed-at file. A confident apply is an idempotent upsert: it replaces
80
- * our exports if present, adds them if not.
81
+ * Apply every patch a template declares, merging Kizlo wiring into files the project already owns
82
+ * (today: the root layout and home page SEO exports — but the loop is role-agnostic, so a new patch
83
+ * needs no change here). Never aborts: the target is the exact path the template declares (extension-
84
+ * probed so a JS project's `.js`/`.jsx` is found), and on any doubt — the file isn't at that path, or
85
+ * won't parse — the resolved payload is printed at the end with placement instructions rather than
86
+ * written to a guessed-at file. We never scan the tree to find a stand-in. A confident apply is an
87
+ * idempotent upsert: it replaces our exports if present, adds them if not.
81
88
  */
82
- function applyLayoutPatches(cwd, patches, conventions, scaffold) {
89
+ function applyProjectPatches(cwd, patches, conventions, scaffold) {
83
90
  const manualSteps = [];
84
91
  for (const entry of patches) {
85
92
  const resolved = resolvePatch(entry, conventions, scaffold);
86
- const target = findRootLayout(cwd, scaffold.appDir, resolved.relPath);
93
+ if (resolved.mode === "note") {
94
+ manualSteps.push(resolved);
95
+ p.log.info(`Update your ${resolved.label} to finish wiring Kizlo — see the change to make below`);
96
+ continue;
97
+ }
98
+ const target = resolvePatchTargetPath(cwd, resolved.relPath);
87
99
  if (!target) {
88
100
  manualSteps.push(resolved);
89
101
  p.log.info(`Couldn't find your ${resolved.label} to wire Kizlo into — see the code to add below`);
@@ -105,8 +117,19 @@ function applyLayoutPatches(cwd, patches, conventions, scaffold) {
105
117
  p.log.success(`Wired Kizlo into your ${resolved.label} (${relTarget})`);
106
118
  } else p.log.info(`Your ${resolved.label} is already wired (${relTarget})`);
107
119
  }
108
- for (const resolved of manualSteps) p.note(renderPatchCode(resolved), `Add these to your ${resolved.label} (the layout that renders <html>)`);
120
+ for (const resolved of manualSteps) printManualStep(resolved.mode === "note" ? `Update your ${resolved.label} (${resolved.relPath})` : `Add these to your ${resolved.label} (${resolved.relPath})`, renderPatchCode(resolved));
121
+ }
122
+ /**
123
+ * Print a manual-step code block the user copies into a file. Deliberately avoids `p.note`: clack
124
+ * draws a `│`/`─` box around the body, so selecting the block in a terminal drags those border
125
+ * characters into the clipboard alongside the code. We print the heading through clack for the usual
126
+ * voice, then the code verbatim with no gutter so a terminal selection yields exactly the source.
127
+ */
128
+ function printManualStep(heading, code) {
129
+ p.log.step(heading);
130
+ const body = code.endsWith("\n") ? code.slice(0, -1) : code;
131
+ process.stdout.write(`\n${body}\n\n`);
109
132
  }
110
133
 
111
134
  //#endregion
112
- export { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract };
135
+ export { applyProjectPatches, buildScaffoldContext, kizloConfigTemplate, printManualStep, reportScaffold, scaffoldFile, writeGeneratedContract };
@@ -1,8 +1,8 @@
1
- import { availablePackageManagers, detectInvokingPackageManager, ensureGitignored, frameworkCreateArgs, installArgs, runCommandAsync, runCommandCaptured } from "../utils.js";
1
+ import { approveBuildsCommand, ensureGitignored, frameworkCreateArgs, initGitRepository, installArgs, isCommandAvailable, isGitRepository, runCommandCaptured, runCommandCapturedAsync } from "../utils.js";
2
2
  import { fetchRegistry, listTemplates, resolveRegistry } from "../presets/source.js";
3
3
  import { adaptFile, changesFor, fileEntries, minCliError, patchEntries, readManifest, resolveDependencies } from "../presets/template.js";
4
- import { collectConnectionInteractively, nextStepsLines, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
5
- import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
4
+ import { collectConnectionInteractively, nextStepsLines, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, selectPackageManager, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
5
+ import { applyProjectPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
6
6
  import z from "zod/v4";
7
7
  import path from "node:path";
8
8
  import fs from "node:fs";
@@ -34,13 +34,6 @@ const projectNameSchema = z.string().trim().min(1, "Required").refine((value) =>
34
34
  return base.length > 0 && base !== "." && base !== ".." && /^[A-Za-z0-9._-]+$/.test(base);
35
35
  }, "The app folder (last path segment) can only contain letters, numbers, dots, and dashes");
36
36
  const projectName = validate(projectNameSchema);
37
- /** Package managers `create` can wire the getting-started steps for, in display order. */
38
- const PACKAGE_MANAGERS = [
39
- "pnpm",
40
- "npm",
41
- "yarn",
42
- "bun"
43
- ];
44
37
  /**
45
38
  * Pin Kizlo's dependencies in the freshly bootstrapped `package.json`. The template is authoritative
46
39
  * for the versions (its manifest's `dependencies`/`devDependencies`, stamped at release), falling back
@@ -88,7 +81,7 @@ function bootstrapArgs(manifest, pm, name) {
88
81
  * `kizlo` dependency, write `kizlo.config.ts`, scaffold the manifest's files (core wiring from `base`
89
82
  * and `create` always, the `example`-flagged demo pages only when `includeExamples`),
90
83
  * seed the generated contract, and ignore `.env` and the `.kizlo/` working dir (which holds the local WordPress install). On a fresh app Kizlo owns the layout, so `create`
91
- * writes it whole (already SEO-wired) rather than patching — `applyLayoutPatches` runs over `create`'s
84
+ * writes it whole (already SEO-wired) rather than patching — `applyProjectPatches` runs over `create`'s
92
85
  * patches, which are none today. The target directory layout comes straight from the manifest's
93
86
  * conventions, which the bootstrap flags are chosen to match, so files land where they expect. Fresh
94
87
  * files are written with `force`, silently replacing the framework defaults.
@@ -115,7 +108,7 @@ async function applyManifestWiring(dir, templateDir, manifest, opts) {
115
108
  writeGeneratedContract(dir, path.join(kizloDir, "server"));
116
109
  ensureGitignored(dir, ".env");
117
110
  ensureGitignored(dir, ".kizlo/");
118
- applyLayoutPatches(dir, patchEntries(changes), manifest.conventions, scaffold);
111
+ applyProjectPatches(dir, patchEntries(changes), manifest.conventions, scaffold);
119
112
  }
120
113
  const create = defineCommand({
121
114
  meta: {
@@ -195,17 +188,7 @@ const create = defineCommand({
195
188
  }
196
189
  const dir = path.resolve(cwd, name);
197
190
  if (fs.existsSync(dir)) return cancelFrom(`${name} already exists — pick a different name or remove it.`);
198
- const installed = availablePackageManagers(PACKAGE_MANAGERS);
199
- const invokingPm = detectInvokingPackageManager();
200
- const defaultPm = invokingPm && installed.includes(invokingPm) ? invokingPm : installed[0];
201
- const pm = orCancel(await p.select({
202
- message: "Package manager",
203
- options: installed.map((id) => ({
204
- value: id,
205
- label: id
206
- })),
207
- initialValue: defaultPm
208
- }));
191
+ const pm = await selectPackageManager("Package manager");
209
192
  const conn = await collectConnectionInteractively(manifest.apiPath, { baseUrl: `http://localhost:${await pickAppPort()}` });
210
193
  if (manifest.apiPath && conn.baseUrl) conn.baseUrl = withApiPath(conn.baseUrl, manifest.apiPath);
211
194
  const includeExamples = orCancel(await p.confirm({
@@ -233,13 +216,16 @@ const create = defineCommand({
233
216
  }
234
217
  fetchedRegistry.cleanup();
235
218
  let depsInstalled = false;
219
+ let approveBuilds;
236
220
  if (orCancel(await p.confirm({
237
221
  message: "Install dependencies now?",
238
222
  initialValue: true
239
223
  }))) {
240
224
  const is = p.spinner();
241
225
  is.start(`Installing dependencies with ${pm}`);
242
- depsInstalled = await runCommandAsync(installArgs(pm), dir, "ignore");
226
+ const install = await runCommandCapturedAsync(installArgs(pm), dir);
227
+ depsInstalled = install.ok;
228
+ if (depsInstalled) approveBuilds = approveBuildsCommand(pm, install.output);
243
229
  is.stop(depsInstalled ? "Installed dependencies" : "Could not install dependencies");
244
230
  if (!depsInstalled) p.log.warn(`Install them yourself: cd ${name} && ${installArgs(pm).join(" ")}`);
245
231
  }
@@ -250,9 +236,19 @@ const create = defineCommand({
250
236
  exampleTemplate
251
237
  });
252
238
  await syncRemote(conn);
239
+ if (isCommandAvailable("git") && !isGitRepository(dir) && orCancel(await p.confirm({
240
+ message: "Initialize a git repository?",
241
+ initialValue: true
242
+ }))) {
243
+ const gs = p.spinner();
244
+ gs.start("Initializing git repository");
245
+ const ok = initGitRepository(dir);
246
+ gs.stop(ok ? "Initialized git repository" : "Could not initialize git repository");
247
+ }
253
248
  p.note([
254
249
  `cd ${name}`,
255
250
  ...depsInstalled ? [] : [`${pm} install`],
251
+ ...approveBuilds ? [approveBuilds] : [],
256
252
  ``,
257
253
  ...nextStepsLines(conn)
258
254
  ].join("\n"), "Next steps");
@@ -1,8 +1,8 @@
1
- import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, runCommandAsync } from "../utils.js";
1
+ import { addDependencyArgs, aliasWithSlash, detectImportAlias, detectInvokingPackageManager, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, persistPackageManagerField, readPersistedAlias, runCommandAsync } from "../utils.js";
2
2
  import { DEFAULT_REGISTRY, detectTemplates, fetchRegistry, isSingleTemplate, listTemplates, resolveRegistry } from "../presets/source.js";
3
3
  import { adaptFile, changesFor, fileEntries, isOlderVersion, minCliError, patchEntries, readManifest, renderNote, resolveDependencies } from "../presets/template.js";
4
- import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, readEnvExample, resolveEnvKeys, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
5
- import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
4
+ import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, readEnvExample, resolveEnvKeys, selectPackageManager, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
5
+ import { applyProjectPatches, buildScaffoldContext, kizloConfigTemplate, printManualStep, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
6
6
  import path from "node:path";
7
7
  import fs from "node:fs";
8
8
  import { defineCommand } from "citty";
@@ -12,10 +12,6 @@ import * as p from "@clack/prompts";
12
12
  function defaultDir(hasSrcDir) {
13
13
  return hasSrcDir ? "src/lib/kizlo" : "lib/kizlo";
14
14
  }
15
- /** Normalizes an alias prefix to the familiar `@/` form (or empty for relative). */
16
- function aliasWithSlash(alias) {
17
- return alias ? `${alias.replace(/\/+$/, "")}/` : "";
18
- }
19
15
  /** Whether two URLs share an origin (scheme + host + port); false when either can't be parsed. */
20
16
  function sameOrigin(a, b) {
21
17
  try {
@@ -42,14 +38,24 @@ function detectAppDir(cwd, hasSrcDir, templateAppDir) {
42
38
  if (fs.existsSync(path.join(cwd, "app"))) return "app";
43
39
  return hasSrcDir ? "src/app" : "app";
44
40
  }
41
+ /**
42
+ * The import alias settled without asking — the explicit `--alias` flag, else the alias a previous init
43
+ * persisted in kizlo.config. `undefined` when neither is set: the choice hasn't been made, so it must be
44
+ * prompted for (interactive) or defaulted to relative imports (`--yes`). Detection never enters here — a
45
+ * tsconfig alias is only ever a prompt *hint*, since relative imports always work and a detected alias
46
+ * isn't a guarantee (a project may have several, or map one Kizlo's dir isn't under).
47
+ */
48
+ function decidedAlias(flagAlias, persistedAlias) {
49
+ return flagAlias ?? persistedAlias;
50
+ }
45
51
  async function collectInteractively(ctx) {
46
52
  const dir = orCancel(await p.text({
47
53
  message: "Kizlo directory",
48
54
  initialValue: defaultDir(ctx.hasSrcDir),
49
55
  validate: validate(dirPath)
50
56
  }));
51
- let alias = "";
52
- if (ctx.apiPath) {
57
+ let alias = decidedAlias(ctx.flagAlias, ctx.persistedAlias);
58
+ if (alias === void 0) if (ctx.apiPath) {
53
59
  const serverDir = path.join(dir.replace(/^\.\//, "").replace(/\/+$/, ""), "server");
54
60
  const detected = detectImportAlias(ctx.cwd, serverDir)?.prefix;
55
61
  alias = orCancel(await p.text({
@@ -57,7 +63,7 @@ async function collectInteractively(ctx) {
57
63
  placeholder: "@/",
58
64
  initialValue: detected ? `${detected}/` : ""
59
65
  })).trim();
60
- }
66
+ } else alias = "";
61
67
  return {
62
68
  ...await collectConnectionInteractively(ctx.apiPath),
63
69
  dir,
@@ -65,9 +71,11 @@ async function collectInteractively(ctx) {
65
71
  };
66
72
  }
67
73
  /**
68
- * Non-interactive setup: skip prompts and use defaults. Values present in the
69
- * environment are used; missing ones are left empty for the user to fill in
70
- * later. Never fails — `--yes` always scaffolds a fillable project.
74
+ * Non-interactive setup: skip prompts and use defaults. Values present in the environment are used;
75
+ * missing ones are left empty for the user to fill in later. The alias is the `--alias` flag, else the
76
+ * one persisted from a prior run, else relative imports — never silent tsconfig detection, since `--yes`
77
+ * can't confirm it and relative imports always work. Never fails — `--yes` always scaffolds a fillable
78
+ * project.
71
79
  */
72
80
  function collectFromEnv(ctx) {
73
81
  const conn = collectConnectionFromEnv(ctx.envKeys);
@@ -75,28 +83,49 @@ function collectFromEnv(ctx) {
75
83
  return {
76
84
  ...conn,
77
85
  dir,
78
- alias: ctx.apiPath ? aliasWithSlash(detectImportAlias(ctx.cwd, path.join(dir, "server"))?.prefix) : ""
86
+ alias: decidedAlias(ctx.flagAlias, ctx.persistedAlias) ?? ""
79
87
  };
80
88
  }
81
89
  function readPkg(pkgPath) {
82
90
  return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
83
91
  }
84
92
  /**
85
- * The template is authoritative for the pinned versions: it declares `dependencies`/`devDependencies`
86
- * in its manifest (stamped at release), falling back to the running CLI's version for `kizlo` in-repo
87
- * before the first stamp. For each declared package the project already has at an older release, upgrade
88
- * it up to the template's pin and say so — a deliberate pin is never changed silently. Never downgrades,
89
- * and never adds a package the project doesn't already have (a missing `kizlo` is installed separately).
93
+ * Bring the project's dependencies in line with the template's pins. The template is authoritative for
94
+ * the versions: it declares `dependencies`/`devDependencies` in its manifest (stamped at release),
95
+ * falling back to the running CLI's version for `kizlo` in-repo before the first stamp. For each declared
96
+ * package:
97
+ *
98
+ * - missing from the project — install it at the template's pin, as a dev dependency when the template
99
+ * declares it there. `create` records the template's whole dependency set into the fresh app before
100
+ * installing; `init` layers onto an existing project, so packages the template needs but the project
101
+ * lacks (e.g. Astro's `@astrojs/node` adapter) must be added here or the app won't run.
102
+ * - present at an older release — upgrade it up to the pin and say so; a deliberate newer pin is never
103
+ * changed silently, and nothing is ever downgraded.
104
+ *
105
+ * A *missing* `kizlo` is installed on its own earlier (with its own messaging), so it's skipped here;
106
+ * an existing older `kizlo` still gets the upgrade path.
90
107
  */
91
108
  async function alignDependencies(cwd, pm, pkg, manifest) {
92
109
  const { dependencies, devDependencies } = resolveDependencies(manifest);
93
- for (const [name, want] of [...Object.entries(dependencies), ...Object.entries(devDependencies)]) {
110
+ const declared = [...Object.entries(dependencies).map(([name, want]) => ({
111
+ name,
112
+ want,
113
+ dev: false
114
+ })), ...Object.entries(devDependencies).map(([name, want]) => ({
115
+ name,
116
+ want,
117
+ dev: true
118
+ }))];
119
+ for (const { name, want, dev } of declared) {
94
120
  const have = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
95
- if (!have || !isOlderVersion(have, want)) continue;
121
+ if (name === "kizlo" && !have) continue;
122
+ if (have && !isOlderVersion(have, want)) continue;
123
+ const args = addDependencyArgs(pm, `${name}@${want}`, { dev });
96
124
  const s = p.spinner();
97
- s.start(`Upgrading ${name} from ${have} to ${want}`);
98
- const ok = await runCommandAsync(addDependencyArgs(pm, `${name}@${want}`), cwd, "ignore");
99
- s.stop(ok ? `Upgraded ${name} to ${want}` : `Could not upgrade ${name} automatically — install ${name}@${want} yourself`);
125
+ s.start(have ? `Upgrading ${name} from ${have} to ${want}` : `Installing ${name}@${want}`);
126
+ const ok = await runCommandAsync(args, cwd, "ignore");
127
+ if (have) s.stop(ok ? `Upgraded ${name} to ${want}` : `Could not upgrade ${name} automatically — install ${name}@${want} yourself`);
128
+ else s.stop(ok ? `Installed ${name}@${want}` : `Could not install ${name} automatically — run \`${args.join(" ")}\` yourself`);
100
129
  }
101
130
  }
102
131
  const init = defineCommand({
@@ -145,7 +174,9 @@ const init = defineCommand({
145
174
  process.exit(1);
146
175
  }
147
176
  const pkg = readPkg(pkgPath);
148
- const pm = detectPackageManager(cwd);
177
+ const detectedPm = detectPackageManager(cwd);
178
+ const pm = detectedPm ?? (yes ? detectInvokingPackageManager() ?? "npm" : await selectPackageManager("Couldn't detect this project's package manager — which should Kizlo use?"));
179
+ if (!detectedPm && !yes) persistPackageManagerField(cwd, pm);
149
180
  const hasKizlo = Boolean(pkg.dependencies?.kizlo) || Boolean(pkg.devDependencies?.kizlo);
150
181
  const hasSrcDir = fs.existsSync(path.join(cwd, "src"));
151
182
  if (yes) loadEnvFiles(cwd);
@@ -221,17 +252,18 @@ const init = defineCommand({
221
252
  if (manifest.requires && !manifest.requires.anyDir.some((rel) => fs.existsSync(path.join(cwd, rel)))) cancelFrom(manifest.requires.message);
222
253
  const apiPath = manifest.apiPath;
223
254
  const envKeys = resolveEnvKeys(manifest);
224
- const setup = yes ? collectFromEnv({
255
+ const flagAlias = args.alias !== void 0 ? String(args.alias).trim() : void 0;
256
+ const aliasCtx = {
225
257
  cwd,
226
258
  hasSrcDir,
227
259
  apiPath,
260
+ persistedAlias: readPersistedAlias(cwd),
261
+ flagAlias
262
+ };
263
+ const setup = yes ? collectFromEnv({
264
+ ...aliasCtx,
228
265
  envKeys
229
- }) : await collectInteractively({
230
- cwd,
231
- hasSrcDir,
232
- apiPath
233
- });
234
- if (args.alias !== void 0) setup.alias = String(args.alias).trim();
266
+ }) : await collectInteractively(aliasCtx);
235
267
  setup.alias = aliasWithSlash(setup.alias);
236
268
  const includeExamples = yes ? true : orCancel(await p.confirm({
237
269
  message: "Add example pages?",
@@ -281,10 +313,10 @@ const init = defineCommand({
281
313
  const gitignore = ensureGitignored(cwd, ".env");
282
314
  ensureGitignored(cwd, ".kizlo/");
283
315
  for (const { file, result } of scaffolded) reportScaffold(file, result, yes);
284
- applyLayoutPatches(cwd, patchEntries(changesFor(manifest, "init")), manifest.conventions, scaffold);
316
+ applyProjectPatches(cwd, patchEntries(changesFor(manifest, "init")), manifest.conventions, scaffold);
285
317
  for (const note of manifest.notes ?? []) {
286
318
  const rendered = renderNote(note, setup.alias);
287
- p.note(rendered.body, rendered.title);
319
+ printManualStep(rendered.title, rendered.body);
288
320
  }
289
321
  if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
290
322
  nextStepsNote(setup);
@@ -105,6 +105,14 @@ function resolveStackName(configDir, configName) {
105
105
  return sanitizeProjectName(pkgName ?? path.basename(configDir));
106
106
  }
107
107
  /**
108
+ * Compose project id for a stack: `kizlo-<name>-<kind>`. The leading `kizlo-` is what
109
+ * lets every stack this tool starts be filtered together in Docker (e.g.
110
+ * `docker ps --filter name=kizlo-`), independent of each project's own name.
111
+ */
112
+ function stackProject(baseName, kind) {
113
+ return `kizlo-${baseName}-${kind}`;
114
+ }
115
+ /**
108
116
  * Resolve the `test` block from `kizlo.config.*` into concrete values for the
109
117
  * `test` command, applying defaults (port 8889, auto-detected package manager)
110
118
  * and anchoring the credentials artifact to the config directory.
@@ -116,13 +124,13 @@ async function resolveTestConfig(cwd) {
116
124
  return {
117
125
  configDir,
118
126
  local: Boolean(test.local),
119
- project: `${resolveStackName(configDir, fileConfig?.name)}-test`,
127
+ project: stackProject(resolveStackName(configDir, fileConfig?.name), "test"),
120
128
  command: test.command,
121
129
  port: test.port ?? DEFAULT_TEST_PORT,
122
130
  portExplicit: test.port !== void 0,
123
131
  fixtures: test.fixtures ?? [],
124
132
  credentialsPath: credentialsPath(cwd),
125
- packageManager: test.packageManager ?? detectPackageManager(configDir)
133
+ packageManager: test.packageManager ?? detectPackageManager(configDir) ?? "npm"
126
134
  };
127
135
  }
128
136
  /**
@@ -148,7 +156,7 @@ async function resolveDevConfig(cwd) {
148
156
  const dev = fileConfig?.dev ?? {};
149
157
  return {
150
158
  configDir,
151
- project: `${resolveStackName(configDir, fileConfig?.name)}-dev`,
159
+ project: stackProject(resolveStackName(configDir, fileConfig?.name), "dev"),
152
160
  port: dev.port ?? DEFAULT_DEV_PORT,
153
161
  portExplicit: dev.port !== void 0,
154
162
  dbPort: dev.dbPort ?? DEFAULT_DEV_DB_PORT,
@@ -160,4 +168,4 @@ async function resolveDevConfig(cwd) {
160
168
  }
161
169
 
162
170
  //#endregion
163
- export { CONFIG_FILES, DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveConfig, resolveDevConfig, resolveStackName, resolveTestConfig, usesLocalWordPress };
171
+ export { CONFIG_FILES, DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveConfig, resolveDevConfig, resolveStackName, resolveTestConfig, stackProject, usesLocalWordPress };
@@ -47,6 +47,36 @@ function staticCounterpart(name) {
47
47
  function sameExpr(a, b) {
48
48
  return a.replace(/\s+/g, "") === b.replace(/\s+/g, "");
49
49
  }
50
+ /** The module's statement list (magicast's `$ast` is a `Program`, but tolerate a `File` wrapper too). */
51
+ function programBody(mod) {
52
+ const ast = mod.$ast;
53
+ return (ast.type === "File" ? ast.program : ast).body;
54
+ }
55
+ /** The name of an `export const <name> = …` statement, or undefined for any other node. */
56
+ function exportedConstName(node) {
57
+ if (node.type !== "ExportNamedDeclaration") return void 0;
58
+ const decl = node.declaration;
59
+ if (decl?.type !== "VariableDeclaration") return void 0;
60
+ const id = decl.declarations?.[0]?.id;
61
+ return id?.type === "Identifier" ? id.name : void 0;
62
+ }
63
+ /**
64
+ * Move the just-inserted exports (named in `names`) up to sit right after the import block, so wiring
65
+ * lands near the top of the file instead of appended below the component. magicast always appends a new
66
+ * export to the end of the program; this re-splices those nodes in after the last import, preserving
67
+ * their declared order. Exports we replaced in place are untouched — only nodes we added this run move.
68
+ */
69
+ function hoistExportsAfterImports(mod, names) {
70
+ const body = programBody(mod);
71
+ const moved = [];
72
+ for (const name of names) {
73
+ const idx = body.findIndex((node) => exportedConstName(node) === name);
74
+ if (idx !== -1) moved.push(body.splice(idx, 1)[0]);
75
+ }
76
+ let insertAt = 0;
77
+ for (let i = 0; i < body.length; i++) if (body[i]?.type === "ImportDeclaration") insertAt = i + 1;
78
+ body.splice(insertAt, 0, ...moved);
79
+ }
50
80
  /**
51
81
  * Merge a resolved patch into `source` by parsing it with magicast (a real TS/JS/JSX parser via Babel,
52
82
  * so declarations are bounded exactly — never by a text heuristic). Missing imports are added; each
@@ -57,6 +87,10 @@ function sameExpr(a, b) {
57
87
  * export body isn't meant to be hand-customized. Idempotent: an export already equal to ours is left
58
88
  * untouched, so re-running never churns the file.
59
89
  *
90
+ * magicast appends new exports to the end of the file; we then hoist the ones we added this run up to
91
+ * sit right after the import block, so wiring reads near the top instead of below the component. Exports
92
+ * replaced in place keep their position — only nodes newly inserted this run move.
93
+ *
60
94
  * Throws if the target cannot be parsed or regenerated. Callers must NOT swallow that — a file we
61
95
  * can't parse is one we must not guess at, so wiring falls back to a printed instruction instead.
62
96
  */
@@ -71,6 +105,7 @@ function applyPatchToSource(source, patch) {
71
105
  addedExports: [],
72
106
  replacedExports: []
73
107
  };
108
+ const insertedNames = [];
74
109
  for (const imp of patch.imports) for (const name of imp.names) {
75
110
  if (mod.imports.$items.some((item) => item.from === imp.module && item.imported === name)) continue;
76
111
  mod.imports.$append({
@@ -92,11 +127,14 @@ function applyPatchToSource(source, patch) {
92
127
  delete mod.exports[counterpart];
93
128
  mod.exports[exp.name] = builders.raw(exp.value);
94
129
  changes.replacedExports.push(exp.name);
130
+ insertedNames.push(exp.name);
95
131
  continue;
96
132
  }
97
133
  mod.exports[exp.name] = builders.raw(exp.value);
98
134
  changes.addedExports.push(exp.name);
135
+ insertedNames.push(exp.name);
99
136
  }
137
+ if (insertedNames.length) hoistExportsAfterImports(mod, insertedNames);
100
138
  return {
101
139
  text: generateCode(mod, { format }).code,
102
140
  changes
@@ -105,9 +143,11 @@ function applyPatchToSource(source, patch) {
105
143
  /**
106
144
  * Render a resolved patch as source the user can paste in, for the printed fallback when the target
107
145
  * can't be resolved or parsed. The same declared payload the confident apply upserts, so what we tell
108
- * the user to add never drifts from what we would have written.
146
+ * the user to add never drifts from what we would have written. A `note`-mode patch carries its own
147
+ * verbatim body (its change isn't an import/`export const`), so that is printed as-is when present.
109
148
  */
110
149
  function renderPatchCode(patch) {
150
+ if (patch.note) return patch.note.endsWith("\n") ? patch.note : `${patch.note}\n`;
111
151
  const imports = patch.imports.map((imp) => `import { ${imp.names.join(", ")} } from "${imp.module}"`);
112
152
  const exports = patch.exports.map((exp) => `export const ${exp.name} = ${exp.value}`);
113
153
  return `${[
@@ -1,5 +1,4 @@
1
- import { getVersion } from "../utils.js";
2
- import { resolvePatchTargetPath } from "./patch.js";
1
+ import { getVersion, readTsconfigPaths } from "../utils.js";
3
2
  import z from "zod/v4";
4
3
  import path from "node:path";
5
4
  import fs from "node:fs";
@@ -35,7 +34,9 @@ import fs from "node:fs";
35
34
  * overwrite policy.
36
35
  * - `patch` — a partial injection into a file the framework owns (the root layout's SEO exports):
37
36
  * it adds or replaces individual modules (imports/exports) inside the file, never the whole file.
38
- * Both commands try a confident apply, otherwise print the payload with placement instructions.
37
+ * Both commands try a confident apply, otherwise print the payload with placement instructions. A
38
+ * patch may instead set `mode: "note"` to always print its instructions rather than edit the file —
39
+ * for a change too shaped-unlike an import/export to auto-merge (Astro's `defineConfig` output/adapter).
39
40
  */
40
41
  const conventionsSchema = z.object({
41
42
  kizloDir: z.string(),
@@ -53,14 +54,16 @@ const patchSchema = z.object({
53
54
  role: z.string(),
54
55
  label: z.string(),
55
56
  path: z.string(),
57
+ mode: z.enum(["apply", "note"]).default("apply"),
56
58
  imports: z.array(z.object({
57
59
  module: z.string(),
58
60
  names: z.array(z.string())
59
- })),
61
+ })).default([]),
60
62
  exports: z.array(z.object({
61
63
  name: z.string(),
62
64
  value: z.string()
63
- }))
65
+ })).default([]),
66
+ note: z.string().optional()
64
67
  });
65
68
  const bootstrapSchema = z.object({
66
69
  initializer: z.string(),
@@ -239,10 +242,41 @@ function patchEntries(changes) {
239
242
  function isExample(change) {
240
243
  return change.kind === "file" && change.example === true;
241
244
  }
242
- /** The specifier the template's files use to reach the server entry, e.g. `@/lib/kizlo/server`. */
243
- function serverImportSpecifier(conventions) {
244
- const withoutSrc = conventions.kizloDir.replace(/^src\//, "");
245
- return `${conventions.alias.replace(/\/+$/, "")}/${withoutSrc}/server`;
245
+ /** Escape a string so its characters are matched literally inside a RegExp. */
246
+ function escapeRegExp(value) {
247
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
248
+ }
249
+ /**
250
+ * The directory the template's alias resolves to — the maintainer's own preference, read straight from
251
+ * the template's tsconfig `paths` (e.g. `@/* → ./src/*` yields `src`). This is the root stripped to turn
252
+ * an alias import back into a real template path before it's remapped to the project. Falls back to the
253
+ * `src`/root heuristic when the template declares no matching alias, staying consistent with the rest of
254
+ * the alias handling.
255
+ */
256
+ function templateAliasBase(templateDir, conventions) {
257
+ const alias = conventions.alias.replace(/\/+$/, "");
258
+ const mapping = readTsconfigPaths(templateDir)?.[`${alias}/*`]?.[0];
259
+ if (mapping) return mapping.replace(/^\.\//, "").replace(/\/\*$/, "").replace(/\/+$/, "");
260
+ return conventions.kizloDir.startsWith("src/") ? "src" : "";
261
+ }
262
+ /**
263
+ * Rewrite every template-alias import in a file to the project's chosen import style — the one place that
264
+ * translates the maintainer's import preference into the user's. A template writes its cross-file imports
265
+ * against its own alias (`@/layouts/Layout.astro`, `@/lib/kizlo/server`), rooted at the directory its
266
+ * tsconfig maps the alias to (`base`). On apply each such specifier is resolved back to the imported
267
+ * file's real location in the project (via {@link adaptTemplatePath}, so a relocated Kizlo dir is honored)
268
+ * and re-emitted through the project's alias — or a relative import when the user wants no alias at all.
269
+ * Only quoted specifiers that begin with the template alias are touched, so an unrelated string can't be
270
+ * corrupted. Subsumes the server-entry import: it is simply the alias import that lands in the Kizlo dir.
271
+ */
272
+ function rewriteAliasImports(body, fromDir, base, conventions, ctx) {
273
+ const alias = conventions.alias.replace(/\/+$/, "");
274
+ if (!alias) return body;
275
+ const pattern = new RegExp(`(["'])${escapeRegExp(alias)}/([^"']+)\\1`, "g");
276
+ return body.replace(pattern, (_match, quote, rest) => {
277
+ const targetRel = adaptTemplatePath(base ? `${base}/${rest}` : rest, conventions, ctx);
278
+ return `${quote}${ctx.importFrom(targetRel, fromDir)}${quote}`;
279
+ });
246
280
  }
247
281
  /**
248
282
  * Adapt a template path prefix (`src/app/...`, `src/lib/kizlo/...`) to the project's real directories.
@@ -256,19 +290,18 @@ function adaptTemplatePath(relPath, conventions, ctx) {
256
290
  }
257
291
  /**
258
292
  * Turn a `file` manifest entry into the {@link ScaffoldFile} init writes: read the template's real
259
- * file, rewrite its path prefix to the project's directories, and swap the server-entry import
260
- * specifier for the one resolved at the file's new location (tsconfig alias or relative). The swap is
261
- * surgical — it replaces the exact specifier the template uses, not a loose directory substring — so
262
- * an unrelated occurrence can't be corrupted. This is the job the old build-time extractor did; it
263
- * moves to runtime and stays as precise.
293
+ * file, rewrite its path prefix to the project's directories, and retarget every template-alias import
294
+ * (the server entry plus any sibling like Astro's `@/layouts/Layout.astro`) to the specifier resolved
295
+ * at the file's new location — the project's alias or a relative import. Only the exact alias specifiers
296
+ * the template uses are rewritten, not a loose directory substring, so an unrelated occurrence can't be
297
+ * corrupted. This is the job the old build-time extractor did; it moves to runtime and stays as precise.
264
298
  */
265
299
  function adaptFile(templateDir, entry, conventions, ctx) {
266
300
  const abs = path.join(templateDir, entry.path);
267
301
  if (!fs.existsSync(abs)) throw new Error(`Template file "${entry.path}" is listed in the manifest but does not exist in the template.`);
268
302
  const body = fs.readFileSync(abs, "utf8");
269
303
  const relPath = adaptTemplatePath(entry.path, conventions, ctx);
270
- const fromDir = path.posix.dirname(relPath);
271
- const contents = body.split(serverImportSpecifier(conventions)).join(ctx.serverImport(fromDir));
304
+ const contents = rewriteAliasImports(body, path.posix.dirname(relPath), templateAliasBase(templateDir, conventions), conventions, ctx);
272
305
  return {
273
306
  label: ROLE_LABELS[entry.role] ?? entry.role,
274
307
  relPath,
@@ -286,43 +319,12 @@ function resolvePatch(entry, conventions, ctx) {
286
319
  return {
287
320
  label: entry.label,
288
321
  relPath,
322
+ mode: entry.mode,
289
323
  imports,
290
- exports: entry.exports
324
+ exports: entry.exports,
325
+ note: entry.note
291
326
  };
292
327
  }
293
- const LAYOUT_FILE = /^layout\.(tsx|jsx|ts|js|mjs|cjs)$/;
294
- /** A root layout is the one that renders `<html>` — its identity, not its path. */
295
- const RENDERS_HTML = /<html[\s/>]/;
296
- /** Every file under `dir`, recursively (files only). Missing dir yields nothing. */
297
- function walk(dir) {
298
- if (!fs.existsSync(dir)) return [];
299
- const out = [];
300
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
301
- const full = path.join(dir, entry.name);
302
- if (entry.isDirectory()) out.push(...walk(full));
303
- else out.push(full);
304
- }
305
- return out;
306
- }
307
- /**
308
- * Resolve the root layout by identity, not path. Try the manifest hint first; if it exists and
309
- * renders `<html>`, use it. Otherwise scan the App Router directory for a layout that renders `<html>`
310
- * and use it only when there is exactly one. On zero, many, or an unreadable file, return undefined so
311
- * the caller prints the wiring for the user to place. Never guesses.
312
- */
313
- function findRootLayout(cwd, appDir, hintRelPath) {
314
- const hint = resolvePatchTargetPath(cwd, hintRelPath);
315
- if (hint && rendersHtml(hint)) return hint;
316
- const matches = walk(path.join(cwd, appDir)).filter((file) => LAYOUT_FILE.test(path.basename(file)) && rendersHtml(file));
317
- return matches.length === 1 ? matches[0] : void 0;
318
- }
319
- function rendersHtml(file) {
320
- try {
321
- return RENDERS_HTML.test(fs.readFileSync(file, "utf8"));
322
- } catch {
323
- return false;
324
- }
325
- }
326
328
 
327
329
  //#endregion
328
- export { adaptFile, changesFor, fileEntries, findRootLayout, isOlderVersion, minCliError, patchEntries, readManifest, renderNote, resolveDependencies, resolvePatch };
330
+ export { adaptFile, changesFor, fileEntries, isOlderVersion, minCliError, patchEntries, readManifest, renderNote, resolveDependencies, resolvePatch };
package/dist/cli/utils.js CHANGED
@@ -45,6 +45,30 @@ function readTsconfigPaths(cwd) {
45
45
  }
46
46
  return json.compilerOptions?.paths;
47
47
  }
48
+ /**
49
+ * The kizlo.config file names, in resolution order. Mirrors `CONFIG_FILES` in daemon/config — kept here
50
+ * to avoid an import cycle, since daemon/config already depends on this module.
51
+ */
52
+ const KIZLO_CONFIG_FILES = [
53
+ "kizlo.config.ts",
54
+ "kizlo.config.js",
55
+ "kizlo.config.mjs"
56
+ ];
57
+ /**
58
+ * The import alias a previous `kizlo init` recorded in kizlo.config — the persisted preference a re-run
59
+ * reuses so it never asks again. Returns the prefix (`""` for the deliberate relative-imports choice),
60
+ * or `undefined` when there's no config or it declares no `alias` at all (the choice was never made, so
61
+ * the caller must prompt or default). A light text scan of the generated file, so setup never has to
62
+ * import the TS config — or resolve `kizlo` — just to read one field back.
63
+ */
64
+ function readPersistedAlias(cwd) {
65
+ for (const name of KIZLO_CONFIG_FILES) {
66
+ const file = path.join(cwd, name);
67
+ if (!fs.existsSync(file)) continue;
68
+ const match = fs.readFileSync(file, "utf8").match(/\balias\s*:\s*["']([^"']*)["']/);
69
+ return match ? match[1] : void 0;
70
+ }
71
+ }
48
72
  function relativeImport(targetRel, fromDirRel) {
49
73
  const rel = path.relative(fromDirRel, targetRel).split(path.sep).join("/");
50
74
  return rel.startsWith(".") ? rel : `./${rel}`;
@@ -71,6 +95,14 @@ function detectImportAlias(cwd, targetRel) {
71
95
  }
72
96
  }
73
97
  /**
98
+ * Normalize an alias prefix to the canonical `@/` form — the way an import alias is actually written and
99
+ * declared in tsconfig `paths` (`@/*`), so the persisted preference reads like the imports it produces.
100
+ * A trailing slash is enforced (`@` → `@/`); empty stays empty, the recorded "relative imports" choice.
101
+ */
102
+ function aliasWithSlash(alias) {
103
+ return alias ? `${alias.replace(/\/+$/, "")}/` : "";
104
+ }
105
+ /**
74
106
  * Builds an import specifier for `targetRel` from `fromDirRel`. With an explicit
75
107
  * `alias` prefix (from kizlo.config), builds `<alias>/<target>`; an empty alias
76
108
  * forces a relative import. Without one, detects from tsconfig, else relative.
@@ -101,11 +133,62 @@ function getVersion() {
101
133
  const pkgPath = path.resolve(here, "../../package.json");
102
134
  return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version;
103
135
  }
136
+ /**
137
+ * The manager a project pins via package.json's corepack `packageManager` field (e.g. `"pnpm@9.0.0"`
138
+ * → `pnpm`). Declared intent that's present *before* the first install, so it's a reliable signal on a
139
+ * freshly bootstrapped project that has no lockfile yet. Undefined when the field is absent, unparseable,
140
+ * or names an unsupported manager.
141
+ */
142
+ function packageManagerField(cwd) {
143
+ try {
144
+ const name = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8")).packageManager?.split("@")[0];
145
+ return name === "pnpm" || name === "yarn" || name === "bun" || name === "npm" ? name : void 0;
146
+ } catch {
147
+ return;
148
+ }
149
+ }
150
+ /**
151
+ * The package manager a project uses, from evidence in the project itself — a lockfile (the strongest
152
+ * signal, it reflects an actual install), else package.json's corepack `packageManager` field (declared
153
+ * intent, present before the first install). Returns `undefined` when neither is present rather than
154
+ * guessing: the invoking manager is unreliable (`npx` reports `npm` whatever the user really uses), and a
155
+ * missing lockfile can just mean a freshly bootstrapped project in a monorepo. Callers that can't proceed
156
+ * without one should ask the user (see `selectPackageManager`) instead of defaulting.
157
+ */
104
158
  function detectPackageManager(cwd) {
105
159
  if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
106
160
  if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
107
161
  if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) return "bun";
108
- return "npm";
162
+ return packageManagerField(cwd);
163
+ }
164
+ /** The installed version of `pm`, from `pm --version` (e.g. `"9.0.0"`). Undefined when it can't be run. */
165
+ function packageManagerVersion(pm) {
166
+ const result = spawnSync(pm, ["--version"], {
167
+ encoding: "utf8",
168
+ shell: process.platform === "win32"
169
+ });
170
+ if (result.error || result.status !== 0) return void 0;
171
+ const version = result.stdout.trim();
172
+ return version.length > 0 ? version : void 0;
173
+ }
174
+ /**
175
+ * Record `pm` as package.json's corepack `packageManager` field so a later `detectPackageManager` settles on
176
+ * it instead of asking again — the fix for a project that gave no signal and had to be prompted for. Stamps
177
+ * the installed version (`pm --version`) to keep the field corepack-valid, dropping it only when the version
178
+ * can't be read. Leaves an existing field untouched, preserves the file's indentation, and swallows any
179
+ * read/write failure: persisting the choice is a convenience, not worth failing the command over.
180
+ */
181
+ function persistPackageManagerField(cwd, pm) {
182
+ const pkgPath = path.join(cwd, "package.json");
183
+ try {
184
+ const raw = fs.readFileSync(pkgPath, "utf8");
185
+ const pkg = JSON.parse(raw);
186
+ if (pkg.packageManager) return;
187
+ const version = packageManagerVersion(pm);
188
+ pkg.packageManager = version ? `${pm}@${version}` : pm;
189
+ const indent = raw.match(/\n([ \t]+)/)?.[1] ?? " ";
190
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, indent)}\n`);
191
+ } catch {}
109
192
  }
110
193
  /**
111
194
  * The package manager that invoked the current process, read from `npm_config_user_agent`
@@ -132,23 +215,40 @@ function isCommandAvailable(command) {
132
215
  function availablePackageManagers(order) {
133
216
  return order.filter((pm) => isCommandAvailable(pm));
134
217
  }
135
- function addDependencyArgs(pm, pkg) {
136
- if (pm === "npm") return [
218
+ /**
219
+ * The argv to add a single package with `pm`, e.g. `pnpm add kizlo`. `dev: true` installs it as a
220
+ * dev dependency — npm spells that `--save-dev`, every other supported manager `--dev`.
221
+ */
222
+ function addDependencyArgs(pm, pkg, opts = {}) {
223
+ const base = pm === "npm" ? [
137
224
  "npm",
138
225
  "install",
139
226
  pkg
140
- ];
141
- return [
227
+ ] : [
142
228
  pm,
143
229
  "add",
144
230
  pkg
145
231
  ];
232
+ if (!opts.dev) return base;
233
+ return [...base, pm === "npm" ? "--save-dev" : "--dev"];
146
234
  }
147
235
  /** The argv to install a project's dependencies with `pm` — every supported manager spells it `<pm> install`. */
148
236
  function installArgs(pm) {
149
237
  return [pm, "install"];
150
238
  }
151
239
  /**
240
+ * pnpm (v10+) and bun refuse to run dependencies' build/lifecycle scripts on install by default, leaving
241
+ * those packages unbuilt — pnpm prints an `Ignored build scripts:` warning, bun a `Blocked N postinstall`
242
+ * one. The install still exits 0, so nothing else flags it. Scan an install's captured output for that
243
+ * warning and, when present, return the command that runs the blocked scripts (`pnpm approve-builds` /
244
+ * `bun pm trust --all`) so the caller can surface it as a follow-up step. Undefined when the output shows
245
+ * none, or the manager runs scripts on install anyway (npm, yarn) so there's nothing to approve.
246
+ */
247
+ function approveBuildsCommand(pm, output) {
248
+ if (pm === "pnpm" && /ignored build scripts/i.test(output)) return "pnpm approve-builds";
249
+ if (pm === "bun" && /blocked \d+ postinstall/i.test(output)) return "bun pm trust --all";
250
+ }
251
+ /**
152
252
  * The argv to run a framework's `create-*` initializer through a package manager, e.g.
153
253
  * `pnpm create next-app@latest my-app --ts …`. The mechanics differ per manager: npm forwards
154
254
  * flags to the initializer only after a `--` separator, and `yarn create` resolves the latest
@@ -180,6 +280,15 @@ function frameworkCreateArgs(pm, initializer, name, flags) {
180
280
  ];
181
281
  }
182
282
  }
283
+ function runCommand(args, cwd, stdio = "inherit") {
284
+ const [command, ...rest] = args;
285
+ if (!command) return false;
286
+ return spawnSync(command, rest, {
287
+ cwd,
288
+ stdio,
289
+ shell: process.platform === "win32"
290
+ }).status === 0;
291
+ }
183
292
  /**
184
293
  * Async sibling of {@link runCommand}: runs a command without blocking the event loop, so a clack
185
294
  * spinner can keep animating while it works (a synchronous `spawnSync` would freeze it on the first
@@ -219,6 +328,40 @@ function runCommandCaptured(args, cwd) {
219
328
  output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
220
329
  };
221
330
  }
331
+ /**
332
+ * Async sibling of {@link runCommandCaptured}: captures a command's combined output without blocking the
333
+ * event loop, so a clack spinner keeps animating while it runs (the synchronous `runCommandCaptured` would
334
+ * freeze it on the first frame). Resolves whether it exited cleanly plus the trimmed stdout+stderr; a
335
+ * spawn error (binary off PATH) resolves `{ ok: false, output: "" }`.
336
+ */
337
+ function runCommandCapturedAsync(args, cwd) {
338
+ const [command, ...rest] = args;
339
+ if (!command) return Promise.resolve({
340
+ ok: false,
341
+ output: ""
342
+ });
343
+ return new Promise((resolve$1) => {
344
+ const child = spawn(command, rest, {
345
+ cwd,
346
+ shell: process.platform === "win32"
347
+ });
348
+ let output = "";
349
+ child.stdout?.on("data", (chunk) => {
350
+ output += chunk;
351
+ });
352
+ child.stderr?.on("data", (chunk) => {
353
+ output += chunk;
354
+ });
355
+ child.on("error", () => resolve$1({
356
+ ok: false,
357
+ output: ""
358
+ }));
359
+ child.on("close", (code) => resolve$1({
360
+ ok: code === 0,
361
+ output: output.trim()
362
+ }));
363
+ });
364
+ }
222
365
  function writeFileIfAbsent(filePath, contents) {
223
366
  if (fs.existsSync(filePath)) return false;
224
367
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
@@ -333,6 +476,42 @@ function mergeEnv(existing, values, overwriteKeys, groups) {
333
476
  kept
334
477
  };
335
478
  }
479
+ /**
480
+ * Whether `dir` already sits inside a git work tree — true when the framework CLI ran `git init`, or
481
+ * when scaffolding into a subfolder of an existing repo (e.g. a monorepo). Used to skip offering
482
+ * `git init` where a repo is already present rather than nesting one. A missing `git` binary (spawn
483
+ * error) reads as "not a repo".
484
+ */
485
+ function isGitRepository(dir) {
486
+ const result = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
487
+ cwd: dir,
488
+ stdio: "ignore",
489
+ shell: process.platform === "win32"
490
+ });
491
+ return !result.error && result.status === 0;
492
+ }
493
+ /**
494
+ * Initialize a git repository in `dir` with an initial commit. Best-effort: returns whether the repo
495
+ * was created (`git init` succeeded). The commit is attempted but not required — it needs a configured
496
+ * `user.name`/`user.email` a bare machine may lack, and a repo with staged-but-uncommitted files is
497
+ * still a usable starting point. Returns false when `git` isn't installed.
498
+ */
499
+ function initGitRepository(dir) {
500
+ if (!isCommandAvailable("git")) return false;
501
+ if (!runCommand(["git", "init"], dir, "ignore")) return false;
502
+ runCommand([
503
+ "git",
504
+ "add",
505
+ "-A"
506
+ ], dir, "ignore");
507
+ runCommand([
508
+ "git",
509
+ "commit",
510
+ "-m",
511
+ "Initial commit"
512
+ ], dir, "ignore");
513
+ return true;
514
+ }
336
515
  function ensureGitignored(cwd, entry) {
337
516
  const gitignorePath = path.join(cwd, ".gitignore");
338
517
  const existed = fs.existsSync(gitignorePath);
@@ -376,4 +555,4 @@ function groupDefault(subCommandNames, run) {
376
555
  }
377
556
 
378
557
  //#endregion
379
- export { DEFAULT_ENV_KEYS, addDependencyArgs, availablePackageManagers, detectImportAlias, detectInvokingPackageManager, detectPackageManager, ensureGitignored, envGroups, envKeysPresent, frameworkCreateArgs, getVersion, groupDefault, installArgs, loadEnvFiles, mergeEnv, pickStackPort, resolveModuleImport, runCommandAsync, runCommandCaptured, withSpinner, writeFileIfAbsent };
558
+ export { DEFAULT_ENV_KEYS, addDependencyArgs, aliasWithSlash, approveBuildsCommand, availablePackageManagers, detectImportAlias, detectInvokingPackageManager, detectPackageManager, ensureGitignored, envGroups, envKeysPresent, frameworkCreateArgs, getVersion, groupDefault, initGitRepository, installArgs, isCommandAvailable, isGitRepository, loadEnvFiles, mergeEnv, persistPackageManagerField, pickStackPort, readPersistedAlias, readTsconfigPaths, resolveModuleImport, runCommandAsync, runCommandCaptured, runCommandCapturedAsync, withSpinner, writeFileIfAbsent };
@@ -8,7 +8,7 @@ function localMounts(fixtures) {
8
8
  return fixtures.flatMap((fixture) => fixture.plugins ?? []).filter(isLocalPlugin).map((plugin) => plugin.path);
9
9
  }
10
10
  /**
11
- * The test stack: the base compose file under a `<name>-test` project, plus a generated
11
+ * The test stack: the base compose file under a `kizlo-<name>-test` project, plus a generated
12
12
  * mount override when any fixture carries a local (`{ path }`) plugin — so the same
13
13
  * fixtures bind-mount their live plugin source here exactly as in local WordPress.
14
14
  */
@@ -37,7 +37,7 @@ function detectHostUser() {
37
37
  }
38
38
  /**
39
39
  * Local WordPress: the base compose file plus the generated mount override, under a
40
- * `<name>-dev` project. Building it (re)writes the override so it always matches the
40
+ * `kizlo-<name>-dev` project. Building it (re)writes the override so it always matches the
41
41
  * install folder (`.kizlo/local`) and the `dev.fixtures` local plugins.
42
42
  */
43
43
  function devStack(cfg) {
package/dist/config.d.ts CHANGED
@@ -63,12 +63,13 @@ interface KizloGlobalConfig {
63
63
  */
64
64
  dir?: string;
65
65
  /**
66
- * Import alias prefix for generated imports (e.g. `@`). Detected from
67
- * tsconfig at init; omit (or empty) to use relative imports.
66
+ * Import alias prefix for generated imports (e.g. `@`). Recorded by `kizlo init`
67
+ * from your `--alias` flag or its prompt, so later runs reuse it instead of asking
68
+ * again; an empty string (`""`) is the recorded choice to use relative imports.
68
69
  */
69
70
  alias?: string;
70
71
  /**
71
- * Base name for the local Docker stacks (`<name>-dev`, `<name>-test`). Defaults
72
+ * Base name for the local Docker stacks (`kizlo-<name>-dev`, `kizlo-<name>-test`). Defaults
72
73
  * to the sanitized `package.json` name, falling back to the config dir basename.
73
74
  */
74
75
  name?: string;
package/dist/context.js CHANGED
@@ -6,7 +6,7 @@ import { WordPressService } from "./wordpress/service.js";
6
6
  import { EmailService } from "./email/service.js";
7
7
  import { SettingsService } from "./settings/service.js";
8
8
  import { PLUGIN_VERSION_HEADER, base64Decode, isPluginVersionSupported, pluginUpdateMessage, tryCatchSync } from "@kizlo/shared";
9
- import { serialize } from "cookie";
9
+ import { stringifySetCookie } from "cookie";
10
10
 
11
11
  //#region src/context.ts
12
12
  var Context = class {
@@ -93,13 +93,19 @@ var Context = class {
93
93
  },
94
94
  setAll: async (cookies) => {
95
95
  cookies.forEach((cookie) => {
96
- headers.append("Set-Cookie", serialize(cookie.name, cookie.value, cookie.options));
96
+ headers.append("Set-Cookie", stringifySetCookie({
97
+ ...cookie.options,
98
+ name: cookie.name,
99
+ value: cookie.value
100
+ }));
97
101
  });
98
102
  },
99
103
  deleteAll: async (cookies) => {
100
104
  cookies.forEach((cookie) => {
101
- headers.append("Set-Cookie", serialize(cookie.name, "", {
105
+ headers.append("Set-Cookie", stringifySetCookie({
102
106
  ...cookie.options,
107
+ name: cookie.name,
108
+ value: "",
103
109
  maxAge: 0,
104
110
  expires: /* @__PURE__ */ new Date(0)
105
111
  }));
@@ -13,6 +13,12 @@ declare function createRootMetadata(client: S2SClient<[]>): () => Promise<Metada
13
13
  * is set, both are emitted with `prefers-color-scheme` media queries.
14
14
  */
15
15
  declare function createRootViewport(client: S2SClient<[]>): () => Promise<Viewport>;
16
+ /**
17
+ * Home `generateMetadata` for `app/page.tsx`, resolving the homepage SEO head from WordPress.
18
+ * A factory (rather than the inline call the `create` template shows) so `init` can wire it into
19
+ * an existing home page as a single `export const generateMetadata = createHomeMetadata(client)`.
20
+ */
21
+ declare function createHomeMetadata(client: S2SClient<[]>): () => Promise<Metadata>;
16
22
  declare function createPageMetadata(head: SeoHead): Metadata;
17
23
  //#endregion
18
- export { createPageMetadata, createRootMetadata, createRootViewport };
24
+ export { createHomeMetadata, createPageMetadata, createRootMetadata, createRootViewport };
@@ -49,6 +49,17 @@ function toNextIcon(icon) {
49
49
  sizes: icon.sizes
50
50
  };
51
51
  }
52
+ /**
53
+ * Home `generateMetadata` for `app/page.tsx`, resolving the homepage SEO head from WordPress.
54
+ * A factory (rather than the inline call the `create` template shows) so `init` can wire it into
55
+ * an existing home page as a single `export const generateMetadata = createHomeMetadata(client)`.
56
+ */
57
+ function createHomeMetadata(client) {
58
+ return async function generateMetadata() {
59
+ const { head } = await client.seo.homepage.call();
60
+ return createPageMetadata(head);
61
+ };
62
+ }
52
63
  function createPageMetadata(head) {
53
64
  return {
54
65
  title: { absolute: head.title },
@@ -90,4 +101,4 @@ function createPageMetadata(head) {
90
101
  }
91
102
 
92
103
  //#endregion
93
- export { createPageMetadata, createRootMetadata, createRootViewport };
104
+ export { createHomeMetadata, createPageMetadata, createRootMetadata, createRootViewport };
@@ -4,8 +4,8 @@ import { createSitemapRedirectRoute } from "../seo/sitemap.js";
4
4
  import { NextRevalidateOptions, RevalidatePathFn, RevalidateTagFn, RevalidateTarget, nextRevalidation } from "../integrations/nextjs/revalidate.js";
5
5
  import { KizloServerOptions, createKizlo } from "../integrations/nextjs/kizlo.server.js";
6
6
  import { MANIFEST_CACHE_TAG, createManifestRoute } from "../integrations/nextjs/manifest.js";
7
- import { createPageMetadata, createRootMetadata, createRootViewport } from "../integrations/nextjs/metadata.js";
7
+ import { createHomeMetadata, createPageMetadata, createRootMetadata, createRootViewport } from "../integrations/nextjs/metadata.js";
8
8
  import { ROBOTS_CACHE_TAG, createRobotsRoute } from "../integrations/nextjs/robots.js";
9
9
  import { SITEMAP_CACHE_TAG, SITEMAP_ROUTE, createSitemapRoute } from "../integrations/nextjs/sitemap.js";
10
10
  import { createCachedTextRoute, createNextCookiesInterface, getServerBaseUrl } from "../integrations/nextjs/utils.js";
11
- export { KizloServerOptions, MANIFEST_CACHE_TAG, NextRevalidateOptions, ROBOTS_CACHE_TAG, RevalidatePathFn, RevalidateTagFn, RevalidateTarget, SITEMAP_CACHE_TAG, SITEMAP_ROUTE, WEB_MANIFEST_ROUTE, createCachedTextRoute, createKizlo, createManifestRoute, createNextCookiesInterface, createPageMetadata, createRobotsRoute, createRootMetadata, createRootViewport, createSitemapRedirectRoute, createSitemapRoute, getServerBaseUrl, nextRevalidation, renderJsonLd };
11
+ export { KizloServerOptions, MANIFEST_CACHE_TAG, NextRevalidateOptions, ROBOTS_CACHE_TAG, RevalidatePathFn, RevalidateTagFn, RevalidateTarget, SITEMAP_CACHE_TAG, SITEMAP_ROUTE, WEB_MANIFEST_ROUTE, createCachedTextRoute, createHomeMetadata, createKizlo, createManifestRoute, createNextCookiesInterface, createPageMetadata, createRobotsRoute, createRootMetadata, createRootViewport, createSitemapRedirectRoute, createSitemapRoute, getServerBaseUrl, nextRevalidation, renderJsonLd };
@@ -7,6 +7,6 @@ import { SITEMAP_CACHE_TAG, SITEMAP_ROUTE, createSitemapRoute } from "../integra
7
7
  import { nextRevalidation } from "../integrations/nextjs/revalidate.js";
8
8
  import { createKizlo } from "../integrations/nextjs/kizlo.server.js";
9
9
  import { MANIFEST_CACHE_TAG, createManifestRoute } from "../integrations/nextjs/manifest.js";
10
- import { createPageMetadata, createRootMetadata, createRootViewport } from "../integrations/nextjs/metadata.js";
10
+ import { createHomeMetadata, createPageMetadata, createRootMetadata, createRootViewport } from "../integrations/nextjs/metadata.js";
11
11
 
12
- export { MANIFEST_CACHE_TAG, ROBOTS_CACHE_TAG, SITEMAP_CACHE_TAG, SITEMAP_ROUTE, WEB_MANIFEST_ROUTE, createCachedTextRoute, createKizlo, createManifestRoute, createNextCookiesInterface, createPageMetadata, createRobotsRoute, createRootMetadata, createRootViewport, createSitemapRedirectRoute, createSitemapRoute, getServerBaseUrl, nextRevalidation, renderJsonLd };
12
+ export { MANIFEST_CACHE_TAG, ROBOTS_CACHE_TAG, SITEMAP_CACHE_TAG, SITEMAP_ROUTE, WEB_MANIFEST_ROUTE, createCachedTextRoute, createHomeMetadata, createKizlo, createManifestRoute, createNextCookiesInterface, createPageMetadata, createRobotsRoute, createRootMetadata, createRootViewport, createSitemapRedirectRoute, createSitemapRoute, getServerBaseUrl, nextRevalidation, renderJsonLd };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.12.0";
2
+ var version = "0.13.0";
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?: "date" | "id" | "slug" | "parent" | "title" | "author" | "include" | "modified" | "relevance" | "include_slugs" | undefined;
182
+ orderby?: "id" | "date" | "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?: "date" | "id" | "slug" | "parent" | "title" | "author" | "include" | "modified" | "relevance" | "include_slugs" | "menu_order" | undefined;
510
+ orderby?: "id" | "date" | "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;
@@ -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?: "date" | "id" | "slug" | "parent" | "title" | "author" | "include" | "relevance" | "include_slugs" | "menu_order" | undefined;
1189
+ orderby?: "id" | "date" | "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?: "date" | "id" | "slug" | "parent" | "title" | "author" | "include" | "relevance" | "include_slugs" | "menu_order" | undefined;
1268
+ orderby?: "id" | "date" | "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?: "date" | "id" | "parent" | "post" | "include" | undefined;
1417
+ orderby?: "id" | "date" | "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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kizlo",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "A headless WordPress framework for TypeScript.",
6
6
  "keywords": [
@@ -97,7 +97,7 @@
97
97
  "chokidar": "^5.0.0",
98
98
  "citty": "^0.1.6",
99
99
  "consola": "^3.2.3",
100
- "cookie": "^1.0.2",
100
+ "cookie": "^2.0.1",
101
101
  "get-port": "^7.2.0",
102
102
  "giget": "^3.0.0",
103
103
  "jiti": "^2.7.0",