kizlo 0.1.0 → 0.2.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 (87) hide show
  1. package/dist/adapters/auth.d.ts +11 -2
  2. package/dist/adapters/auth.js +5 -1
  3. package/dist/adapters/captcha.d.ts +3 -1
  4. package/dist/adapters/captcha.js +5 -1
  5. package/dist/adapters/cookies.d.ts +7 -0
  6. package/dist/adapters/cookies.js +8 -0
  7. package/dist/adapters/geo.d.ts +13 -2
  8. package/dist/adapters/geo.js +5 -1
  9. package/dist/adapters/logger.d.ts +8 -1
  10. package/dist/adapters/logger.js +5 -1
  11. package/dist/cli/banner.js +12 -1
  12. package/dist/cli/commands/dev.js +262 -0
  13. package/dist/cli/commands/init.js +235 -43
  14. package/dist/cli/commands/test.js +133 -34
  15. package/dist/cli/commands/watch.js +2 -2
  16. package/dist/cli/daemon/config.js +73 -7
  17. package/dist/cli/daemon/generate.js +1 -1
  18. package/dist/cli/daemon/lock.js +1 -1
  19. package/dist/cli/index.js +1 -1
  20. package/dist/cli/presets/base.js +8 -1
  21. package/dist/cli/presets/nextjs.js +1 -1
  22. package/dist/cli/utils.js +90 -6
  23. package/dist/cli/wp/bootstrap.js +13 -10
  24. package/dist/cli/wp/byo.js +113 -0
  25. package/dist/cli/wp/compose/docker-compose.yml +13 -2
  26. package/dist/cli/wp/compose/opcache.ini +16 -0
  27. package/dist/cli/wp/compose/override.js +104 -0
  28. package/dist/cli/wp/compose/override.test.ts +115 -0
  29. package/dist/cli/wp/compose/override.ts +134 -0
  30. package/dist/cli/wp/compose/remap-entrypoint.sh +17 -0
  31. package/dist/cli/wp/constants.js +7 -8
  32. package/dist/cli/wp/dev.js +146 -0
  33. package/dist/cli/wp/docker.d.ts +55 -15
  34. package/dist/cli/wp/docker.js +102 -41
  35. package/dist/cli/wp/ports.js +72 -0
  36. package/dist/cli/wp/session.js +150 -0
  37. package/dist/cli/wp/settings.js +73 -0
  38. package/dist/cli/wp/setup.js +1 -1
  39. package/dist/cli/wp/stack.js +58 -0
  40. package/dist/cli/wp/types.d.ts +26 -5
  41. package/dist/cli/wp/types.js +5 -1
  42. package/dist/cli/wp/utils.d.ts +29 -2
  43. package/dist/cli/wp/utils.js +61 -3
  44. package/dist/cli/wp/watchdog.d.ts +1 -0
  45. package/dist/cli/wp/watchdog.js +45 -0
  46. package/dist/client.d.ts +1 -1
  47. package/dist/client.js +2 -2
  48. package/dist/config.d.ts +49 -3
  49. package/dist/context.d.ts +12 -1
  50. package/dist/context.js +1 -1
  51. package/dist/index.d.ts +16 -11
  52. package/dist/index.js +10 -6
  53. package/dist/integrations/nextjs/kizlo.server.d.ts +1 -1
  54. package/dist/integrations/nextjs/kizlo.server.js +2 -2
  55. package/dist/integrations/nextjs/utils.js +2 -2
  56. package/dist/kizlo.d.ts +27 -6
  57. package/dist/kizlo.js +24 -11
  58. package/dist/menu/fixture.d.ts +1 -1
  59. package/dist/package.js +1 -1
  60. package/dist/post/fixture.d.ts +1 -1
  61. package/dist/service.d.ts +2 -0
  62. package/dist/service.js +3 -0
  63. package/dist/settings/service.d.ts +22 -0
  64. package/dist/settings/service.interface.d.ts +134 -0
  65. package/dist/settings/service.js +43 -0
  66. package/dist/shared/error.d.ts +16 -5
  67. package/dist/shared/error.js +4 -0
  68. package/dist/shared/extension.d.ts +2 -0
  69. package/dist/shared/middleware.d.ts +13 -10
  70. package/dist/shared/middleware.js +7 -0
  71. package/dist/shared/procedure.d.ts +34 -28
  72. package/dist/shared/procedure.js +3 -6
  73. package/dist/shared/types.d.ts +19 -13
  74. package/dist/test/client.d.ts +5 -0
  75. package/dist/test/client.js +5 -0
  76. package/dist/test/server.d.ts +6 -0
  77. package/dist/test/server.js +6 -0
  78. package/dist/test.d.ts +4 -4
  79. package/dist/test.js +4 -4
  80. package/dist/webhook/index.d.ts +14 -13
  81. package/dist/webhook/index.js +5 -5
  82. package/dist/webhook/schema.d.ts +3 -3
  83. package/dist/webhook/schema.js +37 -37
  84. package/dist/webhook/utils.js +4 -4
  85. package/dist/wordpress/service.js +6 -0
  86. package/package.json +2 -2
  87. package/dist/cli/commands/wp.js +0 -66
@@ -0,0 +1,115 @@
1
+ import fs from "node:fs"
2
+ import os from "node:os"
3
+ import path from "node:path"
4
+ import { afterEach, beforeEach, describe, expect, test } from "vitest"
5
+ import { OPCACHE_INI, REMAP_ENTRYPOINT } from "../constants"
6
+ import { writeDevOverride, writeTestOverride } from "./override"
7
+
8
+ describe("writeDevOverride", () => {
9
+ let dir: string
10
+ let wordpressDir: string
11
+
12
+ beforeEach(() => {
13
+ dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "kizlo-ovr-")))
14
+ wordpressDir = path.join(dir, "wordpress")
15
+ })
16
+
17
+ afterEach(() => {
18
+ fs.rmSync(dir, { recursive: true, force: true })
19
+ })
20
+
21
+ test("bind-mounts the whole install on both services and creates the host dir", () => {
22
+ const file = writeDevOverride(dir, { wordpressDir, mounts: [] })
23
+ expect(file).toBe(path.join(dir, ".kizlo", "docker-compose.dev.override.yml"))
24
+ expect(fs.existsSync(wordpressDir)).toBe(true)
25
+
26
+ const body = fs.readFileSync(file, "utf8")
27
+ expect(body).toContain(" wordpress:")
28
+ expect(body).toContain(" wp-cli:")
29
+ expect(body).toContain(`- "${wordpressDir}:/var/www/html"`)
30
+ })
31
+
32
+ test("always mounts the dev OPcache config into the wordpress service, read-only", () => {
33
+ const body = fs.readFileSync(writeDevOverride(dir, { wordpressDir, mounts: [] }), "utf8")
34
+ expect(body).toContain(`- "${OPCACHE_INI}:/usr/local/etc/php/conf.d/zz-kizlo-opcache.ini:ro"`)
35
+ })
36
+
37
+ test("layers plugin mounts by basename, after the root bind", () => {
38
+ const body = fs.readFileSync(writeDevOverride(dir, { wordpressDir, mounts: ["plugins/kizlo"] }), "utf8")
39
+ const rootBind = `- "${wordpressDir}:/var/www/html"`
40
+ const pluginBind = `- "${path.join(dir, "plugins/kizlo")}:/var/www/html/wp-content/plugins/kizlo"`
41
+ expect(body).toContain(pluginBind)
42
+ expect(body.indexOf(rootBind)).toBeLessThan(body.indexOf(pluginBind))
43
+ })
44
+
45
+ test("keeps absolute mount paths verbatim", () => {
46
+ const abs = path.join(dir, "external", "my-plugin")
47
+ const body = fs.readFileSync(writeDevOverride(dir, { wordpressDir, mounts: [abs] }), "utf8")
48
+ expect(body).toContain(`- "${abs}:/var/www/html/wp-content/plugins/my-plugin"`)
49
+ })
50
+
51
+ test("publishes MySQL on the given host port, bound to loopback", () => {
52
+ const body = fs.readFileSync(writeDevOverride(dir, { wordpressDir, mounts: [], dbPort: 3307 }), "utf8")
53
+ expect(body).toContain(" mysql:")
54
+ expect(body).toContain('- "127.0.0.1:3307:3306"')
55
+ })
56
+
57
+ test("leaves MySQL internal-only when no db port is given", () => {
58
+ const body = fs.readFileSync(writeDevOverride(dir, { wordpressDir, mounts: [] }), "utf8")
59
+ expect(body).not.toContain(" mysql:")
60
+ })
61
+
62
+ test("omits the uid-remap entrypoint when no host user is given (macOS/Windows)", () => {
63
+ const body = fs.readFileSync(writeDevOverride(dir, { wordpressDir, mounts: [] }), "utf8")
64
+ expect(body).not.toContain("entrypoint:")
65
+ expect(body).not.toContain("KIZLO_PUID")
66
+ expect(body).not.toContain("user:")
67
+ })
68
+
69
+ test("runs the container as the host user and injects the remap entrypoint (Linux)", () => {
70
+ const body = fs.readFileSync(writeDevOverride(dir, { wordpressDir, mounts: [], hostUser: { uid: 1000, gid: 1000 } }), "utf8")
71
+ expect(body).toContain('entrypoint: ["sh","/usr/local/bin/kizlo-remap-entrypoint.sh"]')
72
+ expect(body).toContain(`- "${REMAP_ENTRYPOINT}:/usr/local/bin/kizlo-remap-entrypoint.sh:ro"`)
73
+ expect(body).toContain('KIZLO_PUID: "1000"')
74
+ expect(body).toContain('KIZLO_PGID: "1000"')
75
+ // wp-cli runs directly as the host user (no entrypoint wrapper needed there).
76
+ expect(body).toContain('user: "1000:1000"')
77
+ })
78
+ })
79
+
80
+ describe("writeTestOverride", () => {
81
+ let dir: string
82
+
83
+ beforeEach(() => {
84
+ dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "kizlo-tovr-")))
85
+ })
86
+
87
+ afterEach(() => {
88
+ fs.rmSync(dir, { recursive: true, force: true })
89
+ })
90
+
91
+ test("mounts local plugin dirs by basename on both services", () => {
92
+ const file = writeTestOverride(dir, ["plugins/kizlo-woocommerce"])
93
+ expect(file).toBe(path.join(dir, ".kizlo", "docker-compose.test.override.yml"))
94
+
95
+ const body = fs.readFileSync(file, "utf8")
96
+ const pluginBind = `- "${path.join(dir, "plugins/kizlo-woocommerce")}:/var/www/html/wp-content/plugins/kizlo-woocommerce"`
97
+ expect(body).toContain(" wordpress:")
98
+ expect(body).toContain(" wp-cli:")
99
+ // One bind per service.
100
+ expect(body.split(pluginBind).length - 1).toBe(2)
101
+ })
102
+
103
+ test("keeps absolute mount paths verbatim", () => {
104
+ const abs = path.join(dir, "external", "my-plugin")
105
+ const body = fs.readFileSync(writeTestOverride(dir, [abs]), "utf8")
106
+ expect(body).toContain(`- "${abs}:/var/www/html/wp-content/plugins/my-plugin"`)
107
+ })
108
+
109
+ test("touches nothing but plugin mounts — no root bind, OPcache, or db port", () => {
110
+ const body = fs.readFileSync(writeTestOverride(dir, ["plugins/kizlo"]), "utf8")
111
+ expect(body).not.toContain(':/var/www/html"')
112
+ expect(body).not.toContain("opcache")
113
+ expect(body).not.toContain(" mysql:")
114
+ })
115
+ })
@@ -0,0 +1,134 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs"
2
+ import { basename, dirname, isAbsolute, resolve } from "node:path"
3
+ import { OPCACHE_INI, REMAP_ENTRYPOINT } from "../constants"
4
+
5
+ /** Generated dev override location, relative to the config root. */
6
+ const OVERRIDE_REL = ".kizlo/docker-compose.dev.override.yml"
7
+ /** Generated test override location (plugin mounts only), relative to the config root. */
8
+ const TEST_OVERRIDE_REL = ".kizlo/docker-compose.test.override.yml"
9
+
10
+ const WP_ROOT = "/var/www/html"
11
+ const PLUGINS_DIR = `${WP_ROOT}/wp-content/plugins`
12
+ /** Where the dev OPcache config is dropped so the container's PHP picks it up. */
13
+ const PHP_CONF_TARGET = "/usr/local/etc/php/conf.d/zz-kizlo-opcache.ini"
14
+ /** Where the Linux uid-remap entrypoint is mounted (run via `sh`, so no +x needed). */
15
+ const REMAP_TARGET = "/usr/local/bin/kizlo-remap-entrypoint.sh"
16
+
17
+ export interface DevOverrideOptions {
18
+ /** Absolute host directory the whole WordPress install is bind-mounted to. */
19
+ wordpressDir: string
20
+ /** Local plugin directories to bind-mount into `wp-content/plugins`, by basename. */
21
+ mounts: string[]
22
+ /**
23
+ * Host user to run the container as (Linux only). When set, www-data is retagged
24
+ * to this uid/gid so files WordPress writes are owned by the host user. Omitted on
25
+ * macOS/Windows, where Docker already maps ownership.
26
+ */
27
+ hostUser?: { uid: number; gid: number }
28
+ /**
29
+ * Host port to publish the MySQL service on, bound to `127.0.0.1`, so a SQL client
30
+ * can reach the dev database directly. Omitted leaves MySQL internal-only.
31
+ */
32
+ dbPort?: number
33
+ }
34
+
35
+ interface ServiceBlock {
36
+ user?: string
37
+ entrypoint?: string[]
38
+ env?: Record<string, string>
39
+ ports?: string[]
40
+ volumes?: string[]
41
+ }
42
+
43
+ function renderService(name: string, block: ServiceBlock): string {
44
+ const lines = [` ${name}:`]
45
+ if (block.user) lines.push(` user: "${block.user}"`)
46
+ if (block.entrypoint) lines.push(` entrypoint: ${JSON.stringify(block.entrypoint)}`)
47
+ if (block.env) {
48
+ lines.push(" environment:")
49
+ for (const [key, value] of Object.entries(block.env)) lines.push(` ${key}: "${value}"`)
50
+ }
51
+ if (block.ports?.length) lines.push(" ports:", ...block.ports.map((p) => ` - "${p}"`))
52
+ if (block.volumes?.length) lines.push(" volumes:", ...block.volumes)
53
+ return lines.join("\n")
54
+ }
55
+
56
+ /**
57
+ * A compose volume line (`- "host:container"`). `:ro` marks it read-only — the
58
+ * container can read it but never write back to the host file.
59
+ */
60
+ function bind(host: string, container: string, readOnly = false): string {
61
+ return ` - "${host}:${container}${readOnly ? ":ro" : ""}"`
62
+ }
63
+
64
+ /** Resolve each local-plugin dir to a volume bind under `wp-content/plugins`, layered by basename. */
65
+ function pluginBinds(configDir: string, mounts: string[]): string[] {
66
+ return mounts.map((entry) => bind(isAbsolute(entry) ? entry : resolve(configDir, entry), `${PLUGINS_DIR}/${basename(entry)}`))
67
+ }
68
+
69
+ /**
70
+ * Generate the dev compose override. The whole install is bind-mounted from the
71
+ * host `wordpressDir`, so every file is live and editable; local plugin dirs layer
72
+ * on top by basename; and the dev OPcache config is dropped into PHP (a speed win
73
+ * with live-edit-safe settings). Binds apply to both `wordpress` (serves the site)
74
+ * and `wp-cli` (activates plugins). On Linux (`hostUser` set) the container also
75
+ * runs as the host user so generated files are owned by you, not uid 33. Returns
76
+ * the override path to append to the dev stack's compose files.
77
+ */
78
+ export function writeDevOverride(configDir: string, opts: DevOverrideOptions): string {
79
+ const path = resolve(configDir, OVERRIDE_REL)
80
+ mkdirSync(dirname(path), { recursive: true })
81
+ mkdirSync(opts.wordpressDir, { recursive: true })
82
+
83
+ const mounts = pluginBinds(configDir, opts.mounts)
84
+ const rootBind = bind(opts.wordpressDir, WP_ROOT)
85
+ const remap = opts.hostUser
86
+ ? {
87
+ // Run the wrapper as root so it can retag www-data, then it hands off to
88
+ // the normal entrypoint; wp-cli just runs directly as the host user.
89
+ wordpress: {
90
+ user: "0:0",
91
+ entrypoint: ["sh", REMAP_TARGET],
92
+ env: { KIZLO_PUID: String(opts.hostUser.uid), KIZLO_PGID: String(opts.hostUser.gid) },
93
+ scriptBind: bind(REMAP_ENTRYPOINT, REMAP_TARGET, true),
94
+ },
95
+ wpCliUser: `${opts.hostUser.uid}:${opts.hostUser.gid}`,
96
+ }
97
+ : undefined
98
+
99
+ const wordpress: ServiceBlock = {
100
+ user: remap?.wordpress.user,
101
+ entrypoint: remap?.wordpress.entrypoint,
102
+ env: remap?.wordpress.env,
103
+ volumes: [...(remap ? [remap.wordpress.scriptBind] : []), rootBind, ...mounts, bind(OPCACHE_INI, PHP_CONF_TARGET, true)],
104
+ }
105
+ const wpCli: ServiceBlock = { user: remap?.wpCliUser, volumes: [rootBind, ...mounts] }
106
+
107
+ // Publish MySQL on a host port (loopback only) so advanced users can point a SQL
108
+ // client at the dev database. Internal-only when no port is configured.
109
+ const services = [renderService("wordpress", wordpress), renderService("wp-cli", wpCli)]
110
+ if (opts.dbPort) services.push(renderService("mysql", { ports: [`127.0.0.1:${opts.dbPort}:3306`] }))
111
+
112
+ writeFileSync(path, `services:\n${services.join("\n")}\n`)
113
+ return path
114
+ }
115
+
116
+ /**
117
+ * Generate the test compose override: bind-mount the fixtures' local plugin dirs over
118
+ * `wp-content/plugins` (by basename) on both `wordpress` and `wp-cli`, layered on top of
119
+ * the test stack's `wp_data` volume. Unlike the dev override it touches nothing else —
120
+ * the test stack keeps its volume-backed install, OPcache, and default entrypoints — so a
121
+ * fixture carrying a `{ path }` plugin runs against your live source with no build/zip
122
+ * step, exactly as in `kizlo dev`. Returns the override path to append to the test stack's
123
+ * compose files.
124
+ */
125
+ export function writeTestOverride(configDir: string, mounts: string[]): string {
126
+ const path = resolve(configDir, TEST_OVERRIDE_REL)
127
+ mkdirSync(dirname(path), { recursive: true })
128
+
129
+ const binds = pluginBinds(configDir, mounts)
130
+ const services = [renderService("wordpress", { volumes: binds }), renderService("wp-cli", { volumes: binds })]
131
+
132
+ writeFileSync(path, `services:\n${services.join("\n")}\n`)
133
+ return path
134
+ }
@@ -0,0 +1,17 @@
1
+ #!/bin/sh
2
+ # Kizlo dev — Linux file-ownership fix.
3
+ #
4
+ # On native Linux, files the container writes into the bind-mounted install would
5
+ # be owned by www-data (uid 33), not you, leaving "permission denied" when you edit
6
+ # them from your file manager. This wrapper runs first (as root), retags www-data to
7
+ # the host user's uid/gid, then hands off to the normal WordPress entrypoint — so
8
+ # everything WordPress creates is owned by you. Injected only on Linux; Mac/Windows
9
+ # never see this file.
10
+ set -e
11
+
12
+ if [ -n "$KIZLO_PUID" ] && [ "$KIZLO_PUID" != "0" ]; then
13
+ usermod -o -u "$KIZLO_PUID" www-data 2>/dev/null || true
14
+ groupmod -o -g "$KIZLO_PGID" www-data 2>/dev/null || true
15
+ fi
16
+
17
+ exec docker-entrypoint.sh "$@"
@@ -1,4 +1,4 @@
1
- import { githubRelease } from "./utils.js";
1
+ import { kizloRelease } from "./utils.js";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
@@ -7,6 +7,10 @@ const DEFAULT_PORT = 8080;
7
7
  const HERE = dirname(fileURLToPath(import.meta.url));
8
8
  /** The docker-compose file shipped alongside this module (in both `src/` and `dist/`). */
9
9
  const COMPOSE_FILE = resolve(HERE, "compose/docker-compose.yml");
10
+ /** Dev PHP OPcache config, shipped alongside this module and bind-mounted into the dev stack. */
11
+ const OPCACHE_INI = resolve(HERE, "compose/opcache.ini");
12
+ /** Linux-only entrypoint that retags www-data to the host user (see the script for why). */
13
+ const REMAP_ENTRYPOINT = resolve(HERE, "compose/remap-entrypoint.sh");
10
14
  const CONFIG_FILES = [
11
15
  "kizlo.config.ts",
12
16
  "kizlo.config.js",
@@ -14,11 +18,6 @@ const CONFIG_FILES = [
14
18
  ];
15
19
  /** Credentials artifact location, relative to the config root. */
16
20
  const CREDENTIALS_REL = ".kizlo/test-credentials.json";
17
- const COMPOSE = [
18
- "compose",
19
- "-f",
20
- COMPOSE_FILE
21
- ];
22
21
  /**
23
22
  * DB-side marker written as the final bootstrap step and checked by `isSeeded`.
24
23
  * It records that this database holds a completed bootstrap — independent of any
@@ -30,7 +29,7 @@ const SEED_VERSION = "1";
30
29
  /** The kizlo core WordPress plugin, always installed during bootstrap. */
31
30
  const DEFAULT_PLUGINS = [{
32
31
  name: "kizlo",
33
- source: githubRelease("kizlo-io/kizlo-wordpress", "kizlo-v1.0.0-beta.3")
32
+ source: kizloRelease("kizlo")
34
33
  }];
35
34
  const TEST_ADMIN = {
36
35
  firstName: "Admin",
@@ -49,4 +48,4 @@ const TEST_USER = {
49
48
  };
50
49
 
51
50
  //#endregion
52
- export { COMPOSE, CONFIG_FILES, CREDENTIALS_REL, DEFAULT_PLUGINS, DEFAULT_PORT, SEED_MARKER_OPTION, SEED_VERSION, TEST_ADMIN, TEST_USER };
51
+ export { COMPOSE_FILE, CONFIG_FILES, CREDENTIALS_REL, DEFAULT_PLUGINS, DEFAULT_PORT, OPCACHE_INI, REMAP_ENTRYPOINT, SEED_MARKER_OPTION, SEED_VERSION, TEST_ADMIN, TEST_USER };
@@ -0,0 +1,146 @@
1
+ import { WordPressService } from "../../wordpress/service.js";
2
+ import { compose, composeUp, wpCli } from "./docker.js";
3
+ import { DEFAULT_PLUGINS, TEST_ADMIN } from "./constants.js";
4
+ import { ensurePlugins } from "./utils.js";
5
+ import { createAdminAppPassword, seedUsers } from "./bootstrap.js";
6
+ import { prepareByo } from "./byo.js";
7
+ import { randomBytes } from "node:crypto";
8
+ import { existsSync, rmSync } from "node:fs";
9
+ import { join } from "node:path";
10
+
11
+ //#region src/cli/wp/dev.ts
12
+ /** A strong random admin password (144 bits, URL/JSON/CLI-safe characters). */
13
+ function generatePassword() {
14
+ return randomBytes(18).toString("base64url");
15
+ }
16
+ /**
17
+ * Seed `dev.fixtures` into a freshly installed dev stack, reusing the test seeding
18
+ * primitives: seed the default subscriber so `ctx.userId` exists, then run each `seed`
19
+ * over REST. Plugins are already active (`bootstrapDev` ensures them before seeding).
20
+ * The application password is minted only to drive seeding here — it's never printed
21
+ * or persisted. Returns the number of fixtures seeded.
22
+ */
23
+ async function seedDevFixtures(cfg, url) {
24
+ const userId = await seedUsers();
25
+ const adminId = Number(await wpCli([
26
+ "user",
27
+ "get",
28
+ TEST_ADMIN.username,
29
+ "--field=ID"
30
+ ]));
31
+ const password = await createAdminAppPassword("kizlo-dev-seed");
32
+ const ctx = {
33
+ service: new WordPressService({ credentials: {
34
+ url,
35
+ username: TEST_ADMIN.username,
36
+ password
37
+ } }),
38
+ adminId,
39
+ userId
40
+ };
41
+ let seeded = 0;
42
+ for (const fixture of cfg.fixtures) if (fixture.seed) {
43
+ await fixture.seed(ctx);
44
+ seeded++;
45
+ }
46
+ return seeded;
47
+ }
48
+ /**
49
+ * Boot the dev stack via docker + wp-cli. A fresh stack is provisioned one of two ways:
50
+ * a default `wp core install`, or — when `dev.byo` points at an archive — hydrated from
51
+ * that existing site (files + database). Either way it then sets permalinks and ensures
52
+ * the `dev.fixtures` plugins (installed sources + bind-mounted locals). An already-provisioned
53
+ * stack is left untouched (idempotent reruns).
54
+ *
55
+ * Credentials are an output, not a stored file: a default fresh install mints a random
56
+ * admin password and returns it once (to log into wp-admin); a BYO install uses the
57
+ * imported site's own users. No application password is minted — that's a test-stack
58
+ * concern.
59
+ */
60
+ async function bootstrapDev(cfg) {
61
+ const url = `http://localhost:${cfg.port}`;
62
+ const byo = !existsSync(join(cfg.wordpressDir, "wp-includes", "version.php")) && cfg.byo ? await prepareByo(cfg.byo, cfg.wordpressDir, cfg.configDir) : void 0;
63
+ await composeUp();
64
+ const installed = (await compose([
65
+ "exec",
66
+ "-T",
67
+ "wp-cli",
68
+ "wp",
69
+ "core",
70
+ "is-installed"
71
+ ])).code === 0;
72
+ let password;
73
+ let imported = false;
74
+ if (!installed && byo) {
75
+ imported = true;
76
+ if (byo.prefix !== "wp_") await wpCli([
77
+ "config",
78
+ "set",
79
+ "table_prefix",
80
+ byo.prefix,
81
+ "--type=variable"
82
+ ]);
83
+ const imp = await compose([
84
+ "exec",
85
+ "-T",
86
+ "mysql",
87
+ "mysql",
88
+ "-uwordpress",
89
+ "-pwppass",
90
+ "wordpress"
91
+ ], { inputFile: byo.sqlPath });
92
+ if (imp.code !== 0) throw new Error(`importing dev.byo database failed:\n${imp.stderr || imp.stdout}`);
93
+ rmSync(byo.sqlPath, { force: true });
94
+ const oldUrl = await wpCli([
95
+ "option",
96
+ "get",
97
+ "siteurl"
98
+ ]).catch(() => "");
99
+ if (oldUrl && oldUrl !== url) await wpCli([
100
+ "search-replace",
101
+ oldUrl,
102
+ url,
103
+ "--all-tables",
104
+ "--skip-columns=guid",
105
+ "--report-changed-only"
106
+ ]);
107
+ await wpCli([
108
+ "rewrite",
109
+ "flush",
110
+ "--hard"
111
+ ]);
112
+ } else if (!installed) {
113
+ password = generatePassword();
114
+ await wpCli([
115
+ "core",
116
+ "install",
117
+ `--url=${url}`,
118
+ "--title=Kizlo Dev",
119
+ `--admin_user=${TEST_ADMIN.username}`,
120
+ `--admin_password=${password}`,
121
+ `--admin_email=${TEST_ADMIN.email}`,
122
+ "--skip-email"
123
+ ]);
124
+ await wpCli([
125
+ "rewrite",
126
+ "structure",
127
+ "/%postname%/",
128
+ "--hard"
129
+ ]);
130
+ }
131
+ await ensurePlugins([...DEFAULT_PLUGINS, ...cfg.fixtures.flatMap((fixture) => fixture.plugins ?? [])]);
132
+ const seeded = !installed && !byo && cfg.fixtures.length ? await seedDevFixtures(cfg, url) : 0;
133
+ const appPassword = password ? await createAdminAppPassword("kizlo-dev") : void 0;
134
+ return {
135
+ url,
136
+ username: TEST_ADMIN.username,
137
+ dbPort: cfg.dbPort,
138
+ imported,
139
+ seeded,
140
+ appPassword,
141
+ secrets: password ? { password } : void 0
142
+ };
143
+ }
144
+
145
+ //#endregion
146
+ export { bootstrapDev };
@@ -1,24 +1,64 @@
1
1
  //#region src/cli/wp/docker.d.ts
2
+ /**
3
+ * Options for a spawned docker command.
4
+ * - `input`/`inputFile`: data piped to stdin (an in-memory string, or a file streamed from disk).
5
+ * - `detached`: run in its own process group so a terminal Ctrl+C (delivered to the whole
6
+ * foreground group) can't kill it mid-flight — used for teardown's `stop`, which must finish.
7
+ */
8
+ type RunInput = {
9
+ input?: string;
10
+ inputFile?: string;
11
+ detached?: boolean;
12
+ };
13
+ /** A docker-compose stack: project id (`-p`), published port, and compose files (`-f`). */
14
+ interface Stack {
15
+ /** Compose project name — isolates this stack's containers + volumes. */
16
+ project: string;
17
+ /** Host port published for WordPress (exported as `WP_PORT`). */
18
+ port: number;
19
+ /** Compose files, base first then any generated override. */
20
+ composeFiles: string[];
21
+ }
22
+ /** Stack-bound docker helpers. */
23
+ interface DockerStack {
24
+ compose(args: string[], opts?: RunInput): Promise<RunResult>;
25
+ composeUp(): Promise<void>;
26
+ composeStop(opts?: {
27
+ detached?: boolean;
28
+ }): Promise<void>;
29
+ composeDown(opts?: {
30
+ volumes?: boolean;
31
+ }): Promise<void>;
32
+ /** The host port this stack's WordPress is currently published on, or `undefined` if it isn't running. */
33
+ publishedPort(): Promise<number | undefined>;
34
+ wpCli(args: string[]): Promise<string>;
35
+ }
2
36
  interface RunResult {
3
37
  code: number;
4
38
  stdout: string;
5
39
  stderr: string;
6
40
  }
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
41
  /**
20
- * Run a wp-cli command against the warm `wp-cli` container and return trimmed stdout.
42
+ * Whether the Docker daemon is reachable. `docker version` contacts the server (not just
43
+ * the client), so a non-zero exit means the daemon is down or Docker isn't installed —
44
+ * the early check before a command that needs a stack (e.g. `init`'s local setup).
21
45
  */
22
- declare function wpCli(args: string[]): Promise<string>;
46
+ declare function dockerAvailable(): Promise<boolean>;
47
+ /** Build a stack-bound docker helper and mark it active for the bare helpers below. */
48
+ declare function createStack(stack: Stack): DockerStack;
49
+ /** `docker compose <args>` against the active stack. */
50
+ declare const compose: DockerStack["compose"];
51
+ /** Bring the active stack's services up detached and wait for health checks. */
52
+ declare const composeUp: () => Promise<void>;
53
+ /** Tear the active stack down, optionally removing volumes. */
54
+ declare const composeDown: (opts?: {
55
+ volumes?: boolean;
56
+ }) => Promise<void>;
57
+ /** `docker compose stop` — halt the active stack's containers but keep volumes. */
58
+ declare const composeStop: (opts?: {
59
+ detached?: boolean;
60
+ }) => Promise<void>;
61
+ /** Run a wp-cli command against the active stack's warm `wp-cli` container. */
62
+ declare const wpCli: (args: string[]) => Promise<string>;
23
63
  //#endregion
24
- export { compose, composeDown, composeStop, composeUp, wpCli };
64
+ export { DockerStack, RunResult, Stack, compose, composeDown, composeStop, composeUp, createStack, dockerAvailable, wpCli };