@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.
- package/README.md +67 -35
- 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/dist/cli/init.js +642 -0
- 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/init.mjs +0 -575
- package/cli/log.mjs +0 -178
- package/cli/scan.mjs +0 -112
- package/cli/template.mjs +0 -190
- package/cli/validate.mjs +0 -391
package/cli/validate.mjs
DELETED
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The build check.
|
|
3
|
-
*
|
|
4
|
-
* Structural rules first (cheap, from the filesystem), then the modules are
|
|
5
|
-
* actually imported so a broken export or a flow that fails to compile is
|
|
6
|
-
* reported here rather than as a stack trace at request time.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { readFileSync } from "node:fs";
|
|
10
|
-
import { join, relative } from "node:path";
|
|
11
|
-
import { loadAppEnv } from "./env.mjs";
|
|
12
|
-
import { compare, floorOf, installable } from "./peers.mjs";
|
|
13
|
-
|
|
14
|
-
const NAME_RE = /^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* What may appear in an endpoint path segment. Deliberately narrower than what
|
|
18
|
-
* a filesystem allows: the segment becomes a URL path, and a file called
|
|
19
|
-
* `Book Call.ts` would be served at a URL nobody would guess.
|
|
20
|
-
*/
|
|
21
|
-
const SEGMENT_RE = /^[a-zA-Z0-9._-]+$/;
|
|
22
|
-
|
|
23
|
-
const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
24
|
-
|
|
25
|
-
class Report {
|
|
26
|
-
constructor(root) {
|
|
27
|
-
this.root = root;
|
|
28
|
-
this.errors = [];
|
|
29
|
-
this.warnings = [];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
error(where, message, hint) {
|
|
33
|
-
this.errors.push({ where, message, hint });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
warn(where, message, hint) {
|
|
37
|
-
this.warnings.push({ where, message, hint });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
get ok() {
|
|
41
|
-
return this.errors.length === 0;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function rel(root, file) {
|
|
46
|
-
return relative(root, file) || ".";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Everything the filesystem alone can tell us. */
|
|
50
|
-
function checkStructure(app, report) {
|
|
51
|
-
const { root } = app;
|
|
52
|
-
|
|
53
|
-
if (!app.configFile) {
|
|
54
|
-
report.error(
|
|
55
|
-
"waniwani.config.ts",
|
|
56
|
-
"missing app config",
|
|
57
|
-
"create waniwani.config.ts with `export default defineApp({ name: '...' })`",
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (app.tools.length + app.widgets.length + app.flows.length === 0) {
|
|
62
|
-
report.error(
|
|
63
|
-
".",
|
|
64
|
-
"this app exposes nothing",
|
|
65
|
-
"add a tool (tools/<name>.ts), a widget (widgets/<name>/), or a flow (flows/<name>.ts)",
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (const widget of app.widgets) {
|
|
70
|
-
const where = rel(root, widget.dir);
|
|
71
|
-
if (!widget.contract) {
|
|
72
|
-
report.error(
|
|
73
|
-
where,
|
|
74
|
-
"missing widget.ts",
|
|
75
|
-
"every widget folder needs a widget.ts with `export default defineWidget({ ... })`",
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
if (!widget.ui) {
|
|
79
|
-
report.error(
|
|
80
|
-
where,
|
|
81
|
-
"missing ui.tsx",
|
|
82
|
-
"every widget folder needs a ui.tsx with a default-exported React component",
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Styling is Tailwind, out of the template's `src/index.css`. Nothing imports
|
|
88
|
-
// an app's own CSS, so a styles.css is a file whose rules never load — and it
|
|
89
|
-
// fails in the worst way, by rendering an unstyled widget rather than an
|
|
90
|
-
// error. Naming it here costs one deletion; missing it costs a debugging
|
|
91
|
-
// session against a bundle that never mentions the file.
|
|
92
|
-
for (const file of app.strayStyles) {
|
|
93
|
-
report.error(
|
|
94
|
-
rel(root, file),
|
|
95
|
-
"app CSS is not bundled — nothing imports this file",
|
|
96
|
-
"style with Tailwind utility classes in ui.tsx; the template's src/index.css carries the @theme tokens and the `dark` variant",
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// A widget's folder name is its MCP tool name and its bundle entry name, so
|
|
101
|
-
// it has to survive both.
|
|
102
|
-
const named = [
|
|
103
|
-
...app.tools.map((t) => ({ kind: "tool", name: t.name, where: rel(root, t.file) })),
|
|
104
|
-
...app.widgets.map((w) => ({ kind: "widget", name: w.name, where: rel(root, w.dir) })),
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
const seen = new Map();
|
|
108
|
-
for (const entry of named) {
|
|
109
|
-
if (!NAME_RE.test(entry.name)) {
|
|
110
|
-
report.error(
|
|
111
|
-
entry.where,
|
|
112
|
-
`"${entry.name}" is not a valid MCP tool name`,
|
|
113
|
-
"use lowercase letters, digits, dashes and underscores",
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
const previous = seen.get(entry.name);
|
|
117
|
-
if (previous) {
|
|
118
|
-
report.error(
|
|
119
|
-
entry.where,
|
|
120
|
-
`name "${entry.name}" is already taken by ${previous.kind} ${previous.where}`,
|
|
121
|
-
"tool and widget names share one namespace — rename one of them",
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
seen.set(entry.name, entry);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// An endpoint's file position is its URL, so a segment that cannot appear in
|
|
128
|
-
// a URL is a file served somewhere unguessable, and two files resolving to
|
|
129
|
-
// one path means the second mount is dead — Express answers from the first.
|
|
130
|
-
const paths = new Map();
|
|
131
|
-
// The generator names one import per endpoint, camel-cased from the path, so
|
|
132
|
-
// two paths that camel-case alike (`api/cal-slots.ts`, `api/cal/slots.ts`)
|
|
133
|
-
// would emit the same identifier twice and fail in generated code the author
|
|
134
|
-
// cannot open.
|
|
135
|
-
const identifiers = new Map();
|
|
136
|
-
for (const endpoint of app.endpoints) {
|
|
137
|
-
const where = rel(root, endpoint.file);
|
|
138
|
-
|
|
139
|
-
const identifier = endpoint.segments.join("-").replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
|
|
140
|
-
const clash = identifiers.get(identifier);
|
|
141
|
-
if (clash) {
|
|
142
|
-
report.error(
|
|
143
|
-
where,
|
|
144
|
-
`this path generates the same import name as ${clash}`,
|
|
145
|
-
"rename one of the two — the generator derives an identifier from the path",
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
identifiers.set(identifier, where);
|
|
149
|
-
|
|
150
|
-
for (const segment of endpoint.segments) {
|
|
151
|
-
if (SEGMENT_RE.test(segment)) continue;
|
|
152
|
-
report.error(
|
|
153
|
-
where,
|
|
154
|
-
`"${segment}" cannot be part of a URL path`,
|
|
155
|
-
"use letters, digits, dashes, dots and underscores — the file's position is the endpoint's path",
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const previous = paths.get(endpoint.path);
|
|
160
|
-
if (previous) {
|
|
161
|
-
report.error(
|
|
162
|
-
where,
|
|
163
|
-
`${endpoint.path} is already served by ${previous}`,
|
|
164
|
-
"two files resolve to one path — Express answers from the first, so this one never runs",
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
paths.set(endpoint.path, where);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Flows point at widgets by name. Catching a typo here beats catching it
|
|
171
|
-
// when a user is halfway through a conversation.
|
|
172
|
-
const widgetNames = new Set(app.widgets.map((w) => w.name));
|
|
173
|
-
for (const flow of app.flows) {
|
|
174
|
-
const source = readFileSync(flow.file, "utf-8");
|
|
175
|
-
for (const match of source.matchAll(/showWidget\(\s*\{[^}]*?tool:\s*["'`]([^"'`]+)["'`]/gs)) {
|
|
176
|
-
const target = match[1];
|
|
177
|
-
if (!widgetNames.has(target)) {
|
|
178
|
-
report.error(
|
|
179
|
-
rel(root, flow.file),
|
|
180
|
-
`showWidget references the widget "${target}", which does not exist`,
|
|
181
|
-
widgetNames.size > 0
|
|
182
|
-
? `known widgets: ${[...widgetNames].join(", ")}`
|
|
183
|
-
: "this app has no widgets/ folder",
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Import each module and check the shape of what it exports. */
|
|
191
|
-
async function checkModules(app, report) {
|
|
192
|
-
const { root } = app;
|
|
193
|
-
|
|
194
|
-
if (app.configFile) {
|
|
195
|
-
const config = await load(app.configFile, rel(root, app.configFile), report);
|
|
196
|
-
if (config && !config.name) {
|
|
197
|
-
report.error(
|
|
198
|
-
rel(root, app.configFile),
|
|
199
|
-
"defineApp() is missing `name`",
|
|
200
|
-
"the MCP server name, e.g. name: 'oney-split-payment'",
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
for (const tool of app.tools) {
|
|
206
|
-
const where = rel(root, tool.file);
|
|
207
|
-
const def = await load(tool.file, where, report);
|
|
208
|
-
if (!def) continue;
|
|
209
|
-
if (typeof def.run !== "function") {
|
|
210
|
-
report.error(where, "tool is missing run()", "export default defineTool({ ..., run })");
|
|
211
|
-
}
|
|
212
|
-
if (!def.description) {
|
|
213
|
-
report.error(
|
|
214
|
-
where,
|
|
215
|
-
"tool is missing a description",
|
|
216
|
-
"the description is how the model decides to call it — say when to use it",
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
if (!def.title) {
|
|
220
|
-
report.warn(where, "tool is missing a title", "titles show up in connector UIs");
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
for (const widget of app.widgets) {
|
|
225
|
-
if (!widget.contract) continue;
|
|
226
|
-
const where = rel(root, widget.contract);
|
|
227
|
-
const def = await load(widget.contract, where, report);
|
|
228
|
-
if (!def) continue;
|
|
229
|
-
if (!def.data || typeof def.data !== "object") {
|
|
230
|
-
report.error(
|
|
231
|
-
where,
|
|
232
|
-
"widget is missing a `data` schema",
|
|
233
|
-
"data is the single schema for input, output, and the component's props",
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
if (!def.description) {
|
|
237
|
-
report.error(
|
|
238
|
-
where,
|
|
239
|
-
"widget is missing a description",
|
|
240
|
-
"the description is how the model decides to show it",
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
for (const endpoint of app.endpoints) {
|
|
246
|
-
const where = rel(root, endpoint.file);
|
|
247
|
-
const def = await load(endpoint.file, where, report);
|
|
248
|
-
if (!def) continue;
|
|
249
|
-
if (typeof def.handler !== "function") {
|
|
250
|
-
report.error(
|
|
251
|
-
where,
|
|
252
|
-
"endpoint is missing handler()",
|
|
253
|
-
"export default defineEndpoint({ handler: (req, res) => { ... } })",
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
for (const method of def.method ? [def.method].flat() : []) {
|
|
257
|
-
if (HTTP_METHODS.has(method)) continue;
|
|
258
|
-
report.error(
|
|
259
|
-
where,
|
|
260
|
-
`"${method}" is not an HTTP method`,
|
|
261
|
-
`one of: ${[...HTTP_METHODS].join(", ")}`,
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
for (const flow of app.flows) {
|
|
267
|
-
const where = rel(root, flow.file);
|
|
268
|
-
const def = await load(flow.file, where, report);
|
|
269
|
-
if (!def) continue;
|
|
270
|
-
if (!def.name || !def.config || typeof def.handler !== "function") {
|
|
271
|
-
report.error(
|
|
272
|
-
where,
|
|
273
|
-
"this is not a compiled flow",
|
|
274
|
-
"export default createFlow({ ... }).addEdge(...).compile()",
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* App modules are TypeScript, and they import each other with the `.js`
|
|
282
|
-
* specifiers TypeScript's ESM output requires — `../lib/plans.js` for a file on
|
|
283
|
-
* disk called `plans.ts`. Node's built-in type stripping does not remap those,
|
|
284
|
-
* so validation registers tsx's resolver before importing anything out of the
|
|
285
|
-
* app folder. Bun does the remapping on its own, which is what hid this while
|
|
286
|
-
* the CLI still ran under bun.
|
|
287
|
-
*
|
|
288
|
-
* Registration is global to the process and idempotent here, so it happens once
|
|
289
|
-
* on the first load rather than at startup — `waniwani start` never validates.
|
|
290
|
-
*/
|
|
291
|
-
let resolverRegistered = false;
|
|
292
|
-
async function registerTypeScriptResolver() {
|
|
293
|
-
if (resolverRegistered) return;
|
|
294
|
-
resolverRegistered = true;
|
|
295
|
-
const { register } = await import("tsx/esm/api");
|
|
296
|
-
register();
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async function load(file, where, report) {
|
|
300
|
-
try {
|
|
301
|
-
await registerTypeScriptResolver();
|
|
302
|
-
const module = await import(`${file}?t=${Date.now()}`);
|
|
303
|
-
const def = module.default;
|
|
304
|
-
if (!def) {
|
|
305
|
-
report.error(where, "no default export", "the runtime loads this module's default export");
|
|
306
|
-
return null;
|
|
307
|
-
}
|
|
308
|
-
return def;
|
|
309
|
-
} catch (error) {
|
|
310
|
-
report.error(where, "failed to load", error instanceof Error ? error.message : String(error));
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* The SDK version an app asked for, against the floor this package declares.
|
|
317
|
-
*
|
|
318
|
-
* `@waniwani/sdk` is a required peer (see the manifest's `//sdk` note), so the
|
|
319
|
-
* app owns the version and this is the one place that says what the runtime and
|
|
320
|
-
* the pinned template need underneath it. It reads two manifests off disk and
|
|
321
|
-
* fetches nothing, which is why it runs in `check` rather than waiting for the
|
|
322
|
-
* dependency merge in `codegen.mjs` — a version that cannot work should not
|
|
323
|
-
* need a template download to be told so.
|
|
324
|
-
*
|
|
325
|
-
* Undeclared is not an error. npm and bun both install a required peer, and
|
|
326
|
-
* `codegen.mjs` writes one into the generated project, so an app that never
|
|
327
|
-
* mentions the SDK still gets a working copy.
|
|
328
|
-
*/
|
|
329
|
-
function checkPeers(app, report) {
|
|
330
|
-
let manifest;
|
|
331
|
-
try {
|
|
332
|
-
manifest = JSON.parse(readFileSync(join(app.root, "package.json"), "utf-8"));
|
|
333
|
-
} catch {
|
|
334
|
-
// No manifest, or an unparseable one. Both are `init`'s business, and
|
|
335
|
-
// neither is improved by a second error about a dependency inside it.
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const name = "@waniwani/sdk";
|
|
340
|
-
const spec = manifest.dependencies?.[name] ?? manifest.devDependencies?.[name];
|
|
341
|
-
if (spec == null) {
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const floor = floorOf(name);
|
|
346
|
-
const suggestion = installable(name);
|
|
347
|
-
switch (compare(spec, name)) {
|
|
348
|
-
case "below":
|
|
349
|
-
report.error(
|
|
350
|
-
"package.json",
|
|
351
|
-
`${name} ${spec} cannot reach ${floor}, which this kit needs`,
|
|
352
|
-
`no version that range allows will work: below the floor the SDK declares a @modelcontextprotocol/ext-apps peer that conflicts with the framework's, and npm refuses the tree. Set ${name} to ${suggestion}.`,
|
|
353
|
-
);
|
|
354
|
-
break;
|
|
355
|
-
case "prerelease":
|
|
356
|
-
report.warn(
|
|
357
|
-
"package.json",
|
|
358
|
-
`${name} ${spec} is a prerelease, and ${floor} does not accept one`,
|
|
359
|
-
`npm and bun both exclude a prerelease from a range that names none, so the install warns and the tree may not be what this spec says. Deliberate is fine; ${suggestion} is the released floor.`,
|
|
360
|
-
);
|
|
361
|
-
break;
|
|
362
|
-
case "reachable":
|
|
363
|
-
report.warn(
|
|
364
|
-
"package.json",
|
|
365
|
-
`${name} ${spec} also allows versions below ${floor}`,
|
|
366
|
-
`a fresh install resolves above the floor, and a lockfile written before it moved can hold this tree below it. ${suggestion} says the floor out loud.`,
|
|
367
|
-
);
|
|
368
|
-
break;
|
|
369
|
-
default:
|
|
370
|
-
// "ok", and "unknown" for an expression this cannot parse — a
|
|
371
|
-
// workspace protocol or a git URL, where the version is not in the
|
|
372
|
-
// string and guessing at it would be a false alarm either way.
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
export async function validateApp(app) {
|
|
378
|
-
// The check imports every server-safe module for real, and a module that
|
|
379
|
-
// builds a client at import time reads the environment while doing it. An app
|
|
380
|
-
// that runs fine would otherwise fail its own build check over a variable
|
|
381
|
-
// sitting in the file next to it.
|
|
382
|
-
loadAppEnv(app.root);
|
|
383
|
-
const report = new Report(app.root);
|
|
384
|
-
checkStructure(app, report);
|
|
385
|
-
checkPeers(app, report);
|
|
386
|
-
// Importing broken modules produces noise on top of structural errors.
|
|
387
|
-
if (report.ok) {
|
|
388
|
-
await checkModules(app, report);
|
|
389
|
-
}
|
|
390
|
-
return report;
|
|
391
|
-
}
|