kizlo 0.1.1 → 0.2.1
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/README.md +1 -1
- 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 +203 -20
- package/dist/cli/commands/init.js +240 -47
- package/dist/cli/commands/test.js +56 -19
- package/dist/cli/commands/watch.js +5 -23
- package/dist/cli/daemon/config.js +11 -5
- package/dist/cli/daemon/logger.js +1 -0
- package/dist/cli/daemon/watch.js +35 -2
- package/dist/cli/presets/base.js +8 -1
- package/dist/cli/presets/nextjs.js +1 -1
- package/dist/cli/utils.js +74 -5
- package/dist/cli/wp/compose/docker-compose.yml +12 -0
- package/dist/cli/wp/dev.js +4 -2
- package/dist/cli/wp/docker.d.ts +22 -4
- package/dist/cli/wp/docker.js +27 -5
- 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/types.d.ts +5 -3
- package/dist/cli/wp/utils.d.ts +7 -1
- package/dist/cli/wp/utils.js +17 -1
- 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/context.d.ts +12 -1
- package/dist/context.js +1 -1
- package/dist/index.d.ts +16 -12
- package/dist/index.js +9 -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/package.js +1 -1
- package/dist/router.d.ts +7 -7
- 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 +3 -3
- 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 +1 -1
package/README.md
CHANGED
package/dist/adapters/auth.d.ts
CHANGED
|
@@ -2,17 +2,26 @@ import { Promisify } from "@kizlo/shared";
|
|
|
2
2
|
|
|
3
3
|
//#region src/adapters/auth.d.ts
|
|
4
4
|
interface AuthUser {
|
|
5
|
+
/** The WordPress user id this caller maps to. */
|
|
5
6
|
id: number;
|
|
7
|
+
/** The user's email, if known. */
|
|
6
8
|
email?: string;
|
|
9
|
+
/** The user's phone number, if known. */
|
|
7
10
|
phone?: string;
|
|
11
|
+
/** The user's first name. */
|
|
8
12
|
firstName: string;
|
|
13
|
+
/** The user's last name. */
|
|
9
14
|
lastName: string;
|
|
10
15
|
}
|
|
16
|
+
type AuthGetUser = (request: Request | null) => Promisify<AuthUser | null>;
|
|
11
17
|
interface AuthAdapter {
|
|
12
|
-
|
|
18
|
+
/** Resolve the authenticated user from the request, or `null` when there is no session. Receives `null` for server-side (non-HTTP) invocations. */
|
|
19
|
+
getUser: AuthGetUser;
|
|
13
20
|
}
|
|
21
|
+
/** Author a custom auth adapter, typed against the {@link AuthAdapter} contract. */
|
|
22
|
+
declare function createAuthAdapter(adapter: AuthAdapter): AuthAdapter;
|
|
14
23
|
declare function authMock(options?: {
|
|
15
24
|
mockUserId?: number;
|
|
16
25
|
}): AuthAdapter;
|
|
17
26
|
//#endregion
|
|
18
|
-
export { AuthAdapter, AuthUser, authMock };
|
|
27
|
+
export { AuthAdapter, AuthGetUser, AuthUser, authMock, createAuthAdapter };
|
package/dist/adapters/auth.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
//#region src/adapters/auth.ts
|
|
2
|
+
/** Author a custom auth adapter, typed against the {@link AuthAdapter} contract. */
|
|
3
|
+
function createAuthAdapter(adapter) {
|
|
4
|
+
return adapter;
|
|
5
|
+
}
|
|
2
6
|
function authMock(options) {
|
|
3
7
|
return { getUser() {
|
|
4
8
|
return {
|
|
@@ -12,4 +16,4 @@ function authMock(options) {
|
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
//#endregion
|
|
15
|
-
export { authMock };
|
|
19
|
+
export { authMock, createAuthAdapter };
|
|
@@ -3,6 +3,8 @@ type CaptchaAdapter = (input: {
|
|
|
3
3
|
token: string;
|
|
4
4
|
ip: string;
|
|
5
5
|
}) => Promise<boolean>;
|
|
6
|
+
/** Author a custom captcha adapter — types your verify function against the {@link CaptchaAdapter} contract. */
|
|
7
|
+
declare function createCaptchaAdapter(adapter: CaptchaAdapter): CaptchaAdapter;
|
|
6
8
|
declare function recaptcha(opts: {
|
|
7
9
|
secret: string;
|
|
8
10
|
minScore?: number;
|
|
@@ -25,4 +27,4 @@ declare function altcha(opts: {
|
|
|
25
27
|
hmacKey: string;
|
|
26
28
|
}): CaptchaAdapter;
|
|
27
29
|
//#endregion
|
|
28
|
-
export { CaptchaAdapter, altcha, arkose, friendlyCaptcha, hcaptcha, recaptcha, turnstile };
|
|
30
|
+
export { CaptchaAdapter, altcha, arkose, createCaptchaAdapter, friendlyCaptcha, hcaptcha, recaptcha, turnstile };
|
package/dist/adapters/captcha.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
//#region src/adapters/captcha.ts
|
|
2
|
+
/** Author a custom captcha adapter — types your verify function against the {@link CaptchaAdapter} contract. */
|
|
3
|
+
function createCaptchaAdapter(adapter) {
|
|
4
|
+
return adapter;
|
|
5
|
+
}
|
|
2
6
|
function recaptcha(opts) {
|
|
3
7
|
return async ({ token, ip }) => {
|
|
4
8
|
const data = await (await fetch("https://www.google.com/recaptcha/api/siteverify", {
|
|
@@ -83,4 +87,4 @@ function altcha(opts) {
|
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
//#endregion
|
|
86
|
-
export { altcha, arkose, friendlyCaptcha, hcaptcha, recaptcha, turnstile };
|
|
90
|
+
export { altcha, arkose, createCaptchaAdapter, friendlyCaptcha, hcaptcha, recaptcha, turnstile };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CookiesAdapter } from "../shared/types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/adapters/cookies.d.ts
|
|
4
|
+
/** Author a custom cookies adapter, typed against the {@link CookiesAdapter} contract. */
|
|
5
|
+
declare function createCookiesAdapter(adapter: CookiesAdapter): CookiesAdapter;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { createCookiesAdapter };
|
package/dist/adapters/geo.d.ts
CHANGED
|
@@ -2,17 +2,28 @@ import { Promisify } from "@kizlo/shared";
|
|
|
2
2
|
|
|
3
3
|
//#region src/adapters/geo.d.ts
|
|
4
4
|
interface ConnInfo {
|
|
5
|
+
/** Client IP address, or `null` if unavailable. */
|
|
5
6
|
ip: string | null;
|
|
7
|
+
/** ISO country code, or `null`. */
|
|
6
8
|
country: string | null;
|
|
9
|
+
/** City name, or `null`. */
|
|
7
10
|
city: string | null;
|
|
11
|
+
/** Region/state, or `null`. */
|
|
8
12
|
state: string | null;
|
|
13
|
+
/** Postal code, or `null`. */
|
|
9
14
|
postcode: string | null;
|
|
15
|
+
/** IANA timezone, or `null`. */
|
|
10
16
|
timezone: string | null;
|
|
17
|
+
/** The request's `User-Agent`, or `null`. */
|
|
11
18
|
userAgent: string | null;
|
|
12
19
|
}
|
|
20
|
+
type GeoGetConnInfo = (request: Request | null) => Promisify<ConnInfo>;
|
|
13
21
|
interface GeoAdapter {
|
|
14
|
-
|
|
22
|
+
/** Resolve connection info (IP, geo, user agent) from the request. Return the shape with `null` fields when unknown — never throw. Receives `null` server-side. */
|
|
23
|
+
getConnInfo: GeoGetConnInfo;
|
|
15
24
|
}
|
|
25
|
+
/** Author a custom geo adapter, typed against the {@link GeoAdapter} contract. */
|
|
26
|
+
declare function createGeoAdapter(adapter: GeoAdapter): GeoAdapter;
|
|
16
27
|
declare function geoMock(values?: Partial<ConnInfo>): GeoAdapter;
|
|
17
28
|
//#endregion
|
|
18
|
-
export { ConnInfo, GeoAdapter, geoMock };
|
|
29
|
+
export { ConnInfo, GeoAdapter, GeoGetConnInfo, createGeoAdapter, geoMock };
|
package/dist/adapters/geo.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
//#region src/adapters/geo.ts
|
|
2
|
+
/** Author a custom geo adapter, typed against the {@link GeoAdapter} contract. */
|
|
3
|
+
function createGeoAdapter(adapter) {
|
|
4
|
+
return adapter;
|
|
5
|
+
}
|
|
2
6
|
function geoMock(values) {
|
|
3
7
|
return { getConnInfo(request) {
|
|
4
8
|
return {
|
|
@@ -15,4 +19,4 @@ function geoMock(values) {
|
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
//#endregion
|
|
18
|
-
export { geoMock };
|
|
22
|
+
export { createGeoAdapter, geoMock };
|
|
@@ -4,13 +4,20 @@ import { LiteralUnion } from "@kizlo/shared";
|
|
|
4
4
|
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
5
5
|
type Environment = LiteralUnion<"development" | "production" | "test", string>;
|
|
6
6
|
interface LogPayload {
|
|
7
|
+
/** Severity of the record: `debug`, `info`, `warn`, or `error`. */
|
|
7
8
|
level: LogLevel;
|
|
9
|
+
/** The log message. */
|
|
8
10
|
message: string;
|
|
11
|
+
/** When the record was created. */
|
|
9
12
|
timestamp: Date;
|
|
13
|
+
/** Structured fields attached to the call, if any. */
|
|
10
14
|
context?: Record<string, unknown>;
|
|
15
|
+
/** The associated error, present on `error`-level records that pass one. */
|
|
11
16
|
error?: Error;
|
|
12
17
|
}
|
|
13
18
|
type LoggerAdapter = (payload: LogPayload) => void | Promise<void>;
|
|
19
|
+
/** Author a custom logger adapter — types your handler against the {@link LoggerAdapter} contract. */
|
|
20
|
+
declare function createLoggerAdapter(adapter: LoggerAdapter): LoggerAdapter;
|
|
14
21
|
interface Logger {
|
|
15
22
|
debug(message: string, context?: Record<string, unknown>): void;
|
|
16
23
|
info(message: string, context?: Record<string, unknown>): void;
|
|
@@ -28,4 +35,4 @@ interface ConsoleLogAdapterOptions {
|
|
|
28
35
|
declare function consoleLog(opts?: ConsoleLogAdapterOptions): LoggerAdapter;
|
|
29
36
|
declare const noopAdapter: LoggerAdapter;
|
|
30
37
|
//#endregion
|
|
31
|
-
export { ConsoleLogAdapterOptions, Environment, LogLevel, LogPayload, Logger, LoggerAdapter, consoleLog, noopAdapter };
|
|
38
|
+
export { ConsoleLogAdapterOptions, Environment, LogLevel, LogPayload, Logger, LoggerAdapter, consoleLog, createLoggerAdapter, noopAdapter };
|
package/dist/adapters/logger.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
//#region src/adapters/logger.ts
|
|
2
|
+
/** Author a custom logger adapter — types your handler against the {@link LoggerAdapter} contract. */
|
|
3
|
+
function createLoggerAdapter(adapter) {
|
|
4
|
+
return adapter;
|
|
5
|
+
}
|
|
2
6
|
function consoleLog(opts = {}) {
|
|
3
7
|
const levelsOpt = opts.levels ?? true;
|
|
4
8
|
const allowed = levelsOpt === true ? null : new Set(levelsOpt === false ? [] : levelsOpt);
|
|
@@ -27,4 +31,4 @@ function consoleLog(opts = {}) {
|
|
|
27
31
|
const noopAdapter = () => {};
|
|
28
32
|
|
|
29
33
|
//#endregion
|
|
30
|
-
export { consoleLog, noopAdapter };
|
|
34
|
+
export { consoleLog, createLoggerAdapter, noopAdapter };
|
package/dist/cli/banner.js
CHANGED
|
@@ -11,6 +11,17 @@ const KIZLO_TAGLINE = "Headless WordPress toolkit for TypeScript";
|
|
|
11
11
|
function colorSupported() {
|
|
12
12
|
return Boolean(process.stdout.isTTY) && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb";
|
|
13
13
|
}
|
|
14
|
+
/** The CLI's shared ANSI palette (empty strings when color is unsupported). */
|
|
15
|
+
function palette() {
|
|
16
|
+
const on = colorSupported();
|
|
17
|
+
return {
|
|
18
|
+
cyan: on ? "\x1B[38;5;44m" : "",
|
|
19
|
+
green: on ? "\x1B[32m" : "",
|
|
20
|
+
bold: on ? "\x1B[1m" : "",
|
|
21
|
+
dim: on ? "\x1B[2m" : "",
|
|
22
|
+
reset: on ? "\x1B[0m" : ""
|
|
23
|
+
};
|
|
24
|
+
}
|
|
14
25
|
function printBanner(version) {
|
|
15
26
|
const color = colorSupported();
|
|
16
27
|
const cyan = color ? "\x1B[38;5;44m" : "";
|
|
@@ -23,4 +34,4 @@ function printBanner(version) {
|
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
//#endregion
|
|
26
|
-
export { printBanner };
|
|
37
|
+
export { palette, printBanner };
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -1,36 +1,218 @@
|
|
|
1
1
|
import { createStack } from "../wp/docker.js";
|
|
2
|
-
import { ensureGitignored, groupDefault, withSpinner } from "../utils.js";
|
|
3
|
-
import {
|
|
2
|
+
import { ensureGitignored, envGroups, getVersion, groupDefault, mergeEnv, pickStackPort, withSpinner } from "../utils.js";
|
|
3
|
+
import { palette, printBanner } from "../banner.js";
|
|
4
4
|
import { resolveDevConfig } from "../daemon/config.js";
|
|
5
|
+
import { startWatcher } from "../daemon/watch.js";
|
|
5
6
|
import { bootstrapDev } from "../wp/dev.js";
|
|
7
|
+
import { reapOrphans, registerSession, removeProjectContainers, spawnWatchdog, unregisterSession } from "../wp/session.js";
|
|
8
|
+
import { syncSiteSettings } from "../wp/settings.js";
|
|
6
9
|
import { devStack } from "../wp/stack.js";
|
|
7
|
-
import {
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
8
13
|
import { defineCommand } from "citty";
|
|
14
|
+
import { networkInterfaces } from "node:os";
|
|
9
15
|
|
|
10
16
|
//#region src/cli/commands/dev.ts
|
|
11
|
-
async function resolve(cwd) {
|
|
17
|
+
async function resolve$1(cwd) {
|
|
12
18
|
const cfg = await resolveDevConfig(cwd);
|
|
13
19
|
return {
|
|
14
20
|
cfg,
|
|
15
21
|
stack: createStack(devStack(cfg))
|
|
16
22
|
};
|
|
17
23
|
}
|
|
18
|
-
/**
|
|
24
|
+
/** A dim, untagged one-liner (no consola timestamp) for the clean dev output. */
|
|
25
|
+
function note(text) {
|
|
26
|
+
const { dim, reset } = palette();
|
|
27
|
+
process.stdout.write(` ${dim}${text}${reset}\n`);
|
|
28
|
+
}
|
|
29
|
+
/** First non-internal IPv4 address, for the LAN-reachable "Network" URL (undefined when offline). */
|
|
30
|
+
function lanAddress() {
|
|
31
|
+
for (const ifaces of Object.values(networkInterfaces())) for (const iface of ifaces ?? []) if (iface.family === "IPv4" && !iface.internal) return iface.address;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Print the connection summary as an aligned, Next.js-style block. Shows the wp-admin
|
|
35
|
+
* URL on localhost ("Local") and — when a LAN address exists — on the network, so the
|
|
36
|
+
* stack can be opened from other devices. On a fresh install, also show the one-time
|
|
37
|
+
* wp-admin password (it's never stored, so this is the only chance to see it).
|
|
38
|
+
*/
|
|
19
39
|
function printSummary(info) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
const { cyan, dim, reset } = palette();
|
|
41
|
+
const port = new URL(info.url).port;
|
|
42
|
+
const lan = lanAddress();
|
|
43
|
+
const rows = [["WP Local", `${info.url}/wp-admin`]];
|
|
44
|
+
if (lan) rows.push(["WP Network", `http://${lan}:${port}/wp-admin`]);
|
|
45
|
+
rows.push(["Database Connection", `127.0.0.1:${info.dbPort}`]);
|
|
46
|
+
const width = Math.max(...rows.map(([label]) => label.length)) + 1;
|
|
47
|
+
const lines = ["", ...rows.map(([label, value]) => ` ${dim}- ${`${label}:`.padEnd(width)}${reset} ${cyan}${value}${reset}`)];
|
|
48
|
+
if (info.secrets) lines.push("", ` ${dim}admin login:${reset} ${info.username} / ${info.secrets.password}`, ` ${dim}save the password now — it's shown only once${reset}`);
|
|
49
|
+
else if (info.imported) lines.push("", ` ${dim}log in with your imported site's own credentials${reset}`);
|
|
50
|
+
if (info.seeded > 0) lines.push(` ${dim}seeded ${info.seeded} fixture${info.seeded === 1 ? "" : "s"}${reset}`);
|
|
51
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Dev connection keys a fresh dev install must rewrite (the rest of `.env` is untouched). The
|
|
55
|
+
* production `KIZLO_WORDPRESS_*` / `KIZLO_SITE_SECRET` keys live under their own names and are never
|
|
56
|
+
* touched by `kizlo dev`, so pointing them at a real site survives booting the local stack.
|
|
57
|
+
*/
|
|
58
|
+
const WP_ENV_KEYS = [
|
|
59
|
+
"KIZLO_DEV_WORDPRESS_URL",
|
|
60
|
+
"KIZLO_DEV_WORDPRESS_USERNAME",
|
|
61
|
+
"KIZLO_DEV_WORDPRESS_APPLICATION_PASSWORD"
|
|
62
|
+
];
|
|
63
|
+
/** Read a key's value from raw `.env` content (first match, trimmed); undefined when absent or empty. */
|
|
64
|
+
function readEnvValue(content, key) {
|
|
65
|
+
return content.match(new RegExp(`^\\s*${key}\\s*=\\s*(.*)$`, "m"))?.[1]?.trim() || void 0;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The Kizlo backend URL from `.env` — the one key ending in `KIZLO_BACKEND_URL` (`KIZLO_BACKEND_URL`,
|
|
69
|
+
* or a framework-prefixed `NEXT_PUBLIC_KIZLO_BACKEND_URL`). It's the handler mount the plugin delivers
|
|
70
|
+
* webhook events to; its origin is the site's canonical URL. User-managed (init writes it), so dev
|
|
71
|
+
* only reads it. Undefined before `kizlo init` has populated `.env`.
|
|
72
|
+
*/
|
|
73
|
+
function readBaseUrl(content) {
|
|
74
|
+
return content.match(/^\s*[A-Z_]*KIZLO_BACKEND_URL\s*=\s*(.*)$/m)?.[1]?.trim() || void 0;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Rewrite the dev WordPress connection in `.env` after a fresh install. A reset (or first boot)
|
|
78
|
+
* wipes the database, so the previous application password — and possibly the port — no longer
|
|
79
|
+
* match; without this the app keeps the stale credentials and REST auth fails. Only the three
|
|
80
|
+
* connection keys are overwritten; everything else in `.env` is preserved, and the file is created
|
|
81
|
+
* if absent. Gated by the caller on a minted `appPassword`, so a warm resume never churns it.
|
|
82
|
+
*
|
|
83
|
+
* `KIZLO_TARGET=dev` and `KIZLO_DEV_SITE_SECRET` are written too but kept out of the overwrite set, so a
|
|
84
|
+
* value the user already set is preserved — booting the dev stack only fills them when absent. The
|
|
85
|
+
* resolved dev secret (the existing one, or a freshly minted one) and the Kizlo server base URL are
|
|
86
|
+
* returned so the caller can sync them into the plugin.
|
|
87
|
+
*/
|
|
88
|
+
function updateWpEnv(cfg, info) {
|
|
89
|
+
if (!info.appPassword) return void 0;
|
|
90
|
+
const envPath = join(cfg.configDir, ".env");
|
|
91
|
+
const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
|
|
92
|
+
const siteSecret = readEnvValue(existing, "KIZLO_DEV_SITE_SECRET") ?? randomBytes(32).toString("hex");
|
|
93
|
+
const { content } = mergeEnv(existing, {
|
|
94
|
+
KIZLO_DEV_WORDPRESS_URL: info.url,
|
|
95
|
+
KIZLO_DEV_WORDPRESS_USERNAME: info.username,
|
|
96
|
+
KIZLO_DEV_WORDPRESS_APPLICATION_PASSWORD: info.appPassword,
|
|
97
|
+
KIZLO_DEV_SITE_SECRET: siteSecret,
|
|
98
|
+
KIZLO_TARGET: "dev"
|
|
99
|
+
}, new Set(WP_ENV_KEYS), envGroups("KIZLO_BACKEND_URL"));
|
|
100
|
+
writeFileSync(envPath, content);
|
|
101
|
+
return {
|
|
102
|
+
siteSecret,
|
|
103
|
+
baseUrl: readBaseUrl(existing)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Arm the foreground teardown — call this *before* the containers start, so a cancel at
|
|
108
|
+
* any point (even mid-startup, with mysql up but wordpress still booting) stops whatever
|
|
109
|
+
* already came up. The teardown is owned by a detached **watchdog** process, not by this
|
|
110
|
+
* one: containers belong to the Docker daemon, not our process tree, so — unlike a
|
|
111
|
+
* `next dev` child server — nothing stops them automatically when we die. A dying process
|
|
112
|
+
* also can't be trusted to finish async work (a closed terminal breaks its stdout
|
|
113
|
+
* mid-write), so this process does no docker work on the way out. It only needs to
|
|
114
|
+
* *exit*; the watchdog, watching our PID from its own session, stops the stack the moment
|
|
115
|
+
* we're gone — by Ctrl+C, SIGTERM, a closed tab, SIGKILL, or a crash alike.
|
|
116
|
+
*
|
|
117
|
+
* We trigger that exit two ways, because neither alone is enough: signal handlers (for
|
|
118
|
+
* Ctrl+C / `kill`), and an stdin/TTY-close watch (for a closed terminal, whose SIGHUP
|
|
119
|
+
* an `npm`/`pnpm` wrapper silently swallows — leaving us orphaned but alive otherwise).
|
|
120
|
+
*/
|
|
121
|
+
function armForegroundTeardown(cfg) {
|
|
122
|
+
registerSession(cfg.project);
|
|
123
|
+
spawnWatchdog(cfg.project);
|
|
124
|
+
let exiting = false;
|
|
125
|
+
const shutdown = (reason) => {
|
|
126
|
+
if (exiting) return;
|
|
127
|
+
exiting = true;
|
|
128
|
+
unregisterSession(cfg.project);
|
|
129
|
+
try {
|
|
130
|
+
note(`Stopping dev stack (${reason})…`);
|
|
131
|
+
} catch {}
|
|
132
|
+
process.exit(0);
|
|
133
|
+
};
|
|
134
|
+
for (const signal of [
|
|
135
|
+
"SIGINT",
|
|
136
|
+
"SIGTERM",
|
|
137
|
+
"SIGHUP"
|
|
138
|
+
]) process.on(signal, () => shutdown(signal));
|
|
139
|
+
if (process.stdin.isTTY) {
|
|
140
|
+
process.stdin.resume();
|
|
141
|
+
for (const event of [
|
|
142
|
+
"end",
|
|
143
|
+
"close",
|
|
144
|
+
"error"
|
|
145
|
+
]) process.stdin.on(event, () => shutdown("terminal closed"));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Boot the stack and run in the foreground until exit — the shared path behind bare
|
|
150
|
+
* `kizlo dev` and `kizlo dev reset`. Arms teardown first (so a mid-startup cancel still
|
|
151
|
+
* stops partial containers), shows a timed spinner, prints the summary + stop hint, then
|
|
152
|
+
* parks. A referenced timer holds the event loop open — an unresolved promise alone would
|
|
153
|
+
* not, so without it Node would empty its loop and exit straight after printing.
|
|
154
|
+
*/
|
|
155
|
+
async function startForeground(cfg) {
|
|
156
|
+
await removeProjectContainers(cfg.project);
|
|
157
|
+
const port = await pickStackPort(cfg.port, {
|
|
158
|
+
fixed: cfg.portExplicit,
|
|
159
|
+
configKey: "dev.port"
|
|
160
|
+
});
|
|
161
|
+
const dbPort = await pickStackPort(cfg.dbPort, {
|
|
162
|
+
fixed: cfg.dbPortExplicit,
|
|
163
|
+
host: "127.0.0.1",
|
|
164
|
+
configKey: "dev.dbPort"
|
|
165
|
+
});
|
|
166
|
+
const ready = {
|
|
167
|
+
...cfg,
|
|
168
|
+
port,
|
|
169
|
+
dbPort
|
|
170
|
+
};
|
|
171
|
+
createStack(devStack(ready));
|
|
172
|
+
if (port !== cfg.port) note(`Port ${cfg.port} is in use — serving on ${port} instead.`);
|
|
173
|
+
if (dbPort !== cfg.dbPort) note(`Database port ${cfg.dbPort} is in use — using ${dbPort} instead.`);
|
|
174
|
+
armForegroundTeardown(ready);
|
|
175
|
+
const start = Date.now();
|
|
176
|
+
const creds = await withSpinner("Starting WordPress dev stack", () => bootstrapDev(ready));
|
|
177
|
+
if (creds.appPassword) {
|
|
178
|
+
const env = updateWpEnv(ready, creds);
|
|
179
|
+
note("Updated .env with the new WordPress credentials");
|
|
180
|
+
if (env) {
|
|
181
|
+
const sync = await syncSiteSettings({
|
|
182
|
+
url: creds.url,
|
|
183
|
+
username: creds.username,
|
|
184
|
+
password: creds.appPassword
|
|
185
|
+
}, {
|
|
186
|
+
secret: env.siteSecret,
|
|
187
|
+
backendUrl: env.baseUrl,
|
|
188
|
+
containerized: true
|
|
189
|
+
});
|
|
190
|
+
if (!sync.ok) note(`Could not sync the site settings to WordPress (${sync.error}) — make sure the kizlo plugin is active, then set them from the Kizlo settings.`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
printSummary(creds);
|
|
194
|
+
const { green, dim, bold, reset } = palette();
|
|
195
|
+
const ms = Date.now() - start;
|
|
196
|
+
const took = ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
197
|
+
process.stdout.write(`\n ${dim}Press ${reset}${bold}Ctrl+C${reset}${dim} (or close the terminal) to stop the development stack${reset}\n`);
|
|
198
|
+
process.stdout.write(`\n ${green}✓${reset} Ready in ${took}\n\n`);
|
|
199
|
+
const stopWatcher = await startWatcher(ready.configDir);
|
|
200
|
+
if (stopWatcher) process.on("exit", stopWatcher);
|
|
201
|
+
setInterval(() => {}, 1 << 30);
|
|
202
|
+
await new Promise(() => {});
|
|
28
203
|
}
|
|
29
|
-
/**
|
|
204
|
+
/**
|
|
205
|
+
* Start (or resume) the dev stack. Backs bare `kizlo dev`. Always runs in the foreground
|
|
206
|
+
* and stops the stack on exit, so it never outlives the session — the subcommands
|
|
207
|
+
* (`stop`/`down`/`reset`) manage everything else.
|
|
208
|
+
*/
|
|
30
209
|
async function bringUp() {
|
|
31
|
-
|
|
210
|
+
printBanner(getVersion());
|
|
211
|
+
const reaped = await reapOrphans();
|
|
212
|
+
if (reaped.length) note(`Stopped ${reaped.length} orphaned dev stack${reaped.length === 1 ? "" : "s"}: ${reaped.join(", ")}`);
|
|
213
|
+
const cfg = await resolveDevConfig(process.cwd());
|
|
32
214
|
if (!cfg.wordpressPath.startsWith("..")) ensureGitignored(cfg.configDir, cfg.wordpressPath);
|
|
33
|
-
|
|
215
|
+
await startForeground(cfg);
|
|
34
216
|
}
|
|
35
217
|
const subCommands = {
|
|
36
218
|
stop: defineCommand({
|
|
@@ -39,7 +221,7 @@ const subCommands = {
|
|
|
39
221
|
description: "Stop the dev stack, keeping the database volume"
|
|
40
222
|
},
|
|
41
223
|
async run() {
|
|
42
|
-
const { stack } = await resolve(process.cwd());
|
|
224
|
+
const { stack } = await resolve$1(process.cwd());
|
|
43
225
|
await withSpinner("Stopping WordPress dev stack", () => stack.composeStop(), "Stack stopped (volumes kept)");
|
|
44
226
|
}
|
|
45
227
|
}),
|
|
@@ -49,7 +231,7 @@ const subCommands = {
|
|
|
49
231
|
description: "Stop and remove the dev stack containers (keeps the database and files)"
|
|
50
232
|
},
|
|
51
233
|
async run() {
|
|
52
|
-
const { stack } = await resolve(process.cwd());
|
|
234
|
+
const { stack } = await resolve$1(process.cwd());
|
|
53
235
|
await withSpinner("Removing WordPress dev stack", () => stack.composeDown(), "Stack removed (database and files kept)");
|
|
54
236
|
}
|
|
55
237
|
}),
|
|
@@ -59,13 +241,14 @@ const subCommands = {
|
|
|
59
241
|
description: "Wipe the database and the install folder, then rebuild fresh"
|
|
60
242
|
},
|
|
61
243
|
async run() {
|
|
62
|
-
|
|
244
|
+
printBanner(getVersion());
|
|
245
|
+
const { cfg, stack } = await resolve$1(process.cwd());
|
|
63
246
|
await withSpinner("Wiping WordPress dev stack", () => stack.composeDown({ volumes: true }), "Stack wiped");
|
|
64
247
|
rmSync(cfg.wordpressDir, {
|
|
65
248
|
recursive: true,
|
|
66
249
|
force: true
|
|
67
250
|
});
|
|
68
|
-
|
|
251
|
+
await startForeground(cfg);
|
|
69
252
|
}
|
|
70
253
|
})
|
|
71
254
|
};
|