@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/README.md CHANGED
@@ -2,11 +2,13 @@
2
2
 
3
3
  The SDK a FrockBot Applet is written against: a schema-first Durable Object
4
4
  server, a TanStack DB client over one real-time socket, a precompiled component
5
- kit on the theme tokens, a linter, and the `applet` CLI.
5
+ kit on the theme tokens, a linter, and the build pipeline the cloud build
6
+ service runs.
6
7
 
7
- See [ADR 0022](../../docs/adr/0022-applets-as-instance-packages.md) for why an
8
- Applet's state is a Durable Object facet the kernel owns the lifecycle of, and
9
- `docs/plans/applets.md` §8 for this package's place in the build.
8
+ An Applet is authored with the `applet_*` tools, built by `apps/applet-build`,
9
+ and mounted as a Durable Object facet from an immutable artifact. There is no
10
+ CLI: nothing outside the service builds an Applet, and no Computer is involved
11
+ at any point.
10
12
 
11
13
  ## Entry points
12
14
 
@@ -17,29 +19,24 @@ Applet's state is a Durable Object facet the kernel owns the lifecycle of, and
17
19
  | `@frockbot/applet-sdk/kit` | the fourteen components (`src/kit/README.md`) |
18
20
  | `@frockbot/applet-sdk/lint` | the flat ESLint config and the five custom rules |
19
21
  | `@frockbot/applet-sdk/protocol` | wire protocol v1, for the kernel and for tests |
22
+ | `@frockbot/applet-sdk/build` | `runAppletBuildV1` — the five stages, for the service |
20
23
 
21
- ## The CLI
24
+ ## The build
22
25
 
23
- ```sh
24
- applet new "Weekly Todos" # scaffold from template/
25
- applet check # tsc + lint; path:line:col message; non-zero on error
26
- applet build # dist/{server.js,ui.html,manifest.json}
27
- applet dev # Miniflare on a local port; prints a URL, opens nothing
28
- ```
26
+ `runAppletBuildV1(directory, { mode })` is five named stages over one
27
+ directory: `descriptor`, `typecheck`, `lint`, `bundle`, `describe`. `check`
28
+ stops after the linter; `build` goes on to the artifacts. A stage that fails
29
+ stops the run and names itself, and every failure is a list of
30
+ `{file, line, column, message, severity}`.
29
31
 
30
- `applet build` derives `manifest.json`'s tool declarations by mounting the built
31
- `dist/server.js` in Miniflare and calling `health()` — the same question the
32
- kernel asks the facet before it admits a generation, so the manifest cannot
33
- disagree with the code.
32
+ `manifest.json`'s tool declarations are derived by mounting the built
33
+ `server.js` in Miniflare and calling `health()` — the same question the kernel
34
+ asks the facet before it admits a generation, so the manifest cannot disagree
35
+ with the code.
34
36
 
35
- **The published CLI runs under Node.** `prepublishOnly` bundles
36
- `src/cli/main.ts` to `dist/cli.mjs`, and the package's `bin` points there. The
37
- Computer's `applets` provisioning phase installs this package and its runtime
38
- once under the shared Computer runtime; an in-place runtime update repairs that
39
- installation when it is missing. An Applet project deliberately has no
40
- `node_modules` of its own. The checker and bundler resolve SDK, React, and
41
- TanStack imports from the shared installation, while project dependency trees
42
- remain reproducible scratch and never enter the durable-root sync.
37
+ `template/` is the scaffold a new Applet starts as.
38
+ `scripts/build-applets-assets.ts` turns it into `applets/template.generated.ts`,
39
+ which `applet_create` writes through the Workspace.
43
40
 
44
41
  ## What runs where
45
42
 
@@ -50,9 +47,9 @@ page served from the anonymous artifact origin into a sandboxed iframe, which
50
47
  receives its theme tokens and a short-lived viewer token through the host's
51
48
  `init` message and opens exactly one WebSocket back to the facet.
52
49
 
53
- The Cloudflare programming model is not hidden and ADR 0022 says so: an Applet
54
- is a Durable Object with SQLite and hibernating sockets. What the SDK does hide
55
- is every binding name — an author sees `tables`, `tools`, and `this.db`.
50
+ The Cloudflare programming model is not hidden: an Applet is a Durable Object
51
+ with SQLite and hibernating sockets. What the SDK does hide is every binding
52
+ name — an author sees `tables`, `tools`, and `this.db`.
56
53
 
57
54
  ## Wire protocol v1
58
55
 
@@ -77,5 +74,5 @@ bun test test spike
77
74
 
78
75
  Pure modules and the client are tested in `bun test`: the store runs against
79
76
  `bun:sqlite`, and `test/loopback.ts` joins the real protocol server to the real
80
- client transport through a pair of fake sockets. `test/cli.test.ts` and
81
- `spike/` run the built Applet in Miniflare for real.
77
+ client transport through a pair of fake sockets. `test/build.test.ts` and
78
+ `spike/` run the real pipeline and the built Applet in Miniflare.
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@frockbot/applet-sdk",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "Authoring SDK for FrockBot Applets: schema-first Durable Object server, TanStack DB client, component kit, linter, and the `applet` CLI.",
6
+ "description": "Authoring SDK for FrockBot Applets: schema-first Durable Object server, TanStack DB client, component kit, linter, and the build pipeline.",
7
7
  "license": "UNLICENSED",
8
8
  "exports": {
9
9
  "./server": "./src/server/index.ts",
@@ -11,21 +11,16 @@
11
11
  "./kit": "./src/kit/index.tsx",
12
12
  "./lint": "./src/lint/index.ts",
13
13
  "./protocol": "./src/protocol/index.ts",
14
+ "./build": "./src/build/pipeline.ts",
14
15
  "./package.json": "./package.json"
15
16
  },
16
- "bin": {
17
- "applet": "./dist/cli.mjs"
18
- },
19
17
  "files": [
20
- "dist",
21
18
  "src",
22
19
  "types",
23
20
  "template",
24
21
  "README.md"
25
22
  ],
26
23
  "scripts": {
27
- "build": "bun scripts/build-cli.ts",
28
- "prepublishOnly": "bun scripts/build-cli.ts",
29
24
  "test": "bun test test spike",
30
25
  "typecheck": "tsc --noEmit -p tsconfig.json"
31
26
  },
@@ -52,7 +47,7 @@
52
47
  "repository": {
53
48
  "type": "git",
54
49
  "url": "git+https://github.com/timoconnellaus/frockbot.git",
55
- "directory": "packages/applet-sdk"
50
+ "directory": "applets/sdk"
56
51
  },
57
52
  "frockbot": {
58
53
  "npm": true
@@ -1,12 +1,12 @@
1
1
  /**
2
- * `applet build` — three immutable artifacts from two source files.
2
+ * Three immutable artifacts from two source files.
3
3
  *
4
- * `dist/server.js` one ESM file whose only import is `cloudflare:workers`,
5
- * exporting `Applet`, which is the name the kernel mounts.
6
- * `dist/ui.html` one self-contained page: React, TanStack DB, the kit, and
7
- * the app inlined. No external URL, because the artifact
8
- * origin serves it into a sandbox with no network of its own.
9
- * `dist/manifest.json` `{ contract, tools, hashes }`.
4
+ * `server.js` one ESM file whose only import is `cloudflare:workers`,
5
+ * exporting `Applet`, which is the name the kernel mounts.
6
+ * `ui.html` one self-contained page: React, TanStack DB, the kit, and
7
+ * the app inlined. No external URL, because the artifact
8
+ * origin serves it into a sandbox with no network of its own.
9
+ * `manifest.json` `{ contract, tools, hashes }`.
10
10
  *
11
11
  * The tool declarations come from mounting the built server in Miniflare and
12
12
  * calling `health()`, not from reading the source. Static analysis would be a
@@ -14,33 +14,91 @@
14
14
  * the kernel actually asks — and the kernel admits a generation by comparing
15
15
  * the manifest to the facet's own `health()`, so any disagreement is a failed
16
16
  * publish. Running the code is the only derivation that cannot drift.
17
+ *
18
+ * Nothing here touches a filesystem beyond the source it is given: the build
19
+ * service returns these three strings over its contract, and the app stores
20
+ * them under their content hashes.
17
21
  */
18
22
 
19
23
  import { createHash, randomUUID } from "node:crypto";
20
- import { mkdir, writeFile } from "node:fs/promises";
21
- import { join } from "node:path";
24
+ import { realpathSync } from "node:fs";
25
+ import { relative, resolve } from "node:path";
22
26
 
23
- import { build as esbuild } from "esbuild";
27
+ import { build as esbuild, type Metafile } from "esbuild";
24
28
 
25
29
  import type { AppletDescriptionV1 } from "../server/applet.js";
26
30
  import { readDescriptor, type AppletBuildManifestV1 } from "./manifest.js";
27
- import { bundlerNodePaths, SDK_ENTRIES } from "./paths.js";
31
+ import { bundlerNodePaths, SDK_ENTRIES, SDK_ROOT } from "./paths.js";
28
32
  import { startAppletRuntime } from "./runtime.js";
29
33
 
30
- export interface AppletBuildResult {
31
- directory: string;
32
- serverPath: string;
33
- uiPath: string;
34
- manifestPath: string;
34
+ export interface AppletArtifactsV1 {
35
35
  manifest: AppletBuildManifestV1;
36
+ server: string;
37
+ ui: string;
36
38
  }
37
39
 
38
40
  function sha256(text: string): string {
39
41
  return createHash("sha256").update(text, "utf8").digest("hex");
40
42
  }
41
43
 
42
- function alias(): Record<string, string> {
43
- return { ...SDK_ENTRIES };
44
+ /** esbuild reports real paths; a temp directory is often a symlink to one. */
45
+ function real(path: string): string {
46
+ try {
47
+ return realpathSync(resolve(path));
48
+ } catch {
49
+ return resolve(path);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Where a bundled module came from, said the same way everywhere.
55
+ *
56
+ * An Applet's directory, the SDK's directory and the working directory are all
57
+ * different on an author's machine and in the build container — and esbuild
58
+ * writes each module's path into the unminified output as a comment. Left
59
+ * alone, identical source bundled in two places produces two different files
60
+ * and therefore two different content hashes, which would give R2 a new object
61
+ * on every publish of unchanged code.
62
+ */
63
+ function moduleLabel(absolute: string, appletRoot: string): string {
64
+ for (const [root, prefix] of [
65
+ [appletRoot, ""],
66
+ [real(SDK_ROOT), "applet-sdk/"],
67
+ ] as const) {
68
+ const inside = relative(root, absolute);
69
+ if (!inside.startsWith("..") && inside !== "") return prefix + inside;
70
+ }
71
+ const dependency = absolute.lastIndexOf("node_modules/");
72
+ return dependency === -1 ? absolute : absolute.slice(dependency);
73
+ }
74
+
75
+ /**
76
+ * Rewrites esbuild's module comments to those stable labels.
77
+ *
78
+ * The paths come from the metafile rather than from a pattern over the output,
79
+ * so only lines esbuild actually wrote are touched and a comment in someone's
80
+ * own source is left alone.
81
+ */
82
+ function stableModulePaths(
83
+ text: string,
84
+ metafile: Metafile,
85
+ appletRoot: string,
86
+ ): string {
87
+ const root = real(appletRoot);
88
+ const labels = new Map(
89
+ Object.keys(metafile.inputs).map((input) => [
90
+ input,
91
+ moduleLabel(real(input), root),
92
+ ]),
93
+ );
94
+ return text
95
+ .split("\n")
96
+ .map((line) => {
97
+ if (!line.startsWith("// ")) return line;
98
+ const label = labels.get(line.slice(3));
99
+ return label === undefined ? line : `// ${label}`;
100
+ })
101
+ .join("\n");
44
102
  }
45
103
 
46
104
  async function bundle(options: {
@@ -68,15 +126,16 @@ async function bundle(options: {
68
126
  minify: options.minify,
69
127
  legalComments: "none",
70
128
  external: options.external,
71
- alias: alias(),
129
+ alias: { ...SDK_ENTRIES },
72
130
  nodePaths: bundlerNodePaths(),
73
131
  conditions: ["import", "module", "browser", "default"],
74
132
  define: { "process.env.NODE_ENV": '"production"' },
133
+ metafile: true,
75
134
  logLevel: "silent",
76
135
  });
77
136
  const file = result.outputFiles?.[0];
78
137
  if (!file) throw new Error("The bundler produced no output");
79
- return file.text;
138
+ return stableModulePaths(file.text, result.metafile, options.resolveDir);
80
139
  }
81
140
 
82
141
  function page(title: string, script: string): string {
@@ -130,12 +189,12 @@ export async function readDescription(
130
189
  }
131
190
  }
132
191
 
133
- export async function buildApplet(
192
+ /** The two bundles, in the order that lets a bundle failure precede a boot. */
193
+ export async function bundleAppletArtifacts(
134
194
  directory: string,
135
- ): Promise<AppletBuildResult> {
195
+ ): Promise<{ descriptorId: string; server: string; ui: string }> {
136
196
  const descriptor = await readDescriptor(directory);
137
-
138
- const serverCode = await bundle({
197
+ const server = await bundle({
139
198
  // `Applet` is the export name the kernel's facet mount looks up; the author
140
199
  // writes an ordinary default export and never learns that name.
141
200
  stdin:
@@ -147,7 +206,6 @@ export async function buildApplet(
147
206
  minify: false,
148
207
  loaderName: "applet-server-entry.ts",
149
208
  });
150
-
151
209
  const uiScript = await bundle({
152
210
  stdin: 'import "./ui";\n',
153
211
  resolveDir: directory,
@@ -157,27 +215,34 @@ export async function buildApplet(
157
215
  minify: true,
158
216
  loaderName: "applet-ui-entry.tsx",
159
217
  });
160
- const html = page(descriptor.displayName, uiScript);
218
+ return {
219
+ descriptorId: descriptor.id,
220
+ server,
221
+ ui: page(descriptor.displayName, uiScript),
222
+ };
223
+ }
161
224
 
162
- const description = await readDescription(serverCode, descriptor.id);
163
- const manifest: AppletBuildManifestV1 = {
225
+ /** The manifest for artifacts whose declarations have been read. */
226
+ export function appletManifest(
227
+ tools: AppletDescriptionV1["tools"],
228
+ artifacts: { server: string; ui: string },
229
+ ): AppletBuildManifestV1 {
230
+ return {
164
231
  contract: 1,
165
- tools: description.tools,
166
- hashes: { server: sha256(serverCode), ui: sha256(html) },
232
+ tools,
233
+ hashes: { server: sha256(artifacts.server), ui: sha256(artifacts.ui) },
167
234
  };
235
+ }
168
236
 
169
- const dist = join(directory, "dist");
170
- await mkdir(dist, { recursive: true });
171
- const serverPath = join(dist, "server.js");
172
- const uiPath = join(dist, "ui.html");
173
- const manifestPath = join(dist, "manifest.json");
174
- await writeFile(serverPath, serverCode, "utf8");
175
- await writeFile(uiPath, html, "utf8");
176
- await writeFile(
177
- manifestPath,
178
- `${JSON.stringify(manifest, null, 2)}\n`,
179
- "utf8",
180
- );
181
-
182
- return { directory, serverPath, uiPath, manifestPath, manifest };
237
+ /** Bundle, boot, describe. The whole artifact derivation, with no output. */
238
+ export async function buildAppletArtifacts(
239
+ directory: string,
240
+ ): Promise<AppletArtifactsV1> {
241
+ const { descriptorId, server, ui } = await bundleAppletArtifacts(directory);
242
+ const description = await readDescription(server, descriptorId);
243
+ return {
244
+ manifest: appletManifest(description.tools, { server, ui }),
245
+ server,
246
+ ui,
247
+ };
183
248
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `applet check` — the type checker and the linter, one diagnostic list.
2
+ * The check stage — the type checker and the linter, one diagnostic list.
3
3
  *
4
4
  * The Applet has no `node_modules`: it is source at a durable root. So the
5
5
  * compiler options are built here from `paths.ts` rather than from a tsconfig
@@ -106,7 +106,7 @@ export async function typeCheckApplet(
106
106
  });
107
107
  }
108
108
 
109
- /** Everything `applet check` reports, in source order. */
109
+ /** Everything the check stage reports, in source order. */
110
110
  export async function checkApplet(
111
111
  directory: string,
112
112
  ): Promise<AppletDiagnostic[]> {
@@ -7,7 +7,7 @@ import { APPLET_CONTRACT_VERSION } from "../protocol/index.js";
7
7
  import type { AppletToolDeclarationV1 } from "../server/applet.js";
8
8
 
9
9
  export interface AppletDescriptorV1 {
10
- /** `/^[a-z][a-z0-9-]{0,31}$/`; the directory name `applet new` creates. */
10
+ /** `/^[a-z][a-z0-9-]{0,31}$/`; the scaffold's directory name. */
11
11
  id: string;
12
12
  displayName: string;
13
13
  contract: 1;
@@ -50,9 +50,7 @@ export async function readDescriptor(
50
50
  try {
51
51
  text = await readFile(path, "utf8");
52
52
  } catch {
53
- throw new Error(
54
- `No applet.json in ${directory}; run \`applet new <name>\` first`,
55
- );
53
+ throw new Error(`No applet.json in ${directory}`);
56
54
  }
57
55
  return decodeDescriptor(JSON.parse(text));
58
56
  }
@@ -17,11 +17,12 @@ const require = createRequire(import.meta.url);
17
17
  * Found by walking up to this package's own `package.json`, not by counting
18
18
  * directories.
19
19
  *
20
- * The same module runs from two depths: `src/cli/paths.ts` under Bun, and the
21
- * bundled `dist/cli.mjs` under Node on the Computer, which has no Bun. A fixed
22
- * `../../` is right for one and silently wrong for the other — it would resolve
23
- * the SDK's entries to a directory that does not exist and every Applet import
24
- * would fail to type-check with no explanation.
20
+ * The same module runs from three depths: `src/build/paths.ts` under Bun, the
21
+ * bundled `dist/cli.mjs` under Node, and the build service's own bundle, which
22
+ * is emitted into `dist/` for exactly this reason. A fixed `../../` is right
23
+ * for one and silently wrong for the others — it would resolve the SDK's
24
+ * entries to a directory that does not exist and every Applet import would
25
+ * fail to type-check with no explanation.
25
26
  */
26
27
  function findSdkRoot(): string {
27
28
  let directory = dirname(fileURLToPath(import.meta.url));
@@ -92,7 +93,7 @@ export function typeCheckerPaths(): Record<string, string[]> {
92
93
  paths["react/jsx-runtime"] = [join(types, "jsx-runtime.d.ts")];
93
94
  paths["react/*"] = [join(types, "*")];
94
95
  } catch {
95
- // No React declarations available; `applet check` reports the import.
96
+ // No React declarations available; the check stage reports the import.
96
97
  }
97
98
  return paths;
98
99
  }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The whole Applet build, as five named stages over one directory.
3
+ *
4
+ * `applet_check` and `applet_publish` are the two ways in, and both reach
5
+ * these stages through the same build service. There is one implementation of
6
+ * each stage, so a check and the publish that follows it cannot disagree about
7
+ * whether the code is admissible or about what it hashes to.
8
+ *
9
+ * A stage that fails stops the run and names itself, which is what turns a
10
+ * wall of diagnostics into "your `server.ts` does not type-check". The two
11
+ * stages that produce no diagnostic list of their own — `bundle` and
12
+ * `describe` — report the thrown message as one diagnostic against
13
+ * `applet.json`, so a caller has one shape to render.
14
+ */
15
+
16
+ import { lintApplet, type AppletDiagnostic } from "../lint/index.js";
17
+ import { buildAppletArtifacts, type AppletArtifactsV1 } from "./artifacts.js";
18
+ import { typeCheckApplet } from "./check.js";
19
+ import { readDescriptor } from "./manifest.js";
20
+
21
+ export type AppletBuildStage =
22
+ "descriptor" | "typecheck" | "lint" | "bundle" | "describe";
23
+
24
+ export type AppletBuildOutcome =
25
+ | { status: "checked" }
26
+ | ({ status: "built" } & AppletArtifactsV1)
27
+ | {
28
+ status: "failed";
29
+ stage: AppletBuildStage;
30
+ diagnostics: AppletDiagnostic[];
31
+ };
32
+
33
+ /** A thrown stage failure, reported at `applet.json` for want of a position. */
34
+ function thrown(error: unknown): AppletDiagnostic[] {
35
+ return [
36
+ {
37
+ file: "applet.json",
38
+ line: 1,
39
+ column: 1,
40
+ message: error instanceof Error ? error.message : String(error),
41
+ severity: "error",
42
+ },
43
+ ];
44
+ }
45
+
46
+ function hasError(diagnostics: AppletDiagnostic[]): boolean {
47
+ return diagnostics.some((diagnostic) => diagnostic.severity === "error");
48
+ }
49
+
50
+ export interface AppletBuildPipelineOptions {
51
+ /** `check` stops after the linter; `build` goes on to the artifacts. */
52
+ mode: "check" | "build";
53
+ }
54
+
55
+ export async function runAppletBuildV1(
56
+ directory: string,
57
+ options: AppletBuildPipelineOptions,
58
+ ): Promise<AppletBuildOutcome> {
59
+ try {
60
+ await readDescriptor(directory);
61
+ } catch (error) {
62
+ return {
63
+ status: "failed",
64
+ stage: "descriptor",
65
+ diagnostics: thrown(error),
66
+ };
67
+ }
68
+
69
+ const types = await typeCheckApplet(directory);
70
+ if (hasError(types)) {
71
+ return { status: "failed", stage: "typecheck", diagnostics: types };
72
+ }
73
+ const lint = await lintApplet(directory);
74
+ if (hasError(lint)) {
75
+ return { status: "failed", stage: "lint", diagnostics: lint };
76
+ }
77
+ if (options.mode === "check") return { status: "checked" };
78
+
79
+ let artifacts: AppletArtifactsV1;
80
+ try {
81
+ artifacts = await buildAppletArtifacts(directory);
82
+ } catch (error) {
83
+ // `describe` is the only stage that boots the built module, and both of
84
+ // its failures say so in their own words; anything else thrown here came
85
+ // out of the bundler.
86
+ const message = error instanceof Error ? error.message : String(error);
87
+ return {
88
+ status: "failed",
89
+ stage: /^The Applet (failed to mount|could not describe)/.test(message)
90
+ ? "describe"
91
+ : "bundle",
92
+ diagnostics: thrown(error),
93
+ };
94
+ }
95
+ return { status: "built", ...artifacts };
96
+ }
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Miniflare gives the built `dist/server.js` the one thing no fake can: a
5
5
  * SQLite-backed Durable Object with hibernating WebSockets, which is exactly
6
- * what the loader gives it in production. `applet dev` serves the page from
7
- * it, and `applet build` uses the same runtime to ask the mounted class what
6
+ * what the loader gives it in production. The build uses it to ask the
7
+ * mounted class what
8
8
  * tools it declares rather than guessing from the source.
9
9
  */
10
10
 
@@ -17,7 +17,7 @@
17
17
  * The page never opens the socket itself: the host sends an `init` postMessage
18
18
  * carrying the theme tokens and a short-lived viewer token, and `createApplet`
19
19
  * connects from that. `connect(init)` is the same path, called by hand, which
20
- * is what `applet dev` and the tests use.
20
+ * is what the tests use.
21
21
  */
22
22
 
23
23
  import type { Collection } from "@tanstack/db";
package/src/lint/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `@frockbot/applet-sdk/lint` — the flat config, the rules, and the one call
3
- * `applet check` makes.
3
+ * the check stage makes.
4
4
  *
5
5
  * A diagnostic is the SDK's whole answer to "what did I do wrong": the CLI
6
6
  * prints `path:line:col message` and nothing else, so what a Bot must remember
@@ -61,7 +61,7 @@ export interface AppletHealthV1 {
61
61
  schemaRevision: number;
62
62
  }
63
63
 
64
- /** The build-time description `applet build` writes into `dist/manifest.json`. */
64
+ /** The build-time description the build writes into the manifest. */
65
65
  export interface AppletDescriptionV1 {
66
66
  contract: 1;
67
67
  tools: AppletToolDeclarationV1[];
@@ -250,7 +250,7 @@ export abstract class Applet<
250
250
  };
251
251
  }
252
252
 
253
- /** The tool declarations, for `applet build` to write into the manifest. */
253
+ /** The tool declarations, for the build to write into the manifest. */
254
254
  describe(): AppletDescriptionV1 {
255
255
  return {
256
256
  contract: APPLET_CONTRACT_VERSION,
@@ -9,14 +9,11 @@ A FrockBot Applet. Two files are yours:
9
9
 
10
10
  ## The loop
11
11
 
12
- ```sh
13
- applet check # type-check and lint; every problem prints as path:line:col message
14
- applet build # dist/server.js, dist/ui.html, dist/manifest.json
15
- applet dev # serves the built Applet; prints a URL, opens nothing
16
- ```
17
-
18
- Open the printed URL in the Computer's browser to look at it. Publish with
19
- `applet_publish` once `applet check` is clean.
12
+ `applet_files` and `applet_read_file` to see what is here, `applet_write_file`
13
+ to change it, then `applet_check` — it type-checks, lints, bundles and boots
14
+ your server, and answers either with every problem as `path:line:col message`
15
+ or with the tools it declares and a URL for the page. Publish with
16
+ `applet_publish` once the check is clean.
20
17
 
21
18
  ## Rules the linter enforces
22
19