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.
- package/dist/adapters/auth.d.ts +11 -2
- package/dist/adapters/auth.js +5 -1
- package/dist/adapters/captcha.d.ts +3 -1
- package/dist/adapters/captcha.js +5 -1
- package/dist/adapters/cookies.d.ts +7 -0
- package/dist/adapters/cookies.js +8 -0
- package/dist/adapters/geo.d.ts +13 -2
- package/dist/adapters/geo.js +5 -1
- package/dist/adapters/logger.d.ts +8 -1
- package/dist/adapters/logger.js +5 -1
- package/dist/cli/banner.js +12 -1
- package/dist/cli/commands/dev.js +262 -0
- package/dist/cli/commands/init.js +235 -43
- package/dist/cli/commands/test.js +133 -34
- package/dist/cli/commands/watch.js +2 -2
- package/dist/cli/daemon/config.js +73 -7
- package/dist/cli/daemon/generate.js +1 -1
- package/dist/cli/daemon/lock.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/presets/base.js +8 -1
- package/dist/cli/presets/nextjs.js +1 -1
- package/dist/cli/utils.js +90 -6
- package/dist/cli/wp/bootstrap.js +13 -10
- package/dist/cli/wp/byo.js +113 -0
- package/dist/cli/wp/compose/docker-compose.yml +13 -2
- package/dist/cli/wp/compose/opcache.ini +16 -0
- package/dist/cli/wp/compose/override.js +104 -0
- package/dist/cli/wp/compose/override.test.ts +115 -0
- package/dist/cli/wp/compose/override.ts +134 -0
- package/dist/cli/wp/compose/remap-entrypoint.sh +17 -0
- package/dist/cli/wp/constants.js +7 -8
- package/dist/cli/wp/dev.js +146 -0
- package/dist/cli/wp/docker.d.ts +55 -15
- package/dist/cli/wp/docker.js +102 -41
- package/dist/cli/wp/ports.js +72 -0
- package/dist/cli/wp/session.js +150 -0
- package/dist/cli/wp/settings.js +73 -0
- package/dist/cli/wp/setup.js +1 -1
- package/dist/cli/wp/stack.js +58 -0
- package/dist/cli/wp/types.d.ts +26 -5
- package/dist/cli/wp/types.js +5 -1
- package/dist/cli/wp/utils.d.ts +29 -2
- package/dist/cli/wp/utils.js +61 -3
- package/dist/cli/wp/watchdog.d.ts +1 -0
- package/dist/cli/wp/watchdog.js +45 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +2 -2
- package/dist/config.d.ts +49 -3
- package/dist/context.d.ts +12 -1
- package/dist/context.js +1 -1
- package/dist/index.d.ts +16 -11
- package/dist/index.js +10 -6
- package/dist/integrations/nextjs/kizlo.server.d.ts +1 -1
- package/dist/integrations/nextjs/kizlo.server.js +2 -2
- package/dist/integrations/nextjs/utils.js +2 -2
- package/dist/kizlo.d.ts +27 -6
- package/dist/kizlo.js +24 -11
- package/dist/menu/fixture.d.ts +1 -1
- package/dist/package.js +1 -1
- package/dist/post/fixture.d.ts +1 -1
- package/dist/service.d.ts +2 -0
- package/dist/service.js +3 -0
- package/dist/settings/service.d.ts +22 -0
- package/dist/settings/service.interface.d.ts +134 -0
- package/dist/settings/service.js +43 -0
- package/dist/shared/error.d.ts +16 -5
- package/dist/shared/error.js +4 -0
- package/dist/shared/extension.d.ts +2 -0
- package/dist/shared/middleware.d.ts +13 -10
- package/dist/shared/middleware.js +7 -0
- package/dist/shared/procedure.d.ts +34 -28
- package/dist/shared/procedure.js +3 -6
- package/dist/shared/types.d.ts +19 -13
- package/dist/test/client.d.ts +5 -0
- package/dist/test/client.js +5 -0
- package/dist/test/server.d.ts +6 -0
- package/dist/test/server.js +6 -0
- package/dist/test.d.ts +4 -4
- package/dist/test.js +4 -4
- package/dist/webhook/index.d.ts +14 -13
- package/dist/webhook/index.js +5 -5
- package/dist/webhook/schema.d.ts +3 -3
- package/dist/webhook/schema.js +37 -37
- package/dist/webhook/utils.js +4 -4
- package/dist/wordpress/service.js +6 -0
- package/package.json +2 -2
- 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 "$@"
|
package/dist/cli/wp/constants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
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 {
|
|
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 };
|
package/dist/cli/wp/docker.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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 };
|