kizlo 0.1.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/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/adapters/auth.d.ts +18 -0
- package/dist/adapters/auth.js +15 -0
- package/dist/adapters/captcha.d.ts +28 -0
- package/dist/adapters/captcha.js +86 -0
- package/dist/adapters/geo.d.ts +18 -0
- package/dist/adapters/geo.js +18 -0
- package/dist/adapters/geo.vercel.d.ts +6 -0
- package/dist/adapters/geo.vercel.js +41 -0
- package/dist/adapters/logger.d.ts +31 -0
- package/dist/adapters/logger.js +30 -0
- package/dist/cli/banner.js +26 -0
- package/dist/cli/commands/generate.js +34 -0
- package/dist/cli/commands/init.js +313 -0
- package/dist/cli/commands/test.js +68 -0
- package/dist/cli/commands/watch.js +53 -0
- package/dist/cli/commands/wp.js +66 -0
- package/dist/cli/daemon/config.js +87 -0
- package/dist/cli/daemon/generate.js +40 -0
- package/dist/cli/daemon/lock.js +31 -0
- package/dist/cli/daemon/logger.js +8 -0
- package/dist/cli/daemon/watch.js +37 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +22 -0
- package/dist/cli/presets/base.js +23 -0
- package/dist/cli/presets/index.js +16 -0
- package/dist/cli/presets/nextjs.js +39 -0
- package/dist/cli/utils.js +182 -0
- package/dist/cli/wp/bootstrap.js +114 -0
- package/dist/cli/wp/compose/docker-compose.yml +73 -0
- package/dist/cli/wp/constants.js +52 -0
- package/dist/cli/wp/docker.d.ts +24 -0
- package/dist/cli/wp/docker.js +69 -0
- package/dist/cli/wp/setup.js +39 -0
- package/dist/cli/wp/types.d.ts +72 -0
- package/dist/cli/wp/types.js +8 -0
- package/dist/cli/wp/utils.d.ts +39 -0
- package/dist/cli/wp/utils.js +105 -0
- package/dist/client.d.ts +24 -0
- package/dist/client.js +55 -0
- package/dist/comment/errors.js +47 -0
- package/dist/comment/index.js +138 -0
- package/dist/comment/schema.js +72 -0
- package/dist/comment/utils.js +35 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.js +7 -0
- package/dist/context.d.ts +73 -0
- package/dist/context.js +145 -0
- package/dist/cookie.d.ts +19 -0
- package/dist/cookie.js +25 -0
- package/dist/email/service.d.ts +11 -0
- package/dist/email/service.interface.d.ts +8 -0
- package/dist/email/service.js +20 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +31 -0
- package/dist/integrations/nextjs/kizlo.client.d.ts +10 -0
- package/dist/integrations/nextjs/kizlo.client.js +13 -0
- package/dist/integrations/nextjs/kizlo.server.d.ts +12 -0
- package/dist/integrations/nextjs/kizlo.server.js +13 -0
- package/dist/integrations/nextjs/utils.js +11 -0
- package/dist/kizlo.d.ts +73 -0
- package/dist/kizlo.js +144 -0
- package/dist/menu/errors.js +16 -0
- package/dist/menu/fixture.d.ts +8 -0
- package/dist/menu/fixture.js +62 -0
- package/dist/menu/index.js +70 -0
- package/dist/menu/schema.js +70 -0
- package/dist/menu/utils.js +57 -0
- package/dist/nextjs/server.d.ts +2 -0
- package/dist/nextjs/server.js +3 -0
- package/dist/nextjs.d.ts +2 -0
- package/dist/nextjs.js +3 -0
- package/dist/package.js +5 -0
- package/dist/post/error.js +30 -0
- package/dist/post/fixture.d.ts +8 -0
- package/dist/post/fixture.js +71 -0
- package/dist/post/index.js +123 -0
- package/dist/post/schema.js +120 -0
- package/dist/post/utils.js +42 -0
- package/dist/post-type/service.js +48 -0
- package/dist/router.d.ts +624 -0
- package/dist/router.js +15 -0
- package/dist/seo/index.js +54 -0
- package/dist/seo/schema.d.ts +196 -0
- package/dist/seo/schema.js +96 -0
- package/dist/seo/types.d.ts +80 -0
- package/dist/seo/utils.js +44 -0
- package/dist/service.d.ts +17 -0
- package/dist/service.js +17 -0
- package/dist/shared/constants.js +10 -0
- package/dist/shared/contract.d.ts +21 -0
- package/dist/shared/contract.js +43 -0
- package/dist/shared/crypto.d.ts +28 -0
- package/dist/shared/crypto.js +34 -0
- package/dist/shared/error.d.ts +128 -0
- package/dist/shared/error.js +147 -0
- package/dist/shared/extension.d.ts +25 -0
- package/dist/shared/extension.js +7 -0
- package/dist/shared/identifier.d.ts +11 -0
- package/dist/shared/identifier.js +26 -0
- package/dist/shared/middleware.d.ts +29 -0
- package/dist/shared/procedure.d.ts +114 -0
- package/dist/shared/procedure.js +114 -0
- package/dist/shared/result.d.ts +24 -0
- package/dist/shared/result.js +49 -0
- package/dist/shared/router.d.ts +7 -0
- package/dist/shared/router.js +26 -0
- package/dist/shared/schema.d.ts +55 -0
- package/dist/shared/schema.js +43 -0
- package/dist/shared/serialize.d.ts +12 -0
- package/dist/shared/serialize.js +51 -0
- package/dist/shared/types.d.ts +35 -0
- package/dist/shared/utils.js +17 -0
- package/dist/test/auth.d.ts +7 -0
- package/dist/test/auth.js +15 -0
- package/dist/test/captcha.d.ts +7 -0
- package/dist/test/captcha.js +8 -0
- package/dist/test/client.d.ts +11 -0
- package/dist/test/client.js +14 -0
- package/dist/test/fixtures.d.ts +14 -0
- package/dist/test/fixtures.js +13 -0
- package/dist/test/server.d.ts +10 -0
- package/dist/test/server.js +32 -0
- package/dist/test/users.d.ts +14 -0
- package/dist/test/users.js +17 -0
- package/dist/test.d.ts +12 -0
- package/dist/test.js +13 -0
- package/dist/webhook/index.d.ts +38 -0
- package/dist/webhook/index.js +44 -0
- package/dist/webhook/schema.d.ts +45 -0
- package/dist/webhook/schema.js +72 -0
- package/dist/webhook/utils.js +21 -0
- package/dist/wordpress/comment/service.d.ts +28 -0
- package/dist/wordpress/comment/service.js +55 -0
- package/dist/wordpress/comment/types.d.ts +206 -0
- package/dist/wordpress/constants.d.ts +14 -0
- package/dist/wordpress/constants.js +13 -0
- package/dist/wordpress/error.d.ts +9 -0
- package/dist/wordpress/error.js +12 -0
- package/dist/wordpress/index.d.ts +15 -0
- package/dist/wordpress/menu/items/service.d.ts +28 -0
- package/dist/wordpress/menu/items/service.js +55 -0
- package/dist/wordpress/menu/items/types.d.ts +214 -0
- package/dist/wordpress/menu/service.d.ts +30 -0
- package/dist/wordpress/menu/service.js +58 -0
- package/dist/wordpress/menu/types.d.ts +115 -0
- package/dist/wordpress/post/service.d.ts +28 -0
- package/dist/wordpress/post/service.js +55 -0
- package/dist/wordpress/post/types.d.ts +290 -0
- package/dist/wordpress/service.d.ts +77 -0
- package/dist/wordpress/service.js +180 -0
- package/dist/wordpress/types.d.ts +67 -0
- package/dist/wordpress/user/service.d.ts +28 -0
- package/dist/wordpress/user/service.js +55 -0
- package/dist/wordpress/user/types.d.ts +221 -0
- package/dist/wordpress/utils.d.ts +7 -0
- package/dist/wordpress/utils.js +9 -0
- package/package.json +109 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, envKeysPresent, getVersion, loadEnvFiles, mergeEnv, resolveModuleImport, runCommand, writeFileIfAbsent } from "../utils.js";
|
|
2
|
+
import { CONTRACT_BARREL } from "../daemon/generate.js";
|
|
3
|
+
import { printBanner } from "../banner.js";
|
|
4
|
+
import { PRESETS, detectPreset, getPreset } from "../presets/index.js";
|
|
5
|
+
import z from "zod/v4";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import { defineCommand } from "citty";
|
|
10
|
+
import * as p from "@clack/prompts";
|
|
11
|
+
|
|
12
|
+
//#region src/cli/commands/init.ts
|
|
13
|
+
const WP_ENV_KEYS = [
|
|
14
|
+
"SITE_SECRET",
|
|
15
|
+
"WORDPRESS_URL",
|
|
16
|
+
"WORDPRESS_USERNAME",
|
|
17
|
+
"WORDPRESS_APPLICATION_PASSWORD"
|
|
18
|
+
];
|
|
19
|
+
function envKeys(preset) {
|
|
20
|
+
return [preset.baseUrlEnvKey, ...WP_ENV_KEYS];
|
|
21
|
+
}
|
|
22
|
+
function defaultDir(hasSrcDir) {
|
|
23
|
+
return hasSrcDir ? "src/lib/kizlo" : "lib/kizlo";
|
|
24
|
+
}
|
|
25
|
+
/** Normalizes an alias prefix to the familiar `@/` form (or empty for relative). */
|
|
26
|
+
function aliasWithSlash(alias) {
|
|
27
|
+
return alias ? `${alias.replace(/\/+$/, "")}/` : "";
|
|
28
|
+
}
|
|
29
|
+
function kizloConfigTemplate(dir, alias) {
|
|
30
|
+
return `import { defineConfig } from "kizlo/config"
|
|
31
|
+
|
|
32
|
+
export default defineConfig({
|
|
33
|
+
dir: "${dir}",${alias ? `\n\talias: "${alias}",` : ""}
|
|
34
|
+
})
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
/** Appends the API path to the base URL so the client and route handler agree. */
|
|
38
|
+
function withApiPath(baseUrl, apiPath) {
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(baseUrl);
|
|
41
|
+
const current = url.pathname.replace(/\/+$/, "");
|
|
42
|
+
if (current.endsWith(apiPath)) return url.toString().replace(/\/+$/, "");
|
|
43
|
+
url.pathname = `${current}${apiPath}`;
|
|
44
|
+
return url.toString().replace(/\/+$/, "");
|
|
45
|
+
} catch {
|
|
46
|
+
return baseUrl;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function detectAppDir(cwd, hasSrcDir) {
|
|
50
|
+
if (fs.existsSync(path.join(cwd, "src", "app"))) return "src/app";
|
|
51
|
+
if (fs.existsSync(path.join(cwd, "app"))) return "app";
|
|
52
|
+
return hasSrcDir ? "src/app" : "app";
|
|
53
|
+
}
|
|
54
|
+
const requiredString = z.string().trim().min(1, "Required");
|
|
55
|
+
const urlString = requiredString.pipe(z.url("Must be a valid URL (e.g. https://example.com)"));
|
|
56
|
+
const dirPath = requiredString.refine((value) => !value.endsWith(".ts"), "Enter a directory, not a file");
|
|
57
|
+
function validate(schema) {
|
|
58
|
+
return (value) => {
|
|
59
|
+
const result = schema.safeParse(value ?? "");
|
|
60
|
+
return result.success ? void 0 : result.error.issues[0]?.message;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function orCancel(value) {
|
|
64
|
+
if (p.isCancel(value)) {
|
|
65
|
+
p.cancel("Setup cancelled.");
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
async function collectInteractively(ctx) {
|
|
71
|
+
const baseUrl = orCancel(await p.text({
|
|
72
|
+
message: "Public server base URL",
|
|
73
|
+
placeholder: "https://your-app.com",
|
|
74
|
+
validate: validate(urlString)
|
|
75
|
+
}));
|
|
76
|
+
const siteSecret = orCancel(await p.select({
|
|
77
|
+
message: "SITE_SECRET (signing key)",
|
|
78
|
+
initialValue: "generate",
|
|
79
|
+
options: [{
|
|
80
|
+
value: "generate",
|
|
81
|
+
label: "Generate a secure secret automatically",
|
|
82
|
+
hint: "recommended"
|
|
83
|
+
}, {
|
|
84
|
+
value: "enter",
|
|
85
|
+
label: "Enter my own"
|
|
86
|
+
}]
|
|
87
|
+
})) === "enter" ? orCancel(await p.password({
|
|
88
|
+
message: "Enter SITE_SECRET",
|
|
89
|
+
validate: validate(requiredString)
|
|
90
|
+
})) : randomBytes(32).toString("hex");
|
|
91
|
+
const wpUrl = orCancel(await p.text({
|
|
92
|
+
message: "WordPress URL",
|
|
93
|
+
placeholder: "https://wp.your-app.com",
|
|
94
|
+
validate: validate(urlString)
|
|
95
|
+
}));
|
|
96
|
+
const wpUsername = orCancel(await p.text({
|
|
97
|
+
message: "WordPress username",
|
|
98
|
+
validate: validate(requiredString)
|
|
99
|
+
}));
|
|
100
|
+
const wpPassword = orCancel(await p.password({
|
|
101
|
+
message: "WordPress application password",
|
|
102
|
+
validate: validate(requiredString)
|
|
103
|
+
}));
|
|
104
|
+
const dir = orCancel(await p.text({
|
|
105
|
+
message: "Kizlo directory",
|
|
106
|
+
initialValue: defaultDir(ctx.hasSrcDir),
|
|
107
|
+
validate: validate(dirPath)
|
|
108
|
+
}));
|
|
109
|
+
let alias = "";
|
|
110
|
+
if (ctx.preset.routeHandler) {
|
|
111
|
+
const serverDir = path.join(dir.replace(/^\.\//, "").replace(/\/+$/, ""), "server");
|
|
112
|
+
const detected = detectImportAlias(ctx.cwd, serverDir)?.prefix;
|
|
113
|
+
alias = orCancel(await p.text({
|
|
114
|
+
message: "Import alias (blank for relative imports)",
|
|
115
|
+
placeholder: "@/",
|
|
116
|
+
initialValue: detected ? `${detected}/` : ""
|
|
117
|
+
})).trim();
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
baseUrl,
|
|
121
|
+
siteSecret,
|
|
122
|
+
wpUrl,
|
|
123
|
+
wpUsername,
|
|
124
|
+
wpPassword,
|
|
125
|
+
dir,
|
|
126
|
+
alias
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Non-interactive setup: skip prompts and use defaults. Values present in the
|
|
131
|
+
* environment are used; missing ones are left empty for the user to fill in
|
|
132
|
+
* later. Never fails — `--yes` always scaffolds a fillable project.
|
|
133
|
+
*/
|
|
134
|
+
function collectFromEnv(ctx) {
|
|
135
|
+
const dir = defaultDir(ctx.hasSrcDir);
|
|
136
|
+
return {
|
|
137
|
+
baseUrl: process.env[ctx.preset.baseUrlEnvKey]?.trim() ?? "",
|
|
138
|
+
siteSecret: process.env.SITE_SECRET?.trim() || randomBytes(32).toString("hex"),
|
|
139
|
+
wpUrl: process.env.WORDPRESS_URL?.trim() ?? "",
|
|
140
|
+
wpUsername: process.env.WORDPRESS_USERNAME?.trim() ?? "",
|
|
141
|
+
wpPassword: process.env.WORDPRESS_APPLICATION_PASSWORD?.trim() ?? "",
|
|
142
|
+
dir,
|
|
143
|
+
alias: ctx.preset.routeHandler ? aliasWithSlash(detectImportAlias(ctx.cwd, path.join(dir, "server"))?.prefix) : ""
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function readPkg(pkgPath) {
|
|
147
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
148
|
+
}
|
|
149
|
+
const init = defineCommand({
|
|
150
|
+
meta: {
|
|
151
|
+
name: "init",
|
|
152
|
+
description: "Set up Kizlo in the current project"
|
|
153
|
+
},
|
|
154
|
+
args: {
|
|
155
|
+
yes: {
|
|
156
|
+
type: "boolean",
|
|
157
|
+
alias: "y",
|
|
158
|
+
description: "Skip prompts and scaffold with defaults (non-interactive)",
|
|
159
|
+
default: false
|
|
160
|
+
},
|
|
161
|
+
force: {
|
|
162
|
+
type: "boolean",
|
|
163
|
+
alias: "f",
|
|
164
|
+
description: "Overwrite existing .env values and server entry without asking",
|
|
165
|
+
default: false
|
|
166
|
+
},
|
|
167
|
+
preset: {
|
|
168
|
+
type: "string",
|
|
169
|
+
description: `Force a setup preset (${PRESETS.map((preset) => preset.id).join(", ")})`
|
|
170
|
+
},
|
|
171
|
+
alias: {
|
|
172
|
+
type: "string",
|
|
173
|
+
description: "Import alias prefix for generated imports (e.g. @); blank for relative"
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
async run({ args }) {
|
|
177
|
+
const yes = Boolean(args.yes);
|
|
178
|
+
const force = Boolean(args.force);
|
|
179
|
+
const cwd = process.cwd();
|
|
180
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
181
|
+
if (!yes) printBanner(getVersion());
|
|
182
|
+
p.intro("kizlo init");
|
|
183
|
+
if (!fs.existsSync(pkgPath)) {
|
|
184
|
+
p.cancel("No package.json found — run `kizlo init` inside a project.");
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const pkg = readPkg(pkgPath);
|
|
188
|
+
const pm = detectPackageManager(cwd);
|
|
189
|
+
const hasKizlo = Boolean(pkg.dependencies?.kizlo) || Boolean(pkg.devDependencies?.kizlo);
|
|
190
|
+
const hasSrcDir = fs.existsSync(path.join(cwd, "src"));
|
|
191
|
+
const initCtx = {
|
|
192
|
+
cwd,
|
|
193
|
+
pkg,
|
|
194
|
+
pm,
|
|
195
|
+
hasSrcDir
|
|
196
|
+
};
|
|
197
|
+
let preset;
|
|
198
|
+
if (args.preset) {
|
|
199
|
+
const chosen = getPreset(String(args.preset));
|
|
200
|
+
if (!chosen) {
|
|
201
|
+
p.cancel(`Unknown preset "${args.preset}". Available: ${PRESETS.map((pr) => pr.id).join(", ")}`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
preset = chosen;
|
|
205
|
+
} else {
|
|
206
|
+
preset = detectPreset(initCtx);
|
|
207
|
+
if (preset.id !== "base") p.log.success(`${preset.label} detected`);
|
|
208
|
+
}
|
|
209
|
+
if (yes) loadEnvFiles(cwd);
|
|
210
|
+
const setup = yes ? collectFromEnv({
|
|
211
|
+
cwd,
|
|
212
|
+
hasSrcDir,
|
|
213
|
+
preset
|
|
214
|
+
}) : await collectInteractively({
|
|
215
|
+
cwd,
|
|
216
|
+
hasSrcDir,
|
|
217
|
+
preset
|
|
218
|
+
});
|
|
219
|
+
if (args.alias !== void 0) setup.alias = String(args.alias).trim();
|
|
220
|
+
setup.alias = aliasWithSlash(setup.alias);
|
|
221
|
+
if (!hasKizlo) {
|
|
222
|
+
const s = p.spinner();
|
|
223
|
+
s.start(`Installing kizlo with ${pm}`);
|
|
224
|
+
const ok = runCommand(addDependencyArgs(pm, "kizlo"), cwd, "ignore");
|
|
225
|
+
s.stop(ok ? "Installed kizlo" : "Could not install kizlo automatically");
|
|
226
|
+
if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, "kizlo").join(" ")}`);
|
|
227
|
+
}
|
|
228
|
+
if (preset.apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, preset.apiPath);
|
|
229
|
+
const keys = envKeys(preset);
|
|
230
|
+
const envValues = {
|
|
231
|
+
[preset.baseUrlEnvKey]: setup.baseUrl,
|
|
232
|
+
SITE_SECRET: setup.siteSecret,
|
|
233
|
+
WORDPRESS_URL: setup.wpUrl,
|
|
234
|
+
WORDPRESS_USERNAME: setup.wpUsername,
|
|
235
|
+
WORDPRESS_APPLICATION_PASSWORD: setup.wpPassword
|
|
236
|
+
};
|
|
237
|
+
const envPath = path.join(cwd, ".env");
|
|
238
|
+
const envExisted = fs.existsSync(envPath);
|
|
239
|
+
const existingEnv = envExisted ? fs.readFileSync(envPath, "utf8") : "";
|
|
240
|
+
const conflicts = envKeysPresent(existingEnv, keys);
|
|
241
|
+
let overwriteKeys = new Set(keys);
|
|
242
|
+
if (conflicts.length && !force) if (yes) {
|
|
243
|
+
overwriteKeys = /* @__PURE__ */ new Set();
|
|
244
|
+
p.log.info("Keeping existing .env values (pass --force to overwrite)");
|
|
245
|
+
} else {
|
|
246
|
+
p.log.warn("Some environment variables already exist in .env");
|
|
247
|
+
if (!orCancel(await p.confirm({
|
|
248
|
+
message: "Overwrite their existing values?",
|
|
249
|
+
initialValue: true
|
|
250
|
+
}))) overwriteKeys = /* @__PURE__ */ new Set();
|
|
251
|
+
}
|
|
252
|
+
const merge = mergeEnv(existingEnv, envValues, overwriteKeys);
|
|
253
|
+
fs.writeFileSync(envPath, merge.content);
|
|
254
|
+
const exampleCreated = writeFileIfAbsent(path.join(cwd, ".env.example"), `${keys.map((key) => `${key}=`).join("\n")}\n`);
|
|
255
|
+
const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
256
|
+
const configCreated = writeFileIfAbsent(path.join(cwd, "kizlo.config.ts"), kizloConfigTemplate(dirRel, setup.alias));
|
|
257
|
+
const serverDirRel = path.join(dirRel, "server");
|
|
258
|
+
const serverEntryRel = path.join(serverDirRel, "index.ts");
|
|
259
|
+
const serverPath = path.join(cwd, serverEntryRel);
|
|
260
|
+
const serverExisted = fs.existsSync(serverPath);
|
|
261
|
+
let wroteServer = !serverExisted || force;
|
|
262
|
+
if (serverExisted && !force && !yes) {
|
|
263
|
+
p.log.warn(`A Kizlo server instance already exists at ${serverEntryRel}`);
|
|
264
|
+
wroteServer = orCancel(await p.confirm({
|
|
265
|
+
message: "Overwrite it?",
|
|
266
|
+
initialValue: true
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
if (wroteServer) {
|
|
270
|
+
fs.mkdirSync(path.dirname(serverPath), { recursive: true });
|
|
271
|
+
fs.writeFileSync(serverPath, preset.serverEntry());
|
|
272
|
+
}
|
|
273
|
+
const generatedDirRel = path.join(serverDirRel, "generated");
|
|
274
|
+
writeFileIfAbsent(path.join(cwd, generatedDirRel, "contract.json"), "{}\n");
|
|
275
|
+
writeFileIfAbsent(path.join(cwd, generatedDirRel, "index.ts"), CONTRACT_BARREL);
|
|
276
|
+
const scaffold = {
|
|
277
|
+
serverDirName: path.basename(serverDirRel),
|
|
278
|
+
appDir: detectAppDir(cwd, hasSrcDir),
|
|
279
|
+
serverImport: (fromDir) => resolveModuleImport(cwd, serverDirRel, fromDir, setup.alias)
|
|
280
|
+
};
|
|
281
|
+
const clientRel = path.join(dirRel, "client.ts");
|
|
282
|
+
const clientCreated = writeFileIfAbsent(path.join(cwd, clientRel), preset.clientEntry(scaffold));
|
|
283
|
+
let routeRel;
|
|
284
|
+
let routeCreated = false;
|
|
285
|
+
if (preset.routeHandler) {
|
|
286
|
+
const route = preset.routeHandler(scaffold);
|
|
287
|
+
routeRel = route.path;
|
|
288
|
+
routeCreated = writeFileIfAbsent(path.join(cwd, route.path), route.contents);
|
|
289
|
+
}
|
|
290
|
+
const gitignore = ensureGitignored(cwd, ".env");
|
|
291
|
+
if (!envExisted) p.log.success("Created .env");
|
|
292
|
+
else if (merge.updated.length || merge.added.length) p.log.success("Updated .env");
|
|
293
|
+
else p.log.info("Left .env unchanged");
|
|
294
|
+
p.log.success(exampleCreated ? "Created .env.example" : "Skipped .env.example (exists)");
|
|
295
|
+
p.log.success(configCreated ? "Created kizlo.config.ts" : "Skipped kizlo.config.ts (exists)");
|
|
296
|
+
if (wroteServer) p.log.success(`${serverExisted ? "Overwrote" : "Created"} Kizlo server instance (${serverEntryRel})`);
|
|
297
|
+
else p.log.info(`Kept existing Kizlo server instance (${serverEntryRel})${yes ? " — pass --force to overwrite" : ""}`);
|
|
298
|
+
p.log.success(clientCreated ? `Created browser client (${clientRel})` : "Skipped browser client (exists)");
|
|
299
|
+
if (routeRel) p.log.success(routeCreated ? `Created API route (${routeRel})` : "Skipped API route (exists)");
|
|
300
|
+
if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
|
|
301
|
+
p.note([
|
|
302
|
+
`Watch your extensions and regenerate the contract during development:`,
|
|
303
|
+
` npx kizlo watch`,
|
|
304
|
+
``,
|
|
305
|
+
`Generate the contract once for production builds:`,
|
|
306
|
+
` npx kizlo generate`
|
|
307
|
+
].join("\n"), "Next steps");
|
|
308
|
+
p.outro("Kizlo is ready 🎉");
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
//#endregion
|
|
313
|
+
export { init };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { isSeeded } from "../wp/utils.js";
|
|
2
|
+
import { composeDown, composeUp } from "../wp/docker.js";
|
|
3
|
+
import { withSpinner } from "../utils.js";
|
|
4
|
+
import { log } from "../daemon/logger.js";
|
|
5
|
+
import { resolveTestConfig } from "../daemon/config.js";
|
|
6
|
+
import { runSeeds } from "../wp/setup.js";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { defineCommand } from "citty";
|
|
9
|
+
|
|
10
|
+
//#region src/cli/commands/test.ts
|
|
11
|
+
/** `<pm> test` runs the project's own test script (bun needs `run` to skip its built-in runner). */
|
|
12
|
+
function testCommand(pm) {
|
|
13
|
+
return pm === "bun" ? [
|
|
14
|
+
"bun",
|
|
15
|
+
"run",
|
|
16
|
+
"test"
|
|
17
|
+
] : [pm, "test"];
|
|
18
|
+
}
|
|
19
|
+
/** Spawn the test command, inheriting stdio, and resolve with its exit code. */
|
|
20
|
+
function spawnTest(command, pm) {
|
|
21
|
+
const argv = command ? [command] : testCommand(pm);
|
|
22
|
+
const child = spawn(argv[0], argv.slice(1), {
|
|
23
|
+
stdio: "inherit",
|
|
24
|
+
shell: command !== void 0 || process.platform === "win32"
|
|
25
|
+
});
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
child.on("error", reject);
|
|
28
|
+
child.on("close", (code) => resolve(code ?? 0));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const test = defineCommand({
|
|
32
|
+
meta: {
|
|
33
|
+
name: "test",
|
|
34
|
+
description: "Boot the WordPress test stack (if needed), seed it, and run your tests against it"
|
|
35
|
+
},
|
|
36
|
+
args: {
|
|
37
|
+
teardown: {
|
|
38
|
+
type: "boolean",
|
|
39
|
+
description: "Tear down the test stack after tests finish (default: leave it running for fast reruns)"
|
|
40
|
+
},
|
|
41
|
+
reset: {
|
|
42
|
+
type: "boolean",
|
|
43
|
+
description: "Wipe the database and reseed before running"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
async run({ args }) {
|
|
47
|
+
const cfg = await resolveTestConfig(process.cwd());
|
|
48
|
+
process.env.WP_PORT = String(cfg.port);
|
|
49
|
+
if (args.reset) await withSpinner("Wiping WordPress database", () => composeDown({ volumes: true }), "Database wiped");
|
|
50
|
+
await withSpinner("Starting WordPress test stack", composeUp, "WordPress test stack ready");
|
|
51
|
+
if (!await isSeeded()) await withSpinner("Seeding WordPress", () => runSeeds({
|
|
52
|
+
port: cfg.port,
|
|
53
|
+
fixtures: cfg.fixtures
|
|
54
|
+
}), "WordPress seeded");
|
|
55
|
+
log.info(cfg.command ? `Running: ${cfg.command}` : `Running: ${testCommand(cfg.packageManager).join(" ")}`);
|
|
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.");
|
|
62
|
+
}
|
|
63
|
+
process.exit(code);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
export { test };
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
import { printBanner } from "../banner.js";
|
|
6
|
+
import { acquire, isLocked, lockPath, release } from "../daemon/lock.js";
|
|
7
|
+
import { watch as watch$1 } from "../daemon/watch.js";
|
|
8
|
+
import { defineCommand } from "citty";
|
|
9
|
+
|
|
10
|
+
//#region src/cli/commands/watch.ts
|
|
11
|
+
const args = { dir: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Override the Kizlo directory (defaults to kizlo.config.ts)"
|
|
14
|
+
} };
|
|
15
|
+
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
|
+
printBanner(getVersion());
|
|
23
|
+
const cfg = await resolveConfig(cwd, { dir: args$1.dir });
|
|
24
|
+
acquire(lock);
|
|
25
|
+
const cleanup = () => release(lock);
|
|
26
|
+
process.on("exit", cleanup);
|
|
27
|
+
for (const signal of [
|
|
28
|
+
"SIGINT",
|
|
29
|
+
"SIGTERM",
|
|
30
|
+
"SIGHUP"
|
|
31
|
+
]) process.on(signal, () => {
|
|
32
|
+
cleanup();
|
|
33
|
+
process.exit(0);
|
|
34
|
+
});
|
|
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
|
+
}
|
|
43
|
+
const watch = defineCommand({
|
|
44
|
+
meta: {
|
|
45
|
+
name: "watch",
|
|
46
|
+
description: "Watch your extensions and regenerate the Kizlo contract"
|
|
47
|
+
},
|
|
48
|
+
args,
|
|
49
|
+
run
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
export { watch };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { isSeeded } from "../wp/utils.js";
|
|
2
|
+
import { composeDown, composeStop, composeUp } from "../wp/docker.js";
|
|
3
|
+
import { withSpinner } from "../utils.js";
|
|
4
|
+
import { log } from "../daemon/logger.js";
|
|
5
|
+
import { resolveTestConfig } from "../daemon/config.js";
|
|
6
|
+
import { runSeeds } from "../wp/setup.js";
|
|
7
|
+
import { defineCommand } from "citty";
|
|
8
|
+
|
|
9
|
+
//#region src/cli/commands/wp.ts
|
|
10
|
+
/** Boot the stack and seed it once (idempotent — skips seeding if already seeded). */
|
|
11
|
+
async function bringUp(cwd) {
|
|
12
|
+
const cfg = await resolveTestConfig(cwd);
|
|
13
|
+
process.env.WP_PORT = String(cfg.port);
|
|
14
|
+
await withSpinner("Starting WordPress test stack", composeUp, "WordPress test stack ready");
|
|
15
|
+
if (await isSeeded()) {
|
|
16
|
+
log.info("Stack already seeded — skipping seed.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
await withSpinner("Seeding WordPress", () => runSeeds({
|
|
20
|
+
port: cfg.port,
|
|
21
|
+
fixtures: cfg.fixtures
|
|
22
|
+
}), "WordPress seeded");
|
|
23
|
+
log.success(`Stack ready on http://localhost:${cfg.port} — credentials at ${cfg.credentialsPath}`);
|
|
24
|
+
}
|
|
25
|
+
const up = defineCommand({
|
|
26
|
+
meta: {
|
|
27
|
+
name: "up",
|
|
28
|
+
description: "Start the WordPress test stack and seed it if needed"
|
|
29
|
+
},
|
|
30
|
+
async run() {
|
|
31
|
+
await bringUp(process.cwd());
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const reset = defineCommand({
|
|
35
|
+
meta: {
|
|
36
|
+
name: "reset",
|
|
37
|
+
description: "Wipe the database and reseed a fresh WordPress test stack"
|
|
38
|
+
},
|
|
39
|
+
async run() {
|
|
40
|
+
await withSpinner("Wiping WordPress database", () => composeDown({ volumes: true }), "Database wiped");
|
|
41
|
+
await bringUp(process.cwd());
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const stop = defineCommand({
|
|
45
|
+
meta: {
|
|
46
|
+
name: "stop",
|
|
47
|
+
description: "Stop the WordPress test stack, keeping the database volume"
|
|
48
|
+
},
|
|
49
|
+
async run() {
|
|
50
|
+
await withSpinner("Stopping WordPress test stack", composeStop, "Stack stopped (volumes kept)");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const wp = defineCommand({
|
|
54
|
+
meta: {
|
|
55
|
+
name: "wp",
|
|
56
|
+
description: "Manage the WordPress test stack (advanced: up | reset | stop)"
|
|
57
|
+
},
|
|
58
|
+
subCommands: {
|
|
59
|
+
up,
|
|
60
|
+
reset,
|
|
61
|
+
stop
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
export { wp };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { credentialsPath, findConfigDir } from "../wp/utils.js";
|
|
2
|
+
import { detectPackageManager } from "../utils.js";
|
|
3
|
+
import { log } from "./logger.js";
|
|
4
|
+
import z from "zod/v4";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import { createJiti } from "jiti";
|
|
8
|
+
|
|
9
|
+
//#region src/cli/daemon/config.ts
|
|
10
|
+
const fixtureSchema = z.custom((value) => typeof value === "object" && value !== null && typeof value.name === "string", { message: "must be a fixture object with a string `name` (use defineFixture)" });
|
|
11
|
+
/** Runtime shape of `kizlo.config.*` — mirrors {@link KizloGlobalConfig}. */
|
|
12
|
+
const configSchema = z.object({
|
|
13
|
+
dir: z.string().optional(),
|
|
14
|
+
alias: z.string().optional(),
|
|
15
|
+
test: z.object({
|
|
16
|
+
port: z.number().int().positive().optional(),
|
|
17
|
+
fixtures: z.array(fixtureSchema).optional(),
|
|
18
|
+
packageManager: z.enum([
|
|
19
|
+
"npm",
|
|
20
|
+
"pnpm",
|
|
21
|
+
"yarn",
|
|
22
|
+
"bun"
|
|
23
|
+
]).optional(),
|
|
24
|
+
command: z.string().optional()
|
|
25
|
+
}).optional()
|
|
26
|
+
});
|
|
27
|
+
const CONFIG_FILES = [
|
|
28
|
+
"kizlo.config.ts",
|
|
29
|
+
"kizlo.config.js",
|
|
30
|
+
"kizlo.config.mjs"
|
|
31
|
+
];
|
|
32
|
+
function defaultDir(cwd) {
|
|
33
|
+
return fs.existsSync(path.join(cwd, "src")) ? "src/lib/kizlo" : "lib/kizlo";
|
|
34
|
+
}
|
|
35
|
+
async function loadConfigFile(cwd) {
|
|
36
|
+
const file = CONFIG_FILES.map((name) => path.join(cwd, name)).find((p) => fs.existsSync(p));
|
|
37
|
+
if (!file) return void 0;
|
|
38
|
+
let raw;
|
|
39
|
+
try {
|
|
40
|
+
const mod = await createJiti(cwd, { moduleCache: false }).import(file);
|
|
41
|
+
raw = mod.default ?? mod;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
log.error(`Could not load ${path.basename(file)}:`, error);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const result = configSchema.safeParse(raw);
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
log.error(`Invalid ${path.basename(file)}:\n${z.prettifyError(result.error)}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
return result.data;
|
|
52
|
+
}
|
|
53
|
+
async function resolveConfig(cwd, flags) {
|
|
54
|
+
const fileConfig = await loadConfigFile(cwd);
|
|
55
|
+
const dir = (flags?.dir ?? fileConfig?.dir ?? defaultDir(cwd)).replace(/^\.\//, "").replace(/\/+$/, "");
|
|
56
|
+
const serverDir = path.join(dir, "server");
|
|
57
|
+
const generatedDir = path.join(serverDir, "generated");
|
|
58
|
+
return {
|
|
59
|
+
cwd,
|
|
60
|
+
dir,
|
|
61
|
+
serverDir,
|
|
62
|
+
serverEntry: path.join(serverDir, "index.ts"),
|
|
63
|
+
generatedDir,
|
|
64
|
+
contractPath: path.join(generatedDir, "contract.json"),
|
|
65
|
+
barrelPath: path.join(generatedDir, "index.ts")
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the `test` block from `kizlo.config.*` into concrete values for the
|
|
70
|
+
* `wp`/`test` commands, applying defaults (port 8080, auto-detected package
|
|
71
|
+
* manager) and anchoring the credentials artifact to the config directory.
|
|
72
|
+
*/
|
|
73
|
+
async function resolveTestConfig(cwd) {
|
|
74
|
+
const configDir = findConfigDir(cwd);
|
|
75
|
+
const test = (await loadConfigFile(configDir))?.test ?? {};
|
|
76
|
+
return {
|
|
77
|
+
configDir,
|
|
78
|
+
command: test.command,
|
|
79
|
+
port: test.port ?? 8080,
|
|
80
|
+
fixtures: test.fixtures ?? [],
|
|
81
|
+
credentialsPath: credentialsPath(cwd),
|
|
82
|
+
packageManager: test.packageManager ?? detectPackageManager(configDir)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
export { resolveConfig, resolveTestConfig };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { generateContract } from "../../shared/contract.js";
|
|
2
|
+
import { loadEnvFiles } from "../utils.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { createJiti } from "jiti";
|
|
6
|
+
|
|
7
|
+
//#region src/cli/daemon/generate.ts
|
|
8
|
+
/** The generated contract barrel — also written as a stub by `kizlo init`. */
|
|
9
|
+
const CONTRACT_BARREL = [
|
|
10
|
+
`import type { router } from ".."`,
|
|
11
|
+
`import contractJson from "./contract.json"`,
|
|
12
|
+
``,
|
|
13
|
+
`export const contract = contractJson as unknown as typeof router`,
|
|
14
|
+
``
|
|
15
|
+
].join("\n");
|
|
16
|
+
/** Write atomically: a reader never observes a half-written file. */
|
|
17
|
+
function atomicWrite(file, data) {
|
|
18
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
19
|
+
fs.writeFileSync(tmp, data);
|
|
20
|
+
fs.renameSync(tmp, file);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Loads the Kizlo server, builds its contract, and writes `contract.json` and
|
|
24
|
+
* the generated barrel. Returns false when the entry has no Kizlo server.
|
|
25
|
+
*/
|
|
26
|
+
async function generateOnce(cfg) {
|
|
27
|
+
loadEnvFiles(cfg.cwd);
|
|
28
|
+
const jiti = createJiti(cfg.cwd, { moduleCache: false });
|
|
29
|
+
const entry = path.resolve(cfg.cwd, cfg.serverEntry);
|
|
30
|
+
const { router } = await jiti.import(entry);
|
|
31
|
+
if (!router) return false;
|
|
32
|
+
const contract = JSON.stringify(await generateContract(router));
|
|
33
|
+
fs.mkdirSync(path.resolve(cfg.cwd, cfg.generatedDir), { recursive: true });
|
|
34
|
+
atomicWrite(path.resolve(cfg.cwd, cfg.contractPath), contract);
|
|
35
|
+
atomicWrite(path.resolve(cfg.cwd, cfg.barrelPath), CONTRACT_BARREL);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { CONTRACT_BARREL, generateOnce };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/daemon/lock.ts
|
|
5
|
+
function lockPath(cwd) {
|
|
6
|
+
return path.join(cwd, "node_modules/.cache/kizlo/dev.lock");
|
|
7
|
+
}
|
|
8
|
+
/** True when a live daemon already owns the lock. Stale locks are ignored. */
|
|
9
|
+
function isLocked(file) {
|
|
10
|
+
if (!fs.existsSync(file)) return false;
|
|
11
|
+
const pid = Number(fs.readFileSync(file, "utf8").trim());
|
|
12
|
+
if (!pid) return false;
|
|
13
|
+
try {
|
|
14
|
+
process.kill(pid, 0);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function acquire(file) {
|
|
21
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
+
fs.writeFileSync(file, String(process.pid));
|
|
23
|
+
}
|
|
24
|
+
function release(file) {
|
|
25
|
+
try {
|
|
26
|
+
fs.rmSync(file, { force: true });
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { acquire, isLocked, lockPath, release };
|