kizlo 0.6.1 → 0.8.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/brand/icons.d.ts +10 -12
- package/dist/brand/icons.js +19 -17
- package/dist/category/error.js +14 -0
- package/dist/category/index.js +77 -0
- package/dist/category/schema.js +40 -0
- package/dist/category/utils.js +20 -0
- package/dist/cli/banner.js +1 -1
- package/dist/cli/commands/_setup.js +342 -0
- package/dist/cli/commands/create.js +146 -0
- package/dist/cli/commands/dev.js +26 -15
- package/dist/cli/commands/init.js +53 -300
- package/dist/cli/index.js +2 -1
- package/dist/cli/presets/generated/templates.js +59 -0
- package/dist/cli/presets/nextjs.js +3 -95
- package/dist/cli/presets/patch.js +133 -0
- package/dist/cli/presets/template.js +36 -0
- package/dist/cli/wp/dev.js +16 -1
- package/dist/comment/index.js +4 -4
- package/dist/context.d.ts +25 -8
- package/dist/context.js +15 -5
- package/dist/email/service.d.ts +4 -3
- package/dist/email/service.js +4 -4
- package/dist/index.d.ts +15 -11
- package/dist/index.js +10 -4
- package/dist/integrations/nextjs/metadata.js +7 -5
- package/dist/integrations/nextjs/sitemap.d.ts +8 -7
- package/dist/integrations/nextjs/sitemap.js +3 -3
- package/dist/menu/index.js +19 -14
- package/dist/menu/schema.d.ts +14 -0
- package/dist/menu/schema.js +35 -40
- package/dist/menu/utils.js +23 -14
- package/dist/nextjs/server.d.ts +2 -1
- package/dist/nextjs/server.js +2 -1
- package/dist/package.js +1 -1
- package/dist/page/error.js +20 -0
- package/dist/page/index.js +128 -0
- package/dist/page/schema.js +75 -0
- package/dist/page/utils.js +43 -0
- package/dist/post/error.js +4 -14
- package/dist/post/index.js +17 -9
- package/dist/post/schema.js +37 -66
- package/dist/post/utils.js +27 -24
- package/dist/router.d.ts +1006 -187
- package/dist/router.js +6 -0
- package/dist/seo/index.js +7 -44
- package/dist/seo/schema.d.ts +10 -6
- package/dist/seo/schema.js +7 -5
- package/dist/seo/sitemap.d.ts +9 -1
- package/dist/seo/sitemap.js +17 -1
- package/dist/seo/types.d.ts +17 -20
- package/dist/seo/utils.js +9 -7
- package/dist/settings/index.js +27 -24
- package/dist/settings/service.d.ts +14 -13
- package/dist/settings/service.interface.d.ts +2 -2
- package/dist/settings/service.js +9 -9
- package/dist/shared/middleware.d.ts +4 -4
- package/dist/shared/procedure.d.ts +6 -7
- package/dist/tag/error.js +14 -0
- package/dist/tag/index.js +76 -0
- package/dist/tag/schema.js +38 -0
- package/dist/tag/utils.js +19 -0
- package/dist/taxonomy/service.js +48 -0
- package/dist/test/fixtures.d.ts +1 -1
- package/dist/test/fixtures.js +1 -1
- package/dist/webhook/index.d.ts +2 -2
- package/dist/wordpress/category/service.d.ts +28 -0
- package/dist/wordpress/category/service.js +55 -0
- package/dist/wordpress/category/types.d.ts +120 -0
- package/dist/wordpress/category/types.js +15 -0
- package/dist/wordpress/comment/service.d.ts +1 -1
- package/dist/wordpress/index.d.ts +9 -4
- package/dist/wordpress/menu/items/types.d.ts +7 -4
- package/dist/wordpress/menu/items/types.js +22 -0
- package/dist/wordpress/page/service.d.ts +28 -0
- package/dist/wordpress/page/service.js +55 -0
- package/dist/wordpress/page/types.d.ts +270 -0
- package/dist/wordpress/page/types.js +27 -0
- package/dist/wordpress/post/types.d.ts +9 -5
- package/dist/wordpress/post/types.js +41 -0
- package/dist/wordpress/service.d.ts +7 -1
- package/dist/wordpress/service.js +6 -0
- package/dist/wordpress/tag/service.d.ts +28 -0
- package/dist/wordpress/tag/service.js +55 -0
- package/dist/wordpress/tag/types.d.ts +113 -0
- package/dist/wordpress/tag/types.js +15 -0
- package/package.json +5 -3
- package/dist/service.d.ts +0 -19
- package/dist/service.js +0 -20
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { dockerAvailable } from "../wp/docker.js";
|
|
2
|
+
import { detectPackageManager, getVersion } from "../utils.js";
|
|
3
|
+
import { printBanner } from "../banner.js";
|
|
4
|
+
import { getPreset } from "../presets/index.js";
|
|
5
|
+
import { collectConnectionInteractively, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
|
|
6
|
+
import z from "zod/v4";
|
|
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
|
+
import { downloadTemplate } from "giget";
|
|
12
|
+
|
|
13
|
+
//#region src/cli/commands/create.ts
|
|
14
|
+
/** GitHub repo the full templates are fetched from, matching the release-tag scheme. */
|
|
15
|
+
const TEMPLATE_REPO = "github:kizlo-io/kizlo";
|
|
16
|
+
/**
|
|
17
|
+
* The templates `create` can scaffold from. Each id is both the `templates/<id>` folder and the
|
|
18
|
+
* preset that drives its WordPress setup. When only one exists the template prompt auto-selects it.
|
|
19
|
+
*/
|
|
20
|
+
const TEMPLATES = ["nextjs"];
|
|
21
|
+
function isTemplate(value) {
|
|
22
|
+
return TEMPLATES.includes(value);
|
|
23
|
+
}
|
|
24
|
+
const projectName = validate(z.string().trim().min(1, "Required").refine((value) => !value.startsWith(".") && !value.includes("/") && !value.includes("\\"), "Enter a folder name (letters, numbers, dashes)"));
|
|
25
|
+
/**
|
|
26
|
+
* Resolve where the template is fetched from. Defaults to the GitHub repo pinned to the release
|
|
27
|
+
* tag matching this CLI version (`kizlo-v<version>`), so the template and the `kizlo` dependency it
|
|
28
|
+
* pulls come from the same release. `KIZLO_TEMPLATE_REF` overrides the ref (e.g. `main`) and
|
|
29
|
+
* `KIZLO_TEMPLATE_LOCAL_DIR` points at a local `templates/` directory for pre-release development.
|
|
30
|
+
*/
|
|
31
|
+
function templateSource(template) {
|
|
32
|
+
const localDir = process.env.KIZLO_TEMPLATE_LOCAL_DIR;
|
|
33
|
+
if (localDir) return { local: path.resolve(localDir, template) };
|
|
34
|
+
return { remote: `${TEMPLATE_REPO}/templates/${template}#${process.env.KIZLO_TEMPLATE_REF ?? `kizlo-v${getVersion()}`}` };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Copy the template into `dir`, then make it a standalone project: rewrite the `workspace:*` Kizlo
|
|
38
|
+
* dependency to this CLI's version and name the package after the project folder. The template's
|
|
39
|
+
* `kizlo.template.json` is an internal build input, so it's removed once the copy lands.
|
|
40
|
+
*/
|
|
41
|
+
async function scaffoldTemplate(template, dir, name) {
|
|
42
|
+
const source = templateSource(template);
|
|
43
|
+
if (source.local) {
|
|
44
|
+
if (!fs.existsSync(source.local)) throw new Error(`Local template not found: ${source.local}`);
|
|
45
|
+
fs.cpSync(source.local, dir, { recursive: true });
|
|
46
|
+
} else await downloadTemplate(source.remote, {
|
|
47
|
+
dir,
|
|
48
|
+
forceClean: true
|
|
49
|
+
});
|
|
50
|
+
fs.rmSync(path.join(dir, "kizlo.template.json"), { force: true });
|
|
51
|
+
const pkgPath = path.join(dir, "package.json");
|
|
52
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
53
|
+
pkg.name = name;
|
|
54
|
+
if (pkg.dependencies?.kizlo === "workspace:*") pkg.dependencies.kizlo = `^${getVersion()}`;
|
|
55
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
|
|
56
|
+
}
|
|
57
|
+
const create = defineCommand({
|
|
58
|
+
meta: {
|
|
59
|
+
name: "create",
|
|
60
|
+
description: "Scaffold a new project with Kizlo already wired"
|
|
61
|
+
},
|
|
62
|
+
args: {
|
|
63
|
+
template: {
|
|
64
|
+
type: "positional",
|
|
65
|
+
required: false,
|
|
66
|
+
description: `Template to scaffold from (${TEMPLATES.join(", ")})`
|
|
67
|
+
},
|
|
68
|
+
name: {
|
|
69
|
+
type: "positional",
|
|
70
|
+
required: false,
|
|
71
|
+
description: "Project folder to create"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
async run({ args }) {
|
|
75
|
+
const cwd = process.cwd();
|
|
76
|
+
printBanner(getVersion());
|
|
77
|
+
p.intro("kizlo create");
|
|
78
|
+
const requested = args.template;
|
|
79
|
+
let template;
|
|
80
|
+
if (requested) {
|
|
81
|
+
if (!isTemplate(requested)) {
|
|
82
|
+
p.cancel(`Unknown template "${requested}". Available: ${TEMPLATES.join(", ")}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
template = requested;
|
|
86
|
+
} else if (TEMPLATES.length === 1) template = TEMPLATES[0];
|
|
87
|
+
else template = orCancel(await p.select({
|
|
88
|
+
message: "Template",
|
|
89
|
+
options: TEMPLATES.map((id) => ({
|
|
90
|
+
value: id,
|
|
91
|
+
label: id
|
|
92
|
+
}))
|
|
93
|
+
}));
|
|
94
|
+
const name = args.name ? String(args.name).trim() : orCancel(await p.text({
|
|
95
|
+
message: "Project name",
|
|
96
|
+
placeholder: "my-app",
|
|
97
|
+
validate: projectName
|
|
98
|
+
}));
|
|
99
|
+
if (!args.name) {
|
|
100
|
+
const check = projectName(name);
|
|
101
|
+
if (check) {
|
|
102
|
+
p.cancel(check);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const dir = path.resolve(cwd, name);
|
|
107
|
+
if (fs.existsSync(dir)) {
|
|
108
|
+
p.cancel(`${name} already exists — pick a different name or remove it.`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
const preset = getPreset(template);
|
|
112
|
+
if (!preset) {
|
|
113
|
+
p.cancel(`No preset for template "${template}".`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
const conn = await collectConnectionInteractively(preset);
|
|
117
|
+
if (preset.apiPath && conn.baseUrl) conn.baseUrl = withApiPath(conn.baseUrl, preset.apiPath);
|
|
118
|
+
if (conn.mode === "local" && !await dockerAvailable()) {
|
|
119
|
+
p.cancel("Docker isn't available — start Docker (or install it) and re-run, or choose “Use my own WordPress”.");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const s = p.spinner();
|
|
123
|
+
s.start(`Creating ${name} from the ${template} template`);
|
|
124
|
+
try {
|
|
125
|
+
await scaffoldTemplate(template, dir, name);
|
|
126
|
+
s.stop(`Created ${name}`);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
s.stop("Could not create the project");
|
|
129
|
+
p.cancel(error instanceof Error ? error.message : String(error));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
await setupLocalWordPress(dir, conn);
|
|
133
|
+
await writeEnv(dir, preset, conn, {
|
|
134
|
+
force: true,
|
|
135
|
+
yes: false
|
|
136
|
+
});
|
|
137
|
+
await syncRemote(conn);
|
|
138
|
+
const pm = detectPackageManager(dir);
|
|
139
|
+
p.note([`cd ${name}`, `${pm} install`].join("\n"), "Get started");
|
|
140
|
+
nextStepsNote(conn.mode);
|
|
141
|
+
p.outro("Kizlo is ready 🎉");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
//#endregion
|
|
146
|
+
export { create };
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -2,16 +2,15 @@ import { createStack } from "../wp/docker.js";
|
|
|
2
2
|
import { ensureGitignored, envGroups, getVersion, groupDefault, mergeEnv, pickStackPort, withSpinner } from "../utils.js";
|
|
3
3
|
import { palette, printBanner } from "../banner.js";
|
|
4
4
|
import { resolveDevConfig } from "../daemon/config.js";
|
|
5
|
-
import { startWatcher } from "../daemon/watch.js";
|
|
6
5
|
import { bootstrapDev } from "../wp/dev.js";
|
|
7
6
|
import { reapOrphans, registerSession, removeProjectContainers, spawnWatchdog, unregisterSession } from "../wp/session.js";
|
|
8
7
|
import { syncSiteSettings } from "../wp/settings.js";
|
|
9
8
|
import { devStack } from "../wp/stack.js";
|
|
9
|
+
import { startWatcher } from "../daemon/watch.js";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { defineCommand } from "citty";
|
|
13
13
|
import { randomBytes } from "node:crypto";
|
|
14
|
-
import { networkInterfaces } from "node:os";
|
|
15
14
|
|
|
16
15
|
//#region src/cli/commands/dev.ts
|
|
17
16
|
async function resolve$1(cwd) {
|
|
@@ -26,22 +25,17 @@ function note(text) {
|
|
|
26
25
|
const { dim, reset } = palette();
|
|
27
26
|
process.stdout.write(` ${dim}${text}${reset}\n`);
|
|
28
27
|
}
|
|
29
|
-
/** First non-internal IPv4 address, for the LAN-reachable "Network" URL (undefined when offline). */
|
|
30
|
-
function lanAddress() {
|
|
31
|
-
for (const ifaces of Object.values(networkInterfaces())) for (const iface of ifaces ?? []) if (iface.family === "IPv4" && !iface.internal) return iface.address;
|
|
32
|
-
}
|
|
33
28
|
/**
|
|
34
|
-
* Print the connection summary as an aligned, Next.js-style block. Shows the wp-admin
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* wp-admin password (it's never stored, so this is the only chance to see it).
|
|
29
|
+
* Print the connection summary as an aligned, Next.js-style block. Shows the wp-admin URL on
|
|
30
|
+
* the loopback ("Local") and — when the stack is installed on a LAN address rather than
|
|
31
|
+
* localhost — on the network, so it can be opened from other devices. On a fresh install, also
|
|
32
|
+
* show the one-time wp-admin password (it's never stored, so this is the only chance to see it).
|
|
38
33
|
*/
|
|
39
34
|
function printSummary(info) {
|
|
40
35
|
const { cyan, dim, reset } = palette();
|
|
41
|
-
const port = new URL(info.url)
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
if (lan) rows.push(["WP Network", `http://${lan}:${port}/wp-admin`]);
|
|
36
|
+
const { port, hostname } = new URL(info.url);
|
|
37
|
+
const rows = [["WP Local", `http://localhost:${port}/wp-admin`]];
|
|
38
|
+
if (hostname !== "localhost") rows.push(["WP Network", `${info.url}/wp-admin`]);
|
|
45
39
|
rows.push(["Database Connection", `127.0.0.1:${info.dbPort}`]);
|
|
46
40
|
const width = Math.max(...rows.map(([label]) => label.length)) + 1;
|
|
47
41
|
const lines = ["", ...rows.map(([label, value]) => ` ${dim}- ${`${label}:`.padEnd(width)}${reset} ${cyan}${value}${reset}`)];
|
|
@@ -104,6 +98,23 @@ function updateWpEnv(cfg, info) {
|
|
|
104
98
|
};
|
|
105
99
|
}
|
|
106
100
|
/**
|
|
101
|
+
* Re-point `KIZLO_DEV_WORDPRESS_URL` at the current dev URL on a warm resume, so a LAN address that
|
|
102
|
+
* changed since the last session (a new DHCP lease, a different network) doesn't leave the app talking
|
|
103
|
+
* to a dead IP. Only this one key is rewritten — the credentials and secret from the fresh install are
|
|
104
|
+
* preserved — and it's a no-op when `.env` is absent (nothing provisioned yet) or already current.
|
|
105
|
+
* The full fresh-install write goes through {@link updateWpEnv} instead; this covers the resume path it
|
|
106
|
+
* skips. Returns true when it changed the file. The app still needs a restart to read the new value.
|
|
107
|
+
*/
|
|
108
|
+
function syncDevUrl(cfg, url) {
|
|
109
|
+
const envPath = join(cfg.configDir, ".env");
|
|
110
|
+
if (!existsSync(envPath)) return false;
|
|
111
|
+
const existing = readFileSync(envPath, "utf8");
|
|
112
|
+
if (readEnvValue(existing, "KIZLO_DEV_WORDPRESS_URL") === url) return false;
|
|
113
|
+
const { content } = mergeEnv(existing, { KIZLO_DEV_WORDPRESS_URL: url }, new Set(["KIZLO_DEV_WORDPRESS_URL"]), envGroups("KIZLO_BACKEND_URL"));
|
|
114
|
+
writeFileSync(envPath, content);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
107
118
|
* Arm the foreground teardown — call this *before* the containers start, so a cancel at
|
|
108
119
|
* any point (even mid-startup, with mysql up but wordpress still booting) stops whatever
|
|
109
120
|
* already came up. The teardown is owned by a detached **watchdog** process, not by this
|
|
@@ -189,7 +200,7 @@ async function startForeground(cfg) {
|
|
|
189
200
|
});
|
|
190
201
|
if (!sync.ok) note(`Could not sync the site settings to WordPress (${sync.error}) — make sure the kizlo plugin is active, then set them from the Kizlo settings.`);
|
|
191
202
|
}
|
|
192
|
-
}
|
|
203
|
+
} else if (syncDevUrl(ready, creds.url)) note("Updated .env WordPress URL to the current network address — restart your app to pick it up");
|
|
193
204
|
printSummary(creds);
|
|
194
205
|
const { green, dim, bold, reset } = palette();
|
|
195
206
|
const ms = Date.now() - start;
|
|
@@ -1,65 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored,
|
|
1
|
+
import { dockerAvailable } from "../wp/docker.js";
|
|
2
|
+
import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, 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";
|
|
11
4
|
import { PRESETS, detectPreset, getPreset } from "../presets/index.js";
|
|
12
|
-
import
|
|
5
|
+
import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
|
|
6
|
+
import { CONTRACT_BARREL } from "../daemon/generate.js";
|
|
7
|
+
import { applyPatchToSource, patchChanged, resolvePatch, resolvePatchTargetPath, templatePatches } from "../presets/patch.js";
|
|
13
8
|
import path from "node:path";
|
|
14
9
|
import fs from "node:fs";
|
|
15
10
|
import { defineCommand } from "citty";
|
|
16
11
|
import * as p from "@clack/prompts";
|
|
17
|
-
import { randomBytes } from "node:crypto";
|
|
18
12
|
|
|
19
13
|
//#region src/cli/commands/init.ts
|
|
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"
|
|
26
|
-
];
|
|
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
|
-
};
|
|
62
|
-
}
|
|
63
14
|
function defaultDir(hasSrcDir) {
|
|
64
15
|
return hasSrcDir ? "src/lib/kizlo" : "lib/kizlo";
|
|
65
16
|
}
|
|
@@ -75,18 +26,6 @@ export default defineConfig({
|
|
|
75
26
|
})
|
|
76
27
|
`;
|
|
77
28
|
}
|
|
78
|
-
/** Appends the API path to the base URL so the client and route handler agree. */
|
|
79
|
-
function withApiPath(baseUrl, apiPath) {
|
|
80
|
-
try {
|
|
81
|
-
const url = new URL(baseUrl);
|
|
82
|
-
const current = url.pathname.replace(/\/+$/, "");
|
|
83
|
-
if (current.endsWith(apiPath)) return url.toString().replace(/\/+$/, "");
|
|
84
|
-
url.pathname = `${current}${apiPath}`;
|
|
85
|
-
return url.toString().replace(/\/+$/, "");
|
|
86
|
-
} catch {
|
|
87
|
-
return baseUrl;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
29
|
/** Whether two URLs share an origin (scheme + host + port); false when either can't be parsed. */
|
|
91
30
|
function sameOrigin(a, b) {
|
|
92
31
|
try {
|
|
@@ -100,22 +39,6 @@ function detectAppDir(cwd, hasSrcDir) {
|
|
|
100
39
|
if (fs.existsSync(path.join(cwd, "app"))) return "app";
|
|
101
40
|
return hasSrcDir ? "src/app" : "app";
|
|
102
41
|
}
|
|
103
|
-
const requiredString = z.string().trim().min(1, "Required");
|
|
104
|
-
const urlString = requiredString.pipe(z.url("Must be a valid URL (e.g. https://example.com)"));
|
|
105
|
-
const dirPath = requiredString.refine((value) => !value.endsWith(".ts"), "Enter a directory, not a file");
|
|
106
|
-
function validate(schema) {
|
|
107
|
-
return (value) => {
|
|
108
|
-
const result = schema.safeParse(value ?? "");
|
|
109
|
-
return result.success ? void 0 : result.error.issues[0]?.message;
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
function orCancel(value) {
|
|
113
|
-
if (p.isCancel(value)) {
|
|
114
|
-
p.cancel("Setup cancelled.");
|
|
115
|
-
process.exit(0);
|
|
116
|
-
}
|
|
117
|
-
return value;
|
|
118
|
-
}
|
|
119
42
|
/**
|
|
120
43
|
* The single overwrite policy for every scaffolded file: create it when absent, overwrite on
|
|
121
44
|
* `--force`, and otherwise ask before clobbering an existing file (keeping it when the user
|
|
@@ -146,76 +69,7 @@ function reportScaffold(file, result, yes) {
|
|
|
146
69
|
else p.log.success(`${result === "overwritten" ? "Overwrote" : "Created"} ${file.label} (${file.relPath})`);
|
|
147
70
|
}
|
|
148
71
|
async function collectInteractively(ctx) {
|
|
149
|
-
|
|
150
|
-
let baseUrl;
|
|
151
|
-
if (ctx.preset.apiPath) baseUrl = orCancel(await p.text({
|
|
152
|
-
message: "Public app URL",
|
|
153
|
-
placeholder: "https://your-app.com",
|
|
154
|
-
validate: validate(urlString)
|
|
155
|
-
}));
|
|
156
|
-
else {
|
|
157
|
-
siteUrl = orCancel(await p.text({
|
|
158
|
-
message: "Public site URL",
|
|
159
|
-
placeholder: "https://your-app.com",
|
|
160
|
-
validate: validate(urlString)
|
|
161
|
-
}));
|
|
162
|
-
baseUrl = orCancel(await p.text({
|
|
163
|
-
message: "Kizlo backend URL (where the handler is mounted)",
|
|
164
|
-
placeholder: "https://your-app.com/kizlo",
|
|
165
|
-
validate: validate(urlString)
|
|
166
|
-
}));
|
|
167
|
-
}
|
|
168
|
-
const siteSecret = orCancel(await p.select({
|
|
169
|
-
message: "Site secret (webhook signing key)",
|
|
170
|
-
initialValue: "generate",
|
|
171
|
-
options: [{
|
|
172
|
-
value: "generate",
|
|
173
|
-
label: "Generate a secure secret automatically",
|
|
174
|
-
hint: "recommended"
|
|
175
|
-
}, {
|
|
176
|
-
value: "enter",
|
|
177
|
-
label: "Enter my own"
|
|
178
|
-
}]
|
|
179
|
-
})) === "enter" ? orCancel(await p.password({
|
|
180
|
-
message: "Enter the site secret",
|
|
181
|
-
validate: validate(requiredString)
|
|
182
|
-
})) : randomBytes(32).toString("hex");
|
|
183
|
-
const mode = orCancel(await p.select({
|
|
184
|
-
message: "WordPress connection",
|
|
185
|
-
initialValue: "local",
|
|
186
|
-
options: [{
|
|
187
|
-
value: "local",
|
|
188
|
-
label: "Set up a local dev environment",
|
|
189
|
-
hint: "runs WordPress in Docker"
|
|
190
|
-
}, {
|
|
191
|
-
value: "remote",
|
|
192
|
-
label: "Use my own WordPress",
|
|
193
|
-
hint: "connect to an existing site"
|
|
194
|
-
}]
|
|
195
|
-
}));
|
|
196
|
-
let wpUrl = "";
|
|
197
|
-
let wpUsername = "";
|
|
198
|
-
let wpPassword = "";
|
|
199
|
-
let devPath;
|
|
200
|
-
if (mode === "remote") {
|
|
201
|
-
wpUrl = orCancel(await p.text({
|
|
202
|
-
message: "WordPress URL",
|
|
203
|
-
placeholder: "https://wp.your-app.com",
|
|
204
|
-
validate: validate(urlString)
|
|
205
|
-
}));
|
|
206
|
-
wpUsername = orCancel(await p.text({
|
|
207
|
-
message: "WordPress username",
|
|
208
|
-
validate: validate(requiredString)
|
|
209
|
-
}));
|
|
210
|
-
wpPassword = orCancel(await p.password({
|
|
211
|
-
message: "WordPress application password",
|
|
212
|
-
validate: validate(requiredString)
|
|
213
|
-
}));
|
|
214
|
-
} else devPath = orCancel(await p.text({
|
|
215
|
-
message: "Local WordPress folder",
|
|
216
|
-
initialValue: "wordpress",
|
|
217
|
-
validate: validate(dirPath)
|
|
218
|
-
}));
|
|
72
|
+
const conn = await collectConnectionInteractively(ctx.preset);
|
|
219
73
|
const dir = orCancel(await p.text({
|
|
220
74
|
message: "Kizlo directory",
|
|
221
75
|
initialValue: defaultDir(ctx.hasSrcDir),
|
|
@@ -232,14 +86,7 @@ async function collectInteractively(ctx) {
|
|
|
232
86
|
})).trim();
|
|
233
87
|
}
|
|
234
88
|
return {
|
|
235
|
-
|
|
236
|
-
baseUrl,
|
|
237
|
-
siteUrl,
|
|
238
|
-
siteSecret,
|
|
239
|
-
wpUrl,
|
|
240
|
-
wpUsername,
|
|
241
|
-
wpPassword,
|
|
242
|
-
devPath,
|
|
89
|
+
...conn,
|
|
243
90
|
dir,
|
|
244
91
|
alias
|
|
245
92
|
};
|
|
@@ -250,14 +97,10 @@ async function collectInteractively(ctx) {
|
|
|
250
97
|
* later. Never fails — `--yes` always scaffolds a fillable project.
|
|
251
98
|
*/
|
|
252
99
|
function collectFromEnv(ctx) {
|
|
100
|
+
const conn = collectConnectionFromEnv(ctx.preset);
|
|
253
101
|
const dir = defaultDir(ctx.hasSrcDir);
|
|
254
102
|
return {
|
|
255
|
-
|
|
256
|
-
baseUrl: process.env[ctx.preset.baseUrlEnvKey]?.trim() ?? "",
|
|
257
|
-
siteSecret: process.env.KIZLO_SITE_SECRET?.trim() || randomBytes(32).toString("hex"),
|
|
258
|
-
wpUrl: process.env.KIZLO_WORDPRESS_URL?.trim() ?? "",
|
|
259
|
-
wpUsername: process.env.KIZLO_WORDPRESS_USERNAME?.trim() ?? "",
|
|
260
|
-
wpPassword: process.env.KIZLO_WORDPRESS_APPLICATION_PASSWORD?.trim() ?? "",
|
|
103
|
+
...conn,
|
|
261
104
|
dir,
|
|
262
105
|
alias: ctx.preset.apiPath ? aliasWithSlash(detectImportAlias(ctx.cwd, path.join(dir, "server"))?.prefix) : ""
|
|
263
106
|
};
|
|
@@ -265,66 +108,6 @@ function collectFromEnv(ctx) {
|
|
|
265
108
|
function readPkg(pkgPath) {
|
|
266
109
|
return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
267
110
|
}
|
|
268
|
-
/** Build a {@link ResolvedDevConfig} from the chosen install folder, matching `resolveDevConfig`'s
|
|
269
|
-
* defaults — built directly so init never has to round-trip through the config file it's writing. */
|
|
270
|
-
function devConfigFor(cwd, devPath) {
|
|
271
|
-
return {
|
|
272
|
-
configDir: cwd,
|
|
273
|
-
project: `${resolveStackName(cwd)}-dev`,
|
|
274
|
-
port: DEFAULT_DEV_PORT,
|
|
275
|
-
portExplicit: false,
|
|
276
|
-
dbPort: DEFAULT_DEV_DB_PORT,
|
|
277
|
-
dbPortExplicit: false,
|
|
278
|
-
fixtures: [],
|
|
279
|
-
wordpressPath: devPath,
|
|
280
|
-
wordpressDir: path.resolve(cwd, devPath)
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Boot the local dev stack once to produce working credentials, then stop it (volumes
|
|
285
|
-
* persist, so a later `kizlo dev` resumes instantly). The dev stack mints no application
|
|
286
|
-
* password — that's a test-stack concern — so we create one here for REST auth in `.env`.
|
|
287
|
-
* While the stack is still up, push the dev site settings (`siteSecret` plus the Kizlo server's
|
|
288
|
-
* `url`/`backend_url`, derived from `baseUrl`) into the plugin so webhook signing and event delivery
|
|
289
|
-
* work.
|
|
290
|
-
*/
|
|
291
|
-
async function provisionLocalStack(cfg, siteSecret, baseUrl) {
|
|
292
|
-
await removeProjectContainers(cfg.project);
|
|
293
|
-
const port = await pickStackPort(cfg.port, {
|
|
294
|
-
fixed: cfg.portExplicit,
|
|
295
|
-
configKey: "dev.port"
|
|
296
|
-
});
|
|
297
|
-
const dbPort = await pickStackPort(cfg.dbPort, {
|
|
298
|
-
fixed: cfg.dbPortExplicit,
|
|
299
|
-
host: "127.0.0.1",
|
|
300
|
-
configKey: "dev.dbPort"
|
|
301
|
-
});
|
|
302
|
-
const ready = {
|
|
303
|
-
...cfg,
|
|
304
|
-
port,
|
|
305
|
-
dbPort
|
|
306
|
-
};
|
|
307
|
-
createStack(devStack(ready));
|
|
308
|
-
const info = await bootstrapDev(ready);
|
|
309
|
-
const appPassword = info.appPassword ?? await createAdminAppPassword("kizlo");
|
|
310
|
-
const sync = await syncSiteSettings({
|
|
311
|
-
url: info.url,
|
|
312
|
-
username: info.username,
|
|
313
|
-
password: appPassword
|
|
314
|
-
}, {
|
|
315
|
-
secret: siteSecret,
|
|
316
|
-
backendUrl: baseUrl,
|
|
317
|
-
containerized: true
|
|
318
|
-
});
|
|
319
|
-
await composeStop();
|
|
320
|
-
return {
|
|
321
|
-
url: info.url,
|
|
322
|
-
username: info.username,
|
|
323
|
-
appPassword,
|
|
324
|
-
adminPassword: info.secrets?.password,
|
|
325
|
-
secretSyncError: sync.ok ? void 0 : sync.error
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
111
|
const init = defineCommand({
|
|
329
112
|
meta: {
|
|
330
113
|
name: "init",
|
|
@@ -409,63 +192,17 @@ const init = defineCommand({
|
|
|
409
192
|
if (!ok) p.log.warn(`Install it yourself: ${addDependencyArgs(pm, "kizlo@latest").join(" ")}`);
|
|
410
193
|
}
|
|
411
194
|
if (preset.apiPath && setup.baseUrl) setup.baseUrl = withApiPath(setup.baseUrl, preset.apiPath);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const local = await provisionLocalStack(devConfigFor(cwd, setup.devPath), setup.siteSecret, setup.baseUrl);
|
|
419
|
-
setup.wpUrl = local.url;
|
|
420
|
-
setup.wpUsername = local.username;
|
|
421
|
-
setup.wpPassword = local.appPassword;
|
|
422
|
-
adminPassword = local.adminPassword;
|
|
423
|
-
s.stop("Local WordPress ready");
|
|
424
|
-
if (local.secretSyncError) p.log.warn(`Could not sync KIZLO_DEV_SITE_SECRET to the local plugin (${local.secretSyncError})`);
|
|
425
|
-
} catch (error) {
|
|
426
|
-
s.stop("Local WordPress setup failed");
|
|
427
|
-
p.cancel(error instanceof Error ? error.message : String(error));
|
|
428
|
-
process.exit(1);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
const { keys, values: envValues } = managedEnv(preset, setup);
|
|
432
|
-
const envPath = path.join(cwd, ".env");
|
|
433
|
-
const envExisted = fs.existsSync(envPath);
|
|
434
|
-
const existingEnv = envExisted ? fs.readFileSync(envPath, "utf8") : "";
|
|
435
|
-
const conflicts = envKeysPresent(existingEnv, keys);
|
|
436
|
-
let overwriteKeys = new Set(keys);
|
|
437
|
-
if (conflicts.length && !force) if (yes) {
|
|
438
|
-
overwriteKeys = /* @__PURE__ */ new Set();
|
|
439
|
-
p.log.info("Keeping existing .env values (pass --force to overwrite)");
|
|
440
|
-
} else {
|
|
441
|
-
p.log.warn("Some environment variables already exist in .env");
|
|
442
|
-
if (!orCancel(await p.confirm({
|
|
443
|
-
message: "Overwrite their existing values?",
|
|
444
|
-
initialValue: true
|
|
445
|
-
}))) overwriteKeys = /* @__PURE__ */ new Set();
|
|
446
|
-
}
|
|
447
|
-
const merge = mergeEnv(existingEnv, envValues, overwriteKeys, envGroups(preset.baseUrlEnvKey));
|
|
448
|
-
fs.writeFileSync(envPath, merge.content);
|
|
449
|
-
if (setup.mode === "remote" && setup.wpUrl && setup.wpUsername && setup.wpPassword) {
|
|
450
|
-
const sync = await syncSiteSettings({
|
|
451
|
-
url: setup.wpUrl,
|
|
452
|
-
username: setup.wpUsername,
|
|
453
|
-
password: setup.wpPassword
|
|
454
|
-
}, {
|
|
455
|
-
secret: setup.siteSecret,
|
|
456
|
-
siteUrl: setup.siteUrl,
|
|
457
|
-
backendUrl: setup.baseUrl
|
|
458
|
-
});
|
|
459
|
-
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.`);
|
|
460
|
-
}
|
|
461
|
-
const exampleKeys = [preset.baseUrlEnvKey, ...PROD_WP_ENV_KEYS];
|
|
462
|
-
const { content: exampleEnv } = mergeEnv("", Object.fromEntries(exampleKeys.map((key) => [key, ""])), new Set(exampleKeys), envGroups(preset.baseUrlEnvKey));
|
|
463
|
-
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`;
|
|
464
|
-
const exampleCreated = writeFileIfAbsent(path.join(cwd, ".env.example"), exampleBody);
|
|
195
|
+
await setupLocalWordPress(cwd, setup);
|
|
196
|
+
await writeEnv(cwd, preset, setup, {
|
|
197
|
+
force,
|
|
198
|
+
yes
|
|
199
|
+
});
|
|
200
|
+
await syncRemote(setup);
|
|
465
201
|
const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
466
202
|
const serverDirRel = path.join(dirRel, "server");
|
|
467
203
|
const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
|
|
468
204
|
const scaffold = {
|
|
205
|
+
kizloDir: dirRel,
|
|
469
206
|
serverDirName: path.basename(serverDirRel),
|
|
470
207
|
serverEntryPath: path.join(serverDirRel, "index.ts"),
|
|
471
208
|
clientPath: path.join(dirRel, "client.ts"),
|
|
@@ -490,29 +227,45 @@ const init = defineCommand({
|
|
|
490
227
|
writeFileIfAbsent(path.join(cwd, generatedDirRel, "contract.json"), "{}\n");
|
|
491
228
|
writeFileIfAbsent(path.join(cwd, generatedDirRel, "index.ts"), CONTRACT_BARREL);
|
|
492
229
|
const gitignore = ensureGitignored(cwd, ".env");
|
|
493
|
-
if (!envExisted) p.log.success("Created .env");
|
|
494
|
-
else if (merge.updated.length || merge.added.length) p.log.success("Updated .env");
|
|
495
|
-
else p.log.info("Left .env unchanged");
|
|
496
|
-
p.log.success(exampleCreated ? "Created .env.example" : "Skipped .env.example (exists)");
|
|
497
230
|
for (const { file, result } of scaffolded) reportScaffold(file, result, yes);
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if (
|
|
231
|
+
for (const patch of templatePatches(preset.id)) {
|
|
232
|
+
const resolved = resolvePatch(patch, scaffold);
|
|
233
|
+
const abs = resolvePatchTargetPath(cwd, resolved.relPath);
|
|
234
|
+
if (!abs) {
|
|
235
|
+
p.log.info(`Skipped ${resolved.label} wiring — ${resolved.relPath} not found (add it yourself)`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const relTarget = path.relative(cwd, abs);
|
|
239
|
+
const src = fs.readFileSync(abs, "utf8");
|
|
240
|
+
let text;
|
|
241
|
+
let changes;
|
|
242
|
+
try {
|
|
243
|
+
({text, changes} = applyPatchToSource(src, resolved, { replaceConflicts: force }));
|
|
244
|
+
} catch (err) {
|
|
245
|
+
p.log.error(`Could not parse ${resolved.label} (${relTarget}) to wire Kizlo into it: ${err instanceof Error ? err.message : String(err)}`);
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
if (!force && !yes && changes.keptExports.length) {
|
|
249
|
+
p.log.warn(`${resolved.label} (${relTarget}) already defines ${changes.keptExports.join(", ")}`);
|
|
250
|
+
if (orCancel(await p.confirm({
|
|
251
|
+
message: "Replace with Kizlo's version?",
|
|
252
|
+
initialValue: true
|
|
253
|
+
}))) ({text, changes} = applyPatchToSource(src, resolved, { replaceConflicts: true }));
|
|
254
|
+
}
|
|
255
|
+
if (patchChanged(changes)) {
|
|
256
|
+
fs.writeFileSync(abs, text);
|
|
257
|
+
const parts = [
|
|
258
|
+
...changes.replacedExports.length ? [`replaced ${changes.replacedExports.join(", ")}`] : [],
|
|
259
|
+
...changes.addedExports.length ? [`added ${changes.addedExports.join(", ")}`] : [],
|
|
260
|
+
...changes.addedImports.length ? ["imports"] : []
|
|
261
|
+
];
|
|
262
|
+
p.log.success(`Wired Kizlo into your ${resolved.label} (${relTarget}) — ${parts.join(", ")}`);
|
|
263
|
+
}
|
|
264
|
+
if (changes.keptExports.length) p.log.info(`Kept your existing ${changes.keptExports.join(", ")} in ${resolved.label} — pass --force to replace`);
|
|
265
|
+
else if (!patchChanged(changes)) p.log.info(`Your ${resolved.label} is already wired (${relTarget})`);
|
|
502
266
|
}
|
|
503
|
-
p.
|
|
504
|
-
|
|
505
|
-
`Start your local WordPress dev stack (also watches your extensions):`,
|
|
506
|
-
` npx kizlo dev`,
|
|
507
|
-
``
|
|
508
|
-
] : [
|
|
509
|
-
`Watch your extensions and regenerate the contract during development:`,
|
|
510
|
-
` npx kizlo watch`,
|
|
511
|
-
``
|
|
512
|
-
],
|
|
513
|
-
`Generate the contract once for production builds:`,
|
|
514
|
-
` npx kizlo generate`
|
|
515
|
-
].join("\n"), "Next steps");
|
|
267
|
+
if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
|
|
268
|
+
nextStepsNote(setup.mode);
|
|
516
269
|
p.outro("Kizlo is ready 🎉");
|
|
517
270
|
}
|
|
518
271
|
});
|
package/dist/cli/index.js
CHANGED
|
@@ -7,9 +7,10 @@ runMain(defineCommand({
|
|
|
7
7
|
meta: {
|
|
8
8
|
name: "kizlo",
|
|
9
9
|
version: getVersion(),
|
|
10
|
-
description: "Kizlo CLI — headless WordPress
|
|
10
|
+
description: "Kizlo CLI — headless WordPress framework"
|
|
11
11
|
},
|
|
12
12
|
subCommands: {
|
|
13
|
+
create: () => import("./commands/create.js").then((m) => m.create),
|
|
13
14
|
init: () => import("./commands/init.js").then((m) => m.init),
|
|
14
15
|
watch: () => import("./commands/watch.js").then((m) => m.watch),
|
|
15
16
|
generate: () => import("./commands/generate.js").then((m) => m.generate),
|