kizlo 0.1.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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/adapters/auth.d.ts +18 -0
  4. package/dist/adapters/auth.js +15 -0
  5. package/dist/adapters/captcha.d.ts +28 -0
  6. package/dist/adapters/captcha.js +86 -0
  7. package/dist/adapters/geo.d.ts +18 -0
  8. package/dist/adapters/geo.js +18 -0
  9. package/dist/adapters/geo.vercel.d.ts +6 -0
  10. package/dist/adapters/geo.vercel.js +41 -0
  11. package/dist/adapters/logger.d.ts +31 -0
  12. package/dist/adapters/logger.js +30 -0
  13. package/dist/cli/banner.js +26 -0
  14. package/dist/cli/commands/generate.js +34 -0
  15. package/dist/cli/commands/init.js +313 -0
  16. package/dist/cli/commands/test.js +68 -0
  17. package/dist/cli/commands/watch.js +53 -0
  18. package/dist/cli/commands/wp.js +66 -0
  19. package/dist/cli/daemon/config.js +87 -0
  20. package/dist/cli/daemon/generate.js +40 -0
  21. package/dist/cli/daemon/lock.js +31 -0
  22. package/dist/cli/daemon/logger.js +8 -0
  23. package/dist/cli/daemon/watch.js +37 -0
  24. package/dist/cli/index.d.ts +1 -0
  25. package/dist/cli/index.js +22 -0
  26. package/dist/cli/presets/base.js +23 -0
  27. package/dist/cli/presets/index.js +16 -0
  28. package/dist/cli/presets/nextjs.js +39 -0
  29. package/dist/cli/utils.js +182 -0
  30. package/dist/cli/wp/bootstrap.js +114 -0
  31. package/dist/cli/wp/compose/docker-compose.yml +73 -0
  32. package/dist/cli/wp/constants.js +52 -0
  33. package/dist/cli/wp/docker.d.ts +24 -0
  34. package/dist/cli/wp/docker.js +69 -0
  35. package/dist/cli/wp/setup.js +39 -0
  36. package/dist/cli/wp/types.d.ts +72 -0
  37. package/dist/cli/wp/types.js +8 -0
  38. package/dist/cli/wp/utils.d.ts +39 -0
  39. package/dist/cli/wp/utils.js +105 -0
  40. package/dist/client.d.ts +24 -0
  41. package/dist/client.js +55 -0
  42. package/dist/comment/errors.js +47 -0
  43. package/dist/comment/index.js +138 -0
  44. package/dist/comment/schema.js +72 -0
  45. package/dist/comment/utils.js +35 -0
  46. package/dist/config.d.ts +39 -0
  47. package/dist/config.js +7 -0
  48. package/dist/context.d.ts +73 -0
  49. package/dist/context.js +145 -0
  50. package/dist/cookie.d.ts +19 -0
  51. package/dist/cookie.js +25 -0
  52. package/dist/email/service.d.ts +11 -0
  53. package/dist/email/service.interface.d.ts +8 -0
  54. package/dist/email/service.js +20 -0
  55. package/dist/index.d.ts +39 -0
  56. package/dist/index.js +31 -0
  57. package/dist/integrations/nextjs/kizlo.client.d.ts +10 -0
  58. package/dist/integrations/nextjs/kizlo.client.js +13 -0
  59. package/dist/integrations/nextjs/kizlo.server.d.ts +12 -0
  60. package/dist/integrations/nextjs/kizlo.server.js +13 -0
  61. package/dist/integrations/nextjs/utils.js +11 -0
  62. package/dist/kizlo.d.ts +73 -0
  63. package/dist/kizlo.js +144 -0
  64. package/dist/menu/errors.js +16 -0
  65. package/dist/menu/fixture.d.ts +8 -0
  66. package/dist/menu/fixture.js +62 -0
  67. package/dist/menu/index.js +70 -0
  68. package/dist/menu/schema.js +70 -0
  69. package/dist/menu/utils.js +57 -0
  70. package/dist/nextjs/server.d.ts +2 -0
  71. package/dist/nextjs/server.js +3 -0
  72. package/dist/nextjs.d.ts +2 -0
  73. package/dist/nextjs.js +3 -0
  74. package/dist/package.js +5 -0
  75. package/dist/post/error.js +30 -0
  76. package/dist/post/fixture.d.ts +8 -0
  77. package/dist/post/fixture.js +71 -0
  78. package/dist/post/index.js +123 -0
  79. package/dist/post/schema.js +120 -0
  80. package/dist/post/utils.js +42 -0
  81. package/dist/post-type/service.js +48 -0
  82. package/dist/router.d.ts +624 -0
  83. package/dist/router.js +15 -0
  84. package/dist/seo/index.js +54 -0
  85. package/dist/seo/schema.d.ts +196 -0
  86. package/dist/seo/schema.js +96 -0
  87. package/dist/seo/types.d.ts +80 -0
  88. package/dist/seo/utils.js +44 -0
  89. package/dist/service.d.ts +17 -0
  90. package/dist/service.js +17 -0
  91. package/dist/shared/constants.js +10 -0
  92. package/dist/shared/contract.d.ts +21 -0
  93. package/dist/shared/contract.js +43 -0
  94. package/dist/shared/crypto.d.ts +28 -0
  95. package/dist/shared/crypto.js +34 -0
  96. package/dist/shared/error.d.ts +128 -0
  97. package/dist/shared/error.js +147 -0
  98. package/dist/shared/extension.d.ts +25 -0
  99. package/dist/shared/extension.js +7 -0
  100. package/dist/shared/identifier.d.ts +11 -0
  101. package/dist/shared/identifier.js +26 -0
  102. package/dist/shared/middleware.d.ts +29 -0
  103. package/dist/shared/procedure.d.ts +114 -0
  104. package/dist/shared/procedure.js +114 -0
  105. package/dist/shared/result.d.ts +24 -0
  106. package/dist/shared/result.js +49 -0
  107. package/dist/shared/router.d.ts +7 -0
  108. package/dist/shared/router.js +26 -0
  109. package/dist/shared/schema.d.ts +55 -0
  110. package/dist/shared/schema.js +43 -0
  111. package/dist/shared/serialize.d.ts +12 -0
  112. package/dist/shared/serialize.js +51 -0
  113. package/dist/shared/types.d.ts +35 -0
  114. package/dist/shared/utils.js +17 -0
  115. package/dist/test/auth.d.ts +7 -0
  116. package/dist/test/auth.js +15 -0
  117. package/dist/test/captcha.d.ts +7 -0
  118. package/dist/test/captcha.js +8 -0
  119. package/dist/test/client.d.ts +11 -0
  120. package/dist/test/client.js +14 -0
  121. package/dist/test/fixtures.d.ts +14 -0
  122. package/dist/test/fixtures.js +13 -0
  123. package/dist/test/server.d.ts +10 -0
  124. package/dist/test/server.js +32 -0
  125. package/dist/test/users.d.ts +14 -0
  126. package/dist/test/users.js +17 -0
  127. package/dist/test.d.ts +12 -0
  128. package/dist/test.js +13 -0
  129. package/dist/webhook/index.d.ts +38 -0
  130. package/dist/webhook/index.js +44 -0
  131. package/dist/webhook/schema.d.ts +45 -0
  132. package/dist/webhook/schema.js +72 -0
  133. package/dist/webhook/utils.js +21 -0
  134. package/dist/wordpress/comment/service.d.ts +28 -0
  135. package/dist/wordpress/comment/service.js +55 -0
  136. package/dist/wordpress/comment/types.d.ts +206 -0
  137. package/dist/wordpress/constants.d.ts +14 -0
  138. package/dist/wordpress/constants.js +13 -0
  139. package/dist/wordpress/error.d.ts +9 -0
  140. package/dist/wordpress/error.js +12 -0
  141. package/dist/wordpress/index.d.ts +15 -0
  142. package/dist/wordpress/menu/items/service.d.ts +28 -0
  143. package/dist/wordpress/menu/items/service.js +55 -0
  144. package/dist/wordpress/menu/items/types.d.ts +214 -0
  145. package/dist/wordpress/menu/service.d.ts +30 -0
  146. package/dist/wordpress/menu/service.js +58 -0
  147. package/dist/wordpress/menu/types.d.ts +115 -0
  148. package/dist/wordpress/post/service.d.ts +28 -0
  149. package/dist/wordpress/post/service.js +55 -0
  150. package/dist/wordpress/post/types.d.ts +290 -0
  151. package/dist/wordpress/service.d.ts +77 -0
  152. package/dist/wordpress/service.js +180 -0
  153. package/dist/wordpress/types.d.ts +67 -0
  154. package/dist/wordpress/user/service.d.ts +28 -0
  155. package/dist/wordpress/user/service.js +55 -0
  156. package/dist/wordpress/user/types.d.ts +221 -0
  157. package/dist/wordpress/utils.d.ts +7 -0
  158. package/dist/wordpress/utils.js +9 -0
  159. package/package.json +109 -0
@@ -0,0 +1,313 @@
1
+ import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, envKeysPresent, getVersion, loadEnvFiles, mergeEnv, resolveModuleImport, runCommand, writeFileIfAbsent } from "../utils.js";
2
+ import { CONTRACT_BARREL } from "../daemon/generate.js";
3
+ import { printBanner } from "../banner.js";
4
+ import { PRESETS, detectPreset, getPreset } from "../presets/index.js";
5
+ import z from "zod/v4";
6
+ import { randomBytes } from "node:crypto";
7
+ import path from "node:path";
8
+ import fs from "node:fs";
9
+ import { defineCommand } from "citty";
10
+ import * as p from "@clack/prompts";
11
+
12
+ //#region src/cli/commands/init.ts
13
+ const WP_ENV_KEYS = [
14
+ "SITE_SECRET",
15
+ "WORDPRESS_URL",
16
+ "WORDPRESS_USERNAME",
17
+ "WORDPRESS_APPLICATION_PASSWORD"
18
+ ];
19
+ function envKeys(preset) {
20
+ return [preset.baseUrlEnvKey, ...WP_ENV_KEYS];
21
+ }
22
+ function defaultDir(hasSrcDir) {
23
+ return hasSrcDir ? "src/lib/kizlo" : "lib/kizlo";
24
+ }
25
+ /** Normalizes an alias prefix to the familiar `@/` form (or empty for relative). */
26
+ function aliasWithSlash(alias) {
27
+ return alias ? `${alias.replace(/\/+$/, "")}/` : "";
28
+ }
29
+ function kizloConfigTemplate(dir, alias) {
30
+ return `import { defineConfig } from "kizlo/config"
31
+
32
+ export default defineConfig({
33
+ dir: "${dir}",${alias ? `\n\talias: "${alias}",` : ""}
34
+ })
35
+ `;
36
+ }
37
+ /** Appends the API path to the base URL so the client and route handler agree. */
38
+ function withApiPath(baseUrl, apiPath) {
39
+ try {
40
+ const url = new URL(baseUrl);
41
+ const current = url.pathname.replace(/\/+$/, "");
42
+ if (current.endsWith(apiPath)) return url.toString().replace(/\/+$/, "");
43
+ url.pathname = `${current}${apiPath}`;
44
+ return url.toString().replace(/\/+$/, "");
45
+ } catch {
46
+ return baseUrl;
47
+ }
48
+ }
49
+ function detectAppDir(cwd, hasSrcDir) {
50
+ if (fs.existsSync(path.join(cwd, "src", "app"))) return "src/app";
51
+ if (fs.existsSync(path.join(cwd, "app"))) return "app";
52
+ return hasSrcDir ? "src/app" : "app";
53
+ }
54
+ const requiredString = z.string().trim().min(1, "Required");
55
+ const urlString = requiredString.pipe(z.url("Must be a valid URL (e.g. https://example.com)"));
56
+ const dirPath = requiredString.refine((value) => !value.endsWith(".ts"), "Enter a directory, not a file");
57
+ function validate(schema) {
58
+ return (value) => {
59
+ const result = schema.safeParse(value ?? "");
60
+ return result.success ? void 0 : result.error.issues[0]?.message;
61
+ };
62
+ }
63
+ function orCancel(value) {
64
+ if (p.isCancel(value)) {
65
+ p.cancel("Setup cancelled.");
66
+ process.exit(0);
67
+ }
68
+ return value;
69
+ }
70
+ async function collectInteractively(ctx) {
71
+ const baseUrl = orCancel(await p.text({
72
+ message: "Public server base URL",
73
+ placeholder: "https://your-app.com",
74
+ validate: validate(urlString)
75
+ }));
76
+ const siteSecret = orCancel(await p.select({
77
+ message: "SITE_SECRET (signing key)",
78
+ initialValue: "generate",
79
+ options: [{
80
+ value: "generate",
81
+ label: "Generate a secure secret automatically",
82
+ hint: "recommended"
83
+ }, {
84
+ value: "enter",
85
+ label: "Enter my own"
86
+ }]
87
+ })) === "enter" ? orCancel(await p.password({
88
+ message: "Enter SITE_SECRET",
89
+ validate: validate(requiredString)
90
+ })) : randomBytes(32).toString("hex");
91
+ const wpUrl = orCancel(await p.text({
92
+ message: "WordPress URL",
93
+ placeholder: "https://wp.your-app.com",
94
+ validate: validate(urlString)
95
+ }));
96
+ const wpUsername = orCancel(await p.text({
97
+ message: "WordPress username",
98
+ validate: validate(requiredString)
99
+ }));
100
+ const wpPassword = orCancel(await p.password({
101
+ message: "WordPress application password",
102
+ validate: validate(requiredString)
103
+ }));
104
+ const dir = orCancel(await p.text({
105
+ message: "Kizlo directory",
106
+ initialValue: defaultDir(ctx.hasSrcDir),
107
+ validate: validate(dirPath)
108
+ }));
109
+ let alias = "";
110
+ if (ctx.preset.routeHandler) {
111
+ const serverDir = path.join(dir.replace(/^\.\//, "").replace(/\/+$/, ""), "server");
112
+ const detected = detectImportAlias(ctx.cwd, serverDir)?.prefix;
113
+ alias = orCancel(await p.text({
114
+ message: "Import alias (blank for relative imports)",
115
+ placeholder: "@/",
116
+ initialValue: detected ? `${detected}/` : ""
117
+ })).trim();
118
+ }
119
+ return {
120
+ baseUrl,
121
+ siteSecret,
122
+ wpUrl,
123
+ wpUsername,
124
+ wpPassword,
125
+ dir,
126
+ alias
127
+ };
128
+ }
129
+ /**
130
+ * Non-interactive setup: skip prompts and use defaults. Values present in the
131
+ * environment are used; missing ones are left empty for the user to fill in
132
+ * later. Never fails — `--yes` always scaffolds a fillable project.
133
+ */
134
+ function collectFromEnv(ctx) {
135
+ const dir = defaultDir(ctx.hasSrcDir);
136
+ return {
137
+ baseUrl: process.env[ctx.preset.baseUrlEnvKey]?.trim() ?? "",
138
+ siteSecret: process.env.SITE_SECRET?.trim() || randomBytes(32).toString("hex"),
139
+ wpUrl: process.env.WORDPRESS_URL?.trim() ?? "",
140
+ wpUsername: process.env.WORDPRESS_USERNAME?.trim() ?? "",
141
+ wpPassword: process.env.WORDPRESS_APPLICATION_PASSWORD?.trim() ?? "",
142
+ dir,
143
+ alias: ctx.preset.routeHandler ? aliasWithSlash(detectImportAlias(ctx.cwd, path.join(dir, "server"))?.prefix) : ""
144
+ };
145
+ }
146
+ function readPkg(pkgPath) {
147
+ return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
148
+ }
149
+ const init = defineCommand({
150
+ meta: {
151
+ name: "init",
152
+ description: "Set up Kizlo in the current project"
153
+ },
154
+ args: {
155
+ yes: {
156
+ type: "boolean",
157
+ alias: "y",
158
+ description: "Skip prompts and scaffold with defaults (non-interactive)",
159
+ default: false
160
+ },
161
+ force: {
162
+ type: "boolean",
163
+ alias: "f",
164
+ description: "Overwrite existing .env values and server entry without asking",
165
+ default: false
166
+ },
167
+ preset: {
168
+ type: "string",
169
+ description: `Force a setup preset (${PRESETS.map((preset) => preset.id).join(", ")})`
170
+ },
171
+ alias: {
172
+ type: "string",
173
+ description: "Import alias prefix for generated imports (e.g. @); blank for relative"
174
+ }
175
+ },
176
+ async run({ args }) {
177
+ const yes = Boolean(args.yes);
178
+ const force = Boolean(args.force);
179
+ const cwd = process.cwd();
180
+ const pkgPath = path.join(cwd, "package.json");
181
+ if (!yes) printBanner(getVersion());
182
+ p.intro("kizlo init");
183
+ if (!fs.existsSync(pkgPath)) {
184
+ p.cancel("No package.json found — run `kizlo init` inside a project.");
185
+ process.exit(1);
186
+ }
187
+ const pkg = readPkg(pkgPath);
188
+ const pm = detectPackageManager(cwd);
189
+ const hasKizlo = Boolean(pkg.dependencies?.kizlo) || Boolean(pkg.devDependencies?.kizlo);
190
+ const hasSrcDir = fs.existsSync(path.join(cwd, "src"));
191
+ const initCtx = {
192
+ cwd,
193
+ pkg,
194
+ pm,
195
+ hasSrcDir
196
+ };
197
+ let preset;
198
+ if (args.preset) {
199
+ const chosen = getPreset(String(args.preset));
200
+ if (!chosen) {
201
+ p.cancel(`Unknown preset "${args.preset}". Available: ${PRESETS.map((pr) => pr.id).join(", ")}`);
202
+ process.exit(1);
203
+ }
204
+ preset = chosen;
205
+ } else {
206
+ preset = detectPreset(initCtx);
207
+ if (preset.id !== "base") p.log.success(`${preset.label} detected`);
208
+ }
209
+ if (yes) loadEnvFiles(cwd);
210
+ const setup = yes ? collectFromEnv({
211
+ cwd,
212
+ hasSrcDir,
213
+ preset
214
+ }) : await collectInteractively({
215
+ cwd,
216
+ hasSrcDir,
217
+ preset
218
+ });
219
+ if (args.alias !== void 0) setup.alias = String(args.alias).trim();
220
+ setup.alias = aliasWithSlash(setup.alias);
221
+ if (!hasKizlo) {
222
+ const s = p.spinner();
223
+ s.start(`Installing kizlo with ${pm}`);
224
+ const ok = runCommand(addDependencyArgs(pm, "kizlo"), cwd, "ignore");
225
+ s.stop(ok ? "Installed kizlo" : "Could not install kizlo automatically");
226
+ if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, "kizlo").join(" ")}`);
227
+ }
228
+ if (preset.apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, preset.apiPath);
229
+ const keys = envKeys(preset);
230
+ const envValues = {
231
+ [preset.baseUrlEnvKey]: setup.baseUrl,
232
+ SITE_SECRET: setup.siteSecret,
233
+ WORDPRESS_URL: setup.wpUrl,
234
+ WORDPRESS_USERNAME: setup.wpUsername,
235
+ WORDPRESS_APPLICATION_PASSWORD: setup.wpPassword
236
+ };
237
+ const envPath = path.join(cwd, ".env");
238
+ const envExisted = fs.existsSync(envPath);
239
+ const existingEnv = envExisted ? fs.readFileSync(envPath, "utf8") : "";
240
+ const conflicts = envKeysPresent(existingEnv, keys);
241
+ let overwriteKeys = new Set(keys);
242
+ if (conflicts.length && !force) if (yes) {
243
+ overwriteKeys = /* @__PURE__ */ new Set();
244
+ p.log.info("Keeping existing .env values (pass --force to overwrite)");
245
+ } else {
246
+ p.log.warn("Some environment variables already exist in .env");
247
+ if (!orCancel(await p.confirm({
248
+ message: "Overwrite their existing values?",
249
+ initialValue: true
250
+ }))) overwriteKeys = /* @__PURE__ */ new Set();
251
+ }
252
+ const merge = mergeEnv(existingEnv, envValues, overwriteKeys);
253
+ fs.writeFileSync(envPath, merge.content);
254
+ const exampleCreated = writeFileIfAbsent(path.join(cwd, ".env.example"), `${keys.map((key) => `${key}=`).join("\n")}\n`);
255
+ const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
256
+ const configCreated = writeFileIfAbsent(path.join(cwd, "kizlo.config.ts"), kizloConfigTemplate(dirRel, setup.alias));
257
+ const serverDirRel = path.join(dirRel, "server");
258
+ const serverEntryRel = path.join(serverDirRel, "index.ts");
259
+ const serverPath = path.join(cwd, serverEntryRel);
260
+ const serverExisted = fs.existsSync(serverPath);
261
+ let wroteServer = !serverExisted || force;
262
+ if (serverExisted && !force && !yes) {
263
+ p.log.warn(`A Kizlo server instance already exists at ${serverEntryRel}`);
264
+ wroteServer = orCancel(await p.confirm({
265
+ message: "Overwrite it?",
266
+ initialValue: true
267
+ }));
268
+ }
269
+ if (wroteServer) {
270
+ fs.mkdirSync(path.dirname(serverPath), { recursive: true });
271
+ fs.writeFileSync(serverPath, preset.serverEntry());
272
+ }
273
+ const generatedDirRel = path.join(serverDirRel, "generated");
274
+ writeFileIfAbsent(path.join(cwd, generatedDirRel, "contract.json"), "{}\n");
275
+ writeFileIfAbsent(path.join(cwd, generatedDirRel, "index.ts"), CONTRACT_BARREL);
276
+ const scaffold = {
277
+ serverDirName: path.basename(serverDirRel),
278
+ appDir: detectAppDir(cwd, hasSrcDir),
279
+ serverImport: (fromDir) => resolveModuleImport(cwd, serverDirRel, fromDir, setup.alias)
280
+ };
281
+ const clientRel = path.join(dirRel, "client.ts");
282
+ const clientCreated = writeFileIfAbsent(path.join(cwd, clientRel), preset.clientEntry(scaffold));
283
+ let routeRel;
284
+ let routeCreated = false;
285
+ if (preset.routeHandler) {
286
+ const route = preset.routeHandler(scaffold);
287
+ routeRel = route.path;
288
+ routeCreated = writeFileIfAbsent(path.join(cwd, route.path), route.contents);
289
+ }
290
+ const gitignore = ensureGitignored(cwd, ".env");
291
+ if (!envExisted) p.log.success("Created .env");
292
+ else if (merge.updated.length || merge.added.length) p.log.success("Updated .env");
293
+ else p.log.info("Left .env unchanged");
294
+ p.log.success(exampleCreated ? "Created .env.example" : "Skipped .env.example (exists)");
295
+ p.log.success(configCreated ? "Created kizlo.config.ts" : "Skipped kizlo.config.ts (exists)");
296
+ if (wroteServer) p.log.success(`${serverExisted ? "Overwrote" : "Created"} Kizlo server instance (${serverEntryRel})`);
297
+ else p.log.info(`Kept existing Kizlo server instance (${serverEntryRel})${yes ? " — pass --force to overwrite" : ""}`);
298
+ p.log.success(clientCreated ? `Created browser client (${clientRel})` : "Skipped browser client (exists)");
299
+ if (routeRel) p.log.success(routeCreated ? `Created API route (${routeRel})` : "Skipped API route (exists)");
300
+ if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
301
+ p.note([
302
+ `Watch your extensions and regenerate the contract during development:`,
303
+ ` npx kizlo watch`,
304
+ ``,
305
+ `Generate the contract once for production builds:`,
306
+ ` npx kizlo generate`
307
+ ].join("\n"), "Next steps");
308
+ p.outro("Kizlo is ready 🎉");
309
+ }
310
+ });
311
+
312
+ //#endregion
313
+ export { init };
@@ -0,0 +1,68 @@
1
+ import { isSeeded } from "../wp/utils.js";
2
+ import { composeDown, composeUp } from "../wp/docker.js";
3
+ import { withSpinner } from "../utils.js";
4
+ import { log } from "../daemon/logger.js";
5
+ import { resolveTestConfig } from "../daemon/config.js";
6
+ import { runSeeds } from "../wp/setup.js";
7
+ import { spawn } from "node:child_process";
8
+ import { defineCommand } from "citty";
9
+
10
+ //#region src/cli/commands/test.ts
11
+ /** `<pm> test` runs the project's own test script (bun needs `run` to skip its built-in runner). */
12
+ function testCommand(pm) {
13
+ return pm === "bun" ? [
14
+ "bun",
15
+ "run",
16
+ "test"
17
+ ] : [pm, "test"];
18
+ }
19
+ /** Spawn the test command, inheriting stdio, and resolve with its exit code. */
20
+ function spawnTest(command, pm) {
21
+ const argv = command ? [command] : testCommand(pm);
22
+ const child = spawn(argv[0], argv.slice(1), {
23
+ stdio: "inherit",
24
+ shell: command !== void 0 || process.platform === "win32"
25
+ });
26
+ return new Promise((resolve, reject) => {
27
+ child.on("error", reject);
28
+ child.on("close", (code) => resolve(code ?? 0));
29
+ });
30
+ }
31
+ const test = defineCommand({
32
+ meta: {
33
+ name: "test",
34
+ description: "Boot the WordPress test stack (if needed), seed it, and run your tests against it"
35
+ },
36
+ args: {
37
+ teardown: {
38
+ type: "boolean",
39
+ description: "Tear down the test stack after tests finish (default: leave it running for fast reruns)"
40
+ },
41
+ reset: {
42
+ type: "boolean",
43
+ description: "Wipe the database and reseed before running"
44
+ }
45
+ },
46
+ async run({ args }) {
47
+ const cfg = await resolveTestConfig(process.cwd());
48
+ process.env.WP_PORT = String(cfg.port);
49
+ if (args.reset) await withSpinner("Wiping WordPress database", () => composeDown({ volumes: true }), "Database wiped");
50
+ await withSpinner("Starting WordPress test stack", composeUp, "WordPress test stack ready");
51
+ if (!await isSeeded()) await withSpinner("Seeding WordPress", () => runSeeds({
52
+ port: cfg.port,
53
+ fixtures: cfg.fixtures
54
+ }), "WordPress seeded");
55
+ log.info(cfg.command ? `Running: ${cfg.command}` : `Running: ${testCommand(cfg.packageManager).join(" ")}`);
56
+ let code = 1;
57
+ try {
58
+ code = await spawnTest(cfg.command, cfg.packageManager);
59
+ } finally {
60
+ if (args.teardown) await withSpinner("Tearing down WordPress test stack", () => composeDown(), "WordPress test stack stopped");
61
+ else log.info("Stack left running for fast reruns — `kizlo wp stop` to stop it, or rerun with --teardown.");
62
+ }
63
+ process.exit(code);
64
+ }
65
+ });
66
+
67
+ //#endregion
68
+ export { test };
@@ -0,0 +1,53 @@
1
+ import { getVersion } from "../utils.js";
2
+ import { log } from "../daemon/logger.js";
3
+ import { resolveConfig } from "../daemon/config.js";
4
+ import { generateOnce } from "../daemon/generate.js";
5
+ import { printBanner } from "../banner.js";
6
+ import { acquire, isLocked, lockPath, release } from "../daemon/lock.js";
7
+ import { watch as watch$1 } from "../daemon/watch.js";
8
+ import { defineCommand } from "citty";
9
+
10
+ //#region src/cli/commands/watch.ts
11
+ const args = { dir: {
12
+ type: "string",
13
+ description: "Override the Kizlo directory (defaults to kizlo.config.ts)"
14
+ } };
15
+ async function run({ args: args$1 }) {
16
+ const cwd = process.cwd();
17
+ const lock = lockPath(cwd);
18
+ if (isLocked(lock)) {
19
+ log.info("Watcher already running — nothing to do.");
20
+ return;
21
+ }
22
+ printBanner(getVersion());
23
+ const cfg = await resolveConfig(cwd, { dir: args$1.dir });
24
+ acquire(lock);
25
+ const cleanup = () => release(lock);
26
+ process.on("exit", cleanup);
27
+ for (const signal of [
28
+ "SIGINT",
29
+ "SIGTERM",
30
+ "SIGHUP"
31
+ ]) process.on(signal, () => {
32
+ cleanup();
33
+ process.exit(0);
34
+ });
35
+ try {
36
+ if (await generateOnce(cfg)) log.success("Contract generated");
37
+ else log.warn(`No Kizlo server found in ${cfg.serverEntry}`);
38
+ } catch (error) {
39
+ log.error("Failed to generate the Kizlo contract:", error);
40
+ }
41
+ await watch$1(cfg);
42
+ }
43
+ const watch = defineCommand({
44
+ meta: {
45
+ name: "watch",
46
+ description: "Watch your extensions and regenerate the Kizlo contract"
47
+ },
48
+ args,
49
+ run
50
+ });
51
+
52
+ //#endregion
53
+ export { watch };
@@ -0,0 +1,66 @@
1
+ import { isSeeded } from "../wp/utils.js";
2
+ import { composeDown, composeStop, composeUp } from "../wp/docker.js";
3
+ import { withSpinner } from "../utils.js";
4
+ import { log } from "../daemon/logger.js";
5
+ import { resolveTestConfig } from "../daemon/config.js";
6
+ import { runSeeds } from "../wp/setup.js";
7
+ import { defineCommand } from "citty";
8
+
9
+ //#region src/cli/commands/wp.ts
10
+ /** Boot the stack and seed it once (idempotent — skips seeding if already seeded). */
11
+ async function bringUp(cwd) {
12
+ const cfg = await resolveTestConfig(cwd);
13
+ process.env.WP_PORT = String(cfg.port);
14
+ await withSpinner("Starting WordPress test stack", composeUp, "WordPress test stack ready");
15
+ if (await isSeeded()) {
16
+ log.info("Stack already seeded — skipping seed.");
17
+ return;
18
+ }
19
+ await withSpinner("Seeding WordPress", () => runSeeds({
20
+ port: cfg.port,
21
+ fixtures: cfg.fixtures
22
+ }), "WordPress seeded");
23
+ log.success(`Stack ready on http://localhost:${cfg.port} — credentials at ${cfg.credentialsPath}`);
24
+ }
25
+ const up = defineCommand({
26
+ meta: {
27
+ name: "up",
28
+ description: "Start the WordPress test stack and seed it if needed"
29
+ },
30
+ async run() {
31
+ await bringUp(process.cwd());
32
+ }
33
+ });
34
+ const reset = defineCommand({
35
+ meta: {
36
+ name: "reset",
37
+ description: "Wipe the database and reseed a fresh WordPress test stack"
38
+ },
39
+ async run() {
40
+ await withSpinner("Wiping WordPress database", () => composeDown({ volumes: true }), "Database wiped");
41
+ await bringUp(process.cwd());
42
+ }
43
+ });
44
+ const stop = defineCommand({
45
+ meta: {
46
+ name: "stop",
47
+ description: "Stop the WordPress test stack, keeping the database volume"
48
+ },
49
+ async run() {
50
+ await withSpinner("Stopping WordPress test stack", composeStop, "Stack stopped (volumes kept)");
51
+ }
52
+ });
53
+ const wp = defineCommand({
54
+ meta: {
55
+ name: "wp",
56
+ description: "Manage the WordPress test stack (advanced: up | reset | stop)"
57
+ },
58
+ subCommands: {
59
+ up,
60
+ reset,
61
+ stop
62
+ }
63
+ });
64
+
65
+ //#endregion
66
+ export { wp };
@@ -0,0 +1,87 @@
1
+ import { credentialsPath, findConfigDir } from "../wp/utils.js";
2
+ import { detectPackageManager } from "../utils.js";
3
+ import { log } from "./logger.js";
4
+ import z from "zod/v4";
5
+ import path from "node:path";
6
+ import fs from "node:fs";
7
+ import { createJiti } from "jiti";
8
+
9
+ //#region src/cli/daemon/config.ts
10
+ const fixtureSchema = z.custom((value) => typeof value === "object" && value !== null && typeof value.name === "string", { message: "must be a fixture object with a string `name` (use defineFixture)" });
11
+ /** Runtime shape of `kizlo.config.*` — mirrors {@link KizloGlobalConfig}. */
12
+ const configSchema = z.object({
13
+ dir: z.string().optional(),
14
+ alias: z.string().optional(),
15
+ test: z.object({
16
+ port: z.number().int().positive().optional(),
17
+ fixtures: z.array(fixtureSchema).optional(),
18
+ packageManager: z.enum([
19
+ "npm",
20
+ "pnpm",
21
+ "yarn",
22
+ "bun"
23
+ ]).optional(),
24
+ command: z.string().optional()
25
+ }).optional()
26
+ });
27
+ const CONFIG_FILES = [
28
+ "kizlo.config.ts",
29
+ "kizlo.config.js",
30
+ "kizlo.config.mjs"
31
+ ];
32
+ function defaultDir(cwd) {
33
+ return fs.existsSync(path.join(cwd, "src")) ? "src/lib/kizlo" : "lib/kizlo";
34
+ }
35
+ async function loadConfigFile(cwd) {
36
+ const file = CONFIG_FILES.map((name) => path.join(cwd, name)).find((p) => fs.existsSync(p));
37
+ if (!file) return void 0;
38
+ let raw;
39
+ try {
40
+ const mod = await createJiti(cwd, { moduleCache: false }).import(file);
41
+ raw = mod.default ?? mod;
42
+ } catch (error) {
43
+ log.error(`Could not load ${path.basename(file)}:`, error);
44
+ process.exit(1);
45
+ }
46
+ const result = configSchema.safeParse(raw);
47
+ if (!result.success) {
48
+ log.error(`Invalid ${path.basename(file)}:\n${z.prettifyError(result.error)}`);
49
+ process.exit(1);
50
+ }
51
+ return result.data;
52
+ }
53
+ async function resolveConfig(cwd, flags) {
54
+ const fileConfig = await loadConfigFile(cwd);
55
+ const dir = (flags?.dir ?? fileConfig?.dir ?? defaultDir(cwd)).replace(/^\.\//, "").replace(/\/+$/, "");
56
+ const serverDir = path.join(dir, "server");
57
+ const generatedDir = path.join(serverDir, "generated");
58
+ return {
59
+ cwd,
60
+ dir,
61
+ serverDir,
62
+ serverEntry: path.join(serverDir, "index.ts"),
63
+ generatedDir,
64
+ contractPath: path.join(generatedDir, "contract.json"),
65
+ barrelPath: path.join(generatedDir, "index.ts")
66
+ };
67
+ }
68
+ /**
69
+ * Resolve the `test` block from `kizlo.config.*` into concrete values for the
70
+ * `wp`/`test` commands, applying defaults (port 8080, auto-detected package
71
+ * manager) and anchoring the credentials artifact to the config directory.
72
+ */
73
+ async function resolveTestConfig(cwd) {
74
+ const configDir = findConfigDir(cwd);
75
+ const test = (await loadConfigFile(configDir))?.test ?? {};
76
+ return {
77
+ configDir,
78
+ command: test.command,
79
+ port: test.port ?? 8080,
80
+ fixtures: test.fixtures ?? [],
81
+ credentialsPath: credentialsPath(cwd),
82
+ packageManager: test.packageManager ?? detectPackageManager(configDir)
83
+ };
84
+ }
85
+
86
+ //#endregion
87
+ export { resolveConfig, resolveTestConfig };
@@ -0,0 +1,40 @@
1
+ import { generateContract } from "../../shared/contract.js";
2
+ import { loadEnvFiles } from "../utils.js";
3
+ import path from "node:path";
4
+ import fs from "node:fs";
5
+ import { createJiti } from "jiti";
6
+
7
+ //#region src/cli/daemon/generate.ts
8
+ /** The generated contract barrel — also written as a stub by `kizlo init`. */
9
+ const CONTRACT_BARREL = [
10
+ `import type { router } from ".."`,
11
+ `import contractJson from "./contract.json"`,
12
+ ``,
13
+ `export const contract = contractJson as unknown as typeof router`,
14
+ ``
15
+ ].join("\n");
16
+ /** Write atomically: a reader never observes a half-written file. */
17
+ function atomicWrite(file, data) {
18
+ const tmp = `${file}.${process.pid}.tmp`;
19
+ fs.writeFileSync(tmp, data);
20
+ fs.renameSync(tmp, file);
21
+ }
22
+ /**
23
+ * Loads the Kizlo server, builds its contract, and writes `contract.json` and
24
+ * the generated barrel. Returns false when the entry has no Kizlo server.
25
+ */
26
+ async function generateOnce(cfg) {
27
+ loadEnvFiles(cfg.cwd);
28
+ const jiti = createJiti(cfg.cwd, { moduleCache: false });
29
+ const entry = path.resolve(cfg.cwd, cfg.serverEntry);
30
+ const { router } = await jiti.import(entry);
31
+ if (!router) return false;
32
+ const contract = JSON.stringify(await generateContract(router));
33
+ fs.mkdirSync(path.resolve(cfg.cwd, cfg.generatedDir), { recursive: true });
34
+ atomicWrite(path.resolve(cfg.cwd, cfg.contractPath), contract);
35
+ atomicWrite(path.resolve(cfg.cwd, cfg.barrelPath), CONTRACT_BARREL);
36
+ return true;
37
+ }
38
+
39
+ //#endregion
40
+ export { CONTRACT_BARREL, generateOnce };
@@ -0,0 +1,31 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+
4
+ //#region src/cli/daemon/lock.ts
5
+ function lockPath(cwd) {
6
+ return path.join(cwd, "node_modules/.cache/kizlo/dev.lock");
7
+ }
8
+ /** True when a live daemon already owns the lock. Stale locks are ignored. */
9
+ function isLocked(file) {
10
+ if (!fs.existsSync(file)) return false;
11
+ const pid = Number(fs.readFileSync(file, "utf8").trim());
12
+ if (!pid) return false;
13
+ try {
14
+ process.kill(pid, 0);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+ function acquire(file) {
21
+ fs.mkdirSync(path.dirname(file), { recursive: true });
22
+ fs.writeFileSync(file, String(process.pid));
23
+ }
24
+ function release(file) {
25
+ try {
26
+ fs.rmSync(file, { force: true });
27
+ } catch {}
28
+ }
29
+
30
+ //#endregion
31
+ export { acquire, isLocked, lockPath, release };
@@ -0,0 +1,8 @@
1
+ import { consola } from "consola";
2
+
3
+ //#region src/cli/daemon/logger.ts
4
+ /** Tagged logger for the daemon — every line is prefixed with `kizlo`. */
5
+ const log = consola.withTag("kizlo");
6
+
7
+ //#endregion
8
+ export { log };