kizlo 0.7.0 → 0.8.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/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 +152 -0
- package/dist/cli/commands/dev.js +1 -1
- 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/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 +6 -3
- 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 -185
- 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,14 @@
|
|
|
1
|
+
import { defineErrorMap } from "../shared/error.js";
|
|
2
|
+
|
|
3
|
+
//#region src/category/error.ts
|
|
4
|
+
const GET_CATEGORY_ERROR_MAP = defineErrorMap({ CATEGORY_NOT_FOUND: {
|
|
5
|
+
status: 404,
|
|
6
|
+
message: "Category not found"
|
|
7
|
+
} });
|
|
8
|
+
const LIST_CATEGORY_ERROR_MAP = defineErrorMap({ CATEGORY_INVALID_PAGE: {
|
|
9
|
+
status: 400,
|
|
10
|
+
message: "The requested page does not exist."
|
|
11
|
+
} });
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { GET_CATEGORY_ERROR_MAP, LIST_CATEGORY_ERROR_MAP };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createProcedure } from "../shared/procedure.js";
|
|
2
|
+
import { parseIdentifier } from "../shared/identifier.js";
|
|
3
|
+
import { deserializeListMetadata } from "../shared/serialize.js";
|
|
4
|
+
import { getTaxonomyService } from "../taxonomy/service.js";
|
|
5
|
+
import { GET_CATEGORY_ERROR_MAP, LIST_CATEGORY_ERROR_MAP } from "./error.js";
|
|
6
|
+
import { Category, CategoryList, ListCategoryInput, RetrieveCategoryInput } from "./schema.js";
|
|
7
|
+
import { deserializeCategory } from "./utils.js";
|
|
8
|
+
|
|
9
|
+
//#region src/category/index.ts
|
|
10
|
+
const CATEGORY_ROUTER_MAP = {
|
|
11
|
+
get: createProcedure({
|
|
12
|
+
scope: "api",
|
|
13
|
+
method: "GET",
|
|
14
|
+
path: "/categories/{identifier}",
|
|
15
|
+
params: RetrieveCategoryInput.pick({ identifier: true }),
|
|
16
|
+
output: Category,
|
|
17
|
+
errors: GET_CATEGORY_ERROR_MAP
|
|
18
|
+
}, async ({ input, context, errors }) => {
|
|
19
|
+
const taxonomy = getTaxonomyService("category", context.wordpress);
|
|
20
|
+
const identifier = parseIdentifier(input.params.identifier);
|
|
21
|
+
if (!identifier) throw errors.CATEGORY_NOT_FOUND();
|
|
22
|
+
const response = await taxonomy.get(identifier);
|
|
23
|
+
if (response.error) switch (response.error.code) {
|
|
24
|
+
case "invalid_taxonomy":
|
|
25
|
+
case "term_not_found":
|
|
26
|
+
case "rest_term_invalid": throw errors.CATEGORY_NOT_FOUND();
|
|
27
|
+
default:
|
|
28
|
+
context.logger.error("Get category unhandled error", response.error, {
|
|
29
|
+
identifier,
|
|
30
|
+
code: response.error.code
|
|
31
|
+
});
|
|
32
|
+
throw errors.INTERNAL_SERVER_ERROR();
|
|
33
|
+
}
|
|
34
|
+
return deserializeCategory(response.data);
|
|
35
|
+
}),
|
|
36
|
+
list: createProcedure({
|
|
37
|
+
scope: "api",
|
|
38
|
+
method: "GET",
|
|
39
|
+
path: "/categories",
|
|
40
|
+
query: ListCategoryInput.optional(),
|
|
41
|
+
output: CategoryList,
|
|
42
|
+
errors: LIST_CATEGORY_ERROR_MAP
|
|
43
|
+
}, async ({ input, context, errors }) => {
|
|
44
|
+
const response = await getTaxonomyService("category", context.wordpress).list({
|
|
45
|
+
context: "edit",
|
|
46
|
+
page: input.query?.page,
|
|
47
|
+
per_page: input.query?.perPage,
|
|
48
|
+
search: input.query?.search,
|
|
49
|
+
exclude: input.query?.exclude,
|
|
50
|
+
include: input.query?.include,
|
|
51
|
+
order: input.query?.order,
|
|
52
|
+
orderby: input.query?.orderBy,
|
|
53
|
+
hide_empty: input.query?.hideEmpty,
|
|
54
|
+
parent: input.query?.parent,
|
|
55
|
+
post: input.query?.post,
|
|
56
|
+
slug: input.query?.slug
|
|
57
|
+
});
|
|
58
|
+
if (response.error) switch (response.error.code) {
|
|
59
|
+
case "rest_post_invalid_page_number": throw errors.CATEGORY_INVALID_PAGE();
|
|
60
|
+
default:
|
|
61
|
+
context.logger.error("List categories unhandled error", response.error, { code: response.error.code });
|
|
62
|
+
throw errors.INTERNAL_SERVER_ERROR();
|
|
63
|
+
}
|
|
64
|
+
const list = context.wordpress.resolveList({
|
|
65
|
+
data: response.data,
|
|
66
|
+
headers: response.headers,
|
|
67
|
+
searchParams: input.query
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
meta: deserializeListMetadata(list.meta),
|
|
71
|
+
items: list.items.map((item) => deserializeCategory(item))
|
|
72
|
+
};
|
|
73
|
+
})
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { CATEGORY_ROUTER_MAP };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { IdentifierInput, ListMetadata, ListOrder } from "../shared/schema.js";
|
|
2
|
+
import { WP_CATEGORY_ORDER_BYS } from "../wordpress/category/types.js";
|
|
3
|
+
import { Seo } from "../seo/schema.js";
|
|
4
|
+
import { BooleanLike, Metadata, NumberLike, arrayable, lenient } from "@kizlo/shared";
|
|
5
|
+
import z from "zod/v4";
|
|
6
|
+
|
|
7
|
+
//#region src/category/schema.ts
|
|
8
|
+
const Category = z.object({
|
|
9
|
+
id: z.number(),
|
|
10
|
+
name: z.string(),
|
|
11
|
+
slug: z.string(),
|
|
12
|
+
url: z.string().nullable(),
|
|
13
|
+
description: z.string().nullable(),
|
|
14
|
+
parent: z.number().nullable(),
|
|
15
|
+
postCount: z.number(),
|
|
16
|
+
seo: Seo.nullable(),
|
|
17
|
+
meta: Metadata
|
|
18
|
+
});
|
|
19
|
+
const RetrieveCategoryInput = z.object({ identifier: IdentifierInput });
|
|
20
|
+
const CategoryList = z.object({
|
|
21
|
+
items: z.array(Category),
|
|
22
|
+
meta: ListMetadata
|
|
23
|
+
});
|
|
24
|
+
const CategoryOrderBy = z.enum(WP_CATEGORY_ORDER_BYS);
|
|
25
|
+
const ListCategoryInput = z.object({
|
|
26
|
+
page: NumberLike.pipe(z.number().int().min(1)).catch(1).optional(),
|
|
27
|
+
perPage: lenient(NumberLike.pipe(z.number().int().min(1).max(100))),
|
|
28
|
+
search: lenient(z.string()),
|
|
29
|
+
exclude: lenient(arrayable(NumberLike)),
|
|
30
|
+
include: lenient(arrayable(NumberLike)),
|
|
31
|
+
order: lenient(ListOrder),
|
|
32
|
+
orderBy: lenient(CategoryOrderBy),
|
|
33
|
+
hideEmpty: lenient(BooleanLike),
|
|
34
|
+
parent: lenient(NumberLike),
|
|
35
|
+
post: lenient(NumberLike),
|
|
36
|
+
slug: lenient(arrayable(z.string()))
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { Category, CategoryList, ListCategoryInput, RetrieveCategoryInput };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { deserializeSeo } from "../seo/utils.js";
|
|
2
|
+
import { stringifiedMetaRecord } from "@kizlo/shared";
|
|
3
|
+
|
|
4
|
+
//#region src/category/utils.ts
|
|
5
|
+
function deserializeCategory(data) {
|
|
6
|
+
return {
|
|
7
|
+
id: data.id,
|
|
8
|
+
name: data.name,
|
|
9
|
+
slug: data.slug,
|
|
10
|
+
url: data.kizlo.url ?? null,
|
|
11
|
+
description: data.description.length > 0 ? data.description : null,
|
|
12
|
+
parent: data.parent > 0 ? data.parent : null,
|
|
13
|
+
postCount: data.count,
|
|
14
|
+
seo: data.kizlo.seo ? deserializeSeo(data.kizlo.seo) : null,
|
|
15
|
+
meta: stringifiedMetaRecord(data.meta ?? {})
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { deserializeCategory };
|
package/dist/cli/banner.js
CHANGED
|
@@ -7,7 +7,7 @@ const KIZLO_CLI_WORD_MARK = [
|
|
|
7
7
|
"██║ ██╗██║███████╗███████╗╚██████╔╝",
|
|
8
8
|
"╚═╝ ╚═╝╚═╝╚══════╝╚══════╝ ╚═════╝ "
|
|
9
9
|
];
|
|
10
|
-
const KIZLO_TAGLINE = "Headless WordPress
|
|
10
|
+
const KIZLO_TAGLINE = "Headless WordPress framework for TypeScript";
|
|
11
11
|
function colorSupported() {
|
|
12
12
|
return Boolean(process.stdout.isTTY) && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb";
|
|
13
13
|
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { composeStop, createStack } from "../wp/docker.js";
|
|
2
|
+
import { ensureGitignored, envGroups, envKeysPresent, mergeEnv, pickStackPort, writeFileIfAbsent } from "../utils.js";
|
|
3
|
+
import { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveStackName } from "../daemon/config.js";
|
|
4
|
+
import { createAdminAppPassword } from "../wp/bootstrap.js";
|
|
5
|
+
import { bootstrapDev } from "../wp/dev.js";
|
|
6
|
+
import { removeProjectContainers } from "../wp/session.js";
|
|
7
|
+
import { syncSiteSettings } from "../wp/settings.js";
|
|
8
|
+
import { devStack } from "../wp/stack.js";
|
|
9
|
+
import z from "zod/v4";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import * as p from "@clack/prompts";
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
|
|
15
|
+
//#region src/cli/commands/_setup.ts
|
|
16
|
+
/** Production connection keys — what a real deploy needs, and what `.env.example` always lists. */
|
|
17
|
+
const PROD_WP_ENV_KEYS = [
|
|
18
|
+
"KIZLO_SITE_SECRET",
|
|
19
|
+
"KIZLO_WORDPRESS_URL",
|
|
20
|
+
"KIZLO_WORDPRESS_USERNAME",
|
|
21
|
+
"KIZLO_WORDPRESS_APPLICATION_PASSWORD"
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* The `.env` keys setup manages and the values to write, branched on the connection mode. A local
|
|
25
|
+
* dev stack writes the `KIZLO_DEV_WORDPRESS_*` / `KIZLO_DEV_SITE_SECRET` set plus `KIZLO_TARGET=dev`, so it
|
|
26
|
+
* never touches the production keys (a user can point those at a real site). A remote site writes the
|
|
27
|
+
* bare production keys exactly as before — no `KIZLO_TARGET`, since `"production"` is the default target.
|
|
28
|
+
*/
|
|
29
|
+
function managedEnv(preset, conn) {
|
|
30
|
+
if (conn.mode === "local") return {
|
|
31
|
+
keys: [
|
|
32
|
+
preset.baseUrlEnvKey,
|
|
33
|
+
"KIZLO_TARGET",
|
|
34
|
+
"KIZLO_DEV_SITE_SECRET",
|
|
35
|
+
"KIZLO_DEV_WORDPRESS_URL",
|
|
36
|
+
"KIZLO_DEV_WORDPRESS_USERNAME",
|
|
37
|
+
"KIZLO_DEV_WORDPRESS_APPLICATION_PASSWORD"
|
|
38
|
+
],
|
|
39
|
+
values: {
|
|
40
|
+
[preset.baseUrlEnvKey]: conn.baseUrl,
|
|
41
|
+
KIZLO_TARGET: "dev",
|
|
42
|
+
KIZLO_DEV_SITE_SECRET: conn.siteSecret,
|
|
43
|
+
KIZLO_DEV_WORDPRESS_URL: conn.wpUrl,
|
|
44
|
+
KIZLO_DEV_WORDPRESS_USERNAME: conn.wpUsername,
|
|
45
|
+
KIZLO_DEV_WORDPRESS_APPLICATION_PASSWORD: conn.wpPassword
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
keys: [preset.baseUrlEnvKey, ...PROD_WP_ENV_KEYS],
|
|
50
|
+
values: {
|
|
51
|
+
[preset.baseUrlEnvKey]: conn.baseUrl,
|
|
52
|
+
KIZLO_SITE_SECRET: conn.siteSecret,
|
|
53
|
+
KIZLO_WORDPRESS_URL: conn.wpUrl,
|
|
54
|
+
KIZLO_WORDPRESS_USERNAME: conn.wpUsername,
|
|
55
|
+
KIZLO_WORDPRESS_APPLICATION_PASSWORD: conn.wpPassword
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const requiredString = z.string().trim().min(1, "Required");
|
|
60
|
+
const urlString = requiredString.pipe(z.url("Must be a valid URL (e.g. https://example.com)"));
|
|
61
|
+
const dirPath = requiredString.refine((value) => !value.endsWith(".ts"), "Enter a directory, not a file");
|
|
62
|
+
function validate(schema) {
|
|
63
|
+
return (value) => {
|
|
64
|
+
const result = schema.safeParse(value ?? "");
|
|
65
|
+
return result.success ? void 0 : result.error.issues[0]?.message;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function orCancel(value) {
|
|
69
|
+
if (p.isCancel(value)) {
|
|
70
|
+
p.cancel("Setup cancelled.");
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
/** Appends the API path to the base URL so the client and route handler agree. */
|
|
76
|
+
function withApiPath(baseUrl, apiPath) {
|
|
77
|
+
try {
|
|
78
|
+
const url = new URL(baseUrl);
|
|
79
|
+
const current = url.pathname.replace(/\/+$/, "");
|
|
80
|
+
if (current.endsWith(apiPath)) return url.toString().replace(/\/+$/, "");
|
|
81
|
+
url.pathname = `${current}${apiPath}`;
|
|
82
|
+
return url.toString().replace(/\/+$/, "");
|
|
83
|
+
} catch {
|
|
84
|
+
return baseUrl;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Collect the WordPress connection interactively: the backend/site URLs, the webhook signing
|
|
89
|
+
* secret, and either the local-dev folder or a remote site's credentials. The caller layers any
|
|
90
|
+
* framework-specific prompts (directory, import alias) on top of what this returns.
|
|
91
|
+
*/
|
|
92
|
+
async function collectConnectionInteractively(preset) {
|
|
93
|
+
let siteUrl;
|
|
94
|
+
let baseUrl;
|
|
95
|
+
if (preset.apiPath) baseUrl = orCancel(await p.text({
|
|
96
|
+
message: "Public app URL",
|
|
97
|
+
placeholder: "https://your-app.com",
|
|
98
|
+
validate: validate(urlString)
|
|
99
|
+
}));
|
|
100
|
+
else {
|
|
101
|
+
siteUrl = orCancel(await p.text({
|
|
102
|
+
message: "Public site URL",
|
|
103
|
+
placeholder: "https://your-app.com",
|
|
104
|
+
validate: validate(urlString)
|
|
105
|
+
}));
|
|
106
|
+
baseUrl = orCancel(await p.text({
|
|
107
|
+
message: "Kizlo backend URL (where the handler is mounted)",
|
|
108
|
+
placeholder: "https://your-app.com/kizlo",
|
|
109
|
+
validate: validate(urlString)
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
const siteSecret = orCancel(await p.select({
|
|
113
|
+
message: "Site secret (webhook signing key)",
|
|
114
|
+
initialValue: "generate",
|
|
115
|
+
options: [{
|
|
116
|
+
value: "generate",
|
|
117
|
+
label: "Generate a secure secret automatically",
|
|
118
|
+
hint: "recommended"
|
|
119
|
+
}, {
|
|
120
|
+
value: "enter",
|
|
121
|
+
label: "Enter my own"
|
|
122
|
+
}]
|
|
123
|
+
})) === "enter" ? orCancel(await p.password({
|
|
124
|
+
message: "Enter the site secret",
|
|
125
|
+
validate: validate(requiredString)
|
|
126
|
+
})) : randomBytes(32).toString("hex");
|
|
127
|
+
const mode = orCancel(await p.select({
|
|
128
|
+
message: "WordPress connection",
|
|
129
|
+
initialValue: "local",
|
|
130
|
+
options: [{
|
|
131
|
+
value: "local",
|
|
132
|
+
label: "Set up a local dev environment",
|
|
133
|
+
hint: "runs WordPress in Docker"
|
|
134
|
+
}, {
|
|
135
|
+
value: "remote",
|
|
136
|
+
label: "Use my own WordPress",
|
|
137
|
+
hint: "connect to an existing site"
|
|
138
|
+
}]
|
|
139
|
+
}));
|
|
140
|
+
let wpUrl = "";
|
|
141
|
+
let wpUsername = "";
|
|
142
|
+
let wpPassword = "";
|
|
143
|
+
let devPath;
|
|
144
|
+
if (mode === "remote") {
|
|
145
|
+
wpUrl = orCancel(await p.text({
|
|
146
|
+
message: "WordPress URL",
|
|
147
|
+
placeholder: "https://wp.your-app.com",
|
|
148
|
+
validate: validate(urlString)
|
|
149
|
+
}));
|
|
150
|
+
wpUsername = orCancel(await p.text({
|
|
151
|
+
message: "WordPress username",
|
|
152
|
+
validate: validate(requiredString)
|
|
153
|
+
}));
|
|
154
|
+
wpPassword = orCancel(await p.password({
|
|
155
|
+
message: "WordPress application password",
|
|
156
|
+
validate: validate(requiredString)
|
|
157
|
+
}));
|
|
158
|
+
} else devPath = orCancel(await p.text({
|
|
159
|
+
message: "Local WordPress folder",
|
|
160
|
+
initialValue: "wordpress",
|
|
161
|
+
validate: validate(dirPath)
|
|
162
|
+
}));
|
|
163
|
+
return {
|
|
164
|
+
mode,
|
|
165
|
+
baseUrl,
|
|
166
|
+
siteUrl,
|
|
167
|
+
siteSecret,
|
|
168
|
+
wpUrl,
|
|
169
|
+
wpUsername,
|
|
170
|
+
wpPassword,
|
|
171
|
+
devPath
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Non-interactive connection: skip prompts and use env values where present. Missing ones are
|
|
176
|
+
* left empty for the user to fill in later. Never fails — always yields a fillable project.
|
|
177
|
+
*/
|
|
178
|
+
function collectConnectionFromEnv(preset) {
|
|
179
|
+
return {
|
|
180
|
+
mode: "remote",
|
|
181
|
+
baseUrl: process.env[preset.baseUrlEnvKey]?.trim() ?? "",
|
|
182
|
+
siteSecret: process.env.KIZLO_SITE_SECRET?.trim() || randomBytes(32).toString("hex"),
|
|
183
|
+
wpUrl: process.env.KIZLO_WORDPRESS_URL?.trim() ?? "",
|
|
184
|
+
wpUsername: process.env.KIZLO_WORDPRESS_USERNAME?.trim() ?? "",
|
|
185
|
+
wpPassword: process.env.KIZLO_WORDPRESS_APPLICATION_PASSWORD?.trim() ?? ""
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** Build a {@link ResolvedDevConfig} from the chosen install folder, matching `resolveDevConfig`'s
|
|
189
|
+
* defaults — built directly so setup never has to round-trip through the config file it's writing. */
|
|
190
|
+
function devConfigFor(cwd, devPath) {
|
|
191
|
+
return {
|
|
192
|
+
configDir: cwd,
|
|
193
|
+
project: `${resolveStackName(cwd)}-dev`,
|
|
194
|
+
port: DEFAULT_DEV_PORT,
|
|
195
|
+
portExplicit: false,
|
|
196
|
+
dbPort: DEFAULT_DEV_DB_PORT,
|
|
197
|
+
dbPortExplicit: false,
|
|
198
|
+
fixtures: [],
|
|
199
|
+
wordpressPath: devPath,
|
|
200
|
+
wordpressDir: path.resolve(cwd, devPath)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Boot the local dev stack once to produce working credentials, then stop it (volumes
|
|
205
|
+
* persist, so a later `kizlo dev` resumes instantly). The dev stack mints no application
|
|
206
|
+
* password — that's a test-stack concern — so we create one here for REST auth in `.env`.
|
|
207
|
+
* While the stack is still up, push the dev site settings (`siteSecret` plus the Kizlo server's
|
|
208
|
+
* `url`/`backend_url`, derived from `baseUrl`) into the plugin so webhook signing and event delivery
|
|
209
|
+
* work.
|
|
210
|
+
*/
|
|
211
|
+
async function provisionLocalStack(cfg, siteSecret, baseUrl) {
|
|
212
|
+
await removeProjectContainers(cfg.project);
|
|
213
|
+
const port = await pickStackPort(cfg.port, {
|
|
214
|
+
fixed: cfg.portExplicit,
|
|
215
|
+
configKey: "dev.port"
|
|
216
|
+
});
|
|
217
|
+
const dbPort = await pickStackPort(cfg.dbPort, {
|
|
218
|
+
fixed: cfg.dbPortExplicit,
|
|
219
|
+
host: "127.0.0.1",
|
|
220
|
+
configKey: "dev.dbPort"
|
|
221
|
+
});
|
|
222
|
+
const ready = {
|
|
223
|
+
...cfg,
|
|
224
|
+
port,
|
|
225
|
+
dbPort
|
|
226
|
+
};
|
|
227
|
+
createStack(devStack(ready));
|
|
228
|
+
const info = await bootstrapDev(ready);
|
|
229
|
+
const appPassword = info.appPassword ?? await createAdminAppPassword("kizlo");
|
|
230
|
+
const sync = await syncSiteSettings({
|
|
231
|
+
url: info.url,
|
|
232
|
+
username: info.username,
|
|
233
|
+
password: appPassword
|
|
234
|
+
}, {
|
|
235
|
+
secret: siteSecret,
|
|
236
|
+
backendUrl: baseUrl,
|
|
237
|
+
containerized: true
|
|
238
|
+
});
|
|
239
|
+
await composeStop();
|
|
240
|
+
return {
|
|
241
|
+
url: info.url,
|
|
242
|
+
username: info.username,
|
|
243
|
+
appPassword,
|
|
244
|
+
adminPassword: info.secrets?.password,
|
|
245
|
+
secretSyncError: sync.ok ? void 0 : sync.error
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Provision the local WordPress dev stack and fill the connection's WP credentials from it, then
|
|
250
|
+
* report the outcome. Mutates `conn` in place (wpUrl / wpUsername / wpPassword). Exits on failure,
|
|
251
|
+
* matching the previous inline behavior. No-op for a remote connection.
|
|
252
|
+
*/
|
|
253
|
+
async function setupLocalWordPress(cwd, conn) {
|
|
254
|
+
if (conn.mode !== "local" || !conn.devPath) return;
|
|
255
|
+
ensureGitignored(cwd, conn.devPath);
|
|
256
|
+
const s = p.spinner();
|
|
257
|
+
s.start("Setting up local WordPress (first run downloads images, this can take a while)");
|
|
258
|
+
try {
|
|
259
|
+
const local = await provisionLocalStack(devConfigFor(cwd, conn.devPath), conn.siteSecret, conn.baseUrl);
|
|
260
|
+
conn.wpUrl = local.url;
|
|
261
|
+
conn.wpUsername = local.username;
|
|
262
|
+
conn.wpPassword = local.appPassword;
|
|
263
|
+
s.stop("Local WordPress ready");
|
|
264
|
+
if (local.secretSyncError) p.log.warn(`Could not sync KIZLO_DEV_SITE_SECRET to the local plugin (${local.secretSyncError})`);
|
|
265
|
+
p.log.success(`Local WordPress configured (${conn.devPath}) — .env points at ${conn.wpUrl}`);
|
|
266
|
+
if (local.adminPassword) p.log.warn(`wp-admin login: ${conn.wpUsername} / ${local.adminPassword} — save it, it's shown only once`);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
s.stop("Local WordPress setup failed");
|
|
269
|
+
p.cancel(error instanceof Error ? error.message : String(error));
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Write (or update) `.env` and `.env.example` for the managed keys and report the outcome.
|
|
275
|
+
* Existing conflicting `.env` values are preserved unless `force`; interactively the user is asked,
|
|
276
|
+
* and under `--yes` they are kept. `.env.example` is only written when absent.
|
|
277
|
+
*/
|
|
278
|
+
async function writeEnv(cwd, preset, conn, opts) {
|
|
279
|
+
const { keys, values: envValues } = managedEnv(preset, conn);
|
|
280
|
+
const envPath = path.join(cwd, ".env");
|
|
281
|
+
const envExisted = fs.existsSync(envPath);
|
|
282
|
+
const existingEnv = envExisted ? fs.readFileSync(envPath, "utf8") : "";
|
|
283
|
+
const conflicts = envKeysPresent(existingEnv, keys);
|
|
284
|
+
let overwriteKeys = new Set(keys);
|
|
285
|
+
if (conflicts.length && !opts.force) if (opts.yes) {
|
|
286
|
+
overwriteKeys = /* @__PURE__ */ new Set();
|
|
287
|
+
p.log.info("Keeping existing .env values (pass --force to overwrite)");
|
|
288
|
+
} else {
|
|
289
|
+
p.log.warn("Some environment variables already exist in .env");
|
|
290
|
+
if (!orCancel(await p.confirm({
|
|
291
|
+
message: "Overwrite their existing values?",
|
|
292
|
+
initialValue: true
|
|
293
|
+
}))) overwriteKeys = /* @__PURE__ */ new Set();
|
|
294
|
+
}
|
|
295
|
+
const merge = mergeEnv(existingEnv, envValues, overwriteKeys, envGroups(preset.baseUrlEnvKey));
|
|
296
|
+
fs.writeFileSync(envPath, merge.content);
|
|
297
|
+
const exampleKeys = [preset.baseUrlEnvKey, ...PROD_WP_ENV_KEYS];
|
|
298
|
+
const { content: exampleEnv } = mergeEnv("", Object.fromEntries(exampleKeys.map((key) => [key, ""])), new Set(exampleKeys), envGroups(preset.baseUrlEnvKey));
|
|
299
|
+
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`;
|
|
300
|
+
const exampleCreated = writeFileIfAbsent(path.join(cwd, ".env.example"), exampleBody);
|
|
301
|
+
if (!envExisted) p.log.success("Created .env");
|
|
302
|
+
else if (merge.updated.length || merge.added.length) p.log.success("Updated .env");
|
|
303
|
+
else p.log.info("Left .env unchanged");
|
|
304
|
+
p.log.success(exampleCreated ? "Created .env.example" : "Skipped .env.example (exists)");
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Push the site settings (secret, canonical site URL, backend URL) to a remote WordPress so
|
|
308
|
+
* webhook signing and event delivery work. No-op for local (handled during provisioning) or when
|
|
309
|
+
* credentials are incomplete. Warns and continues on failure.
|
|
310
|
+
*/
|
|
311
|
+
async function syncRemote(conn) {
|
|
312
|
+
if (conn.mode !== "remote" || !conn.wpUrl || !conn.wpUsername || !conn.wpPassword) return;
|
|
313
|
+
const sync = await syncSiteSettings({
|
|
314
|
+
url: conn.wpUrl,
|
|
315
|
+
username: conn.wpUsername,
|
|
316
|
+
password: conn.wpPassword
|
|
317
|
+
}, {
|
|
318
|
+
secret: conn.siteSecret,
|
|
319
|
+
siteUrl: conn.siteUrl,
|
|
320
|
+
backendUrl: conn.baseUrl
|
|
321
|
+
});
|
|
322
|
+
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.`);
|
|
323
|
+
}
|
|
324
|
+
/** The shared "Next steps" note, branched on whether a local dev stack was set up. */
|
|
325
|
+
function nextStepsNote(mode, prefix = "") {
|
|
326
|
+
p.note([
|
|
327
|
+
...mode === "local" ? [
|
|
328
|
+
`Start your local WordPress dev stack (also watches your extensions):`,
|
|
329
|
+
` ${prefix}npx kizlo dev`,
|
|
330
|
+
``
|
|
331
|
+
] : [
|
|
332
|
+
`Watch your extensions and regenerate the contract during development:`,
|
|
333
|
+
` ${prefix}npx kizlo watch`,
|
|
334
|
+
``
|
|
335
|
+
],
|
|
336
|
+
`Generate the contract once for production builds:`,
|
|
337
|
+
` ${prefix}npx kizlo generate`
|
|
338
|
+
].join("\n"), "Next steps");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
//#endregion
|
|
342
|
+
export { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv };
|
|
@@ -0,0 +1,152 @@
|
|
|
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.
|
|
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
|
+
if (!fs.existsSync(pkgPath)) {
|
|
53
|
+
fs.rmSync(dir, {
|
|
54
|
+
recursive: true,
|
|
55
|
+
force: true
|
|
56
|
+
});
|
|
57
|
+
throw new Error(`Template "${template}" wasn't found at ${source.remote ?? source.local}. This ref may predate the templates — retry with KIZLO_TEMPLATE_REF=main.`);
|
|
58
|
+
}
|
|
59
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
60
|
+
pkg.name = name;
|
|
61
|
+
if (pkg.dependencies?.kizlo === "workspace:*") pkg.dependencies.kizlo = `^${getVersion()}`;
|
|
62
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
|
|
63
|
+
}
|
|
64
|
+
const create = defineCommand({
|
|
65
|
+
meta: {
|
|
66
|
+
name: "create",
|
|
67
|
+
description: "Scaffold a new project with Kizlo already wired"
|
|
68
|
+
},
|
|
69
|
+
args: {
|
|
70
|
+
template: {
|
|
71
|
+
type: "positional",
|
|
72
|
+
required: false,
|
|
73
|
+
description: `Template to scaffold from (${TEMPLATES.join(", ")})`
|
|
74
|
+
},
|
|
75
|
+
name: {
|
|
76
|
+
type: "positional",
|
|
77
|
+
required: false,
|
|
78
|
+
description: "Project folder to create"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
async run({ args }) {
|
|
82
|
+
const cwd = process.cwd();
|
|
83
|
+
printBanner(getVersion());
|
|
84
|
+
p.intro("kizlo create");
|
|
85
|
+
const requested = args.template;
|
|
86
|
+
let template;
|
|
87
|
+
if (requested) {
|
|
88
|
+
if (!isTemplate(requested)) {
|
|
89
|
+
p.cancel(`Unknown template "${requested}". Available: ${TEMPLATES.join(", ")}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
template = requested;
|
|
93
|
+
} else template = orCancel(await p.select({
|
|
94
|
+
message: "Template",
|
|
95
|
+
options: TEMPLATES.map((id) => ({
|
|
96
|
+
value: id,
|
|
97
|
+
label: id
|
|
98
|
+
}))
|
|
99
|
+
}));
|
|
100
|
+
const name = args.name ? String(args.name).trim() : orCancel(await p.text({
|
|
101
|
+
message: "Project name",
|
|
102
|
+
placeholder: "my-app",
|
|
103
|
+
validate: projectName
|
|
104
|
+
}));
|
|
105
|
+
if (!args.name) {
|
|
106
|
+
const check = projectName(name);
|
|
107
|
+
if (check) {
|
|
108
|
+
p.cancel(check);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const dir = path.resolve(cwd, name);
|
|
113
|
+
if (fs.existsSync(dir)) {
|
|
114
|
+
p.cancel(`${name} already exists — pick a different name or remove it.`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const preset = getPreset(template);
|
|
118
|
+
if (!preset) {
|
|
119
|
+
p.cancel(`No preset for template "${template}".`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const conn = await collectConnectionInteractively(preset);
|
|
123
|
+
if (preset.apiPath && conn.baseUrl) conn.baseUrl = withApiPath(conn.baseUrl, preset.apiPath);
|
|
124
|
+
if (conn.mode === "local" && !await dockerAvailable()) {
|
|
125
|
+
p.cancel("Docker isn't available — start Docker (or install it) and re-run, or choose “Use my own WordPress”.");
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
const s = p.spinner();
|
|
129
|
+
s.start(`Creating ${name} from the ${template} template`);
|
|
130
|
+
try {
|
|
131
|
+
await scaffoldTemplate(template, dir, name);
|
|
132
|
+
s.stop(`Created ${name}`);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
s.stop("Could not create the project");
|
|
135
|
+
p.cancel(error instanceof Error ? error.message : String(error));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
await setupLocalWordPress(dir, conn);
|
|
139
|
+
await writeEnv(dir, preset, conn, {
|
|
140
|
+
force: true,
|
|
141
|
+
yes: false
|
|
142
|
+
});
|
|
143
|
+
await syncRemote(conn);
|
|
144
|
+
const pm = detectPackageManager(dir);
|
|
145
|
+
p.note([`cd ${name}`, `${pm} install`].join("\n"), "Get started");
|
|
146
|
+
nextStepsNote(conn.mode);
|
|
147
|
+
p.outro("Kizlo is ready 🎉");
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
export { create };
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -2,11 +2,11 @@ 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";
|