@waniwani/kit 0.1.0
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/LICENSE +21 -0
- package/README.md +566 -0
- package/cli/account.mjs +264 -0
- package/cli/codegen.mjs +1069 -0
- package/cli/framework.mjs +244 -0
- package/cli/index.mjs +563 -0
- package/cli/log.mjs +177 -0
- package/cli/scan.mjs +84 -0
- package/cli/template.mjs +152 -0
- package/cli/tunnel.mjs +140 -0
- package/cli/validate.mjs +248 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +71 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +212 -0
- package/dist/server.js.map +1 -0
- package/dist/web.d.ts +39 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +35 -0
- package/dist/web.js.map +1 -0
- package/package.json +88 -0
- package/src/index.ts +138 -0
- package/src/server.ts +285 -0
- package/src/web.tsx +68 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framework boundary.
|
|
3
|
+
*
|
|
4
|
+
* The generated project is built on a third-party MCP framework, and that
|
|
5
|
+
* framework's CLI narrates itself: a branded banner on `dev`, `build` and
|
|
6
|
+
* `start`, pointers at its own hosted tunnel and playground, a support link, and
|
|
7
|
+
* an analytics event per command. An app author is using `waniwani`, so none of
|
|
8
|
+
* that is output to hand them. Every place the framework speaks is intercepted
|
|
9
|
+
* here rather than scattered across the commands.
|
|
10
|
+
*
|
|
11
|
+
* Three seams do the work:
|
|
12
|
+
*
|
|
13
|
+
* dev `--plain` drops the interactive UI and writes each diagnostic to
|
|
14
|
+
* stderr as one plain line, which makes the stream rewritable.
|
|
15
|
+
* build the step list behind the interactive UI is plain data, so this CLI
|
|
16
|
+
* loads it, drives the steps, and prints the progress itself.
|
|
17
|
+
* start the banner is ordinary `console.log`, so stdout is rewritten the
|
|
18
|
+
* same way as dev's stderr.
|
|
19
|
+
*
|
|
20
|
+
* What no amount of output rewriting reaches: the devtools page served at the
|
|
21
|
+
* dev server's root is the framework's own UI, and the generated project names
|
|
22
|
+
* the framework in its `tsconfig`, its dependencies, and its type-output
|
|
23
|
+
* directory. Nothing here is concealment — the dependency is declared in
|
|
24
|
+
* `package.json` like any other. It is a house style: one CLI does the talking.
|
|
25
|
+
*
|
|
26
|
+
* Module specifiers, the dependency name, the telemetry variable and the
|
|
27
|
+
* patterns that have to match the framework's own strings are load-bearing and
|
|
28
|
+
* stay as they are. Prose does not.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { existsSync } from "node:fs";
|
|
32
|
+
import { dirname, join } from "node:path";
|
|
33
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
34
|
+
import { bold, dim, endpoint, green, red, yellow } from "./log.mjs";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The framework package root. It may be hoisted anywhere above us, so resolve
|
|
38
|
+
* it rather than guess: its `tsconfig` export is the only one that maps to a
|
|
39
|
+
* file at the package root, which makes it a reliable anchor for the directory.
|
|
40
|
+
*/
|
|
41
|
+
export function frameworkDir() {
|
|
42
|
+
return dirname(fileURLToPath(import.meta.resolve("skybridge/tsconfig")));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function frameworkBin() {
|
|
46
|
+
return join(frameworkDir(), "bin", "run.js");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Environment applied to every framework subprocess.
|
|
51
|
+
*
|
|
52
|
+
* The framework's CLI reports each command to a third-party analytics endpoint,
|
|
53
|
+
* keyed by a machine id it persists in the user's home directory. Running
|
|
54
|
+
* someone's build is not consent to that, so it is off by both switches the
|
|
55
|
+
* framework honours.
|
|
56
|
+
*/
|
|
57
|
+
export const FRAMEWORK_ENV = {
|
|
58
|
+
SKYBRIDGE_TELEMETRY_DISABLED: "1",
|
|
59
|
+
DO_NOT_TRACK: "1",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Match against the text, not the colours the framework wrapped it in.
|
|
64
|
+
*
|
|
65
|
+
* ESC is built from its char code rather than written as a raw byte or an
|
|
66
|
+
* escape sequence. The byte is invisible in source and survives no copy or
|
|
67
|
+
* reformat reliably; an escape sequence gets normalised back into the byte by
|
|
68
|
+
* some tooling. Either way the loss is silent, and every rule below then fails
|
|
69
|
+
* to match any coloured line.
|
|
70
|
+
*/
|
|
71
|
+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A framework command name leaking through an otherwise fine line — an error
|
|
75
|
+
* hint, a nodemon echo. The commands map one to one, so the name is swapped for
|
|
76
|
+
* ours rather than the line dropped.
|
|
77
|
+
*/
|
|
78
|
+
function reword(line) {
|
|
79
|
+
return line.replace(/\bskybridge (build|dev|start)\b/g, "waniwani $1");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* How each line of `dev --plain` diagnostics is rewritten.
|
|
84
|
+
*
|
|
85
|
+
* The framework's stderr is a closed set: a banner, three or four URLs, the
|
|
86
|
+
* tunnel's state, restart notices, and TypeScript errors. The app's own stderr
|
|
87
|
+
* comes through the same pipe, so an unmatched line is passed through — except
|
|
88
|
+
* under the emoji prefixes the framework owns, which are dropped rather than
|
|
89
|
+
* guessed at, since a `starting` tunnel message is verbatim output from a
|
|
90
|
+
* subprocess of its own and can say anything.
|
|
91
|
+
*
|
|
92
|
+
* The framework's own tunnel is one of those lines. No command here asks for it,
|
|
93
|
+
* so what arrives is its offer of one, and the emoji it carries drops the line.
|
|
94
|
+
* A public hostname comes from `waniwani tunnel` instead (see ./tunnel.mjs).
|
|
95
|
+
*
|
|
96
|
+
* Each pattern leads with `\W*` to absorb whatever emoji prefixes the line and
|
|
97
|
+
* ends at `$`, so a rule reads the framework's whole line and can't fire on an
|
|
98
|
+
* app log that happens to open with the same words.
|
|
99
|
+
*/
|
|
100
|
+
const BANNER_AND_URLS = [
|
|
101
|
+
// Matched by shape — `<name> v1.2.3` alone on a line — which costs nothing in
|
|
102
|
+
// precision and keeps the framework's name out of this file.
|
|
103
|
+
[/^\W*\S+ v\d+\.\d+\.\d+\S*$/u, () => null],
|
|
104
|
+
[/^\W*(\d+) in use, running on (\S+)$/u, (m) => `${endpoint("server", m[2])} ${dim(`(${m[1]} in use)`)}`],
|
|
105
|
+
[/^\W*Running on (\S+)$/u, (m) => endpoint("server", m[1])],
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
const DEV_RULES = [
|
|
109
|
+
...BANNER_AND_URLS,
|
|
110
|
+
// The devtools page is the framework's own UI and nobody here reaches for it,
|
|
111
|
+
// so its URL is dropped rather than restated.
|
|
112
|
+
[/^\W*Test locally with DevTools: \S+$/u, () => null],
|
|
113
|
+
[/^\W*Server restarted due to file changes: (.*)$/u, (m) => dim(`[waniwani] restarted — ${m[1]}`)],
|
|
114
|
+
[/^\W*TypeScript errors found:\W*$/u, () => `${yellow("!")} ${bold("TypeScript errors")}`],
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
/** `start` prints its banner to stdout without ever mounting the UI. */
|
|
118
|
+
const START_RULES = BANNER_AND_URLS;
|
|
119
|
+
|
|
120
|
+
/** Emoji the framework prefixes its own chrome with. */
|
|
121
|
+
const FRAMEWORK_PREFIX = /^(?:⛰|🏠|🌍|🛟|→)/u;
|
|
122
|
+
|
|
123
|
+
function rewriter(rules, { dropUnmatchedChrome }) {
|
|
124
|
+
return (line) => {
|
|
125
|
+
const text = line.replace(ANSI, "").trimEnd();
|
|
126
|
+
for (const [pattern, format] of rules) {
|
|
127
|
+
const match = pattern.exec(text);
|
|
128
|
+
if (match) return format(match);
|
|
129
|
+
}
|
|
130
|
+
if (dropUnmatchedChrome && FRAMEWORK_PREFIX.test(text)) return null;
|
|
131
|
+
return reword(line);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Feed a subprocess stream through `rewrite` one whole line at a time, writing
|
|
137
|
+
* the result to `out`. A rewrite of `null` drops the line.
|
|
138
|
+
*
|
|
139
|
+
* Chunk boundaries fall anywhere, so a partial line is held until its newline
|
|
140
|
+
* arrives; `flush` emits whatever is left when the stream closes.
|
|
141
|
+
*/
|
|
142
|
+
function lineFilter(rewrite, out) {
|
|
143
|
+
let buffered = "";
|
|
144
|
+
const emit = (line) => {
|
|
145
|
+
const rewritten = rewrite(line);
|
|
146
|
+
if (rewritten !== null) {
|
|
147
|
+
out.write(`${rewritten}\n`);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
write(chunk) {
|
|
152
|
+
buffered += chunk;
|
|
153
|
+
const lines = buffered.split("\n");
|
|
154
|
+
buffered = lines.pop() ?? "";
|
|
155
|
+
for (const line of lines) emit(line);
|
|
156
|
+
},
|
|
157
|
+
flush() {
|
|
158
|
+
if (buffered) {
|
|
159
|
+
const line = buffered;
|
|
160
|
+
buffered = "";
|
|
161
|
+
emit(line);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Diagnostics stay on stderr, so the app's own stdout is never in the way. */
|
|
168
|
+
export function devFilter() {
|
|
169
|
+
return lineFilter(rewriter(DEV_RULES, { dropUnmatchedChrome: true }), process.stderr);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function startFilter() {
|
|
173
|
+
return lineFilter(rewriter(START_RULES, { dropUnmatchedChrome: false }), process.stdout);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The framework's build as a list of labelled steps, or null if it can't be
|
|
178
|
+
* reached in that form.
|
|
179
|
+
*
|
|
180
|
+
* The step list sits outside the package's `exports` map, so it is reachable
|
|
181
|
+
* only by absolute path — the same coupling this CLI already accepts for the
|
|
182
|
+
* framework's `bin`, against a version codegen pins exactly. When the module or
|
|
183
|
+
* its shape has moved, null tells the caller to shell out to the framework's own
|
|
184
|
+
* build command instead: a build that narrates itself beats no build at all.
|
|
185
|
+
*/
|
|
186
|
+
export async function loadBuildSteps(root) {
|
|
187
|
+
const path = join(frameworkDir(), "dist", "cli", "build-steps.js");
|
|
188
|
+
if (!existsSync(path)) return null;
|
|
189
|
+
|
|
190
|
+
let getCommandSteps;
|
|
191
|
+
try {
|
|
192
|
+
({ getCommandSteps } = await import(pathToFileURL(path).href));
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
if (typeof getCommandSteps !== "function") return null;
|
|
197
|
+
|
|
198
|
+
// A throw from here on is a real build failure — a broken vite config, two
|
|
199
|
+
// views with one name — and belongs to the caller, not to the fallback.
|
|
200
|
+
const steps = await getCommandSteps(root);
|
|
201
|
+
const usable =
|
|
202
|
+
Array.isArray(steps) &&
|
|
203
|
+
steps.length > 0 &&
|
|
204
|
+
steps.every(
|
|
205
|
+
(step) =>
|
|
206
|
+
step && typeof step.label === "string" && (typeof step.run === "function" || typeof step.command === "string"),
|
|
207
|
+
);
|
|
208
|
+
return usable ? steps : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Run the loaded steps, one printed line each. `runShell` is injected so the
|
|
213
|
+
* caller keeps ownership of how subprocesses are spawned — PATH, environment,
|
|
214
|
+
* which stream they inherit.
|
|
215
|
+
*/
|
|
216
|
+
export async function runBuildSteps(steps, { root, runShell }) {
|
|
217
|
+
// The steps read and write relative to the working directory, which is the
|
|
218
|
+
// generated project when the framework runs them itself.
|
|
219
|
+
const previousCwd = process.cwd();
|
|
220
|
+
process.chdir(root);
|
|
221
|
+
try {
|
|
222
|
+
for (const step of steps) {
|
|
223
|
+
try {
|
|
224
|
+
if (step.run) {
|
|
225
|
+
await step.run();
|
|
226
|
+
}
|
|
227
|
+
if (step.command) {
|
|
228
|
+
const code = await runShell(step.command);
|
|
229
|
+
if (code !== 0) {
|
|
230
|
+
console.error(` ${red("✗")} ${step.label}`);
|
|
231
|
+
return code;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error(` ${red("✗")} ${step.label}`);
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
console.log(` ${green("✓")} ${dim(step.label)}`);
|
|
239
|
+
}
|
|
240
|
+
return 0;
|
|
241
|
+
} finally {
|
|
242
|
+
process.chdir(previousCwd);
|
|
243
|
+
}
|
|
244
|
+
}
|