kizlo 0.10.0 → 0.12.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/README.md +4 -4
- package/dist/astro/server.d.ts +9 -0
- package/dist/astro/server.js +10 -0
- package/dist/astro.d.ts +2 -0
- package/dist/astro.js +3 -0
- package/dist/cli/commands/_setup.js +19 -10
- package/dist/cli/commands/create.js +102 -72
- package/dist/cli/commands/dev.js +113 -20
- package/dist/cli/commands/generate.js +4 -0
- package/dist/cli/commands/init.js +111 -57
- package/dist/cli/daemon/config.js +13 -7
- package/dist/cli/daemon/generate.js +2 -3
- package/dist/cli/daemon/jiti.js +101 -0
- package/dist/cli/daemon/logger.js +1 -1
- package/dist/cli/daemon/reload.js +66 -0
- package/dist/cli/daemon/watch.js +7 -4
- package/dist/cli/presets/source.js +122 -24
- package/dist/cli/presets/template.js +75 -17
- package/dist/cli/wp/settings.js +8 -2
- package/dist/comment/schema.js +2 -2
- package/dist/context.d.ts +9 -0
- package/dist/context.js +20 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/integrations/astro/kizlo.client.d.ts +15 -0
- package/dist/integrations/astro/kizlo.client.js +13 -0
- package/dist/integrations/astro/kizlo.server.d.ts +10 -0
- package/dist/integrations/astro/kizlo.server.js +12 -0
- package/dist/integrations/astro/manifest.d.ts +9 -0
- package/dist/integrations/astro/manifest.js +11 -0
- package/dist/integrations/astro/metadata.d.ts +64 -0
- package/dist/integrations/astro/metadata.js +71 -0
- package/dist/integrations/astro/robots.d.ts +8 -0
- package/dist/integrations/astro/robots.js +11 -0
- package/dist/integrations/astro/sitemap.d.ts +16 -0
- package/dist/integrations/astro/sitemap.js +20 -0
- package/dist/integrations/astro/utils.d.ts +19 -0
- package/dist/integrations/astro/utils.js +17 -0
- package/dist/integrations/nextjs/metadata.d.ts +1 -0
- package/dist/integrations/nextjs/metadata.js +1 -0
- package/dist/kizlo.d.ts +8 -1
- package/dist/kizlo.js +16 -13
- package/dist/nextjs/server.d.ts +2 -1
- package/dist/nextjs/server.js +2 -1
- package/dist/package.js +1 -1
- package/dist/page/schema.js +2 -2
- package/dist/post/schema.js +2 -2
- package/dist/seo/utils.d.ts +8 -2
- package/dist/seo/utils.js +9 -1
- package/dist/shared/constants.js +9 -1
- package/dist/shared/schema.d.ts +1 -2
- package/dist/shared/schema.js +2 -2
- package/dist/wordpress/service.d.ts +4 -0
- package/dist/wordpress/service.js +5 -0
- package/package.json +18 -2
- package/dist/cli/presets/base.js +0 -40
- package/dist/cli/presets/index.js +0 -16
- package/dist/cli/presets/nextjs.js +0 -16
package/README.md
CHANGED
|
@@ -23,16 +23,16 @@ Kizlo is an open-source framework for building content-driven applications with
|
|
|
23
23
|
|
|
24
24
|
## Quick start
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Scaffold a new project with Kizlo already wired:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
npx kizlo@latest
|
|
29
|
+
npx kizlo@latest create
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Or add Kizlo to an existing project:
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
npx kizlo
|
|
35
|
+
npx kizlo@latest init
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
See the [docs](https://kizlo.io/docs) for the full guide.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { WEB_MANIFEST_ROUTE } from "../brand/manifest.js";
|
|
2
|
+
import { renderJsonLd } from "../seo/utils.js";
|
|
3
|
+
import { KizloServerOptions, createKizlo } from "../integrations/astro/kizlo.server.js";
|
|
4
|
+
import { AstroRoute, AstroRouteContext, getServerBaseUrl } from "../integrations/astro/utils.js";
|
|
5
|
+
import { createManifestRoute } from "../integrations/astro/manifest.js";
|
|
6
|
+
import { PageHead, RootHead, ThemeColor, resolvePageHead, resolveRootHead } from "../integrations/astro/metadata.js";
|
|
7
|
+
import { createRobotsRoute } from "../integrations/astro/robots.js";
|
|
8
|
+
import { createSitemapRedirectRoute, createSitemapRoute } from "../integrations/astro/sitemap.js";
|
|
9
|
+
export { AstroRoute, AstroRouteContext, KizloServerOptions, PageHead, RootHead, ThemeColor, WEB_MANIFEST_ROUTE, createKizlo, createManifestRoute, createRobotsRoute, createSitemapRedirectRoute, createSitemapRoute, getServerBaseUrl, renderJsonLd, resolvePageHead, resolveRootHead };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { WEB_MANIFEST_ROUTE } from "../brand/manifest.js";
|
|
2
|
+
import { renderJsonLd } from "../seo/utils.js";
|
|
3
|
+
import { getServerBaseUrl } from "../integrations/astro/utils.js";
|
|
4
|
+
import { createKizlo } from "../integrations/astro/kizlo.server.js";
|
|
5
|
+
import { createManifestRoute } from "../integrations/astro/manifest.js";
|
|
6
|
+
import { resolvePageHead, resolveRootHead } from "../integrations/astro/metadata.js";
|
|
7
|
+
import { createRobotsRoute } from "../integrations/astro/robots.js";
|
|
8
|
+
import { createSitemapRedirectRoute, createSitemapRoute } from "../integrations/astro/sitemap.js";
|
|
9
|
+
|
|
10
|
+
export { WEB_MANIFEST_ROUTE, createKizlo, createManifestRoute, createRobotsRoute, createSitemapRedirectRoute, createSitemapRoute, getServerBaseUrl, renderJsonLd, resolvePageHead, resolveRootHead };
|
package/dist/astro.d.ts
ADDED
package/dist/astro.js
ADDED
|
@@ -7,6 +7,7 @@ import { bootstrapDev } from "../wp/dev.js";
|
|
|
7
7
|
import { removeProjectContainers } from "../wp/session.js";
|
|
8
8
|
import { syncSiteSettings } from "../wp/settings.js";
|
|
9
9
|
import { devStack } from "../wp/stack.js";
|
|
10
|
+
import { isPluginVersionSupported, pluginUpdateMessage } from "@kizlo/shared";
|
|
10
11
|
import z from "zod/v4";
|
|
11
12
|
import path from "node:path";
|
|
12
13
|
import fs from "node:fs";
|
|
@@ -16,13 +17,13 @@ import getPort, { portNumbers } from "get-port";
|
|
|
16
17
|
|
|
17
18
|
//#region src/cli/commands/_setup.ts
|
|
18
19
|
/**
|
|
19
|
-
* The resolved `.env` key names the scaffold writes: the template's declared `env` when it has one,
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `
|
|
20
|
+
* The resolved `.env` key names the scaffold writes: the template's declared `env` when it has one, else
|
|
21
|
+
* the {@link DEFAULT_ENV_KEYS} fallback — for an in-repo template before the first stamped release, and
|
|
22
|
+
* for the base no-template fallback (which has no manifest). Threaded into `managedEnv`, `writeEnv`, and
|
|
23
|
+
* `collectConnectionFromEnv` so the scaffold writes the names the pinned runtime reads.
|
|
23
24
|
*/
|
|
24
|
-
function resolveEnvKeys(
|
|
25
|
-
return manifest?.env ??
|
|
25
|
+
function resolveEnvKeys(manifest) {
|
|
26
|
+
return manifest?.env ?? DEFAULT_ENV_KEYS;
|
|
26
27
|
}
|
|
27
28
|
/** Remote connection keys — what a real deploy needs, and what `.env.example` always lists. */
|
|
28
29
|
function remoteKeys(envKeys) {
|
|
@@ -80,7 +81,11 @@ function managedEnv(envKeys, conn) {
|
|
|
80
81
|
};
|
|
81
82
|
}
|
|
82
83
|
const requiredString = z.string().trim().min(1, "Required");
|
|
83
|
-
const urlString = requiredString.pipe(z.url(
|
|
84
|
+
const urlString = requiredString.pipe(z.url({
|
|
85
|
+
protocol: /^https?$/,
|
|
86
|
+
hostname: /^(localhost|(\d{1,3}\.){3}\d{1,3}|([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})$/,
|
|
87
|
+
error: "Must be a valid http(s) URL with a real host (e.g. https://example.com)"
|
|
88
|
+
}));
|
|
84
89
|
const dirPath = requiredString.refine((value) => !value.endsWith(".ts"), "Enter a directory, not a file");
|
|
85
90
|
function validate(schema) {
|
|
86
91
|
return (value) => {
|
|
@@ -130,11 +135,11 @@ function pickAppPort() {
|
|
|
130
135
|
* `create` scaffolds a brand-new app that has no public URL yet, so it defaults to the local dev
|
|
131
136
|
* origin and lets the user fill in production later.
|
|
132
137
|
*/
|
|
133
|
-
async function collectConnectionInteractively(
|
|
138
|
+
async function collectConnectionInteractively(apiPath, opts = {}) {
|
|
134
139
|
let siteUrl;
|
|
135
140
|
let baseUrl;
|
|
136
141
|
if (opts.baseUrl !== void 0) baseUrl = opts.baseUrl;
|
|
137
|
-
else if (
|
|
142
|
+
else if (apiPath) baseUrl = orCancel(await p.text({
|
|
138
143
|
message: "Public app URL",
|
|
139
144
|
placeholder: "https://your-app.com",
|
|
140
145
|
validate: validate(urlString)
|
|
@@ -382,7 +387,11 @@ async function syncRemote(conn) {
|
|
|
382
387
|
siteUrl: conn.siteUrl,
|
|
383
388
|
backendUrl: conn.baseUrl
|
|
384
389
|
});
|
|
385
|
-
if (!sync.ok)
|
|
390
|
+
if (!sync.ok) {
|
|
391
|
+
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.`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (!isPluginVersionSupported(sync.pluginVersion)) p.log.warn(pluginUpdateMessage(sync.pluginVersion));
|
|
386
395
|
}
|
|
387
396
|
/** The "Next steps" lines. `kizlo dev` is the single entry point for development. */
|
|
388
397
|
function nextStepsLines(conn, prefix = "") {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { availablePackageManagers, detectInvokingPackageManager, ensureGitignored, frameworkCreateArgs, installArgs, runCommandAsync, runCommandCaptured } from "../utils.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { adaptFile, changesFor, fileEntries, isExample, minCliError, patchEntries, readManifest, resolveDependencies } from "../presets/template.js";
|
|
2
|
+
import { fetchRegistry, listTemplates, resolveRegistry } from "../presets/source.js";
|
|
3
|
+
import { adaptFile, changesFor, fileEntries, minCliError, patchEntries, readManifest, resolveDependencies } from "../presets/template.js";
|
|
5
4
|
import { collectConnectionInteractively, nextStepsLines, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
|
|
6
5
|
import { applyLayoutPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
|
|
7
6
|
import z from "zod/v4";
|
|
@@ -11,15 +10,30 @@ import { defineCommand } from "citty";
|
|
|
11
10
|
import * as p from "@clack/prompts";
|
|
12
11
|
|
|
13
12
|
//#region src/cli/commands/create.ts
|
|
13
|
+
/** The final path segment of a project name — the app folder itself; everything before it is where it lands. */
|
|
14
|
+
function folderSegment(value) {
|
|
15
|
+
const segments = value.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
16
|
+
return segments[segments.length - 1] ?? "";
|
|
17
|
+
}
|
|
14
18
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
19
|
+
* Tidy the entered name/path for both the target dir and the display: trim, drop a leading `./`, and drop a
|
|
20
|
+
* trailing separator, so `cd`, the spinner, and the warning lines read as `templates/my-app`, not
|
|
21
|
+
* `./templates/my-app/`. Preserves `../` and absolute paths (only a leading `./` is redundant).
|
|
17
22
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return TEMPLATES.includes(value);
|
|
23
|
+
function normalizeProjectName(value) {
|
|
24
|
+
return value.trim().replace(/^\.\/+/, "").replace(/[/\\]+$/, "");
|
|
21
25
|
}
|
|
22
|
-
|
|
26
|
+
/**
|
|
27
|
+
* A project name may be a bare folder (`my-app`) or a path to scaffold somewhere other than the current
|
|
28
|
+
* dir — relative (`apps/my-app`, `../my-app`) or absolute (`/srv/my-app`). Only the last segment is the
|
|
29
|
+
* app folder, so that's what's validated as a name; the parent dirs (which may not exist yet) are created
|
|
30
|
+
* by the framework's own CLI. `.`/`..` as the final segment, or a name with no real segment, is rejected.
|
|
31
|
+
*/
|
|
32
|
+
const projectNameSchema = z.string().trim().min(1, "Required").refine((value) => {
|
|
33
|
+
const base = folderSegment(value);
|
|
34
|
+
return base.length > 0 && base !== "." && base !== ".." && /^[A-Za-z0-9._-]+$/.test(base);
|
|
35
|
+
}, "The app folder (last path segment) can only contain letters, numbers, dots, and dashes");
|
|
36
|
+
const projectName = validate(projectNameSchema);
|
|
23
37
|
/** Package managers `create` can wire the getting-started steps for, in display order. */
|
|
24
38
|
const PACKAGE_MANAGERS = [
|
|
25
39
|
"pnpm",
|
|
@@ -37,17 +51,25 @@ function recordDependencies(dir, manifest) {
|
|
|
37
51
|
const pkgPath = path.join(dir, "package.json");
|
|
38
52
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
39
53
|
const { dependencies, devDependencies } = resolveDependencies(manifest);
|
|
40
|
-
pkg.dependencies = {
|
|
54
|
+
pkg.dependencies = sortDependencies({
|
|
41
55
|
...pkg.dependencies,
|
|
42
56
|
...dependencies
|
|
43
|
-
};
|
|
44
|
-
if (Object.keys(devDependencies).length) pkg.devDependencies = {
|
|
57
|
+
});
|
|
58
|
+
if (Object.keys(devDependencies).length) pkg.devDependencies = sortDependencies({
|
|
45
59
|
...pkg.devDependencies,
|
|
46
60
|
...devDependencies
|
|
47
|
-
};
|
|
61
|
+
});
|
|
48
62
|
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
|
|
49
63
|
}
|
|
50
64
|
/**
|
|
65
|
+
* Alphabetize a dependency map by package name. The framework CLI writes its deps sorted, but merging
|
|
66
|
+
* Kizlo's pins on top appends them out of order — sherif flags unsorted dependency keys, so re-sort the
|
|
67
|
+
* whole map before writing.
|
|
68
|
+
*/
|
|
69
|
+
function sortDependencies(deps) {
|
|
70
|
+
return Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
51
73
|
* The argv `create` runs to bootstrap a fresh base app with the framework's own official CLI, built
|
|
52
74
|
* from the template manifest's `bootstrap` (initializer + flags) — the template is the single source
|
|
53
75
|
* of truth for the framework. The CLI owns only the package-manager mechanics: {@link frameworkCreateArgs}
|
|
@@ -63,8 +85,8 @@ function bootstrapArgs(manifest, pm, name) {
|
|
|
63
85
|
* Layer Kizlo's wiring onto a freshly bootstrapped app in `dir`, from an already-fetched template
|
|
64
86
|
* directory + manifest. The framework CLI has produced the base project (package.json, config,
|
|
65
87
|
* tsconfig, the root layout); this drives the manifest — the same engine `init` uses — to record the
|
|
66
|
-
* `kizlo` dependency, write `kizlo.config.ts`, scaffold the manifest's files (
|
|
67
|
-
*
|
|
88
|
+
* `kizlo` dependency, write `kizlo.config.ts`, scaffold the manifest's files (core wiring from `base`
|
|
89
|
+
* and `create` always, the `example`-flagged demo pages only when `includeExamples`),
|
|
68
90
|
* seed the generated contract, and ignore `.env` and the `.kizlo/` working dir (which holds the local WordPress install). On a fresh app Kizlo owns the layout, so `create`
|
|
69
91
|
* writes it whole (already SEO-wired) rather than patching — `applyLayoutPatches` runs over `create`'s
|
|
70
92
|
* patches, which are none today. The target directory layout comes straight from the manifest's
|
|
@@ -80,7 +102,7 @@ async function applyManifestWiring(dir, templateDir, manifest, opts) {
|
|
|
80
102
|
clientUrl: void 0
|
|
81
103
|
});
|
|
82
104
|
recordDependencies(dir, manifest);
|
|
83
|
-
const changes = changesFor(manifest, "create"
|
|
105
|
+
const changes = changesFor(manifest, "create", { includeExamples: opts.includeExamples });
|
|
84
106
|
const files = [{
|
|
85
107
|
label: "Kizlo config",
|
|
86
108
|
relPath: "kizlo.config.ts",
|
|
@@ -104,54 +126,75 @@ const create = defineCommand({
|
|
|
104
126
|
template: {
|
|
105
127
|
type: "positional",
|
|
106
128
|
required: false,
|
|
107
|
-
description:
|
|
129
|
+
description: "Template to scaffold from (discovered from the registry)"
|
|
108
130
|
},
|
|
109
131
|
name: {
|
|
110
132
|
type: "positional",
|
|
111
133
|
required: false,
|
|
112
|
-
description: "Project folder to create"
|
|
134
|
+
description: "Project folder to create — a bare name or a path (e.g. apps/my-app, ../my-app, /srv/my-app)"
|
|
135
|
+
},
|
|
136
|
+
source: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description: "Where to scaffold from — a local dir or giget source, either a registry of templates or a single template (default: Kizlo's GitHub templates)"
|
|
113
139
|
}
|
|
114
140
|
},
|
|
115
141
|
async run({ args }) {
|
|
116
142
|
const cwd = process.cwd();
|
|
117
143
|
p.intro(`Let's create a new Kizlo application`);
|
|
144
|
+
const registry = resolveRegistry(args.source ? String(args.source) : void 0);
|
|
145
|
+
let fetchedRegistry;
|
|
146
|
+
try {
|
|
147
|
+
fetchedRegistry = await fetchRegistry(registry);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
p.cancel(error instanceof Error ? error.message : String(error));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
const templates = listTemplates(fetchedRegistry.dir);
|
|
153
|
+
if (templates.length === 0) {
|
|
154
|
+
fetchedRegistry.cleanup();
|
|
155
|
+
p.cancel(`No templates found in the registry (${registry.source}). Each template needs a template.json.`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
const cancelFrom = (message) => {
|
|
159
|
+
fetchedRegistry.cleanup();
|
|
160
|
+
p.cancel(message);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
};
|
|
118
163
|
const requested = args.template;
|
|
119
|
-
let
|
|
164
|
+
let selected;
|
|
120
165
|
if (requested) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
166
|
+
selected = templates.find((entry) => entry.id === requested);
|
|
167
|
+
if (!selected) return cancelFrom(`Unknown template "${requested}". Available: ${templates.map((entry) => entry.id).join(", ")}`);
|
|
168
|
+
} else {
|
|
169
|
+
const id = orCancel(await p.select({
|
|
170
|
+
message: "Template",
|
|
171
|
+
options: templates.map((entry) => ({
|
|
172
|
+
value: entry.id,
|
|
173
|
+
label: entry.label
|
|
174
|
+
}))
|
|
175
|
+
}));
|
|
176
|
+
selected = templates.find((entry) => entry.id === id);
|
|
177
|
+
}
|
|
178
|
+
if (!selected) return cancelFrom("No template selected.");
|
|
179
|
+
const template = selected.id;
|
|
180
|
+
const templateDir = selected.dir;
|
|
181
|
+
const label = selected.label;
|
|
182
|
+
const fail = cancelFrom;
|
|
183
|
+
const manifest = readManifest(templateDir);
|
|
184
|
+
const minErr = minCliError(manifest);
|
|
185
|
+
if (minErr) return fail(minErr);
|
|
186
|
+
const name = normalizeProjectName(args.name ? String(args.name) : orCancel(await p.text({
|
|
187
|
+
message: "What is your project named?",
|
|
135
188
|
placeholder: "my-app",
|
|
136
189
|
validate: projectName
|
|
137
|
-
}));
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
p.cancel(check);
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
const dir = path.resolve(cwd, name);
|
|
146
|
-
if (fs.existsSync(dir)) {
|
|
147
|
-
p.cancel(`${name} already exists — pick a different name or remove it.`);
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
const preset = getPreset(template);
|
|
151
|
-
if (!preset) {
|
|
152
|
-
p.cancel(`No preset for template "${template}".`);
|
|
190
|
+
})));
|
|
191
|
+
const invalid = projectName(name);
|
|
192
|
+
if (invalid) {
|
|
193
|
+
p.cancel(invalid);
|
|
153
194
|
process.exit(1);
|
|
154
195
|
}
|
|
196
|
+
const dir = path.resolve(cwd, name);
|
|
197
|
+
if (fs.existsSync(dir)) return cancelFrom(`${name} already exists — pick a different name or remove it.`);
|
|
155
198
|
const installed = availablePackageManagers(PACKAGE_MANAGERS);
|
|
156
199
|
const invokingPm = detectInvokingPackageManager();
|
|
157
200
|
const defaultPm = invokingPm && installed.includes(invokingPm) ? invokingPm : installed[0];
|
|
@@ -163,45 +206,32 @@ const create = defineCommand({
|
|
|
163
206
|
})),
|
|
164
207
|
initialValue: defaultPm
|
|
165
208
|
}));
|
|
166
|
-
const conn = await collectConnectionInteractively(
|
|
167
|
-
if (
|
|
209
|
+
const conn = await collectConnectionInteractively(manifest.apiPath, { baseUrl: `http://localhost:${await pickAppPort()}` });
|
|
210
|
+
if (manifest.apiPath && conn.baseUrl) conn.baseUrl = withApiPath(conn.baseUrl, manifest.apiPath);
|
|
168
211
|
const includeExamples = orCancel(await p.confirm({
|
|
169
212
|
message: "Add examples?",
|
|
170
213
|
initialValue: true
|
|
171
214
|
}));
|
|
172
|
-
const fetched = await fetchTemplate(template);
|
|
173
|
-
const fail = (message) => {
|
|
174
|
-
fetched.cleanup();
|
|
175
|
-
p.cancel(message);
|
|
176
|
-
process.exit(1);
|
|
177
|
-
};
|
|
178
|
-
const manifest = readManifest(fetched.dir);
|
|
179
|
-
const minErr = minCliError(manifest);
|
|
180
|
-
if (minErr) fail(minErr);
|
|
181
215
|
const bootstrap = bootstrapArgs(manifest, pm, name);
|
|
182
|
-
if (!bootstrap) {
|
|
183
|
-
fetched.cleanup();
|
|
184
|
-
p.cancel(`Template "${template}" can't be scaffolded — its manifest declares no framework bootstrap.`);
|
|
185
|
-
process.exit(1);
|
|
186
|
-
}
|
|
216
|
+
if (!bootstrap) return fail(`Template "${template}" can't be scaffolded — its manifest declares no framework bootstrap.`);
|
|
187
217
|
const s = p.spinner();
|
|
188
|
-
s.start(`Creating ${name} with the ${
|
|
218
|
+
s.start(`Creating ${name} with the ${label} CLI`);
|
|
189
219
|
const scaffold = runCommandCaptured(bootstrap, cwd);
|
|
190
|
-
s.stop(scaffold.ok ? `Created ${name} with the ${
|
|
220
|
+
s.stop(scaffold.ok ? `Created ${name} with the ${label} CLI` : `${label} setup failed`);
|
|
191
221
|
if (!scaffold.ok) {
|
|
192
222
|
if (scaffold.output) p.log.error(scaffold.output);
|
|
193
|
-
fail(`${
|
|
223
|
+
fail(`${label} setup failed — see the output above and try again.`);
|
|
194
224
|
}
|
|
195
|
-
const exampleTemplate = readEnvExample(
|
|
225
|
+
const exampleTemplate = readEnvExample(templateDir);
|
|
196
226
|
try {
|
|
197
|
-
await applyManifestWiring(dir,
|
|
227
|
+
await applyManifestWiring(dir, templateDir, manifest, {
|
|
198
228
|
includeExamples,
|
|
199
229
|
localDev: conn.mode === "local"
|
|
200
230
|
});
|
|
201
231
|
} catch (error) {
|
|
202
232
|
fail(error instanceof Error ? error.message : String(error));
|
|
203
233
|
}
|
|
204
|
-
|
|
234
|
+
fetchedRegistry.cleanup();
|
|
205
235
|
let depsInstalled = false;
|
|
206
236
|
if (orCancel(await p.confirm({
|
|
207
237
|
message: "Install dependencies now?",
|
|
@@ -214,7 +244,7 @@ const create = defineCommand({
|
|
|
214
244
|
if (!depsInstalled) p.log.warn(`Install them yourself: cd ${name} && ${installArgs(pm).join(" ")}`);
|
|
215
245
|
}
|
|
216
246
|
await setupLocalWordPress(dir, conn);
|
|
217
|
-
await writeEnv(dir, resolveEnvKeys(
|
|
247
|
+
await writeEnv(dir, resolveEnvKeys(manifest), conn, {
|
|
218
248
|
force: true,
|
|
219
249
|
yes: false,
|
|
220
250
|
exampleTemplate
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createStack, dockerHint, dockerStatus } from "../wp/docker.js";
|
|
2
|
+
import { findConfigDir } from "../wp/utils.js";
|
|
3
|
+
import { log } from "../daemon/logger.js";
|
|
2
4
|
import { DEFAULT_ENV_KEYS, ensureGitignored, envGroups, groupDefault, mergeEnv, pickStackPort, withSpinner } from "../utils.js";
|
|
3
5
|
import { resolveDevConfig, usesLocalWordPress } from "../daemon/config.js";
|
|
4
6
|
import { bootstrapDev } from "../wp/dev.js";
|
|
@@ -6,6 +8,7 @@ import { reapOrphans, registerSession, removeProjectContainers, spawnWatchdog, u
|
|
|
6
8
|
import { syncSiteSettings } from "../wp/settings.js";
|
|
7
9
|
import { devStack } from "../wp/stack.js";
|
|
8
10
|
import { palette } from "../banner.js";
|
|
11
|
+
import { watchReload } from "../daemon/reload.js";
|
|
9
12
|
import { startWatcher } from "../daemon/watch.js";
|
|
10
13
|
import { join } from "node:path";
|
|
11
14
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -166,13 +169,13 @@ function armForegroundTeardown(cfg) {
|
|
|
166
169
|
}
|
|
167
170
|
}
|
|
168
171
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
172
|
+
* Bring the dev stack's containers up for `cfg`: clear any stale containers, resolve the published
|
|
173
|
+
* ports (stepping off a collision unless the port is pinned in config), write the compose files, and
|
|
174
|
+
* note any port that moved. Returns `cfg` with the resolved ports — the shape the rest of the boot
|
|
175
|
+
* and the teardown work against. Shared by the first boot and every reload reboot, so a changed
|
|
176
|
+
* `dev.port` in `kizlo.config.ts` is re-resolved the same way each time.
|
|
174
177
|
*/
|
|
175
|
-
async function
|
|
178
|
+
async function prepareStack(cfg) {
|
|
176
179
|
await removeProjectContainers(cfg.project);
|
|
177
180
|
const port = await pickStackPort(cfg.port, {
|
|
178
181
|
fixed: cfg.portExplicit,
|
|
@@ -191,11 +194,17 @@ async function startForeground(cfg) {
|
|
|
191
194
|
createStack(devStack(ready));
|
|
192
195
|
if (port !== cfg.port) note(`Port ${cfg.port} is in use — serving on ${port} instead.`);
|
|
193
196
|
if (dbPort !== cfg.dbPort) note(`Database port ${cfg.dbPort} is in use — using ${dbPort} instead.`);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
return ready;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* After a boot, sync the fresh credentials (or the current LAN URL) into `.env` and the plugin, print
|
|
201
|
+
* the connection summary, and print the timed ready line. `reloaded` swaps the wording and drops the
|
|
202
|
+
* one-time Ctrl+C hint. `start` is when the boot began, so the timing spans the whole reboot including
|
|
203
|
+
* the spinner. The `.env` writes here are why the caller pauses the reload watcher around a reboot.
|
|
204
|
+
*/
|
|
205
|
+
async function report(cfg, creds, reloaded, start) {
|
|
197
206
|
if (creds.appPassword) {
|
|
198
|
-
const env = updateWpEnv(
|
|
207
|
+
const env = updateWpEnv(cfg, creds);
|
|
199
208
|
note("Updated .env with the new WordPress credentials");
|
|
200
209
|
if (env) {
|
|
201
210
|
const sync = await syncSiteSettings({
|
|
@@ -209,34 +218,118 @@ async function startForeground(cfg) {
|
|
|
209
218
|
});
|
|
210
219
|
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.`);
|
|
211
220
|
}
|
|
212
|
-
} else if (syncLocalUrl(
|
|
221
|
+
} else if (syncLocalUrl(cfg, creds.url)) note("Updated .env WordPress URL to the current network address — restart your app to pick it up");
|
|
213
222
|
printSummary(creds);
|
|
214
223
|
const { green, dim, bold, reset } = palette();
|
|
215
224
|
const ms = Date.now() - start;
|
|
216
225
|
const took = ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
|
|
217
|
-
process.stdout.write(`\n ${dim}Press ${reset}${bold}Ctrl+C${reset}${dim} (or close the terminal) to stop local WordPress${reset}\n`);
|
|
218
|
-
process.stdout.write(`\n ${green}✓${reset} Ready in ${took}\n\n`);
|
|
219
|
-
|
|
220
|
-
|
|
226
|
+
if (!reloaded) process.stdout.write(`\n ${dim}Press ${reset}${bold}Ctrl+C${reset}${dim} (or close the terminal) to stop local WordPress${reset}\n`);
|
|
227
|
+
process.stdout.write(`\n ${green}✓${reset} ${reloaded ? "Reloaded" : "Ready"} in ${took}\n\n`);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Boot local WordPress for an already-prepared `cfg` (ports resolved, teardown armed) behind a spinner,
|
|
231
|
+
* then report. The first-boot path only — reboots go through {@link rebootStack}, which also folds the
|
|
232
|
+
* prepare step under the spinner.
|
|
233
|
+
*/
|
|
234
|
+
async function bootAndReport(cfg) {
|
|
235
|
+
const start = Date.now();
|
|
236
|
+
await report(cfg, await withSpinner("Starting local WordPress", () => bootstrapDev(cfg)), false, start);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Reboot the whole stack in place after a config/env change: re-resolve the config, recreate the
|
|
240
|
+
* containers, and boot WordPress — all behind a single spinner labelled with the file that changed, so
|
|
241
|
+
* there's no loaderless gap between detecting the change and the boot. Teardown is already armed from
|
|
242
|
+
* the first boot, so this never re-arms. Returns the freshly resolved config (ports may have moved) so
|
|
243
|
+
* the caller can re-target its watchers.
|
|
244
|
+
*/
|
|
245
|
+
async function rebootStack(cwd, label) {
|
|
246
|
+
const start = Date.now();
|
|
247
|
+
const { ready, creds } = await withSpinner(label, async () => {
|
|
248
|
+
const ready$1 = await prepareStack(await resolveDevConfig(cwd));
|
|
249
|
+
return {
|
|
250
|
+
ready: ready$1,
|
|
251
|
+
creds: await bootstrapDev(ready$1)
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
await report(ready, creds, true, start);
|
|
255
|
+
return ready;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Boot local WordPress and run in the foreground until exit — the path behind bare `kizlo dev`. Arms
|
|
259
|
+
* teardown first (so a mid-startup cancel still stops partial containers), boots and prints the
|
|
260
|
+
* summary, then parks. While parked it watches `.env` and `kizlo.config.*`: a change reboots the whole
|
|
261
|
+
* stack in place so a new port, connection, or fixture takes effect without rerunning the command. The
|
|
262
|
+
* reboot runs in the same process — the watchdog and session keep watching this PID throughout — and is
|
|
263
|
+
* guarded so the credential/URL writes it makes to `.env` don't trigger another reboot. A referenced
|
|
264
|
+
* timer holds the event loop open — an unresolved promise alone would not, so without it Node would
|
|
265
|
+
* empty its loop and exit straight after printing.
|
|
266
|
+
*/
|
|
267
|
+
async function startForeground(cfg) {
|
|
268
|
+
const cwd = process.cwd();
|
|
269
|
+
let ready = await prepareStack(cfg);
|
|
270
|
+
armForegroundTeardown(ready);
|
|
271
|
+
await bootAndReport(ready);
|
|
272
|
+
let stopContract = await startWatcher(ready.configDir);
|
|
273
|
+
let reloading = false;
|
|
274
|
+
const reload = async (changed) => {
|
|
275
|
+
if (reloading) return;
|
|
276
|
+
reloading = true;
|
|
277
|
+
try {
|
|
278
|
+
stopContract?.();
|
|
279
|
+
ready = await rebootStack(cwd, `${changed} changed — restarting the session`);
|
|
280
|
+
stopContract = await startWatcher(ready.configDir);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
log.error("Failed to reload the dev session:", error);
|
|
283
|
+
} finally {
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
reloading = false;
|
|
286
|
+
}, 500);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const stopReload = watchReload(ready.configDir, (changed) => void reload(changed));
|
|
290
|
+
process.on("exit", () => {
|
|
291
|
+
stopContract?.();
|
|
292
|
+
stopReload();
|
|
293
|
+
});
|
|
221
294
|
setInterval(() => {}, 1 << 30);
|
|
222
295
|
await new Promise(() => {});
|
|
223
296
|
}
|
|
224
297
|
/**
|
|
225
298
|
* Run the contract watcher in the foreground until exit — the path bare `kizlo dev` takes when local
|
|
226
299
|
* WordPress isn't configured (a project pointing at its own WordPress). Generates once, then watches
|
|
227
|
-
* the server files and regenerates on save.
|
|
228
|
-
* the
|
|
300
|
+
* the server files and regenerates on save. It also watches `.env` and `kizlo.config.*` and restarts
|
|
301
|
+
* the watcher on a change, so a new server dir or env value is picked up the same way the local path
|
|
302
|
+
* reboots its stack. Returns immediately when another watcher already holds the lock; otherwise the
|
|
303
|
+
* persistent watcher keeps the process alive on its own.
|
|
229
304
|
*/
|
|
230
305
|
async function watchOnly(cwd) {
|
|
231
|
-
|
|
306
|
+
let stop = await startWatcher(cwd);
|
|
232
307
|
if (!stop) return;
|
|
233
|
-
|
|
308
|
+
let reloading = false;
|
|
309
|
+
const reload = async () => {
|
|
310
|
+
if (reloading) return;
|
|
311
|
+
reloading = true;
|
|
312
|
+
try {
|
|
313
|
+
stop?.();
|
|
314
|
+
stop = await startWatcher(cwd);
|
|
315
|
+
} finally {
|
|
316
|
+
setTimeout(() => {
|
|
317
|
+
reloading = false;
|
|
318
|
+
}, 300);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const stopReload = watchReload(findConfigDir(cwd), () => void reload());
|
|
322
|
+
const shutdown = () => {
|
|
323
|
+
stop?.();
|
|
324
|
+
stopReload();
|
|
325
|
+
};
|
|
326
|
+
process.on("exit", shutdown);
|
|
234
327
|
for (const signal of [
|
|
235
328
|
"SIGINT",
|
|
236
329
|
"SIGTERM",
|
|
237
330
|
"SIGHUP"
|
|
238
331
|
]) process.on(signal, () => {
|
|
239
|
-
|
|
332
|
+
shutdown();
|
|
240
333
|
process.exit(0);
|
|
241
334
|
});
|
|
242
335
|
}
|
|
@@ -15,6 +15,10 @@ const generate = defineCommand({
|
|
|
15
15
|
} },
|
|
16
16
|
async run({ args }) {
|
|
17
17
|
const cfg = await resolveConfig(process.cwd(), { dir: args.dir });
|
|
18
|
+
if (!cfg) {
|
|
19
|
+
log.info("No Kizlo server directory configured, nothing to generate. Set `dir` in kizlo.config.ts or pass --dir.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
18
22
|
let ok;
|
|
19
23
|
try {
|
|
20
24
|
ok = await generateOnce(cfg);
|