kizlo 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/_setup.js +91 -55
- package/dist/cli/commands/_tasks.js +152 -0
- package/dist/cli/commands/_wiring.js +11 -10
- package/dist/cli/commands/create.js +270 -87
- package/dist/cli/commands/init.js +122 -61
- package/dist/cli/daemon/config.js +1 -1
- package/dist/cli/presets/source.js +13 -8
- package/dist/cli/presets/template.js +255 -50
- package/dist/cli/utils.js +149 -80
- package/dist/integrations/tanstack-start/api.d.ts +15 -0
- package/dist/integrations/tanstack-start/api.js +21 -0
- package/dist/integrations/tanstack-start/kizlo.client.d.ts +20 -0
- package/dist/integrations/tanstack-start/kizlo.client.js +18 -0
- package/dist/integrations/tanstack-start/kizlo.server.d.ts +14 -0
- package/dist/integrations/tanstack-start/kizlo.server.js +15 -0
- package/dist/integrations/tanstack-start/manifest.d.ts +9 -0
- package/dist/integrations/tanstack-start/manifest.js +11 -0
- package/dist/integrations/tanstack-start/metadata.d.ts +49 -0
- package/dist/integrations/tanstack-start/metadata.js +112 -0
- package/dist/integrations/tanstack-start/robots.d.ts +8 -0
- package/dist/integrations/tanstack-start/robots.js +11 -0
- package/dist/integrations/tanstack-start/root-head.d.ts +15 -0
- package/dist/integrations/tanstack-start/root-head.js +54 -0
- package/dist/integrations/tanstack-start/sitemap.d.ts +16 -0
- package/dist/integrations/tanstack-start/sitemap.js +20 -0
- package/dist/integrations/tanstack-start/utils.d.ts +20 -0
- package/dist/integrations/tanstack-start/utils.js +17 -0
- package/dist/package.js +1 -1
- package/dist/router.d.ts +5 -5
- package/dist/tanstack-start/server.d.ts +11 -0
- package/dist/tanstack-start/server.js +12 -0
- package/dist/tanstack-start.d.ts +4 -0
- package/dist/tanstack-start.js +5 -0
- package/package.json +22 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LOCAL_DIR_REL } from "../wp/constants.js";
|
|
2
2
|
import { composeStop, createStack, dockerHint, dockerStatus } from "../wp/docker.js";
|
|
3
3
|
import { DEFAULT_ENV_KEYS, availablePackageManagers, detectInvokingPackageManager, ensureGitignored, envGroups, envKeysPresent, mergeEnv, pickStackPort, writeFileIfAbsent } from "../utils.js";
|
|
4
|
+
import { promptFragment, resolvePromptDefault } from "../presets/template.js";
|
|
4
5
|
import { DEFAULT_DEV_DB_PORT, DEFAULT_DEV_PORT, resolveStackName, stackProject } from "../daemon/config.js";
|
|
5
6
|
import { createAdminAppPassword } from "../wp/bootstrap.js";
|
|
6
7
|
import { bootstrapDev } from "../wp/dev.js";
|
|
@@ -126,6 +127,54 @@ async function selectPackageManager(message) {
|
|
|
126
127
|
initialValue
|
|
127
128
|
}));
|
|
128
129
|
}
|
|
130
|
+
/** The choices summary shows a prompt under a short label — the title-cased token (`linter` → `Linter`). */
|
|
131
|
+
function promptLabel(token) {
|
|
132
|
+
return token.charAt(0).toUpperCase() + token.slice(1);
|
|
133
|
+
}
|
|
134
|
+
/** What a resolved prompt answer reads as in the choices summary: the option label, `Yes`/`No`, or the text. */
|
|
135
|
+
function promptDisplay(prompt, answer) {
|
|
136
|
+
if (prompt.kind === "confirm") return answer ? "Yes" : "No";
|
|
137
|
+
if (prompt.kind === "select") return prompt.options.find((opt) => opt.value === answer)?.label ?? String(answer);
|
|
138
|
+
return answer === "" ? "(default)" : String(answer);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Ask the template's own prompts and turn the answers into `bootstrap` substitutions. Each prompt maps its
|
|
142
|
+
* answer to a CLI fragment (via {@link promptFragment}) keyed by the prompt's `{{token}}`, which
|
|
143
|
+
* `bootstrapArgs` splices into the framework command — so a template surfaces a real framework choice
|
|
144
|
+
* without the CLI knowing anything framework-specific. Non-interactive (`--yes`) resolves every prompt to
|
|
145
|
+
* its {@link resolvePromptDefault} with no I/O. Returns the token→fragment map plus the choices-summary rows.
|
|
146
|
+
*/
|
|
147
|
+
async function collectTemplatePrompts(manifest, opts) {
|
|
148
|
+
const args = {};
|
|
149
|
+
const rows = [];
|
|
150
|
+
for (const prompt of manifest.create?.prompts ?? []) {
|
|
151
|
+
let answer;
|
|
152
|
+
if (opts.yes) answer = resolvePromptDefault(prompt).answer;
|
|
153
|
+
else if (prompt.kind === "select") answer = orCancel(await p.select({
|
|
154
|
+
message: prompt.message,
|
|
155
|
+
options: prompt.options.map((opt) => ({
|
|
156
|
+
value: opt.value,
|
|
157
|
+
label: opt.label
|
|
158
|
+
})),
|
|
159
|
+
initialValue: prompt.default ?? prompt.options[0]?.value
|
|
160
|
+
}));
|
|
161
|
+
else if (prompt.kind === "confirm") answer = orCancel(await p.confirm({
|
|
162
|
+
message: prompt.message,
|
|
163
|
+
initialValue: prompt.default
|
|
164
|
+
}));
|
|
165
|
+
else answer = orCancel(await p.text({
|
|
166
|
+
message: prompt.message,
|
|
167
|
+
placeholder: prompt.placeholder,
|
|
168
|
+
defaultValue: prompt.default ?? ""
|
|
169
|
+
}));
|
|
170
|
+
args[prompt.token] = promptFragment(prompt, answer);
|
|
171
|
+
rows.push([promptLabel(prompt.token), promptDisplay(prompt, answer)]);
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
args,
|
|
175
|
+
rows
|
|
176
|
+
};
|
|
177
|
+
}
|
|
129
178
|
/** Appends the API path to the base URL so the client and route handler agree. */
|
|
130
179
|
function withApiPath(baseUrl, apiPath) {
|
|
131
180
|
try {
|
|
@@ -329,29 +378,21 @@ async function provisionLocalStack(cfg, siteSecret, baseUrl) {
|
|
|
329
378
|
};
|
|
330
379
|
}
|
|
331
380
|
/**
|
|
332
|
-
* Provision local WordPress and fill the connection's WP credentials from it
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
381
|
+
* Provision local WordPress and fill the connection's WP credentials from it. Mutates `conn` in place
|
|
382
|
+
* (wpUrl / wpUsername / wpPassword / adminPassword). Owns no spinner and never exits — it runs as one
|
|
383
|
+
* step of the execute checklist, so it throws on failure for the runner to render, and returns a
|
|
384
|
+
* non-fatal warning string (or `undefined`) the caller surfaces after the run. Writing the `local` flags
|
|
385
|
+
* into `kizlo.config.ts` is the caller's job. No-op for a remote connection.
|
|
336
386
|
*/
|
|
337
|
-
async function
|
|
338
|
-
if (conn.mode !== "local") return;
|
|
387
|
+
async function provisionLocalWordPress(cwd, conn) {
|
|
388
|
+
if (conn.mode !== "local") return void 0;
|
|
339
389
|
ensureGitignored(cwd, ".kizlo/");
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
conn.wpPassword = local.appPassword;
|
|
347
|
-
conn.adminPassword = local.adminPassword;
|
|
348
|
-
s.stop("Local WordPress ready");
|
|
349
|
-
if (local.secretSyncError) p.log.warn(`Could not sync KIZLO_LOCAL_WP_SECRET to the local plugin (${local.secretSyncError})`);
|
|
350
|
-
} catch (error) {
|
|
351
|
-
s.stop("Local WordPress setup failed");
|
|
352
|
-
p.cancel(error instanceof Error ? error.message : String(error));
|
|
353
|
-
process.exit(1);
|
|
354
|
-
}
|
|
390
|
+
const local = await provisionLocalStack(devConfigFor(cwd), conn.siteSecret, conn.baseUrl);
|
|
391
|
+
conn.wpUrl = local.url;
|
|
392
|
+
conn.wpUsername = local.username;
|
|
393
|
+
conn.wpPassword = local.appPassword;
|
|
394
|
+
conn.adminPassword = local.adminPassword;
|
|
395
|
+
return local.secretSyncError ? `Could not sync KIZLO_LOCAL_WP_SECRET to the local plugin (${local.secretSyncError})` : void 0;
|
|
355
396
|
}
|
|
356
397
|
/**
|
|
357
398
|
* The template's own `.env.example`, when it ships one. Templates own the framework-specific key names
|
|
@@ -374,45 +415,43 @@ function generatedEnvExample(envKeys) {
|
|
|
374
415
|
return `${content}\n# Point the app at local WordPress (managed by \`kizlo dev\`) instead of the keys above:\n# ${envKeys.local.connect}=local\n`;
|
|
375
416
|
}
|
|
376
417
|
/**
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
* generated block for the base preset.
|
|
418
|
+
* The managed keys already present in an existing `.env` — the values a write would overwrite. Collected
|
|
419
|
+
* up front (before anything runs) so the overwrite decision is made during the lazy prompt phase and
|
|
420
|
+
* passed to {@link writeEnv} as a resolved `overwrite` flag, rather than {@link writeEnv} stopping to ask
|
|
421
|
+
* mid-checklist. Empty when there's no `.env` or nothing collides.
|
|
382
422
|
*/
|
|
383
|
-
|
|
423
|
+
function envConflicts(cwd, envKeys, conn) {
|
|
424
|
+
const { keys } = managedEnv(envKeys, conn);
|
|
425
|
+
const envPath = path.join(cwd, ".env");
|
|
426
|
+
if (!fs.existsSync(envPath)) return [];
|
|
427
|
+
return envKeysPresent(fs.readFileSync(envPath, "utf8"), keys);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Write (or update) `.env` and `.env.example` for the managed keys and return a one-line summary for the
|
|
431
|
+
* checklist. Runs as a step of the execute phase, so it neither prompts nor logs: `overwrite` (whether to
|
|
432
|
+
* replace conflicting values) is decided during collection — see {@link envConflicts} — and passed in.
|
|
433
|
+
* `.env.example` is only written when absent, sourced from the template's own file (`opts.exampleTemplate`)
|
|
434
|
+
* so it matches the `.env` structure, falling back to a generated block for the base preset.
|
|
435
|
+
*/
|
|
436
|
+
function writeEnv(cwd, envKeys, conn, opts) {
|
|
384
437
|
const { keys, values: envValues } = managedEnv(envKeys, conn);
|
|
385
438
|
const envPath = path.join(cwd, ".env");
|
|
386
439
|
const envExisted = fs.existsSync(envPath);
|
|
387
|
-
const
|
|
388
|
-
const conflicts = envKeysPresent(existingEnv, keys);
|
|
389
|
-
let overwriteKeys = new Set(keys);
|
|
390
|
-
if (conflicts.length && !opts.force) if (opts.yes) {
|
|
391
|
-
overwriteKeys = /* @__PURE__ */ new Set();
|
|
392
|
-
p.log.info("Keeping existing .env values (pass --force to overwrite)");
|
|
393
|
-
} else {
|
|
394
|
-
p.log.warn("Some environment variables already exist in .env");
|
|
395
|
-
if (!orCancel(await p.confirm({
|
|
396
|
-
message: "Overwrite their existing values?",
|
|
397
|
-
initialValue: true
|
|
398
|
-
}))) overwriteKeys = /* @__PURE__ */ new Set();
|
|
399
|
-
}
|
|
400
|
-
const merge = mergeEnv(existingEnv, envValues, overwriteKeys, envGroups(envKeys));
|
|
440
|
+
const merge = mergeEnv(envExisted ? fs.readFileSync(envPath, "utf8") : "", envValues, opts.overwrite ? new Set(keys) : /* @__PURE__ */ new Set(), envGroups(envKeys));
|
|
401
441
|
fs.writeFileSync(envPath, merge.content);
|
|
402
442
|
const exampleBody = opts.exampleTemplate ?? generatedEnvExample(envKeys);
|
|
403
443
|
const exampleCreated = writeFileIfAbsent(path.join(cwd, ".env.example"), exampleBody);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
else p.log.info("Left .env unchanged");
|
|
407
|
-
p.log.success(exampleCreated ? "Created .env.example" : "Skipped .env.example (exists)");
|
|
444
|
+
const envMsg = !envExisted ? "Created .env" : merge.updated.length || merge.added.length ? "Updated .env" : "Left .env unchanged";
|
|
445
|
+
return exampleCreated ? `${envMsg}, created .env.example` : envMsg;
|
|
408
446
|
}
|
|
409
447
|
/**
|
|
410
|
-
* Push the site settings (secret, canonical site URL, backend URL) to a remote WordPress so
|
|
411
|
-
*
|
|
412
|
-
*
|
|
448
|
+
* Push the site settings (secret, canonical site URL, backend URL) to a remote WordPress so webhook
|
|
449
|
+
* signing and event delivery work. No-op for local (handled during provisioning) or when credentials are
|
|
450
|
+
* incomplete. Runs as a checklist step, so instead of logging it returns any non-fatal warnings (a failed
|
|
451
|
+
* sync, an outdated plugin) for the caller to surface after the run; success returns an empty array.
|
|
413
452
|
*/
|
|
414
453
|
async function syncRemote(conn) {
|
|
415
|
-
if (conn.mode !== "remote" || !conn.wpUrl || !conn.wpUsername || !conn.wpPassword) return;
|
|
454
|
+
if (conn.mode !== "remote" || !conn.wpUrl || !conn.wpUsername || !conn.wpPassword) return [];
|
|
416
455
|
const sync = await syncSiteSettings({
|
|
417
456
|
url: conn.wpUrl,
|
|
418
457
|
username: conn.wpUsername,
|
|
@@ -422,11 +461,8 @@ async function syncRemote(conn) {
|
|
|
422
461
|
siteUrl: conn.siteUrl,
|
|
423
462
|
backendUrl: conn.baseUrl
|
|
424
463
|
});
|
|
425
|
-
if (!sync.ok) {
|
|
426
|
-
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
if (!isPluginVersionSupported(sync.pluginVersion)) p.log.warn(pluginUpdateMessage(sync.pluginVersion));
|
|
464
|
+
if (!sync.ok) return [`Could not sync the site settings to WordPress (${sync.error}) — make sure the kizlo plugin is active, then set them from the Kizlo settings.`];
|
|
465
|
+
return isPluginVersionSupported(sync.pluginVersion) ? [] : [pluginUpdateMessage(sync.pluginVersion)];
|
|
430
466
|
}
|
|
431
467
|
/** The "Next steps" lines. `kizlo dev` is the single entry point for development. */
|
|
432
468
|
function nextStepsLines(conn, prefix = "") {
|
|
@@ -447,4 +483,4 @@ function nextStepsNote(conn, prefix = "") {
|
|
|
447
483
|
}
|
|
448
484
|
|
|
449
485
|
//#endregion
|
|
450
|
-
export { collectConnectionFromEnv, collectConnectionInteractively, dirPath, nextStepsLines, nextStepsNote, orCancel, pickAppPort, readEnvExample, resolveEnvKeys, selectPackageManager,
|
|
486
|
+
export { collectConnectionFromEnv, collectConnectionInteractively, collectTemplatePrompts, dirPath, envConflicts, nextStepsLines, nextStepsNote, orCancel, pickAppPort, provisionLocalWordPress, readEnvExample, resolveEnvKeys, selectPackageManager, syncRemote, validate, withApiPath, writeEnv };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { orCancel } from "./_setup.js";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import { S_BAR, S_RADIO_INACTIVE, S_STEP_ERROR, S_STEP_SUBMIT, isCI, isTTY, unicode } from "@clack/prompts";
|
|
4
|
+
import { styleText } from "node:util";
|
|
5
|
+
|
|
6
|
+
//#region src/cli/commands/_tasks.ts
|
|
7
|
+
const FINAL_CONFIRMATION_TEXT = "Continue with these choices?";
|
|
8
|
+
/**
|
|
9
|
+
* A failed checklist step. Throwing this from a step's `run` renders the step with an error mark (never a
|
|
10
|
+
* checkmark) and prints its optional `detail` (captured command output, a stack) below the block. `fatal`
|
|
11
|
+
* (the default) also stops the run — no later step executes. A step that failed but the setup can recover
|
|
12
|
+
* from (an install or git init that didn't complete) throws with `fatal: false`: it still shows the error
|
|
13
|
+
* mark, but the run carries on to the remaining steps.
|
|
14
|
+
*/
|
|
15
|
+
var StepError = class extends Error {
|
|
16
|
+
detail;
|
|
17
|
+
fatal;
|
|
18
|
+
constructor(message, opts = {}) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "StepError";
|
|
21
|
+
this.detail = opts.detail;
|
|
22
|
+
this.fatal = opts.fatal ?? true;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/** The animated in-progress frames and cadence, matching clack's own spinner. */
|
|
26
|
+
const FRAMES = unicode ? [
|
|
27
|
+
"◒",
|
|
28
|
+
"◐",
|
|
29
|
+
"◓",
|
|
30
|
+
"◑"
|
|
31
|
+
] : [
|
|
32
|
+
"•",
|
|
33
|
+
"o",
|
|
34
|
+
"O",
|
|
35
|
+
"0"
|
|
36
|
+
];
|
|
37
|
+
const FRAME_MS = unicode ? 80 : 120;
|
|
38
|
+
/** A checkmark for completed steps, in place of clack's `◇`; falls back to clack's `o` where unicode isn't supported. */
|
|
39
|
+
const S_DONE = unicode ? "✓" : S_STEP_SUBMIT;
|
|
40
|
+
/** One checklist line: a state marker, two spaces, then the label (truncated to keep it a single row). */
|
|
41
|
+
function renderLine(state, label, frame, columns) {
|
|
42
|
+
const budget = Math.max(1, columns - 3);
|
|
43
|
+
const text = label.length > budget ? `${label.slice(0, budget - 1)}…` : label;
|
|
44
|
+
switch (state) {
|
|
45
|
+
case "done": return `${styleText("green", S_DONE)} ${text}`;
|
|
46
|
+
case "error": return `${styleText("red", S_STEP_ERROR)} ${text}`;
|
|
47
|
+
case "active": return `${styleText("magenta", frame)} ${text}`;
|
|
48
|
+
default: return styleText("dim", `${S_RADIO_INACTIVE} ${text}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Run the enabled steps as a single live checklist. The whole list is drawn up front — every step visible
|
|
53
|
+
* at once — and repainted in place as work moves down it: a pending marker becomes an animated loader
|
|
54
|
+
* while a step runs, then a checkmark with the step's completion message. A step that throws is fatal: it
|
|
55
|
+
* gets an error mark, the rest stay pending, its {@link StepError} detail is printed below, and the run
|
|
56
|
+
* stops. Returns whether it aborted, so the caller can exit without printing a "ready" outro over a
|
|
57
|
+
* failed setup. Falls back to committing one line per step (no animation) when stdout isn't an
|
|
58
|
+
* interactive TTY, so piped or CI output stays readable.
|
|
59
|
+
*/
|
|
60
|
+
async function runChecklist(input) {
|
|
61
|
+
const steps = input.filter((step) => step.enabled !== false).map((step) => ({
|
|
62
|
+
step,
|
|
63
|
+
state: "pending",
|
|
64
|
+
label: step.title
|
|
65
|
+
}));
|
|
66
|
+
if (steps.length === 0) return false;
|
|
67
|
+
const out = process.stdout;
|
|
68
|
+
out.write(`${styleText("gray", S_BAR)}\n`);
|
|
69
|
+
if (!isTTY(out) || isCI()) {
|
|
70
|
+
const details$1 = [];
|
|
71
|
+
let aborted$1 = false;
|
|
72
|
+
for (const item of steps) try {
|
|
73
|
+
const done = await item.step.run(() => {});
|
|
74
|
+
out.write(`${styleText("green", S_DONE)} ${done.length > 0 ? done : item.step.title}\n`);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
out.write(`${styleText("red", S_STEP_ERROR)} ${error instanceof Error ? error.message : String(error)}\n`);
|
|
77
|
+
if (error instanceof StepError && error.detail) details$1.push(error.detail);
|
|
78
|
+
if (!(error instanceof StepError) || error.fatal) {
|
|
79
|
+
aborted$1 = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const detail of details$1) p.log.error(detail);
|
|
84
|
+
return aborted$1;
|
|
85
|
+
}
|
|
86
|
+
const columns = out.columns || 80;
|
|
87
|
+
let frame = 0;
|
|
88
|
+
const draw = (first) => {
|
|
89
|
+
if (!first) {
|
|
90
|
+
out.moveCursor(0, -steps.length);
|
|
91
|
+
out.cursorTo(0);
|
|
92
|
+
out.clearScreenDown();
|
|
93
|
+
}
|
|
94
|
+
out.write(`${steps.map((item) => renderLine(item.state, item.label, FRAMES[frame % FRAMES.length] ?? "", columns)).join("\n")}\n`);
|
|
95
|
+
};
|
|
96
|
+
out.write("\x1B[?25l");
|
|
97
|
+
draw(true);
|
|
98
|
+
const timer = setInterval(() => {
|
|
99
|
+
frame++;
|
|
100
|
+
draw(false);
|
|
101
|
+
}, FRAME_MS);
|
|
102
|
+
const details = [];
|
|
103
|
+
let aborted = false;
|
|
104
|
+
try {
|
|
105
|
+
for (const item of steps) {
|
|
106
|
+
item.state = "active";
|
|
107
|
+
draw(false);
|
|
108
|
+
try {
|
|
109
|
+
const done = await item.step.run((text) => {
|
|
110
|
+
item.label = text;
|
|
111
|
+
});
|
|
112
|
+
item.label = done.length > 0 ? done : item.step.title;
|
|
113
|
+
item.state = "done";
|
|
114
|
+
draw(false);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
item.label = error instanceof Error ? error.message : String(error);
|
|
117
|
+
item.state = "error";
|
|
118
|
+
draw(false);
|
|
119
|
+
if (error instanceof StepError && error.detail) details.push(error.detail);
|
|
120
|
+
if (!(error instanceof StepError) || error.fatal) {
|
|
121
|
+
aborted = true;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
clearInterval(timer);
|
|
128
|
+
out.write("\x1B[?25h");
|
|
129
|
+
}
|
|
130
|
+
for (const detail of details) p.log.error(detail);
|
|
131
|
+
return aborted;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The "here's everything you chose" note shown after collecting inputs and before running any step —
|
|
135
|
+
* the heart of the lazy flow: nothing has happened yet, and the user reviews the whole plan at once.
|
|
136
|
+
* Labels are padded to a common width so the values line up in a column.
|
|
137
|
+
*/
|
|
138
|
+
function summaryNote(rows, title = "Review your choices") {
|
|
139
|
+
const width = Math.max(...rows.map(([label]) => label.length));
|
|
140
|
+
const body = rows.map(([label, value]) => `${`${label}:`.padEnd(width + 1)} ${value}`).join("\n");
|
|
141
|
+
p.note(body, title);
|
|
142
|
+
}
|
|
143
|
+
/** Ask the single go/no-go question that gates the execute phase. Cancelling exits cleanly. */
|
|
144
|
+
async function confirmProceed(message) {
|
|
145
|
+
return orCancel(await p.confirm({
|
|
146
|
+
message,
|
|
147
|
+
initialValue: true
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
export { FINAL_CONFIRMATION_TEXT, StepError, confirmProceed, runChecklist, summaryNote };
|
|
@@ -22,21 +22,22 @@ export default defineConfig({
|
|
|
22
22
|
`;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* Build the {@link ScaffoldContext} that adapts a template's
|
|
26
|
-
* project. `dirRel` is the Kizlo home directory, `
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
25
|
+
* Build the {@link ScaffoldContext} that adapts a template's config (paths and imports) to a real
|
|
26
|
+
* project. `dirRel` is the Kizlo home directory, `hasSrcDir` whether the project keeps source under
|
|
27
|
+
* `src/` (decides the `src/` normalization of template paths), and `alias` the import-alias prefix (empty
|
|
28
|
+
* for relative imports). Imports are resolved per calling file so each scaffolded file references its
|
|
29
|
+
* targets through the right specifier — `importFrom` for any project path (used to retarget every
|
|
30
|
+
* template-alias import), `serverImport` the server-entry shorthand.
|
|
30
31
|
*/
|
|
31
|
-
function buildScaffoldContext(cwd, { dirRel,
|
|
32
|
+
function buildScaffoldContext(cwd, { dirRel, hasSrcDir, alias, clientUrl }) {
|
|
32
33
|
const serverDirRel = path.join(dirRel, "server");
|
|
33
34
|
const importFrom = (targetRel, fromDir) => resolveModuleImport(cwd, targetRel, fromDir, alias);
|
|
34
35
|
return {
|
|
35
|
-
|
|
36
|
+
kizloPath: dirRel,
|
|
36
37
|
serverDirName: path.basename(serverDirRel),
|
|
37
38
|
serverEntryPath: path.join(serverDirRel, "index.ts"),
|
|
38
39
|
clientPath: path.join(dirRel, "client.ts"),
|
|
39
|
-
|
|
40
|
+
hasSrcDir,
|
|
40
41
|
serverImport: (fromDir) => importFrom(serverDirRel, fromDir),
|
|
41
42
|
importFrom,
|
|
42
43
|
clientUrl
|
|
@@ -86,10 +87,10 @@ function writeGeneratedContract(cwd, serverDirRel) {
|
|
|
86
87
|
* written to a guessed-at file. We never scan the tree to find a stand-in. A confident apply is an
|
|
87
88
|
* idempotent upsert: it replaces our exports if present, adds them if not.
|
|
88
89
|
*/
|
|
89
|
-
function applyProjectPatches(cwd, patches,
|
|
90
|
+
function applyProjectPatches(cwd, patches, config, scaffold) {
|
|
90
91
|
const manualSteps = [];
|
|
91
92
|
for (const entry of patches) {
|
|
92
|
-
const resolved = resolvePatch(entry,
|
|
93
|
+
const resolved = resolvePatch(entry, config, scaffold);
|
|
93
94
|
if (resolved.mode === "note") {
|
|
94
95
|
manualSteps.push(resolved);
|
|
95
96
|
p.log.info(`Update your ${resolved.label} to finish wiring Kizlo — see the change to make below`);
|