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.
Files changed (34) hide show
  1. package/dist/cli/commands/_setup.js +91 -55
  2. package/dist/cli/commands/_tasks.js +152 -0
  3. package/dist/cli/commands/_wiring.js +11 -10
  4. package/dist/cli/commands/create.js +270 -87
  5. package/dist/cli/commands/init.js +122 -61
  6. package/dist/cli/daemon/config.js +1 -1
  7. package/dist/cli/presets/source.js +13 -8
  8. package/dist/cli/presets/template.js +255 -50
  9. package/dist/cli/utils.js +149 -80
  10. package/dist/integrations/tanstack-start/api.d.ts +15 -0
  11. package/dist/integrations/tanstack-start/api.js +21 -0
  12. package/dist/integrations/tanstack-start/kizlo.client.d.ts +20 -0
  13. package/dist/integrations/tanstack-start/kizlo.client.js +18 -0
  14. package/dist/integrations/tanstack-start/kizlo.server.d.ts +14 -0
  15. package/dist/integrations/tanstack-start/kizlo.server.js +15 -0
  16. package/dist/integrations/tanstack-start/manifest.d.ts +9 -0
  17. package/dist/integrations/tanstack-start/manifest.js +11 -0
  18. package/dist/integrations/tanstack-start/metadata.d.ts +49 -0
  19. package/dist/integrations/tanstack-start/metadata.js +112 -0
  20. package/dist/integrations/tanstack-start/robots.d.ts +8 -0
  21. package/dist/integrations/tanstack-start/robots.js +11 -0
  22. package/dist/integrations/tanstack-start/root-head.d.ts +15 -0
  23. package/dist/integrations/tanstack-start/root-head.js +54 -0
  24. package/dist/integrations/tanstack-start/sitemap.d.ts +16 -0
  25. package/dist/integrations/tanstack-start/sitemap.js +20 -0
  26. package/dist/integrations/tanstack-start/utils.d.ts +20 -0
  27. package/dist/integrations/tanstack-start/utils.js +17 -0
  28. package/dist/package.js +1 -1
  29. package/dist/router.d.ts +5 -5
  30. package/dist/tanstack-start/server.d.ts +11 -0
  31. package/dist/tanstack-start/server.js +12 -0
  32. package/dist/tanstack-start.d.ts +4 -0
  33. package/dist/tanstack-start.js +5 -0
  34. 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, then report the
333
- * outcome. Mutates `conn` in place (wpUrl / wpUsername / wpPassword). Writing the `local` flags into
334
- * `kizlo.config.ts` is the caller's job (the generated config for `create`/`init`). Exits on failure.
335
- * No-op for a remote connection.
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 setupLocalWordPress(cwd, conn) {
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 s = p.spinner();
341
- s.start("Setting up local WordPress (first run downloads images, this can take a while)");
342
- try {
343
- const local = await provisionLocalStack(devConfigFor(cwd), conn.siteSecret, conn.baseUrl);
344
- conn.wpUrl = local.url;
345
- conn.wpUsername = local.username;
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
- * Write (or update) `.env` and `.env.example` for the managed keys and report the outcome.
378
- * Existing conflicting `.env` values are preserved unless `force`; interactively the user is asked,
379
- * and under `--yes` they are kept. `.env.example` is only written when absent, and is sourced from the
380
- * template's own file (`opts.exampleTemplate`) so it matches the `.env` structure, falling back to a
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
- async function writeEnv(cwd, envKeys, conn, opts) {
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 existingEnv = envExisted ? fs.readFileSync(envPath, "utf8") : "";
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
- if (!envExisted) p.log.success("Created .env");
405
- else if (merge.updated.length || merge.added.length) p.log.success("Updated .env");
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
- * webhook signing and event delivery work. No-op for local (handled during provisioning) or when
412
- * credentials are incomplete. Warns and continues on failure.
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
- 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.`);
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, setupLocalWordPress, syncRemote, validate, withApiPath, writeEnv };
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 conventions (paths and imports) to a real
26
- * project. `dirRel` is the Kizlo home directory, `appDir` the App Router directory, and `alias` the
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.
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, appDir, alias, clientUrl }) {
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
- kizloDir: dirRel,
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
- appDir,
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, conventions, scaffold) {
90
+ function applyProjectPatches(cwd, patches, config, scaffold) {
90
91
  const manualSteps = [];
91
92
  for (const entry of patches) {
92
- const resolved = resolvePatch(entry, conventions, scaffold);
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`);