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
package/dist/cli/wp/docker.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { COMPOSE } from "./constants.js";
|
|
2
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
3
|
|
|
4
4
|
//#region src/cli/wp/docker.ts
|
|
5
|
-
function run(cmd, args, opts) {
|
|
5
|
+
function run(cmd, args, env, opts) {
|
|
6
6
|
return new Promise((resolvePromise, reject) => {
|
|
7
|
-
const child = spawn(cmd, args, {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
const child = spawn(cmd, args, {
|
|
8
|
+
stdio: [
|
|
9
|
+
"pipe",
|
|
10
|
+
"pipe",
|
|
11
|
+
"pipe"
|
|
12
|
+
],
|
|
13
|
+
env,
|
|
14
|
+
detached: opts?.detached
|
|
15
|
+
});
|
|
12
16
|
let stdout = "";
|
|
13
17
|
let stderr = "";
|
|
14
18
|
child.stdout.on("data", (d) => {
|
|
@@ -23,47 +27,104 @@ function run(cmd, args, opts) {
|
|
|
23
27
|
stdout,
|
|
24
28
|
stderr
|
|
25
29
|
}));
|
|
26
|
-
if (opts?.
|
|
30
|
+
if (opts?.inputFile !== void 0) createReadStream(opts.inputFile).on("error", reject).pipe(child.stdin);
|
|
31
|
+
else if (opts?.input !== void 0) child.stdin.end(opts.input);
|
|
27
32
|
else child.stdin.end();
|
|
28
33
|
});
|
|
29
34
|
}
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
async function
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
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);
|
|
35
|
+
/**
|
|
36
|
+
* Whether the Docker daemon is reachable. `docker version` contacts the server (not just
|
|
37
|
+
* the client), so a non-zero exit means the daemon is down or Docker isn't installed —
|
|
38
|
+
* the early check before a command that needs a stack (e.g. `init`'s local setup).
|
|
39
|
+
*/
|
|
40
|
+
async function dockerAvailable() {
|
|
41
|
+
try {
|
|
42
|
+
return (await run("docker", ["version"], process.env)).code === 0;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
48
46
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
function bind(stack) {
|
|
48
|
+
const base = [
|
|
49
|
+
"compose",
|
|
50
|
+
"-p",
|
|
51
|
+
stack.project,
|
|
52
|
+
...stack.composeFiles.flatMap((file) => ["-f", file])
|
|
53
|
+
];
|
|
54
|
+
const env = {
|
|
55
|
+
...process.env,
|
|
56
|
+
WP_PORT: String(stack.port)
|
|
57
|
+
};
|
|
58
|
+
const compose$1 = (args, opts) => run("docker", [...base, ...args], env, opts);
|
|
59
|
+
const composeUp$1 = async () => {
|
|
60
|
+
const res = await compose$1([
|
|
61
|
+
"up",
|
|
62
|
+
"-d",
|
|
63
|
+
"--wait"
|
|
64
|
+
]);
|
|
65
|
+
if (res.code !== 0) throw new Error(`docker compose up failed:\n${res.stderr}`);
|
|
66
|
+
};
|
|
67
|
+
const composeDown$1 = async (opts) => {
|
|
68
|
+
const args = ["down"];
|
|
69
|
+
if (opts?.volumes) args.push("-v");
|
|
70
|
+
await compose$1(args);
|
|
71
|
+
};
|
|
72
|
+
const composeStop$1 = async (opts) => {
|
|
73
|
+
await compose$1(["stop"], opts);
|
|
74
|
+
};
|
|
75
|
+
const publishedPort = async () => {
|
|
76
|
+
const match = (await compose$1([
|
|
77
|
+
"port",
|
|
78
|
+
"wordpress",
|
|
79
|
+
"80"
|
|
80
|
+
])).stdout.match(/:(\d+)\s*$/);
|
|
81
|
+
return match ? Number(match[1]) : void 0;
|
|
82
|
+
};
|
|
83
|
+
const wpCli$1 = async (args) => {
|
|
84
|
+
const res = await compose$1([
|
|
85
|
+
"exec",
|
|
86
|
+
"-T",
|
|
87
|
+
"wp-cli",
|
|
88
|
+
"wp",
|
|
89
|
+
...args
|
|
90
|
+
]);
|
|
91
|
+
if (res.code !== 0) throw new Error(`wp ${args.join(" ")} failed:\n${res.stderr || res.stdout}`);
|
|
92
|
+
return res.stdout.replace(/\r/g, "").trim();
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
compose: compose$1,
|
|
96
|
+
composeUp: composeUp$1,
|
|
97
|
+
composeStop: composeStop$1,
|
|
98
|
+
composeDown: composeDown$1,
|
|
99
|
+
publishedPort,
|
|
100
|
+
wpCli: wpCli$1
|
|
101
|
+
};
|
|
52
102
|
}
|
|
53
103
|
/**
|
|
54
|
-
*
|
|
104
|
+
* The stack the module-level helpers target. `createStack` sets it so fixtures —
|
|
105
|
+
* which call the public `wpCli`/`compose` with no stack — hit whatever stack the
|
|
106
|
+
* running command activated (the test stack during seeding).
|
|
55
107
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return res.stdout.replace(/\r/g, "").trim();
|
|
108
|
+
let active = null;
|
|
109
|
+
/** Build a stack-bound docker helper and mark it active for the bare helpers below. */
|
|
110
|
+
function createStack(stack) {
|
|
111
|
+
active = stack;
|
|
112
|
+
return bind(stack);
|
|
113
|
+
}
|
|
114
|
+
function activeStack() {
|
|
115
|
+
if (!active) throw new Error("No active kizlo stack — createStack() must run first.");
|
|
116
|
+
return bind(active);
|
|
66
117
|
}
|
|
118
|
+
/** `docker compose <args>` against the active stack. */
|
|
119
|
+
const compose = (args, opts) => activeStack().compose(args, opts);
|
|
120
|
+
/** Bring the active stack's services up detached and wait for health checks. */
|
|
121
|
+
const composeUp = () => activeStack().composeUp();
|
|
122
|
+
/** Tear the active stack down, optionally removing volumes. */
|
|
123
|
+
const composeDown = (opts) => activeStack().composeDown(opts);
|
|
124
|
+
/** `docker compose stop` — halt the active stack's containers but keep volumes. */
|
|
125
|
+
const composeStop = (opts) => activeStack().composeStop(opts);
|
|
126
|
+
/** Run a wp-cli command against the active stack's warm `wp-cli` container. */
|
|
127
|
+
const wpCli = (args) => activeStack().wpCli(args);
|
|
67
128
|
|
|
68
129
|
//#endregion
|
|
69
|
-
export { compose, composeDown, composeStop, composeUp, wpCli };
|
|
130
|
+
export { compose, composeDown, composeStop, composeUp, createStack, dockerAvailable, wpCli };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createServer } from "node:net";
|
|
2
|
+
|
|
3
|
+
//#region src/cli/wp/ports.ts
|
|
4
|
+
/** Thrown when an explicitly-configured port is already taken — a collision the user owns. */
|
|
5
|
+
var PortInUseError = class extends Error {
|
|
6
|
+
port;
|
|
7
|
+
host;
|
|
8
|
+
constructor(port, host) {
|
|
9
|
+
super(`Port ${host}:${port} is already in use`);
|
|
10
|
+
this.name = "PortInUseError";
|
|
11
|
+
this.port = port;
|
|
12
|
+
this.host = host;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/** True if `host:port` can be bound right now (i.e. nothing else is listening on it). */
|
|
16
|
+
function isFree(port, host = "0.0.0.0") {
|
|
17
|
+
return new Promise((resolvePromise) => {
|
|
18
|
+
const server = createServer();
|
|
19
|
+
server.once("error", () => resolvePromise(false));
|
|
20
|
+
server.listen({
|
|
21
|
+
port,
|
|
22
|
+
host
|
|
23
|
+
}, () => server.close(() => resolvePromise(true)));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** An OS-assigned ephemeral port on `host` — the fallback when the whole scan range is taken. */
|
|
27
|
+
function ephemeralPort(host) {
|
|
28
|
+
return new Promise((resolvePromise, reject) => {
|
|
29
|
+
const server = createServer();
|
|
30
|
+
server.once("error", reject);
|
|
31
|
+
server.listen({
|
|
32
|
+
port: 0,
|
|
33
|
+
host
|
|
34
|
+
}, () => {
|
|
35
|
+
const address = server.address();
|
|
36
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
37
|
+
server.close(() => resolvePromise(port));
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve `preferred` to a host port that's actually free, scanning upward from it.
|
|
43
|
+
* The configured/default value is only a *starting point*: if it (or the next few) are
|
|
44
|
+
* already taken — by a stray container, another stack, or any unrelated server — we step
|
|
45
|
+
* to the next free port instead of letting `docker compose up` fail with a port clash.
|
|
46
|
+
* Falls back to an OS-assigned ephemeral port if the whole scan range is busy.
|
|
47
|
+
*
|
|
48
|
+
* `host` must match how the port is published: `0.0.0.0` for the all-interfaces WP port,
|
|
49
|
+
* `127.0.0.1` for the loopback-only MySQL port — a port can be free on one and not the other.
|
|
50
|
+
*/
|
|
51
|
+
async function findFreePort(preferred, host = "0.0.0.0") {
|
|
52
|
+
for (let port = preferred; port < preferred + 100; port++) if (await isFree(port, host)) return port;
|
|
53
|
+
return ephemeralPort(host);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a host port, branching on whether the value was *chosen by the user*:
|
|
57
|
+
*
|
|
58
|
+
* - A **default** port (`fixed: false`) is only a starting point — if it's taken (by another
|
|
59
|
+
* project's stack or any unrelated server) we step to the next free one via {@link findFreePort},
|
|
60
|
+
* so the stack comes up instead of failing on a clash.
|
|
61
|
+
* - An **explicit** port (`fixed: true`, set in `kizlo.config`) is the user taking control: a
|
|
62
|
+
* collision there is theirs to resolve, so we throw {@link PortInUseError} rather than silently
|
|
63
|
+
* serving somewhere they didn't ask for.
|
|
64
|
+
*/
|
|
65
|
+
async function resolveHostPort(preferred, { fixed, host = "0.0.0.0" }) {
|
|
66
|
+
if (!fixed) return findFreePort(preferred, host);
|
|
67
|
+
if (await isFree(preferred, host)) return preferred;
|
|
68
|
+
throw new PortInUseError(preferred, host);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
export { PortInUseError, isFree, resolveHostPort };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
|
|
8
|
+
//#region src/cli/wp/session.ts
|
|
9
|
+
const exec = promisify(execFile);
|
|
10
|
+
/**
|
|
11
|
+
* Where foreground `kizlo dev` sessions are recorded, keyed by compose project.
|
|
12
|
+
* Global (not per-repo) on purpose: the whole point is to reap a *different*
|
|
13
|
+
* project's stack that was orphaned by a crashed session you never came back to.
|
|
14
|
+
*/
|
|
15
|
+
function registryPath() {
|
|
16
|
+
return join(homedir(), ".cache", "kizlo", "dev-sessions.json");
|
|
17
|
+
}
|
|
18
|
+
function readRegistry() {
|
|
19
|
+
const file = registryPath();
|
|
20
|
+
if (!existsSync(file)) return {};
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
23
|
+
} catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function writeRegistry(registry) {
|
|
28
|
+
const file = registryPath();
|
|
29
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
30
|
+
writeFileSync(file, JSON.stringify(registry, null, 2));
|
|
31
|
+
}
|
|
32
|
+
/** True when a process with this PID is still alive (signal 0 probes without killing). */
|
|
33
|
+
function pidAlive(pid) {
|
|
34
|
+
try {
|
|
35
|
+
process.kill(pid, 0);
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Stop a compose project's containers by label, without needing its compose files.
|
|
43
|
+
* Used for orphans owned by a dead PID — we can't assume we're in that project's
|
|
44
|
+
* directory, so we target the running containers directly rather than via compose.
|
|
45
|
+
*/
|
|
46
|
+
async function stopByProject(project) {
|
|
47
|
+
const { stdout } = await exec("docker", [
|
|
48
|
+
"ps",
|
|
49
|
+
"-q",
|
|
50
|
+
"--filter",
|
|
51
|
+
`label=com.docker.compose.project=${project}`
|
|
52
|
+
]);
|
|
53
|
+
const ids = stdout.split("\n").filter(Boolean);
|
|
54
|
+
if (ids.length) await exec("docker", ["stop", ...ids]);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Stop and remove *every* container for `project` — running or stopped — keeping its
|
|
58
|
+
* volumes (the DB and bind-mounted install survive). Run right before bringing a dev
|
|
59
|
+
* stack up so `compose up` creates brand-new containers instead of reconciling a stale
|
|
60
|
+
* one left by a crashed session or a second invocation: reconciling an existing — often
|
|
61
|
+
* unhealthy — mysql in place is what intermittently fails the dependency wait with
|
|
62
|
+
* "exited (137)". Returns true if it removed anything. Best effort: a docker-down error
|
|
63
|
+
* surfaces later on `up`, where the message is actionable.
|
|
64
|
+
*/
|
|
65
|
+
async function removeProjectContainers(project) {
|
|
66
|
+
try {
|
|
67
|
+
const { stdout } = await exec("docker", [
|
|
68
|
+
"ps",
|
|
69
|
+
"-aq",
|
|
70
|
+
"--filter",
|
|
71
|
+
`label=com.docker.compose.project=${project}`
|
|
72
|
+
]);
|
|
73
|
+
const ids = stdout.split("\n").filter(Boolean);
|
|
74
|
+
if (!ids.length) return false;
|
|
75
|
+
await exec("docker", ["stop", ...ids]).catch(() => {});
|
|
76
|
+
await exec("docker", [
|
|
77
|
+
"rm",
|
|
78
|
+
"-f",
|
|
79
|
+
...ids
|
|
80
|
+
]);
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Spawn the detached watchdog that stops `project`'s stack when this process dies by
|
|
88
|
+
* any means the in-process handlers can't catch (terminal closed, SIGKILL, crash).
|
|
89
|
+
* Returns its PID so the graceful teardown path can kill it (the stack is already
|
|
90
|
+
* being stopped, so the watchdog would have nothing to do). Best effort: a failure
|
|
91
|
+
* here just means we fall back to signal handlers + the startup reaper.
|
|
92
|
+
*/
|
|
93
|
+
function spawnWatchdog(project) {
|
|
94
|
+
try {
|
|
95
|
+
const watchdog = fileURLToPath(new URL("./watchdog.js", import.meta.url));
|
|
96
|
+
const child = spawn(process.execPath, [
|
|
97
|
+
watchdog,
|
|
98
|
+
String(process.pid),
|
|
99
|
+
project
|
|
100
|
+
], {
|
|
101
|
+
detached: true,
|
|
102
|
+
stdio: "ignore"
|
|
103
|
+
});
|
|
104
|
+
child.unref();
|
|
105
|
+
return child.pid;
|
|
106
|
+
} catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Record this process as the owner of `project`'s foreground stack. */
|
|
111
|
+
function registerSession(project) {
|
|
112
|
+
const registry = readRegistry();
|
|
113
|
+
registry[project] = { pid: process.pid };
|
|
114
|
+
writeRegistry(registry);
|
|
115
|
+
}
|
|
116
|
+
/** Drop `project` from the registry (graceful teardown). Best effort. */
|
|
117
|
+
function unregisterSession(project) {
|
|
118
|
+
try {
|
|
119
|
+
const registry = readRegistry();
|
|
120
|
+
if (registry[project]) {
|
|
121
|
+
delete registry[project];
|
|
122
|
+
writeRegistry(registry);
|
|
123
|
+
}
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Stop any foreground stacks whose owning session has died (SIGKILL, crash, power
|
|
128
|
+
* loss) — the cases a signal handler can never catch. Runs at the start of every
|
|
129
|
+
* `kizlo dev`, so leaked stacks from *any* project self-heal the next time you
|
|
130
|
+
* bring one up. Returns the projects that were reaped, for logging.
|
|
131
|
+
*/
|
|
132
|
+
async function reapOrphans() {
|
|
133
|
+
const registry = readRegistry();
|
|
134
|
+
const reaped = [];
|
|
135
|
+
let changed = false;
|
|
136
|
+
for (const [project, session] of Object.entries(registry)) {
|
|
137
|
+
if (pidAlive(session.pid)) continue;
|
|
138
|
+
try {
|
|
139
|
+
await stopByProject(project);
|
|
140
|
+
reaped.push(project);
|
|
141
|
+
} catch {}
|
|
142
|
+
delete registry[project];
|
|
143
|
+
changed = true;
|
|
144
|
+
}
|
|
145
|
+
if (changed) writeRegistry(registry);
|
|
146
|
+
return reaped;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
//#endregion
|
|
150
|
+
export { reapOrphans, registerSession, removeProjectContainers, spawnWatchdog, unregisterSession };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { WP_KIZLO_BASE } from "../../wordpress/constants.js";
|
|
2
|
+
import { WordPressService } from "../../wordpress/service.js";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/wp/settings.ts
|
|
5
|
+
/** Loopback hosts that, from inside the dev Docker stack, must be rewritten to reach the host. */
|
|
6
|
+
const LOOPBACK_HOSTS = new Set([
|
|
7
|
+
"localhost",
|
|
8
|
+
"127.0.0.1",
|
|
9
|
+
"0.0.0.0",
|
|
10
|
+
"::1"
|
|
11
|
+
]);
|
|
12
|
+
/** The origin of a URL (scheme + host + port), or undefined when it can't be parsed. */
|
|
13
|
+
function originOf(url) {
|
|
14
|
+
try {
|
|
15
|
+
return new URL(url).origin;
|
|
16
|
+
} catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Rewrite a loopback host to `host.docker.internal` so the dockerized WordPress dev stack can reach a
|
|
22
|
+
* Kizlo server on the host (the container's own `localhost` is not the host). Non-loopback hosts (real
|
|
23
|
+
* deployments) and unparseable URLs are returned unchanged.
|
|
24
|
+
*/
|
|
25
|
+
function toContainerHostUrl(url) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(url);
|
|
28
|
+
if (!LOOPBACK_HOSTS.has(parsed.hostname)) return url;
|
|
29
|
+
parsed.hostname = "host.docker.internal";
|
|
30
|
+
return parsed.toString();
|
|
31
|
+
} catch {
|
|
32
|
+
return url;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Push site-level settings into the WordPress plugin's `kizlo_settings_site` option: the shared
|
|
37
|
+
* `secret` (so webhook signing/verification share a key) plus the `url`/`backend_url` the plugin
|
|
38
|
+
* needs to reach the Kizlo server when delivering events. `backend_url` is the backend URL verbatim;
|
|
39
|
+
* `url` is an explicit `siteUrl` when given (split deployments), otherwise the backend's origin (the
|
|
40
|
+
* single-origin default used by dev + local provision). `PUT /kizlo/v1/settings/site` merges, so
|
|
41
|
+
* sending only the keys we have is a safe partial update that leaves other site settings intact;
|
|
42
|
+
* empty fields are omitted. Best-effort: the plugin may not be active yet (remote `init`), so
|
|
43
|
+
* failures are returned, never thrown.
|
|
44
|
+
*/
|
|
45
|
+
async function syncSiteSettings(credentials, settings) {
|
|
46
|
+
const body = {};
|
|
47
|
+
if (settings.secret) body.secret = settings.secret;
|
|
48
|
+
const url = settings.siteUrl ?? (settings.backendUrl ? originOf(settings.backendUrl) : void 0);
|
|
49
|
+
if (settings.backendUrl) body.backend_url = settings.containerized ? toContainerHostUrl(settings.backendUrl) : settings.backendUrl;
|
|
50
|
+
if (url) body.url = url;
|
|
51
|
+
const response = await new WordPressService({ credentials }).put("settings/site", {
|
|
52
|
+
base: WP_KIZLO_BASE,
|
|
53
|
+
body
|
|
54
|
+
});
|
|
55
|
+
if (response.error) return {
|
|
56
|
+
ok: false,
|
|
57
|
+
error: describeError(response.status, response.error)
|
|
58
|
+
};
|
|
59
|
+
return { ok: true };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A short, log-friendly summary of a failed request. A transport failure has no HTTP status (`0`), so
|
|
63
|
+
* its message (e.g. `fetch failed`) is the only signal; otherwise the status code is shown — the raw
|
|
64
|
+
* `error.message` can be a full HTML page (when the URL serves the frontend instead of WordPress) and
|
|
65
|
+
* would flood the warning.
|
|
66
|
+
*/
|
|
67
|
+
function describeError(status, error) {
|
|
68
|
+
if (status === 0) return error.message;
|
|
69
|
+
return `HTTP ${status}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
export { syncSiteSettings };
|
package/dist/cli/wp/setup.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { WordPressService } from "../../wordpress/service.js";
|
|
2
2
|
import { credentialsPath } from "./utils.js";
|
|
3
3
|
import { bootstrapWp } from "./bootstrap.js";
|
|
4
|
-
import { dirname } from "node:path";
|
|
5
4
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
6
|
|
|
7
7
|
//#region src/cli/wp/setup.ts
|
|
8
8
|
/**
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isLocalPlugin } from "./types.js";
|
|
2
|
+
import { COMPOSE_FILE } from "./constants.js";
|
|
3
|
+
import { writeDevOverride, writeTestOverride } from "./compose/override.js";
|
|
4
|
+
|
|
5
|
+
//#region src/cli/wp/stack.ts
|
|
6
|
+
/** The local-plugin dirs to bind-mount, gathered from every fixture's `plugins`. */
|
|
7
|
+
function localMounts(fixtures) {
|
|
8
|
+
return fixtures.flatMap((fixture) => fixture.plugins ?? []).filter(isLocalPlugin).map((plugin) => plugin.path);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The test stack: the base compose file under a `<name>-test` project, plus a generated
|
|
12
|
+
* mount override when any fixture carries a local (`{ path }`) plugin — so the same
|
|
13
|
+
* fixtures bind-mount their live plugin source here exactly as in the dev stack.
|
|
14
|
+
*/
|
|
15
|
+
function testStack(cfg) {
|
|
16
|
+
const mounts = localMounts(cfg.fixtures);
|
|
17
|
+
const composeFiles = mounts.length ? [COMPOSE_FILE, writeTestOverride(cfg.configDir, mounts)] : [COMPOSE_FILE];
|
|
18
|
+
return {
|
|
19
|
+
project: cfg.project,
|
|
20
|
+
port: cfg.port,
|
|
21
|
+
composeFiles
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The host user to run the dev container as, so bind-mounted files come out owned
|
|
26
|
+
* by you. Only on native Linux, where Docker doesn't remap ownership; `undefined`
|
|
27
|
+
* (a no-op) on macOS/Windows, and when running as root (no benefit).
|
|
28
|
+
*/
|
|
29
|
+
function detectHostUser() {
|
|
30
|
+
if (process.platform !== "linux" || typeof process.getuid !== "function") return void 0;
|
|
31
|
+
const uid = process.getuid();
|
|
32
|
+
if (uid === 0) return void 0;
|
|
33
|
+
return {
|
|
34
|
+
uid,
|
|
35
|
+
gid: typeof process.getgid === "function" ? process.getgid() : uid
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The dev stack: the base compose file plus the generated mount override, under a
|
|
40
|
+
* `<name>-dev` project. Building the stack (re)writes the override so it always
|
|
41
|
+
* matches the current `dev.path` and the `dev.fixtures` local plugins.
|
|
42
|
+
*/
|
|
43
|
+
function devStack(cfg) {
|
|
44
|
+
const override = writeDevOverride(cfg.configDir, {
|
|
45
|
+
wordpressDir: cfg.wordpressDir,
|
|
46
|
+
mounts: localMounts(cfg.fixtures),
|
|
47
|
+
hostUser: detectHostUser(),
|
|
48
|
+
dbPort: cfg.dbPort
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
project: cfg.project,
|
|
52
|
+
port: cfg.port,
|
|
53
|
+
composeFiles: [COMPOSE_FILE, override]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
export { devStack, testStack };
|
package/dist/cli/wp/types.d.ts
CHANGED
|
@@ -55,18 +55,39 @@ type PluginSource = string | {
|
|
|
55
55
|
name: string;
|
|
56
56
|
source: string;
|
|
57
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* A local plugin directory to bind-mount into the dev stack (`kizlo dev`) instead
|
|
60
|
+
* of installing it. The container gets the live files at
|
|
61
|
+
* `wp-content/plugins/<basename(path)>`, so edits show up without a reinstall, and
|
|
62
|
+
* the plugin is activated by that basename. `path` is relative to the config dir.
|
|
63
|
+
*/
|
|
64
|
+
type LocalPlugin = {
|
|
65
|
+
path: string;
|
|
66
|
+
};
|
|
67
|
+
/** A `kizlo dev` plugin entry: an installable {@link PluginSource} or a {@link LocalPlugin} to mount. */
|
|
68
|
+
type DevPluginSource = PluginSource | LocalPlugin;
|
|
69
|
+
/** True when a dev plugin entry is a local directory to bind-mount, not something to install. */
|
|
70
|
+
declare function isLocalPlugin(plugin: DevPluginSource): plugin is LocalPlugin;
|
|
71
|
+
type FixtureSeedFn = (ctx: SeedContext) => Promise<Record<string, JsonValue>>;
|
|
72
|
+
type FixtureCleanupFn = (ctx: SeedContext) => Promise<void>;
|
|
58
73
|
/** The serializable test layer an extension ships alongside its live extension. */
|
|
59
74
|
interface Fixture {
|
|
60
75
|
/** Namespace key under `TestCredentials.fixtures`. */
|
|
61
76
|
name: string;
|
|
62
|
-
/**
|
|
63
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Plugins this fixture needs active, in order (deps first). Each entry is either
|
|
79
|
+
* an installable {@link PluginSource} (wp.org slug / zip source) or a
|
|
80
|
+
* {@link LocalPlugin} (`{ path }`) bind-mounted live from your repo — the same
|
|
81
|
+
* shape `kizlo dev` mounts, so a fixture can carry your plugin's local source
|
|
82
|
+
* and it behaves identically in the dev and test stacks (no build/zip step).
|
|
83
|
+
*/
|
|
84
|
+
plugins?: DevPluginSource[];
|
|
64
85
|
/** Build this extension's world once during seeding; return created handles. */
|
|
65
|
-
seed?:
|
|
86
|
+
seed?: FixtureSeedFn;
|
|
66
87
|
/** Revert per-test mutations (teardown); does NOT uninstall plugins or seeded data. */
|
|
67
|
-
cleanup?:
|
|
88
|
+
cleanup?: FixtureCleanupFn;
|
|
68
89
|
}
|
|
69
90
|
/** Identity helper for authoring a `Fixture` with full inference and autocomplete. */
|
|
70
91
|
declare function defineFixture(fixture: Fixture): Fixture;
|
|
71
92
|
//#endregion
|
|
72
|
-
export { Fixture, JsonValue, PluginSource, SeedContext, TestAdminUser, TestCredentials, TestUser, defineFixture };
|
|
93
|
+
export { DevPluginSource, Fixture, FixtureCleanupFn, FixtureSeedFn, JsonValue, LocalPlugin, PluginSource, SeedContext, TestAdminUser, TestCredentials, TestUser, defineFixture, isLocalPlugin };
|
package/dist/cli/wp/types.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
//#region src/cli/wp/types.ts
|
|
2
|
+
/** True when a dev plugin entry is a local directory to bind-mount, not something to install. */
|
|
3
|
+
function isLocalPlugin(plugin) {
|
|
4
|
+
return typeof plugin === "object" && "path" in plugin;
|
|
5
|
+
}
|
|
2
6
|
/** Identity helper for authoring a `Fixture` with full inference and autocomplete. */
|
|
3
7
|
function defineFixture(fixture) {
|
|
4
8
|
return fixture;
|
|
5
9
|
}
|
|
6
10
|
|
|
7
11
|
//#endregion
|
|
8
|
-
export { defineFixture };
|
|
12
|
+
export { defineFixture, isLocalPlugin };
|
package/dist/cli/wp/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PluginSource, TestCredentials } from "./types.js";
|
|
1
|
+
import { DevPluginSource, PluginSource, TestCredentials } from "./types.js";
|
|
2
2
|
|
|
3
3
|
//#region src/cli/wp/utils.d.ts
|
|
4
4
|
|
|
@@ -16,6 +16,12 @@ import { PluginSource, TestCredentials } from "./types.js";
|
|
|
16
16
|
declare function isSeeded(): Promise<boolean>;
|
|
17
17
|
/** Build a GitHub release-zip URL for `PluginSource.source` (asset named `<tag>.zip`). */
|
|
18
18
|
declare function githubRelease(repo: string, tag: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Download URL for a Kizlo plugin's latest release, served by kizlo.io (it 302s to
|
|
21
|
+
* the current GitHub release asset). Always tracks the newest published version —
|
|
22
|
+
* use {@link githubRelease} instead to pin a specific tag.
|
|
23
|
+
*/
|
|
24
|
+
declare function kizloRelease(slug: string): string;
|
|
19
25
|
/** Resolve a `PluginSource` to the `(name, source)` pair `ensurePlugin` needs. */
|
|
20
26
|
declare function resolvePluginSource(plugin: PluginSource): [name: string, source: string];
|
|
21
27
|
/**
|
|
@@ -25,6 +31,21 @@ declare function resolvePluginSource(plugin: PluginSource): [name: string, sourc
|
|
|
25
31
|
* are absent (i.e. after `wp reset` wipes the volume).
|
|
26
32
|
*/
|
|
27
33
|
declare function ensurePlugin(name: string, source: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Activate a bind-mounted local plugin by its directory basename (the slug the
|
|
36
|
+
* mount lands at under `wp-content/plugins`). Idempotent: the files arrive via the
|
|
37
|
+
* compose bind, so there's nothing to install — just activate if it isn't already.
|
|
38
|
+
*/
|
|
39
|
+
declare function activateLocalPlugin(pluginPath: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Ensure every plugin is active. Installable sources go first so a bind-mounted
|
|
42
|
+
* local plugin that depends on one (e.g. a WooCommerce extension) finds it already
|
|
43
|
+
* present; then each {@link LocalPlugin} is activated by basename. A local plugin
|
|
44
|
+
* whose slug matches an installable (e.g. mounting `kizlo` over the released
|
|
45
|
+
* `kizlo`) is seen as already installed, so `ensurePlugin` activates the live
|
|
46
|
+
* files instead of downloading over them.
|
|
47
|
+
*/
|
|
48
|
+
declare function ensurePlugins(plugins: DevPluginSource[]): Promise<void>;
|
|
28
49
|
/**
|
|
29
50
|
* The directory holding `kizlo.config.*`, found by walking up from `cwd` (falling
|
|
30
51
|
* back to `cwd` when none is found). Anchoring the credentials artifact here —
|
|
@@ -35,5 +56,11 @@ declare function findConfigDir(cwd?: string): string;
|
|
|
35
56
|
/** Fixed credentials artifact path, anchored to the config root. */
|
|
36
57
|
declare function credentialsPath(cwd?: string): string;
|
|
37
58
|
declare function getTestCredentials(): TestCredentials;
|
|
59
|
+
/**
|
|
60
|
+
* The host port the credentials artifact advertises, if it exists — the port a warm test
|
|
61
|
+
* stack is pinned to. Tests connect via this URL, so a reused stack must come back up on
|
|
62
|
+
* the same port; `undefined` means no stack has ever been seeded here (a cold boot).
|
|
63
|
+
*/
|
|
64
|
+
declare function recordedPort(): number | undefined;
|
|
38
65
|
//#endregion
|
|
39
|
-
export { credentialsPath, ensurePlugin, findConfigDir, getTestCredentials, githubRelease, isSeeded, resolvePluginSource };
|
|
66
|
+
export { activateLocalPlugin, credentialsPath, ensurePlugin, ensurePlugins, findConfigDir, getTestCredentials, githubRelease, isSeeded, kizloRelease, recordedPort, resolvePluginSource };
|