@frockbot/applet-sdk 0.0.0 → 0.3.13
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 +77 -1
- package/dist/cli.mjs +1007 -0
- package/package.json +51 -5
- package/src/cli/bin.ts +128 -0
- package/src/cli/build.ts +181 -0
- package/src/cli/check.ts +134 -0
- package/src/cli/dev.ts +84 -0
- package/src/cli/main.ts +17 -0
- package/src/cli/manifest.ts +58 -0
- package/src/cli/new.ts +74 -0
- package/src/cli/paths.ts +98 -0
- package/src/cli/runtime.ts +123 -0
- package/src/client/collections.ts +72 -0
- package/src/client/index.ts +203 -0
- package/src/client/transport.ts +334 -0
- package/src/kit/README.md +130 -0
- package/src/kit/index.tsx +427 -0
- package/src/kit/styles.ts +200 -0
- package/src/lint/index.ts +148 -0
- package/src/lint/rules.ts +394 -0
- package/src/protocol/index.ts +411 -0
- package/src/schema/index.ts +436 -0
- package/src/server/applet.ts +399 -0
- package/src/server/index.ts +53 -0
- package/src/server/session.ts +156 -0
- package/src/server/store.ts +398 -0
- package/template/README.md +37 -0
- package/template/applet.json +5 -0
- package/template/server.ts +46 -0
- package/template/ui.tsx +113 -0
- package/types/cloudflare-workers.d.ts +55 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/bin.ts
|
|
4
|
+
import { resolve as resolve3 } from "node:path";
|
|
5
|
+
|
|
6
|
+
// src/lint/index.ts
|
|
7
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
8
|
+
import { join, relative, resolve } from "node:path";
|
|
9
|
+
import { ESLint } from "eslint";
|
|
10
|
+
import tseslint from "typescript-eslint";
|
|
11
|
+
|
|
12
|
+
// src/lint/rules.ts
|
|
13
|
+
function child(node, field) {
|
|
14
|
+
const value = node?.[field];
|
|
15
|
+
return value && typeof value === "object" ? value : void 0;
|
|
16
|
+
}
|
|
17
|
+
function name(node) {
|
|
18
|
+
if (!node) return void 0;
|
|
19
|
+
if (node.type === "Identifier" && typeof node.name === "string")
|
|
20
|
+
return node.name;
|
|
21
|
+
if (node.type === "Literal" && typeof node.value === "string")
|
|
22
|
+
return node.value;
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
function list(node, field) {
|
|
26
|
+
const value = node?.[field];
|
|
27
|
+
return Array.isArray(value) ? value : [];
|
|
28
|
+
}
|
|
29
|
+
var FUNCTIONAL_COLOR = /#[0-9a-fA-F]{3,8}\b|\brgba?\s*\(|\bhsla?\s*\(|\bcolor-mix\s*\(/;
|
|
30
|
+
var NAMED_COLORS = /* @__PURE__ */ new Set([
|
|
31
|
+
"aqua",
|
|
32
|
+
"black",
|
|
33
|
+
"blue",
|
|
34
|
+
"brown",
|
|
35
|
+
"cyan",
|
|
36
|
+
"fuchsia",
|
|
37
|
+
"gold",
|
|
38
|
+
"gray",
|
|
39
|
+
"green",
|
|
40
|
+
"grey",
|
|
41
|
+
"indigo",
|
|
42
|
+
"lime",
|
|
43
|
+
"magenta",
|
|
44
|
+
"maroon",
|
|
45
|
+
"navy",
|
|
46
|
+
"olive",
|
|
47
|
+
"orange",
|
|
48
|
+
"pink",
|
|
49
|
+
"purple",
|
|
50
|
+
"red",
|
|
51
|
+
"silver",
|
|
52
|
+
"teal",
|
|
53
|
+
"violet",
|
|
54
|
+
"white",
|
|
55
|
+
"yellow"
|
|
56
|
+
]);
|
|
57
|
+
var COLOR_PROPERTIES = /* @__PURE__ */ new Set([
|
|
58
|
+
"color",
|
|
59
|
+
"background",
|
|
60
|
+
"backgroundColor",
|
|
61
|
+
"borderColor",
|
|
62
|
+
"borderTopColor",
|
|
63
|
+
"borderRightColor",
|
|
64
|
+
"borderBottomColor",
|
|
65
|
+
"borderLeftColor",
|
|
66
|
+
"outlineColor",
|
|
67
|
+
"fill",
|
|
68
|
+
"stroke",
|
|
69
|
+
"caretColor",
|
|
70
|
+
"accentColor",
|
|
71
|
+
"textDecorationColor",
|
|
72
|
+
"boxShadow",
|
|
73
|
+
"textShadow"
|
|
74
|
+
]);
|
|
75
|
+
function anchoredToToken(text) {
|
|
76
|
+
return /var\(\s*--frockbot-[a-z-]+/.test(text);
|
|
77
|
+
}
|
|
78
|
+
var noRawColors = {
|
|
79
|
+
meta: {
|
|
80
|
+
type: "problem",
|
|
81
|
+
docs: {
|
|
82
|
+
description: "Colours come from the nine --frockbot-* theme tokens, never from a literal"
|
|
83
|
+
},
|
|
84
|
+
schema: []
|
|
85
|
+
},
|
|
86
|
+
create(context) {
|
|
87
|
+
const flag = (node, text) => {
|
|
88
|
+
if (anchoredToToken(text)) return;
|
|
89
|
+
if (!FUNCTIONAL_COLOR.test(text)) return;
|
|
90
|
+
context.report({
|
|
91
|
+
node,
|
|
92
|
+
message: "Use a --frockbot-* theme token instead of a colour literal (the kit's components already do)."
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
Literal(node) {
|
|
97
|
+
if (typeof node.value === "string") flag(node, node.value);
|
|
98
|
+
},
|
|
99
|
+
TemplateElement(node) {
|
|
100
|
+
const value = child(node, "value");
|
|
101
|
+
const raw = value?.raw;
|
|
102
|
+
if (typeof raw === "string") flag(node, raw);
|
|
103
|
+
},
|
|
104
|
+
Property(node) {
|
|
105
|
+
const key = name(child(node, "key"));
|
|
106
|
+
if (!key || !COLOR_PROPERTIES.has(key)) return;
|
|
107
|
+
const value = child(node, "value");
|
|
108
|
+
if (value?.type !== "Literal" || typeof value.value !== "string")
|
|
109
|
+
return;
|
|
110
|
+
const text = value.value.trim().toLowerCase();
|
|
111
|
+
if (anchoredToToken(text) || !NAMED_COLORS.has(text)) return;
|
|
112
|
+
context.report({
|
|
113
|
+
node: value,
|
|
114
|
+
message: `"${text}" is a raw colour; use a --frockbot-* theme token.`
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var FORBIDDEN_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
121
|
+
"XMLHttpRequest",
|
|
122
|
+
"WebSocket",
|
|
123
|
+
"EventSource"
|
|
124
|
+
]);
|
|
125
|
+
var noNetwork = {
|
|
126
|
+
meta: {
|
|
127
|
+
type: "problem",
|
|
128
|
+
docs: {
|
|
129
|
+
description: "An Applet reaches the outside world through its tools, never directly"
|
|
130
|
+
},
|
|
131
|
+
schema: []
|
|
132
|
+
},
|
|
133
|
+
create(context) {
|
|
134
|
+
const complain = (node, what) => context.report({
|
|
135
|
+
node,
|
|
136
|
+
message: `${what} is not available to an Applet: the loader runs it with no outbound network. Add a tool on the server, or use the Applet socket.`
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
CallExpression(node) {
|
|
140
|
+
const callee = child(node, "callee");
|
|
141
|
+
if (name(callee) === "fetch") {
|
|
142
|
+
complain(node, "fetch()");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (callee?.type !== "MemberExpression") return;
|
|
146
|
+
const property = name(child(callee, "property"));
|
|
147
|
+
const object = name(child(callee, "object"));
|
|
148
|
+
if (property === "fetch" && (object === "window" || object === "globalThis")) {
|
|
149
|
+
complain(node, "fetch()");
|
|
150
|
+
}
|
|
151
|
+
if (property === "sendBeacon" && object === "navigator") {
|
|
152
|
+
complain(node, "navigator.sendBeacon()");
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
NewExpression(node) {
|
|
156
|
+
const callee = name(child(node, "callee"));
|
|
157
|
+
if (callee && FORBIDDEN_CONSTRUCTORS.has(callee))
|
|
158
|
+
complain(node, `new ${callee}`);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
function importAllowed(specifier) {
|
|
164
|
+
if (specifier.startsWith(".")) return true;
|
|
165
|
+
if (specifier === "react" || specifier.startsWith("react/")) return true;
|
|
166
|
+
return specifier === "@frockbot/applet-sdk" || specifier.startsWith("@frockbot/applet-sdk/");
|
|
167
|
+
}
|
|
168
|
+
var allowedImports = {
|
|
169
|
+
meta: {
|
|
170
|
+
type: "problem",
|
|
171
|
+
docs: {
|
|
172
|
+
description: "An Applet bundles from @frockbot/applet-sdk, react, and its own files only"
|
|
173
|
+
},
|
|
174
|
+
schema: []
|
|
175
|
+
},
|
|
176
|
+
create(context) {
|
|
177
|
+
const check = (node, source) => {
|
|
178
|
+
const specifier = source?.type === "Literal" ? source.value : void 0;
|
|
179
|
+
if (typeof specifier !== "string" || importAllowed(specifier)) return;
|
|
180
|
+
context.report({
|
|
181
|
+
node,
|
|
182
|
+
message: `"${specifier}" cannot be imported: an Applet may import @frockbot/applet-sdk/*, react, and its own relative files.`
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
ImportDeclaration: (node) => check(node, child(node, "source")),
|
|
187
|
+
ExportNamedDeclaration: (node) => check(node, child(node, "source")),
|
|
188
|
+
ExportAllDeclaration: (node) => check(node, child(node, "source")),
|
|
189
|
+
ImportExpression: (node) => check(node, child(node, "source")),
|
|
190
|
+
CallExpression(node) {
|
|
191
|
+
if (name(child(node, "callee")) !== "require") return;
|
|
192
|
+
check(node, list(node, "arguments")[0]);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
function isAppletClassBody(node) {
|
|
198
|
+
const parent = child(node, "parent");
|
|
199
|
+
return name(child(parent, "superClass")) === "Applet";
|
|
200
|
+
}
|
|
201
|
+
function declaredProperty(node, field) {
|
|
202
|
+
for (const member of list(node, "body")) {
|
|
203
|
+
if (member.type !== "PropertyDefinition") continue;
|
|
204
|
+
if (name(child(member, "key")) === field) return member;
|
|
205
|
+
}
|
|
206
|
+
return void 0;
|
|
207
|
+
}
|
|
208
|
+
var tablesViaTable = {
|
|
209
|
+
meta: {
|
|
210
|
+
type: "problem",
|
|
211
|
+
docs: {
|
|
212
|
+
description: "Tables are declared with table() so the SDK can derive DDL"
|
|
213
|
+
},
|
|
214
|
+
schema: []
|
|
215
|
+
},
|
|
216
|
+
create(context) {
|
|
217
|
+
const objectsByName = /* @__PURE__ */ new Map();
|
|
218
|
+
const classBodies = [];
|
|
219
|
+
return {
|
|
220
|
+
VariableDeclarator(node) {
|
|
221
|
+
const identifier = name(child(node, "id"));
|
|
222
|
+
const init = child(node, "init");
|
|
223
|
+
if (identifier && init?.type === "ObjectExpression") {
|
|
224
|
+
objectsByName.set(identifier, init);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
ClassBody(node) {
|
|
228
|
+
if (isAppletClassBody(node)) classBodies.push(node);
|
|
229
|
+
},
|
|
230
|
+
"Program:exit"() {
|
|
231
|
+
for (const body of classBodies) {
|
|
232
|
+
const property = declaredProperty(body, "tables");
|
|
233
|
+
if (!property) continue;
|
|
234
|
+
const declared = child(property, "value");
|
|
235
|
+
const value = declared?.type === "Identifier" ? objectsByName.get(name(declared) ?? "") : declared;
|
|
236
|
+
if (value?.type !== "ObjectExpression") {
|
|
237
|
+
context.report({
|
|
238
|
+
node: property,
|
|
239
|
+
message: "`tables` must be an object literal of table({ \u2026 }) declarations."
|
|
240
|
+
});
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
for (const entry of list(value, "properties")) {
|
|
244
|
+
const declaration = child(entry, "value");
|
|
245
|
+
if (declaration?.type === "CallExpression" && name(child(declaration, "callee")) === "table") {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
context.report({
|
|
249
|
+
node: entry,
|
|
250
|
+
message: "Each table must be declared with table({ \u2026 }) from the SDK."
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
var toolsViaThisTool = {
|
|
259
|
+
meta: {
|
|
260
|
+
type: "problem",
|
|
261
|
+
docs: {
|
|
262
|
+
description: "Tools are declared with this.tool() so health() can report them"
|
|
263
|
+
},
|
|
264
|
+
schema: []
|
|
265
|
+
},
|
|
266
|
+
create(context) {
|
|
267
|
+
return {
|
|
268
|
+
ClassBody(node) {
|
|
269
|
+
if (!isAppletClassBody(node)) return;
|
|
270
|
+
const property = declaredProperty(node, "tools");
|
|
271
|
+
if (!property) return;
|
|
272
|
+
const value = child(property, "value");
|
|
273
|
+
if (value?.type !== "ObjectExpression") {
|
|
274
|
+
context.report({
|
|
275
|
+
node: property,
|
|
276
|
+
message: "`tools` must be an object literal of this.tool(\u2026) declarations."
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
for (const entry of list(value, "properties")) {
|
|
281
|
+
const declaration = child(entry, "value");
|
|
282
|
+
const callee = child(declaration, "callee");
|
|
283
|
+
if (declaration?.type === "CallExpression" && callee?.type === "MemberExpression" && child(callee, "object")?.type === "ThisExpression" && name(child(callee, "property")) === "tool") {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
context.report({
|
|
287
|
+
node: entry,
|
|
288
|
+
message: "Each tool must be declared with this.tool({ \u2026 }, handler)."
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
var appletRules = {
|
|
296
|
+
"no-raw-colors": noRawColors,
|
|
297
|
+
"no-network": noNetwork,
|
|
298
|
+
"allowed-imports": allowedImports,
|
|
299
|
+
"tables-via-table": tablesViaTable,
|
|
300
|
+
"tools-via-this-tool": toolsViaThisTool
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// src/lint/index.ts
|
|
304
|
+
function formatDiagnostic(diagnostic) {
|
|
305
|
+
return `${diagnostic.file}:${diagnostic.line}:${diagnostic.column} ${diagnostic.message}`;
|
|
306
|
+
}
|
|
307
|
+
var appletPlugin = {
|
|
308
|
+
rules: appletRules
|
|
309
|
+
};
|
|
310
|
+
function appletLintConfig() {
|
|
311
|
+
return [
|
|
312
|
+
{
|
|
313
|
+
files: ["**/*.ts", "**/*.tsx"],
|
|
314
|
+
languageOptions: {
|
|
315
|
+
parser: tseslint.parser,
|
|
316
|
+
parserOptions: {
|
|
317
|
+
ecmaVersion: 2023,
|
|
318
|
+
sourceType: "module",
|
|
319
|
+
ecmaFeatures: { jsx: true }
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
plugins: { applet: appletPlugin },
|
|
323
|
+
rules: {
|
|
324
|
+
"applet/no-raw-colors": "error",
|
|
325
|
+
"applet/no-network": "error",
|
|
326
|
+
"applet/allowed-imports": "error",
|
|
327
|
+
"applet/tables-via-table": "error",
|
|
328
|
+
"applet/tools-via-this-tool": "error"
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
];
|
|
332
|
+
}
|
|
333
|
+
var CSS_COLOR = /#[0-9a-fA-F]{3,8}\b|\brgba?\s*\([^)]*\)|\bhsla?\s*\([^)]*\)|\bcolor-mix\s*\(/g;
|
|
334
|
+
function lintCssText(text, file) {
|
|
335
|
+
const diagnostics = [];
|
|
336
|
+
const lines = text.split("\n");
|
|
337
|
+
lines.forEach((line, index) => {
|
|
338
|
+
if (line.trimStart().startsWith("/*")) return;
|
|
339
|
+
for (const match of line.matchAll(CSS_COLOR)) {
|
|
340
|
+
const start = line.lastIndexOf(";", match.index) + 1;
|
|
341
|
+
const end = line.indexOf(";", match.index);
|
|
342
|
+
const declaration = line.slice(start, end === -1 ? void 0 : end);
|
|
343
|
+
if (anchoredToToken(declaration)) continue;
|
|
344
|
+
diagnostics.push({
|
|
345
|
+
file,
|
|
346
|
+
line: index + 1,
|
|
347
|
+
column: match.index + 1,
|
|
348
|
+
message: `"${match[0]}" is a raw colour; use a --frockbot-* theme token (or a var(--frockbot-\u2026, fallback)).`,
|
|
349
|
+
severity: "error"
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
return diagnostics;
|
|
354
|
+
}
|
|
355
|
+
async function sourceFiles(directory, extensions) {
|
|
356
|
+
const found = [];
|
|
357
|
+
const walk = async (current) => {
|
|
358
|
+
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
359
|
+
if (entry.name === "node_modules" || entry.name === "dist") continue;
|
|
360
|
+
if (entry.name.startsWith(".")) continue;
|
|
361
|
+
const path = join(current, entry.name);
|
|
362
|
+
if (entry.isDirectory()) await walk(path);
|
|
363
|
+
else if (extensions.some((extension) => entry.name.endsWith(extension))) {
|
|
364
|
+
found.push(path);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
await walk(directory);
|
|
369
|
+
return found;
|
|
370
|
+
}
|
|
371
|
+
async function lintApplet(directory) {
|
|
372
|
+
const root = resolve(directory);
|
|
373
|
+
const eslint = new ESLint({
|
|
374
|
+
cwd: root,
|
|
375
|
+
overrideConfigFile: true,
|
|
376
|
+
overrideConfig: appletLintConfig(),
|
|
377
|
+
errorOnUnmatchedPattern: false
|
|
378
|
+
});
|
|
379
|
+
const results = await eslint.lintFiles(
|
|
380
|
+
await sourceFiles(root, [".ts", ".tsx"])
|
|
381
|
+
);
|
|
382
|
+
const diagnostics = [];
|
|
383
|
+
for (const result of results) {
|
|
384
|
+
for (const message of result.messages) {
|
|
385
|
+
diagnostics.push({
|
|
386
|
+
file: relative(root, result.filePath),
|
|
387
|
+
line: message.line ?? 1,
|
|
388
|
+
column: message.column ?? 1,
|
|
389
|
+
message: `${message.message}${message.ruleId ? ` (${message.ruleId})` : ""}`,
|
|
390
|
+
severity: message.severity === 2 ? "error" : "warning"
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (const path of await sourceFiles(root, [".css"])) {
|
|
395
|
+
diagnostics.push(
|
|
396
|
+
...lintCssText(await readFile(path, "utf8"), relative(root, path))
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
return diagnostics;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/cli/build.ts
|
|
403
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
404
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
405
|
+
import { join as join4 } from "node:path";
|
|
406
|
+
import { build as esbuild } from "esbuild";
|
|
407
|
+
|
|
408
|
+
// src/cli/manifest.ts
|
|
409
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
410
|
+
import { join as join2 } from "node:path";
|
|
411
|
+
|
|
412
|
+
// src/protocol/index.ts
|
|
413
|
+
var APPLET_CONTRACT_VERSION = 1;
|
|
414
|
+
var APPLET_FRAME_BYTE_LIMIT = 64 * 1024;
|
|
415
|
+
|
|
416
|
+
// src/cli/manifest.ts
|
|
417
|
+
function decodeDescriptor(input) {
|
|
418
|
+
if (!input || typeof input !== "object")
|
|
419
|
+
throw new Error("applet.json must be an object");
|
|
420
|
+
const value = input;
|
|
421
|
+
const id = value.id;
|
|
422
|
+
if (typeof id !== "string" || !/^[a-z][a-z0-9-]{0,31}$/.test(id)) {
|
|
423
|
+
throw new Error('applet.json "id" must match /^[a-z][a-z0-9-]{0,31}$/');
|
|
424
|
+
}
|
|
425
|
+
if (typeof value.displayName !== "string" || value.displayName.length === 0 || value.displayName.length > 64) {
|
|
426
|
+
throw new Error('applet.json "displayName" must be 1-64 characters');
|
|
427
|
+
}
|
|
428
|
+
if (value.contract !== APPLET_CONTRACT_VERSION) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`applet.json "contract" must be ${APPLET_CONTRACT_VERSION}`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
return { id, displayName: value.displayName, contract: 1 };
|
|
434
|
+
}
|
|
435
|
+
async function readDescriptor(directory) {
|
|
436
|
+
const path = join2(directory, "applet.json");
|
|
437
|
+
let text;
|
|
438
|
+
try {
|
|
439
|
+
text = await readFile2(path, "utf8");
|
|
440
|
+
} catch {
|
|
441
|
+
throw new Error(
|
|
442
|
+
`No applet.json in ${directory}; run \`applet new <name>\` first`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
return decodeDescriptor(JSON.parse(text));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/cli/paths.ts
|
|
449
|
+
import { readFileSync } from "node:fs";
|
|
450
|
+
import { createRequire } from "node:module";
|
|
451
|
+
import { dirname, join as join3 } from "node:path";
|
|
452
|
+
import { fileURLToPath } from "node:url";
|
|
453
|
+
var require2 = createRequire(import.meta.url);
|
|
454
|
+
function findSdkRoot() {
|
|
455
|
+
let directory = dirname(fileURLToPath(import.meta.url));
|
|
456
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
457
|
+
try {
|
|
458
|
+
const manifest = JSON.parse(
|
|
459
|
+
readFileSync(join3(directory, "package.json"), "utf8")
|
|
460
|
+
);
|
|
461
|
+
if (manifest.name === "@frockbot/applet-sdk") return `${directory}/`;
|
|
462
|
+
} catch {
|
|
463
|
+
}
|
|
464
|
+
const parent = dirname(directory);
|
|
465
|
+
if (parent === directory) break;
|
|
466
|
+
directory = parent;
|
|
467
|
+
}
|
|
468
|
+
throw new Error(
|
|
469
|
+
"the Applets SDK cannot find its own package root; reinstall @frockbot/applet-sdk"
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
var SDK_ROOT = findSdkRoot();
|
|
473
|
+
var SDK_ENTRIES = {
|
|
474
|
+
"@frockbot/applet-sdk/server": join3(SDK_ROOT, "src/server/index.ts"),
|
|
475
|
+
"@frockbot/applet-sdk/client": join3(SDK_ROOT, "src/client/index.ts"),
|
|
476
|
+
"@frockbot/applet-sdk/kit": join3(SDK_ROOT, "src/kit/index.tsx"),
|
|
477
|
+
"@frockbot/applet-sdk/protocol": join3(SDK_ROOT, "src/protocol/index.ts")
|
|
478
|
+
};
|
|
479
|
+
var SDK_WORKERS_TYPES = join3(
|
|
480
|
+
SDK_ROOT,
|
|
481
|
+
"types/cloudflare-workers.d.ts"
|
|
482
|
+
);
|
|
483
|
+
function packageDirectory(specifier) {
|
|
484
|
+
return dirname(require2.resolve(`${specifier}/package.json`));
|
|
485
|
+
}
|
|
486
|
+
function bundlerNodePaths() {
|
|
487
|
+
const paths = /* @__PURE__ */ new Set([join3(SDK_ROOT, "node_modules")]);
|
|
488
|
+
for (const specifier of [
|
|
489
|
+
"react",
|
|
490
|
+
"react-dom",
|
|
491
|
+
"@tanstack/db",
|
|
492
|
+
"@tanstack/react-db"
|
|
493
|
+
]) {
|
|
494
|
+
try {
|
|
495
|
+
paths.add(join3(packageDirectory(specifier), "..", ".."));
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return [...paths];
|
|
500
|
+
}
|
|
501
|
+
function typeCheckerPaths() {
|
|
502
|
+
const paths = {};
|
|
503
|
+
for (const [specifier, file] of Object.entries(SDK_ENTRIES))
|
|
504
|
+
paths[specifier] = [file];
|
|
505
|
+
try {
|
|
506
|
+
const types = packageDirectory("@types/react");
|
|
507
|
+
paths.react = [join3(types, "index.d.ts")];
|
|
508
|
+
paths["react/jsx-runtime"] = [join3(types, "jsx-runtime.d.ts")];
|
|
509
|
+
paths["react/*"] = [join3(types, "*")];
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
return paths;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/cli/runtime.ts
|
|
516
|
+
import { convertV4MiniflareOptions, Miniflare } from "miniflare";
|
|
517
|
+
var APPLET_COMPATIBILITY_DATE = "2026-08-27";
|
|
518
|
+
var DEV_WORKER = `
|
|
519
|
+
export { Applet } from "./server.js";
|
|
520
|
+
|
|
521
|
+
export default {
|
|
522
|
+
async fetch(request, env) {
|
|
523
|
+
const url = new URL(request.url);
|
|
524
|
+
const stub = env.APPLET.get(env.APPLET.idFromName(env.APPLET_ID));
|
|
525
|
+
|
|
526
|
+
if (url.pathname === "/socket") {
|
|
527
|
+
if (url.searchParams.get("token") !== env.APPLET_TOKEN) {
|
|
528
|
+
return new Response("Forbidden", { status: 403 });
|
|
529
|
+
}
|
|
530
|
+
url.searchParams.set("viewer", url.searchParams.get("viewer") ?? "dev-viewer");
|
|
531
|
+
return stub.fetch(new Request(url, request));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (url.pathname === "/health") {
|
|
535
|
+
return Response.json(await stub.health());
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (url.pathname === "/describe") {
|
|
539
|
+
return Response.json(await stub.describe());
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (url.pathname === "/tool" && request.method === "POST") {
|
|
543
|
+
const body = await request.json();
|
|
544
|
+
try {
|
|
545
|
+
return Response.json({ ok: true, result: await stub.invokeTool(body.name, body.input) });
|
|
546
|
+
} catch (error) {
|
|
547
|
+
return Response.json({ ok: false, error: String(error && error.message || error) });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
552
|
+
return new Response(env.APPLET_UI, {
|
|
553
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
return new Response("Not found", { status: 404 });
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
`;
|
|
560
|
+
async function startAppletRuntime(options) {
|
|
561
|
+
const miniflare = new Miniflare(
|
|
562
|
+
convertV4MiniflareOptions({
|
|
563
|
+
modules: [
|
|
564
|
+
{ type: "ESModule", path: "/index.mjs", contents: DEV_WORKER },
|
|
565
|
+
{ type: "ESModule", path: "/server.js", contents: options.serverCode }
|
|
566
|
+
],
|
|
567
|
+
modulesRoot: "/",
|
|
568
|
+
compatibilityDate: APPLET_COMPATIBILITY_DATE,
|
|
569
|
+
compatibilityFlags: ["nodejs_compat"],
|
|
570
|
+
durableObjects: { APPLET: { className: "Applet", useSQLite: true } },
|
|
571
|
+
serviceBindings: {
|
|
572
|
+
// The lease-backed proxy is a later slice; models are unavailable.
|
|
573
|
+
CAPABILITIES: async () => Response.json({ status: "unavailable", reason: "dev" })
|
|
574
|
+
},
|
|
575
|
+
bindings: {
|
|
576
|
+
APPLET_ID: options.appletId,
|
|
577
|
+
APPLET_TOKEN: options.token,
|
|
578
|
+
APPLET_UI: options.html ?? ""
|
|
579
|
+
},
|
|
580
|
+
host: "127.0.0.1",
|
|
581
|
+
port: options.port ?? 0
|
|
582
|
+
})
|
|
583
|
+
);
|
|
584
|
+
const url = await miniflare.ready;
|
|
585
|
+
return {
|
|
586
|
+
url,
|
|
587
|
+
fetch: (path, init) => miniflare.dispatchFetch(
|
|
588
|
+
new URL(path, url).toString(),
|
|
589
|
+
init
|
|
590
|
+
),
|
|
591
|
+
dispose: () => miniflare.dispose()
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/cli/build.ts
|
|
596
|
+
function sha256(text) {
|
|
597
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
598
|
+
}
|
|
599
|
+
function alias() {
|
|
600
|
+
return { ...SDK_ENTRIES };
|
|
601
|
+
}
|
|
602
|
+
async function bundle(options) {
|
|
603
|
+
const result = await esbuild({
|
|
604
|
+
stdin: {
|
|
605
|
+
contents: options.stdin,
|
|
606
|
+
resolveDir: options.resolveDir,
|
|
607
|
+
sourcefile: options.loaderName,
|
|
608
|
+
loader: "tsx"
|
|
609
|
+
},
|
|
610
|
+
bundle: true,
|
|
611
|
+
write: false,
|
|
612
|
+
format: options.format,
|
|
613
|
+
platform: options.platform,
|
|
614
|
+
target: "es2022",
|
|
615
|
+
jsx: "automatic",
|
|
616
|
+
minify: options.minify,
|
|
617
|
+
legalComments: "none",
|
|
618
|
+
external: options.external,
|
|
619
|
+
alias: alias(),
|
|
620
|
+
nodePaths: bundlerNodePaths(),
|
|
621
|
+
conditions: ["import", "module", "browser", "default"],
|
|
622
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
623
|
+
logLevel: "silent"
|
|
624
|
+
});
|
|
625
|
+
const file = result.outputFiles?.[0];
|
|
626
|
+
if (!file) throw new Error("The bundler produced no output");
|
|
627
|
+
return file.text;
|
|
628
|
+
}
|
|
629
|
+
function page(title, script) {
|
|
630
|
+
return [
|
|
631
|
+
"<!doctype html>",
|
|
632
|
+
'<html lang="en">',
|
|
633
|
+
"<head>",
|
|
634
|
+
'<meta charset="utf-8">',
|
|
635
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
636
|
+
`<title>${title.replaceAll("<", "<")}</title>`,
|
|
637
|
+
"<style>html,body{margin:0;height:100%;background:var(--frockbot-surface,#ffffff)}</style>",
|
|
638
|
+
"</head>",
|
|
639
|
+
"<body>",
|
|
640
|
+
'<div id="applet-root"></div>',
|
|
641
|
+
`<script>${script}</script>`,
|
|
642
|
+
"</body>",
|
|
643
|
+
"</html>"
|
|
644
|
+
].join("\n");
|
|
645
|
+
}
|
|
646
|
+
async function readDescription(serverCode, appletId) {
|
|
647
|
+
const runtime = await startAppletRuntime({
|
|
648
|
+
serverCode,
|
|
649
|
+
appletId,
|
|
650
|
+
token: randomUUID()
|
|
651
|
+
});
|
|
652
|
+
try {
|
|
653
|
+
const health = await runtime.fetch("/health");
|
|
654
|
+
if (!health.ok) {
|
|
655
|
+
throw new Error(`The Applet failed to mount: ${await health.text()}`);
|
|
656
|
+
}
|
|
657
|
+
const response = await runtime.fetch("/describe");
|
|
658
|
+
if (!response.ok) {
|
|
659
|
+
throw new Error(
|
|
660
|
+
`The Applet could not describe its tools: ${await response.text()}`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
return await response.json();
|
|
664
|
+
} finally {
|
|
665
|
+
await runtime.dispose();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async function buildApplet(directory) {
|
|
669
|
+
const descriptor = await readDescriptor(directory);
|
|
670
|
+
const serverCode = await bundle({
|
|
671
|
+
// `Applet` is the export name the kernel's facet mount looks up; the author
|
|
672
|
+
// writes an ordinary default export and never learns that name.
|
|
673
|
+
stdin: 'import AppletClass from "./server";\nexport { AppletClass as Applet };\n',
|
|
674
|
+
resolveDir: directory,
|
|
675
|
+
platform: "neutral",
|
|
676
|
+
format: "esm",
|
|
677
|
+
external: ["cloudflare:workers"],
|
|
678
|
+
minify: false,
|
|
679
|
+
loaderName: "applet-server-entry.ts"
|
|
680
|
+
});
|
|
681
|
+
const uiScript = await bundle({
|
|
682
|
+
stdin: 'import "./ui";\n',
|
|
683
|
+
resolveDir: directory,
|
|
684
|
+
platform: "browser",
|
|
685
|
+
format: "iife",
|
|
686
|
+
external: [],
|
|
687
|
+
minify: true,
|
|
688
|
+
loaderName: "applet-ui-entry.tsx"
|
|
689
|
+
});
|
|
690
|
+
const html = page(descriptor.displayName, uiScript);
|
|
691
|
+
const description = await readDescription(serverCode, descriptor.id);
|
|
692
|
+
const manifest = {
|
|
693
|
+
contract: 1,
|
|
694
|
+
tools: description.tools,
|
|
695
|
+
hashes: { server: sha256(serverCode), ui: sha256(html) }
|
|
696
|
+
};
|
|
697
|
+
const dist = join4(directory, "dist");
|
|
698
|
+
await mkdir(dist, { recursive: true });
|
|
699
|
+
const serverPath = join4(dist, "server.js");
|
|
700
|
+
const uiPath = join4(dist, "ui.html");
|
|
701
|
+
const manifestPath = join4(dist, "manifest.json");
|
|
702
|
+
await writeFile(serverPath, serverCode, "utf8");
|
|
703
|
+
await writeFile(uiPath, html, "utf8");
|
|
704
|
+
await writeFile(
|
|
705
|
+
manifestPath,
|
|
706
|
+
`${JSON.stringify(manifest, null, 2)}
|
|
707
|
+
`,
|
|
708
|
+
"utf8"
|
|
709
|
+
);
|
|
710
|
+
return { directory, serverPath, uiPath, manifestPath, manifest };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/cli/check.ts
|
|
714
|
+
import { readdir as readdir2 } from "node:fs/promises";
|
|
715
|
+
import { join as join5, relative as relative2, resolve as resolve2 } from "node:path";
|
|
716
|
+
import ts from "typescript";
|
|
717
|
+
var COMPILER_OPTIONS = {
|
|
718
|
+
target: ts.ScriptTarget.ES2022,
|
|
719
|
+
module: ts.ModuleKind.ESNext,
|
|
720
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
721
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
722
|
+
strict: true,
|
|
723
|
+
noEmit: true,
|
|
724
|
+
skipLibCheck: true,
|
|
725
|
+
esModuleInterop: true,
|
|
726
|
+
allowSyntheticDefaultImports: true,
|
|
727
|
+
forceConsistentCasingInFileNames: true,
|
|
728
|
+
lib: ["lib.es2022.d.ts", "lib.dom.d.ts"],
|
|
729
|
+
types: []
|
|
730
|
+
};
|
|
731
|
+
async function appletSources(directory) {
|
|
732
|
+
const found = [];
|
|
733
|
+
const walk = async (current) => {
|
|
734
|
+
for (const entry of await readdir2(current, { withFileTypes: true })) {
|
|
735
|
+
if (entry.name === "node_modules" || entry.name === "dist") continue;
|
|
736
|
+
if (entry.name.startsWith(".")) continue;
|
|
737
|
+
const path = join5(current, entry.name);
|
|
738
|
+
if (entry.isDirectory()) await walk(path);
|
|
739
|
+
else if (/\.(ts|tsx)$/.test(entry.name)) found.push(path);
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
await walk(directory);
|
|
743
|
+
return found;
|
|
744
|
+
}
|
|
745
|
+
async function typeCheckApplet(directory) {
|
|
746
|
+
const root = resolve2(directory);
|
|
747
|
+
const files = await appletSources(root);
|
|
748
|
+
if (files.length === 0) {
|
|
749
|
+
return [
|
|
750
|
+
{
|
|
751
|
+
file: "applet.json",
|
|
752
|
+
line: 1,
|
|
753
|
+
column: 1,
|
|
754
|
+
message: "No TypeScript sources found; an Applet needs server.ts and ui.tsx.",
|
|
755
|
+
severity: "error"
|
|
756
|
+
}
|
|
757
|
+
];
|
|
758
|
+
}
|
|
759
|
+
const program = ts.createProgram([...files, SDK_WORKERS_TYPES], {
|
|
760
|
+
...COMPILER_OPTIONS,
|
|
761
|
+
baseUrl: root,
|
|
762
|
+
paths: typeCheckerPaths()
|
|
763
|
+
});
|
|
764
|
+
return ts.getPreEmitDiagnostics(program).filter(
|
|
765
|
+
(diagnostic) => !diagnostic.file || diagnostic.file.fileName.startsWith(root)
|
|
766
|
+
).map((diagnostic) => {
|
|
767
|
+
const message = ts.flattenDiagnosticMessageText(
|
|
768
|
+
diagnostic.messageText,
|
|
769
|
+
" "
|
|
770
|
+
);
|
|
771
|
+
if (!diagnostic.file || diagnostic.start === void 0) {
|
|
772
|
+
return {
|
|
773
|
+
file: "applet.json",
|
|
774
|
+
line: 1,
|
|
775
|
+
column: 1,
|
|
776
|
+
message,
|
|
777
|
+
severity: "error"
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
const position = diagnostic.file.getLineAndCharacterOfPosition(
|
|
781
|
+
diagnostic.start
|
|
782
|
+
);
|
|
783
|
+
return {
|
|
784
|
+
file: relative2(root, diagnostic.file.fileName),
|
|
785
|
+
line: position.line + 1,
|
|
786
|
+
column: position.character + 1,
|
|
787
|
+
message: `${message} (TS${diagnostic.code})`,
|
|
788
|
+
severity: diagnostic.category === ts.DiagnosticCategory.Error ? "error" : "warning"
|
|
789
|
+
};
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
async function checkApplet(directory) {
|
|
793
|
+
const root = resolve2(directory);
|
|
794
|
+
const diagnostics = [];
|
|
795
|
+
try {
|
|
796
|
+
await readDescriptor(root);
|
|
797
|
+
} catch (error) {
|
|
798
|
+
diagnostics.push({
|
|
799
|
+
file: "applet.json",
|
|
800
|
+
line: 1,
|
|
801
|
+
column: 1,
|
|
802
|
+
message: error instanceof Error ? error.message : String(error),
|
|
803
|
+
severity: "error"
|
|
804
|
+
});
|
|
805
|
+
return diagnostics;
|
|
806
|
+
}
|
|
807
|
+
diagnostics.push(...await typeCheckApplet(root));
|
|
808
|
+
diagnostics.push(...await lintApplet(root));
|
|
809
|
+
return diagnostics.sort(
|
|
810
|
+
(left, right) => left.file.localeCompare(right.file) || left.line - right.line || left.column - right.column
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/cli/dev.ts
|
|
815
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
816
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
817
|
+
import { join as join6 } from "node:path";
|
|
818
|
+
var DEV_THEME_TOKENS = {
|
|
819
|
+
surface: "#ffffff",
|
|
820
|
+
"surface-raised": "#ffffff",
|
|
821
|
+
"surface-subtle": "#f3f4f6",
|
|
822
|
+
text: "#16181d",
|
|
823
|
+
"text-muted": "#5b616b",
|
|
824
|
+
border: "#d8dbe0",
|
|
825
|
+
"accent-surface": "#2f6feb",
|
|
826
|
+
"accent-text": "#ffffff",
|
|
827
|
+
"radius-card": "10px"
|
|
828
|
+
};
|
|
829
|
+
function injectDevInit(html, token, generationId) {
|
|
830
|
+
const init = {
|
|
831
|
+
schemaVersion: 1,
|
|
832
|
+
type: "init",
|
|
833
|
+
themeTokens: DEV_THEME_TOKENS,
|
|
834
|
+
packageId: "applets",
|
|
835
|
+
botId: "dev",
|
|
836
|
+
slot: "frockbot.right-panel",
|
|
837
|
+
applet: { socketUrl: "", token, generationId }
|
|
838
|
+
};
|
|
839
|
+
const script = `<script>(()=>{const m=${JSON.stringify(init)};m.applet.socketUrl=location.origin.replace(/^http/,"ws")+"/socket";window.postMessage(m,"*");})();</script>`;
|
|
840
|
+
return html.replace("</body>", `${script}
|
|
841
|
+
</body>`);
|
|
842
|
+
}
|
|
843
|
+
async function startAppletDev(options) {
|
|
844
|
+
const descriptor = await readDescriptor(options.directory);
|
|
845
|
+
const dist = join6(options.directory, "dist");
|
|
846
|
+
let serverCode;
|
|
847
|
+
let html;
|
|
848
|
+
try {
|
|
849
|
+
serverCode = await readFile3(join6(dist, "server.js"), "utf8");
|
|
850
|
+
html = await readFile3(join6(dist, "ui.html"), "utf8");
|
|
851
|
+
} catch {
|
|
852
|
+
throw new Error("No dist/ to serve; run `applet build` first");
|
|
853
|
+
}
|
|
854
|
+
const token = randomUUID2();
|
|
855
|
+
const runtime = await startAppletRuntime({
|
|
856
|
+
serverCode,
|
|
857
|
+
html: injectDevInit(html, token, "dev"),
|
|
858
|
+
appletId: descriptor.id,
|
|
859
|
+
token,
|
|
860
|
+
port: options.port ?? 0
|
|
861
|
+
});
|
|
862
|
+
return { ...runtime, token };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/cli/new.ts
|
|
866
|
+
import { mkdir as mkdir2, readdir as readdir3, readFile as readFile4, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
867
|
+
import { join as join7 } from "node:path";
|
|
868
|
+
var TEMPLATE_ROOT = join7(SDK_ROOT, "template");
|
|
869
|
+
function appletIdFrom(name2) {
|
|
870
|
+
const id = name2.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32).replace(/-+$/g, "");
|
|
871
|
+
if (!/^[a-z][a-z0-9-]{0,31}$/.test(id)) {
|
|
872
|
+
throw new Error(`"${name2}" does not yield a usable Applet id`);
|
|
873
|
+
}
|
|
874
|
+
return id;
|
|
875
|
+
}
|
|
876
|
+
function fill(text, id, displayName) {
|
|
877
|
+
return text.replaceAll("__APPLET_ID__", id).replaceAll("__APPLET_NAME__", displayName);
|
|
878
|
+
}
|
|
879
|
+
async function exists(path) {
|
|
880
|
+
try {
|
|
881
|
+
await stat(path);
|
|
882
|
+
return true;
|
|
883
|
+
} catch {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
async function newApplet(options) {
|
|
888
|
+
const id = appletIdFrom(options.name);
|
|
889
|
+
const directory = join7(options.parent, id);
|
|
890
|
+
if (await exists(directory)) {
|
|
891
|
+
throw new Error(`${directory} already exists`);
|
|
892
|
+
}
|
|
893
|
+
const files = [];
|
|
894
|
+
const copy = async (from, to) => {
|
|
895
|
+
await mkdir2(to, { recursive: true });
|
|
896
|
+
for (const entry of await readdir3(from, { withFileTypes: true })) {
|
|
897
|
+
const source = join7(from, entry.name);
|
|
898
|
+
const target = join7(to, entry.name);
|
|
899
|
+
if (entry.isDirectory()) {
|
|
900
|
+
await copy(source, target);
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
const text = await readFile4(source, "utf8");
|
|
904
|
+
await writeFile2(target, fill(text, id, options.name), "utf8");
|
|
905
|
+
files.push(entry.name);
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
await copy(TEMPLATE_ROOT, directory);
|
|
909
|
+
return { id, displayName: options.name, directory, files };
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/cli/bin.ts
|
|
913
|
+
var USAGE = `applet <command>
|
|
914
|
+
|
|
915
|
+
new <name> [--dir <parent>] scaffold an Applet from the template
|
|
916
|
+
check [dir] type-check and lint
|
|
917
|
+
build [dir] write dist/{server.js,ui.html,manifest.json}
|
|
918
|
+
dev [dir] [--port <n>] serve the built Applet locally
|
|
919
|
+
`;
|
|
920
|
+
function parseArgs(argv) {
|
|
921
|
+
const positional = [];
|
|
922
|
+
const options = {};
|
|
923
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
924
|
+
const token = argv[index];
|
|
925
|
+
if (token.startsWith("--")) {
|
|
926
|
+
const [flag, inline] = token.slice(2).split("=", 2);
|
|
927
|
+
options[flag] = inline ?? argv[++index] ?? "";
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
positional.push(token);
|
|
931
|
+
}
|
|
932
|
+
return { command: positional.shift(), positional, options };
|
|
933
|
+
}
|
|
934
|
+
function report(diagnostics) {
|
|
935
|
+
for (const diagnostic of diagnostics)
|
|
936
|
+
console.error(formatDiagnostic(diagnostic));
|
|
937
|
+
const errors = diagnostics.filter(
|
|
938
|
+
(diagnostic) => diagnostic.severity === "error"
|
|
939
|
+
);
|
|
940
|
+
if (errors.length === 0) {
|
|
941
|
+
console.log("applet check: no problems found");
|
|
942
|
+
return 0;
|
|
943
|
+
}
|
|
944
|
+
console.error(`applet check: ${errors.length} error(s)`);
|
|
945
|
+
return 1;
|
|
946
|
+
}
|
|
947
|
+
async function run(argv) {
|
|
948
|
+
const { command, positional, options } = parseArgs(argv);
|
|
949
|
+
const directory = resolve3(positional[0] ?? ".");
|
|
950
|
+
switch (command) {
|
|
951
|
+
case "new": {
|
|
952
|
+
const name2 = positional[0];
|
|
953
|
+
if (!name2) {
|
|
954
|
+
console.error("applet new needs a name");
|
|
955
|
+
return 2;
|
|
956
|
+
}
|
|
957
|
+
const created = await newApplet({
|
|
958
|
+
name: name2,
|
|
959
|
+
parent: resolve3(options.dir ?? ".")
|
|
960
|
+
});
|
|
961
|
+
console.log(`Created ${created.directory} (id ${created.id})`);
|
|
962
|
+
console.log("Next: applet check && applet build");
|
|
963
|
+
return 0;
|
|
964
|
+
}
|
|
965
|
+
case "check":
|
|
966
|
+
return report(await checkApplet(directory));
|
|
967
|
+
case "build": {
|
|
968
|
+
const result = await buildApplet(directory);
|
|
969
|
+
console.log(
|
|
970
|
+
`${result.serverPath} ${result.manifest.hashes.server.slice(0, 12)}`
|
|
971
|
+
);
|
|
972
|
+
console.log(`${result.uiPath} ${result.manifest.hashes.ui.slice(0, 12)}`);
|
|
973
|
+
console.log(
|
|
974
|
+
`${result.manifestPath} ${result.manifest.tools.length} tool(s): ` + result.manifest.tools.map((tool) => tool.name).join(", ")
|
|
975
|
+
);
|
|
976
|
+
return 0;
|
|
977
|
+
}
|
|
978
|
+
case "dev": {
|
|
979
|
+
const port = options.port === void 0 ? 0 : Number(options.port);
|
|
980
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
981
|
+
console.error("applet dev --port must be a port number");
|
|
982
|
+
return 2;
|
|
983
|
+
}
|
|
984
|
+
const server = await startAppletDev({ directory, port });
|
|
985
|
+
console.log(server.url.toString());
|
|
986
|
+
const stop = () => {
|
|
987
|
+
void server.dispose().then(() => process.exit(0));
|
|
988
|
+
};
|
|
989
|
+
process.on("SIGINT", stop);
|
|
990
|
+
process.on("SIGTERM", stop);
|
|
991
|
+
await new Promise(() => {
|
|
992
|
+
});
|
|
993
|
+
return 0;
|
|
994
|
+
}
|
|
995
|
+
default:
|
|
996
|
+
console.log(USAGE);
|
|
997
|
+
return command === void 0 || command === "help" ? 0 : 2;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// src/cli/main.ts
|
|
1002
|
+
try {
|
|
1003
|
+
process.exitCode = await run(process.argv.slice(2));
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1006
|
+
process.exitCode = 1;
|
|
1007
|
+
}
|