kizlo 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/cli/commands/_setup.js +91 -55
  2. package/dist/cli/commands/_tasks.js +152 -0
  3. package/dist/cli/commands/_wiring.js +11 -10
  4. package/dist/cli/commands/create.js +270 -87
  5. package/dist/cli/commands/init.js +122 -61
  6. package/dist/cli/daemon/config.js +1 -1
  7. package/dist/cli/presets/source.js +13 -8
  8. package/dist/cli/presets/template.js +255 -50
  9. package/dist/cli/utils.js +149 -80
  10. package/dist/integrations/tanstack-start/api.d.ts +15 -0
  11. package/dist/integrations/tanstack-start/api.js +21 -0
  12. package/dist/integrations/tanstack-start/kizlo.client.d.ts +20 -0
  13. package/dist/integrations/tanstack-start/kizlo.client.js +18 -0
  14. package/dist/integrations/tanstack-start/kizlo.server.d.ts +14 -0
  15. package/dist/integrations/tanstack-start/kizlo.server.js +15 -0
  16. package/dist/integrations/tanstack-start/manifest.d.ts +9 -0
  17. package/dist/integrations/tanstack-start/manifest.js +11 -0
  18. package/dist/integrations/tanstack-start/metadata.d.ts +49 -0
  19. package/dist/integrations/tanstack-start/metadata.js +112 -0
  20. package/dist/integrations/tanstack-start/robots.d.ts +8 -0
  21. package/dist/integrations/tanstack-start/robots.js +11 -0
  22. package/dist/integrations/tanstack-start/root-head.d.ts +15 -0
  23. package/dist/integrations/tanstack-start/root-head.js +54 -0
  24. package/dist/integrations/tanstack-start/sitemap.d.ts +16 -0
  25. package/dist/integrations/tanstack-start/sitemap.js +20 -0
  26. package/dist/integrations/tanstack-start/utils.d.ts +20 -0
  27. package/dist/integrations/tanstack-start/utils.js +17 -0
  28. package/dist/package.js +1 -1
  29. package/dist/router.d.ts +5 -5
  30. package/dist/tanstack-start/server.d.ts +11 -0
  31. package/dist/tanstack-start/server.js +12 -0
  32. package/dist/tanstack-start.d.ts +4 -0
  33. package/dist/tanstack-start.js +5 -0
  34. package/package.json +22 -1
@@ -1,7 +1,8 @@
1
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
- import { adaptFile, changesFor, fileEntries, isOlderVersion, minCliError, patchEntries, readManifest, renderNote, resolveDependencies } from "../presets/template.js";
4
- import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, readEnvExample, resolveEnvKeys, selectPackageManager, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
3
+ import { adaptFile, changesFor, fileEntries, isOlderVersion, minCliError, patchEntries, readManifest, renderNote, resolveDependencies, unmetRequirement } from "../presets/template.js";
4
+ import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, envConflicts, nextStepsNote, orCancel, provisionLocalWordPress, readEnvExample, resolveEnvKeys, selectPackageManager, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
5
+ import { FINAL_CONFIRMATION_TEXT, StepError, confirmProceed, runChecklist, summaryNote } from "./_tasks.js";
5
6
  import { applyProjectPatches, buildScaffoldContext, kizloConfigTemplate, printManualStep, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
6
7
  import path from "node:path";
7
8
  import fs from "node:fs";
@@ -21,24 +22,6 @@ function sameOrigin(a, b) {
21
22
  }
22
23
  }
23
24
  /**
24
- * The framework's route directory in the user's project. When a template is present its
25
- * `conventions.appDir` (e.g. `src/app` for Next, `src/pages` for Astro) is the expected layout; we
26
- * probe that path and its no-`src` variant so a project that keeps routes at the top level is still
27
- * found, falling back to whether the project has a `src` dir. With no known template appDir we keep the
28
- * App Router default.
29
- */
30
- function detectAppDir(cwd, hasSrcDir, templateAppDir) {
31
- if (templateAppDir) {
32
- const withoutSrc = templateAppDir.replace(/^src\//, "");
33
- if (fs.existsSync(path.join(cwd, templateAppDir))) return templateAppDir;
34
- if (fs.existsSync(path.join(cwd, withoutSrc))) return withoutSrc;
35
- return hasSrcDir ? templateAppDir : withoutSrc;
36
- }
37
- if (fs.existsSync(path.join(cwd, "src", "app"))) return "src/app";
38
- if (fs.existsSync(path.join(cwd, "app"))) return "app";
39
- return hasSrcDir ? "src/app" : "app";
40
- }
41
- /**
42
25
  * The import alias settled without asking — the explicit `--alias` flag, else the alias a previous init
43
26
  * persisted in kizlo.config. `undefined` when neither is set: the choice hasn't been made, so it must be
44
27
  * prompted for (interactive) or defaulted to relative imports (`--yes`). Detection never enters here — a
@@ -105,7 +88,7 @@ function readPkg(pkgPath) {
105
88
  * A *missing* `kizlo` is installed on its own earlier (with its own messaging), so it's skipped here;
106
89
  * an existing older `kizlo` still gets the upgrade path.
107
90
  */
108
- async function alignDependencies(cwd, pm, pkg, manifest) {
91
+ async function alignDependencies(cwd, pm, pkg, manifest, message, warnings) {
109
92
  const { dependencies, devDependencies } = resolveDependencies(manifest);
110
93
  const declared = [...Object.entries(dependencies).map(([name, want]) => ({
111
94
  name,
@@ -116,17 +99,23 @@ async function alignDependencies(cwd, pm, pkg, manifest) {
116
99
  want,
117
100
  dev: true
118
101
  }))];
102
+ let installed = 0;
103
+ let upgraded = 0;
119
104
  for (const { name, want, dev } of declared) {
120
105
  const have = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
121
106
  if (name === "kizlo" && !have) continue;
122
107
  if (have && !isOlderVersion(have, want)) continue;
123
108
  const args = addDependencyArgs(pm, `${name}@${want}`, { dev });
124
- const s = p.spinner();
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`);
109
+ message(have ? `Upgrading ${name} from ${have} to ${want}` : `Installing ${name}@${want}`);
110
+ if (!await runCommandAsync(args, cwd, "ignore")) {
111
+ warnings.push(have ? `Could not upgrade ${name} automatically — install ${name}@${want} yourself` : `Could not install ${name} — run \`${args.join(" ")}\` yourself`);
112
+ continue;
113
+ }
114
+ if (have) upgraded++;
115
+ else installed++;
129
116
  }
117
+ const parts = [installed ? `installed ${installed}` : "", upgraded ? `upgraded ${upgraded}` : ""].filter(Boolean);
118
+ return parts.length ? `Aligned dependencies (${parts.join(", ")})` : "Dependencies already aligned";
130
119
  }
131
120
  const init = defineCommand({
132
121
  meta: {
@@ -174,9 +163,9 @@ const init = defineCommand({
174
163
  process.exit(1);
175
164
  }
176
165
  const pkg = readPkg(pkgPath);
177
- const detectedPm = detectPackageManager(cwd);
166
+ const detectedPm = await detectPackageManager(cwd);
178
167
  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);
168
+ const shouldPersistPm = !detectedPm && !yes;
180
169
  const hasKizlo = Boolean(pkg.dependencies?.kizlo) || Boolean(pkg.devDependencies?.kizlo);
181
170
  const hasSrcDir = fs.existsSync(path.join(cwd, "src"));
182
171
  if (yes) loadEnvFiles(cwd);
@@ -249,8 +238,9 @@ const init = defineCommand({
249
238
  const manifest = readManifest(entry.dir);
250
239
  const minErr = minCliError(manifest);
251
240
  if (minErr) cancelFrom(minErr);
252
- if (manifest.requires && !manifest.requires.anyDir.some((rel) => fs.existsSync(path.join(cwd, rel)))) cancelFrom(manifest.requires.message);
253
- const apiPath = manifest.apiPath;
241
+ const unmet = unmetRequirement(manifest.init.requires, projectDeps, (rel) => fs.existsSync(path.join(cwd, rel)));
242
+ if (unmet) cancelFrom(unmet);
243
+ const apiPath = manifest.config.apiPath;
254
244
  const envKeys = resolveEnvKeys(manifest);
255
245
  const flagAlias = args.alias !== void 0 ? String(args.alias).trim() : void 0;
256
246
  const aliasCtx = {
@@ -265,32 +255,17 @@ const init = defineCommand({
265
255
  envKeys
266
256
  }) : await collectInteractively(aliasCtx);
267
257
  setup.alias = aliasWithSlash(setup.alias);
258
+ if (apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, apiPath);
268
259
  const includeExamples = yes ? true : orCancel(await p.confirm({
269
260
  message: "Add example pages?",
270
261
  initialValue: true
271
262
  }));
272
- if (!hasKizlo) {
273
- const spec = `kizlo@${manifest.dependencies?.kizlo ?? `^${getVersion()}`}`;
274
- const s = p.spinner();
275
- s.start(`Installing kizlo with ${pm}`);
276
- const ok = await runCommandAsync(addDependencyArgs(pm, spec), cwd, "ignore");
277
- s.stop(ok ? "Installed kizlo" : "Could not install kizlo automatically");
278
- if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, spec).join(" ")}`);
279
- }
280
- if (apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, apiPath);
281
- await setupLocalWordPress(cwd, setup);
282
- await writeEnv(cwd, envKeys, setup, {
283
- force,
284
- yes,
285
- exampleTemplate: readEnvExample(entry.dir)
286
- });
287
- await syncRemote(setup);
288
263
  const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
289
264
  const serverDirRel = path.join(dirRel, "server");
290
265
  const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
291
266
  const scaffold = buildScaffoldContext(cwd, {
292
267
  dirRel,
293
- appDir: detectAppDir(cwd, hasSrcDir, manifest.conventions.appDir),
268
+ hasSrcDir,
294
269
  alias: setup.alias,
295
270
  clientUrl
296
271
  });
@@ -299,22 +274,108 @@ const init = defineCommand({
299
274
  relPath: "kizlo.config.ts",
300
275
  contents: kizloConfigTemplate(dirRel, setup.alias, setup.mode === "local")
301
276
  }];
302
- await alignDependencies(cwd, pm, pkg, manifest);
303
- for (const fileEntry of fileEntries(changesFor(manifest, "init", { includeExamples }))) files.push(adaptFile(entry.dir, fileEntry, manifest.conventions, scaffold));
277
+ for (const fileEntry of fileEntries(changesFor(manifest, "init", { includeExamples }))) files.push(adaptFile(entry.dir, fileEntry, manifest.config, scaffold));
278
+ const existingFiles = files.filter((file) => fs.existsSync(path.join(cwd, file.relPath)));
279
+ const overwritePaths = force || existingFiles.length === 0 || yes ? new Set(force ? existingFiles.map((f) => f.relPath) : []) : new Set(orCancel(await p.multiselect({
280
+ message: `${existingFiles.length} file(s) already exist. Select which to overwrite (space to toggle, enter to confirm).`,
281
+ options: existingFiles.map((f) => ({
282
+ value: f.relPath,
283
+ label: f.relPath
284
+ })),
285
+ initialValues: existingFiles.map((f) => f.relPath),
286
+ required: false
287
+ })));
288
+ const envKeyConflicts = envConflicts(cwd, envKeys, setup);
289
+ const overwriteEnv = force || envKeyConflicts.length > 0 && !yes && orCancel(await p.confirm({
290
+ message: `Overwrite existing .env values (${envKeyConflicts.join(", ")})?`,
291
+ initialValue: true
292
+ }));
293
+ summaryNote([
294
+ ["Template", entry.label],
295
+ ["Package manager", pm],
296
+ ["Kizlo directory", setup.dir],
297
+ ["Import alias", setup.alias || "relative imports"],
298
+ ["API URL", setup.baseUrl || "fill in .env later"],
299
+ ["WordPress", setup.mode === "local" ? "Local" : "Remote"],
300
+ ["Examples", includeExamples ? "Yes" : "No"]
301
+ ]);
302
+ if (!yes && !await confirmProceed(FINAL_CONFIRMATION_TEXT)) {
303
+ fetchedRegistry.cleanup();
304
+ p.cancel("Setup cancelled.");
305
+ process.exit(0);
306
+ }
307
+ if (shouldPersistPm) persistPackageManagerField(cwd, pm);
308
+ const warnings = [];
304
309
  const scaffolded = [];
305
- for (const file of files) scaffolded.push({
306
- file,
307
- result: await scaffoldFile(cwd, file, {
308
- force,
309
- yes
310
- })
311
- });
312
- writeGeneratedContract(cwd, serverDirRel);
313
- const gitignore = ensureGitignored(cwd, ".env");
314
- ensureGitignored(cwd, ".kizlo/");
310
+ let gitignore = "present";
311
+ const kizloSpec = `kizlo@${manifest.dependencies?.kizlo ?? `^${getVersion()}`}`;
312
+ if (await runChecklist([
313
+ {
314
+ title: `Installing kizlo with ${pm}`,
315
+ enabled: !hasKizlo,
316
+ run: async () => {
317
+ if (await runCommandAsync(addDependencyArgs(pm, kizloSpec), cwd, "ignore")) return "Installed kizlo";
318
+ warnings.push(`Install it yourself: ${addDependencyArgs(pm, kizloSpec).join(" ")}`);
319
+ throw new StepError("Could not install kizlo automatically", { fatal: false });
320
+ }
321
+ },
322
+ {
323
+ title: "Aligning dependencies",
324
+ run: (message) => alignDependencies(cwd, pm, pkg, manifest, message, warnings)
325
+ },
326
+ {
327
+ title: "Setting up local WordPress (first run downloads images, this can take a while)",
328
+ enabled: setup.mode === "local",
329
+ run: async () => {
330
+ try {
331
+ const warning = await provisionLocalWordPress(cwd, setup);
332
+ if (warning) warnings.push(warning);
333
+ return "Local WordPress ready";
334
+ } catch (error) {
335
+ throw new StepError("Local WordPress setup failed", { detail: error instanceof Error ? error.message : String(error) });
336
+ }
337
+ }
338
+ },
339
+ {
340
+ title: "Writing environment files",
341
+ run: () => writeEnv(cwd, envKeys, setup, {
342
+ overwrite: overwriteEnv,
343
+ exampleTemplate: readEnvExample(entry.dir)
344
+ })
345
+ },
346
+ {
347
+ title: "Syncing settings to WordPress",
348
+ enabled: setup.mode === "remote" && Boolean(setup.wpUrl && setup.wpUsername && setup.wpPassword),
349
+ run: async () => {
350
+ warnings.push(...await syncRemote(setup));
351
+ return "Synced settings to WordPress";
352
+ }
353
+ },
354
+ {
355
+ title: "Scaffolding Kizlo files",
356
+ run: async () => {
357
+ for (const file of files) scaffolded.push({
358
+ file,
359
+ result: await scaffoldFile(cwd, file, {
360
+ force: overwritePaths.has(file.relPath),
361
+ yes: true
362
+ })
363
+ });
364
+ writeGeneratedContract(cwd, serverDirRel);
365
+ gitignore = ensureGitignored(cwd, ".env");
366
+ ensureGitignored(cwd, ".kizlo/");
367
+ const written = scaffolded.filter(({ result }) => result !== "kept").length;
368
+ return `Scaffolded ${written} file${written === 1 ? "" : "s"}`;
369
+ }
370
+ }
371
+ ])) {
372
+ p.cancel("Setup failed — see the errors above.");
373
+ process.exit(1);
374
+ }
375
+ for (const warning of warnings) p.log.warn(warning);
315
376
  for (const { file, result } of scaffolded) reportScaffold(file, result, yes);
316
- applyProjectPatches(cwd, patchEntries(changesFor(manifest, "init")), manifest.conventions, scaffold);
317
- for (const note of manifest.notes ?? []) {
377
+ applyProjectPatches(cwd, patchEntries(changesFor(manifest, "init")), manifest.config, scaffold);
378
+ for (const note of manifest.init.notes) {
318
379
  const rendered = renderNote(note, setup.alias);
319
380
  printManualStep(rendered.title, rendered.body);
320
381
  }
@@ -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) ?? "npm"
133
+ packageManager: test.packageManager ?? await detectPackageManager(configDir) ?? "npm"
134
134
  };
135
135
  }
136
136
  /**
@@ -54,7 +54,7 @@ function readMeta(manifestPath) {
54
54
  const raw = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
55
55
  return {
56
56
  name: typeof raw.name === "string" && raw.name.trim() ? raw.name.trim() : void 0,
57
- framework: typeof raw.framework === "string" && raw.framework.trim() ? raw.framework.trim() : void 0
57
+ id: typeof raw.id === "string" && raw.id.trim() ? raw.id.trim() : void 0
58
58
  };
59
59
  } catch {
60
60
  return {};
@@ -69,7 +69,7 @@ function readMeta(manifestPath) {
69
69
  * break the listing.
70
70
  * - A single template: a directory that is itself a template (`template.json` at its root) — the escape
71
71
  * hatch for a community template shipped on its own, e.g. `--source github:acme/kizlo-astro-template`.
72
- * Its id comes from the manifest's `framework` (a stable name that also matches what `init`'s detected
72
+ * Its id comes from the manifest's `id` (a stable name that also matches what `init`'s detected
73
73
  * preset asks for), not the directory name, which for a downloaded repo is a random temp folder.
74
74
  *
75
75
  * Discovery is by scanning, not a hardcoded list, and the manifest is read only for its display fields
@@ -88,10 +88,10 @@ function listTemplates(registryDir) {
88
88
  if (!fs.existsSync(registryDir)) return [];
89
89
  if (isSingleTemplate(registryDir)) {
90
90
  const meta = readMeta(path.join(registryDir, "template.json"));
91
- const id = meta.framework ?? path.basename(registryDir);
91
+ const id = meta.id ?? path.basename(registryDir);
92
92
  return [{
93
93
  id,
94
- label: meta.name ?? meta.framework ?? id,
94
+ label: meta.name ?? meta.id ?? id,
95
95
  dir: registryDir
96
96
  }];
97
97
  }
@@ -104,17 +104,22 @@ function listTemplates(registryDir) {
104
104
  const meta = readMeta(manifestPath);
105
105
  entries.push({
106
106
  id: dirent.name,
107
- label: meta.name ?? meta.framework ?? dirent.name,
107
+ label: meta.name ?? meta.id ?? dirent.name,
108
108
  dir
109
109
  });
110
110
  }
111
111
  return entries.sort((a, b) => a.id.localeCompare(b.id));
112
112
  }
113
- /** A template's detection dependency list, read leniently so one bad manifest can't break detection. */
113
+ /**
114
+ * A template's detection package list — the `values` of every `dep` requirement under `init.requires`,
115
+ * read leniently (raw JSON, no schema) so one malformed manifest can't break detection for the rest of a
116
+ * registry. Full schema validation happens when the template is actually applied (`readManifest`).
117
+ */
114
118
  function readDetectDeps(templateDir) {
115
119
  try {
116
- const deps = JSON.parse(fs.readFileSync(path.join(templateDir, "template.json"), "utf8")).detect?.dependencies;
117
- return Array.isArray(deps) ? deps.filter((name) => typeof name === "string") : [];
120
+ const requires = JSON.parse(fs.readFileSync(path.join(templateDir, "template.json"), "utf8")).init?.requires;
121
+ if (!Array.isArray(requires)) return [];
122
+ return requires.filter((req) => req?.kind === "dep" && Array.isArray(req.values)).flatMap((req) => req.values).filter((name) => typeof name === "string");
118
123
  } catch {
119
124
  return [];
120
125
  }