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
|
@@ -1,23 +1,64 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { composeStop, createStack, dockerAvailable } from "../wp/docker.js";
|
|
2
|
+
import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, envGroups, envKeysPresent, getVersion, loadEnvFiles, mergeEnv, pickStackPort, resolveModuleImport, runCommand, writeFileIfAbsent } from "../utils.js";
|
|
3
3
|
import { printBanner } from "../banner.js";
|
|
4
|
+
import { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveStackName } from "../daemon/config.js";
|
|
5
|
+
import { createAdminAppPassword } from "../wp/bootstrap.js";
|
|
6
|
+
import { bootstrapDev } from "../wp/dev.js";
|
|
7
|
+
import { removeProjectContainers } from "../wp/session.js";
|
|
8
|
+
import { syncSiteSettings } from "../wp/settings.js";
|
|
9
|
+
import { devStack } from "../wp/stack.js";
|
|
10
|
+
import { CONTRACT_BARREL } from "../daemon/generate.js";
|
|
4
11
|
import { PRESETS, detectPreset, getPreset } from "../presets/index.js";
|
|
5
12
|
import z from "zod/v4";
|
|
6
13
|
import { randomBytes } from "node:crypto";
|
|
7
|
-
import path from "node:path";
|
|
8
14
|
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
9
16
|
import { defineCommand } from "citty";
|
|
10
17
|
import * as p from "@clack/prompts";
|
|
11
18
|
|
|
12
19
|
//#region src/cli/commands/init.ts
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
20
|
+
/** Production connection keys — what a real deploy needs, and what `.env.example` always lists. */
|
|
21
|
+
const PROD_WP_ENV_KEYS = [
|
|
22
|
+
"KIZLO_SITE_SECRET",
|
|
23
|
+
"KIZLO_WORDPRESS_URL",
|
|
24
|
+
"KIZLO_WORDPRESS_USERNAME",
|
|
25
|
+
"KIZLO_WORDPRESS_APPLICATION_PASSWORD"
|
|
18
26
|
];
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The `.env` keys init manages and the values to write, branched on the connection mode. A local
|
|
29
|
+
* dev stack writes the `KIZLO_DEV_WORDPRESS_*` / `KIZLO_DEV_SITE_SECRET` set plus `KIZLO_TARGET=dev`, so it
|
|
30
|
+
* never touches the production keys (a user can point those at a real site). A remote site writes the
|
|
31
|
+
* bare production keys exactly as before — no `KIZLO_TARGET`, since `"production"` is the default target.
|
|
32
|
+
*/
|
|
33
|
+
function managedEnv(preset, setup) {
|
|
34
|
+
if (setup.mode === "local") return {
|
|
35
|
+
keys: [
|
|
36
|
+
preset.baseUrlEnvKey,
|
|
37
|
+
"KIZLO_TARGET",
|
|
38
|
+
"KIZLO_DEV_SITE_SECRET",
|
|
39
|
+
"KIZLO_DEV_WORDPRESS_URL",
|
|
40
|
+
"KIZLO_DEV_WORDPRESS_USERNAME",
|
|
41
|
+
"KIZLO_DEV_WORDPRESS_APPLICATION_PASSWORD"
|
|
42
|
+
],
|
|
43
|
+
values: {
|
|
44
|
+
[preset.baseUrlEnvKey]: setup.baseUrl,
|
|
45
|
+
KIZLO_TARGET: "dev",
|
|
46
|
+
KIZLO_DEV_SITE_SECRET: setup.siteSecret,
|
|
47
|
+
KIZLO_DEV_WORDPRESS_URL: setup.wpUrl,
|
|
48
|
+
KIZLO_DEV_WORDPRESS_USERNAME: setup.wpUsername,
|
|
49
|
+
KIZLO_DEV_WORDPRESS_APPLICATION_PASSWORD: setup.wpPassword
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
keys: [preset.baseUrlEnvKey, ...PROD_WP_ENV_KEYS],
|
|
54
|
+
values: {
|
|
55
|
+
[preset.baseUrlEnvKey]: setup.baseUrl,
|
|
56
|
+
KIZLO_SITE_SECRET: setup.siteSecret,
|
|
57
|
+
KIZLO_WORDPRESS_URL: setup.wpUrl,
|
|
58
|
+
KIZLO_WORDPRESS_USERNAME: setup.wpUsername,
|
|
59
|
+
KIZLO_WORDPRESS_APPLICATION_PASSWORD: setup.wpPassword
|
|
60
|
+
}
|
|
61
|
+
};
|
|
21
62
|
}
|
|
22
63
|
function defaultDir(hasSrcDir) {
|
|
23
64
|
return hasSrcDir ? "src/lib/kizlo" : "lib/kizlo";
|
|
@@ -26,11 +67,11 @@ function defaultDir(hasSrcDir) {
|
|
|
26
67
|
function aliasWithSlash(alias) {
|
|
27
68
|
return alias ? `${alias.replace(/\/+$/, "")}/` : "";
|
|
28
69
|
}
|
|
29
|
-
function kizloConfigTemplate(dir, alias) {
|
|
70
|
+
function kizloConfigTemplate(dir, alias, devPath) {
|
|
30
71
|
return `import { defineConfig } from "kizlo/config"
|
|
31
72
|
|
|
32
73
|
export default defineConfig({
|
|
33
|
-
dir: "${dir}",${alias ? `\n\talias: "${alias}",` : ""}
|
|
74
|
+
dir: "${dir}",${alias ? `\n\talias: "${alias}",` : ""}${devPath ? `\n\tdev: { path: "${devPath}" },` : ""}
|
|
34
75
|
})
|
|
35
76
|
`;
|
|
36
77
|
}
|
|
@@ -46,6 +87,14 @@ function withApiPath(baseUrl, apiPath) {
|
|
|
46
87
|
return baseUrl;
|
|
47
88
|
}
|
|
48
89
|
}
|
|
90
|
+
/** Whether two URLs share an origin (scheme + host + port); false when either can't be parsed. */
|
|
91
|
+
function sameOrigin(a, b) {
|
|
92
|
+
try {
|
|
93
|
+
return new URL(a).origin === new URL(b).origin;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
49
98
|
function detectAppDir(cwd, hasSrcDir) {
|
|
50
99
|
if (fs.existsSync(path.join(cwd, "src", "app"))) return "src/app";
|
|
51
100
|
if (fs.existsSync(path.join(cwd, "app"))) return "app";
|
|
@@ -68,13 +117,27 @@ function orCancel(value) {
|
|
|
68
117
|
return value;
|
|
69
118
|
}
|
|
70
119
|
async function collectInteractively(ctx) {
|
|
71
|
-
|
|
72
|
-
|
|
120
|
+
let siteUrl;
|
|
121
|
+
let baseUrl;
|
|
122
|
+
if (ctx.preset.apiPath) baseUrl = orCancel(await p.text({
|
|
123
|
+
message: "Public app URL",
|
|
73
124
|
placeholder: "https://your-app.com",
|
|
74
125
|
validate: validate(urlString)
|
|
75
126
|
}));
|
|
127
|
+
else {
|
|
128
|
+
siteUrl = orCancel(await p.text({
|
|
129
|
+
message: "Public site URL",
|
|
130
|
+
placeholder: "https://your-app.com",
|
|
131
|
+
validate: validate(urlString)
|
|
132
|
+
}));
|
|
133
|
+
baseUrl = orCancel(await p.text({
|
|
134
|
+
message: "Kizlo backend URL (where the handler is mounted)",
|
|
135
|
+
placeholder: "https://your-app.com/kizlo",
|
|
136
|
+
validate: validate(urlString)
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
76
139
|
const siteSecret = orCancel(await p.select({
|
|
77
|
-
message: "
|
|
140
|
+
message: "Site secret (webhook signing key)",
|
|
78
141
|
initialValue: "generate",
|
|
79
142
|
options: [{
|
|
80
143
|
value: "generate",
|
|
@@ -85,21 +148,44 @@ async function collectInteractively(ctx) {
|
|
|
85
148
|
label: "Enter my own"
|
|
86
149
|
}]
|
|
87
150
|
})) === "enter" ? orCancel(await p.password({
|
|
88
|
-
message: "Enter
|
|
151
|
+
message: "Enter the site secret",
|
|
89
152
|
validate: validate(requiredString)
|
|
90
153
|
})) : randomBytes(32).toString("hex");
|
|
91
|
-
const
|
|
92
|
-
message: "WordPress
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
154
|
+
const mode = orCancel(await p.select({
|
|
155
|
+
message: "WordPress connection",
|
|
156
|
+
initialValue: "local",
|
|
157
|
+
options: [{
|
|
158
|
+
value: "local",
|
|
159
|
+
label: "Set up a local dev environment",
|
|
160
|
+
hint: "runs WordPress in Docker"
|
|
161
|
+
}, {
|
|
162
|
+
value: "remote",
|
|
163
|
+
label: "Use my own WordPress",
|
|
164
|
+
hint: "connect to an existing site"
|
|
165
|
+
}]
|
|
99
166
|
}));
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
167
|
+
let wpUrl = "";
|
|
168
|
+
let wpUsername = "";
|
|
169
|
+
let wpPassword = "";
|
|
170
|
+
let devPath;
|
|
171
|
+
if (mode === "remote") {
|
|
172
|
+
wpUrl = orCancel(await p.text({
|
|
173
|
+
message: "WordPress URL",
|
|
174
|
+
placeholder: "https://wp.your-app.com",
|
|
175
|
+
validate: validate(urlString)
|
|
176
|
+
}));
|
|
177
|
+
wpUsername = orCancel(await p.text({
|
|
178
|
+
message: "WordPress username",
|
|
179
|
+
validate: validate(requiredString)
|
|
180
|
+
}));
|
|
181
|
+
wpPassword = orCancel(await p.password({
|
|
182
|
+
message: "WordPress application password",
|
|
183
|
+
validate: validate(requiredString)
|
|
184
|
+
}));
|
|
185
|
+
} else devPath = orCancel(await p.text({
|
|
186
|
+
message: "Local WordPress folder",
|
|
187
|
+
initialValue: "wordpress",
|
|
188
|
+
validate: validate(dirPath)
|
|
103
189
|
}));
|
|
104
190
|
const dir = orCancel(await p.text({
|
|
105
191
|
message: "Kizlo directory",
|
|
@@ -117,11 +203,14 @@ async function collectInteractively(ctx) {
|
|
|
117
203
|
})).trim();
|
|
118
204
|
}
|
|
119
205
|
return {
|
|
206
|
+
mode,
|
|
120
207
|
baseUrl,
|
|
208
|
+
siteUrl,
|
|
121
209
|
siteSecret,
|
|
122
210
|
wpUrl,
|
|
123
211
|
wpUsername,
|
|
124
212
|
wpPassword,
|
|
213
|
+
devPath,
|
|
125
214
|
dir,
|
|
126
215
|
alias
|
|
127
216
|
};
|
|
@@ -134,11 +223,12 @@ async function collectInteractively(ctx) {
|
|
|
134
223
|
function collectFromEnv(ctx) {
|
|
135
224
|
const dir = defaultDir(ctx.hasSrcDir);
|
|
136
225
|
return {
|
|
226
|
+
mode: "remote",
|
|
137
227
|
baseUrl: process.env[ctx.preset.baseUrlEnvKey]?.trim() ?? "",
|
|
138
|
-
siteSecret: process.env.
|
|
139
|
-
wpUrl: process.env.
|
|
140
|
-
wpUsername: process.env.
|
|
141
|
-
wpPassword: process.env.
|
|
228
|
+
siteSecret: process.env.KIZLO_SITE_SECRET?.trim() || randomBytes(32).toString("hex"),
|
|
229
|
+
wpUrl: process.env.KIZLO_WORDPRESS_URL?.trim() ?? "",
|
|
230
|
+
wpUsername: process.env.KIZLO_WORDPRESS_USERNAME?.trim() ?? "",
|
|
231
|
+
wpPassword: process.env.KIZLO_WORDPRESS_APPLICATION_PASSWORD?.trim() ?? "",
|
|
142
232
|
dir,
|
|
143
233
|
alias: ctx.preset.routeHandler ? aliasWithSlash(detectImportAlias(ctx.cwd, path.join(dir, "server"))?.prefix) : ""
|
|
144
234
|
};
|
|
@@ -146,6 +236,66 @@ function collectFromEnv(ctx) {
|
|
|
146
236
|
function readPkg(pkgPath) {
|
|
147
237
|
return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
148
238
|
}
|
|
239
|
+
/** Build a {@link ResolvedDevConfig} from the chosen install folder, matching `resolveDevConfig`'s
|
|
240
|
+
* defaults — built directly so init never has to round-trip through the config file it's writing. */
|
|
241
|
+
function devConfigFor(cwd, devPath) {
|
|
242
|
+
return {
|
|
243
|
+
configDir: cwd,
|
|
244
|
+
project: `${resolveStackName(cwd)}-dev`,
|
|
245
|
+
port: DEFAULT_DEV_PORT,
|
|
246
|
+
portExplicit: false,
|
|
247
|
+
dbPort: DEFAULT_DEV_DB_PORT,
|
|
248
|
+
dbPortExplicit: false,
|
|
249
|
+
fixtures: [],
|
|
250
|
+
wordpressPath: devPath,
|
|
251
|
+
wordpressDir: path.resolve(cwd, devPath)
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Boot the local dev stack once to produce working credentials, then stop it (volumes
|
|
256
|
+
* persist, so a later `kizlo dev` resumes instantly). The dev stack mints no application
|
|
257
|
+
* password — that's a test-stack concern — so we create one here for REST auth in `.env`.
|
|
258
|
+
* While the stack is still up, push the dev site settings (`siteSecret` plus the Kizlo server's
|
|
259
|
+
* `url`/`backend_url`, derived from `baseUrl`) into the plugin so webhook signing and event delivery
|
|
260
|
+
* work.
|
|
261
|
+
*/
|
|
262
|
+
async function provisionLocalStack(cfg, siteSecret, baseUrl) {
|
|
263
|
+
await removeProjectContainers(cfg.project);
|
|
264
|
+
const port = await pickStackPort(cfg.port, {
|
|
265
|
+
fixed: cfg.portExplicit,
|
|
266
|
+
configKey: "dev.port"
|
|
267
|
+
});
|
|
268
|
+
const dbPort = await pickStackPort(cfg.dbPort, {
|
|
269
|
+
fixed: cfg.dbPortExplicit,
|
|
270
|
+
host: "127.0.0.1",
|
|
271
|
+
configKey: "dev.dbPort"
|
|
272
|
+
});
|
|
273
|
+
const ready = {
|
|
274
|
+
...cfg,
|
|
275
|
+
port,
|
|
276
|
+
dbPort
|
|
277
|
+
};
|
|
278
|
+
createStack(devStack(ready));
|
|
279
|
+
const info = await bootstrapDev(ready);
|
|
280
|
+
const appPassword = info.appPassword ?? await createAdminAppPassword("kizlo");
|
|
281
|
+
const sync = await syncSiteSettings({
|
|
282
|
+
url: info.url,
|
|
283
|
+
username: info.username,
|
|
284
|
+
password: appPassword
|
|
285
|
+
}, {
|
|
286
|
+
secret: siteSecret,
|
|
287
|
+
backendUrl: baseUrl,
|
|
288
|
+
containerized: true
|
|
289
|
+
});
|
|
290
|
+
await composeStop();
|
|
291
|
+
return {
|
|
292
|
+
url: info.url,
|
|
293
|
+
username: info.username,
|
|
294
|
+
appPassword,
|
|
295
|
+
adminPassword: info.secrets?.password,
|
|
296
|
+
secretSyncError: sync.ok ? void 0 : sync.error
|
|
297
|
+
};
|
|
298
|
+
}
|
|
149
299
|
const init = defineCommand({
|
|
150
300
|
meta: {
|
|
151
301
|
name: "init",
|
|
@@ -218,6 +368,10 @@ const init = defineCommand({
|
|
|
218
368
|
});
|
|
219
369
|
if (args.alias !== void 0) setup.alias = String(args.alias).trim();
|
|
220
370
|
setup.alias = aliasWithSlash(setup.alias);
|
|
371
|
+
if (setup.mode === "local" && !await dockerAvailable()) {
|
|
372
|
+
p.cancel("Docker isn't available — start Docker (or install it) and re-run, or choose “Use my own WordPress”.");
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
221
375
|
if (!hasKizlo) {
|
|
222
376
|
const s = p.spinner();
|
|
223
377
|
s.start(`Installing kizlo with ${pm}`);
|
|
@@ -226,14 +380,26 @@ const init = defineCommand({
|
|
|
226
380
|
if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, "kizlo").join(" ")}`);
|
|
227
381
|
}
|
|
228
382
|
if (preset.apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, preset.apiPath);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
383
|
+
let adminPassword;
|
|
384
|
+
if (setup.mode === "local" && setup.devPath) {
|
|
385
|
+
ensureGitignored(cwd, setup.devPath);
|
|
386
|
+
const s = p.spinner();
|
|
387
|
+
s.start("Setting up local WordPress (first run downloads images, this can take a while)");
|
|
388
|
+
try {
|
|
389
|
+
const local = await provisionLocalStack(devConfigFor(cwd, setup.devPath), setup.siteSecret, setup.baseUrl);
|
|
390
|
+
setup.wpUrl = local.url;
|
|
391
|
+
setup.wpUsername = local.username;
|
|
392
|
+
setup.wpPassword = local.appPassword;
|
|
393
|
+
adminPassword = local.adminPassword;
|
|
394
|
+
s.stop("Local WordPress ready");
|
|
395
|
+
if (local.secretSyncError) p.log.warn(`Could not sync KIZLO_DEV_SITE_SECRET to the local plugin (${local.secretSyncError})`);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
s.stop("Local WordPress setup failed");
|
|
398
|
+
p.cancel(error instanceof Error ? error.message : String(error));
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const { keys, values: envValues } = managedEnv(preset, setup);
|
|
237
403
|
const envPath = path.join(cwd, ".env");
|
|
238
404
|
const envExisted = fs.existsSync(envPath);
|
|
239
405
|
const existingEnv = envExisted ? fs.readFileSync(envPath, "utf8") : "";
|
|
@@ -249,11 +415,26 @@ const init = defineCommand({
|
|
|
249
415
|
initialValue: true
|
|
250
416
|
}))) overwriteKeys = /* @__PURE__ */ new Set();
|
|
251
417
|
}
|
|
252
|
-
const merge = mergeEnv(existingEnv, envValues, overwriteKeys);
|
|
418
|
+
const merge = mergeEnv(existingEnv, envValues, overwriteKeys, envGroups(preset.baseUrlEnvKey));
|
|
253
419
|
fs.writeFileSync(envPath, merge.content);
|
|
254
|
-
|
|
420
|
+
if (setup.mode === "remote" && setup.wpUrl && setup.wpUsername && setup.wpPassword) {
|
|
421
|
+
const sync = await syncSiteSettings({
|
|
422
|
+
url: setup.wpUrl,
|
|
423
|
+
username: setup.wpUsername,
|
|
424
|
+
password: setup.wpPassword
|
|
425
|
+
}, {
|
|
426
|
+
secret: setup.siteSecret,
|
|
427
|
+
siteUrl: setup.siteUrl,
|
|
428
|
+
backendUrl: setup.baseUrl
|
|
429
|
+
});
|
|
430
|
+
if (!sync.ok) p.log.warn(`Could not sync the site settings to WordPress (${sync.error}) — make sure the kizlo plugin is active, then set them from the Kizlo settings.`);
|
|
431
|
+
}
|
|
432
|
+
const exampleKeys = [preset.baseUrlEnvKey, ...PROD_WP_ENV_KEYS];
|
|
433
|
+
const { content: exampleEnv } = mergeEnv("", Object.fromEntries(exampleKeys.map((key) => [key, ""])), new Set(exampleKeys), envGroups(preset.baseUrlEnvKey));
|
|
434
|
+
const exampleBody = `${exampleEnv}\n# Point the app at a local dev stack (managed by \`kizlo dev\`) instead of the keys above:\n# KIZLO_TARGET=dev\n`;
|
|
435
|
+
const exampleCreated = writeFileIfAbsent(path.join(cwd, ".env.example"), exampleBody);
|
|
255
436
|
const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
256
|
-
const configCreated = writeFileIfAbsent(path.join(cwd, "kizlo.config.ts"), kizloConfigTemplate(dirRel, setup.alias));
|
|
437
|
+
const configCreated = writeFileIfAbsent(path.join(cwd, "kizlo.config.ts"), kizloConfigTemplate(dirRel, setup.alias, setup.devPath));
|
|
257
438
|
const serverDirRel = path.join(dirRel, "server");
|
|
258
439
|
const serverEntryRel = path.join(serverDirRel, "index.ts");
|
|
259
440
|
const serverPath = path.join(cwd, serverEntryRel);
|
|
@@ -273,10 +454,12 @@ const init = defineCommand({
|
|
|
273
454
|
const generatedDirRel = path.join(serverDirRel, "generated");
|
|
274
455
|
writeFileIfAbsent(path.join(cwd, generatedDirRel, "contract.json"), "{}\n");
|
|
275
456
|
writeFileIfAbsent(path.join(cwd, generatedDirRel, "index.ts"), CONTRACT_BARREL);
|
|
457
|
+
const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
|
|
276
458
|
const scaffold = {
|
|
277
459
|
serverDirName: path.basename(serverDirRel),
|
|
278
460
|
appDir: detectAppDir(cwd, hasSrcDir),
|
|
279
|
-
serverImport: (fromDir) => resolveModuleImport(cwd, serverDirRel, fromDir, setup.alias)
|
|
461
|
+
serverImport: (fromDir) => resolveModuleImport(cwd, serverDirRel, fromDir, setup.alias),
|
|
462
|
+
clientUrl
|
|
280
463
|
};
|
|
281
464
|
const clientRel = path.join(dirRel, "client.ts");
|
|
282
465
|
const clientCreated = writeFileIfAbsent(path.join(cwd, clientRel), preset.clientEntry(scaffold));
|
|
@@ -298,7 +481,16 @@ const init = defineCommand({
|
|
|
298
481
|
p.log.success(clientCreated ? `Created browser client (${clientRel})` : "Skipped browser client (exists)");
|
|
299
482
|
if (routeRel) p.log.success(routeCreated ? `Created API route (${routeRel})` : "Skipped API route (exists)");
|
|
300
483
|
if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
|
|
484
|
+
if (setup.mode === "local") {
|
|
485
|
+
p.log.success(`Local WordPress configured (${setup.devPath}) — .env points at ${setup.wpUrl}`);
|
|
486
|
+
if (adminPassword) p.log.warn(`wp-admin login: ${setup.wpUsername} / ${adminPassword} — save it, it's shown only once`);
|
|
487
|
+
}
|
|
301
488
|
p.note([
|
|
489
|
+
...setup.mode === "local" ? [
|
|
490
|
+
`Start your local WordPress dev stack:`,
|
|
491
|
+
` npx kizlo dev`,
|
|
492
|
+
``
|
|
493
|
+
] : [],
|
|
302
494
|
`Watch your extensions and regenerate the contract during development:`,
|
|
303
495
|
` npx kizlo watch`,
|
|
304
496
|
``,
|
|
@@ -1,13 +1,74 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { withSpinner } from "../utils.js";
|
|
1
|
+
import { createStack } from "../wp/docker.js";
|
|
2
|
+
import { isSeeded, recordedPort } from "../wp/utils.js";
|
|
4
3
|
import { log } from "../daemon/logger.js";
|
|
4
|
+
import { isFree } from "../wp/ports.js";
|
|
5
|
+
import { groupDefault, pickStackPort, withSpinner } from "../utils.js";
|
|
5
6
|
import { resolveTestConfig } from "../daemon/config.js";
|
|
7
|
+
import { testStack } from "../wp/stack.js";
|
|
6
8
|
import { runSeeds } from "../wp/setup.js";
|
|
7
9
|
import { spawn } from "node:child_process";
|
|
8
10
|
import { defineCommand } from "citty";
|
|
9
11
|
|
|
10
12
|
//#region src/cli/commands/test.ts
|
|
13
|
+
async function resolve(cwd) {
|
|
14
|
+
const cfg = await resolveTestConfig(cwd);
|
|
15
|
+
return {
|
|
16
|
+
cfg,
|
|
17
|
+
stack: createStack(testStack(cfg))
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The host port to bring the test stack up on. Unlike the dev stack (rebuilt every run), the
|
|
22
|
+
* test stack is reused and its tests connect via the URL recorded in the credentials artifact —
|
|
23
|
+
* so the port can't just be re-chosen each time:
|
|
24
|
+
*
|
|
25
|
+
* - A **running** stack keeps its live published port (fast reruns stay on the same URL).
|
|
26
|
+
* - A **warm but stopped** stack reuses the port recorded in the credentials — that URL is what
|
|
27
|
+
* tests read, so it must not move; if something else stole it meanwhile we stop and say so,
|
|
28
|
+
* rather than silently relocate.
|
|
29
|
+
* - Only a **cold** stack (never seeded, or wiped by `reset` / `--reset`) picks a fresh free port
|
|
30
|
+
* from the configured default — auto-stepping past a collision (e.g. another project's stack)
|
|
31
|
+
* unless `test.port` was set explicitly, in which case the user owns the clash.
|
|
32
|
+
*
|
|
33
|
+
* `fresh` forces the cold path so a `reset` re-picks — that's how a poisoned recorded port heals.
|
|
34
|
+
*/
|
|
35
|
+
async function resolveTestPort(cfg, stack, fresh) {
|
|
36
|
+
if (!fresh) {
|
|
37
|
+
const live = await stack.publishedPort();
|
|
38
|
+
if (live !== void 0) return live;
|
|
39
|
+
const recorded = recordedPort();
|
|
40
|
+
if (recorded !== void 0) {
|
|
41
|
+
if (await isFree(recorded)) return recorded;
|
|
42
|
+
log.error(`The test stack's port ${recorded} (from a previous run) is now held by something else.\nFree that port, or run \`kizlo test reset\` to rebuild on a fresh one.`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return pickStackPort(cfg.port, {
|
|
47
|
+
fixed: cfg.portExplicit,
|
|
48
|
+
configKey: "test.port"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Boot the test stack on a resolved host port and seed it once (idempotent — skips seeding when
|
|
53
|
+
* already seeded). Returns the port-bound stack and the port, so callers report the real URL.
|
|
54
|
+
*/
|
|
55
|
+
async function bringUp(cfg, stack, fresh = false) {
|
|
56
|
+
const port = await resolveTestPort(cfg, stack, fresh);
|
|
57
|
+
const bound = port === cfg.port ? stack : createStack(testStack({
|
|
58
|
+
...cfg,
|
|
59
|
+
port
|
|
60
|
+
}));
|
|
61
|
+
await withSpinner("Starting WordPress test stack", () => bound.composeUp(), "WordPress test stack ready");
|
|
62
|
+
if (await isSeeded()) log.info("Stack already seeded — skipping seed.");
|
|
63
|
+
else await withSpinner("Seeding WordPress", () => runSeeds({
|
|
64
|
+
port,
|
|
65
|
+
fixtures: cfg.fixtures
|
|
66
|
+
}), "WordPress seeded");
|
|
67
|
+
return {
|
|
68
|
+
stack: bound,
|
|
69
|
+
port
|
|
70
|
+
};
|
|
71
|
+
}
|
|
11
72
|
/** `<pm> test` runs the project's own test script (bun needs `run` to skip its built-in runner). */
|
|
12
73
|
function testCommand(pm) {
|
|
13
74
|
return pm === "bun" ? [
|
|
@@ -23,45 +84,83 @@ function spawnTest(command, pm) {
|
|
|
23
84
|
stdio: "inherit",
|
|
24
85
|
shell: command !== void 0 || process.platform === "win32"
|
|
25
86
|
});
|
|
26
|
-
return new Promise((
|
|
87
|
+
return new Promise((resolvePromise, reject) => {
|
|
27
88
|
child.on("error", reject);
|
|
28
|
-
child.on("close", (code) =>
|
|
89
|
+
child.on("close", (code) => resolvePromise(code ?? 0));
|
|
29
90
|
});
|
|
30
91
|
}
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
description: "
|
|
92
|
+
const runArgs = {
|
|
93
|
+
teardown: {
|
|
94
|
+
type: "boolean",
|
|
95
|
+
description: "Tear down the test stack after tests finish (default: leave it running for fast reruns)"
|
|
35
96
|
},
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
97
|
+
reset: {
|
|
98
|
+
type: "boolean",
|
|
99
|
+
description: "Wipe the database and reseed before running"
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
/** Boot (+ seed) the stack, run the project's test script, then leave it up (or tear down). */
|
|
103
|
+
async function runSuite({ args }) {
|
|
104
|
+
const { cfg, stack: probe } = await resolve(process.cwd());
|
|
105
|
+
if (args.reset) await withSpinner("Wiping WordPress database", () => probe.composeDown({ volumes: true }), "Database wiped");
|
|
106
|
+
const { stack } = await bringUp(cfg, probe, Boolean(args.reset));
|
|
107
|
+
log.info(cfg.command ? `Running: ${cfg.command}` : `Running: ${testCommand(cfg.packageManager).join(" ")}`);
|
|
108
|
+
let code = 1;
|
|
109
|
+
try {
|
|
110
|
+
code = await spawnTest(cfg.command, cfg.packageManager);
|
|
111
|
+
} finally {
|
|
112
|
+
if (args.teardown) await withSpinner("Tearing down WordPress test stack", () => stack.composeDown(), "WordPress test stack stopped");
|
|
113
|
+
else log.info("Stack left running for fast reruns — `kizlo test stop` to stop it, or rerun with --teardown.");
|
|
114
|
+
}
|
|
115
|
+
process.exit(code);
|
|
116
|
+
}
|
|
117
|
+
const subCommands = {
|
|
118
|
+
stop: defineCommand({
|
|
119
|
+
meta: {
|
|
120
|
+
name: "stop",
|
|
121
|
+
description: "Stop the WordPress test stack, keeping the database volume"
|
|
122
|
+
},
|
|
123
|
+
async run() {
|
|
124
|
+
const { stack } = await resolve(process.cwd());
|
|
125
|
+
await withSpinner("Stopping WordPress test stack", () => stack.composeStop(), "Stack stopped (volumes kept)");
|
|
126
|
+
}
|
|
127
|
+
}),
|
|
128
|
+
down: defineCommand({
|
|
129
|
+
meta: {
|
|
130
|
+
name: "down",
|
|
131
|
+
description: "Stop and remove the test stack containers"
|
|
40
132
|
},
|
|
41
|
-
|
|
133
|
+
args: { volumes: {
|
|
42
134
|
type: "boolean",
|
|
43
|
-
|
|
135
|
+
alias: "v",
|
|
136
|
+
description: "Also drop the database volume"
|
|
137
|
+
} },
|
|
138
|
+
async run({ args }) {
|
|
139
|
+
const { stack } = await resolve(process.cwd());
|
|
140
|
+
await withSpinner("Removing WordPress test stack", () => stack.composeDown({ volumes: args.volumes }), args.volumes ? "Stack removed (volumes dropped)" : "Stack removed (volumes kept)");
|
|
44
141
|
}
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
let code = 1;
|
|
57
|
-
try {
|
|
58
|
-
code = await spawnTest(cfg.command, cfg.packageManager);
|
|
59
|
-
} finally {
|
|
60
|
-
if (args.teardown) await withSpinner("Tearing down WordPress test stack", () => composeDown(), "WordPress test stack stopped");
|
|
61
|
-
else log.info("Stack left running for fast reruns — `kizlo wp stop` to stop it, or rerun with --teardown.");
|
|
142
|
+
}),
|
|
143
|
+
reset: defineCommand({
|
|
144
|
+
meta: {
|
|
145
|
+
name: "reset",
|
|
146
|
+
description: "Wipe the database and reseed a fresh WordPress test stack"
|
|
147
|
+
},
|
|
148
|
+
async run() {
|
|
149
|
+
const { cfg, stack } = await resolve(process.cwd());
|
|
150
|
+
await withSpinner("Wiping WordPress database", () => stack.composeDown({ volumes: true }), "Database wiped");
|
|
151
|
+
const { port } = await bringUp(cfg, stack, true);
|
|
152
|
+
log.success(`Stack ready on http://localhost:${port} — credentials at ${cfg.credentialsPath}`);
|
|
62
153
|
}
|
|
63
|
-
|
|
64
|
-
|
|
154
|
+
})
|
|
155
|
+
};
|
|
156
|
+
const test = defineCommand({
|
|
157
|
+
meta: {
|
|
158
|
+
name: "test",
|
|
159
|
+
description: "Manage the WordPress test stack and run tests (stop | down | reset)"
|
|
160
|
+
},
|
|
161
|
+
args: runArgs,
|
|
162
|
+
subCommands,
|
|
163
|
+
run: groupDefault(Object.keys(subCommands), runSuite)
|
|
65
164
|
});
|
|
66
165
|
|
|
67
166
|
//#endregion
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { getVersion } from "../utils.js";
|
|
2
1
|
import { log } from "../daemon/logger.js";
|
|
2
|
+
import { getVersion } from "../utils.js";
|
|
3
|
+
import { printBanner } from "../banner.js";
|
|
3
4
|
import { resolveConfig } from "../daemon/config.js";
|
|
4
5
|
import { generateOnce } from "../daemon/generate.js";
|
|
5
|
-
import { printBanner } from "../banner.js";
|
|
6
6
|
import { acquire, isLocked, lockPath, release } from "../daemon/lock.js";
|
|
7
7
|
import { watch as watch$1 } from "../daemon/watch.js";
|
|
8
8
|
import { defineCommand } from "citty";
|