@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/log.mjs DELETED
@@ -1,178 +0,0 @@
1
- /** Build output formatting. Errors point at a file and say how to fix it. */
2
-
3
- /** ESC by char code: a raw control byte in source is invisible and fragile. */
4
- const ESC = String.fromCharCode(27);
5
-
6
- const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
7
- const wrap = (code) => (text) => (useColor ? `${ESC}[${code}m${text}${ESC}[0m` : text);
8
-
9
- export const red = wrap("31");
10
- export const green = wrap("32");
11
- export const yellow = wrap("33");
12
- export const dim = wrap("2");
13
- export const bold = wrap("1");
14
-
15
- /**
16
- * The wordmark, at half the height of the art `waniwani login` prints.
17
- *
18
- * Same letterforms: the tall version is six rows of full blocks with a box-glyph
19
- * drop shadow, and each row here folds two of those together — `▀` where only
20
- * the upper row had ink, `▄` where only the lower did, `█` where both. Three
21
- * rows instead of six, at the same width, and nothing about the shapes changes.
22
- *
23
- * One colour per row rather than per glyph: a vertical ramp needs three escape
24
- * sequences instead of two hundred, and reads the same.
25
- */
26
- const LOGO = [
27
- "██ ██ ▄█▀▀▀█▄ ███▄ ██ ██ ██ ██ ▄█▀▀▀█▄ ███▄ ██ ██",
28
- "██ ▄█▄ ██ ██▀▀▀██ ██ ▀█▄ ██ ██ ██ ▄█▄ ██ ██▀▀▀██ ██ ▀█▄ ██ ██",
29
- " ▀▀▀ ▀▀▀ ▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀▀ ▀▀▀ ▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀",
30
- ];
31
-
32
- const LOGO_WIDTH = 65;
33
-
34
- /** The brand accent, `#04d916`. */
35
- const ACCENT = [4, 217, 22];
36
-
37
- /** How far the first and last rows are mixed toward white and black. */
38
- const LIGHTEN = 0.42;
39
- const DARKEN = 0.35;
40
-
41
- /**
42
- * The ramp stop for one row: a tint of the accent at the top, the accent itself
43
- * in the middle, a shade of it at the bottom — so the wordmark reads as one
44
- * object lit from above.
45
- *
46
- * Interpolated from the row count rather than written out, so the art and the
47
- * ramp cannot drift apart when either changes.
48
- */
49
- function rampStop(index, rows) {
50
- // -1 on the first row, 0 in the middle, +1 on the last.
51
- const position = rows === 1 ? 0 : (index / (rows - 1)) * 2 - 1;
52
- if (position <= 0) {
53
- const mix = LIGHTEN * -position;
54
- return ACCENT.map((channel) => Math.round(channel + (255 - channel) * mix));
55
- }
56
- return ACCENT.map((channel) => Math.round(channel * (1 - DARKEN * position)));
57
- }
58
-
59
- /** 24-bit colour is used only where the terminal says it has it. */
60
- const truecolor = ["truecolor", "24bit"].includes(process.env.COLORTERM ?? "");
61
-
62
- /** The six levels each channel can take in the xterm 256-colour cube. */
63
- const CUBE_LEVELS = [0, 95, 135, 175, 215, 255];
64
-
65
- /** The nearest cube entry, for a terminal that does not announce 24-bit colour. */
66
- function cube([r, g, b]) {
67
- const nearest = (channel) =>
68
- CUBE_LEVELS.reduce(
69
- (best, _, index) =>
70
- Math.abs(CUBE_LEVELS[index] - channel) < Math.abs(CUBE_LEVELS[best] - channel) ? index : best,
71
- 0,
72
- );
73
- return 16 + 36 * nearest(r) + 6 * nearest(g) + nearest(b);
74
- }
75
-
76
- /** The foreground escape for one row of the ramp. */
77
- function ramp(index, rows) {
78
- const rgb = rampStop(index, rows);
79
- if (!truecolor) {
80
- return `${ESC}[38;5;${cube(rgb)}m`;
81
- }
82
- return `${ESC}[38;2;${rgb.join(";")}m`;
83
- }
84
-
85
- /**
86
- * Print the banner a command opens with.
87
- *
88
- * The art needs 65 columns and a terminal that wants colour. A narrower one, a
89
- * pipe, a CI log or `NO_COLOR` gets the wordmark on one line instead — the same
90
- * information, and nothing that turns into wrapped garbage in a build log.
91
- */
92
- export function banner(version) {
93
- // `||`, not `??`: a pty whose window size was never set reports 0 columns,
94
- // which is unknown rather than narrow.
95
- const columns = process.stdout.columns || 80;
96
- if (!useColor || columns < LOGO_WIDTH) {
97
- // The wordmark still carries the accent — it is the same banner, narrowed.
98
- const mark = useColor ? `${ramp(0, 1)}${ESC}[1mwaniwani${ESC}[0m` : "waniwani";
99
- console.log(`\n${mark} ${dim(`v${version}`)}`);
100
- return;
101
- }
102
- console.log("");
103
- for (const [index, line] of LOGO.entries()) {
104
- console.log(`${ramp(index, LOGO.length)}${line}${ESC}[0m`);
105
- }
106
- console.log(dim(`v${version}`.padStart(LOGO_WIDTH)));
107
- console.log("");
108
- }
109
-
110
- /**
111
- * One URL a command hands the developer, in the shape every command uses for
112
- * them. The label is padded so a list of endpoints aligns whether the framework
113
- * printed the line or this CLI did.
114
- */
115
- export function endpoint(label, url) {
116
- return ` ${dim(label.padEnd(8))} ${green(url)}`;
117
- }
118
-
119
- function printGroup(entries, marker, color) {
120
- const byFile = new Map();
121
- for (const entry of entries) {
122
- const list = byFile.get(entry.where) ?? [];
123
- list.push(entry);
124
- byFile.set(entry.where, list);
125
- }
126
-
127
- for (const [where, list] of byFile) {
128
- console.log(` ${bold(where)}`);
129
- for (const entry of list) {
130
- console.log(` ${color(marker)} ${entry.message}`);
131
- if (entry.hint) {
132
- console.log(` ${dim(entry.hint)}`);
133
- }
134
- }
135
- console.log("");
136
- }
137
- }
138
-
139
- export function printReport(app, report) {
140
- if (!report.ok) {
141
- console.log(`\n${red("✗")} ${bold("Build check failed")}\n`);
142
- printGroup(report.errors, "└", red);
143
- if (report.warnings.length > 0) {
144
- printGroup(report.warnings, "└", yellow);
145
- }
146
- return;
147
- }
148
-
149
- const counts = [
150
- [app.widgets.length, "widget"],
151
- [app.tools.length, "tool"],
152
- [app.flows.length, "flow"],
153
- [app.endpoints.length, "endpoint"],
154
- ]
155
- .filter(([count]) => count > 0)
156
- .map(([count, label]) => `${count} ${label}${count === 1 ? "" : "s"}`);
157
-
158
- console.log(`${green("✓")} ${bold("Build check passed")} ${dim(`— ${counts.join(", ")}`)}`);
159
-
160
- for (const widget of app.widgets) {
161
- console.log(` ${dim("widget")} ${widget.name}`);
162
- }
163
- for (const tool of app.tools) {
164
- console.log(` ${dim("tool ")} ${tool.name}`);
165
- }
166
- for (const flow of app.flows) {
167
- console.log(` ${dim("flow ")} ${flow.name}`);
168
- }
169
- // The path, not the filename: what a widget writes into a `fetch()` is the
170
- // thing worth checking against this line.
171
- for (const endpoint of app.endpoints) {
172
- console.log(` ${dim("api ")} ${endpoint.path}`);
173
- }
174
- if (report.warnings.length > 0) {
175
- console.log("");
176
- printGroup(report.warnings, "└", yellow);
177
- }
178
- }
package/cli/scan.mjs DELETED
@@ -1,112 +0,0 @@
1
- /**
2
- * Discover an app by walking its folders. Convention, not configuration.
3
- *
4
- * waniwani.config.ts
5
- * tools/<name>.ts
6
- * widgets/<name>/{widget.ts,ui.tsx}
7
- * flows/<name>.ts
8
- * api/<path>.ts
9
- *
10
- * There is no CSS in that list. Styling is Tailwind, from the distribution
11
- * template's `src/index.css` — its `@theme` tokens and its `dark` variant — and
12
- * a widget writes utility classes in `ui.tsx`. Stray `styles.css` files are
13
- * collected only so the build check can tell an author they are dead.
14
- */
15
-
16
- import { existsSync, readdirSync, statSync } from "node:fs";
17
- import { basename, extname, join } from "node:path";
18
-
19
- const CODE_EXT = new Set([".ts", ".tsx", ".mts"]);
20
-
21
- function listFiles(dir) {
22
- if (!existsSync(dir)) return [];
23
- return readdirSync(dir)
24
- .filter((entry) => !entry.startsWith(".") && !entry.startsWith("_"))
25
- .map((entry) => join(dir, entry))
26
- .filter((path) => statSync(path).isFile());
27
- }
28
-
29
- function listDirs(dir) {
30
- if (!existsSync(dir)) return [];
31
- return readdirSync(dir)
32
- .filter((entry) => !entry.startsWith(".") && !entry.startsWith("_"))
33
- .map((entry) => join(dir, entry))
34
- .filter((path) => statSync(path).isDirectory());
35
- }
36
-
37
- function stripExt(path) {
38
- return basename(path, extname(path));
39
- }
40
-
41
- /**
42
- * Every code file under `dir`, depth first, as `{ file, segments }` where
43
- * `segments` is its path below `dir` with the extension gone.
44
- *
45
- * `api/` is the one convention folder that nests: an HTTP path has more than
46
- * one segment, and the only place it can come from without a registry is the
47
- * filesystem.
48
- */
49
- function listTree(dir, trail = []) {
50
- if (!existsSync(dir)) return [];
51
-
52
- return readdirSync(dir)
53
- .filter((entry) => !entry.startsWith(".") && !entry.startsWith("_"))
54
- .flatMap((entry) => {
55
- const path = join(dir, entry);
56
- if (statSync(path).isDirectory()) {
57
- return listTree(path, [...trail, entry]);
58
- }
59
- if (!CODE_EXT.has(extname(path))) return [];
60
- return [{ file: path, segments: [...trail, stripExt(path)] }];
61
- });
62
- }
63
-
64
- /**
65
- * The URL an endpoint file is served at: its position under the app root,
66
- * `/api` included, since that is the folder's name.
67
- *
68
- * `index` names the directory itself, so `api/cal/index.ts` answers `/api/cal`
69
- * — the one place a filename is not taken verbatim, and the convention every
70
- * web framework already uses.
71
- */
72
- function endpointPath(segments) {
73
- const parts = segments.at(-1) === "index" ? segments.slice(0, -1) : segments;
74
- return `/api/${parts.join("/")}`.replace(/\/$/, "") || "/api";
75
- }
76
-
77
- export function scanApp(root) {
78
- const configFile = [join(root, "waniwani.config.ts"), join(root, "waniwani.config.js")].find(
79
- existsSync,
80
- );
81
-
82
- const tools = listFiles(join(root, "tools"))
83
- .filter((file) => CODE_EXT.has(extname(file)))
84
- .map((file) => ({ name: stripExt(file), file }));
85
-
86
- const widgets = listDirs(join(root, "widgets")).map((dir) => {
87
- const name = basename(dir);
88
- const contract = [join(dir, "widget.ts"), join(dir, "widget.tsx")].find(existsSync);
89
- const ui = [join(dir, "ui.tsx"), join(dir, "ui.jsx")].find(existsSync);
90
- return { name, dir, contract, ui };
91
- });
92
-
93
- const flows = listFiles(join(root, "flows"))
94
- .filter((file) => CODE_EXT.has(extname(file)))
95
- .map((file) => ({ name: stripExt(file), file }));
96
-
97
- const endpoints = listTree(join(root, "api")).map(({ file, segments }) => ({
98
- path: endpointPath(segments),
99
- segments,
100
- file,
101
- }));
102
-
103
- // The two paths an author is most likely to expect the kit to pick up. It
104
- // imports neither, so a file at either one is styling that never reaches the
105
- // browser — the kind of silent no-op the build check exists to name.
106
- const strayStyles = [
107
- join(root, "styles.css"),
108
- ...widgets.map((widget) => join(widget.dir, "styles.css")),
109
- ].filter(existsSync);
110
-
111
- return { root, configFile, tools, widgets, flows, endpoints, strayStyles };
112
- }
package/cli/template.mjs DELETED
@@ -1,190 +0,0 @@
1
- /**
2
- * Resolve the distribution template.
3
- *
4
- * The template is a separate public repo, consumed as-is. Nothing is forked
5
- * into this package: the generator downloads the repo at a pinned commit,
6
- * caches it, and copies the plumbing out of it. What ships to customers is the
7
- * same tree anyone can read on GitHub, clone, and deploy by hand.
8
- *
9
- * Sources:
10
- * github:OWNER/REPO#REF a GitHub repo at a branch, tag, or SHA (default)
11
- * /path/to/checkout a local clone, for working on the template itself
12
- *
13
- * Override per command with `--template <source>` or `WANIWANI_TEMPLATE`.
14
- */
15
-
16
- import { spawnSync } from "node:child_process";
17
- import { existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
18
- import { homedir, tmpdir } from "node:os";
19
- import { join, resolve } from "node:path";
20
-
21
- /**
22
- * A commit, not a branch.
23
- *
24
- * A published version of this package is frozen, and what it generates has to
25
- * be frozen with it. While the default was `beta`, the ref was re-resolved on
26
- * the customer's machine at every command, so a push to that branch changed the
27
- * output of every installed copy, and the assertions that catch a layout move
28
- * (`REQUIRED` and `assertSeam` in `./codegen.mjs`) fired in a customer's
29
- * terminal. Pinning a commit moves that failure into this repo's CI, where
30
- * `scripts/template-contract.mjs` builds a real app against the pin before a
31
- * release goes out.
32
- *
33
- * Bumping it is a one-line diff, and `scripts/bump-deps.mjs` proposes it. The
34
- * commit is on the template's `beta` branch: the generator is written against
35
- * that branch's layout (`vite.config.ts`, `src/server.ts`, `src/views/`), and
36
- * `main` is still the older `server/` + `web/` + `api/` split, which it cannot
37
- * absorb. An annotated tag can replace the SHA here whenever the template grows
38
- * one, with no change to the resolver.
39
- *
40
- * This commit is `beta`'s head, and it reads `search` and `tracking` off
41
- * `src/waniwani.ts` — the two fields `generateServerApp` emits from the app's
42
- * `defineApp({ ... })`. That pairing is the reason to bump the two together:
43
- * moving the pin here without the generator emitting those fields compiles to
44
- * TS2339, and the contract is what catches it.
45
- *
46
- * Working on the template itself does not need a release: pass `--template` or
47
- * set `WANIWANI_TEMPLATE` to a branch ref or a local checkout.
48
- */
49
- export const DEFAULT_TEMPLATE =
50
- "github:WaniWani-AI/mcp-distribution-template#c0d00e72a3733a5f42389731fe6bbaf7e0e07863";
51
-
52
- const CACHE_ROOT = join(homedir(), ".cache", "waniwani", "templates");
53
-
54
- /** `github:owner/repo#ref` -> its parts. */
55
- function parseGithub(source) {
56
- const match = /^github:([^/]+)\/([^#]+)(?:#(.+))?$/.exec(source);
57
- if (!match) return null;
58
- return { owner: match[1], repo: match[2], ref: match[3] ?? "main" };
59
- }
60
-
61
- /** A full commit SHA, which is already the thing a ref has to be resolved to. */
62
- function isSha(ref) {
63
- return /^[0-9a-f]{40}$/i.test(ref);
64
- }
65
-
66
- /**
67
- * Resolve a ref to a commit SHA, so a cache entry is content-addressed and two
68
- * builds of the same ref cannot silently differ.
69
- */
70
- async function resolveSha({ owner, repo, ref }) {
71
- // The pinned default is a commit, and asking the API to resolve a commit to
72
- // itself is a round trip that can rate-limit, fail, or go down. A cached
73
- // pin then needs no network at all, which is the point of pinning.
74
- if (isSha(ref)) return ref.toLowerCase();
75
-
76
- let response;
77
- try {
78
- response = await fetch(`https://api.github.com/repos/${owner}/${repo}/commits/${ref}`, {
79
- headers: { Accept: "application/vnd.github.sha" },
80
- });
81
- } catch (cause) {
82
- // Unreachable network. A cached template is a reasonable answer.
83
- throw Object.assign(new Error(`cannot reach GitHub: ${cause.message}`), { offline: true });
84
- }
85
-
86
- // A bad ref is the caller's mistake, not a network problem — falling back
87
- // to a cached template here would hide the typo.
88
- if (!response.ok) {
89
- throw new Error(
90
- `GitHub returned ${response.status} for ${owner}/${repo}@${ref}` +
91
- (response.status === 404 ? " — check the repo name and ref, or that it is public" : ""),
92
- );
93
- }
94
- return (await response.text()).trim();
95
- }
96
-
97
- async function download({ owner, repo, sha }, destination) {
98
- const response = await fetch(
99
- `https://codeload.github.com/${owner}/${repo}/tar.gz/${sha}`,
100
- );
101
- if (!response.ok) {
102
- throw new Error(`could not download ${owner}/${repo}@${sha}: ${response.status}`);
103
- }
104
-
105
- const archive = join(tmpdir(), `waniwani-template-${sha}.tar.gz`);
106
- writeFileSync(archive, Buffer.from(await response.arrayBuffer()));
107
-
108
- // Extract into a staging directory first, so an interrupted run cannot
109
- // leave a half-populated cache entry that later builds would trust.
110
- const staging = `${destination}.partial`;
111
- rmSync(staging, { recursive: true, force: true });
112
- mkdirSync(staging, { recursive: true });
113
-
114
- const result = spawnSync("tar", ["-xzf", archive, "-C", staging, "--strip-components=1"]);
115
- rmSync(archive, { force: true });
116
- if (result.status !== 0) {
117
- rmSync(staging, { recursive: true, force: true });
118
- throw new Error(`could not extract the template archive: ${result.stderr?.toString().trim()}`);
119
- }
120
-
121
- rmSync(destination, { recursive: true, force: true });
122
- spawnSync("mv", [staging, destination]);
123
- }
124
-
125
- /** The newest cache entry for a repo, used when the network is unavailable. */
126
- function newestCached(owner, repo) {
127
- if (!existsSync(CACHE_ROOT)) return null;
128
- const prefix = `${owner}-${repo}-`;
129
- const entries = readdirSync(CACHE_ROOT)
130
- .filter((name) => name.startsWith(prefix))
131
- .map((name) => ({ name, mtime: statSync(join(CACHE_ROOT, name)).mtimeMs }))
132
- .sort((a, b) => b.mtime - a.mtime);
133
- return entries[0] ? join(CACHE_ROOT, entries[0].name) : null;
134
- }
135
-
136
- /**
137
- * @param source `github:owner/repo#ref` or a local path
138
- * @returns `{ dir, source, ref, sha, cached, local }`
139
- */
140
- export async function resolveTemplate(source = DEFAULT_TEMPLATE) {
141
- const github = parseGithub(source);
142
-
143
- if (!github) {
144
- const dir = resolve(source);
145
- if (!existsSync(dir)) {
146
- throw new Error(`template not found: ${dir}`);
147
- }
148
- return { dir, source, local: true };
149
- }
150
-
151
- const { owner, repo, ref } = github;
152
- mkdirSync(CACHE_ROOT, { recursive: true });
153
-
154
- let sha;
155
- try {
156
- sha = await resolveSha(github);
157
- } catch (error) {
158
- const fallback = error.offline && newestCached(owner, repo);
159
- if (!fallback) throw error;
160
- return {
161
- dir: fallback,
162
- source,
163
- ref,
164
- sha: fallback.split("-").pop(),
165
- cached: true,
166
- offline: true,
167
- };
168
- }
169
-
170
- const dir = join(CACHE_ROOT, `${owner}-${repo}-${sha}`);
171
- const cached = existsSync(dir);
172
- if (!cached) {
173
- await download({ owner, repo, sha }, dir);
174
- }
175
-
176
- return { dir, source, ref, sha, cached };
177
- }
178
-
179
- /** One-line description of what a build used, for logs and provenance files. */
180
- export function describeTemplate(template) {
181
- if (template.local) return `${template.dir} (local)`;
182
- const state = template.offline ? "offline, cached" : template.cached ? "cached" : "downloaded";
183
- // A pinned source already carries the commit, so printing the source verbatim
184
- // would repeat all 40 characters of it next to the short form. Collapse to
185
- // the repo, and say that the commit came from a pin rather than a branch.
186
- const github = parseGithub(template.source);
187
- const pinned = github && isSha(github.ref);
188
- const origin = pinned ? `github:${github.owner}/${github.repo}` : template.source;
189
- return `${origin} @ ${template.sha?.slice(0, 7)} (${pinned ? `pinned, ${state}` : state})`;
190
- }