@waniwani/kit 0.1.6 → 0.1.8

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 (45) hide show
  1. package/README.md +67 -35
  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/dist/cli/init.js +642 -0
  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/init.mjs +0 -575
  42. package/cli/log.mjs +0 -178
  43. package/cli/scan.mjs +0 -112
  44. package/cli/template.mjs +0 -190
  45. package/cli/validate.mjs +0 -391
package/cli/init.mjs DELETED
@@ -1,575 +0,0 @@
1
- /**
2
- * `waniwani init`, the first command anyone runs.
3
- *
4
- * What it writes is an app folder that already passes `waniwani check` and
5
- * already renders something: a config, a tool, and the widget that displays what
6
- * the tool returned. The point of scaffolding a working pair instead of an empty
7
- * folder is that the tool-to-widget hand-off is the one piece of this framework
8
- * nobody guesses correctly from the type signatures.
9
- *
10
- * waniwani init my-app create my-app/ and scaffold in it
11
- * waniwani init . scaffold in the current directory
12
- * waniwani init ask for a name, and put the app where the
13
- * answer says: a name of its own creates
14
- * ./<name>/, the offered default (the current
15
- * folder's name) scaffolds in place
16
- *
17
- * --name <name> the MCP server name, default the directory name
18
- * --minimal config and one tool, no widget
19
- * --yes take every default, ask nothing
20
- * --no-install skip the dependency install
21
- * --force overwrite app files that are already there
22
- *
23
- * Running it inside a repo that already has files is expected and supported.
24
- * A `package.json` is merged rather than replaced, a `.gitignore` gains the
25
- * lines it lacks, and a `README.md` or `.env.example` that exists is left
26
- * alone. Only the app's own source files count as a collision, and those stop
27
- * the command until `--force` says otherwise.
28
- */
29
-
30
- import { spawn } from "node:child_process";
31
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
32
- import { basename, dirname, join, relative } from "node:path";
33
- 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
-
41
- /** A directory name as an MCP server name: `My App` becomes `my-app`. */
42
- 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";
49
- }
50
-
51
- /** `my-app` becomes `My app`, for the human-facing title. */
52
- function titleize(slug) {
53
- const words = slug.replace(/[-_]+/g, " ");
54
- return words.charAt(0).toUpperCase() + words.slice(1);
55
- }
56
-
57
- /**
58
- * A title, made safe to drop into the generated TypeScript and Markdown.
59
- * Backticks, quotes, backslashes and `$` are the characters that would end a
60
- * string or a template literal early, and a product name needs none of them.
61
- */
62
- function cleanTitle(input) {
63
- return input
64
- .replace(/[`"'\\$]/g, "")
65
- .replace(/\s+/g, " ")
66
- .trim();
67
- }
68
-
69
- /**
70
- * What a new app depends on.
71
- *
72
- * Every version is read off this package's own manifest: `@waniwani/kit` at the
73
- * version of the CLI doing the scaffolding, and the four peers at the floors
74
- * this package declares, capped by `installable` so a floor does not install
75
- * the next major on the day it lands. A scaffold that wrote its own numbers
76
- * here would be the one file in the folder that can be wrong the day it is
77
- * created.
78
- *
79
- * `@waniwani/sdk` is written out even though a required peer is auto-installed
80
- * without it, because the app imports it directly — `flows/*.ts` calls
81
- * `createFlow` — and a package you import belongs in your own manifest rather
82
- * than arriving because something else asked for it.
83
- */
84
- 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
- };
92
- }
93
-
94
- // ------------------------------------------------------------ scaffold content
95
-
96
- /**
97
- * The tool name and the widget name the scaffold uses. They appear in five
98
- * files, including inside prose the model reads, so they are named once.
99
- */
100
- const TOOL = "search-products";
101
- const WIDGET = "product-list";
102
-
103
- 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`;
120
- }
121
-
122
- function appConfig(app) {
123
- return `import { defineApp } from "@waniwani/kit";
124
-
125
- export default defineApp({
126
- // The MCP server name. Hosts show \`title\` to humans and use this one as the id.
127
- name: ${JSON.stringify(app.name)},
128
- title: ${JSON.stringify(app.title)},
129
- });
130
- `;
131
- }
132
-
133
- function tool() {
134
- return `import { defineTool } from "@waniwani/kit";
135
- import { z } from "zod";
136
-
137
- /**
138
- * The filename is the tool name, so this file is \`${TOOL}\`. Rename the
139
- * file and the tool renames with it.
140
- *
141
- * Swap CATALOGUE for whatever answers the question for real: a fetch, a
142
- * database, an internal API. \`run\` may be async.
143
- */
144
- const CATALOGUE = [
145
- { id: "aeron", name: "Aeron chair", price: 1290, blurb: "Mesh task chair, twelve-year warranty." },
146
- { id: "sayl", name: "Sayl chair", price: 545, blurb: "Suspension back, the light one." },
147
- { id: "nevi", name: "Nevi sit-stand desk", price: 890, blurb: "Electric, 70 to 120 cm." },
148
- { id: "ollin", name: "Ollin monitor arm", price: 235, blurb: "Single arm, holds up to 9 kg." },
149
- ];
150
-
151
- export default defineTool({
152
- // Shown to humans in connector UIs.
153
- title: "Search the catalogue",
154
- // The only thing the model reads before deciding to call this, so it says
155
- // when to call it and what not to do instead.
156
- description:
157
- "Find products matching what the shopper asked for. Call this before naming any product or quoting any price, and never answer either from memory. Pass the shopper's own words as the query.",
158
- // Zod shapes, written as plain objects instead of z.object({ ... }).
159
- input: {
160
- query: z.string().describe("What the shopper asked for, in their words, e.g. 'a chair under 600'."),
161
- },
162
- output: {
163
- products: z.array(
164
- z.object({
165
- id: z.string(),
166
- name: z.string(),
167
- price: z.number().describe("Price in euros."),
168
- blurb: z.string(),
169
- }),
170
- ),
171
- },
172
- // Becomes MCP annotations. This tool reads and does nothing else.
173
- hints: { readOnly: true },
174
- run: ({ query }) => {
175
- const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);
176
- const matched = CATALOGUE.filter((product) =>
177
- terms.some((term) => \`\${product.name} \${product.blurb}\`.toLowerCase().includes(term)),
178
- );
179
- // The whole catalogue when nothing matched, so an early conversation has
180
- // something on screen while you are still wiring this up.
181
- return { products: matched.length > 0 ? matched : CATALOGUE };
182
- },
183
- });
184
- `;
185
- }
186
-
187
- function widgetContract() {
188
- return `import { defineWidget } from "@waniwani/kit";
189
- import { z } from "zod";
190
-
191
- const product = z.object({
192
- id: z.string(),
193
- name: z.string(),
194
- price: z.number().describe("Price in euros."),
195
- blurb: z.string().describe("One line about the product."),
196
- });
197
-
198
- /**
199
- * The folder name is the tool name, so this widget is \`${WIDGET}\`.
200
- *
201
- * \`data\` is one schema doing three jobs: the tool's input, its structured
202
- * output, and the props \`useWidget()\` hands ui.tsx. Server and UI cannot drift.
203
- *
204
- * This file is imported by the server and by the browser bundle, so it stays
205
- * free of React and CSS. The component sits next to it in ui.tsx.
206
- */
207
- export default defineWidget({
208
- title: "Product list",
209
- description:
210
- "Show the product cards. Call this once ${TOOL} has returned products, passing them through unmodified. Frame it in one short sentence before calling, e.g. \\"Here's what fits.\\" The widget renders every name and price itself, so do NOT list them in text.",
211
- data: {
212
- query: z.string().describe("What the shopper asked for. Shown as the heading."),
213
- products: z.array(product).describe("Products returned by ${TOOL}, unmodified."),
214
- },
215
- hints: { readOnly: true },
216
- // Text handed to the model alongside the rendered widget. Use it to say what
217
- // the model should not repeat, and what it should wait for.
218
- llmText: (data) =>
219
- \`The product list is on screen with \${data.products.length} products. It renders every name and price itself, so do NOT repeat them in text.
220
-
221
- Wait for the shopper to pick one, then answer about that product.\`,
222
- });
223
- `;
224
- }
225
-
226
- function widgetUi() {
227
- return `import { useLayout, useSendFollowUpMessage, useWidget } from "@waniwani/kit/web";
228
- import widget from "./widget.js";
229
-
230
- const euros = (value: number) =>
231
- new Intl.NumberFormat("en-IE", { style: "currency", currency: "EUR" }).format(value);
232
-
233
- export default function ProductList() {
234
- // Typed off the widget's own \`data\` schema. No generated helpers, no server
235
- // type import.
236
- const { data } = useWidget(widget);
237
- const sendFollowUp = useSendFollowUpMessage();
238
-
239
- // The host hands the colour scheme to the view instead of to the browser, so
240
- // \`prefers-color-scheme\` is the wrong signal and Tailwind's \`dark:\` variant is
241
- // wired to a \`dark\` class (see the template's src/index.css). Every widget puts
242
- // that class on its own root: a view is its own bundle in its own iframe, so
243
- // there is no shared ancestor to hang it off.
244
- const { theme } = useLayout();
245
- const root = theme === "dark" ? "dark" : "";
246
-
247
- // \`data\` arrives as soon as the host has the tool input, which on most hosts is
248
- // before the server has responded. Render optimistically.
249
- if (!data) {
250
- return <div className={\`\${root} font-sans text-sm text-slate-500\`}>Loading…</div>;
251
- }
252
-
253
- return (
254
- <div className={\`\${root} font-sans text-slate-900 dark:text-slate-100\`}>
255
- <h1 className="mb-3 text-lg font-semibold tracking-tight">{data.query}</h1>
256
-
257
- <div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-2.5">
258
- {data.products.map((product) => (
259
- <button
260
- type="button"
261
- key={product.id}
262
- // A click becomes a message from the shopper, which is what moves
263
- // the conversation on.
264
- onClick={() => sendFollowUp(\`Tell me more about the \${product.name}.\`)}
265
- className="flex cursor-pointer flex-col items-start gap-1 rounded-2xl border-[1.5px] border-slate-200 bg-white p-3.5 text-left transition duration-150 hover:-translate-y-px hover:border-slate-400 hover:shadow-lg hover:shadow-slate-900/10 dark:border-slate-700 dark:bg-slate-900 dark:hover:border-slate-500"
266
- // What the model reads in place of the pixels.
267
- data-llm={\`\${product.name}, \${euros(product.price)}: \${product.blurb}\`}
268
- >
269
- <span className="text-[22px] font-bold tracking-tight">{euros(product.price)}</span>
270
- <span className="font-semibold">{product.name}</span>
271
- <span className="text-[13px] text-slate-500 dark:text-slate-400">{product.blurb}</span>
272
- </button>
273
- ))}
274
- </div>
275
- </div>
276
- );
277
- }
278
- `;
279
- }
280
-
281
- function envExample() {
282
- return `# Optional. Without it the app still runs: flows use MemoryKvStore and
283
- # withWaniwani degrades to a no-op. With it, flow state is hosted and tracking
284
- # reaches app.waniwani.ai.
285
- WANIWANI_API_KEY=
286
- WANIWANI_PUBLIC_KEY=
287
- `;
288
- }
289
-
290
- function gitignore() {
291
- return `node_modules/
292
- .waniwani/
293
- .env
294
- .env.local
295
- `;
296
- }
297
-
298
- function readme(app) {
299
- return `# ${app.title}
300
-
301
- An MCP app built with [@waniwani/kit](https://www.npmjs.com/package/@waniwani/kit).
302
- You own the folders below. The server, the transport, the bundling and the deploy
303
- files are the kit's.
304
-
305
- \`\`\`
306
- waniwani.config.ts the app's name and title, plus optional instructions
307
- tools/*.ts one file per tool; the filename is the tool name
308
- widgets/<name>/ widget.ts for the contract, ui.tsx for the component
309
- flows/*.ts multi-step conversations, from @waniwani/sdk
310
- \`\`\`
311
-
312
- ## Commands
313
-
314
- \`\`\`bash
315
- npm run dev # dev server, regenerating on every change
316
- npm run check # validate the folder without building
317
- npm run build # production build
318
- npm run start # run the production build
319
- \`\`\`
320
-
321
- \`.waniwani/\` is build output, the way \`.next/\` is. Every command regenerates it
322
- and it stays out of git.
323
- `;
324
- }
325
-
326
- /**
327
- * The files a new app gets.
328
- *
329
- * `whenPresent` decides what happens to one that is already on disk:
330
- * `undefined` is a collision that stops the command, `merge` folds the
331
- * scaffold's contribution into what is there, and `keep` leaves it untouched.
332
- */
333
- 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;
351
- }
352
-
353
- // -------------------------------------------------------------------- merging
354
-
355
- /**
356
- * Add the scripts and dependencies an app needs to a manifest that is already
357
- * there, and touch nothing else. A key the repo already declares is the repo's
358
- * decision, including a `dev` script that runs something other than this CLI.
359
- *
360
- * @returns the keys added, for the CLI to report
361
- */
362
- 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;
391
- }
392
-
393
- /**
394
- * Add the lines the app does not ignore yet, leaving every line it wrote alone.
395
- * A trailing slash is not part of the comparison, so a repo ignoring
396
- * `node_modules` does not gain `node_modules/` next to it.
397
- *
398
- * @returns the lines added, for the CLI to report
399
- */
400
- 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;
413
- }
414
-
415
- // ------------------------------------------------------------------- the shell
416
-
417
- function write(file, contents) {
418
- mkdirSync(dirname(file), { recursive: true });
419
- writeFileSync(file, contents);
420
- }
421
-
422
- /** One question, with the default in parentheses and Enter taking it. */
423
- 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
- }
431
- }
432
-
433
- /**
434
- * The package manager that invoked this command, which npm, pnpm, yarn and bun
435
- * all announce in `npm_config_user_agent`. Nothing to detect from lockfiles: a
436
- * new folder has none, and a `waniwani init` inside an existing repo was still
437
- * typed with one of the four.
438
- */
439
- function packageManager() {
440
- const agent = process.env.npm_config_user_agent ?? "";
441
- return ["bun", "pnpm", "yarn", "npm"].find((name) => agent.startsWith(name)) ?? "npm";
442
- }
443
-
444
- /** 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
-
447
- 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
- });
459
- }
460
-
461
- /**
462
- * Scaffold an app folder, install its dependencies, and say what to run.
463
- *
464
- * @param appRoot the directory to scaffold, created if it does not exist
465
- * @param flags parsed CLI flags
466
- * @param options.targeted a directory was named on the command line
467
- * @returns a process exit code
468
- */
469
- 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;
575
- }