kizlo 0.12.1 → 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,6 +1,6 @@
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";
3
+ import { DEFAULT_ENV_KEYS, availablePackageManagers, detectInvokingPackageManager, ensureGitignored, envGroups, envKeysPresent, mergeEnv, pickStackPort, writeFileIfAbsent } from "../utils.js";
4
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";
@@ -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,
@@ -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,4 +1,4 @@
1
- import { resolveModuleImport, writeFileIfAbsent } from "../utils.js";
1
+ import { aliasWithSlash, resolveModuleImport, writeFileIfAbsent } from "../utils.js";
2
2
  import { resolvePatch } from "../presets/template.js";
3
3
  import { orCancel } from "./_setup.js";
4
4
  import { CONTRACT_BARREL } from "../daemon/generate.js";
@@ -8,32 +8,37 @@ 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
  }
@@ -85,6 +90,11 @@ function applyProjectPatches(cwd, patches, conventions, scaffold) {
85
90
  const manualSteps = [];
86
91
  for (const entry of patches) {
87
92
  const resolved = resolvePatch(entry, conventions, scaffold);
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
+ }
88
98
  const target = resolvePatchTargetPath(cwd, resolved.relPath);
89
99
  if (!target) {
90
100
  manualSteps.push(resolved);
@@ -107,8 +117,19 @@ function applyProjectPatches(cwd, patches, conventions, scaffold) {
107
117
  p.log.success(`Wired Kizlo into your ${resolved.label} (${relTarget})`);
108
118
  } else p.log.info(`Your ${resolved.label} is already wired (${relTarget})`);
109
119
  }
110
- for (const resolved of manualSteps) p.note(renderPatchCode(resolved), `Add these to your ${resolved.label} (${resolved.relPath})`);
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`);
111
132
  }
112
133
 
113
134
  //#endregion
114
- export { applyProjectPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract };
135
+ export { applyProjectPatches, buildScaffoldContext, kizloConfigTemplate, printManualStep, reportScaffold, scaffoldFile, writeGeneratedContract };
@@ -1,7 +1,7 @@
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";
4
+ import { collectConnectionInteractively, nextStepsLines, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, selectPackageManager, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
5
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";
@@ -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
@@ -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 { applyProjectPatches, 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?",
@@ -284,7 +316,7 @@ const init = defineCommand({
284
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);
@@ -130,7 +130,7 @@ async function resolveTestConfig(cwd) {
130
130
  portExplicit: test.port !== void 0,
131
131
  fixtures: test.fixtures ?? [],
132
132
  credentialsPath: credentialsPath(cwd),
133
- packageManager: test.packageManager ?? detectPackageManager(configDir)
133
+ packageManager: test.packageManager ?? detectPackageManager(configDir) ?? "npm"
134
134
  };
135
135
  }
136
136
  /**
@@ -143,9 +143,11 @@ function applyPatchToSource(source, patch) {
143
143
  /**
144
144
  * Render a resolved patch as source the user can paste in, for the printed fallback when the target
145
145
  * can't be resolved or parsed. The same declared payload the confident apply upserts, so what we tell
146
- * 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.
147
148
  */
148
149
  function renderPatchCode(patch) {
150
+ if (patch.note) return patch.note.endsWith("\n") ? patch.note : `${patch.note}\n`;
149
151
  const imports = patch.imports.map((imp) => `import { ${imp.names.join(", ")} } from "${imp.module}"`);
150
152
  const exports = patch.exports.map((exp) => `export const ${exp.name} = ${exp.value}`);
151
153
  return `${[
@@ -1,4 +1,4 @@
1
- import { getVersion } from "../utils.js";
1
+ import { getVersion, readTsconfigPaths } from "../utils.js";
2
2
  import z from "zod/v4";
3
3
  import path from "node:path";
4
4
  import fs from "node:fs";
@@ -34,7 +34,9 @@ import fs from "node:fs";
34
34
  * overwrite policy.
35
35
  * - `patch` — a partial injection into a file the framework owns (the root layout's SEO exports):
36
36
  * it adds or replaces individual modules (imports/exports) inside the file, never the whole file.
37
- * 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).
38
40
  */
39
41
  const conventionsSchema = z.object({
40
42
  kizloDir: z.string(),
@@ -52,14 +54,16 @@ const patchSchema = z.object({
52
54
  role: z.string(),
53
55
  label: z.string(),
54
56
  path: z.string(),
57
+ mode: z.enum(["apply", "note"]).default("apply"),
55
58
  imports: z.array(z.object({
56
59
  module: z.string(),
57
60
  names: z.array(z.string())
58
- })),
61
+ })).default([]),
59
62
  exports: z.array(z.object({
60
63
  name: z.string(),
61
64
  value: z.string()
62
- }))
65
+ })).default([]),
66
+ note: z.string().optional()
63
67
  });
64
68
  const bootstrapSchema = z.object({
65
69
  initializer: z.string(),
@@ -238,10 +242,41 @@ function patchEntries(changes) {
238
242
  function isExample(change) {
239
243
  return change.kind === "file" && change.example === true;
240
244
  }
241
- /** The specifier the template's files use to reach the server entry, e.g. `@/lib/kizlo/server`. */
242
- function serverImportSpecifier(conventions) {
243
- const withoutSrc = conventions.kizloDir.replace(/^src\//, "");
244
- 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
+ });
245
280
  }
246
281
  /**
247
282
  * Adapt a template path prefix (`src/app/...`, `src/lib/kizlo/...`) to the project's real directories.
@@ -255,19 +290,18 @@ function adaptTemplatePath(relPath, conventions, ctx) {
255
290
  }
256
291
  /**
257
292
  * Turn a `file` manifest entry into the {@link ScaffoldFile} init writes: read the template's real
258
- * file, rewrite its path prefix to the project's directories, and swap the server-entry import
259
- * specifier for the one resolved at the file's new location (tsconfig alias or relative). The swap is
260
- * surgical — it replaces the exact specifier the template uses, not a loose directory substring — so
261
- * an unrelated occurrence can't be corrupted. This is the job the old build-time extractor did; it
262
- * 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.
263
298
  */
264
299
  function adaptFile(templateDir, entry, conventions, ctx) {
265
300
  const abs = path.join(templateDir, entry.path);
266
301
  if (!fs.existsSync(abs)) throw new Error(`Template file "${entry.path}" is listed in the manifest but does not exist in the template.`);
267
302
  const body = fs.readFileSync(abs, "utf8");
268
303
  const relPath = adaptTemplatePath(entry.path, conventions, ctx);
269
- const fromDir = path.posix.dirname(relPath);
270
- const contents = body.split(serverImportSpecifier(conventions)).join(ctx.serverImport(fromDir));
304
+ const contents = rewriteAliasImports(body, path.posix.dirname(relPath), templateAliasBase(templateDir, conventions), conventions, ctx);
271
305
  return {
272
306
  label: ROLE_LABELS[entry.role] ?? entry.role,
273
307
  relPath,
@@ -285,8 +319,10 @@ function resolvePatch(entry, conventions, ctx) {
285
319
  return {
286
320
  label: entry.label,
287
321
  relPath,
322
+ mode: entry.mode,
288
323
  imports,
289
- exports: entry.exports
324
+ exports: entry.exports,
325
+ note: entry.note
290
326
  };
291
327
  }
292
328
 
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 };
package/dist/config.d.ts CHANGED
@@ -63,8 +63,9 @@ 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
  /**
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.12.1";
2
+ var version = "0.13.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kizlo",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "A headless WordPress framework for TypeScript.",
6
6
  "keywords": [