kizlo 0.14.1 → 0.15.1

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 (131) hide show
  1. package/dist/category/error.js +10 -4
  2. package/dist/category/index.js +9 -9
  3. package/dist/category/schema.js +10 -1
  4. package/dist/category/utils.js +3 -3
  5. package/dist/cli/commands/_setup.js +5 -3
  6. package/dist/cli/commands/_test-wordpress.js +27 -0
  7. package/dist/cli/commands/_wiring.js +2 -1
  8. package/dist/cli/commands/check.js +99 -0
  9. package/dist/cli/commands/create.js +6 -3
  10. package/dist/cli/commands/dev.js +27 -18
  11. package/dist/cli/commands/generate.js +54 -11
  12. package/dist/cli/commands/init.js +6 -3
  13. package/dist/cli/commands/test.js +9 -1
  14. package/dist/cli/daemon/config.js +73 -14
  15. package/dist/cli/daemon/generate.js +213 -6
  16. package/dist/cli/daemon/lock.js +31 -9
  17. package/dist/cli/daemon/watch.js +135 -27
  18. package/dist/cli/index.js +1 -0
  19. package/dist/cli/process-owner.js +96 -0
  20. package/dist/cli/utils.js +28 -14
  21. package/dist/cli/wp/bootstrap.js +6 -2
  22. package/dist/cli/wp/compose/docker-compose.yml +11 -2
  23. package/dist/cli/wp/compose/override.js +20 -6
  24. package/dist/cli/wp/compose/override.test.ts +97 -40
  25. package/dist/cli/wp/compose/override.ts +21 -6
  26. package/dist/cli/wp/constants.d.ts +19 -1
  27. package/dist/cli/wp/constants.js +19 -1
  28. package/dist/cli/wp/dev.js +27 -8
  29. package/dist/cli/wp/docker.d.ts +29 -3
  30. package/dist/cli/wp/docker.js +38 -2
  31. package/dist/cli/wp/ports.js +3 -1
  32. package/dist/cli/wp/session.js +12 -30
  33. package/dist/cli/wp/settings.js +2 -2
  34. package/dist/cli/wp/setup.js +6 -4
  35. package/dist/cli/wp/stack.js +2 -0
  36. package/dist/cli/wp/types.d.ts +28 -3
  37. package/dist/cli/wp/utils.d.ts +19 -3
  38. package/dist/cli/wp/utils.js +23 -4
  39. package/dist/cli/wp/version.js +57 -0
  40. package/dist/cli/wp/watchdog.js +37 -15
  41. package/dist/comment/index.js +68 -56
  42. package/dist/comment/utils.js +16 -8
  43. package/dist/config.d.ts +45 -0
  44. package/dist/context.d.ts +17 -12
  45. package/dist/context.js +71 -22
  46. package/dist/email/service.d.ts +2 -2
  47. package/dist/email/service.js +1 -7
  48. package/dist/index.d.ts +9 -24
  49. package/dist/index.js +6 -18
  50. package/dist/integrations/nextjs/revalidate.js +1 -1
  51. package/dist/kizlo.d.ts +11 -4
  52. package/dist/kizlo.js +34 -16
  53. package/dist/menu/fixture.js +26 -13
  54. package/dist/menu/index.js +22 -8
  55. package/dist/menu/schema.js +15 -1
  56. package/dist/menu/utils.js +10 -11
  57. package/dist/package.js +1 -1
  58. package/dist/page/error.js +10 -4
  59. package/dist/page/index.js +28 -33
  60. package/dist/page/schema.js +21 -1
  61. package/dist/page/utils.js +9 -8
  62. package/dist/post/error.js +10 -4
  63. package/dist/post/fixture.js +33 -20
  64. package/dist/post/index.js +31 -36
  65. package/dist/post/schema.js +33 -1
  66. package/dist/post/utils.js +10 -9
  67. package/dist/post-type/status.js +19 -0
  68. package/dist/router.d.ts +27 -11
  69. package/dist/seo/index.js +43 -11
  70. package/dist/seo/types.d.ts +11 -79
  71. package/dist/seo/utils.js +4 -1
  72. package/dist/settings/service.d.ts +14 -16
  73. package/dist/settings/service.interface.d.ts +7 -5
  74. package/dist/settings/service.js +18 -17
  75. package/dist/shared/extension.d.ts +33 -2
  76. package/dist/shared/extension.js +31 -1
  77. package/dist/tag/error.js +10 -4
  78. package/dist/tag/index.js +9 -9
  79. package/dist/tag/schema.js +10 -1
  80. package/dist/tag/utils.js +3 -3
  81. package/dist/test.d.ts +5 -7
  82. package/dist/test.js +4 -6
  83. package/dist/wordpress/constants.js +7 -5
  84. package/dist/wordpress/endpoint.d.ts +23 -0
  85. package/dist/wordpress/endpoint.js +178 -0
  86. package/dist/wordpress/fetch-introspection.js +74 -0
  87. package/dist/wordpress/generate.js +413 -0
  88. package/dist/wordpress/index.d.ts +3 -18
  89. package/dist/wordpress/introspection.js +144 -0
  90. package/dist/wordpress/{service.d.ts → transport.d.ts} +18 -25
  91. package/dist/wordpress/transport.js +264 -0
  92. package/dist/wordpress/typecheck.js +122 -0
  93. package/dist/wordpress/types.d.ts +136 -17
  94. package/package.json +8 -3
  95. package/dist/post-type/service.js +0 -48
  96. package/dist/taxonomy/service.js +0 -48
  97. package/dist/test/client.d.ts +0 -16
  98. package/dist/test/client.js +0 -19
  99. package/dist/test/server.d.ts +0 -16
  100. package/dist/test/server.js +0 -38
  101. package/dist/wordpress/category/service.d.ts +0 -28
  102. package/dist/wordpress/category/service.js +0 -55
  103. package/dist/wordpress/category/types.d.ts +0 -120
  104. package/dist/wordpress/category/types.js +0 -15
  105. package/dist/wordpress/comment/service.d.ts +0 -28
  106. package/dist/wordpress/comment/service.js +0 -55
  107. package/dist/wordpress/comment/types.d.ts +0 -206
  108. package/dist/wordpress/constants.d.ts +0 -14
  109. package/dist/wordpress/menu/items/service.d.ts +0 -28
  110. package/dist/wordpress/menu/items/service.js +0 -55
  111. package/dist/wordpress/menu/items/types.d.ts +0 -217
  112. package/dist/wordpress/menu/items/types.js +0 -22
  113. package/dist/wordpress/menu/service.d.ts +0 -30
  114. package/dist/wordpress/menu/service.js +0 -58
  115. package/dist/wordpress/menu/types.d.ts +0 -115
  116. package/dist/wordpress/page/service.d.ts +0 -28
  117. package/dist/wordpress/page/service.js +0 -55
  118. package/dist/wordpress/page/types.d.ts +0 -270
  119. package/dist/wordpress/page/types.js +0 -27
  120. package/dist/wordpress/post/service.d.ts +0 -28
  121. package/dist/wordpress/post/service.js +0 -55
  122. package/dist/wordpress/post/types.d.ts +0 -294
  123. package/dist/wordpress/post/types.js +0 -41
  124. package/dist/wordpress/service.js +0 -200
  125. package/dist/wordpress/tag/service.d.ts +0 -28
  126. package/dist/wordpress/tag/service.js +0 -55
  127. package/dist/wordpress/tag/types.d.ts +0 -113
  128. package/dist/wordpress/tag/types.js +0 -15
  129. package/dist/wordpress/user/service.d.ts +0 -28
  130. package/dist/wordpress/user/service.js +0 -55
  131. package/dist/wordpress/user/types.d.ts +0 -221
@@ -5,10 +5,16 @@ const GET_CATEGORY_ERROR_MAP = defineErrorMap({ CATEGORY_NOT_FOUND: {
5
5
  status: 404,
6
6
  message: "Category not found"
7
7
  } });
8
- const LIST_CATEGORY_ERROR_MAP = defineErrorMap({ CATEGORY_INVALID_PAGE: {
9
- status: 400,
10
- message: "The requested page does not exist."
11
- } });
8
+ const LIST_CATEGORY_ERROR_MAP = defineErrorMap({
9
+ CATEGORY_INVALID_PAGE: {
10
+ status: 400,
11
+ message: "The requested page does not exist."
12
+ },
13
+ CATEGORY_NOT_FOUND: {
14
+ status: 404,
15
+ message: "Categories are not available."
16
+ }
17
+ });
12
18
 
13
19
  //#endregion
14
20
  export { GET_CATEGORY_ERROR_MAP, LIST_CATEGORY_ERROR_MAP };
@@ -1,10 +1,10 @@
1
1
  import { createProcedure } from "../shared/procedure.js";
2
2
  import { parseIdentifier } from "../shared/identifier.js";
3
3
  import { deserializeListMetadata } from "../shared/serialize.js";
4
- import { getTaxonomyService } from "../taxonomy/service.js";
5
4
  import { GET_CATEGORY_ERROR_MAP, LIST_CATEGORY_ERROR_MAP } from "./error.js";
6
5
  import { Category, CategoryList, ListCategoryInput, RetrieveCategoryInput } from "./schema.js";
7
6
  import { deserializeCategory } from "./utils.js";
7
+ import { normalizeArrayableValue } from "@kizlo/shared";
8
8
 
9
9
  //#region src/category/index.ts
10
10
  const CATEGORY_ROUTER_MAP = {
@@ -16,14 +16,14 @@ const CATEGORY_ROUTER_MAP = {
16
16
  output: Category,
17
17
  errors: GET_CATEGORY_ERROR_MAP
18
18
  }, async ({ input, context, errors }) => {
19
- const taxonomy = getTaxonomyService("category", context.wordpress);
20
19
  const identifier = parseIdentifier(input.params.identifier);
21
20
  if (!identifier) throw errors.CATEGORY_NOT_FOUND();
22
- const response = await taxonomy.get(identifier);
21
+ const response = await context.wordpress.taxonomies.category.retrieve({ identifier: String(identifier.value) });
23
22
  if (response.error) switch (response.error.code) {
24
23
  case "invalid_taxonomy":
25
24
  case "term_not_found":
26
- case "rest_term_invalid": throw errors.CATEGORY_NOT_FOUND();
25
+ case "rest_term_invalid":
26
+ case "rest_no_route": throw errors.CATEGORY_NOT_FOUND();
27
27
  default:
28
28
  context.logger.error("Get category unhandled error", response.error, {
29
29
  identifier,
@@ -41,22 +41,22 @@ const CATEGORY_ROUTER_MAP = {
41
41
  output: CategoryList,
42
42
  errors: LIST_CATEGORY_ERROR_MAP
43
43
  }, async ({ input, context, errors }) => {
44
- const response = await getTaxonomyService("category", context.wordpress).list({
45
- context: "edit",
44
+ const response = await context.wordpress.taxonomies.category.list({
46
45
  page: input.query?.page,
47
46
  per_page: input.query?.perPage,
48
47
  search: input.query?.search,
49
- exclude: input.query?.exclude,
50
- include: input.query?.include,
48
+ exclude: normalizeArrayableValue(input.query?.exclude),
49
+ include: normalizeArrayableValue(input.query?.include),
51
50
  order: input.query?.order,
52
51
  orderby: input.query?.orderBy,
53
52
  hide_empty: input.query?.hideEmpty,
54
53
  parent: input.query?.parent,
55
54
  post: input.query?.post,
56
- slug: input.query?.slug
55
+ slug: normalizeArrayableValue(input.query?.slug)
57
56
  });
58
57
  if (response.error) switch (response.error.code) {
59
58
  case "rest_post_invalid_page_number": throw errors.CATEGORY_INVALID_PAGE();
59
+ case "rest_no_route": throw errors.CATEGORY_NOT_FOUND();
60
60
  default:
61
61
  context.logger.error("List categories unhandled error", response.error, { code: response.error.code });
62
62
  throw errors.INTERNAL_SERVER_ERROR();
@@ -1,10 +1,19 @@
1
1
  import { IdentifierInput, ListMetadata, ListOrder } from "../shared/schema.js";
2
- import { WP_CATEGORY_ORDER_BYS } from "../wordpress/category/types.js";
3
2
  import { Seo } from "../seo/schema.js";
4
3
  import { BooleanLike, Metadata, NumberLike, arrayable, lenient } from "@kizlo/shared";
5
4
  import z from "zod/v4";
6
5
 
7
6
  //#region src/category/schema.ts
7
+ const WP_CATEGORY_ORDER_BYS = [
8
+ "id",
9
+ "include",
10
+ "name",
11
+ "slug",
12
+ "include_slugs",
13
+ "term_group",
14
+ "description",
15
+ "count"
16
+ ];
8
17
  const Category = z.object({
9
18
  id: z.number(),
10
19
  name: z.string(),
@@ -7,12 +7,12 @@ function deserializeCategory(data) {
7
7
  id: data.id,
8
8
  name: data.name,
9
9
  slug: data.slug,
10
- url: data.kizlo.url ?? null,
10
+ url: data.kizlo.url,
11
11
  description: data.description.length > 0 ? data.description : null,
12
12
  parent: data.parent > 0 ? data.parent : null,
13
13
  postCount: data.count,
14
- seo: data.kizlo.seo ? deserializeSeo(data.kizlo.seo) : null,
15
- meta: stringifiedMetaRecord(data.meta ?? {})
14
+ seo: "seo" in data.kizlo ? deserializeSeo(data.kizlo.seo) : null,
15
+ meta: stringifiedMetaRecord(data.meta)
16
16
  };
17
17
  }
18
18
 
@@ -1,8 +1,8 @@
1
- import { LOCAL_DIR_REL } from "../wp/constants.js";
1
+ import { DEFAULT_WORDPRESS_TAG, LOCAL_DIR_REL } from "../wp/constants.js";
2
2
  import { composeStop, createStack, dockerHint, dockerStatus } from "../wp/docker.js";
3
3
  import { DEFAULT_ENV_KEYS, availablePackageManagers, detectInvokingPackageManager, ensureGitignored, envGroups, envKeysPresent, mergeEnv, pickStackPort, writeFileIfAbsent } from "../utils.js";
4
- import { promptFragment, resolvePromptDefault } from "../presets/template.js";
5
4
  import { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveStackName, stackProject } from "../daemon/config.js";
5
+ import { promptFragment, resolvePromptDefault } from "../presets/template.js";
6
6
  import { createAdminAppPassword } from "../wp/bootstrap.js";
7
7
  import { bootstrapDev } from "../wp/dev.js";
8
8
  import { removeProjectContainers } from "../wp/session.js";
@@ -129,7 +129,8 @@ async function selectPackageManager(message) {
129
129
  }
130
130
  /** The choices summary shows a prompt under a short label — the title-cased token (`linter` → `Linter`). */
131
131
  function promptLabel(token) {
132
- return token.charAt(0).toUpperCase() + token.slice(1);
132
+ const spaced = token.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
133
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
133
134
  }
134
135
  /** What a resolved prompt answer reads as in the choices summary: the option label, `Yes`/`No`, or the text. */
135
136
  function promptDisplay(prompt, answer) {
@@ -327,6 +328,7 @@ function devConfigFor(cwd) {
327
328
  portExplicit: false,
328
329
  dbPort: DEFAULT_DEV_DB_PORT,
329
330
  dbPortExplicit: false,
331
+ wordpressTag: DEFAULT_WORDPRESS_TAG,
330
332
  fixtures: [],
331
333
  wordpressPath: LOCAL_DIR_REL,
332
334
  wordpressDir: path.resolve(cwd, LOCAL_DIR_REL)
@@ -0,0 +1,27 @@
1
+ import { resolveTestConfig } from "../daemon/config.js";
2
+ import fs from "node:fs";
3
+
4
+ //#region src/cli/commands/_test-wordpress.ts
5
+ /** Turn the test harness artifact into the credentials accepted by the introspection transport. */
6
+ function wordPressCredentialsFromTestArtifact(artifact, project) {
7
+ if (artifact.project && artifact.project !== project) throw new Error("The saved test credentials belong to another WordPress stack. Run `kizlo test` first.");
8
+ return {
9
+ url: artifact.url,
10
+ username: artifact.users.admin.username,
11
+ password: artifact.users.admin.applicationPassword
12
+ };
13
+ }
14
+ /** Read credentials written by `kizlo test`, ensuring they belong to this branch's configured stack. */
15
+ async function testWordPressCredentials(cwd) {
16
+ const config = await resolveTestConfig(cwd);
17
+ let artifact;
18
+ try {
19
+ artifact = JSON.parse(fs.readFileSync(config.credentialsPath, "utf8"));
20
+ } catch {
21
+ throw new Error("No test WordPress credentials found. Run `kizlo test` first.");
22
+ }
23
+ return wordPressCredentialsFromTestArtifact(artifact, config.project);
24
+ }
25
+
26
+ //#endregion
27
+ export { testWordPressCredentials };
@@ -1,7 +1,7 @@
1
1
  import { aliasWithSlash, resolveModuleImport, writeFileIfAbsent } from "../utils.js";
2
+ import { CONTRACT_BARREL, WORDPRESS_STUB } from "../daemon/generate.js";
2
3
  import { resolvePatch } from "../presets/template.js";
3
4
  import { orCancel } from "./_setup.js";
4
- import { CONTRACT_BARREL } from "../daemon/generate.js";
5
5
  import { applyPatchToSource, patchChanged, renderPatchCode, resolvePatchTargetPath } from "../presets/patch.js";
6
6
  import path from "node:path";
7
7
  import fs from "node:fs";
@@ -77,6 +77,7 @@ function writeGeneratedContract(cwd, serverDirRel) {
77
77
  const generatedDirRel = path.join(serverDirRel, "generated");
78
78
  writeFileIfAbsent(path.join(cwd, generatedDirRel, "contract.json"), "{}\n");
79
79
  writeFileIfAbsent(path.join(cwd, generatedDirRel, "index.ts"), CONTRACT_BARREL);
80
+ writeFileIfAbsent(path.join(cwd, generatedDirRel, "wordpress.ts"), WORDPRESS_STUB);
80
81
  }
81
82
  /**
82
83
  * Apply every patch a template declares, merging Kizlo wiring into files the project already owns
@@ -0,0 +1,99 @@
1
+ import { log } from "../daemon/logger.js";
2
+ import { resolveConfig, resolveWordPressClientDir } from "../daemon/config.js";
3
+ import { generateWordPressSource, reportGenerationError } from "../daemon/generate.js";
4
+ import { testWordPressCredentials } from "./_test-wordpress.js";
5
+ import path from "node:path";
6
+ import fs from "node:fs";
7
+ import { defineCommand } from "citty";
8
+ import { createTwoFilesPatch } from "diff";
9
+
10
+ //#region src/cli/commands/check.ts
11
+ /** Compare generated output with every configured copy without changing the files. */
12
+ function findGeneratedFileMismatches(files, expected) {
13
+ const mismatches = [];
14
+ for (const file of files) {
15
+ const actual = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
16
+ if (actual !== expected) mismatches.push({
17
+ file,
18
+ actual,
19
+ expected
20
+ });
21
+ }
22
+ return mismatches;
23
+ }
24
+ /** A standard unified diff, labelled so CI says which side is committed and which is current. */
25
+ function formatGeneratedFileDiff(cwd, mismatch) {
26
+ const file = path.relative(cwd, mismatch.file) || path.basename(mismatch.file);
27
+ return createTwoFilesPatch(file, file, mismatch.actual, mismatch.expected, "committed", "generated", { context: 3 });
28
+ }
29
+ /** Every WordPress client the project configuration says it owns, with duplicates collapsed. */
30
+ async function wordPressClientFiles(cwd, dir) {
31
+ const files = /* @__PURE__ */ new Set();
32
+ const workspaceDir = await resolveWordPressClientDir(cwd);
33
+ if (workspaceDir) files.add(path.resolve(cwd, workspaceDir, "wordpress.ts"));
34
+ const config = await resolveConfig(cwd, { dir });
35
+ if (config) files.add(path.resolve(cwd, config.wordpressPath));
36
+ return [...files];
37
+ }
38
+ const check = defineCommand({
39
+ meta: {
40
+ name: "check",
41
+ description: "Check the Kizlo project without changing files"
42
+ },
43
+ args: {
44
+ dir: {
45
+ type: "string",
46
+ description: "Override the Kizlo directory (defaults to kizlo.config.ts)"
47
+ },
48
+ test: {
49
+ type: "boolean",
50
+ description: "Check against WordPress left running by kizlo test"
51
+ }
52
+ },
53
+ async run({ args }) {
54
+ const cwd = process.cwd();
55
+ const files = await wordPressClientFiles(cwd, args.dir);
56
+ if (files.length === 0) {
57
+ log.info("No generated WordPress client configured, nothing to check.");
58
+ return;
59
+ }
60
+ let credentials;
61
+ if (args.test) try {
62
+ credentials = await testWordPressCredentials(cwd);
63
+ } catch (error) {
64
+ log.error(error instanceof Error ? error.message : String(error));
65
+ process.exitCode = 1;
66
+ return;
67
+ }
68
+ let expected;
69
+ try {
70
+ expected = await generateWordPressSource(cwd, {
71
+ credentials,
72
+ strict: true
73
+ });
74
+ } catch (error) {
75
+ reportGenerationError("Failed to check the generated WordPress client:", error);
76
+ process.exitCode = 1;
77
+ return;
78
+ }
79
+ const mismatches = findGeneratedFileMismatches(files, expected);
80
+ if (mismatches.length === 0) {
81
+ log.success(`${files.length === 1 ? "WordPress client is" : "WordPress clients are"} current`);
82
+ return;
83
+ }
84
+ for (const mismatch of mismatches) {
85
+ log.error(`${path.relative(cwd, mismatch.file)} is stale`);
86
+ process.stderr.write(`${formatGeneratedFileDiff(cwd, mismatch)}\n`);
87
+ }
88
+ const command = [
89
+ "kizlo generate",
90
+ args.test ? "--test" : "",
91
+ args.dir ? `--dir ${args.dir}` : ""
92
+ ].filter(Boolean).join(" ");
93
+ log.info(`Run \`${command}\` and commit the updated client.`);
94
+ process.exitCode = 1;
95
+ }
96
+ });
97
+
98
+ //#endregion
99
+ export { check };
@@ -1,4 +1,4 @@
1
- import { approveBuildsCommand, cleanupWorkspaceArtifacts, detectInvokingPackageManager, detectPackageManager, ensureGitignored, findWorkspaceRoot, initGitRepository, installArgs, isCommandAvailable, isGitRepository, isWorkspaceMember, runCommandCapturedAsync, tokenizeCommand } from "../utils.js";
1
+ import { aliasWithSlash, approveBuildsCommand, cleanupWorkspaceArtifacts, detectInvokingPackageManager, detectPackageManager, effectiveAlias, ensureGitignored, findWorkspaceRoot, initGitRepository, installArgs, isCommandAvailable, isGitRepository, isWorkspaceMember, runCommandCapturedAsync, tokenizeCommand } 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
4
  import { collectConnectionFromEnv, collectConnectionInteractively, collectTemplatePrompts, nextStepsLines, orCancel, pickAppPort, provisionLocalWordPress, readEnvExample, resolveEnvKeys, selectPackageManager, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
@@ -183,13 +183,16 @@ function bootstrapArgs(manifest, pm, name, promptArgs = {}) {
183
183
  * files are written with `force`, silently replacing the framework defaults.
184
184
  */
185
185
  async function applyManifestWiring(dir, templateDir, manifest, opts) {
186
- const { alias, kizloPath } = manifest.config;
186
+ const { kizloPath } = manifest.config;
187
+ const hasSrcDir = fs.existsSync(path.join(dir, "src"));
188
+ const alias = aliasWithSlash(effectiveAlias(dir, kizloPath, manifest.config.alias));
187
189
  const scaffold = buildScaffoldContext(dir, {
188
190
  dirRel: kizloPath,
189
- hasSrcDir: fs.existsSync(path.join(dir, "src")),
191
+ hasSrcDir,
190
192
  alias,
191
193
  clientUrl: void 0
192
194
  });
195
+ if (manifest.config.alias && !alias) p.log.warn(`The ${manifest.name ?? manifest.id} scaffold declares no tsconfig path for \`${aliasWithSlash(manifest.config.alias)}\`, so Kizlo wrote relative imports instead.`);
193
196
  recordDependencies(dir, manifest);
194
197
  const changes = changesFor(manifest, "create", { includeExamples: opts.includeExamples });
195
198
  const files = [{
@@ -1,4 +1,4 @@
1
- import { createStack, dockerHint, dockerStatus } from "../wp/docker.js";
1
+ import { createStack, dockerHint, dockerStatus, stackStatus } from "../wp/docker.js";
2
2
  import { findConfigDir } from "../wp/utils.js";
3
3
  import { log } from "../daemon/logger.js";
4
4
  import { DEFAULT_ENV_KEYS, ensureGitignored, envGroups, groupDefault, mergeEnv, pickStackPort, withSpinner } from "../utils.js";
@@ -117,8 +117,8 @@ function updateWpEnv(cfg, info) {
117
117
  * The full fresh-install write goes through {@link updateWpEnv} instead; this covers the resume path it
118
118
  * skips. Returns true when it changed the file. The app still needs a restart to read the new value.
119
119
  */
120
- function syncLocalUrl(cfg, url) {
121
- const envPath = join(cfg.configDir, ".env");
120
+ function syncLocalUrl(configDir, url) {
121
+ const envPath = join(configDir, ".env");
122
122
  if (!existsSync(envPath)) return false;
123
123
  const existing = readFileSync(envPath, "utf8");
124
124
  if (readEnvValue(existing, "KIZLO_LOCAL_WP_URL") === url) return false;
@@ -134,23 +134,23 @@ function syncLocalUrl(cfg, url) {
134
134
  * `next dev` child server — nothing stops them automatically when we die. A dying process
135
135
  * also can't be trusted to finish async work (a closed terminal breaks its stdout
136
136
  * mid-write), so this process does no docker work on the way out. It only needs to
137
- * *exit*; the watchdog, watching our PID from its own session, stops it the moment
138
- * we're gone — by Ctrl+C, SIGTERM, a closed tab, SIGKILL, or a crash alike.
137
+ * *exit*; the watchdog verifies our process identity from its own session and stops it
138
+ * the moment we're gone, by Ctrl+C, SIGTERM, a closed tab, SIGKILL, or a crash alike.
139
139
  *
140
140
  * We trigger that exit two ways, because neither alone is enough: signal handlers (for
141
141
  * Ctrl+C / `kill`), and an stdin/TTY-close watch (for a closed terminal, whose SIGHUP
142
142
  * an `npm`/`pnpm` wrapper silently swallows — leaving us orphaned but alive otherwise).
143
143
  */
144
- function armForegroundTeardown(cfg) {
145
- registerSession(cfg.project);
146
- spawnWatchdog(cfg.project);
144
+ async function armForegroundTeardown(cfg) {
145
+ const owner = await registerSession(cfg.project);
146
+ spawnWatchdog(cfg.project, owner);
147
147
  let exiting = false;
148
- const shutdown = (reason) => {
148
+ const shutdown = (line) => {
149
149
  if (exiting) return;
150
150
  exiting = true;
151
151
  unregisterSession(cfg.project);
152
152
  try {
153
- note(`Stopping local WordPress (${reason})…`);
153
+ if (line) note(line);
154
154
  } catch {}
155
155
  process.exit(0);
156
156
  };
@@ -158,15 +158,20 @@ function armForegroundTeardown(cfg) {
158
158
  "SIGINT",
159
159
  "SIGTERM",
160
160
  "SIGHUP"
161
- ]) process.on(signal, () => shutdown(signal));
161
+ ]) process.on(signal, () => shutdown(stopping(signal)));
162
162
  if (process.stdin.isTTY) {
163
163
  process.stdin.resume();
164
164
  for (const event of [
165
165
  "end",
166
166
  "close",
167
167
  "error"
168
- ]) process.stdin.on(event, () => shutdown("terminal closed"));
168
+ ]) process.stdin.on(event, () => shutdown(stopping("terminal closed")));
169
169
  }
170
+ return shutdown;
171
+ }
172
+ /** The teardown line for a session leaving while its stack is still up, which this one is stopping. */
173
+ function stopping(reason) {
174
+ return `Stopping local WordPress (${reason})…`;
170
175
  }
171
176
  /**
172
177
  * Bring the dev stack's containers up for `cfg`: clear any stale containers, resolve the published
@@ -218,7 +223,7 @@ async function report(cfg, creds, reloaded, start) {
218
223
  });
219
224
  if (!sync.ok) note(`Could not sync the site settings to WordPress (${sync.error}) — make sure the kizlo plugin is active, then set them from the Kizlo settings.`);
220
225
  }
221
- } else if (syncLocalUrl(cfg, creds.url)) note("Updated .env WordPress URL to the current network address — restart your app to pick it up");
226
+ } else if (syncLocalUrl(cfg.configDir, creds.url)) note("Updated .env WordPress URL to the current network address — restart your app to pick it up");
222
227
  printSummary(creds);
223
228
  const { green, dim, bold, reset } = palette();
224
229
  const ms = Date.now() - start;
@@ -259,17 +264,21 @@ async function rebootStack(cwd, label) {
259
264
  * teardown first (so a mid-startup cancel still stops partial containers), boots and prints the
260
265
  * summary, then parks. While parked it watches `.env` and `kizlo.config.*`: a change reboots the whole
261
266
  * stack in place so a new port, connection, or fixture takes effect without rerunning the command. The
262
- * reboot runs in the same process — the watchdog and session keep watching this PID throughout — and is
263
- * guarded so the credential/URL writes it makes to `.env` don't trigger another reboot. A referenced
267
+ * reboot runs in the same process. The watchdog and session keep watching this owner throughout, and it
268
+ * is guarded so the credential/URL writes it makes to `.env` don't trigger another reboot. A referenced
264
269
  * timer holds the event loop open — an unresolved promise alone would not, so without it Node would
265
270
  * empty its loop and exit straight after printing.
266
271
  */
267
272
  async function startForeground(cfg) {
268
273
  const cwd = process.cwd();
269
274
  let ready = await prepareStack(cfg);
270
- armForegroundTeardown(ready);
275
+ const shutdown = await armForegroundTeardown(ready);
271
276
  await bootAndReport(ready);
272
- let stopContract = await startWatcher(ready.configDir);
277
+ const stack = {
278
+ status: () => stackStatus(ready.project),
279
+ onStopped: () => shutdown()
280
+ };
281
+ let stopContract = await startWatcher(ready.configDir, { stack });
273
282
  let reloading = false;
274
283
  const reload = async (changed) => {
275
284
  if (reloading) return;
@@ -277,7 +286,7 @@ async function startForeground(cfg) {
277
286
  try {
278
287
  stopContract?.();
279
288
  ready = await rebootStack(cwd, `${changed} changed — restarting the session`);
280
- stopContract = await startWatcher(ready.configDir);
289
+ stopContract = await startWatcher(ready.configDir, { stack });
281
290
  } catch (error) {
282
291
  log.error("Failed to reload the dev session:", error);
283
292
  } finally {
@@ -1,30 +1,73 @@
1
1
  import { log } from "../daemon/logger.js";
2
- import { resolveConfig } from "../daemon/config.js";
3
- import { generateOnce } from "../daemon/generate.js";
2
+ import { resolveConfig, resolveWordPressClientDir } from "../daemon/config.js";
3
+ import { PartialContractError, generateOnce, generateWorkspaceClientOnce, reportGenerationError } from "../daemon/generate.js";
4
+ import { testWordPressCredentials } from "./_test-wordpress.js";
5
+ import path from "node:path";
4
6
  import { defineCommand } from "citty";
5
7
 
6
8
  //#region src/cli/commands/generate.ts
9
+ /**
10
+ * Report and exit non-zero. A refused partial contract says one thing here that it cannot say
11
+ * anywhere else, since its diagnostics are already on screen and the useful detail is what strict
12
+ * mode did not do to the files on disk.
13
+ */
14
+ function fail(message, error) {
15
+ if (error instanceof PartialContractError) log.error(`${error.message} The generated client on disk is unchanged.`);
16
+ else reportGenerationError(message, error);
17
+ process.exit(1);
18
+ }
7
19
  const generate = defineCommand({
8
20
  meta: {
9
21
  name: "generate",
10
22
  description: "Generate the Kizlo contract once (for builds and CI)"
11
23
  },
12
- args: { dir: {
13
- type: "string",
14
- description: "Override the Kizlo directory (defaults to kizlo.config.ts)"
15
- } },
24
+ args: {
25
+ dir: {
26
+ type: "string",
27
+ description: "Override the Kizlo directory (defaults to kizlo.config.ts)"
28
+ },
29
+ strict: {
30
+ type: "boolean",
31
+ description: "Fail instead of generating a client WordPress had to exclude routes or types from"
32
+ },
33
+ test: {
34
+ type: "boolean",
35
+ description: "Generate against WordPress left running by kizlo test"
36
+ }
37
+ },
16
38
  async run({ args }) {
17
- const cfg = await resolveConfig(process.cwd(), { dir: args.dir });
39
+ const cwd = process.cwd();
40
+ const strict = args.strict === true;
41
+ let credentials;
42
+ if (args.test) try {
43
+ credentials = await testWordPressCredentials(cwd);
44
+ } catch (error) {
45
+ log.error(error instanceof Error ? error.message : String(error));
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+ const options = {
50
+ credentials,
51
+ strict
52
+ };
53
+ const wordpressClientDir = await resolveWordPressClientDir(cwd);
54
+ if (wordpressClientDir) try {
55
+ const result = await generateWorkspaceClientOnce(cwd, wordpressClientDir, options);
56
+ const file = path.resolve(cwd, wordpressClientDir, "wordpress.ts");
57
+ log.success(`WordPress client ${result === "generated" ? "written to" : "already current at"} ${file}`);
58
+ } catch (error) {
59
+ fail("Failed to generate the WordPress client:", error);
60
+ }
61
+ const cfg = await resolveConfig(cwd, { dir: args.dir });
18
62
  if (!cfg) {
19
- log.info("No Kizlo server directory configured, nothing to generate. Set `dir` in kizlo.config.ts or pass --dir.");
63
+ if (!wordpressClientDir) log.info("No Kizlo server directory configured, nothing to generate. Set `dir` in kizlo.config.ts or pass --dir.");
20
64
  return;
21
65
  }
22
66
  let ok;
23
67
  try {
24
- ok = await generateOnce(cfg);
68
+ ok = await generateOnce(cfg, options);
25
69
  } catch (error) {
26
- log.error("Failed to generate the Kizlo contract:", error);
27
- process.exit(1);
70
+ fail("Failed to generate the Kizlo contract:", error);
28
71
  }
29
72
  if (!ok) {
30
73
  log.error(`No Kizlo server found in ${cfg.serverEntry}`);
@@ -1,4 +1,4 @@
1
- import { addDependencyArgs, aliasWithSlash, detectImportAlias, detectInvokingPackageManager, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, persistPackageManagerField, readPersistedAlias, runCommandAsync } from "../utils.js";
1
+ import { addDependencyArgs, aliasWithSlash, detectImportAlias, detectInvokingPackageManager, detectPackageManager, effectiveAlias, 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, unmetRequirement } from "../presets/template.js";
4
4
  import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, envConflicts, nextStepsNote, orCancel, provisionLocalWordPress, readEnvExample, resolveEnvKeys, selectPackageManager, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
@@ -254,13 +254,15 @@ const init = defineCommand({
254
254
  ...aliasCtx,
255
255
  envKeys
256
256
  }) : await collectInteractively(aliasCtx);
257
- setup.alias = aliasWithSlash(setup.alias);
257
+ const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
258
+ const requestedAlias = aliasWithSlash(setup.alias);
259
+ setup.alias = aliasWithSlash(effectiveAlias(cwd, dirRel, setup.alias));
260
+ const droppedAlias = requestedAlias && !setup.alias ? requestedAlias : void 0;
258
261
  if (apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, apiPath);
259
262
  const includeExamples = yes ? true : orCancel(await p.confirm({
260
263
  message: "Add example pages?",
261
264
  initialValue: true
262
265
  }));
263
- const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
264
266
  const serverDirRel = path.join(dirRel, "server");
265
267
  const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
266
268
  const scaffold = buildScaffoldContext(cwd, {
@@ -306,6 +308,7 @@ const init = defineCommand({
306
308
  }
307
309
  if (shouldPersistPm) persistPackageManagerField(cwd, pm);
308
310
  const warnings = [];
311
+ if (droppedAlias) warnings.push(`No tsconfig path maps \`${droppedAlias}\` onto ${dirRel}, so relative imports were written instead. Add \`"${droppedAlias}*": ["./${hasSrcDir ? "src/" : ""}*"]\` to your tsconfig \`paths\` and re-run to use the alias.`);
309
312
  const scaffolded = [];
310
313
  let gitignore = "present";
311
314
  const kizloSpec = `kizlo@${manifest.dependencies?.kizlo ?? `^${getVersion()}`}`;
@@ -3,6 +3,7 @@ import { isSeeded, recordedPort } from "../wp/utils.js";
3
3
  import { log } from "../daemon/logger.js";
4
4
  import { isFree } from "../wp/ports.js";
5
5
  import { groupDefault, pickStackPort, withSpinner } from "../utils.js";
6
+ import { warnVersionDrift } from "../wp/version.js";
6
7
  import { resolveTestConfig } from "../daemon/config.js";
7
8
  import { testStack } from "../wp/stack.js";
8
9
  import { runPluginPhpunit } from "../wp/phpunit.js";
@@ -37,7 +38,7 @@ async function resolveTestPort(cfg, stack, fresh) {
37
38
  if (!fresh) {
38
39
  const live = await stack.publishedPort();
39
40
  if (live !== void 0) return live;
40
- const recorded = recordedPort();
41
+ const recorded = recordedPort(cfg.project);
41
42
  if (recorded !== void 0) {
42
43
  if (await isFree(recorded)) return recorded;
43
44
  log.error(`The test WordPress port ${recorded} (from a previous run) is now held by something else.\nFree that port, or run \`kizlo test reset\` to rebuild on a fresh one.`);
@@ -60,9 +61,16 @@ async function bringUp(cfg, stack, fresh = false) {
60
61
  port
61
62
  }));
62
63
  await withSpinner("Starting test WordPress", () => bound.composeUp(), "Test WordPress ready");
64
+ await warnVersionDrift({
65
+ wpCli: bound.wpCli,
66
+ tag: cfg.wordpressTag,
67
+ resetCommand: "kizlo test reset",
68
+ warn: (message) => log.warn(message)
69
+ });
63
70
  if (await isSeeded()) log.info("Test WordPress already seeded — skipping seed.");
64
71
  else await withSpinner("Seeding WordPress", () => runSeeds({
65
72
  port,
73
+ project: cfg.project,
66
74
  fixtures: cfg.fixtures
67
75
  }), "WordPress seeded");
68
76
  return {