@frockbot/applet-sdk 0.5.2 → 0.6.1

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/src/cli/bin.ts DELETED
@@ -1,128 +0,0 @@
1
- /**
2
- * `applet` — the four commands a Bot runs on the Computer.
3
- *
4
- * `scripts/build-cli.ts` bundles `./main.ts` (which calls `run` below) into
5
- * `dist/cli.mjs`, and that file is the package's `bin`: the Computer image has
6
- * Node and no Bun, and these sources are TypeScript resolving siblings through
7
- * `.js` specifiers, which plain Node cannot load.
8
- *
9
- * applet new <name> [--dir <parent>]
10
- * applet check [dir]
11
- * applet build [dir]
12
- * applet dev [dir] [--port <n>]
13
- *
14
- * Output is deliberately spare: diagnostics as `path:line:col message`, one
15
- * per line, and a non-zero exit when any of them is an error. That is the whole
16
- * contract a Bot has to remember about this CLI.
17
- */
18
-
19
- import { resolve } from "node:path";
20
-
21
- import { formatDiagnostic } from "../lint/index.js";
22
- import { buildApplet } from "./build.js";
23
- import { checkApplet } from "./check.js";
24
- import { startAppletDev } from "./dev.js";
25
- import { newApplet } from "./new.js";
26
-
27
- const USAGE = `applet <command>
28
-
29
- new <name> [--dir <parent>] scaffold an Applet from the template
30
- check [dir] type-check and lint
31
- build [dir] write dist/{server.js,ui.html,manifest.json}
32
- dev [dir] [--port <n>] serve the built Applet locally
33
- `;
34
-
35
- interface Args {
36
- command?: string;
37
- positional: string[];
38
- options: Record<string, string>;
39
- }
40
-
41
- export function parseArgs(argv: string[]): Args {
42
- const positional: string[] = [];
43
- const options: Record<string, string> = {};
44
- for (let index = 0; index < argv.length; index += 1) {
45
- const token = argv[index]!;
46
- if (token.startsWith("--")) {
47
- const [flag, inline] = token.slice(2).split("=", 2);
48
- options[flag!] = inline ?? argv[++index] ?? "";
49
- continue;
50
- }
51
- positional.push(token);
52
- }
53
- return { command: positional.shift(), positional, options };
54
- }
55
-
56
- function report(diagnostics: Awaited<ReturnType<typeof checkApplet>>): number {
57
- for (const diagnostic of diagnostics)
58
- console.error(formatDiagnostic(diagnostic));
59
- const errors = diagnostics.filter(
60
- (diagnostic) => diagnostic.severity === "error",
61
- );
62
- if (errors.length === 0) {
63
- console.log("applet check: no problems found");
64
- return 0;
65
- }
66
- console.error(`applet check: ${errors.length} error(s)`);
67
- return 1;
68
- }
69
-
70
- export async function run(argv: string[]): Promise<number> {
71
- const { command, positional, options } = parseArgs(argv);
72
- const directory = resolve(positional[0] ?? ".");
73
-
74
- switch (command) {
75
- case "new": {
76
- const name = positional[0];
77
- if (!name) {
78
- console.error("applet new needs a name");
79
- return 2;
80
- }
81
- const created = await newApplet({
82
- name,
83
- parent: resolve(options.dir ?? "."),
84
- });
85
- console.log(`Created ${created.directory} (id ${created.id})`);
86
- console.log("Next: applet check && applet build");
87
- return 0;
88
- }
89
- case "check":
90
- return report(await checkApplet(directory));
91
- case "build": {
92
- const result = await buildApplet(directory);
93
- console.log(
94
- `${result.serverPath} ${result.manifest.hashes.server.slice(0, 12)}`,
95
- );
96
- console.log(`${result.uiPath} ${result.manifest.hashes.ui.slice(0, 12)}`);
97
- console.log(
98
- `${result.manifestPath} ${result.manifest.tools.length} tool(s): ` +
99
- result.manifest.tools.map((tool) => tool.name).join(", "),
100
- );
101
- return 0;
102
- }
103
- case "dev": {
104
- const port = options.port === undefined ? 0 : Number(options.port);
105
- if (!Number.isInteger(port) || port < 0 || port > 65_535) {
106
- console.error("applet dev --port must be a port number");
107
- return 2;
108
- }
109
- const server = await startAppletDev({ directory, port });
110
- console.log(server.url.toString());
111
- const stop = () => {
112
- void server.dispose().then(() => process.exit(0));
113
- };
114
- process.on("SIGINT", stop);
115
- process.on("SIGTERM", stop);
116
- await new Promise(() => {});
117
- return 0;
118
- }
119
- default:
120
- console.log(USAGE);
121
- return command === undefined || command === "help" ? 0 : 2;
122
- }
123
- }
124
-
125
- // No `import.meta.main` guard: this module is the command table, and
126
- // `./main.ts` is the entry point the published `dist/cli.mjs` bundles. Two
127
- // entry points would run every command twice under a Node that implements
128
- // `import.meta.main`, which is exactly what happened before this comment.
package/src/cli/dev.ts DELETED
@@ -1,84 +0,0 @@
1
- /**
2
- * `applet dev` — the built Applet on a local port, ready to be opened and
3
- * screenshotted from the Computer's browser. It opens nothing itself.
4
- */
5
-
6
- import { randomUUID } from "node:crypto";
7
- import { readFile } from "node:fs/promises";
8
- import { join } from "node:path";
9
-
10
- import { readDescriptor } from "./manifest.js";
11
- import { startAppletRuntime, type AppletRuntime } from "./runtime.js";
12
-
13
- /**
14
- * In production the shell nests the Applet page and sends it `init`. There is
15
- * no shell here, so the dev page sends itself the same message — the page-side
16
- * contract is identical, which is the point of running it this way at all.
17
- */
18
- const DEV_THEME_TOKENS: Record<string, string> = {
19
- surface: "#ffffff",
20
- "surface-raised": "#ffffff",
21
- "surface-subtle": "#f3f4f6",
22
- text: "#16181d",
23
- "text-muted": "#5b616b",
24
- border: "#d8dbe0",
25
- "accent-surface": "#2f6feb",
26
- "accent-text": "#ffffff",
27
- "radius-card": "10px",
28
- };
29
-
30
- function injectDevInit(
31
- html: string,
32
- token: string,
33
- generationId: string,
34
- ): string {
35
- const init = {
36
- schemaVersion: 1,
37
- type: "init",
38
- themeTokens: DEV_THEME_TOKENS,
39
- packageId: "applets",
40
- botId: "dev",
41
- slot: "frockbot.right-panel",
42
- applet: { socketUrl: "", token, generationId },
43
- };
44
- const script =
45
- `<script>(()=>{const m=${JSON.stringify(init)};` +
46
- `m.applet.socketUrl=location.origin.replace(/^http/,"ws")+"/socket";` +
47
- `window.postMessage(m,"*");})();</script>`;
48
- return html.replace("</body>", `${script}\n</body>`);
49
- }
50
-
51
- export interface AppletDevServer extends AppletRuntime {
52
- token: string;
53
- }
54
-
55
- export interface AppletDevOptions {
56
- directory: string;
57
- /** 0 picks a free port. */
58
- port?: number;
59
- }
60
-
61
- /** Serve `dist/` from Miniflare. Build first; this does not bundle. */
62
- export async function startAppletDev(
63
- options: AppletDevOptions,
64
- ): Promise<AppletDevServer> {
65
- const descriptor = await readDescriptor(options.directory);
66
- const dist = join(options.directory, "dist");
67
- let serverCode: string;
68
- let html: string;
69
- try {
70
- serverCode = await readFile(join(dist, "server.js"), "utf8");
71
- html = await readFile(join(dist, "ui.html"), "utf8");
72
- } catch {
73
- throw new Error("No dist/ to serve; run `applet build` first");
74
- }
75
- const token = randomUUID();
76
- const runtime = await startAppletRuntime({
77
- serverCode,
78
- html: injectDevInit(html, token, "dev"),
79
- appletId: descriptor.id,
80
- token,
81
- port: options.port ?? 0,
82
- });
83
- return { ...runtime, token };
84
- }
package/src/cli/main.ts DELETED
@@ -1,17 +0,0 @@
1
- /**
2
- * The bundled CLI's entry point.
3
- *
4
- * `bin.ts` guards its own execution with `import.meta.main` so it can be
5
- * imported by tests under Bun. `dist/cli.mjs` is only ever executed, and it
6
- * runs under whatever Node the Computer image ships — where `import.meta.main`
7
- * may not exist, and where an absent guard would mean a CLI that exits 0 and
8
- * does nothing. So the bundle gets its own entry with no guard at all.
9
- */
10
- import { run } from "./bin.js";
11
-
12
- try {
13
- process.exitCode = await run(process.argv.slice(2));
14
- } catch (error) {
15
- console.error(error instanceof Error ? error.message : String(error));
16
- process.exitCode = 1;
17
- }
package/src/cli/new.ts DELETED
@@ -1,74 +0,0 @@
1
- /** `applet new` — the template, with the name filled in. */
2
-
3
- import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
4
- import { join } from "node:path";
5
-
6
- import { SDK_ROOT } from "./paths.js";
7
-
8
- const TEMPLATE_ROOT = join(SDK_ROOT, "template");
9
-
10
- /** `Weekly Plan` -> `weekly-plan`. */
11
- export function appletIdFrom(name: string): string {
12
- const id = name
13
- .trim()
14
- .toLowerCase()
15
- .replace(/[^a-z0-9]+/g, "-")
16
- .replace(/^-+|-+$/g, "")
17
- .slice(0, 32)
18
- .replace(/-+$/g, "");
19
- if (!/^[a-z][a-z0-9-]{0,31}$/.test(id)) {
20
- throw new Error(`"${name}" does not yield a usable Applet id`);
21
- }
22
- return id;
23
- }
24
-
25
- function fill(text: string, id: string, displayName: string): string {
26
- return text
27
- .replaceAll("__APPLET_ID__", id)
28
- .replaceAll("__APPLET_NAME__", displayName);
29
- }
30
-
31
- async function exists(path: string): Promise<boolean> {
32
- try {
33
- await stat(path);
34
- return true;
35
- } catch {
36
- return false;
37
- }
38
- }
39
-
40
- export interface NewAppletResult {
41
- id: string;
42
- displayName: string;
43
- directory: string;
44
- files: string[];
45
- }
46
-
47
- export async function newApplet(options: {
48
- name: string;
49
- /** Where the Applet directory is created; the durable root in production. */
50
- parent: string;
51
- }): Promise<NewAppletResult> {
52
- const id = appletIdFrom(options.name);
53
- const directory = join(options.parent, id);
54
- if (await exists(directory)) {
55
- throw new Error(`${directory} already exists`);
56
- }
57
- const files: string[] = [];
58
- const copy = async (from: string, to: string): Promise<void> => {
59
- await mkdir(to, { recursive: true });
60
- for (const entry of await readdir(from, { withFileTypes: true })) {
61
- const source = join(from, entry.name);
62
- const target = join(to, entry.name);
63
- if (entry.isDirectory()) {
64
- await copy(source, target);
65
- continue;
66
- }
67
- const text = await readFile(source, "utf8");
68
- await writeFile(target, fill(text, id, options.name), "utf8");
69
- files.push(entry.name);
70
- }
71
- };
72
- await copy(TEMPLATE_ROOT, directory);
73
- return { id, displayName: options.name, directory, files };
74
- }