@waniwani/kit 0.1.6 → 0.1.7

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 (44) hide show
  1. package/README.md +39 -39
  2. package/dist/cli/codegen.js +1105 -0
  3. package/dist/cli/codegen.js.map +1 -0
  4. package/{cli/env.mjs → dist/cli/env.js} +5 -6
  5. package/dist/cli/env.js.map +1 -0
  6. package/{cli/framework.mjs → dist/cli/framework.js} +112 -115
  7. package/dist/cli/framework.js.map +1 -0
  8. package/dist/cli/index.js +378 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/{cli/init.mjs → dist/cli/init.js} +218 -259
  11. package/dist/cli/init.js.map +1 -0
  12. package/dist/cli/log.js +156 -0
  13. package/dist/cli/log.js.map +1 -0
  14. package/dist/cli/manifest.js +57 -0
  15. package/dist/cli/manifest.js.map +1 -0
  16. package/{cli/peers.mjs → dist/cli/peers.js} +77 -88
  17. package/dist/cli/peers.js.map +1 -0
  18. package/dist/cli/scan.js +100 -0
  19. package/dist/cli/scan.js.map +1 -0
  20. package/dist/cli/template.js +173 -0
  21. package/dist/cli/template.js.map +1 -0
  22. package/dist/cli/types.js +14 -0
  23. package/dist/cli/types.js.map +1 -0
  24. package/dist/cli/validate.js +328 -0
  25. package/dist/cli/validate.js.map +1 -0
  26. package/dist/cli/vercel.js +103 -0
  27. package/dist/cli/vercel.js.map +1 -0
  28. package/dist/server.d.ts +1 -1
  29. package/dist/server.d.ts.map +1 -1
  30. package/dist/server.js +0 -1
  31. package/dist/server.js.map +1 -1
  32. package/dist/web.d.ts +8 -7
  33. package/dist/web.d.ts.map +1 -1
  34. package/dist/web.js +7 -6
  35. package/dist/web.js.map +1 -1
  36. package/package.json +13 -9
  37. package/src/server.ts +7 -9
  38. package/src/web.tsx +12 -13
  39. package/cli/codegen.mjs +0 -1267
  40. package/cli/index.mjs +0 -409
  41. package/cli/log.mjs +0 -178
  42. package/cli/scan.mjs +0 -112
  43. package/cli/template.mjs +0 -190
  44. package/cli/validate.mjs +0 -391
@@ -26,46 +26,38 @@
26
26
  * alone. Only the app's own source files count as a collision, and those stop
27
27
  * the command until `--force` says otherwise.
28
28
  */
29
-
30
29
  import { spawn } from "node:child_process";
31
30
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
32
31
  import { basename, dirname, join, relative } from "node:path";
33
32
  import { createInterface } from "node:readline/promises";
34
- import { fileURLToPath } from "node:url";
35
- import { bold, dim, green, red, yellow } from "./log.mjs";
36
- import { installable } from "./peers.mjs";
37
-
38
- const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
39
- const MANIFEST = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"));
40
-
33
+ import { bold, dim, green, red, yellow } from "./log.js";
34
+ import { PACKAGE_VERSION } from "./manifest.js";
35
+ import { installable } from "./peers.js";
41
36
  /** A directory name as an MCP server name: `My App` becomes `my-app`. */
42
37
  function slugify(input) {
43
- const slug = input
44
- .trim()
45
- .toLowerCase()
46
- .replace(/[^a-z0-9]+/g, "-")
47
- .replace(/^-+|-+$/g, "");
48
- return slug || "my-app";
38
+ const slug = input
39
+ .trim()
40
+ .toLowerCase()
41
+ .replace(/[^a-z0-9]+/g, "-")
42
+ .replace(/^-+|-+$/g, "");
43
+ return slug || "my-app";
49
44
  }
50
-
51
45
  /** `my-app` becomes `My app`, for the human-facing title. */
52
46
  function titleize(slug) {
53
- const words = slug.replace(/[-_]+/g, " ");
54
- return words.charAt(0).toUpperCase() + words.slice(1);
47
+ const words = slug.replace(/[-_]+/g, " ");
48
+ return words.charAt(0).toUpperCase() + words.slice(1);
55
49
  }
56
-
57
50
  /**
58
51
  * A title, made safe to drop into the generated TypeScript and Markdown.
59
52
  * Backticks, quotes, backslashes and `$` are the characters that would end a
60
53
  * string or a template literal early, and a product name needs none of them.
61
54
  */
62
55
  function cleanTitle(input) {
63
- return input
64
- .replace(/[`"'\\$]/g, "")
65
- .replace(/\s+/g, " ")
66
- .trim();
56
+ return input
57
+ .replace(/[`"'\\$]/g, "")
58
+ .replace(/\s+/g, " ")
59
+ .trim();
67
60
  }
68
-
69
61
  /**
70
62
  * What a new app depends on.
71
63
  *
@@ -82,45 +74,37 @@ function cleanTitle(input) {
82
74
  * than arriving because something else asked for it.
83
75
  */
84
76
  function dependencies() {
85
- return {
86
- "@waniwani/kit": `^${MANIFEST.version}`,
87
- "@waniwani/sdk": installable("@waniwani/sdk"),
88
- react: installable("react"),
89
- "react-dom": installable("react-dom"),
90
- zod: installable("zod"),
91
- };
77
+ return {
78
+ "@waniwani/kit": `^${PACKAGE_VERSION}`,
79
+ "@waniwani/sdk": installable("@waniwani/sdk"),
80
+ react: installable("react"),
81
+ "react-dom": installable("react-dom"),
82
+ zod: installable("zod"),
83
+ };
92
84
  }
93
-
94
85
  // ------------------------------------------------------------ scaffold content
95
-
96
86
  /**
97
87
  * The tool name and the widget name the scaffold uses. They appear in five
98
88
  * files, including inside prose the model reads, so they are named once.
99
89
  */
100
90
  const TOOL = "search-products";
101
91
  const WIDGET = "product-list";
102
-
103
92
  function packageJson(app) {
104
- return `${JSON.stringify(
105
- {
106
- name: app.name,
107
- private: true,
108
- type: "module",
109
- scripts: {
110
- check: "waniwani check",
111
- dev: "waniwani dev",
112
- build: "waniwani build",
113
- start: "waniwani start",
114
- },
115
- dependencies: dependencies(),
116
- },
117
- null,
118
- 2,
119
- )}\n`;
93
+ return `${JSON.stringify({
94
+ name: app.name,
95
+ private: true,
96
+ type: "module",
97
+ scripts: {
98
+ check: "waniwani check",
99
+ dev: "waniwani dev",
100
+ build: "waniwani build",
101
+ start: "waniwani start",
102
+ },
103
+ dependencies: dependencies(),
104
+ }, null, 2)}\n`;
120
105
  }
121
-
122
106
  function appConfig(app) {
123
- return `import { defineApp } from "@waniwani/kit";
107
+ return `import { defineApp } from "@waniwani/kit";
124
108
 
125
109
  export default defineApp({
126
110
  // The MCP server name. Hosts show \`title\` to humans and use this one as the id.
@@ -129,9 +113,8 @@ export default defineApp({
129
113
  });
130
114
  `;
131
115
  }
132
-
133
116
  function tool() {
134
- return `import { defineTool } from "@waniwani/kit";
117
+ return `import { defineTool } from "@waniwani/kit";
135
118
  import { z } from "zod";
136
119
 
137
120
  /**
@@ -183,9 +166,8 @@ export default defineTool({
183
166
  });
184
167
  `;
185
168
  }
186
-
187
169
  function widgetContract() {
188
- return `import { defineWidget } from "@waniwani/kit";
170
+ return `import { defineWidget } from "@waniwani/kit";
189
171
  import { z } from "zod";
190
172
 
191
173
  const product = z.object({
@@ -222,9 +204,8 @@ Wait for the shopper to pick one, then answer about that product.\`,
222
204
  });
223
205
  `;
224
206
  }
225
-
226
207
  function widgetUi() {
227
- return `import { useLayout, useSendFollowUpMessage, useWidget } from "@waniwani/kit/web";
208
+ return `import { useLayout, useSendFollowUpMessage, useWidget } from "@waniwani/kit/web";
228
209
  import widget from "./widget.js";
229
210
 
230
211
  const euros = (value: number) =>
@@ -277,26 +258,23 @@ export default function ProductList() {
277
258
  }
278
259
  `;
279
260
  }
280
-
281
261
  function envExample() {
282
- return `# Optional. Without it the app still runs: flows use MemoryKvStore and
262
+ return `# Optional. Without it the app still runs: flows use MemoryKvStore and
283
263
  # withWaniwani degrades to a no-op. With it, flow state is hosted and tracking
284
264
  # reaches app.waniwani.ai.
285
265
  WANIWANI_API_KEY=
286
266
  WANIWANI_PUBLIC_KEY=
287
267
  `;
288
268
  }
289
-
290
269
  function gitignore() {
291
- return `node_modules/
270
+ return `node_modules/
292
271
  .waniwani/
293
272
  .env
294
273
  .env.local
295
274
  `;
296
275
  }
297
-
298
276
  function readme(app) {
299
- return `# ${app.title}
277
+ return `# ${app.title}
300
278
 
301
279
  An MCP app built with [@waniwani/kit](https://www.npmjs.com/package/@waniwani/kit).
302
280
  You own the folders below. The server, the transport, the bundling and the deploy
@@ -322,7 +300,6 @@ npm run start # run the production build
322
300
  and it stays out of git.
323
301
  `;
324
302
  }
325
-
326
303
  /**
327
304
  * The files a new app gets.
328
305
  *
@@ -331,27 +308,25 @@ and it stays out of git.
331
308
  * scaffold's contribution into what is there, and `keep` leaves it untouched.
332
309
  */
333
310
  function scaffold(app, { minimal }) {
334
- const files = [
335
- { path: "package.json", contents: packageJson(app), whenPresent: "merge", merge: mergePackageJson },
336
- { path: ".gitignore", contents: gitignore(), whenPresent: "merge", merge: mergeGitignore },
337
- { path: ".env.example", contents: envExample(), whenPresent: "keep" },
338
- { path: "README.md", contents: readme(app), whenPresent: "keep" },
339
- { path: "waniwani.config.ts", contents: appConfig(app) },
340
- { path: `tools/${TOOL}.ts`, contents: tool() },
341
- ];
342
-
343
- if (!minimal) {
344
- files.push(
345
- { path: `widgets/${WIDGET}/widget.ts`, contents: widgetContract() },
346
- { path: `widgets/${WIDGET}/ui.tsx`, contents: widgetUi() },
347
- );
348
- }
349
-
350
- return files;
311
+ const files = [
312
+ {
313
+ path: "package.json",
314
+ contents: packageJson(app),
315
+ whenPresent: "merge",
316
+ merge: mergePackageJson,
317
+ },
318
+ { path: ".gitignore", contents: gitignore(), whenPresent: "merge", merge: mergeGitignore },
319
+ { path: ".env.example", contents: envExample(), whenPresent: "keep" },
320
+ { path: "README.md", contents: readme(app), whenPresent: "keep" },
321
+ { path: "waniwani.config.ts", contents: appConfig(app) },
322
+ { path: `tools/${TOOL}.ts`, contents: tool() },
323
+ ];
324
+ if (!minimal) {
325
+ files.push({ path: `widgets/${WIDGET}/widget.ts`, contents: widgetContract() }, { path: `widgets/${WIDGET}/ui.tsx`, contents: widgetUi() });
326
+ }
327
+ return files;
351
328
  }
352
-
353
329
  // -------------------------------------------------------------------- merging
354
-
355
330
  /**
356
331
  * Add the scripts and dependencies an app needs to a manifest that is already
357
332
  * there, and touch nothing else. A key the repo already declares is the repo's
@@ -360,36 +335,34 @@ function scaffold(app, { minimal }) {
360
335
  * @returns the keys added, for the CLI to report
361
336
  */
362
337
  function mergePackageJson(file, contents) {
363
- const existing = JSON.parse(readFileSync(file, "utf-8"));
364
- const generated = JSON.parse(contents);
365
- const added = [];
366
-
367
- const fold = (section) => {
368
- const merged = { ...existing[section] };
369
- for (const [key, value] of Object.entries(generated[section])) {
370
- if (merged[key]) continue;
371
- merged[key] = value;
372
- added.push(`${section}.${key}`);
373
- }
374
- return merged;
375
- };
376
-
377
- const next = {
378
- ...existing,
379
- // App modules are ESM. A repo that says commonjs is warned about instead of
380
- // rewritten, since flipping it changes how the rest of that repo loads.
381
- type: existing.type ?? "module",
382
- scripts: fold("scripts"),
383
- dependencies: fold("dependencies"),
384
- };
385
- if (!existing.type) added.push("type");
386
-
387
- if (added.length > 0) {
388
- writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`);
389
- }
390
- return added;
338
+ const existing = JSON.parse(readFileSync(file, "utf-8"));
339
+ const generated = JSON.parse(contents);
340
+ const added = [];
341
+ const fold = (section) => {
342
+ const merged = { ...existing[section] };
343
+ for (const [key, value] of Object.entries(generated[section] ?? {})) {
344
+ if (merged[key])
345
+ continue;
346
+ merged[key] = value;
347
+ added.push(`${section}.${key}`);
348
+ }
349
+ return merged;
350
+ };
351
+ const next = {
352
+ ...existing,
353
+ // App modules are ESM. A repo that says commonjs is warned about instead of
354
+ // rewritten, since flipping it changes how the rest of that repo loads.
355
+ type: existing.type ?? "module",
356
+ scripts: fold("scripts"),
357
+ dependencies: fold("dependencies"),
358
+ };
359
+ if (!existing.type)
360
+ added.push("type");
361
+ if (added.length > 0) {
362
+ writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`);
363
+ }
364
+ return added;
391
365
  }
392
-
393
366
  /**
394
367
  * Add the lines the app does not ignore yet, leaving every line it wrote alone.
395
368
  * A trailing slash is not part of the comparison, so a repo ignoring
@@ -398,38 +371,34 @@ function mergePackageJson(file, contents) {
398
371
  * @returns the lines added, for the CLI to report
399
372
  */
400
373
  function mergeGitignore(file, contents) {
401
- const existing = readFileSync(file, "utf-8");
402
- const bare = (line) => line.trim().replace(/\/$/, "");
403
- const known = new Set(existing.split("\n").map(bare));
404
-
405
- const additions = contents
406
- .split("\n")
407
- .filter((line) => line.trim() && !line.trim().startsWith("#") && !known.has(bare(line)));
408
-
409
- if (additions.length === 0) return [];
410
- const prefix = !existing || existing.endsWith("\n") ? "" : "\n";
411
- writeFileSync(file, `${existing}${prefix}${additions.join("\n")}\n`);
412
- return additions;
374
+ const existing = readFileSync(file, "utf-8");
375
+ const bare = (line) => line.trim().replace(/\/$/, "");
376
+ const known = new Set(existing.split("\n").map(bare));
377
+ const additions = contents
378
+ .split("\n")
379
+ .filter((line) => line.trim() && !line.trim().startsWith("#") && !known.has(bare(line)));
380
+ if (additions.length === 0)
381
+ return [];
382
+ const prefix = !existing || existing.endsWith("\n") ? "" : "\n";
383
+ writeFileSync(file, `${existing}${prefix}${additions.join("\n")}\n`);
384
+ return additions;
413
385
  }
414
-
415
386
  // ------------------------------------------------------------------- the shell
416
-
417
387
  function write(file, contents) {
418
- mkdirSync(dirname(file), { recursive: true });
419
- writeFileSync(file, contents);
388
+ mkdirSync(dirname(file), { recursive: true });
389
+ writeFileSync(file, contents);
420
390
  }
421
-
422
391
  /** One question, with the default in parentheses and Enter taking it. */
423
392
  async function ask(question, fallback) {
424
- const rl = createInterface({ input: process.stdin, output: process.stdout });
425
- try {
426
- const answer = await rl.question(`${question} ${dim(`(${fallback})`)} `);
427
- return answer.trim() || fallback;
428
- } finally {
429
- rl.close();
430
- }
393
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
394
+ try {
395
+ const answer = await rl.question(`${question} ${dim(`(${fallback})`)} `);
396
+ return answer.trim() || fallback;
397
+ }
398
+ finally {
399
+ rl.close();
400
+ }
431
401
  }
432
-
433
402
  /**
434
403
  * The package manager that invoked this command, which npm, pnpm, yarn and bun
435
404
  * all announce in `npm_config_user_agent`. Nothing to detect from lockfiles: a
@@ -437,27 +406,30 @@ async function ask(question, fallback) {
437
406
  * typed with one of the four.
438
407
  */
439
408
  function packageManager() {
440
- const agent = process.env.npm_config_user_agent ?? "";
441
- return ["bun", "pnpm", "yarn", "npm"].find((name) => agent.startsWith(name)) ?? "npm";
409
+ const agent = process.env.npm_config_user_agent ?? "";
410
+ const names = ["bun", "pnpm", "yarn", "npm"];
411
+ return names.find((name) => agent.startsWith(name)) ?? "npm";
442
412
  }
443
-
444
413
  /** How each manager runs a binary out of node_modules, for the closing lines. */
445
- const RUNNERS = { npm: "npx", pnpm: "pnpm", yarn: "yarn", bun: "bunx" };
446
-
414
+ const RUNNERS = {
415
+ npm: "npx",
416
+ pnpm: "pnpm",
417
+ yarn: "yarn",
418
+ bun: "bunx",
419
+ };
447
420
  function install(root, manager) {
448
- console.log(`\n${dim(`installing with ${manager}…`)}`);
449
- return new Promise((resolvePromise) => {
450
- const child = spawn(manager, ["install"], {
451
- cwd: root,
452
- stdio: "inherit",
453
- // npm and yarn are .cmd shims on Windows, which execvp cannot run.
454
- shell: process.platform === "win32",
455
- });
456
- child.on("close", (code) => resolvePromise(code === 0));
457
- child.on("error", () => resolvePromise(false));
458
- });
421
+ console.log(`\n${dim(`installing with ${manager}…`)}`);
422
+ return new Promise((resolvePromise) => {
423
+ const child = spawn(manager, ["install"], {
424
+ cwd: root,
425
+ stdio: "inherit",
426
+ // npm and yarn are .cmd shims on Windows, which execvp cannot run.
427
+ shell: process.platform === "win32",
428
+ });
429
+ child.on("close", (code) => resolvePromise(code === 0));
430
+ child.on("error", () => resolvePromise(false));
431
+ });
459
432
  }
460
-
461
433
  /**
462
434
  * Scaffold an app folder, install its dependencies, and say what to run.
463
435
  *
@@ -467,109 +439,96 @@ function install(root, manager) {
467
439
  * @returns a process exit code
468
440
  */
469
441
  export async function init(appRoot, flags, { targeted = true } = {}) {
470
- const interactive = process.stdin.isTTY && !flags.yes && !flags.name;
471
- const suggested = slugify(basename(appRoot));
472
-
473
- // One question, and both fields come out of the answer: `Acme Shop` gives the
474
- // server the name `acme-shop` and keeps `Acme Shop` as the title. An answer
475
- // that is already a slug gets a title with a capital on the front.
476
- const answer = flags.name ?? (interactive ? await ask("App name", suggested) : suggested);
477
- const name = slugify(answer);
478
- const typed = cleanTitle(answer);
479
- const app = { name, title: typed && typed !== name ? typed : titleize(name) };
480
-
481
- // A name typed at the prompt with no directory to put it in names the
482
- // directory as well: `oney` in ~/Projects means ~/Projects/oney, which is how
483
- // create-next-app's one question reads. Taking the offered default leaves
484
- // everything where it is, since that default is the current folder's own name,
485
- // and `waniwani init .` names the current folder outright.
486
- const root = !targeted && interactive && name !== suggested ? join(appRoot, name) : appRoot;
487
-
488
- const files = scaffold(app, { minimal: Boolean(flags.minimal) });
489
-
490
- // Nothing is written until every collision is known, so a refusal leaves the
491
- // directory exactly as it was.
492
- const clashes = files.filter((file) => !file.whenPresent && existsSync(join(root, file.path)));
493
- if (clashes.length > 0 && !flags.force) {
494
- console.error(`\n${red("✗")} ${bold("already an app folder here")}\n`);
495
- for (const file of clashes) {
496
- console.error(` ${file.path}`);
497
- }
498
- console.error(`\n${dim("pass --force to overwrite, or init into a new directory")}`);
499
- return 1;
500
- }
501
-
502
- mkdirSync(root, { recursive: true });
503
-
504
- const actions = [];
505
- for (const file of files) {
506
- const target = join(root, file.path);
507
-
508
- if (!existsSync(target)) {
509
- write(target, file.contents);
510
- actions.push([green("+"), file.path, null]);
511
- continue;
512
- }
513
- if (file.whenPresent === "keep") {
514
- actions.push([dim("·"), file.path, "yours, left alone"]);
515
- continue;
516
- }
517
- if (file.whenPresent === "merge") {
518
- const changed = file.merge(target, file.contents);
519
- actions.push([
520
- changed.length > 0 ? yellow("~") : dim("·"),
521
- file.path,
522
- changed.length > 0 ? `+ ${changed.join(", ")}` : "nothing to add",
523
- ]);
524
- continue;
525
- }
526
- // A collision the caller chose to overwrite.
527
- write(target, file.contents);
528
- actions.push([yellow("~"), file.path, "overwritten"]);
529
- }
530
-
531
- console.log(`\n${green("✓")} ${bold(app.name)} ${dim(`→ ${root}`)}\n`);
532
- for (const [marker, path, note] of actions) {
533
- console.log(` ${marker} ${path}${note ? ` ${dim(note)}` : ""}`);
534
- }
535
-
536
- // The merge leaves a script the repo already had alone, so the way into the dev
537
- // loop is whatever survived that: `npm run dev` when it is ours, the CLI by
538
- // name when the repo's own `dev` runs something else.
539
- const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
540
- const ours = manifest.scripts?.dev === "waniwani dev";
541
-
542
- if (manifest.type !== "module") {
543
- console.log(
544
- `\n${yellow("!")} ${bold("package.json")} says ${bold(`"type": "${manifest.type}"`)}`,
545
- );
546
- console.log(` ${dim('app modules are ESM: set it to "module" or the build cannot load them')}`);
547
- }
548
-
549
- const manager = packageManager();
550
- // A scaffold with no node_modules still checks out and still reads, so a
551
- // failed install is reported and the folder is kept.
552
- const installed = flags.install === false ? null : await install(root, manager);
553
-
554
- console.log(`\n${bold("From here")}`);
555
- // `init apps/store` is scaffolded two levels down, so the path is the one to
556
- // print rather than the directory's own name.
557
- const from = relative(process.cwd(), root);
558
- if (from) {
559
- console.log(` cd ${from}`);
560
- }
561
- if (installed === false) {
562
- console.log(` ${yellow(`${manager} install`)} ${dim("(the first attempt failed)")}`);
563
- } else if (installed === null) {
564
- console.log(` ${manager} install`);
565
- }
566
- console.log(` ${ours ? `${manager} run dev` : `${RUNNERS[manager]} waniwani dev`}`);
567
-
568
- console.log(`\n${bold("Then")}`);
569
- console.log(` ${dim("·")} edit ${bold(`tools/${TOOL}.ts`)} to answer with your own data`);
570
- if (!flags.minimal) {
571
- console.log(` ${dim("·")} edit ${bold(`widgets/${WIDGET}/ui.tsx`)} for how it looks on screen`);
572
- }
573
- console.log(` ${dim("·")} add ${bold("flows/<name>.ts")} for a multi-step conversation`);
574
- return 0;
442
+ const interactive = process.stdin.isTTY && !flags.yes && !flags.name;
443
+ const suggested = slugify(basename(appRoot));
444
+ // One question, and both fields come out of the answer: `Acme Shop` gives the
445
+ // server the name `acme-shop` and keeps `Acme Shop` as the title. An answer
446
+ // that is already a slug gets a title with a capital on the front.
447
+ const answer = flags.name ?? (interactive ? await ask("App name", suggested) : suggested);
448
+ const name = slugify(answer);
449
+ const typed = cleanTitle(answer);
450
+ const app = { name, title: typed && typed !== name ? typed : titleize(name) };
451
+ // A name typed at the prompt with no directory to put it in names the
452
+ // directory as well: `oney` in ~/Projects means ~/Projects/oney, which is how
453
+ // create-next-app's one question reads. Taking the offered default leaves
454
+ // everything where it is, since that default is the current folder's own name,
455
+ // and `waniwani init .` names the current folder outright.
456
+ const root = !targeted && interactive && name !== suggested ? join(appRoot, name) : appRoot;
457
+ const files = scaffold(app, { minimal: Boolean(flags.minimal) });
458
+ // Nothing is written until every collision is known, so a refusal leaves the
459
+ // directory exactly as it was.
460
+ const clashes = files.filter((file) => !file.whenPresent && existsSync(join(root, file.path)));
461
+ if (clashes.length > 0 && !flags.force) {
462
+ console.error(`\n${red("✗")} ${bold("already an app folder here")}\n`);
463
+ for (const file of clashes) {
464
+ console.error(` ${file.path}`);
465
+ }
466
+ console.error(`\n${dim("pass --force to overwrite, or init into a new directory")}`);
467
+ return 1;
468
+ }
469
+ mkdirSync(root, { recursive: true });
470
+ const actions = [];
471
+ for (const file of files) {
472
+ const target = join(root, file.path);
473
+ if (!existsSync(target)) {
474
+ write(target, file.contents);
475
+ actions.push([green("+"), file.path, null]);
476
+ continue;
477
+ }
478
+ if (file.whenPresent === "keep") {
479
+ actions.push([dim("·"), file.path, "yours, left alone"]);
480
+ continue;
481
+ }
482
+ if (file.whenPresent === "merge" && file.merge) {
483
+ const changed = file.merge(target, file.contents);
484
+ actions.push([
485
+ changed.length > 0 ? yellow("~") : dim("·"),
486
+ file.path,
487
+ changed.length > 0 ? `+ ${changed.join(", ")}` : "nothing to add",
488
+ ]);
489
+ continue;
490
+ }
491
+ // A collision the caller chose to overwrite.
492
+ write(target, file.contents);
493
+ actions.push([yellow("~"), file.path, "overwritten"]);
494
+ }
495
+ console.log(`\n${green("✓")} ${bold(app.name)} ${dim(`→ ${root}`)}\n`);
496
+ for (const [marker, path, note] of actions) {
497
+ console.log(` ${marker} ${path}${note ? ` ${dim(note)}` : ""}`);
498
+ }
499
+ // The merge leaves a script the repo already had alone, so the way into the dev
500
+ // loop is whatever survived that: `npm run dev` when it is ours, the CLI by
501
+ // name when the repo's own `dev` runs something else.
502
+ const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
503
+ const ours = manifest.scripts?.dev === "waniwani dev";
504
+ if (manifest.type !== "module") {
505
+ console.log(`\n${yellow("!")} ${bold("package.json")} says ${bold(`"type": "${manifest.type}"`)}`);
506
+ console.log(` ${dim('app modules are ESM: set it to "module" or the build cannot load them')}`);
507
+ }
508
+ const manager = packageManager();
509
+ // A scaffold with no node_modules still checks out and still reads, so a
510
+ // failed install is reported and the folder is kept.
511
+ const installed = flags.install === false ? null : await install(root, manager);
512
+ console.log(`\n${bold("From here")}`);
513
+ // `init apps/store` is scaffolded two levels down, so the path is the one to
514
+ // print rather than the directory's own name.
515
+ const from = relative(process.cwd(), root);
516
+ if (from) {
517
+ console.log(` cd ${from}`);
518
+ }
519
+ if (installed === false) {
520
+ console.log(` ${yellow(`${manager} install`)} ${dim("(the first attempt failed)")}`);
521
+ }
522
+ else if (installed === null) {
523
+ console.log(` ${manager} install`);
524
+ }
525
+ console.log(` ${ours ? `${manager} run dev` : `${RUNNERS[manager]} waniwani dev`}`);
526
+ console.log(`\n${bold("Then")}`);
527
+ console.log(` ${dim("·")} edit ${bold(`tools/${TOOL}.ts`)} to answer with your own data`);
528
+ if (!flags.minimal) {
529
+ console.log(` ${dim("·")} edit ${bold(`widgets/${WIDGET}/ui.tsx`)} for how it looks on screen`);
530
+ }
531
+ console.log(` ${dim("·")} add ${bold("flows/<name>.ts")} for a multi-step conversation`);
532
+ return 0;
575
533
  }
534
+ //# sourceMappingURL=init.js.map