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,37 @@
1
+ import { log } from "./logger.js";
2
+ import { generateOnce } from "./generate.js";
3
+ import path from "node:path";
4
+ import { FSWatcher } from "chokidar";
5
+
6
+ //#region src/cli/daemon/watch.ts
7
+ function debounce(fn, delay) {
8
+ let timer;
9
+ return ((...args) => {
10
+ clearTimeout(timer);
11
+ timer = setTimeout(() => void fn(...args), delay);
12
+ });
13
+ }
14
+ async function regenerate(cfg) {
15
+ try {
16
+ if (await generateOnce(cfg)) log.success("Contract updated");
17
+ else log.warn(`No Kizlo server found in ${cfg.serverEntry}`);
18
+ } catch (error) {
19
+ log.error("Failed to update the Kizlo contract:", error);
20
+ }
21
+ }
22
+ /** Watches the server directory and regenerates the contract on change. */
23
+ async function watch(cfg) {
24
+ const watcher = new FSWatcher({
25
+ persistent: true,
26
+ ignoreInitial: true,
27
+ ignored: path.resolve(cfg.cwd, cfg.generatedDir)
28
+ });
29
+ const onChange = debounce(() => regenerate(cfg), 300);
30
+ watcher.add(path.resolve(cfg.cwd, cfg.serverDir));
31
+ watcher.on("ready", () => log.start("Watching for changes..."));
32
+ watcher.on("all", () => void onChange());
33
+ return watcher;
34
+ }
35
+
36
+ //#endregion
37
+ export { watch };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import { getVersion } from "./utils.js";
3
+ import { defineCommand, runMain } from "citty";
4
+
5
+ //#region src/cli/index.ts
6
+ runMain(defineCommand({
7
+ meta: {
8
+ name: "kizlo",
9
+ version: getVersion(),
10
+ description: "Kizlo CLI — headless WordPress toolkit"
11
+ },
12
+ subCommands: {
13
+ init: () => import("./commands/init.js").then((m) => m.init),
14
+ watch: () => import("./commands/watch.js").then((m) => m.watch),
15
+ generate: () => import("./commands/generate.js").then((m) => m.generate),
16
+ wp: () => import("./commands/wp.js").then((m) => m.wp),
17
+ test: () => import("./commands/test.js").then((m) => m.test)
18
+ }
19
+ }));
20
+
21
+ //#endregion
22
+ export { };
@@ -0,0 +1,23 @@
1
+ //#region src/cli/presets/base.ts
2
+ const base = {
3
+ id: "base",
4
+ label: "Generic (no framework detected)",
5
+ detect: () => 1,
6
+ baseUrlEnvKey: "SERVER_BASE_URL",
7
+ serverEntry() {
8
+ return `import { createKizlo } from "kizlo"
9
+
10
+ export const { router, client, context, handler } = createKizlo()
11
+ `;
12
+ },
13
+ clientEntry(ctx) {
14
+ return `import { createKizloClient } from "kizlo"
15
+ import { contract } from "./${ctx.serverDirName}/generated"
16
+
17
+ export const client = createKizloClient(contract)
18
+ `;
19
+ }
20
+ };
21
+
22
+ //#endregion
23
+ export { base };
@@ -0,0 +1,16 @@
1
+ import { base } from "./base.js";
2
+ import { nextjs } from "./nextjs.js";
3
+
4
+ //#region src/cli/presets/index.ts
5
+ /** Framework presets, in declaration order. `base` always matches last. */
6
+ const PRESETS = [nextjs, base];
7
+ function getPreset(id) {
8
+ return PRESETS.find((preset) => preset.id === id);
9
+ }
10
+ /** Highest-scoring preset for the project; falls back to `base`. */
11
+ function detectPreset(ctx) {
12
+ return PRESETS.reduce((best, preset) => preset.detect(ctx) > best.detect(ctx) ? preset : best);
13
+ }
14
+
15
+ //#endregion
16
+ export { PRESETS, detectPreset, getPreset };
@@ -0,0 +1,39 @@
1
+ //#region src/cli/presets/nextjs.ts
2
+ const nextjs = {
3
+ id: "nextjs",
4
+ label: "Next.js",
5
+ detect(ctx) {
6
+ return {
7
+ ...ctx.pkg.dependencies,
8
+ ...ctx.pkg.devDependencies
9
+ }.next ? 100 : 0;
10
+ },
11
+ baseUrlEnvKey: "NEXT_PUBLIC_SERVER_BASE_URL",
12
+ apiPath: "/api/kizlo",
13
+ serverEntry() {
14
+ return `import { createKizlo } from "kizlo/nextjs/server"
15
+
16
+ export const { router, client, context, handler } = createKizlo()
17
+ `;
18
+ },
19
+ clientEntry(ctx) {
20
+ return `import { createKizloClient } from "kizlo/nextjs"
21
+ import { contract } from "./${ctx.serverDirName}/generated"
22
+
23
+ export const client = createKizloClient(contract)
24
+ `;
25
+ },
26
+ routeHandler(ctx) {
27
+ const dir = `${ctx.appDir}/api/kizlo/[[...rest]]`;
28
+ return {
29
+ path: `${dir}/route.ts`,
30
+ contents: `import { handler } from "${ctx.serverImport(dir)}"
31
+
32
+ export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE, handler as OPTIONS }
33
+ `
34
+ };
35
+ }
36
+ };
37
+
38
+ //#endregion
39
+ export { nextjs };
@@ -0,0 +1,182 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import fs from "node:fs";
5
+ import * as p from "@clack/prompts";
6
+
7
+ //#region src/cli/utils.ts
8
+ function stripJsonComments(source) {
9
+ return source.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (match, comment) => comment ? "" : match);
10
+ }
11
+ function readTsconfigPaths(cwd) {
12
+ const file = path.join(cwd, "tsconfig.json");
13
+ if (!fs.existsSync(file)) return void 0;
14
+ const raw = fs.readFileSync(file, "utf8");
15
+ let json;
16
+ try {
17
+ json = JSON.parse(raw);
18
+ } catch {
19
+ try {
20
+ json = JSON.parse(stripJsonComments(raw));
21
+ } catch {
22
+ return;
23
+ }
24
+ }
25
+ return json.compilerOptions?.paths;
26
+ }
27
+ function relativeImport(targetRel, fromDirRel) {
28
+ const rel = path.relative(fromDirRel, targetRel).split(path.sep).join("/");
29
+ return rel.startsWith(".") ? rel : `./${rel}`;
30
+ }
31
+ /**
32
+ * Detects an import alias from tsconfig `paths` that covers `targetRel`, returning
33
+ * `{ prefix, importPath }`. The prefix is what `init` persists to kizlo.config;
34
+ * the import path is the full specifier. Returns undefined when no alias matches.
35
+ */
36
+ function detectImportAlias(cwd, targetRel) {
37
+ const target = targetRel.split(path.sep).join("/").replace(/\/+$/, "");
38
+ for (const [alias, mappings] of Object.entries(readTsconfigPaths(cwd) ?? {})) {
39
+ if (!alias.endsWith("/*")) continue;
40
+ const prefix = alias.slice(0, -2);
41
+ for (const mapping of mappings ?? []) {
42
+ if (!mapping.endsWith("/*")) continue;
43
+ const base = mapping.slice(0, -2).replace(/^\.\//, "").replace(/^\.$/, "").replace(/\/+$/, "");
44
+ const baseSlash = base ? `${base}/` : "";
45
+ if (target === base || target.startsWith(baseSlash)) return {
46
+ prefix,
47
+ importPath: `${prefix}/${target.slice(baseSlash.length)}`
48
+ };
49
+ }
50
+ }
51
+ }
52
+ /**
53
+ * Builds an import specifier for `targetRel` from `fromDirRel`. With an explicit
54
+ * `alias` prefix (from kizlo.config), builds `<alias>/<target>`; an empty alias
55
+ * forces a relative import. Without one, detects from tsconfig, else relative.
56
+ */
57
+ function resolveModuleImport(cwd, targetRel, fromDirRel, alias) {
58
+ const target = targetRel.split(path.sep).join("/").replace(/\/+$/, "");
59
+ if (alias !== void 0) {
60
+ if (alias === "") return relativeImport(targetRel, fromDirRel);
61
+ const prefix = alias.replace(/\/+$/, "");
62
+ const base = detectImportAlias(cwd, targetRel);
63
+ return base && base.prefix === prefix ? base.importPath : `${prefix}/${target.replace(/^src\//, "")}`;
64
+ }
65
+ return detectImportAlias(cwd, targetRel)?.importPath ?? relativeImport(targetRel, fromDirRel);
66
+ }
67
+ /**
68
+ * Loads the project's `.env` files into `process.env`. The single place env
69
+ * values come from — shared by `init --yes` and the `kizlo watch`/`generate`
70
+ * daemon so they always read the same source.
71
+ */
72
+ function loadEnvFiles(cwd) {
73
+ for (const name of [".env", ".env.local"]) {
74
+ const file = path.join(cwd, name);
75
+ if (fs.existsSync(file)) process.loadEnvFile(file);
76
+ }
77
+ }
78
+ function getVersion() {
79
+ const here = path.dirname(fileURLToPath(import.meta.url));
80
+ const pkgPath = path.resolve(here, "../../package.json");
81
+ return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version;
82
+ }
83
+ function detectPackageManager(cwd) {
84
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
85
+ if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
86
+ if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) return "bun";
87
+ return "npm";
88
+ }
89
+ function addDependencyArgs(pm, pkg) {
90
+ if (pm === "npm") return [
91
+ "npm",
92
+ "install",
93
+ pkg
94
+ ];
95
+ return [
96
+ pm,
97
+ "add",
98
+ pkg
99
+ ];
100
+ }
101
+ function runCommand(args, cwd, stdio = "inherit") {
102
+ const [command, ...rest] = args;
103
+ if (!command) return false;
104
+ return spawnSync(command, rest, {
105
+ cwd,
106
+ stdio,
107
+ shell: process.platform === "win32"
108
+ }).status === 0;
109
+ }
110
+ function writeFileIfAbsent(filePath, contents) {
111
+ if (fs.existsSync(filePath)) return false;
112
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
113
+ fs.writeFileSync(filePath, contents);
114
+ return true;
115
+ }
116
+ function envKeysPresent(contents, keys) {
117
+ return keys.filter((key) => new RegExp(`^\\s*${key}\\s*=`, "m").test(contents));
118
+ }
119
+ /**
120
+ * Merges `values` into an existing .env body: other variables, comments and
121
+ * blank lines are preserved. A managed key is rewritten only when listed in
122
+ * `overwriteKeys`; otherwise its existing line is kept. Missing keys are appended.
123
+ */
124
+ function mergeEnv(existing, values, overwriteKeys) {
125
+ const body = existing.replace(/\s*$/, "");
126
+ const lines = body.length ? body.split(/\r?\n/) : [];
127
+ const added = [];
128
+ const updated = [];
129
+ const kept = [];
130
+ const present = /* @__PURE__ */ new Set();
131
+ const next = lines.map((line) => {
132
+ const key = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/)?.[1];
133
+ if (key && key in values) {
134
+ present.add(key);
135
+ if (overwriteKeys.has(key)) {
136
+ updated.push(key);
137
+ return `${key}=${values[key]}`;
138
+ }
139
+ kept.push(key);
140
+ }
141
+ return line;
142
+ });
143
+ for (const [key, value] of Object.entries(values)) if (!present.has(key)) {
144
+ added.push(key);
145
+ next.push(`${key}=${value}`);
146
+ }
147
+ return {
148
+ content: `${next.join("\n")}\n`,
149
+ added,
150
+ updated,
151
+ kept
152
+ };
153
+ }
154
+ function ensureGitignored(cwd, entry) {
155
+ const gitignorePath = path.join(cwd, ".gitignore");
156
+ const existed = fs.existsSync(gitignorePath);
157
+ const contents = existed ? fs.readFileSync(gitignorePath, "utf8") : "";
158
+ if (contents.split(/\r?\n/).some((line) => line.trim() === entry)) return "present";
159
+ const prefix = contents.length && !contents.endsWith("\n") ? `${contents}\n` : contents;
160
+ fs.writeFileSync(gitignorePath, `${prefix}${entry}\n`);
161
+ return existed ? "added" : "created";
162
+ }
163
+ /**
164
+ * Run a slow async step behind a clack spinner. Shows `message` while `fn` runs,
165
+ * stops with `done` (or `message`) on success, and stops with an error mark then
166
+ * rethrows on failure so callers keep their existing try/finally control flow.
167
+ */
168
+ async function withSpinner(message, fn, done) {
169
+ const s = p.spinner();
170
+ s.start(message);
171
+ try {
172
+ const result = await fn();
173
+ s.stop(done ?? message);
174
+ return result;
175
+ } catch (error) {
176
+ s.error(`${message} failed`);
177
+ throw error;
178
+ }
179
+ }
180
+
181
+ //#endregion
182
+ export { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, envKeysPresent, getVersion, loadEnvFiles, mergeEnv, resolveModuleImport, runCommand, withSpinner, writeFileIfAbsent };
@@ -0,0 +1,114 @@
1
+ import { ensurePlugin, resolvePluginSource } from "./utils.js";
2
+ import { DEFAULT_PLUGINS, DEFAULT_PORT, SEED_MARKER_OPTION, SEED_VERSION, TEST_ADMIN, TEST_USER } from "./constants.js";
3
+ import { compose, composeUp, wpCli } from "./docker.js";
4
+
5
+ //#region src/cli/wp/bootstrap.ts
6
+ async function seedUsers() {
7
+ const existing = await wpCli([
8
+ "user",
9
+ "get",
10
+ TEST_USER.username,
11
+ "--field=ID"
12
+ ]).catch(() => "");
13
+ const id = existing ? existing : await wpCli([
14
+ "user",
15
+ "create",
16
+ TEST_USER.username,
17
+ TEST_USER.email,
18
+ `--role=${TEST_USER.role}`,
19
+ `--user_pass=${TEST_USER.password}`,
20
+ "--porcelain"
21
+ ]);
22
+ if (!id) throw new Error("seed/users.json must include a user with username \"user\"");
23
+ return Number(id);
24
+ }
25
+ async function createAdminAppPassword() {
26
+ await compose([
27
+ "exec",
28
+ "-T",
29
+ "wp-cli",
30
+ "wp",
31
+ "user",
32
+ "application-password",
33
+ "delete",
34
+ TEST_ADMIN.username,
35
+ "--all"
36
+ ]);
37
+ return wpCli([
38
+ "user",
39
+ "application-password",
40
+ "create",
41
+ TEST_ADMIN.username,
42
+ "harness",
43
+ "--porcelain"
44
+ ]);
45
+ }
46
+ /**
47
+ * Phase 1 — boot WordPress via docker + wp-cli: core install, plugins (kizlo core +
48
+ * each fixture's plugin), the base users, the admin app password, and the seed
49
+ * marker. Content (posts/menu) is left to fixtures. Returns everything needed to
50
+ * construct the REST client in Phase 2.
51
+ */
52
+ async function bootstrapWp(config) {
53
+ const port = config.port ?? DEFAULT_PORT;
54
+ const url = `http://localhost:${port}`;
55
+ process.env.WP_PORT = String(port);
56
+ await composeUp();
57
+ if ((await compose([
58
+ "exec",
59
+ "-T",
60
+ "wp-cli",
61
+ "wp",
62
+ "core",
63
+ "is-installed"
64
+ ])).code !== 0) await wpCli([
65
+ "core",
66
+ "install",
67
+ `--url=${url}`,
68
+ "--title=Kizlo Test",
69
+ `--admin_user=${TEST_ADMIN.username}`,
70
+ `--admin_password=${TEST_ADMIN.password}`,
71
+ `--admin_email=${TEST_ADMIN.email}`,
72
+ "--skip-email"
73
+ ]);
74
+ await wpCli([
75
+ "rewrite",
76
+ "structure",
77
+ "/%postname%/",
78
+ "--hard"
79
+ ]);
80
+ for (const plugin of [...DEFAULT_PLUGINS, ...config.fixtures?.flatMap((a) => a.plugins ?? []) ?? []]) await ensurePlugin(...resolvePluginSource(plugin));
81
+ const adminId = Number(await wpCli([
82
+ "user",
83
+ "get",
84
+ TEST_ADMIN.username,
85
+ "--field=ID"
86
+ ]));
87
+ const userId = await seedUsers();
88
+ const appPassword = await createAdminAppPassword();
89
+ await wpCli([
90
+ "option",
91
+ "update",
92
+ SEED_MARKER_OPTION,
93
+ SEED_VERSION,
94
+ "--autoload=no"
95
+ ]);
96
+ return {
97
+ url,
98
+ users: {
99
+ admin: {
100
+ id: adminId,
101
+ ...TEST_ADMIN,
102
+ applicationPassword: appPassword
103
+ },
104
+ user: {
105
+ id: userId,
106
+ ...TEST_USER
107
+ }
108
+ },
109
+ fixtures: {}
110
+ };
111
+ }
112
+
113
+ //#endregion
114
+ export { bootstrapWp };
@@ -0,0 +1,73 @@
1
+ name: kizlo-wp-test
2
+
3
+ services:
4
+ mysql:
5
+ image: mysql:8
6
+ environment:
7
+ MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpass}
8
+ MYSQL_DATABASE: ${MYSQL_DATABASE:-wordpress}
9
+ MYSQL_USER: ${MYSQL_USER:-wordpress}
10
+ MYSQL_PASSWORD: ${MYSQL_PASSWORD:-wppass}
11
+ volumes:
12
+ - mysql_data:/var/lib/mysql
13
+ healthcheck:
14
+ test:
15
+ - CMD
16
+ - mysqladmin
17
+ - ping
18
+ - -h
19
+ - localhost
20
+ - -uroot
21
+ - -p${MYSQL_ROOT_PASSWORD:-rootpass}
22
+ interval: 5s
23
+ timeout: 5s
24
+ retries: 30
25
+ start_period: 20s
26
+
27
+ wordpress:
28
+ image: wordpress:latest
29
+ depends_on:
30
+ mysql:
31
+ condition: service_healthy
32
+ ports:
33
+ - "${WP_PORT:-8080}:80"
34
+ environment:
35
+ WORDPRESS_DB_HOST: mysql:3306
36
+ WORDPRESS_DB_NAME: ${MYSQL_DATABASE:-wordpress}
37
+ WORDPRESS_DB_USER: ${MYSQL_USER:-wordpress}
38
+ WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD:-wppass}
39
+ WORDPRESS_CONFIG_EXTRA: |
40
+ define('WP_ENVIRONMENT_TYPE', 'local');
41
+ volumes:
42
+ - wp_data:/var/www/html
43
+ healthcheck:
44
+ # wp-login.php always returns 200 unauthenticated; /wp-json can be gated to 401
45
+ # by plugins, so it's not a reliable liveness probe.
46
+ test: ["CMD-SHELL", "curl -sf http://localhost/wp-login.php > /dev/null || exit 1"]
47
+ interval: 5s
48
+ timeout: 5s
49
+ retries: 60
50
+ start_period: 30s
51
+
52
+ wp-cli:
53
+ image: wordpress:cli
54
+ depends_on:
55
+ wordpress:
56
+ condition: service_healthy
57
+ user: "33:33"
58
+ volumes:
59
+ - wp_data:/var/www/html
60
+ environment:
61
+ WORDPRESS_DB_HOST: mysql:3306
62
+ WORDPRESS_DB_NAME: ${MYSQL_DATABASE:-wordpress}
63
+ WORDPRESS_DB_USER: ${MYSQL_USER:-wordpress}
64
+ WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD:-wppass}
65
+ HOME: /tmp
66
+ WP_CLI_CACHE_DIR: /tmp/wp-cli-cache
67
+ # Kept warm (long-lived) so the harness can `docker compose exec wp-cli wp ...`
68
+ # instead of paying container-startup cost on every wp-cli call.
69
+ entrypoint: ["tail", "-f", "/dev/null"]
70
+
71
+ volumes:
72
+ mysql_data:
73
+ wp_data:
@@ -0,0 +1,52 @@
1
+ import { githubRelease } from "./utils.js";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ //#region src/cli/wp/constants.ts
6
+ const DEFAULT_PORT = 8080;
7
+ const HERE = dirname(fileURLToPath(import.meta.url));
8
+ /** The docker-compose file shipped alongside this module (in both `src/` and `dist/`). */
9
+ const COMPOSE_FILE = resolve(HERE, "compose/docker-compose.yml");
10
+ const CONFIG_FILES = [
11
+ "kizlo.config.ts",
12
+ "kizlo.config.js",
13
+ "kizlo.config.mjs"
14
+ ];
15
+ /** Credentials artifact location, relative to the config root. */
16
+ const CREDENTIALS_REL = ".kizlo/test-credentials.json";
17
+ const COMPOSE = [
18
+ "compose",
19
+ "-f",
20
+ COMPOSE_FILE
21
+ ];
22
+ /**
23
+ * DB-side marker written as the final bootstrap step and checked by `isSeeded`.
24
+ * It records that this database holds a completed bootstrap — independent of any
25
+ * fixture content, which is optional and varies per consumer. Bump `SEED_VERSION`
26
+ * to force a reseed when the bootstrap contract changes.
27
+ */
28
+ const SEED_MARKER_OPTION = "kizlo_test_seeded";
29
+ const SEED_VERSION = "1";
30
+ /** The kizlo core WordPress plugin, always installed during bootstrap. */
31
+ const DEFAULT_PLUGINS = [{
32
+ name: "kizlo",
33
+ source: githubRelease("kizlo-io/kizlo-wordpress", "kizlo-v1.0.0-beta.3")
34
+ }];
35
+ const TEST_ADMIN = {
36
+ firstName: "Admin",
37
+ lastName: "",
38
+ username: "admin",
39
+ password: "admin_pass",
40
+ email: "admin@example.com"
41
+ };
42
+ const TEST_USER = {
43
+ firstName: "User",
44
+ lastName: "",
45
+ username: "user",
46
+ email: "user@example.com",
47
+ password: "user_pass",
48
+ role: "subscriber"
49
+ };
50
+
51
+ //#endregion
52
+ export { COMPOSE, CONFIG_FILES, CREDENTIALS_REL, DEFAULT_PLUGINS, DEFAULT_PORT, SEED_MARKER_OPTION, SEED_VERSION, TEST_ADMIN, TEST_USER };
@@ -0,0 +1,24 @@
1
+ //#region src/cli/wp/docker.d.ts
2
+ interface RunResult {
3
+ code: number;
4
+ stdout: string;
5
+ stderr: string;
6
+ }
7
+ /** `docker compose <args>` against the harness compose file. */
8
+ declare function compose(args: string[], opts?: {
9
+ input?: string;
10
+ }): Promise<RunResult>;
11
+ /** Bring services up detached and wait for health checks. */
12
+ declare function composeUp(): Promise<void>;
13
+ /** Tear everything down, optionally removing volumes. */
14
+ declare function composeDown(opts?: {
15
+ volumes?: boolean;
16
+ }): Promise<void>;
17
+ /** `docker compose stop` — halt containers but keep volumes (DB intact). */
18
+ declare function composeStop(): Promise<void>;
19
+ /**
20
+ * Run a wp-cli command against the warm `wp-cli` container and return trimmed stdout.
21
+ */
22
+ declare function wpCli(args: string[]): Promise<string>;
23
+ //#endregion
24
+ export { compose, composeDown, composeStop, composeUp, wpCli };
@@ -0,0 +1,69 @@
1
+ import { COMPOSE } from "./constants.js";
2
+ import { spawn } from "node:child_process";
3
+
4
+ //#region src/cli/wp/docker.ts
5
+ function run(cmd, args, opts) {
6
+ return new Promise((resolvePromise, reject) => {
7
+ const child = spawn(cmd, args, { stdio: [
8
+ "pipe",
9
+ "pipe",
10
+ "pipe"
11
+ ] });
12
+ let stdout = "";
13
+ let stderr = "";
14
+ child.stdout.on("data", (d) => {
15
+ stdout += d;
16
+ });
17
+ child.stderr.on("data", (d) => {
18
+ stderr += d;
19
+ });
20
+ child.on("error", reject);
21
+ child.on("close", (code) => resolvePromise({
22
+ code: code ?? 0,
23
+ stdout,
24
+ stderr
25
+ }));
26
+ if (opts?.input !== void 0) child.stdin.end(opts.input);
27
+ else child.stdin.end();
28
+ });
29
+ }
30
+ /** `docker compose <args>` against the harness compose file. */
31
+ async function compose(args, opts) {
32
+ return run("docker", [...COMPOSE, ...args], opts);
33
+ }
34
+ /** Bring services up detached and wait for health checks. */
35
+ async function composeUp() {
36
+ const res = await compose([
37
+ "up",
38
+ "-d",
39
+ "--wait"
40
+ ]);
41
+ if (res.code !== 0) throw new Error(`docker compose up failed:\n${res.stderr}`);
42
+ }
43
+ /** Tear everything down, optionally removing volumes. */
44
+ async function composeDown(opts) {
45
+ const args = ["down"];
46
+ if (opts?.volumes) args.push("-v");
47
+ await compose(args);
48
+ }
49
+ /** `docker compose stop` — halt containers but keep volumes (DB intact). */
50
+ async function composeStop() {
51
+ await compose(["stop"]);
52
+ }
53
+ /**
54
+ * Run a wp-cli command against the warm `wp-cli` container and return trimmed stdout.
55
+ */
56
+ async function wpCli(args) {
57
+ const res = await compose([
58
+ "exec",
59
+ "-T",
60
+ "wp-cli",
61
+ "wp",
62
+ ...args
63
+ ]);
64
+ if (res.code !== 0) throw new Error(`wp ${args.join(" ")} failed:\n${res.stderr || res.stdout}`);
65
+ return res.stdout.replace(/\r/g, "").trim();
66
+ }
67
+
68
+ //#endregion
69
+ export { compose, composeDown, composeStop, composeUp, wpCli };