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
|
@@ -1,6 +1,13 @@
|
|
|
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 { CONTRACT_BARREL } from "../daemon/generate.js";
|
|
6
|
+
import { createAdminAppPassword } from "../wp/bootstrap.js";
|
|
7
|
+
import { bootstrapDev } from "../wp/dev.js";
|
|
8
|
+
import { removeProjectContainers } from "../wp/session.js";
|
|
9
|
+
import { syncSiteSettings } from "../wp/settings.js";
|
|
10
|
+
import { devStack } from "../wp/stack.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";
|
|
@@ -10,14 +17,48 @@ 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,22 +368,38 @@ 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}`);
|
|
224
|
-
const ok = runCommand(addDependencyArgs(pm, "kizlo"), cwd, "ignore");
|
|
378
|
+
const ok = runCommand(addDependencyArgs(pm, "kizlo@latest"), cwd, "ignore");
|
|
225
379
|
s.stop(ok ? "Installed kizlo" : "Could not install kizlo automatically");
|
|
226
|
-
if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, "kizlo").join(" ")}`);
|
|
380
|
+
if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, "kizlo@latest").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,10 +481,20 @@ 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([
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
489
|
+
...setup.mode === "local" ? [
|
|
490
|
+
`Start your local WordPress dev stack (also watches your extensions):`,
|
|
491
|
+
` npx kizlo dev`,
|
|
492
|
+
``
|
|
493
|
+
] : [
|
|
494
|
+
`Watch your extensions and regenerate the contract during development:`,
|
|
495
|
+
` npx kizlo watch`,
|
|
496
|
+
``
|
|
497
|
+
],
|
|
305
498
|
`Generate the contract once for production builds:`,
|
|
306
499
|
` npx kizlo generate`
|
|
307
500
|
].join("\n"), "Next steps");
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createStack } from "../wp/docker.js";
|
|
2
|
-
import { isSeeded } from "../wp/utils.js";
|
|
3
|
-
import { groupDefault, withSpinner } from "../utils.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";
|
|
6
7
|
import { testStack } from "../wp/stack.js";
|
|
7
8
|
import { runSeeds } from "../wp/setup.js";
|
|
@@ -16,18 +17,57 @@ async function resolve(cwd) {
|
|
|
16
17
|
stack: createStack(testStack(cfg))
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
}
|
|
25
45
|
}
|
|
26
|
-
|
|
27
|
-
|
|
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,
|
|
28
65
|
fixtures: cfg.fixtures
|
|
29
66
|
}), "WordPress seeded");
|
|
30
|
-
|
|
67
|
+
return {
|
|
68
|
+
stack: bound,
|
|
69
|
+
port
|
|
70
|
+
};
|
|
31
71
|
}
|
|
32
72
|
/** `<pm> test` runs the project's own test script (bun needs `run` to skip its built-in runner). */
|
|
33
73
|
function testCommand(pm) {
|
|
@@ -61,13 +101,9 @@ const runArgs = {
|
|
|
61
101
|
};
|
|
62
102
|
/** Boot (+ seed) the stack, run the project's test script, then leave it up (or tear down). */
|
|
63
103
|
async function runSuite({ args }) {
|
|
64
|
-
const { cfg, stack } = await resolve(process.cwd());
|
|
65
|
-
if (args.reset) await withSpinner("Wiping WordPress database", () =>
|
|
66
|
-
|
|
67
|
-
if (!await isSeeded()) await withSpinner("Seeding WordPress", () => runSeeds({
|
|
68
|
-
port: cfg.port,
|
|
69
|
-
fixtures: cfg.fixtures
|
|
70
|
-
}), "WordPress seeded");
|
|
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));
|
|
71
107
|
log.info(cfg.command ? `Running: ${cfg.command}` : `Running: ${testCommand(cfg.packageManager).join(" ")}`);
|
|
72
108
|
let code = 1;
|
|
73
109
|
try {
|
|
@@ -112,7 +148,8 @@ const subCommands = {
|
|
|
112
148
|
async run() {
|
|
113
149
|
const { cfg, stack } = await resolve(process.cwd());
|
|
114
150
|
await withSpinner("Wiping WordPress database", () => stack.composeDown({ volumes: true }), "Database wiped");
|
|
115
|
-
await bringUp(cfg, stack);
|
|
151
|
+
const { port } = await bringUp(cfg, stack, true);
|
|
152
|
+
log.success(`Stack ready on http://localhost:${port} — credentials at ${cfg.credentialsPath}`);
|
|
116
153
|
}
|
|
117
154
|
})
|
|
118
155
|
};
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { getVersion } from "../utils.js";
|
|
2
|
-
import { log } from "../daemon/logger.js";
|
|
3
|
-
import { resolveConfig } from "../daemon/config.js";
|
|
4
|
-
import { generateOnce } from "../daemon/generate.js";
|
|
5
2
|
import { printBanner } from "../banner.js";
|
|
6
|
-
import {
|
|
7
|
-
import { watch as watch$1 } from "../daemon/watch.js";
|
|
3
|
+
import { startWatcher } from "../daemon/watch.js";
|
|
8
4
|
import { defineCommand } from "citty";
|
|
9
5
|
|
|
10
6
|
//#region src/cli/commands/watch.ts
|
|
@@ -13,32 +9,18 @@ const args = { dir: {
|
|
|
13
9
|
description: "Override the Kizlo directory (defaults to kizlo.config.ts)"
|
|
14
10
|
} };
|
|
15
11
|
async function run({ args: args$1 }) {
|
|
16
|
-
const cwd = process.cwd();
|
|
17
|
-
const lock = lockPath(cwd);
|
|
18
|
-
if (isLocked(lock)) {
|
|
19
|
-
log.info("Watcher already running — nothing to do.");
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
12
|
printBanner(getVersion());
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
process.on("exit", cleanup);
|
|
13
|
+
const stop = await startWatcher(process.cwd(), { dir: args$1.dir });
|
|
14
|
+
if (!stop) return;
|
|
15
|
+
process.on("exit", stop);
|
|
27
16
|
for (const signal of [
|
|
28
17
|
"SIGINT",
|
|
29
18
|
"SIGTERM",
|
|
30
19
|
"SIGHUP"
|
|
31
20
|
]) process.on(signal, () => {
|
|
32
|
-
|
|
21
|
+
stop();
|
|
33
22
|
process.exit(0);
|
|
34
23
|
});
|
|
35
|
-
try {
|
|
36
|
-
if (await generateOnce(cfg)) log.success("Contract generated");
|
|
37
|
-
else log.warn(`No Kizlo server found in ${cfg.serverEntry}`);
|
|
38
|
-
} catch (error) {
|
|
39
|
-
log.error("Failed to generate the Kizlo contract:", error);
|
|
40
|
-
}
|
|
41
|
-
await watch$1(cfg);
|
|
42
24
|
}
|
|
43
25
|
const watch = defineCommand({
|
|
44
26
|
meta: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { credentialsPath, findConfigDir } from "../wp/utils.js";
|
|
2
|
-
import { detectPackageManager } from "../utils.js";
|
|
3
2
|
import { log } from "./logger.js";
|
|
3
|
+
import { detectPackageManager } from "../utils.js";
|
|
4
4
|
import z from "zod/v4";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
@@ -37,6 +37,9 @@ const CONFIG_FILES = [
|
|
|
37
37
|
"kizlo.config.js",
|
|
38
38
|
"kizlo.config.mjs"
|
|
39
39
|
];
|
|
40
|
+
const DEFAULT_DEV_PORT = 8080;
|
|
41
|
+
const DEFAULT_DEV_DB_PORT = 3307;
|
|
42
|
+
const DEFAULT_TEST_PORT = 8889;
|
|
40
43
|
function defaultDir(cwd) {
|
|
41
44
|
return fs.existsSync(path.join(cwd, "src")) ? "src/lib/kizlo" : "lib/kizlo";
|
|
42
45
|
}
|
|
@@ -107,7 +110,8 @@ async function resolveTestConfig(cwd) {
|
|
|
107
110
|
configDir,
|
|
108
111
|
project: `${resolveStackName(configDir, fileConfig?.name)}-test`,
|
|
109
112
|
command: test.command,
|
|
110
|
-
port: test.port ??
|
|
113
|
+
port: test.port ?? DEFAULT_TEST_PORT,
|
|
114
|
+
portExplicit: test.port !== void 0,
|
|
111
115
|
fixtures: test.fixtures ?? [],
|
|
112
116
|
credentialsPath: credentialsPath(cwd),
|
|
113
117
|
packageManager: test.packageManager ?? detectPackageManager(configDir)
|
|
@@ -134,8 +138,10 @@ async function resolveDevConfig(cwd) {
|
|
|
134
138
|
return {
|
|
135
139
|
configDir,
|
|
136
140
|
project: `${resolveStackName(configDir, fileConfig?.name)}-dev`,
|
|
137
|
-
port: dev.port ??
|
|
138
|
-
|
|
141
|
+
port: dev.port ?? DEFAULT_DEV_PORT,
|
|
142
|
+
portExplicit: dev.port !== void 0,
|
|
143
|
+
dbPort: dev.dbPort ?? DEFAULT_DEV_DB_PORT,
|
|
144
|
+
dbPortExplicit: dev.dbPort !== void 0,
|
|
139
145
|
byo: dev.byo ? path.resolve(configDir, dev.byo) : void 0,
|
|
140
146
|
fixtures: dev.fixtures ?? [],
|
|
141
147
|
wordpressPath: dev.path,
|
|
@@ -144,4 +150,4 @@ async function resolveDevConfig(cwd) {
|
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
//#endregion
|
|
147
|
-
export { resolveConfig, resolveDevConfig, resolveTestConfig };
|
|
153
|
+
export { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveConfig, resolveDevConfig, resolveStackName, resolveTestConfig };
|
package/dist/cli/daemon/watch.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { log } from "./logger.js";
|
|
2
|
+
import { resolveConfig } from "./config.js";
|
|
2
3
|
import { generateOnce } from "./generate.js";
|
|
4
|
+
import { acquire, isLocked, lockPath, release } from "./lock.js";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import { FSWatcher } from "chokidar";
|
|
5
7
|
|
|
@@ -28,10 +30,41 @@ async function watch(cfg) {
|
|
|
28
30
|
});
|
|
29
31
|
const onChange = debounce(() => regenerate(cfg), 300);
|
|
30
32
|
watcher.add(path.resolve(cfg.cwd, cfg.serverDir));
|
|
31
|
-
watcher.on("ready", () => log.start("Watching for changes..."));
|
|
33
|
+
watcher.on("ready", () => log.start("Watching for contract changes..."));
|
|
32
34
|
watcher.on("all", () => void onChange());
|
|
33
35
|
return watcher;
|
|
34
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Acquire the single-instance lock, generate the contract once, and start the file
|
|
39
|
+
* watcher. Returns a synchronous `stop()` that closes the watcher and releases the
|
|
40
|
+
* lock — or `undefined` when another watcher already holds the lock (a standalone
|
|
41
|
+
* `kizlo watch`, or a framework dev script), in which case the caller carries on
|
|
42
|
+
* without watching. Shared by the standalone `kizlo watch` command and the
|
|
43
|
+
* folded-in watcher of `kizlo dev`, so a single terminal covers both.
|
|
44
|
+
*/
|
|
45
|
+
async function startWatcher(cwd, opts) {
|
|
46
|
+
const lock = lockPath(cwd);
|
|
47
|
+
if (isLocked(lock)) {
|
|
48
|
+
log.info("Watcher already running — skipping the contract watcher.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const cfg = await resolveConfig(cwd, { dir: opts?.dir });
|
|
52
|
+
acquire(lock);
|
|
53
|
+
try {
|
|
54
|
+
if (await generateOnce(cfg)) log.success("Contract generated");
|
|
55
|
+
else log.warn(`No Kizlo server found in ${cfg.serverEntry}`);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
log.error("Failed to generate the Kizlo contract:", error);
|
|
58
|
+
}
|
|
59
|
+
const watcher = await watch(cfg);
|
|
60
|
+
let stopped = false;
|
|
61
|
+
return () => {
|
|
62
|
+
if (stopped) return;
|
|
63
|
+
stopped = true;
|
|
64
|
+
release(lock);
|
|
65
|
+
watcher.close();
|
|
66
|
+
};
|
|
67
|
+
}
|
|
35
68
|
|
|
36
69
|
//#endregion
|
|
37
|
-
export {
|
|
70
|
+
export { startWatcher };
|