create-gesso-app 0.4.2 → 0.6.3
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/CHANGELOG.md +41 -0
- package/README.md +3 -2
- package/dist/create-gesso-app.mjs +18 -3
- package/dist/create-gesso-app.mjs.map +1 -1
- package/package.json +12 -8
- package/templates/_shared/AGENTS.md +168 -0
- package/templates/_shared/CLAUDE.md +1 -0
- package/templates/electrobun/README.md +24 -0
- package/templates/electrobun/hutch.config.ts +12 -4
- package/templates/electrobun/package.json +7 -6
- package/templates/electrobun/src/main/index.ts +31 -11
- package/templates/electrobun/src/shared/Counter.ts +22 -5
- package/templates/electrobun/src/shared/channels.described.ts +65 -0
- package/templates/electrobun/src/view/index.html +6 -0
- package/templates/electrobun/src/view/main.ts +2 -0
- package/templates/electrobun/tsconfig.json +6 -0
- package/templates/electrobun/vite.config.ts +2 -2
- package/templates/web/README.md +3 -0
- package/templates/web/index.html +7 -0
- package/templates/web/package.json +5 -5
- package/templates/web/src/main.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# create-gesso-app
|
|
2
2
|
|
|
3
|
+
## 0.6.3
|
|
4
|
+
|
|
5
|
+
## 0.6.2
|
|
6
|
+
|
|
7
|
+
## 0.6.1
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- The `create-gesso-app` command is in the published package again. npm 11 dropped a `bin` path written with a leading `./`, so `npm create gesso-app` found nothing to run.
|
|
12
|
+
|
|
13
|
+
## 0.6.0
|
|
14
|
+
|
|
15
|
+
## 0.5.1
|
|
16
|
+
|
|
17
|
+
## 0.5.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 9322268: A new project comes with an `AGENTS.md`: the framework's rules in one page, written for a coding agent that has only ever seen React, and a `CLAUDE.md` that points Claude Code at it. It covers what Gesso is not, why a component runs once and what that means for state, layout and theme tokens, channels, and how to check work you cannot see in the DOM. The documentation site now publishes `llms.txt`, `llms-full.txt` and a markdown copy of every page for the same readers.
|
|
22
|
+
- fa859bb: A desktop app serves its channels to AI agents. `serveDesktopAgent(channels, options)` in `gesso-electrobun/desktop` serves them over MCP from the main process with `Bun.serve`, which Cottontail provides, on `127.0.0.1:7310` or the next free port after it, refusing any request a web page sends, and reports the URL to connect. `messageBoxConfirm(Utils.showMessageBox)` puts a `@confirm` command to the person with the native dialog, Decline by default. The screen tools are not offered there, since the screen is in each window's render worker.
|
|
23
|
+
|
|
24
|
+
Electrobun bundles the main process with its own build, which takes no plugins, so `gesso-vite-plugin` now ships `gesso-channels`, a command that reads contracts with the same TypeScript 7 checker and writes a module describing them, for the main process to import once; `--check` fails when it is out of date.
|
|
25
|
+
|
|
26
|
+
The Electrobun template uses all of it: the counter is served to agents as the app starts, `hutch run channels` writes `src/shared/channels.described.ts` before every build, `hutch run typecheck` checks it is current, and the contract's JSDoc is written for an agent to read. The template moves to TypeScript 7, clearing the projected config's `baseUrl`, which TypeScript 7 removed, and to Vite 8, with `vite.config.ts` using `import.meta.dirname` and an explicit `.ts` import as Vite 8's config loader asks.
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- f265910: An AI agent can drive a web application while it runs in development. `gesso-vite-plugin` serves MCP at `/__gesso/mcp` on the dev server and prints the `claude mcp add` line to connect; the agent then sees every channel the open page can reach, the ones its render worker feeds and the ones its application and channel workers serve, and its commands change the page as a click would. Messages travel down the HMR socket to the page, which answers them against the render worker, which asks each worker behind it over a `gesso:agent` port. A command marked `@confirm` is put to the person with the browser's dialog first. The endpoint refuses requests from browser pages, the newest open tab answers, and `agent: false` turns it off. A build carries none of it.
|
|
31
|
+
|
|
32
|
+
`gesso-framework/agent` gains what the bridge is made of: `serveAgentPort`, `remoteSurface` and `combineSurfaces` for a surface across threads, `connectDevAgent` for the page's half, and `AgentSurfaceLike` for a surface whose answers are promises, which `handleMcpMessage` and `mcpHandler` now accept. `WorkerApp.openRenderPort(key)` opens a port to the render worker. The scaffolded `AGENTS.md` says how to connect.
|
|
33
|
+
|
|
34
|
+
- b90ecb2: An agent can operate the interface, not only the channels. Beside the channel tools, the page now offers `ui_snapshot`, the screen as an outline of what a screen reader announces with a short ref per control, and `ui_press`, `ui_type`, `ui_focus` and `ui_key`, which act on a control named by ref or by role and name and answer with the outline afterwards. They go through the accessibility mirror's own path, so a press is a click, a value is a keyboard edit, a disabled control refuses, and a focus trap holds. Available in the dev server endpoint and through WebMCP. `GessoRuntime.focusedNodeId()` reports which node holds focus.
|
|
35
|
+
|
|
36
|
+
The dev bridge also announces the page again whenever its HMR socket reconnects, so a restarted dev server no longer tells an agent that no page is open while one is.
|
|
37
|
+
|
|
38
|
+
- 28f5b72: `createApp({ pageKeys: true })` says the application is the page: a key pressed while nothing on the page has focus goes to the app, and the canvas takes focus. Keys only reached the app through its canvas, so a page that loads with focus on its body ignored every shortcut until the first click. The templates `create-gesso-app` writes turn it on; an app embedded in a larger page leaves it off.
|
|
39
|
+
- 765fd4d: The templates' pages set `overscroll-behavior: none`. A full-page app hands a scroll it can't use back to the page, and on a Mac Chrome then stretched the whole page and showed white behind it, or took a sideways swipe as Back.
|
|
40
|
+
- cf3b16a: `createApp({ webmcp: true })` offers an application's channels to an AI agent in the browser through WebMCP. Once the app mounts, every tool the agent surface offers, a view tool per channel and a tool per command, is registered with `document.modelContext.registerTool` (or the older `navigator.modelContext`), and removed when the app is disposed. View tools carry `readOnlyHint`, `@destructive` commands carry `consequentialHint`, a call answers with the view it left or rejects with the sentence that says why, and a `@confirm` command is put to the person with `window.confirm` unless `webmcp: { confirm }` supplies the application's own dialog. In a browser without WebMCP nothing is registered and nothing fails. The code loads on demand, so the shell is no bigger for an app that does not ask.
|
|
41
|
+
|
|
42
|
+
`gesso-framework/agent` adds `registerWebMcpTools`, `connectWebMcp`, `pageModelContext` and `confirmInWindow`. `gesso-vite-plugin` turns `webmcp` on in a dev server; the app's own setting still decides.
|
|
43
|
+
|
|
3
44
|
## 0.4.2
|
|
4
45
|
|
|
5
46
|
## 0.4.1
|
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# create-gesso-app
|
|
2
2
|
|
|
3
|
-
Scaffolds a Gesso
|
|
4
|
-
laid out and painted
|
|
3
|
+
Scaffolds a Gesso app: a native-grade web application built from
|
|
4
|
+
declarative components, whose interface is built, laid out and painted
|
|
5
|
+
in a render worker.
|
|
5
6
|
|
|
6
7
|
```bash
|
|
7
8
|
npm create gesso-app my-app
|
|
@@ -109,7 +109,8 @@ const TEMPLATES = {
|
|
|
109
109
|
"core",
|
|
110
110
|
"framework",
|
|
111
111
|
"components",
|
|
112
|
-
"electrobun"
|
|
112
|
+
"electrobun",
|
|
113
|
+
"vite-plugin"
|
|
113
114
|
],
|
|
114
115
|
substitute: [
|
|
115
116
|
"electrobun.config.ts",
|
|
@@ -214,7 +215,7 @@ function parseArgs(argv) {
|
|
|
214
215
|
/** Looks a template up, and lists the ones that exist when it is not one. */
|
|
215
216
|
function templateOf(name) {
|
|
216
217
|
const template = TEMPLATES[name];
|
|
217
|
-
if (template === void 0 || !existsSync(join(packageRoot, "templates", name))) fail(`There is no "${name}" template. Available: ${Object.keys(TEMPLATES).join(", ")}.`);
|
|
218
|
+
if (template === void 0 || name === SHARED_DIR || !existsSync(join(packageRoot, "templates", name))) fail(`There is no "${name}" template. Available: ${Object.keys(TEMPLATES).join(", ")}.`);
|
|
218
219
|
return template;
|
|
219
220
|
}
|
|
220
221
|
function prepareTarget(options) {
|
|
@@ -291,8 +292,22 @@ function vendorPackages(options, template) {
|
|
|
291
292
|
}
|
|
292
293
|
return specifiers;
|
|
293
294
|
}
|
|
294
|
-
/**
|
|
295
|
+
/**
|
|
296
|
+
* Files every template gets: `AGENTS.md`, the framework's rules for a
|
|
297
|
+
* coding agent, and the `CLAUDE.md` that points Claude Code at it.
|
|
298
|
+
*
|
|
299
|
+
* Shared rather than copied into each template because the rules are
|
|
300
|
+
* the framework's, not the template's, and two copies of them would
|
|
301
|
+
* drift. A template that needs to say something different writes its
|
|
302
|
+
* own file of the same name, which wins because it is copied second.
|
|
303
|
+
*/
|
|
304
|
+
const SHARED_DIR = "_shared";
|
|
305
|
+
/** Copies the shared files and then the template, renaming the files that had to be disguised. */
|
|
295
306
|
function copyTemplate(from, options) {
|
|
307
|
+
cpSync(join(packageRoot, "templates", SHARED_DIR), options.target, {
|
|
308
|
+
recursive: true,
|
|
309
|
+
force: true
|
|
310
|
+
});
|
|
296
311
|
cpSync(from, options.target, {
|
|
297
312
|
recursive: true,
|
|
298
313
|
force: true
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-gesso-app.mjs","names":[],"sources":["../bin/displayPath.ts","../bin/create-gesso-app.ts"],"sourcesContent":["import { relative } from 'node:path';\n\n/**\n * The new project's directory, written the way the reader should type it.\n *\n * This used to slice `process.cwd()` off the front of the target when\n * the target started with it, which is a string test standing in for a\n * path one. A sibling whose name extends the current directory's —\n * `../gessosheet` scaffolded from inside `gesso` — passes that test\n * without ever having been inside it, and the message printed the last\n * four characters of the name: \"Created gessosheet in heet.\"\n *\n * `relative` answers the question that was being asked. Which of the\n * two to print is then a readability choice rather than a correctness\n * one: a child or a sibling is shorter said relatively and is what the\n * caller typed, while a target on the far side of the tree is a run of\n * `..` segments that the absolute path beats. Shorter wins, and the\n * tie goes to the absolute path because it is unambiguous.\n */\nexport function displayPath(from: string, target: string): string {\n const fromHere = relative(from, target);\n if (fromHere === '') {\n return target;\n }\n return fromHere.length < target.length ? fromHere : target;\n}\n","#!/usr/bin/env node\n/**\n * `create-gesso-app`: scaffolds a Gesso application (the\n * last item).\n *\n * What it writes is the configuration this framework is for. The\n * application runs in a render worker; the page's own script creates\n * the app, hands it a worker constructor and mounts it, and that is the\n * whole of the main thread's job.\n *\n * By default the generated `package.json` names the published\n * `gesso-*` packages by version range and an install goes to the\n * registry, which is what a scaffold anywhere else does.\n *\n * `--local` is the other route, and the one this CLI keeps that a\n * public scaffolder would not: it packs the packages a template needs\n * out of this workspace with `pnpm pack`, drops the tarballs into the\n * new project's `vendor/` directory and writes `file:` specifiers at\n * them. That is the only way to scaffold against changes that are not\n * released yet, it is the same route `scripts/check-install.ts` takes,\n * and it is the route that exercises each package's `publishConfig`,\n * which is the only thing that rewrites `exports` from `src/*.ts` to\n * `dist`. `pnpm check:scaffold` runs this way, so the gate tests the\n * working tree rather than the last release.\n *\n * node packages/create-gesso-app/bin/create-gesso-app.ts ../my-app\n * node packages/create-gesso-app/bin/create-gesso-app.ts ../my-app --local\n * node packages/create-gesso-app/bin/create-gesso-app.ts ../my-app --local --no-build\n * node packages/create-gesso-app/bin/create-gesso-app.ts ../my-app --template electrobun\n *\n * The two templates differ in what runs beneath the same application.\n * `web` is a Vite project a browser loads. `electrobun` is a native\n * window whose application layer is a separate process, and it is set\n * up by Hutch rather than by npm: the Electrobun SDK is projected into\n * a project by `hutch electrobun prepare` rather than installed from a\n * registry, so the generated project depends on the toolchain being\n * present and says so.\n */\nimport { execFileSync } from 'node:child_process';\nimport {\n cpSync,\n existsSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n renameSync,\n rmSync,\n statSync,\n writeFileSync\n} from 'node:fs';\nimport { basename, isAbsolute, join, resolve } from 'node:path';\n\nimport { displayPath } from './displayPath.ts';\n\n/** Where the packed tarballs land inside the generated project. */\nconst VENDOR_DIR = 'vendor';\n/** Template files whose names cannot be checked into this repository as-is. */\nconst RENAMED = new Map([['_gitignore', '.gitignore']]);\n\ninterface Template {\n /** Workspace packages to pack into the project, in dependency order. */\n readonly vendored: readonly string[];\n /** Files carrying `{{name}}`, relative to the project directory. */\n readonly substitute: readonly string[];\n /** What to tell the person once the project is written. */\n readonly next: (name: string, where: string, local: boolean) => string;\n}\n\n/**\n * The templates, and the two things that differ between them: what a\n * project depends on, and how a person starts it.\n *\n * `devtools` and `vite-plugin` joined the web template's first three\n * when the feedback loop was wired in by default:\n * the plugin is what writes the worker construction and the\n * hot-replacement wiring, and it loads the overlay from devtools the\n * first time the worker throws. Both are development dependencies of\n * the generated project and neither is reachable from a production\n * build.\n */\nconst TEMPLATES: Record<string, Template> = {\n web: {\n vendored: ['core', 'framework', 'components', 'devtools', 'vite-plugin'],\n substitute: ['index.html', 'README.md'],\n next: (name, where, local) =>\n [\n `\\nCreated ${name} in ${where}.\\n`,\n ` cd ${where}`,\n ' pnpm install # or npm install',\n ' pnpm dev\\n',\n ...(local\n ? [\n 'Gesso came from this workspace rather than the registry, packed into',\n 'vendor/. The packed packages ask each other for version ranges, so npm',\n 'is pointed at the tarballs by `overrides` in package.json and pnpm by',\n \"`overrides` in pnpm-workspace.yaml. The project's README says how to\",\n 'move to the published packages.\\n'\n ]\n : [])\n ].join('\\n')\n },\n electrobun: {\n vendored: ['core', 'framework', 'components', 'electrobun'],\n substitute: ['electrobun.config.ts', 'src/main/index.ts', 'src/view/index.html', 'README.md'],\n next: (name, where) => `\nCreated ${name} in ${where}.\n\n cd ${where}\n hutch install\n hutch run dev\n\nHutch rather than npm, and this is the part to read before running it.\nElectrobun 2.x is a toolchain a launcher downloads, not a package a\nregistry serves: \\`hutch electrobun prepare\\` projects the SDK into the\nproject's own .hutch/devkit, which is where vite.config.ts and\ntsconfig.json look for it, and every script in hutch.config.ts runs that\nfirst. \\`hutch install\\` runs npm underneath. If \\`hutch\\` is not on your\npath yet, the project's README says how to get it.\n\n\\`pnpm check:scaffold:electrobun\\` installs, typechecks and builds a\nproject like this one without opening it. The window itself is checked\nby running it; the README records the last time that was done.\n`\n }\n};\n\nconst packageRoot = join(import.meta.dirname, '..');\nconst workspaceRoot = join(packageRoot, '..', '..');\n\ninterface Options {\n readonly target: string;\n readonly name: string;\n readonly template: string;\n readonly force: boolean;\n readonly build: boolean;\n /**\n * Install the packages out of this workspace instead of the registry.\n *\n * What every scaffold did before the packages were published, kept\n * because it is the only way to scaffold a project against changes\n * that are not released yet. `pnpm check:scaffold` runs this way, so\n * the gate tests the working tree rather than the last release.\n */\n readonly local: boolean;\n}\n\nconst USAGE = `Usage: create-gesso-app <directory> [options]\n\nCreates a Gesso application in <directory>: a project whose interface is\nbuilt, laid out and painted in a render worker.\n\nOptions:\n --name <name> Package name for the new project. Defaults to the\n directory's own name.\n --template <name> \"web\" for a Vite project a browser loads, or\n \"electrobun\" for a native window with its state in a\n main process. Defaults to \"web\".\n --force Write into a directory that already has files in it.\n --local Install the Gesso packages from this workspace, packed\n into the project, instead of from the registry. Only\n works inside a Gesso checkout.\n --no-build With --local, pack without rebuilding first. Only safe\n when dist/ is already current.\n -h, --help Print this.\n`;\n\nfunction fail(message: string): never {\n console.error(message);\n process.exit(1);\n}\n\nfunction parseArgs(argv: readonly string[]): Options {\n let target: string | undefined;\n let name: string | undefined;\n let template = 'web';\n let force = false;\n let build = true;\n let local = false;\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n switch (arg) {\n case '-h':\n case '--help':\n console.log(USAGE);\n process.exit(0);\n break;\n case '--force':\n force = true;\n break;\n case '--local':\n local = true;\n break;\n case '--no-build':\n build = false;\n break;\n case '--name':\n name = argv[++i];\n break;\n case '--template':\n template = argv[++i] ?? '';\n break;\n default:\n if (arg.startsWith('-')) {\n fail(`Unknown option ${arg}.\\n\\n${USAGE}`);\n }\n if (target !== undefined) {\n fail(`Two directories were given, ${target} and ${arg}.\\n\\n${USAGE}`);\n }\n target = arg;\n }\n }\n\n if (target === undefined) {\n fail(`A directory to create is required.\\n\\n${USAGE}`);\n }\n if (name !== undefined && !/^[a-z0-9][a-z0-9._-]*$/.test(name)) {\n fail(`\"${name}\" is not a usable package name: use lowercase letters, digits, dots, dashes and underscores.`);\n }\n\n const absolute = isAbsolute(target) ? target : resolve(process.cwd(), target);\n return { target: absolute, name: name ?? basename(absolute), template, force, build, local };\n}\n\n/** Looks a template up, and lists the ones that exist when it is not one. */\nfunction templateOf(name: string): Template {\n const template = TEMPLATES[name];\n if (template === undefined || !existsSync(join(packageRoot, 'templates', name))) {\n fail(`There is no \"${name}\" template. Available: ${Object.keys(TEMPLATES).join(', ')}.`);\n }\n return template;\n}\n\nfunction prepareTarget(options: Options): void {\n if (!existsSync(options.target)) {\n mkdirSync(options.target, { recursive: true });\n return;\n }\n const existing = readdirSync(options.target);\n if (existing.length > 0 && !options.force) {\n fail(`${options.target} is not empty (${existing.length} entries). Pass --force to write into it anyway.`);\n }\n}\n\n/**\n * Whether a package's `dist` is older than anything in its `src`.\n *\n * A newest-mtime comparison rather than a build system: it is wrong\n * only in the direction of building something that did not need it,\n * and a missing `dist` always builds. `--no-build` skips the question\n * entirely for the case where the caller knows.\n */\nfunction needsBuild(pkg: string): boolean {\n const dir = join(workspaceRoot, 'packages', pkg);\n const dist = join(dir, 'dist');\n if (!existsSync(dist)) {\n return true;\n }\n return newestChange(join(dir, 'src')) > newestChange(dist);\n}\n\n/** The most recent modification time anywhere under a directory. */\nfunction newestChange(dir: string): number {\n let newest = 0;\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n const path = join(dir, entry.name);\n newest = Math.max(newest, entry.isDirectory() ? newestChange(path) : statSync(path).mtimeMs);\n }\n return newest;\n}\n\n/**\n * Packs the workspace packages into the new project and returns the\n * `file:` specifier for each.\n *\n * The specifiers are relative to the project directory, so the whole\n * directory can be moved or copied and still install.\n */\nfunction vendorPackages(options: Options, template: Template): Map<string, string> {\n if (options.build) {\n // Only the packages that go into the project, and only the ones\n // whose `dist` is older than their `src`. Building the whole\n // workspace took the better part of a minute for a scaffold that\n // does not install two of the packages it built, and every second\n // of it was spent between a person typing a command and seeing\n // anything happen.\n const stale = template.vendored.filter(needsBuild);\n if (stale.length > 0) {\n console.log(`building ${stale.join(', ')}…`);\n const filters = stale.flatMap(pkg => ['--filter', `./packages/${pkg}`]);\n execFileSync('pnpm', [...filters, 'build'], { cwd: workspaceRoot, stdio: 'ignore' });\n }\n }\n\n const into = join(options.target, VENDOR_DIR);\n rmSync(into, { recursive: true, force: true });\n mkdirSync(into, { recursive: true });\n\n console.log('packing them into the new project…');\n const specifiers = new Map<string, string>();\n for (const pkg of template.vendored) {\n const before = new Set(readdirSync(into));\n execFileSync('pnpm', ['pack', '--pack-destination', into], {\n cwd: join(workspaceRoot, 'packages', pkg),\n stdio: 'ignore'\n });\n const created = readdirSync(into).filter(entry => !before.has(entry) && entry.endsWith('.tgz'));\n if (created.length !== 1) {\n fail(`pnpm pack in packages/${pkg} produced ${created.length} tarballs, expected 1.`);\n }\n specifiers.set(`gesso-${pkg}`, `file:${VENDOR_DIR}/${created[0]}`);\n }\n return specifiers;\n}\n\n/** Copies the template, renaming the files that had to be disguised. */\nfunction copyTemplate(from: string, options: Options): void {\n // `force` here is not the option of the same name: whether writing\n // into an occupied directory is allowed was settled by `prepareTarget`,\n // and a template file always wins over whatever it lands on.\n cpSync(from, options.target, { recursive: true, force: true });\n for (const [disguised, real] of RENAMED) {\n const path = join(options.target, disguised);\n if (existsSync(path)) {\n renameSync(path, join(options.target, real));\n }\n }\n}\n\n/**\n * Writes the project's name and its dependency specifiers.\n *\n * `overrides` carries the same specifiers, because the packages declare\n * each other by version range: `gesso-framework` and\n * `gesso-components` do, and so does `gesso-electrobun`. Without it an\n * installer scaffolded with `--local` is free to resolve\n * `gesso-core@^0.1.0` off the registry rather than from the tarball\n * beside it, and whether it does so depends on what else is in the\n * tree. Without `--local` there are no specifiers and no `overrides`.\n */\nfunction writeManifest(options: Options, specifiers: ReadonlyMap<string, string>): void {\n const path = join(options.target, 'package.json');\n const manifest = JSON.parse(readFileSync(path, 'utf8')) as {\n name: string;\n dependencies: Record<string, string>;\n devDependencies?: Record<string, string>;\n overrides?: Record<string, string>;\n };\n manifest.name = options.name;\n for (const [pkg, specifier] of specifiers) {\n // The plugin and the overlay are development dependencies and the\n // runtime packages are not, so the specifier goes wherever the\n // template already declared the package.\n const where =\n manifest.dependencies[pkg] !== undefined\n ? manifest.dependencies\n : manifest.devDependencies?.[pkg] !== undefined\n ? manifest.devDependencies\n : undefined;\n if (where === undefined) {\n fail(`The ${options.template} template does not depend on ${pkg}, so there is nowhere to vendor it.`);\n }\n where[pkg] = specifier;\n }\n if (specifiers.size > 0) {\n manifest.overrides = Object.fromEntries(specifiers);\n } else {\n delete manifest.overrides;\n }\n writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\\n`);\n}\n\n/**\n * Says the same thing to pnpm that `overrides` says to npm.\n *\n * npm was chosen because pnpm did not work, and the reason is still\n * exactly right: `pnpm pack` rewrites `workspace:^` into `^0.1.0`, so\n * the packed `gesso-framework` asks for `gesso-core@^0.1.0` and pnpm 11\n * resolves it off the registry rather than from the tarball beside it,\n * which is a different copy of the package than the one this project\n * was told to use. What that record then rejected was shipping a\n * `pnpm-workspace.yaml` in a project that is not a workspace.\n *\n * That trade has moved. The file is five lines, it is the only place\n * pnpm 11 reads `overrides` from, and the alternative is a scaffold\n * that fails for the package manager this repository itself uses. It\n * says what it is for, and it is written only under `--local`.\n */\nfunction writePnpmOverrides(options: Options, specifiers: ReadonlyMap<string, string>): void {\n const lines = [\n '# The tarballs in vendor/ again, for pnpm.',\n '#',\n '# This project was scaffolded with --local, so the packed gesso-*',\n '# packages ask each other for version ranges the registry would',\n '# answer with a different copy. npm reads the `overrides` in',\n '# package.json; pnpm 11 reads only this file. To move to the',\n '# published packages: delete vendor/, this file and `overrides`,',\n '# and put version ranges back.',\n 'overrides:',\n ...[...specifiers].map(([pkg, specifier]) => ` '${pkg}': '${specifier}'`),\n ''\n ];\n writeFileSync(join(options.target, 'pnpm-workspace.yaml'), lines.join('\\n'));\n}\n\n/**\n * Substitutes the template's placeholders in the files that carry them.\n *\n * The list is the template's rather than a walk of the tree, so that a\n * placeholder added to a file nobody listed fails visibly in the\n * generated project instead of being quietly left as `{{name}}`\n * somewhere it is never read.\n */\nfunction substitute(options: Options, template: Template): void {\n for (const relative of template.substitute) {\n const path = join(options.target, relative);\n if (!existsSync(path)) {\n fail(`The ${options.template} template says ${relative} carries {{name}}, and it was not written.`);\n }\n writeFileSync(path, readFileSync(path, 'utf8').replaceAll('{{name}}', options.name));\n }\n}\n\n/**\n * Appends the `vendor/` explanation to the generated README.\n *\n * It lives here rather than in the template because a project made\n * without `--local` has no `vendor/`, and a README explaining a\n * directory that is not there is worse than one that says nothing.\n */\nfunction explainVendoring(options: Options): void {\n const path = join(options.target, 'README.md');\n if (!existsSync(path)) {\n return;\n }\n const section = [\n '',\n '## Why `vendor/` exists, and how to remove it',\n '',\n 'This project was scaffolded with `--local`, so it installs Gesso from',\n 'a checkout rather than from the registry: the packages were packed',\n 'into `vendor/` and the manifest points at the tarballs.',\n '',\n 'The packed packages declare each other by version range, so without',\n 'help a package manager is free to go looking for `gesso-core@^0.1.0`',\n 'on the registry and get a different copy than the one beside it.',\n '`overrides` in `package.json` is what tells npm; `overrides` in',\n '`pnpm-workspace.yaml` is what tells pnpm, which reads it nowhere else.',\n '',\n 'To pick up a further change, run `create-gesso-app --local` over this',\n 'directory again with `--force`.',\n '',\n 'To move to the published packages: delete `vendor/`, delete',\n '`pnpm-workspace.yaml`, delete `overrides`, and put version ranges back',\n 'in `dependencies`.',\n ''\n ].join('\\n');\n writeFileSync(path, readFileSync(path, 'utf8').trimEnd() + '\\n' + section);\n}\n\nfunction main(): void {\n const options = parseArgs(process.argv.slice(2));\n if (options.local && !existsSync(join(workspaceRoot, 'packages', 'core', 'package.json'))) {\n fail(\n '--local packs the packages out of the Gesso workspace, and this is not one.\\n' +\n 'Run it from inside a checkout, or drop --local to install from the registry.'\n );\n }\n\n const template = templateOf(options.template);\n prepareTarget(options);\n copyTemplate(join(packageRoot, 'templates', options.template), options);\n // Without --local the template's own version ranges are the answer,\n // and the manifest needs nothing but its name.\n const specifiers = options.local ? vendorPackages(options, template) : new Map<string, string>();\n writeManifest(options, specifiers);\n if (options.local) {\n writePnpmOverrides(options, specifiers);\n explainVendoring(options);\n }\n substitute(options, template);\n\n console.log(template.next(options.name, displayPath(process.cwd(), options.target), options.local));\n}\n\nmain();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,YAAY,MAAc,QAAwB;CAChE,MAAM,WAAW,SAAS,MAAM,MAAM;CACtC,IAAI,aAAa,IACf,OAAO;CAET,OAAO,SAAS,SAAS,OAAO,SAAS,WAAW;AACtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC8BA,MAAM,aAAa;;AAEnB,MAAM,0BAAU,IAAI,IAAI,CAAC,CAAC,cAAc,YAAY,CAAC,CAAC;;;;;;;;;;;;;AAuBtD,MAAM,YAAsC;CAC1C,KAAK;EACH,UAAU;GAAC;GAAQ;GAAa;GAAc;GAAY;EAAa;EACvE,YAAY,CAAC,cAAc,WAAW;EACtC,OAAO,MAAM,OAAO,UAClB;GACE,aAAa,KAAK,MAAM,MAAM;GAC9B,QAAQ;GACR;GACA;GACA,GAAI,QACA;IACE;IACA;IACA;IACA;IACA;GACF,IACA,CAAC;EACP,CAAC,CAAC,KAAK,IAAI;CACf;CACA,YAAY;EACV,UAAU;GAAC;GAAQ;GAAa;GAAc;EAAY;EAC1D,YAAY;GAAC;GAAwB;GAAqB;GAAuB;EAAW;EAC5F,OAAO,MAAM,UAAU;UACjB,KAAK,MAAM,MAAM;;OAEpB,MAAM;;;;;;;;;;;;;;;;CAgBX;AACF;AAEA,MAAM,cAAc,KAAK,YAAY,SAAS,IAAI;AAClD,MAAM,gBAAgB,KAAK,aAAa,MAAM,IAAI;AAmBlD,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;AAoBd,SAAS,KAAK,SAAwB;CACpC,QAAQ,MAAM,OAAO;CACrB,QAAQ,KAAK,CAAC;AAChB;AAEA,SAAS,UAAU,MAAkC;CACnD,IAAI;CACJ,IAAI;CACJ,IAAI,WAAW;CACf,IAAI,QAAQ;CACZ,IAAI,QAAQ;CACZ,IAAI,QAAQ;CAEZ,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EACjB,QAAQ,KAAR;GACE,KAAK;GACL,KAAK;IACH,QAAQ,IAAI,KAAK;IACjB,QAAQ,KAAK,CAAC;IACd;GACF,KAAK;IACH,QAAQ;IACR;GACF,KAAK;IACH,QAAQ;IACR;GACF,KAAK;IACH,QAAQ;IACR;GACF,KAAK;IACH,OAAO,KAAK,EAAE;IACd;GACF,KAAK;IACH,WAAW,KAAK,EAAE,MAAM;IACxB;GACF;IACE,IAAI,IAAI,WAAW,GAAG,GACpB,KAAK,kBAAkB,IAAI,OAAO,OAAO;IAE3C,IAAI,WAAW,KAAA,GACb,KAAK,+BAA+B,OAAO,OAAO,IAAI,OAAO,OAAO;IAEtE,SAAS;EACb;CACF;CAEA,IAAI,WAAW,KAAA,GACb,KAAK,yCAAyC,OAAO;CAEvD,IAAI,SAAS,KAAA,KAAa,CAAC,yBAAyB,KAAK,IAAI,GAC3D,KAAK,IAAI,KAAK,6FAA6F;CAG7G,MAAM,WAAW,WAAW,MAAM,IAAI,SAAS,QAAQ,QAAQ,IAAI,GAAG,MAAM;CAC5E,OAAO;EAAE,QAAQ;EAAU,MAAM,QAAQ,SAAS,QAAQ;EAAG;EAAU;EAAO;EAAO;CAAM;AAC7F;;AAGA,SAAS,WAAW,MAAwB;CAC1C,MAAM,WAAW,UAAU;CAC3B,IAAI,aAAa,KAAA,KAAa,CAAC,WAAW,KAAK,aAAa,aAAa,IAAI,CAAC,GAC5E,KAAK,gBAAgB,KAAK,yBAAyB,OAAO,KAAK,SAAS,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE;CAEzF,OAAO;AACT;AAEA,SAAS,cAAc,SAAwB;CAC7C,IAAI,CAAC,WAAW,QAAQ,MAAM,GAAG;EAC/B,UAAU,QAAQ,QAAQ,EAAE,WAAW,KAAK,CAAC;EAC7C;CACF;CACA,MAAM,WAAW,YAAY,QAAQ,MAAM;CAC3C,IAAI,SAAS,SAAS,KAAK,CAAC,QAAQ,OAClC,KAAK,GAAG,QAAQ,OAAO,iBAAiB,SAAS,OAAO,iDAAiD;AAE7G;;;;;;;;;AAUA,SAAS,WAAW,KAAsB;CACxC,MAAM,MAAM,KAAK,eAAe,YAAY,GAAG;CAC/C,MAAM,OAAO,KAAK,KAAK,MAAM;CAC7B,IAAI,CAAC,WAAW,IAAI,GAClB,OAAO;CAET,OAAO,aAAa,KAAK,KAAK,KAAK,CAAC,IAAI,aAAa,IAAI;AAC3D;;AAGA,SAAS,aAAa,KAAqB;CACzC,IAAI,SAAS;CACb,KAAK,MAAM,SAAS,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG;EAC7D,MAAM,OAAO,KAAK,KAAK,MAAM,IAAI;EACjC,SAAS,KAAK,IAAI,QAAQ,MAAM,YAAY,IAAI,aAAa,IAAI,IAAI,SAAS,IAAI,CAAC,CAAC,OAAO;CAC7F;CACA,OAAO;AACT;;;;;;;;AASA,SAAS,eAAe,SAAkB,UAAyC;CACjF,IAAI,QAAQ,OAAO;EAOjB,MAAM,QAAQ,SAAS,SAAS,OAAO,UAAU;EACjD,IAAI,MAAM,SAAS,GAAG;GACpB,QAAQ,IAAI,YAAY,MAAM,KAAK,IAAI,EAAE,EAAE;GAC3C,MAAM,UAAU,MAAM,SAAQ,QAAO,CAAC,YAAY,cAAc,KAAK,CAAC;GACtE,aAAa,QAAQ,CAAC,GAAG,SAAS,OAAO,GAAG;IAAE,KAAK;IAAe,OAAO;GAAS,CAAC;EACrF;CACF;CAEA,MAAM,OAAO,KAAK,QAAQ,QAAQ,UAAU;CAC5C,OAAO,MAAM;EAAE,WAAW;EAAM,OAAO;CAAK,CAAC;CAC7C,UAAU,MAAM,EAAE,WAAW,KAAK,CAAC;CAEnC,QAAQ,IAAI,oCAAoC;CAChD,MAAM,6BAAa,IAAI,IAAoB;CAC3C,KAAK,MAAM,OAAO,SAAS,UAAU;EACnC,MAAM,SAAS,IAAI,IAAI,YAAY,IAAI,CAAC;EACxC,aAAa,QAAQ;GAAC;GAAQ;GAAsB;EAAI,GAAG;GACzD,KAAK,KAAK,eAAe,YAAY,GAAG;GACxC,OAAO;EACT,CAAC;EACD,MAAM,UAAU,YAAY,IAAI,CAAC,CAAC,QAAO,UAAS,CAAC,OAAO,IAAI,KAAK,KAAK,MAAM,SAAS,MAAM,CAAC;EAC9F,IAAI,QAAQ,WAAW,GACrB,KAAK,yBAAyB,IAAI,YAAY,QAAQ,OAAO,uBAAuB;EAEtF,WAAW,IAAI,SAAS,OAAO,QAAQ,WAAW,GAAG,QAAQ,IAAI;CACnE;CACA,OAAO;AACT;;AAGA,SAAS,aAAa,MAAc,SAAwB;CAI1D,OAAO,MAAM,QAAQ,QAAQ;EAAE,WAAW;EAAM,OAAO;CAAK,CAAC;CAC7D,KAAK,MAAM,CAAC,WAAW,SAAS,SAAS;EACvC,MAAM,OAAO,KAAK,QAAQ,QAAQ,SAAS;EAC3C,IAAI,WAAW,IAAI,GACjB,WAAW,MAAM,KAAK,QAAQ,QAAQ,IAAI,CAAC;CAE/C;AACF;;;;;;;;;;;;AAaA,SAAS,cAAc,SAAkB,YAA+C;CACtF,MAAM,OAAO,KAAK,QAAQ,QAAQ,cAAc;CAChD,MAAM,WAAW,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;CAMtD,SAAS,OAAO,QAAQ;CACxB,KAAK,MAAM,CAAC,KAAK,cAAc,YAAY;EAIzC,MAAM,QACJ,SAAS,aAAa,SAAS,KAAA,IAC3B,SAAS,eACT,SAAS,kBAAkB,SAAS,KAAA,IAClC,SAAS,kBACT,KAAA;EACR,IAAI,UAAU,KAAA,GACZ,KAAK,OAAO,QAAQ,SAAS,+BAA+B,IAAI,oCAAoC;EAEtG,MAAM,OAAO;CACf;CACA,IAAI,WAAW,OAAO,GACpB,SAAS,YAAY,OAAO,YAAY,UAAU;MAElD,OAAO,SAAS;CAElB,cAAc,MAAM,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,EAAE,GAAG;AAC9D;;;;;;;;;;;;;;;;;AAkBA,SAAS,mBAAmB,SAAkB,YAA+C;CAC3F,MAAM,QAAQ;EACZ;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC,KAAK,CAAC,KAAK,eAAe,MAAM,IAAI,MAAM,UAAU,EAAE;EACzE;CACF;CACA,cAAc,KAAK,QAAQ,QAAQ,qBAAqB,GAAG,MAAM,KAAK,IAAI,CAAC;AAC7E;;;;;;;;;AAUA,SAAS,WAAW,SAAkB,UAA0B;CAC9D,KAAK,MAAM,YAAY,SAAS,YAAY;EAC1C,MAAM,OAAO,KAAK,QAAQ,QAAQ,QAAQ;EAC1C,IAAI,CAAC,WAAW,IAAI,GAClB,KAAK,OAAO,QAAQ,SAAS,iBAAiB,SAAS,2CAA2C;EAEpG,cAAc,MAAM,aAAa,MAAM,MAAM,CAAC,CAAC,WAAW,YAAY,QAAQ,IAAI,CAAC;CACrF;AACF;;;;;;;;AASA,SAAS,iBAAiB,SAAwB;CAChD,MAAM,OAAO,KAAK,QAAQ,QAAQ,WAAW;CAC7C,IAAI,CAAC,WAAW,IAAI,GAClB;CAEF,MAAM,UAAU;EACd;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC,CAAC,KAAK,IAAI;CACX,cAAc,MAAM,aAAa,MAAM,MAAM,CAAC,CAAC,QAAQ,IAAI,OAAO,OAAO;AAC3E;AAEA,SAAS,OAAa;CACpB,MAAM,UAAU,UAAU,QAAQ,KAAK,MAAM,CAAC,CAAC;CAC/C,IAAI,QAAQ,SAAS,CAAC,WAAW,KAAK,eAAe,YAAY,QAAQ,cAAc,CAAC,GACtF,KACE,2JAEF;CAGF,MAAM,WAAW,WAAW,QAAQ,QAAQ;CAC5C,cAAc,OAAO;CACrB,aAAa,KAAK,aAAa,aAAa,QAAQ,QAAQ,GAAG,OAAO;CAGtE,MAAM,aAAa,QAAQ,QAAQ,eAAe,SAAS,QAAQ,oBAAI,IAAI,IAAoB;CAC/F,cAAc,SAAS,UAAU;CACjC,IAAI,QAAQ,OAAO;EACjB,mBAAmB,SAAS,UAAU;EACtC,iBAAiB,OAAO;CAC1B;CACA,WAAW,SAAS,QAAQ;CAE5B,QAAQ,IAAI,SAAS,KAAK,QAAQ,MAAM,YAAY,QAAQ,IAAI,GAAG,QAAQ,MAAM,GAAG,QAAQ,KAAK,CAAC;AACpG;AAEA,KAAK"}
|
|
1
|
+
{"version":3,"file":"create-gesso-app.mjs","names":[],"sources":["../bin/displayPath.ts","../bin/create-gesso-app.ts"],"sourcesContent":["import { relative } from 'node:path';\n\n/**\n * The new project's directory, written the way the reader should type it.\n *\n * This used to slice `process.cwd()` off the front of the target when\n * the target started with it, which is a string test standing in for a\n * path one. A sibling whose name extends the current directory's —\n * `../gessosheet` scaffolded from inside `gesso` — passes that test\n * without ever having been inside it, and the message printed the last\n * four characters of the name: \"Created gessosheet in heet.\"\n *\n * `relative` answers the question that was being asked. Which of the\n * two to print is then a readability choice rather than a correctness\n * one: a child or a sibling is shorter said relatively and is what the\n * caller typed, while a target on the far side of the tree is a run of\n * `..` segments that the absolute path beats. Shorter wins, and the\n * tie goes to the absolute path because it is unambiguous.\n */\nexport function displayPath(from: string, target: string): string {\n const fromHere = relative(from, target);\n if (fromHere === '') {\n return target;\n }\n return fromHere.length < target.length ? fromHere : target;\n}\n","#!/usr/bin/env node\n/**\n * `create-gesso-app`: scaffolds a Gesso application (the\n * last item).\n *\n * What it writes is the configuration this framework is for. The\n * application runs in a render worker; the page's own script creates\n * the app, hands it a worker constructor and mounts it, and that is the\n * whole of the main thread's job.\n *\n * By default the generated `package.json` names the published\n * `gesso-*` packages by version range and an install goes to the\n * registry, which is what a scaffold anywhere else does.\n *\n * `--local` is the other route, and the one this CLI keeps that a\n * public scaffolder would not: it packs the packages a template needs\n * out of this workspace with `pnpm pack`, drops the tarballs into the\n * new project's `vendor/` directory and writes `file:` specifiers at\n * them. That is the only way to scaffold against changes that are not\n * released yet, it is the same route `scripts/check-install.ts` takes,\n * and it is the route that exercises each package's `publishConfig`,\n * which is the only thing that rewrites `exports` from `src/*.ts` to\n * `dist`. `pnpm check:scaffold` runs this way, so the gate tests the\n * working tree rather than the last release.\n *\n * node packages/create-gesso-app/bin/create-gesso-app.ts ../my-app\n * node packages/create-gesso-app/bin/create-gesso-app.ts ../my-app --local\n * node packages/create-gesso-app/bin/create-gesso-app.ts ../my-app --local --no-build\n * node packages/create-gesso-app/bin/create-gesso-app.ts ../my-app --template electrobun\n *\n * The two templates differ in what runs beneath the same application.\n * `web` is a Vite project a browser loads. `electrobun` is a native\n * window whose application layer is a separate process, and it is set\n * up by Hutch rather than by npm: the Electrobun SDK is projected into\n * a project by `hutch electrobun prepare` rather than installed from a\n * registry, so the generated project depends on the toolchain being\n * present and says so.\n */\nimport { execFileSync } from 'node:child_process';\nimport {\n cpSync,\n existsSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n renameSync,\n rmSync,\n statSync,\n writeFileSync\n} from 'node:fs';\nimport { basename, isAbsolute, join, resolve } from 'node:path';\n\nimport { displayPath } from './displayPath.ts';\n\n/** Where the packed tarballs land inside the generated project. */\nconst VENDOR_DIR = 'vendor';\n/** Template files whose names cannot be checked into this repository as-is. */\nconst RENAMED = new Map([['_gitignore', '.gitignore']]);\n\ninterface Template {\n /** Workspace packages to pack into the project, in dependency order. */\n readonly vendored: readonly string[];\n /** Files carrying `{{name}}`, relative to the project directory. */\n readonly substitute: readonly string[];\n /** What to tell the person once the project is written. */\n readonly next: (name: string, where: string, local: boolean) => string;\n}\n\n/**\n * The templates, and the two things that differ between them: what a\n * project depends on, and how a person starts it.\n *\n * `devtools` and `vite-plugin` joined the web template's first three\n * when the feedback loop was wired in by default:\n * the plugin is what writes the worker construction and the\n * hot-replacement wiring, and it loads the overlay from devtools the\n * first time the worker throws. Both are development dependencies of\n * the generated project and neither is reachable from a production\n * build.\n */\nconst TEMPLATES: Record<string, Template> = {\n web: {\n vendored: ['core', 'framework', 'components', 'devtools', 'vite-plugin'],\n substitute: ['index.html', 'README.md'],\n next: (name, where, local) =>\n [\n `\\nCreated ${name} in ${where}.\\n`,\n ` cd ${where}`,\n ' pnpm install # or npm install',\n ' pnpm dev\\n',\n ...(local\n ? [\n 'Gesso came from this workspace rather than the registry, packed into',\n 'vendor/. The packed packages ask each other for version ranges, so npm',\n 'is pointed at the tarballs by `overrides` in package.json and pnpm by',\n \"`overrides` in pnpm-workspace.yaml. The project's README says how to\",\n 'move to the published packages.\\n'\n ]\n : [])\n ].join('\\n')\n },\n electrobun: {\n vendored: ['core', 'framework', 'components', 'electrobun', 'vite-plugin'],\n substitute: ['electrobun.config.ts', 'src/main/index.ts', 'src/view/index.html', 'README.md'],\n next: (name, where) => `\nCreated ${name} in ${where}.\n\n cd ${where}\n hutch install\n hutch run dev\n\nHutch rather than npm, and this is the part to read before running it.\nElectrobun 2.x is a toolchain a launcher downloads, not a package a\nregistry serves: \\`hutch electrobun prepare\\` projects the SDK into the\nproject's own .hutch/devkit, which is where vite.config.ts and\ntsconfig.json look for it, and every script in hutch.config.ts runs that\nfirst. \\`hutch install\\` runs npm underneath. If \\`hutch\\` is not on your\npath yet, the project's README says how to get it.\n\n\\`pnpm check:scaffold:electrobun\\` installs, typechecks and builds a\nproject like this one without opening it. The window itself is checked\nby running it; the README records the last time that was done.\n`\n }\n};\n\nconst packageRoot = join(import.meta.dirname, '..');\nconst workspaceRoot = join(packageRoot, '..', '..');\n\ninterface Options {\n readonly target: string;\n readonly name: string;\n readonly template: string;\n readonly force: boolean;\n readonly build: boolean;\n /**\n * Install the packages out of this workspace instead of the registry.\n *\n * What every scaffold did before the packages were published, kept\n * because it is the only way to scaffold a project against changes\n * that are not released yet. `pnpm check:scaffold` runs this way, so\n * the gate tests the working tree rather than the last release.\n */\n readonly local: boolean;\n}\n\nconst USAGE = `Usage: create-gesso-app <directory> [options]\n\nCreates a Gesso application in <directory>: a project whose interface is\nbuilt, laid out and painted in a render worker.\n\nOptions:\n --name <name> Package name for the new project. Defaults to the\n directory's own name.\n --template <name> \"web\" for a Vite project a browser loads, or\n \"electrobun\" for a native window with its state in a\n main process. Defaults to \"web\".\n --force Write into a directory that already has files in it.\n --local Install the Gesso packages from this workspace, packed\n into the project, instead of from the registry. Only\n works inside a Gesso checkout.\n --no-build With --local, pack without rebuilding first. Only safe\n when dist/ is already current.\n -h, --help Print this.\n`;\n\nfunction fail(message: string): never {\n console.error(message);\n process.exit(1);\n}\n\nfunction parseArgs(argv: readonly string[]): Options {\n let target: string | undefined;\n let name: string | undefined;\n let template = 'web';\n let force = false;\n let build = true;\n let local = false;\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n switch (arg) {\n case '-h':\n case '--help':\n console.log(USAGE);\n process.exit(0);\n break;\n case '--force':\n force = true;\n break;\n case '--local':\n local = true;\n break;\n case '--no-build':\n build = false;\n break;\n case '--name':\n name = argv[++i];\n break;\n case '--template':\n template = argv[++i] ?? '';\n break;\n default:\n if (arg.startsWith('-')) {\n fail(`Unknown option ${arg}.\\n\\n${USAGE}`);\n }\n if (target !== undefined) {\n fail(`Two directories were given, ${target} and ${arg}.\\n\\n${USAGE}`);\n }\n target = arg;\n }\n }\n\n if (target === undefined) {\n fail(`A directory to create is required.\\n\\n${USAGE}`);\n }\n if (name !== undefined && !/^[a-z0-9][a-z0-9._-]*$/.test(name)) {\n fail(`\"${name}\" is not a usable package name: use lowercase letters, digits, dots, dashes and underscores.`);\n }\n\n const absolute = isAbsolute(target) ? target : resolve(process.cwd(), target);\n return { target: absolute, name: name ?? basename(absolute), template, force, build, local };\n}\n\n/** Looks a template up, and lists the ones that exist when it is not one. */\nfunction templateOf(name: string): Template {\n const template = TEMPLATES[name];\n if (template === undefined || name === SHARED_DIR || !existsSync(join(packageRoot, 'templates', name))) {\n fail(`There is no \"${name}\" template. Available: ${Object.keys(TEMPLATES).join(', ')}.`);\n }\n return template;\n}\n\nfunction prepareTarget(options: Options): void {\n if (!existsSync(options.target)) {\n mkdirSync(options.target, { recursive: true });\n return;\n }\n const existing = readdirSync(options.target);\n if (existing.length > 0 && !options.force) {\n fail(`${options.target} is not empty (${existing.length} entries). Pass --force to write into it anyway.`);\n }\n}\n\n/**\n * Whether a package's `dist` is older than anything in its `src`.\n *\n * A newest-mtime comparison rather than a build system: it is wrong\n * only in the direction of building something that did not need it,\n * and a missing `dist` always builds. `--no-build` skips the question\n * entirely for the case where the caller knows.\n */\nfunction needsBuild(pkg: string): boolean {\n const dir = join(workspaceRoot, 'packages', pkg);\n const dist = join(dir, 'dist');\n if (!existsSync(dist)) {\n return true;\n }\n return newestChange(join(dir, 'src')) > newestChange(dist);\n}\n\n/** The most recent modification time anywhere under a directory. */\nfunction newestChange(dir: string): number {\n let newest = 0;\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n const path = join(dir, entry.name);\n newest = Math.max(newest, entry.isDirectory() ? newestChange(path) : statSync(path).mtimeMs);\n }\n return newest;\n}\n\n/**\n * Packs the workspace packages into the new project and returns the\n * `file:` specifier for each.\n *\n * The specifiers are relative to the project directory, so the whole\n * directory can be moved or copied and still install.\n */\nfunction vendorPackages(options: Options, template: Template): Map<string, string> {\n if (options.build) {\n // Only the packages that go into the project, and only the ones\n // whose `dist` is older than their `src`. Building the whole\n // workspace took the better part of a minute for a scaffold that\n // does not install two of the packages it built, and every second\n // of it was spent between a person typing a command and seeing\n // anything happen.\n const stale = template.vendored.filter(needsBuild);\n if (stale.length > 0) {\n console.log(`building ${stale.join(', ')}…`);\n const filters = stale.flatMap(pkg => ['--filter', `./packages/${pkg}`]);\n execFileSync('pnpm', [...filters, 'build'], { cwd: workspaceRoot, stdio: 'ignore' });\n }\n }\n\n const into = join(options.target, VENDOR_DIR);\n rmSync(into, { recursive: true, force: true });\n mkdirSync(into, { recursive: true });\n\n console.log('packing them into the new project…');\n const specifiers = new Map<string, string>();\n for (const pkg of template.vendored) {\n const before = new Set(readdirSync(into));\n execFileSync('pnpm', ['pack', '--pack-destination', into], {\n cwd: join(workspaceRoot, 'packages', pkg),\n stdio: 'ignore'\n });\n const created = readdirSync(into).filter(entry => !before.has(entry) && entry.endsWith('.tgz'));\n if (created.length !== 1) {\n fail(`pnpm pack in packages/${pkg} produced ${created.length} tarballs, expected 1.`);\n }\n specifiers.set(`gesso-${pkg}`, `file:${VENDOR_DIR}/${created[0]}`);\n }\n return specifiers;\n}\n\n/**\n * Files every template gets: `AGENTS.md`, the framework's rules for a\n * coding agent, and the `CLAUDE.md` that points Claude Code at it.\n *\n * Shared rather than copied into each template because the rules are\n * the framework's, not the template's, and two copies of them would\n * drift. A template that needs to say something different writes its\n * own file of the same name, which wins because it is copied second.\n */\nconst SHARED_DIR = '_shared';\n\n/** Copies the shared files and then the template, renaming the files that had to be disguised. */\nfunction copyTemplate(from: string, options: Options): void {\n // `force` here is not the option of the same name: whether writing\n // into an occupied directory is allowed was settled by `prepareTarget`,\n // and a template file always wins over whatever it lands on.\n cpSync(join(packageRoot, 'templates', SHARED_DIR), options.target, { recursive: true, force: true });\n cpSync(from, options.target, { recursive: true, force: true });\n for (const [disguised, real] of RENAMED) {\n const path = join(options.target, disguised);\n if (existsSync(path)) {\n renameSync(path, join(options.target, real));\n }\n }\n}\n\n/**\n * Writes the project's name and its dependency specifiers.\n *\n * `overrides` carries the same specifiers, because the packages declare\n * each other by version range: `gesso-framework` and\n * `gesso-components` do, and so does `gesso-electrobun`. Without it an\n * installer scaffolded with `--local` is free to resolve\n * `gesso-core@^0.1.0` off the registry rather than from the tarball\n * beside it, and whether it does so depends on what else is in the\n * tree. Without `--local` there are no specifiers and no `overrides`.\n */\nfunction writeManifest(options: Options, specifiers: ReadonlyMap<string, string>): void {\n const path = join(options.target, 'package.json');\n const manifest = JSON.parse(readFileSync(path, 'utf8')) as {\n name: string;\n dependencies: Record<string, string>;\n devDependencies?: Record<string, string>;\n overrides?: Record<string, string>;\n };\n manifest.name = options.name;\n for (const [pkg, specifier] of specifiers) {\n // The plugin and the overlay are development dependencies and the\n // runtime packages are not, so the specifier goes wherever the\n // template already declared the package.\n const where =\n manifest.dependencies[pkg] !== undefined\n ? manifest.dependencies\n : manifest.devDependencies?.[pkg] !== undefined\n ? manifest.devDependencies\n : undefined;\n if (where === undefined) {\n fail(`The ${options.template} template does not depend on ${pkg}, so there is nowhere to vendor it.`);\n }\n where[pkg] = specifier;\n }\n if (specifiers.size > 0) {\n manifest.overrides = Object.fromEntries(specifiers);\n } else {\n delete manifest.overrides;\n }\n writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\\n`);\n}\n\n/**\n * Says the same thing to pnpm that `overrides` says to npm.\n *\n * npm was chosen because pnpm did not work, and the reason is still\n * exactly right: `pnpm pack` rewrites `workspace:^` into `^0.1.0`, so\n * the packed `gesso-framework` asks for `gesso-core@^0.1.0` and pnpm 11\n * resolves it off the registry rather than from the tarball beside it,\n * which is a different copy of the package than the one this project\n * was told to use. What that record then rejected was shipping a\n * `pnpm-workspace.yaml` in a project that is not a workspace.\n *\n * That trade has moved. The file is five lines, it is the only place\n * pnpm 11 reads `overrides` from, and the alternative is a scaffold\n * that fails for the package manager this repository itself uses. It\n * says what it is for, and it is written only under `--local`.\n */\nfunction writePnpmOverrides(options: Options, specifiers: ReadonlyMap<string, string>): void {\n const lines = [\n '# The tarballs in vendor/ again, for pnpm.',\n '#',\n '# This project was scaffolded with --local, so the packed gesso-*',\n '# packages ask each other for version ranges the registry would',\n '# answer with a different copy. npm reads the `overrides` in',\n '# package.json; pnpm 11 reads only this file. To move to the',\n '# published packages: delete vendor/, this file and `overrides`,',\n '# and put version ranges back.',\n 'overrides:',\n ...[...specifiers].map(([pkg, specifier]) => ` '${pkg}': '${specifier}'`),\n ''\n ];\n writeFileSync(join(options.target, 'pnpm-workspace.yaml'), lines.join('\\n'));\n}\n\n/**\n * Substitutes the template's placeholders in the files that carry them.\n *\n * The list is the template's rather than a walk of the tree, so that a\n * placeholder added to a file nobody listed fails visibly in the\n * generated project instead of being quietly left as `{{name}}`\n * somewhere it is never read.\n */\nfunction substitute(options: Options, template: Template): void {\n for (const relative of template.substitute) {\n const path = join(options.target, relative);\n if (!existsSync(path)) {\n fail(`The ${options.template} template says ${relative} carries {{name}}, and it was not written.`);\n }\n writeFileSync(path, readFileSync(path, 'utf8').replaceAll('{{name}}', options.name));\n }\n}\n\n/**\n * Appends the `vendor/` explanation to the generated README.\n *\n * It lives here rather than in the template because a project made\n * without `--local` has no `vendor/`, and a README explaining a\n * directory that is not there is worse than one that says nothing.\n */\nfunction explainVendoring(options: Options): void {\n const path = join(options.target, 'README.md');\n if (!existsSync(path)) {\n return;\n }\n const section = [\n '',\n '## Why `vendor/` exists, and how to remove it',\n '',\n 'This project was scaffolded with `--local`, so it installs Gesso from',\n 'a checkout rather than from the registry: the packages were packed',\n 'into `vendor/` and the manifest points at the tarballs.',\n '',\n 'The packed packages declare each other by version range, so without',\n 'help a package manager is free to go looking for `gesso-core@^0.1.0`',\n 'on the registry and get a different copy than the one beside it.',\n '`overrides` in `package.json` is what tells npm; `overrides` in',\n '`pnpm-workspace.yaml` is what tells pnpm, which reads it nowhere else.',\n '',\n 'To pick up a further change, run `create-gesso-app --local` over this',\n 'directory again with `--force`.',\n '',\n 'To move to the published packages: delete `vendor/`, delete',\n '`pnpm-workspace.yaml`, delete `overrides`, and put version ranges back',\n 'in `dependencies`.',\n ''\n ].join('\\n');\n writeFileSync(path, readFileSync(path, 'utf8').trimEnd() + '\\n' + section);\n}\n\nfunction main(): void {\n const options = parseArgs(process.argv.slice(2));\n if (options.local && !existsSync(join(workspaceRoot, 'packages', 'core', 'package.json'))) {\n fail(\n '--local packs the packages out of the Gesso workspace, and this is not one.\\n' +\n 'Run it from inside a checkout, or drop --local to install from the registry.'\n );\n }\n\n const template = templateOf(options.template);\n prepareTarget(options);\n copyTemplate(join(packageRoot, 'templates', options.template), options);\n // Without --local the template's own version ranges are the answer,\n // and the manifest needs nothing but its name.\n const specifiers = options.local ? vendorPackages(options, template) : new Map<string, string>();\n writeManifest(options, specifiers);\n if (options.local) {\n writePnpmOverrides(options, specifiers);\n explainVendoring(options);\n }\n substitute(options, template);\n\n console.log(template.next(options.name, displayPath(process.cwd(), options.target), options.local));\n}\n\nmain();\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-gesso-app",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Scaffolds a Gesso
|
|
3
|
+
"version": "0.6.3",
|
|
4
|
+
"description": "Scaffolds a Gesso app: a native-grade web application built from declarative components, with its interface in a render worker.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kevin Baker",
|
|
7
7
|
"homepage": "https://gesso-docs.vercel.app",
|
|
@@ -22,11 +22,15 @@
|
|
|
22
22
|
"starter",
|
|
23
23
|
"cli",
|
|
24
24
|
"create-app",
|
|
25
|
-
"template"
|
|
25
|
+
"template",
|
|
26
|
+
"declarative-ui",
|
|
27
|
+
"swiftui",
|
|
28
|
+
"jetpack-compose",
|
|
29
|
+
"web-worker"
|
|
26
30
|
],
|
|
27
31
|
"type": "module",
|
|
28
32
|
"bin": {
|
|
29
|
-
"create-gesso-app": "
|
|
33
|
+
"create-gesso-app": "dist/create-gesso-app.mjs"
|
|
30
34
|
},
|
|
31
35
|
"engines": {
|
|
32
36
|
"node": ">=20.19.0"
|
|
@@ -38,10 +42,10 @@
|
|
|
38
42
|
"LICENSE",
|
|
39
43
|
"templates"
|
|
40
44
|
],
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"tsdown": "^0.22.14"
|
|
43
|
-
},
|
|
44
45
|
"scripts": {
|
|
45
46
|
"build": "tsdown"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"tsdown": "^0.22.14"
|
|
46
50
|
}
|
|
47
|
-
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Working on this project
|
|
2
|
+
|
|
3
|
+
Notes for a coding agent, and for anybody else new to Gesso. `README.md`
|
|
4
|
+
has this project's commands and what each of its files is for; this
|
|
5
|
+
file is the framework's rules, which are the part a model trained mostly
|
|
6
|
+
on React will get wrong.
|
|
7
|
+
|
|
8
|
+
The full documentation is written for a person and also published for a
|
|
9
|
+
model:
|
|
10
|
+
|
|
11
|
+
- https://gesso-docs.vercel.app/llms.txt, an index of every page
|
|
12
|
+
- https://gesso-docs.vercel.app/llms-full.txt, every page in one file
|
|
13
|
+
- any page with `.md` on the end, e.g. https://gesso-docs.vercel.app/guide/counter.md
|
|
14
|
+
|
|
15
|
+
## What Gesso is not
|
|
16
|
+
|
|
17
|
+
There is no DOM, no CSS, no React and no virtual DOM. The interface is
|
|
18
|
+
a tree of retained nodes, laid out by Gesso's own engine and drawn into
|
|
19
|
+
a `<canvas>` from a worker. So:
|
|
20
|
+
|
|
21
|
+
- no `div`, `className`, `style`, stylesheets or Tailwind;
|
|
22
|
+
- no `useState`, `useEffect`, `useMemo`, `useCallback` or dependency arrays;
|
|
23
|
+
- no `document` or `window` in a component. It runs in a worker.
|
|
24
|
+
|
|
25
|
+
## Components run once
|
|
26
|
+
|
|
27
|
+
A component is a function of its inputs and a context. It is called
|
|
28
|
+
**once**, when it mounts, and never again. What it returns stays on
|
|
29
|
+
screen, and everything that changes is an Observable bound into it.
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { Button } from 'gesso-components';
|
|
33
|
+
import { computed, input, internalState, type ComponentContext, type Inputs } from 'gesso-framework';
|
|
34
|
+
|
|
35
|
+
export function Counter(inputs: Inputs<{ label?: string }>, _ctx: ComponentContext) {
|
|
36
|
+
const label = input(inputs.label, 'Count'); // a prop with a default, still live
|
|
37
|
+
const count = internalState(0); // state this component owns
|
|
38
|
+
const caption = computed(() => `${label.value}: ${count.value}`);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<row gap={12} y="center">
|
|
42
|
+
<text text={caption} textStyle="title" />
|
|
43
|
+
<Button label="Add one" onClick={() => count.value++} />
|
|
44
|
+
</row>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- **Bind, don't read.** `text={caption}` follows every change.
|
|
50
|
+
`text={caption.value}` in the body is a snapshot taken once, and the
|
|
51
|
+
screen silently never updates. Read `.value` in handlers and inside
|
|
52
|
+
`computed`, not in the body.
|
|
53
|
+
- Any Observable binds, so `count.pipe(map(n => ...))` is as good as a
|
|
54
|
+
`computed`.
|
|
55
|
+
- **Lists** are `<Each of={rows} by="id">{row => <Row row={row} />}</Each>`.
|
|
56
|
+
Always give `by`.
|
|
57
|
+
- **Conditionals** are `<Show when={open}>{() => <Panel />}</Show>`.
|
|
58
|
+
- **Side effects** go through the context: `ctx.effect(source, fn)`,
|
|
59
|
+
`ctx.onMount(fn)` and `ctx.onUnmount(fn)`. Services come from
|
|
60
|
+
`ctx.inject(Service)` and channels from `ctx.channel(Token)`.
|
|
61
|
+
- Splitting a component never helps performance. Split for readability.
|
|
62
|
+
|
|
63
|
+
## Layout
|
|
64
|
+
|
|
65
|
+
- `<row>`, `<column>` and `<box>` (layered). Lowercase elements are
|
|
66
|
+
intrinsic; capitalised ones are components and are imported.
|
|
67
|
+
- `x` and `y` place children on a row and a column alike. They replace
|
|
68
|
+
`justify-content` and `align-items`. `selfX` and `selfY` override them
|
|
69
|
+
for one child.
|
|
70
|
+
- The cross axis defaults to `stretch`, as in CSS. A child with its own
|
|
71
|
+
size keeps it.
|
|
72
|
+
- A length is a number of pixels or a value from `gesso-core`:
|
|
73
|
+
`percent(100)`, `auto`. It is never a string like `'100%'`.
|
|
74
|
+
- `gap`, `padding`, `flex`, `width` and `height` are typed props. A
|
|
75
|
+
wrong prop or value is a compile error, so run the typechecker.
|
|
76
|
+
|
|
77
|
+
## Appearance
|
|
78
|
+
|
|
79
|
+
- **No hex colours.** Colours are names from the theme: `background`,
|
|
80
|
+
`surface`, `primary`, `secondary`, `text`, `textMuted`, `border`,
|
|
81
|
+
`danger` and the `control*` family. A screen written this way follows
|
|
82
|
+
light and dark without knowing about either.
|
|
83
|
+
- **No font sizes where a role exists.** Use `textStyle="title"` (also
|
|
84
|
+
`headline`, `body`, `bodyLarge`, `bodySmall`, `label`).
|
|
85
|
+
- Prefer the controls in `gesso-components` (`Button`, `TextInput`,
|
|
86
|
+
`Switch`, `Select`, `Dialog`, `DataTable`, ...) to building them from
|
|
87
|
+
boxes. They are themed, keyboard-operable and announced to screen
|
|
88
|
+
readers already.
|
|
89
|
+
|
|
90
|
+
## State that crosses a thread: channels
|
|
91
|
+
|
|
92
|
+
Data that is authoritative, outlives a screen, or lives on another
|
|
93
|
+
thread goes behind a **channel**: a token both sides import, holding
|
|
94
|
+
only names and shapes.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { defineChannel } from 'gesso-framework';
|
|
98
|
+
|
|
99
|
+
/** The notes the person has written. */
|
|
100
|
+
export const Notes = defineChannel('notes', {
|
|
101
|
+
view: { rows: [] as readonly NoteRow[], status: 'loading' as 'loading' | 'ready' },
|
|
102
|
+
commands: {} as {
|
|
103
|
+
/** Opens a note in the editor. @param id The note's id. */
|
|
104
|
+
open(id: string): void;
|
|
105
|
+
/** Deletes a note for good. @destructive @confirm */
|
|
106
|
+
remove(id: string): void;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- Only **plain data** crosses: primitives, arrays and plain objects.
|
|
112
|
+
No `Date`, `Map`, `Set` or class instances. Flatten them where they
|
|
113
|
+
are made.
|
|
114
|
+
- Commands return `void`. The effect comes back as a change to the view,
|
|
115
|
+
never as a return value.
|
|
116
|
+
- A component reads `ctx.channel(Notes).view.rows` like any other cell
|
|
117
|
+
and calls `ctx.channel(Notes).send.open(id)`.
|
|
118
|
+
- **Write the JSDoc.** `gesso-vite-plugin` turns each contract into JSON
|
|
119
|
+
Schema on the token, with descriptions taken from those comments. It
|
|
120
|
+
is what an AI agent driving the app reads to understand it. Mark a
|
|
121
|
+
command `@destructive` if it cannot be undone, `@confirm` if a person
|
|
122
|
+
should approve it first, `@idempotent` if repeating it changes
|
|
123
|
+
nothing, and `@hidden` to keep it from agents.
|
|
124
|
+
|
|
125
|
+
## Checking your work
|
|
126
|
+
|
|
127
|
+
You cannot see a canvas by reading the DOM. The places that tell you
|
|
128
|
+
what the app is doing:
|
|
129
|
+
|
|
130
|
+
- **The typechecker.** Most mistakes in a Gesso tree are type errors.
|
|
131
|
+
Run it after every change.
|
|
132
|
+
- **Tests without a browser.** `gesso-testing` mounts a component in
|
|
133
|
+
node and queries it the way a screen reader would. Add `gesso-testing`
|
|
134
|
+
and `vitest` as development dependencies to use it:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { createComponent } from 'gesso-framework';
|
|
138
|
+
import { renderTest } from 'gesso-testing';
|
|
139
|
+
import 'gesso-testing/matchers';
|
|
140
|
+
|
|
141
|
+
const ui = renderTest(createComponent(Counter, {}), { width: 400, height: 200 });
|
|
142
|
+
ui.fireEvent.click(ui.getByRole('button', { name: 'Add one' }));
|
|
143
|
+
ui.frame(); // frames run when the test says so
|
|
144
|
+
expect(ui.getByText('Count: 1')).toBeDefined();
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
A failed query prints the semantics tree, so read it rather than
|
|
148
|
+
guessing. `ui.explainText(node)` says in sentences why a box is the
|
|
149
|
+
size it is. Use `await ui.settle()` for anything that arrives later
|
|
150
|
+
than the next frame, such as a channel patch.
|
|
151
|
+
|
|
152
|
+
- **In a browser,** the app keeps an accessibility mirror in the DOM
|
|
153
|
+
under `[data-gesso-semantics]`. It is one element per meaningful node,
|
|
154
|
+
with its role, its name and its position over the canvas. A browser
|
|
155
|
+
automation tool's accessibility tree reads it, and clicking those
|
|
156
|
+
elements clicks the app. A screenshot shows what was drawn.
|
|
157
|
+
- **Driving the running app.** While `pnpm dev` runs, the dev server
|
|
158
|
+
serves MCP at `/__gesso/mcp` and prints the `claude mcp add` line
|
|
159
|
+
for it. Connected, you can read every channel's view and send its
|
|
160
|
+
commands to the page open in the browser, which is the quickest way
|
|
161
|
+
to check that a command does what it should. `ui_snapshot`, `ui_press`,
|
|
162
|
+
`ui_type` and `ui_key` read and operate the screen itself, the way a
|
|
163
|
+
screen reader does, for anything no channel covers.
|
|
164
|
+
- **Agents in the browser.** `createApp({ webmcp: true })` registers the
|
|
165
|
+
same tools with WebMCP, for an agent the browser runs. The dev
|
|
166
|
+
server turns it on already; nothing happens in a browser without it.
|
|
167
|
+
- **Errors** from the render worker are drawn over the app by the dev
|
|
168
|
+
overlay, source-mapped. They also reach the browser console.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -170,8 +170,32 @@ main process, that is the escape hatch: bundle `src/main/index.ts` to a
|
|
|
170
170
|
plain `.js` file with `electrobun/main` left external, and point the
|
|
171
171
|
`cottontail.entrypoint` at the bundle instead.
|
|
172
172
|
|
|
173
|
+
## AI agents
|
|
174
|
+
|
|
175
|
+
The main process serves the counter to AI agents over MCP as it starts,
|
|
176
|
+
and prints the line to connect Claude Code:
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
claude mcp add --transport http {{name}} http://127.0.0.1:7310/mcp
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
An agent can then read the count and send `increment` and `setDark` the
|
|
183
|
+
way a window does, and every window follows. Mark a command `@confirm`
|
|
184
|
+
in `src/shared/Counter.ts` and the person is asked with a native dialog
|
|
185
|
+
before an agent can send it.
|
|
186
|
+
|
|
187
|
+
What an agent reads about each command is the JSDoc in the contract,
|
|
188
|
+
which `hutch run channels` turns into `src/shared/channels.described.ts`.
|
|
189
|
+
Every script that builds runs it first, and `hutch run typecheck` fails
|
|
190
|
+
when a contract changed and that file did not, so you only run it by
|
|
191
|
+
hand to see the result. Add a contract to the `channels` script in
|
|
192
|
+
`hutch.config.ts` when you add one.
|
|
193
|
+
|
|
173
194
|
## Where to go next
|
|
174
195
|
|
|
196
|
+
- `AGENTS.md` is the framework's rules in one page, written for a coding
|
|
197
|
+
agent and just as useful to read yourself. `CLAUDE.md` points Claude
|
|
198
|
+
Code at it.
|
|
175
199
|
- `src/render/App.tsx` is commented with what each part of it is doing.
|
|
176
200
|
- A channel is the barrier: view keys out, typed commands in, plain
|
|
177
201
|
data only. Add a key to `CounterView`, serve it in `src/main`, read it
|
|
@@ -32,15 +32,23 @@ export default {
|
|
|
32
32
|
packageManager: 'npm',
|
|
33
33
|
scripts: {
|
|
34
34
|
install: ['hutch', 'install'],
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// The main process is bundled by Electrobun, which gesso-vite-plugin
|
|
36
|
+
// never sees, so the channel contracts are described ahead of time:
|
|
37
|
+
// `gesso-channels` writes the module the main process imports, and
|
|
38
|
+
// every script that builds runs it first.
|
|
39
|
+
channels: 'hutch pm exec -- gesso-channels src/shared/Counter.ts --out src/shared/channels.described.ts',
|
|
40
|
+
start: 'hutch electrobun prepare && hutch run channels && hutch pm exec -- vite build && hutch electrobun dev',
|
|
41
|
+
dev: 'hutch electrobun prepare && hutch run channels && hutch pm exec -- vite build && hutch electrobun dev --watch',
|
|
37
42
|
// The window's assets on Vite's dev server, and the application
|
|
38
43
|
// around them, so a change to a component reloads the webview
|
|
39
44
|
// without rebuilding the native side.
|
|
40
45
|
'dev:hmr': ['hutch', 'pm', 'exec', '--', 'concurrently', 'hutch run hmr', 'hutch run start'],
|
|
41
46
|
hmr: 'hutch electrobun prepare && hutch pm exec -- vite --port 5173',
|
|
42
|
-
build:
|
|
43
|
-
|
|
47
|
+
build:
|
|
48
|
+
'hutch electrobun prepare && hutch run channels && hutch pm exec -- vite build && hutch electrobun build --env=stable',
|
|
49
|
+
// Fails when a contract changed and the described module did not.
|
|
50
|
+
typecheck:
|
|
51
|
+
'hutch electrobun prepare && hutch pm exec -- gesso-channels src/shared/Counter.ts --out src/shared/channels.described.ts --check && hutch pm exec -- tsc --noEmit'
|
|
44
52
|
},
|
|
45
53
|
electrobun: {
|
|
46
54
|
version: '2.0.1'
|
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A Gesso application in a native Electrobun window.",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"gesso-components": "^0.
|
|
9
|
-
"gesso-core": "^0.
|
|
10
|
-
"gesso-electrobun": "^0.
|
|
11
|
-
"gesso-framework": "^0.
|
|
8
|
+
"gesso-components": "^0.6.3",
|
|
9
|
+
"gesso-core": "^0.6.3",
|
|
10
|
+
"gesso-electrobun": "^0.6.3",
|
|
11
|
+
"gesso-framework": "^0.6.3",
|
|
12
12
|
"rxjs": "^7.8.2"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@types/bun": "latest",
|
|
16
16
|
"concurrently": "^9.1.0",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
17
|
+
"gesso-vite-plugin": "^0.6.3",
|
|
18
|
+
"typescript": "~7.0.2",
|
|
19
|
+
"vite": "^8.2.0"
|
|
19
20
|
}
|
|
20
21
|
}
|
|
@@ -15,9 +15,12 @@ import { BrowserView, BrowserWindow, Utils } from 'electrobun/main';
|
|
|
15
15
|
import { BehaviorSubject, map } from 'rxjs';
|
|
16
16
|
|
|
17
17
|
import type { GessoFrame } from 'gesso-electrobun';
|
|
18
|
-
import { createDesktopApp, windowsChannel } from 'gesso-electrobun/desktop';
|
|
18
|
+
import { createDesktopApp, messageBoxConfirm, serveDesktopAgent, windowsChannel } from 'gesso-electrobun/desktop';
|
|
19
|
+
import { serve } from 'gesso-framework';
|
|
19
20
|
|
|
20
21
|
import { Counter } from '../shared/Counter';
|
|
22
|
+
// Describes the channels for agents. Written by `hutch run channels`.
|
|
23
|
+
import '../shared/channels.described';
|
|
21
24
|
import type { GessoWindowRPC } from '../shared/rpc';
|
|
22
25
|
|
|
23
26
|
/** The application's whole state, in the process that owns it. */
|
|
@@ -33,18 +36,18 @@ const count = new BehaviorSubject(0);
|
|
|
33
36
|
*/
|
|
34
37
|
const dark = new BehaviorSubject(true);
|
|
35
38
|
|
|
39
|
+
/** The counter, served to every window and to AI agents alike. */
|
|
40
|
+
const counter = serve(Counter, {
|
|
41
|
+
view: { count, dark },
|
|
42
|
+
commands: {
|
|
43
|
+
increment: (by: number) => count.next(count.value + by),
|
|
44
|
+
setDark: (next: boolean) => dark.next(next)
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
36
48
|
const app = createDesktopApp({
|
|
37
49
|
channels: window => [
|
|
38
|
-
|
|
39
|
-
token: Counter,
|
|
40
|
-
source: {
|
|
41
|
-
view: { count, dark },
|
|
42
|
-
commands: {
|
|
43
|
-
increment: (by: number) => count.next(count.value + by),
|
|
44
|
-
setDark: (next: boolean) => dark.next(next)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
},
|
|
50
|
+
counter,
|
|
48
51
|
// What a window opens another window through. Without it a screen
|
|
49
52
|
// would have to import this adapter to do it.
|
|
50
53
|
windowsChannel(app, window)
|
|
@@ -88,3 +91,20 @@ const app = createDesktopApp({
|
|
|
88
91
|
});
|
|
89
92
|
|
|
90
93
|
app.openWindow();
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The same channel, served to AI agents over MCP on this machine.
|
|
97
|
+
*
|
|
98
|
+
* An agent such as Claude Code connects with the line this prints, then
|
|
99
|
+
* reads the count and sends `increment` and `setDark` the way a window
|
|
100
|
+
* does, and the windows follow. A command marked `@confirm` in the
|
|
101
|
+
* contract is put to the person with the native dialog first.
|
|
102
|
+
* `channels.described.ts`, imported above, is what tells the agent what
|
|
103
|
+
* each command is for; `hutch run channels` rewrites it after a contract
|
|
104
|
+
* changes, and every other script does so first.
|
|
105
|
+
*/
|
|
106
|
+
const agent = serveDesktopAgent([counter], {
|
|
107
|
+
name: '{{name}}',
|
|
108
|
+
confirm: messageBoxConfirm(Utils.showMessageBox)
|
|
109
|
+
});
|
|
110
|
+
console.log(`AI agents can connect: claude mcp add --transport http {{name}} ${agent.url}`);
|
|
@@ -15,14 +15,31 @@ export interface CounterView {
|
|
|
15
15
|
dark: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* What a window, or an AI agent, can ask the main process to do.
|
|
20
|
+
*
|
|
21
|
+
* The JSDoc here is read by people and agents both: `gesso-channels`
|
|
22
|
+
* turns it into the descriptions an agent sees, so it says what each
|
|
23
|
+
* command does rather than how it is built.
|
|
24
|
+
*/
|
|
18
25
|
export interface CounterCommands {
|
|
26
|
+
/**
|
|
27
|
+
* Adds to the count.
|
|
28
|
+
* @param by How much to add; negative to subtract.
|
|
29
|
+
*/
|
|
19
30
|
increment: (by: number) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Switches every window between dark and light.
|
|
33
|
+
* @param dark True for dark.
|
|
34
|
+
*/
|
|
20
35
|
setDark: (dark: boolean) => void;
|
|
21
36
|
}
|
|
22
37
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
// The name is the application's own, and both sides have to agree on
|
|
39
|
+
// it. The second argument is what a window shows before the main
|
|
40
|
+
// process has answered, which on a desktop is a few milliseconds. These
|
|
41
|
+
// are line comments rather than JSDoc because the JSDoc below is what
|
|
42
|
+
// an AI agent is told the channel is, and this is for you.
|
|
43
|
+
|
|
44
|
+
/** A counter shared by every window of the app, and whether the app is dark or light. */
|
|
28
45
|
export const Counter = channel<CounterView, CounterCommands>('counter', { count: 0, dark: true });
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Written by gesso-channels from the channel contracts it names below.
|
|
2
|
+
// Run it again after changing a contract rather than editing this file.
|
|
3
|
+
import { describeChannel } from 'gesso-framework';
|
|
4
|
+
import { Counter } from './Counter';
|
|
5
|
+
|
|
6
|
+
describeChannel(Counter, {
|
|
7
|
+
"view": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"count": {
|
|
11
|
+
"type": "number",
|
|
12
|
+
"description": "The application's whole state, and it lives in the main process."
|
|
13
|
+
},
|
|
14
|
+
"dark": {
|
|
15
|
+
"type": "boolean",
|
|
16
|
+
"description": "Which appearance every window is in. One setting, all windows."
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"required": [
|
|
20
|
+
"count",
|
|
21
|
+
"dark"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"commands": {
|
|
25
|
+
"increment": {
|
|
26
|
+
"parameters": [
|
|
27
|
+
"by"
|
|
28
|
+
],
|
|
29
|
+
"input": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"properties": {
|
|
32
|
+
"by": {
|
|
33
|
+
"type": "number",
|
|
34
|
+
"description": "How much to add; negative to subtract."
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"additionalProperties": false,
|
|
38
|
+
"required": [
|
|
39
|
+
"by"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"description": "Adds to the count."
|
|
43
|
+
},
|
|
44
|
+
"setDark": {
|
|
45
|
+
"parameters": [
|
|
46
|
+
"dark"
|
|
47
|
+
],
|
|
48
|
+
"input": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": {
|
|
51
|
+
"dark": {
|
|
52
|
+
"type": "boolean",
|
|
53
|
+
"description": "True for dark."
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"additionalProperties": false,
|
|
57
|
+
"required": [
|
|
58
|
+
"dark"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"description": "Switches every window between dark and light."
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"description": "A counter shared by every window of the app, and whether the app is dark or light."
|
|
65
|
+
});
|
|
@@ -11,12 +11,18 @@
|
|
|
11
11
|
in it is painted, not styled. The background is here so that
|
|
12
12
|
the moment before the first frame is not a white flash on a
|
|
13
13
|
dark desktop.
|
|
14
|
+
|
|
15
|
+
overscroll-behavior: a scroll the app has no use for, such as one
|
|
16
|
+
past the end of a list, goes back to the page, and on a Mac the
|
|
17
|
+
webview then stretches the whole window's content. The app is the
|
|
18
|
+
window, so it doesn't.
|
|
14
19
|
*/
|
|
15
20
|
html,
|
|
16
21
|
body {
|
|
17
22
|
margin: 0;
|
|
18
23
|
height: 100%;
|
|
19
24
|
background: #14161a;
|
|
25
|
+
overscroll-behavior: none;
|
|
20
26
|
}
|
|
21
27
|
#app {
|
|
22
28
|
width: 100vw;
|
|
@@ -48,6 +48,8 @@ const shell = createApp({
|
|
|
48
48
|
// A link in a desktop application belongs in the person's browser,
|
|
49
49
|
// which only the process outside this window can reach.
|
|
50
50
|
onOpenUrl: url => bridge.openUrl(url),
|
|
51
|
+
// The window is all app: a key pressed before anything is clicked is its.
|
|
52
|
+
pageKeys: true,
|
|
51
53
|
onError: (message, stack, source) => console.error(`[gesso ${source}] ${message}`, stack)
|
|
52
54
|
});
|
|
53
55
|
shell.mount(host);
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
"extends": "./.hutch/devkit/tsconfig.json",
|
|
8
8
|
"compilerOptions": {
|
|
9
|
+
/*
|
|
10
|
+
The projected config sets `baseUrl`, which TypeScript 7 has removed.
|
|
11
|
+
Its `paths` are relative to its own directory either way, so
|
|
12
|
+
clearing it here changes nothing they resolve to.
|
|
13
|
+
*/
|
|
14
|
+
"baseUrl": null,
|
|
9
15
|
"target": "ESNext",
|
|
10
16
|
"module": "ESNext",
|
|
11
17
|
"lib": ["ES2023", "DOM", "DOM.Iterable", "WebWorker"],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { defineConfig } from 'vite';
|
|
3
3
|
|
|
4
|
-
import { electrobunViteAliases } from './.hutch/devkit/api/config/electrobun-vite';
|
|
4
|
+
import { electrobunViteAliases } from './.hutch/devkit/api/config/electrobun-vite.ts';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The window's assets: an ordinary Vite build with one alias list.
|
|
@@ -23,7 +23,7 @@ export default defineConfig({
|
|
|
23
23
|
resolve: {
|
|
24
24
|
// Spread rather than passed through, so an alias of your own has
|
|
25
25
|
// somewhere obvious to go.
|
|
26
|
-
alias: [...electrobunViteAliases(resolve(
|
|
26
|
+
alias: [...electrobunViteAliases(resolve(import.meta.dirname, '.hutch/devkit'))]
|
|
27
27
|
},
|
|
28
28
|
esbuild: { jsx: 'automatic', jsxImportSource: 'gesso-framework' },
|
|
29
29
|
root: 'src/view',
|
package/templates/web/README.md
CHANGED
|
@@ -81,6 +81,9 @@ view has stopped arriving.
|
|
|
81
81
|
|
|
82
82
|
## Where to go next
|
|
83
83
|
|
|
84
|
+
- `AGENTS.md` is the framework's rules in one page, written for a coding
|
|
85
|
+
agent and just as useful to read yourself. `CLAUDE.md` points Claude
|
|
86
|
+
Code at it.
|
|
84
87
|
- `App.tsx` is commented with what each part of it is doing.
|
|
85
88
|
- `gesso-components` has the controls: inputs, overlays, structure,
|
|
86
89
|
data and media. `Switch` in `App.tsx` is one of them.
|
package/templates/web/index.html
CHANGED
|
@@ -9,11 +9,18 @@
|
|
|
9
9
|
The canvas is sized to its host, so the host needs a size. This
|
|
10
10
|
is the whole of the CSS a Gesso application needs; everything
|
|
11
11
|
else on the page is painted, not styled.
|
|
12
|
+
|
|
13
|
+
overscroll-behavior: a scroll the app has no use for, such as one
|
|
14
|
+
past the end of a list, goes back to the page, and on a Mac the
|
|
15
|
+
browser then stretches the whole page and shows white behind it,
|
|
16
|
+
or takes a sideways swipe as Back. The app is the page, so the
|
|
17
|
+
page doesn't do either.
|
|
12
18
|
*/
|
|
13
19
|
html,
|
|
14
20
|
body {
|
|
15
21
|
margin: 0;
|
|
16
22
|
height: 100%;
|
|
23
|
+
overscroll-behavior: none;
|
|
17
24
|
}
|
|
18
25
|
#app {
|
|
19
26
|
width: 100vw;
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
"typecheck": "tsc --noEmit"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"gesso-components": "^0.
|
|
14
|
-
"gesso-core": "^0.
|
|
15
|
-
"gesso-framework": "^0.
|
|
13
|
+
"gesso-components": "^0.6.3",
|
|
14
|
+
"gesso-core": "^0.6.3",
|
|
15
|
+
"gesso-framework": "^0.6.3",
|
|
16
16
|
"rxjs": "^7.8.2"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"gesso-devtools": "^0.
|
|
20
|
-
"gesso-vite-plugin": "^0.
|
|
19
|
+
"gesso-devtools": "^0.6.3",
|
|
20
|
+
"gesso-vite-plugin": "^0.6.3",
|
|
21
21
|
"typescript": "~7.0.2",
|
|
22
22
|
"vite": "^8.2.0"
|
|
23
23
|
}
|
|
@@ -26,6 +26,8 @@ if (host === null) {
|
|
|
26
26
|
throw new Error('index.html has no #app element to mount into.');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// The app is the whole page, so a key pressed before anything is
|
|
30
|
+
// clicked is the app's: `pageKeys` sends it there.
|
|
31
|
+
const app = createApp({ pageKeys: true });
|
|
30
32
|
|
|
31
33
|
app.mount(host);
|