@waniwani/kit 0.1.6-beta.0 → 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} +228 -276
- 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/dist/cli/peers.js +159 -0
- 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 +14 -14
- package/src/server.ts +7 -9
- package/src/web.tsx +12 -13
- package/cli/codegen.mjs +0 -1215
- 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 -327
|
@@ -26,108 +26,85 @@
|
|
|
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
|
-
|
|
37
|
-
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
38
|
-
const MANIFEST = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
39
|
-
|
|
33
|
+
import { bold, dim, green, red, yellow } from "./log.js";
|
|
34
|
+
import { PACKAGE_VERSION } from "./manifest.js";
|
|
35
|
+
import { installable } from "./peers.js";
|
|
40
36
|
/** A directory name as an MCP server name: `My App` becomes `my-app`. */
|
|
41
37
|
function slugify(input) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
const slug = input
|
|
39
|
+
.trim()
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
42
|
+
.replace(/^-+|-+$/g, "");
|
|
43
|
+
return slug || "my-app";
|
|
48
44
|
}
|
|
49
|
-
|
|
50
45
|
/** `my-app` becomes `My app`, for the human-facing title. */
|
|
51
46
|
function titleize(slug) {
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
const words = slug.replace(/[-_]+/g, " ");
|
|
48
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
54
49
|
}
|
|
55
|
-
|
|
56
50
|
/**
|
|
57
51
|
* A title, made safe to drop into the generated TypeScript and Markdown.
|
|
58
52
|
* Backticks, quotes, backslashes and `$` are the characters that would end a
|
|
59
53
|
* string or a template literal early, and a product name needs none of them.
|
|
60
54
|
*/
|
|
61
55
|
function cleanTitle(input) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
56
|
+
return input
|
|
57
|
+
.replace(/[`"'\\$]/g, "")
|
|
58
|
+
.replace(/\s+/g, " ")
|
|
59
|
+
.trim();
|
|
66
60
|
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* A peer range is a floor, `>=19`, and a floor in an app's dependencies installs
|
|
70
|
-
* the next major on the day it lands. Cap it. Anything already ranged, `^4`,
|
|
71
|
-
* passes through as it is.
|
|
72
|
-
*/
|
|
73
|
-
function installable(name, range) {
|
|
74
|
-
if (!range) {
|
|
75
|
-
throw new Error(`@waniwani/kit declares no peer range for ${name}: this package's manifest moved`);
|
|
76
|
-
}
|
|
77
|
-
const floor = /^>=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(range.trim());
|
|
78
|
-
return floor ? `^${floor[1]}.${floor[2] ?? 0}.${floor[3] ?? 0}` : range;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
61
|
/**
|
|
82
62
|
* What a new app depends on.
|
|
83
63
|
*
|
|
84
64
|
* Every version is read off this package's own manifest: `@waniwani/kit` at the
|
|
85
|
-
* version of the CLI doing the scaffolding,
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* file in the folder that can be wrong the day it is
|
|
65
|
+
* version of the CLI doing the scaffolding, and the four peers at the floors
|
|
66
|
+
* this package declares, capped by `installable` so a floor does not install
|
|
67
|
+
* the next major on the day it lands. A scaffold that wrote its own numbers
|
|
68
|
+
* here would be the one file in the folder that can be wrong the day it is
|
|
69
|
+
* created.
|
|
70
|
+
*
|
|
71
|
+
* `@waniwani/sdk` is written out even though a required peer is auto-installed
|
|
72
|
+
* without it, because the app imports it directly — `flows/*.ts` calls
|
|
73
|
+
* `createFlow` — and a package you import belongs in your own manifest rather
|
|
74
|
+
* than arriving because something else asked for it.
|
|
89
75
|
*/
|
|
90
76
|
function dependencies() {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
};
|
|
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
|
+
};
|
|
99
84
|
}
|
|
100
|
-
|
|
101
85
|
// ------------------------------------------------------------ scaffold content
|
|
102
|
-
|
|
103
86
|
/**
|
|
104
87
|
* The tool name and the widget name the scaffold uses. They appear in five
|
|
105
88
|
* files, including inside prose the model reads, so they are named once.
|
|
106
89
|
*/
|
|
107
90
|
const TOOL = "search-products";
|
|
108
91
|
const WIDGET = "product-list";
|
|
109
|
-
|
|
110
92
|
function packageJson(app) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
},
|
|
124
|
-
null,
|
|
125
|
-
2,
|
|
126
|
-
)}\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`;
|
|
127
105
|
}
|
|
128
|
-
|
|
129
106
|
function appConfig(app) {
|
|
130
|
-
|
|
107
|
+
return `import { defineApp } from "@waniwani/kit";
|
|
131
108
|
|
|
132
109
|
export default defineApp({
|
|
133
110
|
// The MCP server name. Hosts show \`title\` to humans and use this one as the id.
|
|
@@ -136,9 +113,8 @@ export default defineApp({
|
|
|
136
113
|
});
|
|
137
114
|
`;
|
|
138
115
|
}
|
|
139
|
-
|
|
140
116
|
function tool() {
|
|
141
|
-
|
|
117
|
+
return `import { defineTool } from "@waniwani/kit";
|
|
142
118
|
import { z } from "zod";
|
|
143
119
|
|
|
144
120
|
/**
|
|
@@ -190,9 +166,8 @@ export default defineTool({
|
|
|
190
166
|
});
|
|
191
167
|
`;
|
|
192
168
|
}
|
|
193
|
-
|
|
194
169
|
function widgetContract() {
|
|
195
|
-
|
|
170
|
+
return `import { defineWidget } from "@waniwani/kit";
|
|
196
171
|
import { z } from "zod";
|
|
197
172
|
|
|
198
173
|
const product = z.object({
|
|
@@ -229,9 +204,8 @@ Wait for the shopper to pick one, then answer about that product.\`,
|
|
|
229
204
|
});
|
|
230
205
|
`;
|
|
231
206
|
}
|
|
232
|
-
|
|
233
207
|
function widgetUi() {
|
|
234
|
-
|
|
208
|
+
return `import { useLayout, useSendFollowUpMessage, useWidget } from "@waniwani/kit/web";
|
|
235
209
|
import widget from "./widget.js";
|
|
236
210
|
|
|
237
211
|
const euros = (value: number) =>
|
|
@@ -284,26 +258,23 @@ export default function ProductList() {
|
|
|
284
258
|
}
|
|
285
259
|
`;
|
|
286
260
|
}
|
|
287
|
-
|
|
288
261
|
function envExample() {
|
|
289
|
-
|
|
262
|
+
return `# Optional. Without it the app still runs: flows use MemoryKvStore and
|
|
290
263
|
# withWaniwani degrades to a no-op. With it, flow state is hosted and tracking
|
|
291
264
|
# reaches app.waniwani.ai.
|
|
292
265
|
WANIWANI_API_KEY=
|
|
293
266
|
WANIWANI_PUBLIC_KEY=
|
|
294
267
|
`;
|
|
295
268
|
}
|
|
296
|
-
|
|
297
269
|
function gitignore() {
|
|
298
|
-
|
|
270
|
+
return `node_modules/
|
|
299
271
|
.waniwani/
|
|
300
272
|
.env
|
|
301
273
|
.env.local
|
|
302
274
|
`;
|
|
303
275
|
}
|
|
304
|
-
|
|
305
276
|
function readme(app) {
|
|
306
|
-
|
|
277
|
+
return `# ${app.title}
|
|
307
278
|
|
|
308
279
|
An MCP app built with [@waniwani/kit](https://www.npmjs.com/package/@waniwani/kit).
|
|
309
280
|
You own the folders below. The server, the transport, the bundling and the deploy
|
|
@@ -329,7 +300,6 @@ npm run start # run the production build
|
|
|
329
300
|
and it stays out of git.
|
|
330
301
|
`;
|
|
331
302
|
}
|
|
332
|
-
|
|
333
303
|
/**
|
|
334
304
|
* The files a new app gets.
|
|
335
305
|
*
|
|
@@ -338,27 +308,25 @@ and it stays out of git.
|
|
|
338
308
|
* scaffold's contribution into what is there, and `keep` leaves it untouched.
|
|
339
309
|
*/
|
|
340
310
|
function scaffold(app, { minimal }) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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;
|
|
358
328
|
}
|
|
359
|
-
|
|
360
329
|
// -------------------------------------------------------------------- merging
|
|
361
|
-
|
|
362
330
|
/**
|
|
363
331
|
* Add the scripts and dependencies an app needs to a manifest that is already
|
|
364
332
|
* there, and touch nothing else. A key the repo already declares is the repo's
|
|
@@ -367,36 +335,34 @@ function scaffold(app, { minimal }) {
|
|
|
367
335
|
* @returns the keys added, for the CLI to report
|
|
368
336
|
*/
|
|
369
337
|
function mergePackageJson(file, contents) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
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;
|
|
398
365
|
}
|
|
399
|
-
|
|
400
366
|
/**
|
|
401
367
|
* Add the lines the app does not ignore yet, leaving every line it wrote alone.
|
|
402
368
|
* A trailing slash is not part of the comparison, so a repo ignoring
|
|
@@ -405,38 +371,34 @@ function mergePackageJson(file, contents) {
|
|
|
405
371
|
* @returns the lines added, for the CLI to report
|
|
406
372
|
*/
|
|
407
373
|
function mergeGitignore(file, contents) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
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;
|
|
420
385
|
}
|
|
421
|
-
|
|
422
386
|
// ------------------------------------------------------------------- the shell
|
|
423
|
-
|
|
424
387
|
function write(file, contents) {
|
|
425
|
-
|
|
426
|
-
|
|
388
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
389
|
+
writeFileSync(file, contents);
|
|
427
390
|
}
|
|
428
|
-
|
|
429
391
|
/** One question, with the default in parentheses and Enter taking it. */
|
|
430
392
|
async function ask(question, fallback) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
+
}
|
|
438
401
|
}
|
|
439
|
-
|
|
440
402
|
/**
|
|
441
403
|
* The package manager that invoked this command, which npm, pnpm, yarn and bun
|
|
442
404
|
* all announce in `npm_config_user_agent`. Nothing to detect from lockfiles: a
|
|
@@ -444,27 +406,30 @@ async function ask(question, fallback) {
|
|
|
444
406
|
* typed with one of the four.
|
|
445
407
|
*/
|
|
446
408
|
function packageManager() {
|
|
447
|
-
|
|
448
|
-
|
|
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";
|
|
449
412
|
}
|
|
450
|
-
|
|
451
413
|
/** How each manager runs a binary out of node_modules, for the closing lines. */
|
|
452
|
-
const RUNNERS = {
|
|
453
|
-
|
|
414
|
+
const RUNNERS = {
|
|
415
|
+
npm: "npx",
|
|
416
|
+
pnpm: "pnpm",
|
|
417
|
+
yarn: "yarn",
|
|
418
|
+
bun: "bunx",
|
|
419
|
+
};
|
|
454
420
|
function install(root, manager) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
+
});
|
|
466
432
|
}
|
|
467
|
-
|
|
468
433
|
/**
|
|
469
434
|
* Scaffold an app folder, install its dependencies, and say what to run.
|
|
470
435
|
*
|
|
@@ -474,109 +439,96 @@ function install(root, manager) {
|
|
|
474
439
|
* @returns a process exit code
|
|
475
440
|
*/
|
|
476
441
|
export async function init(appRoot, flags, { targeted = true } = {}) {
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (installed === false) {
|
|
569
|
-
console.log(` ${yellow(`${manager} install`)} ${dim("(the first attempt failed)")}`);
|
|
570
|
-
} else if (installed === null) {
|
|
571
|
-
console.log(` ${manager} install`);
|
|
572
|
-
}
|
|
573
|
-
console.log(` ${ours ? `${manager} run dev` : `${RUNNERS[manager]} waniwani dev`}`);
|
|
574
|
-
|
|
575
|
-
console.log(`\n${bold("Then")}`);
|
|
576
|
-
console.log(` ${dim("·")} edit ${bold(`tools/${TOOL}.ts`)} to answer with your own data`);
|
|
577
|
-
if (!flags.minimal) {
|
|
578
|
-
console.log(` ${dim("·")} edit ${bold(`widgets/${WIDGET}/ui.tsx`)} for how it looks on screen`);
|
|
579
|
-
}
|
|
580
|
-
console.log(` ${dim("·")} add ${bold("flows/<name>.ts")} for a multi-step conversation`);
|
|
581
|
-
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;
|
|
582
533
|
}
|
|
534
|
+
//# sourceMappingURL=init.js.map
|