@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.
- package/README.md +39 -39
- package/dist/cli/codegen.js +1105 -0
- package/dist/cli/codegen.js.map +1 -0
- package/{cli/env.mjs → dist/cli/env.js} +5 -6
- package/dist/cli/env.js.map +1 -0
- package/{cli/framework.mjs → dist/cli/framework.js} +112 -115
- package/dist/cli/framework.js.map +1 -0
- package/dist/cli/index.js +378 -0
- package/dist/cli/index.js.map +1 -0
- package/{cli/init.mjs → dist/cli/init.js} +218 -259
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/log.js +156 -0
- package/dist/cli/log.js.map +1 -0
- package/dist/cli/manifest.js +57 -0
- package/dist/cli/manifest.js.map +1 -0
- package/{cli/peers.mjs → dist/cli/peers.js} +77 -88
- package/dist/cli/peers.js.map +1 -0
- package/dist/cli/scan.js +100 -0
- package/dist/cli/scan.js.map +1 -0
- package/dist/cli/template.js +173 -0
- package/dist/cli/template.js.map +1 -0
- package/dist/cli/types.js +14 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/validate.js +328 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/cli/vercel.js +103 -0
- package/dist/cli/vercel.js.map +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +0 -1
- package/dist/server.js.map +1 -1
- package/dist/web.d.ts +8 -7
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +7 -6
- package/dist/web.js.map +1 -1
- package/package.json +13 -9
- package/src/server.ts +7 -9
- package/src/web.tsx +12 -13
- package/cli/codegen.mjs +0 -1267
- package/cli/index.mjs +0 -409
- package/cli/log.mjs +0 -178
- package/cli/scan.mjs +0 -112
- package/cli/template.mjs +0 -190
- 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 {
|
|
35
|
-
import {
|
|
36
|
-
import { installable } from "./peers.
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
419
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
441
|
-
|
|
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 = {
|
|
446
|
-
|
|
414
|
+
const RUNNERS = {
|
|
415
|
+
npm: "npx",
|
|
416
|
+
pnpm: "pnpm",
|
|
417
|
+
yarn: "yarn",
|
|
418
|
+
bun: "bunx",
|
|
419
|
+
};
|
|
447
420
|
function install(root, manager) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|