kizlo 0.12.1 → 0.13.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/cli/commands/_setup.js +47 -12
- package/dist/cli/commands/_wiring.js +30 -9
- package/dist/cli/commands/create.js +17 -21
- package/dist/cli/commands/init.js +65 -33
- package/dist/cli/daemon/config.js +1 -1
- package/dist/cli/presets/patch.js +3 -1
- package/dist/cli/presets/template.js +52 -16
- package/dist/cli/utils.js +178 -10
- package/dist/config.d.ts +3 -2
- package/dist/package.js +1 -1
- package/dist/router.d.ts +5 -5
- package/package.json +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LOCAL_DIR_REL } from "../wp/constants.js";
|
|
2
2
|
import { composeStop, createStack, dockerHint, dockerStatus } from "../wp/docker.js";
|
|
3
|
-
import { DEFAULT_ENV_KEYS, ensureGitignored, envGroups, envKeysPresent, mergeEnv, pickStackPort, writeFileIfAbsent } from "../utils.js";
|
|
3
|
+
import { DEFAULT_ENV_KEYS, availablePackageManagers, detectInvokingPackageManager, ensureGitignored, envGroups, envKeysPresent, mergeEnv, pickStackPort, writeFileIfAbsent } from "../utils.js";
|
|
4
4
|
import { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveStackName, stackProject } from "../daemon/config.js";
|
|
5
5
|
import { createAdminAppPassword } from "../wp/bootstrap.js";
|
|
6
6
|
import { bootstrapDev } from "../wp/dev.js";
|
|
@@ -100,6 +100,32 @@ function orCancel(value) {
|
|
|
100
100
|
}
|
|
101
101
|
return value;
|
|
102
102
|
}
|
|
103
|
+
/** Package managers the CLI can wire getting-started steps for, in display order. */
|
|
104
|
+
const PACKAGE_MANAGERS = [
|
|
105
|
+
"pnpm",
|
|
106
|
+
"npm",
|
|
107
|
+
"yarn",
|
|
108
|
+
"bun"
|
|
109
|
+
];
|
|
110
|
+
/**
|
|
111
|
+
* Ask which package manager to use, offering only those installed on the host. The pre-selected default
|
|
112
|
+
* is the manager that invoked this CLI when it's available (a hint, not a decision — `npx` reports
|
|
113
|
+
* `npm`, so it can be wrong, which is exactly why we ask rather than commit to it), else the first
|
|
114
|
+
* installed. `create` asks up front; `init` asks only when it couldn't detect one from the project.
|
|
115
|
+
*/
|
|
116
|
+
async function selectPackageManager(message) {
|
|
117
|
+
const installed = availablePackageManagers(PACKAGE_MANAGERS);
|
|
118
|
+
const invoking = detectInvokingPackageManager();
|
|
119
|
+
const initialValue = invoking && installed.includes(invoking) ? invoking : installed[0];
|
|
120
|
+
return orCancel(await p.select({
|
|
121
|
+
message,
|
|
122
|
+
options: installed.map((id) => ({
|
|
123
|
+
value: id,
|
|
124
|
+
label: id
|
|
125
|
+
})),
|
|
126
|
+
initialValue
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
103
129
|
/** Appends the API path to the base URL so the client and route handler agree. */
|
|
104
130
|
function withApiPath(baseUrl, apiPath) {
|
|
105
131
|
try {
|
|
@@ -174,15 +200,23 @@ async function collectConnectionInteractively(apiPath, opts = {}) {
|
|
|
174
200
|
const mode = orCancel(await p.select({
|
|
175
201
|
message: "WordPress connection",
|
|
176
202
|
initialValue: "local",
|
|
177
|
-
options: [
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
203
|
+
options: [
|
|
204
|
+
{
|
|
205
|
+
value: "local",
|
|
206
|
+
label: "Set up local WordPress",
|
|
207
|
+
hint: "runs in Docker, for dev and test"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
value: "remote",
|
|
211
|
+
label: "Use my own WordPress",
|
|
212
|
+
hint: "connect to your existing WordPress"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
value: "skip",
|
|
216
|
+
label: "Skip for now",
|
|
217
|
+
hint: "fill in the connection in .env later"
|
|
218
|
+
}
|
|
219
|
+
]
|
|
186
220
|
}));
|
|
187
221
|
if (mode === "local") {
|
|
188
222
|
const status = await dockerStatus();
|
|
@@ -209,8 +243,9 @@ async function collectConnectionInteractively(apiPath, opts = {}) {
|
|
|
209
243
|
validate: validate(requiredString)
|
|
210
244
|
}));
|
|
211
245
|
}
|
|
246
|
+
if (mode === "skip") p.log.info("Skipping WordPress setup — the connection keys are written to .env empty for you to fill in later.");
|
|
212
247
|
return {
|
|
213
|
-
mode,
|
|
248
|
+
mode: mode === "local" ? "local" : "remote",
|
|
214
249
|
baseUrl,
|
|
215
250
|
siteUrl,
|
|
216
251
|
siteSecret,
|
|
@@ -412,4 +447,4 @@ function nextStepsNote(conn, prefix = "") {
|
|
|
412
447
|
}
|
|
413
448
|
|
|
414
449
|
//#endregion
|
|
415
|
-
export { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsLines, nextStepsNote, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv };
|
|
450
|
+
export { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsLines, nextStepsNote, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, selectPackageManager, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveModuleImport, writeFileIfAbsent } from "../utils.js";
|
|
1
|
+
import { aliasWithSlash, resolveModuleImport, writeFileIfAbsent } from "../utils.js";
|
|
2
2
|
import { resolvePatch } from "../presets/template.js";
|
|
3
3
|
import { orCancel } from "./_setup.js";
|
|
4
4
|
import { CONTRACT_BARREL } from "../daemon/generate.js";
|
|
@@ -8,32 +8,37 @@ import fs from "node:fs";
|
|
|
8
8
|
import * as p from "@clack/prompts";
|
|
9
9
|
|
|
10
10
|
//#region src/cli/commands/_wiring.ts
|
|
11
|
-
/** The kizlo.config.ts a scaffolded project gets: the Kizlo directory,
|
|
11
|
+
/** The kizlo.config.ts a scaffolded project gets: the Kizlo directory, the import-alias preference, and —
|
|
12
12
|
* when local WordPress is chosen — `dev.local` + `test.local` so `kizlo dev` and `kizlo test` boot the
|
|
13
|
-
* fixed `.kizlo/local` install.
|
|
13
|
+
* fixed `.kizlo/local` install. The alias is always written — `""` for relative imports included — so it
|
|
14
|
+
* records a made decision a later `kizlo init` reads back instead of prompting for it again. The alias
|
|
15
|
+
* is written in its canonical `@/` form (never a bare `@`) so it reads like the imports it produces. */
|
|
14
16
|
function kizloConfigTemplate(dir, alias, localWordPress = false) {
|
|
15
17
|
return `import { defineConfig } from "kizlo/config"
|
|
16
18
|
|
|
17
19
|
export default defineConfig({
|
|
18
|
-
dir: "${dir}",${
|
|
20
|
+
dir: "${dir}",${`\n\talias: "${aliasWithSlash(alias)}",`}${localWordPress ? `\n\tdev: { local: true },\n\ttest: { local: true },` : ""}
|
|
19
21
|
})
|
|
20
22
|
`;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Build the {@link ScaffoldContext} that adapts a template's conventions (paths and imports) to a real
|
|
24
26
|
* project. `dirRel` is the Kizlo home directory, `appDir` the App Router directory, and `alias` the
|
|
25
|
-
* import-alias prefix (empty for relative imports).
|
|
26
|
-
*
|
|
27
|
+
* import-alias prefix (empty for relative imports). Imports are resolved per calling file so each
|
|
28
|
+
* scaffolded file references its targets through the right specifier — `importFrom` for any project
|
|
29
|
+
* path (used to retarget every template-alias import), `serverImport` the server-entry shorthand.
|
|
27
30
|
*/
|
|
28
31
|
function buildScaffoldContext(cwd, { dirRel, appDir, alias, clientUrl }) {
|
|
29
32
|
const serverDirRel = path.join(dirRel, "server");
|
|
33
|
+
const importFrom = (targetRel, fromDir) => resolveModuleImport(cwd, targetRel, fromDir, alias);
|
|
30
34
|
return {
|
|
31
35
|
kizloDir: dirRel,
|
|
32
36
|
serverDirName: path.basename(serverDirRel),
|
|
33
37
|
serverEntryPath: path.join(serverDirRel, "index.ts"),
|
|
34
38
|
clientPath: path.join(dirRel, "client.ts"),
|
|
35
39
|
appDir,
|
|
36
|
-
serverImport: (fromDir) =>
|
|
40
|
+
serverImport: (fromDir) => importFrom(serverDirRel, fromDir),
|
|
41
|
+
importFrom,
|
|
37
42
|
clientUrl
|
|
38
43
|
};
|
|
39
44
|
}
|
|
@@ -85,6 +90,11 @@ function applyProjectPatches(cwd, patches, conventions, scaffold) {
|
|
|
85
90
|
const manualSteps = [];
|
|
86
91
|
for (const entry of patches) {
|
|
87
92
|
const resolved = resolvePatch(entry, conventions, scaffold);
|
|
93
|
+
if (resolved.mode === "note") {
|
|
94
|
+
manualSteps.push(resolved);
|
|
95
|
+
p.log.info(`Update your ${resolved.label} to finish wiring Kizlo — see the change to make below`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
88
98
|
const target = resolvePatchTargetPath(cwd, resolved.relPath);
|
|
89
99
|
if (!target) {
|
|
90
100
|
manualSteps.push(resolved);
|
|
@@ -107,8 +117,19 @@ function applyProjectPatches(cwd, patches, conventions, scaffold) {
|
|
|
107
117
|
p.log.success(`Wired Kizlo into your ${resolved.label} (${relTarget})`);
|
|
108
118
|
} else p.log.info(`Your ${resolved.label} is already wired (${relTarget})`);
|
|
109
119
|
}
|
|
110
|
-
for (const resolved of manualSteps)
|
|
120
|
+
for (const resolved of manualSteps) printManualStep(resolved.mode === "note" ? `Update your ${resolved.label} (${resolved.relPath})` : `Add these to your ${resolved.label} (${resolved.relPath})`, renderPatchCode(resolved));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Print a manual-step code block the user copies into a file. Deliberately avoids `p.note`: clack
|
|
124
|
+
* draws a `│`/`─` box around the body, so selecting the block in a terminal drags those border
|
|
125
|
+
* characters into the clipboard alongside the code. We print the heading through clack for the usual
|
|
126
|
+
* voice, then the code verbatim with no gutter so a terminal selection yields exactly the source.
|
|
127
|
+
*/
|
|
128
|
+
function printManualStep(heading, code) {
|
|
129
|
+
p.log.step(heading);
|
|
130
|
+
const body = code.endsWith("\n") ? code.slice(0, -1) : code;
|
|
131
|
+
process.stdout.write(`\n${body}\n\n`);
|
|
111
132
|
}
|
|
112
133
|
|
|
113
134
|
//#endregion
|
|
114
|
-
export { applyProjectPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract };
|
|
135
|
+
export { applyProjectPatches, buildScaffoldContext, kizloConfigTemplate, printManualStep, reportScaffold, scaffoldFile, writeGeneratedContract };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { approveBuildsCommand, ensureGitignored, frameworkCreateArgs, initGitRepository, installArgs, isCommandAvailable, isGitRepository, runCommandCaptured, runCommandCapturedAsync } from "../utils.js";
|
|
2
2
|
import { fetchRegistry, listTemplates, resolveRegistry } from "../presets/source.js";
|
|
3
3
|
import { adaptFile, changesFor, fileEntries, minCliError, patchEntries, readManifest, resolveDependencies } from "../presets/template.js";
|
|
4
|
-
import { collectConnectionInteractively, nextStepsLines, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
|
|
4
|
+
import { collectConnectionInteractively, nextStepsLines, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, selectPackageManager, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
|
|
5
5
|
import { applyProjectPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
|
|
6
6
|
import z from "zod/v4";
|
|
7
7
|
import path from "node:path";
|
|
@@ -34,13 +34,6 @@ const projectNameSchema = z.string().trim().min(1, "Required").refine((value) =>
|
|
|
34
34
|
return base.length > 0 && base !== "." && base !== ".." && /^[A-Za-z0-9._-]+$/.test(base);
|
|
35
35
|
}, "The app folder (last path segment) can only contain letters, numbers, dots, and dashes");
|
|
36
36
|
const projectName = validate(projectNameSchema);
|
|
37
|
-
/** Package managers `create` can wire the getting-started steps for, in display order. */
|
|
38
|
-
const PACKAGE_MANAGERS = [
|
|
39
|
-
"pnpm",
|
|
40
|
-
"npm",
|
|
41
|
-
"yarn",
|
|
42
|
-
"bun"
|
|
43
|
-
];
|
|
44
37
|
/**
|
|
45
38
|
* Pin Kizlo's dependencies in the freshly bootstrapped `package.json`. The template is authoritative
|
|
46
39
|
* for the versions (its manifest's `dependencies`/`devDependencies`, stamped at release), falling back
|
|
@@ -195,17 +188,7 @@ const create = defineCommand({
|
|
|
195
188
|
}
|
|
196
189
|
const dir = path.resolve(cwd, name);
|
|
197
190
|
if (fs.existsSync(dir)) return cancelFrom(`${name} already exists — pick a different name or remove it.`);
|
|
198
|
-
const
|
|
199
|
-
const invokingPm = detectInvokingPackageManager();
|
|
200
|
-
const defaultPm = invokingPm && installed.includes(invokingPm) ? invokingPm : installed[0];
|
|
201
|
-
const pm = orCancel(await p.select({
|
|
202
|
-
message: "Package manager",
|
|
203
|
-
options: installed.map((id) => ({
|
|
204
|
-
value: id,
|
|
205
|
-
label: id
|
|
206
|
-
})),
|
|
207
|
-
initialValue: defaultPm
|
|
208
|
-
}));
|
|
191
|
+
const pm = await selectPackageManager("Package manager");
|
|
209
192
|
const conn = await collectConnectionInteractively(manifest.apiPath, { baseUrl: `http://localhost:${await pickAppPort()}` });
|
|
210
193
|
if (manifest.apiPath && conn.baseUrl) conn.baseUrl = withApiPath(conn.baseUrl, manifest.apiPath);
|
|
211
194
|
const includeExamples = orCancel(await p.confirm({
|
|
@@ -233,13 +216,16 @@ const create = defineCommand({
|
|
|
233
216
|
}
|
|
234
217
|
fetchedRegistry.cleanup();
|
|
235
218
|
let depsInstalled = false;
|
|
219
|
+
let approveBuilds;
|
|
236
220
|
if (orCancel(await p.confirm({
|
|
237
221
|
message: "Install dependencies now?",
|
|
238
222
|
initialValue: true
|
|
239
223
|
}))) {
|
|
240
224
|
const is = p.spinner();
|
|
241
225
|
is.start(`Installing dependencies with ${pm}`);
|
|
242
|
-
|
|
226
|
+
const install = await runCommandCapturedAsync(installArgs(pm), dir);
|
|
227
|
+
depsInstalled = install.ok;
|
|
228
|
+
if (depsInstalled) approveBuilds = approveBuildsCommand(pm, install.output);
|
|
243
229
|
is.stop(depsInstalled ? "Installed dependencies" : "Could not install dependencies");
|
|
244
230
|
if (!depsInstalled) p.log.warn(`Install them yourself: cd ${name} && ${installArgs(pm).join(" ")}`);
|
|
245
231
|
}
|
|
@@ -250,9 +236,19 @@ const create = defineCommand({
|
|
|
250
236
|
exampleTemplate
|
|
251
237
|
});
|
|
252
238
|
await syncRemote(conn);
|
|
239
|
+
if (isCommandAvailable("git") && !isGitRepository(dir) && orCancel(await p.confirm({
|
|
240
|
+
message: "Initialize a git repository?",
|
|
241
|
+
initialValue: true
|
|
242
|
+
}))) {
|
|
243
|
+
const gs = p.spinner();
|
|
244
|
+
gs.start("Initializing git repository");
|
|
245
|
+
const ok = initGitRepository(dir);
|
|
246
|
+
gs.stop(ok ? "Initialized git repository" : "Could not initialize git repository");
|
|
247
|
+
}
|
|
253
248
|
p.note([
|
|
254
249
|
`cd ${name}`,
|
|
255
250
|
...depsInstalled ? [] : [`${pm} install`],
|
|
251
|
+
...approveBuilds ? [approveBuilds] : [],
|
|
256
252
|
``,
|
|
257
253
|
...nextStepsLines(conn)
|
|
258
254
|
].join("\n"), "Next steps");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { addDependencyArgs, detectImportAlias, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, runCommandAsync } from "../utils.js";
|
|
1
|
+
import { addDependencyArgs, aliasWithSlash, detectImportAlias, detectInvokingPackageManager, detectPackageManager, ensureGitignored, getVersion, loadEnvFiles, persistPackageManagerField, readPersistedAlias, runCommandAsync } from "../utils.js";
|
|
2
2
|
import { DEFAULT_REGISTRY, detectTemplates, fetchRegistry, isSingleTemplate, listTemplates, resolveRegistry } from "../presets/source.js";
|
|
3
3
|
import { adaptFile, changesFor, fileEntries, isOlderVersion, minCliError, patchEntries, readManifest, renderNote, resolveDependencies } from "../presets/template.js";
|
|
4
|
-
import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, readEnvExample, resolveEnvKeys, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
|
|
5
|
-
import { applyProjectPatches, buildScaffoldContext, kizloConfigTemplate, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
|
|
4
|
+
import { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsNote, orCancel, readEnvExample, resolveEnvKeys, selectPackageManager, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv } from "./_setup.js";
|
|
5
|
+
import { applyProjectPatches, buildScaffoldContext, kizloConfigTemplate, printManualStep, reportScaffold, scaffoldFile, writeGeneratedContract } from "./_wiring.js";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import { defineCommand } from "citty";
|
|
@@ -12,10 +12,6 @@ import * as p from "@clack/prompts";
|
|
|
12
12
|
function defaultDir(hasSrcDir) {
|
|
13
13
|
return hasSrcDir ? "src/lib/kizlo" : "lib/kizlo";
|
|
14
14
|
}
|
|
15
|
-
/** Normalizes an alias prefix to the familiar `@/` form (or empty for relative). */
|
|
16
|
-
function aliasWithSlash(alias) {
|
|
17
|
-
return alias ? `${alias.replace(/\/+$/, "")}/` : "";
|
|
18
|
-
}
|
|
19
15
|
/** Whether two URLs share an origin (scheme + host + port); false when either can't be parsed. */
|
|
20
16
|
function sameOrigin(a, b) {
|
|
21
17
|
try {
|
|
@@ -42,14 +38,24 @@ function detectAppDir(cwd, hasSrcDir, templateAppDir) {
|
|
|
42
38
|
if (fs.existsSync(path.join(cwd, "app"))) return "app";
|
|
43
39
|
return hasSrcDir ? "src/app" : "app";
|
|
44
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The import alias settled without asking — the explicit `--alias` flag, else the alias a previous init
|
|
43
|
+
* persisted in kizlo.config. `undefined` when neither is set: the choice hasn't been made, so it must be
|
|
44
|
+
* prompted for (interactive) or defaulted to relative imports (`--yes`). Detection never enters here — a
|
|
45
|
+
* tsconfig alias is only ever a prompt *hint*, since relative imports always work and a detected alias
|
|
46
|
+
* isn't a guarantee (a project may have several, or map one Kizlo's dir isn't under).
|
|
47
|
+
*/
|
|
48
|
+
function decidedAlias(flagAlias, persistedAlias) {
|
|
49
|
+
return flagAlias ?? persistedAlias;
|
|
50
|
+
}
|
|
45
51
|
async function collectInteractively(ctx) {
|
|
46
52
|
const dir = orCancel(await p.text({
|
|
47
53
|
message: "Kizlo directory",
|
|
48
54
|
initialValue: defaultDir(ctx.hasSrcDir),
|
|
49
55
|
validate: validate(dirPath)
|
|
50
56
|
}));
|
|
51
|
-
let alias =
|
|
52
|
-
if (ctx.apiPath) {
|
|
57
|
+
let alias = decidedAlias(ctx.flagAlias, ctx.persistedAlias);
|
|
58
|
+
if (alias === void 0) if (ctx.apiPath) {
|
|
53
59
|
const serverDir = path.join(dir.replace(/^\.\//, "").replace(/\/+$/, ""), "server");
|
|
54
60
|
const detected = detectImportAlias(ctx.cwd, serverDir)?.prefix;
|
|
55
61
|
alias = orCancel(await p.text({
|
|
@@ -57,7 +63,7 @@ async function collectInteractively(ctx) {
|
|
|
57
63
|
placeholder: "@/",
|
|
58
64
|
initialValue: detected ? `${detected}/` : ""
|
|
59
65
|
})).trim();
|
|
60
|
-
}
|
|
66
|
+
} else alias = "";
|
|
61
67
|
return {
|
|
62
68
|
...await collectConnectionInteractively(ctx.apiPath),
|
|
63
69
|
dir,
|
|
@@ -65,9 +71,11 @@ async function collectInteractively(ctx) {
|
|
|
65
71
|
};
|
|
66
72
|
}
|
|
67
73
|
/**
|
|
68
|
-
* Non-interactive setup: skip prompts and use defaults. Values present in the
|
|
69
|
-
*
|
|
70
|
-
*
|
|
74
|
+
* Non-interactive setup: skip prompts and use defaults. Values present in the environment are used;
|
|
75
|
+
* missing ones are left empty for the user to fill in later. The alias is the `--alias` flag, else the
|
|
76
|
+
* one persisted from a prior run, else relative imports — never silent tsconfig detection, since `--yes`
|
|
77
|
+
* can't confirm it and relative imports always work. Never fails — `--yes` always scaffolds a fillable
|
|
78
|
+
* project.
|
|
71
79
|
*/
|
|
72
80
|
function collectFromEnv(ctx) {
|
|
73
81
|
const conn = collectConnectionFromEnv(ctx.envKeys);
|
|
@@ -75,28 +83,49 @@ function collectFromEnv(ctx) {
|
|
|
75
83
|
return {
|
|
76
84
|
...conn,
|
|
77
85
|
dir,
|
|
78
|
-
alias:
|
|
86
|
+
alias: decidedAlias(ctx.flagAlias, ctx.persistedAlias) ?? ""
|
|
79
87
|
};
|
|
80
88
|
}
|
|
81
89
|
function readPkg(pkgPath) {
|
|
82
90
|
return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
83
91
|
}
|
|
84
92
|
/**
|
|
85
|
-
*
|
|
86
|
-
* in its manifest (stamped at release),
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
93
|
+
* Bring the project's dependencies in line with the template's pins. The template is authoritative for
|
|
94
|
+
* the versions: it declares `dependencies`/`devDependencies` in its manifest (stamped at release),
|
|
95
|
+
* falling back to the running CLI's version for `kizlo` in-repo before the first stamp. For each declared
|
|
96
|
+
* package:
|
|
97
|
+
*
|
|
98
|
+
* - missing from the project — install it at the template's pin, as a dev dependency when the template
|
|
99
|
+
* declares it there. `create` records the template's whole dependency set into the fresh app before
|
|
100
|
+
* installing; `init` layers onto an existing project, so packages the template needs but the project
|
|
101
|
+
* lacks (e.g. Astro's `@astrojs/node` adapter) must be added here or the app won't run.
|
|
102
|
+
* - present at an older release — upgrade it up to the pin and say so; a deliberate newer pin is never
|
|
103
|
+
* changed silently, and nothing is ever downgraded.
|
|
104
|
+
*
|
|
105
|
+
* A *missing* `kizlo` is installed on its own earlier (with its own messaging), so it's skipped here;
|
|
106
|
+
* an existing older `kizlo` still gets the upgrade path.
|
|
90
107
|
*/
|
|
91
108
|
async function alignDependencies(cwd, pm, pkg, manifest) {
|
|
92
109
|
const { dependencies, devDependencies } = resolveDependencies(manifest);
|
|
93
|
-
|
|
110
|
+
const declared = [...Object.entries(dependencies).map(([name, want]) => ({
|
|
111
|
+
name,
|
|
112
|
+
want,
|
|
113
|
+
dev: false
|
|
114
|
+
})), ...Object.entries(devDependencies).map(([name, want]) => ({
|
|
115
|
+
name,
|
|
116
|
+
want,
|
|
117
|
+
dev: true
|
|
118
|
+
}))];
|
|
119
|
+
for (const { name, want, dev } of declared) {
|
|
94
120
|
const have = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
|
|
95
|
-
if (
|
|
121
|
+
if (name === "kizlo" && !have) continue;
|
|
122
|
+
if (have && !isOlderVersion(have, want)) continue;
|
|
123
|
+
const args = addDependencyArgs(pm, `${name}@${want}`, { dev });
|
|
96
124
|
const s = p.spinner();
|
|
97
|
-
s.start(`Upgrading ${name} from ${have} to ${want}`);
|
|
98
|
-
const ok = await runCommandAsync(
|
|
99
|
-
s.stop(ok ? `Upgraded ${name} to ${want}` : `Could not upgrade ${name} automatically — install ${name}@${want} yourself`);
|
|
125
|
+
s.start(have ? `Upgrading ${name} from ${have} to ${want}` : `Installing ${name}@${want}`);
|
|
126
|
+
const ok = await runCommandAsync(args, cwd, "ignore");
|
|
127
|
+
if (have) s.stop(ok ? `Upgraded ${name} to ${want}` : `Could not upgrade ${name} automatically — install ${name}@${want} yourself`);
|
|
128
|
+
else s.stop(ok ? `Installed ${name}@${want}` : `Could not install ${name} automatically — run \`${args.join(" ")}\` yourself`);
|
|
100
129
|
}
|
|
101
130
|
}
|
|
102
131
|
const init = defineCommand({
|
|
@@ -145,7 +174,9 @@ const init = defineCommand({
|
|
|
145
174
|
process.exit(1);
|
|
146
175
|
}
|
|
147
176
|
const pkg = readPkg(pkgPath);
|
|
148
|
-
const
|
|
177
|
+
const detectedPm = await detectPackageManager(cwd);
|
|
178
|
+
const pm = detectedPm ?? (yes ? detectInvokingPackageManager() ?? "npm" : await selectPackageManager("Couldn't detect this project's package manager — which should Kizlo use?"));
|
|
179
|
+
if (!detectedPm && !yes) persistPackageManagerField(cwd, pm);
|
|
149
180
|
const hasKizlo = Boolean(pkg.dependencies?.kizlo) || Boolean(pkg.devDependencies?.kizlo);
|
|
150
181
|
const hasSrcDir = fs.existsSync(path.join(cwd, "src"));
|
|
151
182
|
if (yes) loadEnvFiles(cwd);
|
|
@@ -221,17 +252,18 @@ const init = defineCommand({
|
|
|
221
252
|
if (manifest.requires && !manifest.requires.anyDir.some((rel) => fs.existsSync(path.join(cwd, rel)))) cancelFrom(manifest.requires.message);
|
|
222
253
|
const apiPath = manifest.apiPath;
|
|
223
254
|
const envKeys = resolveEnvKeys(manifest);
|
|
224
|
-
const
|
|
255
|
+
const flagAlias = args.alias !== void 0 ? String(args.alias).trim() : void 0;
|
|
256
|
+
const aliasCtx = {
|
|
225
257
|
cwd,
|
|
226
258
|
hasSrcDir,
|
|
227
259
|
apiPath,
|
|
260
|
+
persistedAlias: readPersistedAlias(cwd),
|
|
261
|
+
flagAlias
|
|
262
|
+
};
|
|
263
|
+
const setup = yes ? collectFromEnv({
|
|
264
|
+
...aliasCtx,
|
|
228
265
|
envKeys
|
|
229
|
-
}) : await collectInteractively(
|
|
230
|
-
cwd,
|
|
231
|
-
hasSrcDir,
|
|
232
|
-
apiPath
|
|
233
|
-
});
|
|
234
|
-
if (args.alias !== void 0) setup.alias = String(args.alias).trim();
|
|
266
|
+
}) : await collectInteractively(aliasCtx);
|
|
235
267
|
setup.alias = aliasWithSlash(setup.alias);
|
|
236
268
|
const includeExamples = yes ? true : orCancel(await p.confirm({
|
|
237
269
|
message: "Add example pages?",
|
|
@@ -284,7 +316,7 @@ const init = defineCommand({
|
|
|
284
316
|
applyProjectPatches(cwd, patchEntries(changesFor(manifest, "init")), manifest.conventions, scaffold);
|
|
285
317
|
for (const note of manifest.notes ?? []) {
|
|
286
318
|
const rendered = renderNote(note, setup.alias);
|
|
287
|
-
|
|
319
|
+
printManualStep(rendered.title, rendered.body);
|
|
288
320
|
}
|
|
289
321
|
if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
|
|
290
322
|
nextStepsNote(setup);
|
|
@@ -130,7 +130,7 @@ async function resolveTestConfig(cwd) {
|
|
|
130
130
|
portExplicit: test.port !== void 0,
|
|
131
131
|
fixtures: test.fixtures ?? [],
|
|
132
132
|
credentialsPath: credentialsPath(cwd),
|
|
133
|
-
packageManager: test.packageManager ?? detectPackageManager(configDir)
|
|
133
|
+
packageManager: test.packageManager ?? await detectPackageManager(configDir) ?? "npm"
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
136
|
/**
|
|
@@ -143,9 +143,11 @@ function applyPatchToSource(source, patch) {
|
|
|
143
143
|
/**
|
|
144
144
|
* Render a resolved patch as source the user can paste in, for the printed fallback when the target
|
|
145
145
|
* can't be resolved or parsed. The same declared payload the confident apply upserts, so what we tell
|
|
146
|
-
* the user to add never drifts from what we would have written.
|
|
146
|
+
* the user to add never drifts from what we would have written. A `note`-mode patch carries its own
|
|
147
|
+
* verbatim body (its change isn't an import/`export const`), so that is printed as-is when present.
|
|
147
148
|
*/
|
|
148
149
|
function renderPatchCode(patch) {
|
|
150
|
+
if (patch.note) return patch.note.endsWith("\n") ? patch.note : `${patch.note}\n`;
|
|
149
151
|
const imports = patch.imports.map((imp) => `import { ${imp.names.join(", ")} } from "${imp.module}"`);
|
|
150
152
|
const exports = patch.exports.map((exp) => `export const ${exp.name} = ${exp.value}`);
|
|
151
153
|
return `${[
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getVersion } from "../utils.js";
|
|
1
|
+
import { getVersion, readTsconfigPaths } from "../utils.js";
|
|
2
2
|
import z from "zod/v4";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fs from "node:fs";
|
|
@@ -34,7 +34,9 @@ import fs from "node:fs";
|
|
|
34
34
|
* overwrite policy.
|
|
35
35
|
* - `patch` — a partial injection into a file the framework owns (the root layout's SEO exports):
|
|
36
36
|
* it adds or replaces individual modules (imports/exports) inside the file, never the whole file.
|
|
37
|
-
* Both commands try a confident apply, otherwise print the payload with placement instructions.
|
|
37
|
+
* Both commands try a confident apply, otherwise print the payload with placement instructions. A
|
|
38
|
+
* patch may instead set `mode: "note"` to always print its instructions rather than edit the file —
|
|
39
|
+
* for a change too shaped-unlike an import/export to auto-merge (Astro's `defineConfig` output/adapter).
|
|
38
40
|
*/
|
|
39
41
|
const conventionsSchema = z.object({
|
|
40
42
|
kizloDir: z.string(),
|
|
@@ -52,14 +54,16 @@ const patchSchema = z.object({
|
|
|
52
54
|
role: z.string(),
|
|
53
55
|
label: z.string(),
|
|
54
56
|
path: z.string(),
|
|
57
|
+
mode: z.enum(["apply", "note"]).default("apply"),
|
|
55
58
|
imports: z.array(z.object({
|
|
56
59
|
module: z.string(),
|
|
57
60
|
names: z.array(z.string())
|
|
58
|
-
})),
|
|
61
|
+
})).default([]),
|
|
59
62
|
exports: z.array(z.object({
|
|
60
63
|
name: z.string(),
|
|
61
64
|
value: z.string()
|
|
62
|
-
}))
|
|
65
|
+
})).default([]),
|
|
66
|
+
note: z.string().optional()
|
|
63
67
|
});
|
|
64
68
|
const bootstrapSchema = z.object({
|
|
65
69
|
initializer: z.string(),
|
|
@@ -238,10 +242,41 @@ function patchEntries(changes) {
|
|
|
238
242
|
function isExample(change) {
|
|
239
243
|
return change.kind === "file" && change.example === true;
|
|
240
244
|
}
|
|
241
|
-
/**
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
|
|
245
|
+
/** Escape a string so its characters are matched literally inside a RegExp. */
|
|
246
|
+
function escapeRegExp(value) {
|
|
247
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* The directory the template's alias resolves to — the maintainer's own preference, read straight from
|
|
251
|
+
* the template's tsconfig `paths` (e.g. `@/* → ./src/*` yields `src`). This is the root stripped to turn
|
|
252
|
+
* an alias import back into a real template path before it's remapped to the project. Falls back to the
|
|
253
|
+
* `src`/root heuristic when the template declares no matching alias, staying consistent with the rest of
|
|
254
|
+
* the alias handling.
|
|
255
|
+
*/
|
|
256
|
+
function templateAliasBase(templateDir, conventions) {
|
|
257
|
+
const alias = conventions.alias.replace(/\/+$/, "");
|
|
258
|
+
const mapping = readTsconfigPaths(templateDir)?.[`${alias}/*`]?.[0];
|
|
259
|
+
if (mapping) return mapping.replace(/^\.\//, "").replace(/\/\*$/, "").replace(/\/+$/, "");
|
|
260
|
+
return conventions.kizloDir.startsWith("src/") ? "src" : "";
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Rewrite every template-alias import in a file to the project's chosen import style — the one place that
|
|
264
|
+
* translates the maintainer's import preference into the user's. A template writes its cross-file imports
|
|
265
|
+
* against its own alias (`@/layouts/Layout.astro`, `@/lib/kizlo/server`), rooted at the directory its
|
|
266
|
+
* tsconfig maps the alias to (`base`). On apply each such specifier is resolved back to the imported
|
|
267
|
+
* file's real location in the project (via {@link adaptTemplatePath}, so a relocated Kizlo dir is honored)
|
|
268
|
+
* and re-emitted through the project's alias — or a relative import when the user wants no alias at all.
|
|
269
|
+
* Only quoted specifiers that begin with the template alias are touched, so an unrelated string can't be
|
|
270
|
+
* corrupted. Subsumes the server-entry import: it is simply the alias import that lands in the Kizlo dir.
|
|
271
|
+
*/
|
|
272
|
+
function rewriteAliasImports(body, fromDir, base, conventions, ctx) {
|
|
273
|
+
const alias = conventions.alias.replace(/\/+$/, "");
|
|
274
|
+
if (!alias) return body;
|
|
275
|
+
const pattern = new RegExp(`(["'])${escapeRegExp(alias)}/([^"']+)\\1`, "g");
|
|
276
|
+
return body.replace(pattern, (_match, quote, rest) => {
|
|
277
|
+
const targetRel = adaptTemplatePath(base ? `${base}/${rest}` : rest, conventions, ctx);
|
|
278
|
+
return `${quote}${ctx.importFrom(targetRel, fromDir)}${quote}`;
|
|
279
|
+
});
|
|
245
280
|
}
|
|
246
281
|
/**
|
|
247
282
|
* Adapt a template path prefix (`src/app/...`, `src/lib/kizlo/...`) to the project's real directories.
|
|
@@ -255,19 +290,18 @@ function adaptTemplatePath(relPath, conventions, ctx) {
|
|
|
255
290
|
}
|
|
256
291
|
/**
|
|
257
292
|
* Turn a `file` manifest entry into the {@link ScaffoldFile} init writes: read the template's real
|
|
258
|
-
* file, rewrite its path prefix to the project's directories, and
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* moves to runtime and stays as precise.
|
|
293
|
+
* file, rewrite its path prefix to the project's directories, and retarget every template-alias import
|
|
294
|
+
* (the server entry plus any sibling like Astro's `@/layouts/Layout.astro`) to the specifier resolved
|
|
295
|
+
* at the file's new location — the project's alias or a relative import. Only the exact alias specifiers
|
|
296
|
+
* the template uses are rewritten, not a loose directory substring, so an unrelated occurrence can't be
|
|
297
|
+
* corrupted. This is the job the old build-time extractor did; it moves to runtime and stays as precise.
|
|
263
298
|
*/
|
|
264
299
|
function adaptFile(templateDir, entry, conventions, ctx) {
|
|
265
300
|
const abs = path.join(templateDir, entry.path);
|
|
266
301
|
if (!fs.existsSync(abs)) throw new Error(`Template file "${entry.path}" is listed in the manifest but does not exist in the template.`);
|
|
267
302
|
const body = fs.readFileSync(abs, "utf8");
|
|
268
303
|
const relPath = adaptTemplatePath(entry.path, conventions, ctx);
|
|
269
|
-
const
|
|
270
|
-
const contents = body.split(serverImportSpecifier(conventions)).join(ctx.serverImport(fromDir));
|
|
304
|
+
const contents = rewriteAliasImports(body, path.posix.dirname(relPath), templateAliasBase(templateDir, conventions), conventions, ctx);
|
|
271
305
|
return {
|
|
272
306
|
label: ROLE_LABELS[entry.role] ?? entry.role,
|
|
273
307
|
relPath,
|
|
@@ -285,8 +319,10 @@ function resolvePatch(entry, conventions, ctx) {
|
|
|
285
319
|
return {
|
|
286
320
|
label: entry.label,
|
|
287
321
|
relPath,
|
|
322
|
+
mode: entry.mode,
|
|
288
323
|
imports,
|
|
289
|
-
exports: entry.exports
|
|
324
|
+
exports: entry.exports,
|
|
325
|
+
note: entry.note
|
|
290
326
|
};
|
|
291
327
|
}
|
|
292
328
|
|
package/dist/cli/utils.js
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import * as p from "@clack/prompts";
|
|
8
|
+
import { detect } from "package-manager-detector/detect";
|
|
8
9
|
|
|
9
10
|
//#region src/cli/utils.ts
|
|
10
11
|
/**
|
|
@@ -45,6 +46,30 @@ function readTsconfigPaths(cwd) {
|
|
|
45
46
|
}
|
|
46
47
|
return json.compilerOptions?.paths;
|
|
47
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* The kizlo.config file names, in resolution order. Mirrors `CONFIG_FILES` in daemon/config — kept here
|
|
51
|
+
* to avoid an import cycle, since daemon/config already depends on this module.
|
|
52
|
+
*/
|
|
53
|
+
const KIZLO_CONFIG_FILES = [
|
|
54
|
+
"kizlo.config.ts",
|
|
55
|
+
"kizlo.config.js",
|
|
56
|
+
"kizlo.config.mjs"
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* The import alias a previous `kizlo init` recorded in kizlo.config — the persisted preference a re-run
|
|
60
|
+
* reuses so it never asks again. Returns the prefix (`""` for the deliberate relative-imports choice),
|
|
61
|
+
* or `undefined` when there's no config or it declares no `alias` at all (the choice was never made, so
|
|
62
|
+
* the caller must prompt or default). A light text scan of the generated file, so setup never has to
|
|
63
|
+
* import the TS config — or resolve `kizlo` — just to read one field back.
|
|
64
|
+
*/
|
|
65
|
+
function readPersistedAlias(cwd) {
|
|
66
|
+
for (const name of KIZLO_CONFIG_FILES) {
|
|
67
|
+
const file = path.join(cwd, name);
|
|
68
|
+
if (!fs.existsSync(file)) continue;
|
|
69
|
+
const match = fs.readFileSync(file, "utf8").match(/\balias\s*:\s*["']([^"']*)["']/);
|
|
70
|
+
return match ? match[1] : void 0;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
48
73
|
function relativeImport(targetRel, fromDirRel) {
|
|
49
74
|
const rel = path.relative(fromDirRel, targetRel).split(path.sep).join("/");
|
|
50
75
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
@@ -71,6 +96,14 @@ function detectImportAlias(cwd, targetRel) {
|
|
|
71
96
|
}
|
|
72
97
|
}
|
|
73
98
|
/**
|
|
99
|
+
* Normalize an alias prefix to the canonical `@/` form — the way an import alias is actually written and
|
|
100
|
+
* declared in tsconfig `paths` (`@/*`), so the persisted preference reads like the imports it produces.
|
|
101
|
+
* A trailing slash is enforced (`@` → `@/`); empty stays empty, the recorded "relative imports" choice.
|
|
102
|
+
*/
|
|
103
|
+
function aliasWithSlash(alias) {
|
|
104
|
+
return alias ? `${alias.replace(/\/+$/, "")}/` : "";
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
74
107
|
* Builds an import specifier for `targetRel` from `fromDirRel`. With an explicit
|
|
75
108
|
* `alias` prefix (from kizlo.config), builds `<alias>/<target>`; an empty alias
|
|
76
109
|
* forces a relative import. Without one, detects from tsconfig, else relative.
|
|
@@ -101,11 +134,50 @@ function getVersion() {
|
|
|
101
134
|
const pkgPath = path.resolve(here, "../../package.json");
|
|
102
135
|
return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version;
|
|
103
136
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
137
|
+
/**
|
|
138
|
+
* The package manager a project uses, from evidence in the project or an enclosing workspace — a lockfile
|
|
139
|
+
* (the strongest signal, it reflects an actual install), else package.json's corepack `packageManager` /
|
|
140
|
+
* `devEngines.packageManager` field (declared intent, present before the first install). Delegates to
|
|
141
|
+
* `package-manager-detector`, which recognises every lockfile variant (npm's `package-lock.json` and
|
|
142
|
+
* `npm-shrinkwrap.json` included) and walks up from `cwd` to the filesystem root — so a package in a
|
|
143
|
+
* monorepo resolves to the manager pinned by the workspace root, where the single lockfile lives. Returns
|
|
144
|
+
* `undefined` when no signal is found anywhere up the tree, or the manager isn't one Kizlo supports, rather
|
|
145
|
+
* than guessing: the invoking manager is unreliable (`npx` reports `npm` whatever the user really uses).
|
|
146
|
+
* Callers that can't proceed without one should ask the user (see `selectPackageManager`) instead of
|
|
147
|
+
* defaulting.
|
|
148
|
+
*/
|
|
149
|
+
async function detectPackageManager(cwd) {
|
|
150
|
+
const name = (await detect({ cwd }))?.name;
|
|
151
|
+
return name === "pnpm" || name === "yarn" || name === "bun" || name === "npm" ? name : void 0;
|
|
152
|
+
}
|
|
153
|
+
/** The installed version of `pm`, from `pm --version` (e.g. `"9.0.0"`). Undefined when it can't be run. */
|
|
154
|
+
function packageManagerVersion(pm) {
|
|
155
|
+
const result = spawnSync(pm, ["--version"], {
|
|
156
|
+
encoding: "utf8",
|
|
157
|
+
shell: process.platform === "win32"
|
|
158
|
+
});
|
|
159
|
+
if (result.error || result.status !== 0) return void 0;
|
|
160
|
+
const version = result.stdout.trim();
|
|
161
|
+
return version.length > 0 ? version : void 0;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Record `pm` as package.json's corepack `packageManager` field so a later `detectPackageManager` settles on
|
|
165
|
+
* it instead of asking again — the fix for a project that gave no signal and had to be prompted for. Stamps
|
|
166
|
+
* the installed version (`pm --version`) to keep the field corepack-valid, dropping it only when the version
|
|
167
|
+
* can't be read. Leaves an existing field untouched, preserves the file's indentation, and swallows any
|
|
168
|
+
* read/write failure: persisting the choice is a convenience, not worth failing the command over.
|
|
169
|
+
*/
|
|
170
|
+
function persistPackageManagerField(cwd, pm) {
|
|
171
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
172
|
+
try {
|
|
173
|
+
const raw = fs.readFileSync(pkgPath, "utf8");
|
|
174
|
+
const pkg = JSON.parse(raw);
|
|
175
|
+
if (pkg.packageManager) return;
|
|
176
|
+
const version = packageManagerVersion(pm);
|
|
177
|
+
pkg.packageManager = version ? `${pm}@${version}` : pm;
|
|
178
|
+
const indent = raw.match(/\n([ \t]+)/)?.[1] ?? " ";
|
|
179
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, indent)}\n`);
|
|
180
|
+
} catch {}
|
|
109
181
|
}
|
|
110
182
|
/**
|
|
111
183
|
* The package manager that invoked the current process, read from `npm_config_user_agent`
|
|
@@ -132,23 +204,40 @@ function isCommandAvailable(command) {
|
|
|
132
204
|
function availablePackageManagers(order) {
|
|
133
205
|
return order.filter((pm) => isCommandAvailable(pm));
|
|
134
206
|
}
|
|
135
|
-
|
|
136
|
-
|
|
207
|
+
/**
|
|
208
|
+
* The argv to add a single package with `pm`, e.g. `pnpm add kizlo`. `dev: true` installs it as a
|
|
209
|
+
* dev dependency — npm spells that `--save-dev`, every other supported manager `--dev`.
|
|
210
|
+
*/
|
|
211
|
+
function addDependencyArgs(pm, pkg, opts = {}) {
|
|
212
|
+
const base = pm === "npm" ? [
|
|
137
213
|
"npm",
|
|
138
214
|
"install",
|
|
139
215
|
pkg
|
|
140
|
-
]
|
|
141
|
-
return [
|
|
216
|
+
] : [
|
|
142
217
|
pm,
|
|
143
218
|
"add",
|
|
144
219
|
pkg
|
|
145
220
|
];
|
|
221
|
+
if (!opts.dev) return base;
|
|
222
|
+
return [...base, pm === "npm" ? "--save-dev" : "--dev"];
|
|
146
223
|
}
|
|
147
224
|
/** The argv to install a project's dependencies with `pm` — every supported manager spells it `<pm> install`. */
|
|
148
225
|
function installArgs(pm) {
|
|
149
226
|
return [pm, "install"];
|
|
150
227
|
}
|
|
151
228
|
/**
|
|
229
|
+
* pnpm (v10+) and bun refuse to run dependencies' build/lifecycle scripts on install by default, leaving
|
|
230
|
+
* those packages unbuilt — pnpm prints an `Ignored build scripts:` warning, bun a `Blocked N postinstall`
|
|
231
|
+
* one. The install still exits 0, so nothing else flags it. Scan an install's captured output for that
|
|
232
|
+
* warning and, when present, return the command that runs the blocked scripts (`pnpm approve-builds` /
|
|
233
|
+
* `bun pm trust --all`) so the caller can surface it as a follow-up step. Undefined when the output shows
|
|
234
|
+
* none, or the manager runs scripts on install anyway (npm, yarn) so there's nothing to approve.
|
|
235
|
+
*/
|
|
236
|
+
function approveBuildsCommand(pm, output) {
|
|
237
|
+
if (pm === "pnpm" && /ignored build scripts/i.test(output)) return "pnpm approve-builds";
|
|
238
|
+
if (pm === "bun" && /blocked \d+ postinstall/i.test(output)) return "bun pm trust --all";
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
152
241
|
* The argv to run a framework's `create-*` initializer through a package manager, e.g.
|
|
153
242
|
* `pnpm create next-app@latest my-app --ts …`. The mechanics differ per manager: npm forwards
|
|
154
243
|
* flags to the initializer only after a `--` separator, and `yarn create` resolves the latest
|
|
@@ -180,6 +269,15 @@ function frameworkCreateArgs(pm, initializer, name, flags) {
|
|
|
180
269
|
];
|
|
181
270
|
}
|
|
182
271
|
}
|
|
272
|
+
function runCommand(args, cwd, stdio = "inherit") {
|
|
273
|
+
const [command, ...rest] = args;
|
|
274
|
+
if (!command) return false;
|
|
275
|
+
return spawnSync(command, rest, {
|
|
276
|
+
cwd,
|
|
277
|
+
stdio,
|
|
278
|
+
shell: process.platform === "win32"
|
|
279
|
+
}).status === 0;
|
|
280
|
+
}
|
|
183
281
|
/**
|
|
184
282
|
* Async sibling of {@link runCommand}: runs a command without blocking the event loop, so a clack
|
|
185
283
|
* spinner can keep animating while it works (a synchronous `spawnSync` would freeze it on the first
|
|
@@ -219,6 +317,40 @@ function runCommandCaptured(args, cwd) {
|
|
|
219
317
|
output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
|
|
220
318
|
};
|
|
221
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Async sibling of {@link runCommandCaptured}: captures a command's combined output without blocking the
|
|
322
|
+
* event loop, so a clack spinner keeps animating while it runs (the synchronous `runCommandCaptured` would
|
|
323
|
+
* freeze it on the first frame). Resolves whether it exited cleanly plus the trimmed stdout+stderr; a
|
|
324
|
+
* spawn error (binary off PATH) resolves `{ ok: false, output: "" }`.
|
|
325
|
+
*/
|
|
326
|
+
function runCommandCapturedAsync(args, cwd) {
|
|
327
|
+
const [command, ...rest] = args;
|
|
328
|
+
if (!command) return Promise.resolve({
|
|
329
|
+
ok: false,
|
|
330
|
+
output: ""
|
|
331
|
+
});
|
|
332
|
+
return new Promise((resolve$1) => {
|
|
333
|
+
const child = spawn(command, rest, {
|
|
334
|
+
cwd,
|
|
335
|
+
shell: process.platform === "win32"
|
|
336
|
+
});
|
|
337
|
+
let output = "";
|
|
338
|
+
child.stdout?.on("data", (chunk) => {
|
|
339
|
+
output += chunk;
|
|
340
|
+
});
|
|
341
|
+
child.stderr?.on("data", (chunk) => {
|
|
342
|
+
output += chunk;
|
|
343
|
+
});
|
|
344
|
+
child.on("error", () => resolve$1({
|
|
345
|
+
ok: false,
|
|
346
|
+
output: ""
|
|
347
|
+
}));
|
|
348
|
+
child.on("close", (code) => resolve$1({
|
|
349
|
+
ok: code === 0,
|
|
350
|
+
output: output.trim()
|
|
351
|
+
}));
|
|
352
|
+
});
|
|
353
|
+
}
|
|
222
354
|
function writeFileIfAbsent(filePath, contents) {
|
|
223
355
|
if (fs.existsSync(filePath)) return false;
|
|
224
356
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -333,6 +465,42 @@ function mergeEnv(existing, values, overwriteKeys, groups) {
|
|
|
333
465
|
kept
|
|
334
466
|
};
|
|
335
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* Whether `dir` already sits inside a git work tree — true when the framework CLI ran `git init`, or
|
|
470
|
+
* when scaffolding into a subfolder of an existing repo (e.g. a monorepo). Used to skip offering
|
|
471
|
+
* `git init` where a repo is already present rather than nesting one. A missing `git` binary (spawn
|
|
472
|
+
* error) reads as "not a repo".
|
|
473
|
+
*/
|
|
474
|
+
function isGitRepository(dir) {
|
|
475
|
+
const result = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
476
|
+
cwd: dir,
|
|
477
|
+
stdio: "ignore",
|
|
478
|
+
shell: process.platform === "win32"
|
|
479
|
+
});
|
|
480
|
+
return !result.error && result.status === 0;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Initialize a git repository in `dir` with an initial commit. Best-effort: returns whether the repo
|
|
484
|
+
* was created (`git init` succeeded). The commit is attempted but not required — it needs a configured
|
|
485
|
+
* `user.name`/`user.email` a bare machine may lack, and a repo with staged-but-uncommitted files is
|
|
486
|
+
* still a usable starting point. Returns false when `git` isn't installed.
|
|
487
|
+
*/
|
|
488
|
+
function initGitRepository(dir) {
|
|
489
|
+
if (!isCommandAvailable("git")) return false;
|
|
490
|
+
if (!runCommand(["git", "init"], dir, "ignore")) return false;
|
|
491
|
+
runCommand([
|
|
492
|
+
"git",
|
|
493
|
+
"add",
|
|
494
|
+
"-A"
|
|
495
|
+
], dir, "ignore");
|
|
496
|
+
runCommand([
|
|
497
|
+
"git",
|
|
498
|
+
"commit",
|
|
499
|
+
"-m",
|
|
500
|
+
"Initial commit"
|
|
501
|
+
], dir, "ignore");
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
336
504
|
function ensureGitignored(cwd, entry) {
|
|
337
505
|
const gitignorePath = path.join(cwd, ".gitignore");
|
|
338
506
|
const existed = fs.existsSync(gitignorePath);
|
|
@@ -376,4 +544,4 @@ function groupDefault(subCommandNames, run) {
|
|
|
376
544
|
}
|
|
377
545
|
|
|
378
546
|
//#endregion
|
|
379
|
-
export { DEFAULT_ENV_KEYS, addDependencyArgs, availablePackageManagers, detectImportAlias, detectInvokingPackageManager, detectPackageManager, ensureGitignored, envGroups, envKeysPresent, frameworkCreateArgs, getVersion, groupDefault, installArgs, loadEnvFiles, mergeEnv, pickStackPort, resolveModuleImport, runCommandAsync, runCommandCaptured, withSpinner, writeFileIfAbsent };
|
|
547
|
+
export { DEFAULT_ENV_KEYS, addDependencyArgs, aliasWithSlash, approveBuildsCommand, availablePackageManagers, detectImportAlias, detectInvokingPackageManager, detectPackageManager, ensureGitignored, envGroups, envKeysPresent, frameworkCreateArgs, getVersion, groupDefault, initGitRepository, installArgs, isCommandAvailable, isGitRepository, loadEnvFiles, mergeEnv, persistPackageManagerField, pickStackPort, readPersistedAlias, readTsconfigPaths, resolveModuleImport, runCommandAsync, runCommandCaptured, runCommandCapturedAsync, withSpinner, writeFileIfAbsent };
|
package/dist/config.d.ts
CHANGED
|
@@ -63,8 +63,9 @@ interface KizloGlobalConfig {
|
|
|
63
63
|
*/
|
|
64
64
|
dir?: string;
|
|
65
65
|
/**
|
|
66
|
-
* Import alias prefix for generated imports (e.g. `@`).
|
|
67
|
-
*
|
|
66
|
+
* Import alias prefix for generated imports (e.g. `@`). Recorded by `kizlo init`
|
|
67
|
+
* from your `--alias` flag or its prompt, so later runs reuse it instead of asking
|
|
68
|
+
* again; an empty string (`""`) is the recorded choice to use relative imports.
|
|
68
69
|
*/
|
|
69
70
|
alias?: string;
|
|
70
71
|
/**
|
package/dist/package.js
CHANGED
package/dist/router.d.ts
CHANGED
|
@@ -179,7 +179,7 @@ declare const ROUTER_MAP: {
|
|
|
179
179
|
include?: string | number | (string | number)[] | undefined;
|
|
180
180
|
offset?: string | number | undefined;
|
|
181
181
|
order?: "asc" | "desc" | undefined;
|
|
182
|
-
orderby?: "
|
|
182
|
+
orderby?: "date" | "id" | "slug" | "parent" | "title" | "author" | "include" | "modified" | "relevance" | "include_slugs" | undefined;
|
|
183
183
|
searchColumns?: ("post_title" | "post_content" | "post_excerpt")[] | undefined;
|
|
184
184
|
slug?: string | string[] | undefined;
|
|
185
185
|
taxRelation?: "AND" | "OR" | undefined;
|
|
@@ -507,7 +507,7 @@ declare const ROUTER_MAP: {
|
|
|
507
507
|
include?: string | number | (string | number)[] | undefined;
|
|
508
508
|
offset?: string | number | undefined;
|
|
509
509
|
order?: "asc" | "desc" | undefined;
|
|
510
|
-
orderby?: "
|
|
510
|
+
orderby?: "date" | "id" | "slug" | "parent" | "title" | "author" | "include" | "modified" | "relevance" | "include_slugs" | "menu_order" | undefined;
|
|
511
511
|
searchColumns?: ("post_title" | "post_content" | "post_excerpt")[] | undefined;
|
|
512
512
|
slug?: string | string[] | undefined;
|
|
513
513
|
parent?: string | number | (string | number)[] | undefined;
|
|
@@ -1186,7 +1186,7 @@ declare const ROUTER_MAP: {
|
|
|
1186
1186
|
include?: string | number | (string | number)[] | undefined;
|
|
1187
1187
|
offset?: string | number | undefined;
|
|
1188
1188
|
order?: "asc" | "desc" | undefined;
|
|
1189
|
-
orderby?: "
|
|
1189
|
+
orderby?: "date" | "id" | "slug" | "parent" | "title" | "author" | "include" | "relevance" | "include_slugs" | "menu_order" | undefined;
|
|
1190
1190
|
searchColumns?: ("post_title" | "post_content" | "post_excerpt")[] | undefined;
|
|
1191
1191
|
slug?: string | string[] | undefined;
|
|
1192
1192
|
taxRelation?: "AND" | "OR" | undefined;
|
|
@@ -1265,7 +1265,7 @@ declare const ROUTER_MAP: {
|
|
|
1265
1265
|
include?: string | number | (string | number)[] | undefined;
|
|
1266
1266
|
offset?: string | number | undefined;
|
|
1267
1267
|
order?: "asc" | "desc" | undefined;
|
|
1268
|
-
orderby?: "
|
|
1268
|
+
orderby?: "date" | "id" | "slug" | "parent" | "title" | "author" | "include" | "relevance" | "include_slugs" | "menu_order" | undefined;
|
|
1269
1269
|
searchColumns?: ("post_title" | "post_content" | "post_excerpt")[] | undefined;
|
|
1270
1270
|
slug?: string | string[] | undefined;
|
|
1271
1271
|
taxRelation?: "AND" | "OR" | undefined;
|
|
@@ -1414,7 +1414,7 @@ declare const ROUTER_MAP: {
|
|
|
1414
1414
|
include?: string | number | (string | number)[] | undefined;
|
|
1415
1415
|
offset?: string | number | undefined;
|
|
1416
1416
|
order?: "asc" | "desc" | undefined;
|
|
1417
|
-
orderby?: "
|
|
1417
|
+
orderby?: "date" | "id" | "parent" | "post" | "include" | undefined;
|
|
1418
1418
|
parent?: string | number | (string | number)[] | undefined;
|
|
1419
1419
|
parentExclude?: string | number | (string | number)[] | undefined;
|
|
1420
1420
|
post?: string | number | (string | number)[] | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kizlo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A headless WordPress framework for TypeScript.",
|
|
6
6
|
"keywords": [
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
"giget": "^3.0.0",
|
|
103
103
|
"jiti": "^2.7.0",
|
|
104
104
|
"magicast": "^0.5.3",
|
|
105
|
+
"package-manager-detector": "^1.8.0",
|
|
105
106
|
"zod": "^4.3.6"
|
|
106
107
|
},
|
|
107
108
|
"peerDependencies": {
|