@dolphy-app/extension-sdk 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,228 @@
1
+ # Debugging
2
+
3
+ Where to look when an extension does not do what you expect, from the cheapest
4
+ check to the running app. Commands are those of a generated project; see the
5
+ [quick start](quick-start.md).
6
+
7
+ ## 1. Reproduce it in a test
8
+
9
+ `createTestServer` and `createTestClient` of `@dolphy-app/extension-sdk/testing`
10
+ run your entries in plain Node, with a debugger and `console.log` available.
11
+ Most mistakes (a wrong result shape, a setting read before it is set, a handler
12
+ that throws, an id without the extension prefix) show up there.
13
+
14
+ - `createTestServer(server, { extensionId, logger })` takes a `logger` option:
15
+ pass an object with `debug`, `info`, `warn` and `error` methods to see what
16
+ `server.logger` receives.
17
+ - With `extensionId` set the harness checks that every id you register is the
18
+ extension id or starts with `<id>.`, the way the host does. It also checks what
19
+ the host checks about a result: the shape of a verdict, a grade of 1–5 or
20
+ `null`, a command result (`notify` text of 1–500 characters, a JSON value of
21
+ at most 64 KiB), an importer's or exporter's result. A message from the harness
22
+ usually names the rule.
23
+ - An error in `server` fails `createTestServer` itself, as it fails the load in
24
+ the app: nothing is registered. A handler's failure is not swallowed but
25
+ rejects the promise.
26
+ - The harness does not count the host's time limits, so a test that passes does
27
+ not prove a handler is fast enough: an event handler gets 2 seconds, a command
28
+ or a schedule handler 10, an importer or an exporter 30, and the registration
29
+ (`server`) 10.
30
+
31
+ ## 2. Run the checks
32
+
33
+ ```sh
34
+ pnpm typecheck # tsc
35
+ pnpm validate # the manifest the way the app parses it
36
+ pnpm lint # metadata and bundle findings the catalog review also sees
37
+ ```
38
+
39
+ `pnpm validate` parses the built manifest with the same code as the app and
40
+ exits with code 1 on a problem; a key the manifest does not have (a command, a
41
+ setting, a panel written into `extension.json`) is one: the manifest holds the
42
+ identity of the extension, and the code registers everything else. The build
43
+ also fails when `server` imports `vue`, `vuetify` or a component, or `client`
44
+ imports a `node:*` module, and names the file.
45
+ `pnpm lint` prints `warning <id> <RULE> <field>: <message>` lines; a rule that
46
+ looks wrong for your code (for example `eval` in a bundled dependency) is a hint
47
+ for the reviewer, not a failure.
48
+
49
+ ## 3. The app: development loop
50
+
51
+ `dolphy-ext dev` builds the project in watch mode and starts the installed
52
+ Dolphy app on the result:
53
+
54
+ ```sh
55
+ pnpm exec dolphy-ext dev # the project in the current directory
56
+ pnpm exec dolphy-ext dev ../my-ext # another project
57
+ ```
58
+
59
+ It prints the path of the app it started and the directory it passes to it
60
+ (`DOLPHY_DEV_EXTENSIONS=<project>/dist-ext`). Press Ctrl+C to stop the build and
61
+ the app; the exit code is 0. The app is looked up in this order:
62
+
63
+ 1. `--app <path>`: a macOS `.app` bundle or an executable file.
64
+ 2. The `DOLPHY_APP` environment variable (same kind of path).
65
+ 3. The standard place of your system: macOS `/Applications/Dolphy.app`, then
66
+ `~/Applications/Dolphy.app`; Windows
67
+ `%LOCALAPPDATA%\Programs\Dolphy\Dolphy.exe`; Linux the newest
68
+ `~/Applications/Dolphy-Linux-*.AppImage` (Linux has no fixed place: keep the
69
+ AppImage there or use `--app`).
70
+
71
+ When none exists the command exits with code 2 and lists the places it looked
72
+ at. A path given with `--app` or `DOLPHY_APP` that does not exist is an error as
73
+ well, not a silent fallback.
74
+
75
+ If you prefer to run the two parts yourself, start `pnpm dev`
76
+ (`dolphy-ext build --watch`) and start Dolphy with
77
+ `DOLPHY_DEV_EXTENSIONS=<project>/dist-ext`; from a checkout of the Dolphy
78
+ repository that is `DOLPHY_DEV_EXTENSIONS=<project>/dist-ext pnpm dev`. The app
79
+ rereads the extensions on every file change and applies the change live: the
80
+ extension host runs `server` again and the window loads `client.mjs` again, and
81
+ the window itself does not reload. Things to know:
82
+
83
+ - The app has one instance. If Dolphy is already running, a second start exits
84
+ at once and the variable is lost. `dolphy-ext dev` notices an app that quits
85
+ within 5 seconds and prints "Dolphy is probably already running: quit it and
86
+ run again" (exit code 1): quit the running app and start again.
87
+ - The extension appears in Settings → Extensions with the origin
88
+ "Development". A manifest or load problem is shown there next to the
89
+ extension instead of the extension working: start with that text.
90
+ - If the id is also in the bundled set or installed from the catalog, the
91
+ development copy wins.
92
+ - The components of extensions in development are redrawn on a change, so their
93
+ state can be lost.
94
+
95
+ ### `load-failed`
96
+
97
+ When `server` throws, registers something invalid (an id that is taken or does
98
+ not carry the extension prefix, a bad `when`, an invalid setting definition), or
99
+ does not finish in 10 seconds, the extension host registers
100
+ nothing from it. Registration is all or nothing: the commands, settings and
101
+ event handlers `server` managed to add before the failure are removed too. The
102
+ extension row in Settings → Extensions shows the diagnostic "Could not load the
103
+ extension: {reason}" with the message of the error, and the log has the entry
104
+ `extension registration failed` with the cause (`activation-failed`, or
105
+ `activation-timeout` for the 10 seconds). `server` is called once per load, so
106
+ do slow work (a network request, a big file) in a handler and not in `server`
107
+ itself.
108
+
109
+ The client part has its own failure. When `client.mjs` does not import, `client`
110
+ throws, or a registration is refused (an id that is taken, a bad injection
111
+ target), the window shows "The extension client part failed to load" on the
112
+ extension and keeps no contribution of the client part; the other extensions
113
+ and the server part of this one keep working. The reason is in the DevTools
114
+ console of section 4. After you fix the file, the next rebuild loads it again.
115
+
116
+ ## 4. DevTools
117
+
118
+ While `DOLPHY_DEV_EXTENSIONS` is set (so under `dolphy-ext dev` too), in any
119
+ build of the app, including an installed one, these keys toggle the DevTools of
120
+ the main window: `F12`, `Cmd+Alt+I` (macOS) and `Ctrl+Shift+I`. Without the
121
+ variable the keys do nothing and an installed app has no DevTools.
122
+
123
+ - Panels, injected components, answer views and markdown blocks of an extension
124
+ are Vue components in the page of the app window itself. "Elements" shows
125
+ their markup in the page and the Vue DevTools show the component tree; the
126
+ console evaluates in the same page as the app and shows what the client part
127
+ and its components print with `console`.
128
+ - `dolphy-ext dev` and `dolphy-ext build --watch` put an inline source map
129
+ (`//# sourceMappingURL=data:application/json…`) into every bundle, so
130
+ "Sources" shows your TypeScript (`src/client.ts` and the files it imports)
131
+ for the client part: set a breakpoint there, `debugger;` works too. A plain
132
+ `dolphy-ext build` (`pnpm build`) and the catalog build never write source
133
+ maps, and the catalog check rejects a submission that has one.
134
+ - The code that runs in the extension host (`main.mjs`: `server`, command and
135
+ event handlers, schedules, importers and exporters) is not in this window: see
136
+ section 6 and the log.
137
+
138
+ ## 5. The log
139
+
140
+ `server.logger` has `debug`, `info`, `warn` and `error`; each takes an object of
141
+ fields and an optional message:
142
+
143
+ ```text
144
+ s.logger.info({ id: change.id, value: change.value }, 'setting changed');
145
+ ```
146
+
147
+ The output goes to the log of the app, not to a console of the window. Log
148
+ structured fields, not secrets or the learner's answers. The host writes to the
149
+ same log: an exception in a handler, a handler that outlives its time limit, an
150
+ event dropped from a full queue, and the failure of a registration. The client
151
+ part has no logger: use `console` and the DevTools.
152
+
153
+ ### Reading the log in the app
154
+
155
+ 1. Open Settings → Extensions. In the "Diagnostics" block press "Log" to see
156
+ everything, or press "Log" in the row of your extension to see only its
157
+ entries (the dialog opens with the id of the extension in the filter).
158
+ 2. The dialog lists the last 500 entries, the newest at the bottom. Each entry
159
+ shows the time, the level, the source (`main`, `engine` or `ext-host`), the
160
+ id of the extension that wrote it and the message. "Details" opens the other
161
+ fields of the entry as JSON.
162
+ 3. Filter by extension: type or pick an id in "Extension" (an id that is not in
163
+ the list is accepted). Filter by level: "Minimum level" hides entries below
164
+ it; the default, "Debug", shows all of them.
165
+ 4. The dialog does not update by itself: press "Refresh" after you triggered
166
+ the code you are watching. "No entries match the filters." means the filters
167
+ hide everything, or nothing was logged yet.
168
+
169
+ The labels the dialog shows, with the keys of the app's messages (ru and en):
170
+
171
+ | Where | English | Русский | Message key |
172
+ | -------------------------- | -------------------------------- | ----------------------------- | ------------------------------------------ |
173
+ | Settings section | Extensions | Расширения | `settings.extensions.title` |
174
+ | Origin of a dev extension | Development | Разработка | `settings.extensions.origin.dev` |
175
+ | Load failure of the client part | The extension client part failed to load | Клиентская часть расширения не загрузилась | `settings.extensions.clientFailed.title` |
176
+ | Block with the log button | Diagnostics | Диагностика | `settings.extensions.support.title` |
177
+ | Button of the block | Log | Журнал | `settings.extensions.support.openLog` |
178
+ | Action in an extension row | Log | Журнал | `settings.extensions.log.rowAction` |
179
+ | Dialog title | Log | Журнал | `settings.extensions.log.title` |
180
+ | Extension filter | Extension | Расширение | `settings.extensions.log.filterExtension` |
181
+ | Extension filter, empty | All extensions | Все расширения | `settings.extensions.log.filterExtensionHint` |
182
+ | Level filter | Minimum level | Минимальный уровень | `settings.extensions.log.filterLevel` |
183
+ | Level | Debug | Отладка | `settings.extensions.log.level.debug` |
184
+ | Level | Info | Инфо | `settings.extensions.log.level.info` |
185
+ | Level | Warning | Предупреждение | `settings.extensions.log.level.warn` |
186
+ | Level | Error | Ошибка | `settings.extensions.log.level.error` |
187
+ | Other fields of an entry | Details | Подробности | `settings.extensions.log.details` |
188
+ | Reread the log | Refresh | Обновить | `settings.extensions.log.refresh` |
189
+ | Filters match nothing | No entries match the filters. | Нет записей, подходящих под условия. | `settings.extensions.log.empty` |
190
+
191
+ ### Output of the extension host
192
+
193
+ What `server` and its handlers print with `console.log`, `console.error` or an
194
+ uncaught error goes to the log as `warn` entries of the extension host in
195
+ "Details". Use `s.logger` for anything you want to filter by level (its entries
196
+ carry the extension id); use `console` only for a quick look.
197
+
198
+ ### The file
199
+
200
+ The entries are also in `logs/dolphy-YYYY-MM-DD.log` in the app's data folder,
201
+ one JSON object per line (`level`, `source`, `message`, `at` and the other
202
+ fields). A new file starts every day and at 2 MiB; files older than 7 days are
203
+ removed, and the oldest go first while the folder is over 10 MiB. Attach the
204
+ relevant lines, or the text of "Copy diagnostics" in the "Diagnostics" block (it
205
+ has no paths of your home folder, library content or learning data), to a bug
206
+ report.
207
+
208
+ ## 6. Reading a stack trace
209
+
210
+ The code of the extension host is `dist-ext/<id>/main.mjs`, readable and not
211
+ minified. A watch build (`dolphy-ext dev`, `dolphy-ext build --watch`) appends
212
+ an inline source map to it, but the app does not turn on source maps for that
213
+ process, so a stack trace in the log still points to lines of `main.mjs`: open
214
+ that file to find the place and search it for the name from the trace. The
215
+ map is for the browser file of section 4. A plain `dolphy-ext build` writes no
216
+ maps at all.
217
+
218
+ ## Which limit did I hit?
219
+
220
+ | Symptom | Limit |
221
+ | ----------------------------------------------- | -------------------------------------------------------- |
222
+ | the extension shows `load-failed` with a timeout | `server` must finish in 10 seconds |
223
+ | an event handler stops mid-way | 2 seconds per event; the queue holds 100 events |
224
+ | a command or a schedule handler stops mid-way | 10 seconds |
225
+ | an importer or an exporter stops mid-way | 30 seconds |
226
+ | `StorageQuotaError` | key 128 characters, value 64 KiB, 256 keys, 1 MiB total |
227
+ | a command result is rejected | `notify` text 1–500 characters; a result up to 64 KiB |
228
+ | a panel or a view fails while it draws | the card "Retry" replaces it; the window stays up |
@@ -0,0 +1,135 @@
1
+ # Without a build
2
+
3
+ An extension is a directory with an `extension.json` and, if it has code, one or
4
+ two ES modules: `main.mjs` for the `server` entry and `client.mjs` for the
5
+ `client` entry. `dolphy-ext build` only produces such a directory from a
6
+ TypeScript project; you can write it by hand. You do not need TypeScript, a
7
+ `package.json` or `dolphy-ext` for that. This page shows the smallest case: one
8
+ palette command and a panel, three hand-written files. Use it for a quick
9
+ experiment or when the code is a few lines; switch to the
10
+ [quick start](quick-start.md) layout when you want types, tests and bundling.
11
+
12
+ ## The files
13
+
14
+ File `extension.json` (no build):
15
+
16
+ ```json
17
+ {
18
+ "id": "acme.plain",
19
+ "version": "0.1.0",
20
+ "apiVersion": 1,
21
+ "main": "./main.mjs",
22
+ "client": "./client.mjs",
23
+ "name": "Plain hello",
24
+ "description": "A palette command and a panel written by hand, without a build step.",
25
+ "author": "your-github-login",
26
+ "tags": ["productivity"]
27
+ }
28
+ ```
29
+
30
+ File `main.mjs` (no build):
31
+
32
+ ```js
33
+ export const server = (s) => {
34
+ s.registerCommand({
35
+ id: 'acme.plain.hello',
36
+ title: 'Say hello',
37
+ run: () => ({ notify: `Hello from ${s.extensionId}!` }),
38
+ });
39
+ s.registerCommand({
40
+ id: 'acme.plain.open',
41
+ title: 'Open the plain panel',
42
+ run: () => ({ openPanel: 'acme.plain.view' }),
43
+ });
44
+ };
45
+ ```
46
+
47
+ File `client.mjs` (no build):
48
+
49
+ ```js
50
+ // `vue` is the app's own instance: the window gives it through globalThis.__dolphy
51
+ const { defineComponent, h } = await globalThis.__dolphy.require('vue');
52
+
53
+ export const client = (c) => {
54
+ c.addPanel({
55
+ id: 'acme.plain.view',
56
+ title: 'Plain hello',
57
+ component: defineComponent({
58
+ setup: () => () => h('p', 'Hello from a panel'),
59
+ }),
60
+ });
61
+ };
62
+ ```
63
+
64
+ - `main` and `client` are written in the manifest: nothing fills them in. A part
65
+ that is left out (`null` or no key) does not exist, so an extension may have
66
+ only `main.mjs` or only `client.mjs`. Each is a path inside the directory that
67
+ ends with `.mjs`.
68
+ - The contract of `main.mjs` is `export const server = (s) => { … }`, the same
69
+ function `defineServer` takes in a project; `s` is the `ServerContext`
70
+ (`registerCommand`, `registerSettings`, `on`, `storage`, `logger`, …). The
71
+ contract of `client.mjs` is `export const client = (c) => { … }`
72
+ (`addPanel`, `addInjection`, `addAnswerView`, `addMarkdownRenderer`,
73
+ `addTheme`, `addCommand`). Either may return a cleanup function. `async`
74
+ functions work. `server` is called when the host loads the extension; if it
75
+ throws or takes more than 10 seconds the extension shows `load-failed` and
76
+ registers nothing.
77
+ - Without a build there is no `import 'vue'`: `client.mjs` reads `vue` (and
78
+ `vuetify`, `vuetify/components`, `vuetify/directives`) from
79
+ `globalThis.__dolphy.require(name)`, which resolves to the app's own instance.
80
+ Write the components with `h` or a render function: there is no template
81
+ compiler. `main.mjs` never needs `vue`.
82
+ - There are no helpers, so a command returns the result object itself:
83
+ `{ notify: text }` is what `notify(text)` makes and `{ openPanel: id, props }`
84
+ is what `openPanel(id, props)` makes. Ids are written in the code and must be
85
+ the extension id or start with `<id>.`.
86
+ - The modules must be plain JavaScript that the app runs as it is: no
87
+ TypeScript, no bare imports of packages (nothing is installed next to them),
88
+ and no `node:*` imports in `client.mjs`.
89
+
90
+ ## Check and try
91
+
92
+ Put the files into a directory named exactly as the extension id,
93
+ `acme.plain/` (the app takes the id from the directory name and rejects a
94
+ manifest that disagrees), and check it:
95
+
96
+ ```sh
97
+ npx --package @dolphy-app/extension-tools dolphy-ext validate ./acme.plain
98
+ ```
99
+
100
+ `validate` parses the manifest the way the app does, checks that `main` and
101
+ `client` are files, and exits with code 1 on a problem. To try the extension,
102
+ put the directory into a folder and start the app with `DOLPHY_DEV_EXTENSIONS`
103
+ pointing at that folder (the folder, not the extension directory: it holds one
104
+ directory per extension), or copy the directory to `<userData>/extensions/` and
105
+ restart the app. Edits to the files are picked up without a restart when you use
106
+ `DOLPHY_DEV_EXTENSIONS`.
107
+
108
+ To test the server module without the app, run its entry with the SDK helper:
109
+
110
+ ```sh
111
+ npm install --save-dev @dolphy-app/extension-sdk vitest
112
+ ```
113
+
114
+ <!-- fragment -->
115
+
116
+ ```js
117
+ import { createTestServer } from '@dolphy-app/extension-sdk/testing';
118
+ import { server } from './main.mjs';
119
+
120
+ const running = await createTestServer(server, { extensionId: 'acme.plain' });
121
+ console.log(await running.commands.run('acme.plain.hello'));
122
+ // { kind: 'notify', text: 'Hello from acme.plain!' }
123
+ ```
124
+
125
+ `createTestServer` accepts the export of the module as it is, because it has the
126
+ shape of a server entry. `createTestClient(client, { extensionId })` does the
127
+ same for `client.mjs` and lists the panels it added.
128
+
129
+ ## Publishing
130
+
131
+ The extension catalog reviews a project, not a bare directory: it requires a
132
+ `package.json` with a lock file and a `README.md` that explains what the
133
+ extension does (`dolphy-ext catalog check`). A hand-written directory is for
134
+ yourself or for handing a folder to someone. To publish, generate a project with
135
+ `create-dolphy-extension` and move the code into it.
@@ -0,0 +1,194 @@
1
+ # Quick start
2
+
3
+ This guide takes you from nothing to a working Dolphy extension: a command in
4
+ the command palette that shows a notification. It is the smallest project the
5
+ generator makes (`--template blank`); the recipes next to this file build on the
6
+ same layout:
7
+
8
+ - [Exercise type](recipe-exercise-type.md): a new kind of task with its own answer input.
9
+ - [Theme](recipe-theme.md): colors, registered by the client part.
10
+ - [Command and panel](recipe-command-panel.md): palette commands and a panel, a Vue single-file component (`.vue`) in the app window.
11
+ - [A panel in React](recipe-react.md): the same panel drawn with React: `"frameworks": ["react"]`, `reactComponent`, hooks, tests with `mountForTest`.
12
+ - [A component of any framework](recipe-mountable.md): `defineMountable` on plain DOM, the base for Svelte, Solid or Lit, and what `ctx` gives it.
13
+ - [Events and storage](recipe-event-storage.md): react to learning events and keep data.
14
+ - [Hooks](recipe-hooks.md): change or cancel a session start and the batch of exercises before the engine acts.
15
+ - [Settings](recipe-settings.md): let the user configure the extension.
16
+ - [Visibility conditions and dependencies](recipe-when-dependencies.md): show a command only where it makes sense, a widget in the daily plan, require another extension.
17
+ - [Importer and exporter](recipe-import-export.md): bring a file in as a course, write a course out.
18
+ - [Calls, engine and window](recipe-rpc-and-app.md): `defineRpc` between the parts, `engine` access, `useApp`.
19
+ - [Without a build](no-build.md): a hand-written `extension.json`, `main.mjs` and `client.mjs`, no TypeScript.
20
+ - [Debugging](debugging.md): where to look when something does not work.
21
+
22
+ ## What you need
23
+
24
+ - Node.js 22.12 or newer and pnpm.
25
+ - The Dolphy app, to see the extension work. Everything else (build, type
26
+ check, tests) runs without it.
27
+
28
+ ## 1. Create the project
29
+
30
+ ```sh
31
+ npx @dolphy-app/create-extension my-extension --id acme.hello --template blank
32
+ cd my-extension
33
+ pnpm install
34
+ ```
35
+
36
+ `--id` is the extension id: lowercase letters, digits, dots and hyphens. Every
37
+ id the extension registers (commands, panels, settings…) is the extension id or
38
+ starts with it and a dot, so pick one that is yours (a publisher prefix, then a
39
+ name). Without `--id` the id is the directory name in kebab-case. The
40
+ `--template` values:
41
+
42
+ | Template | What you get |
43
+ | --------------- | ----------------------------------------------------------------- |
44
+ | `exercise` | a task type with an answer input and a setting (the default) |
45
+ | `theme` | a color theme |
46
+ | `command-panel` | palette commands and a panel as a Vue single-file component |
47
+ | `react-panel` | palette commands and a panel drawn with React |
48
+ | `events` | a learning event handler, storage, commands and a panel |
49
+ | `blank` | one palette command |
50
+
51
+ An unknown name exits with code 2 and lists the available ones.
52
+
53
+ ## 2. The files
54
+
55
+ The project is two files that matter. The manifest says who the extension is,
56
+ the code says what it adds.
57
+
58
+ File `extension.json` (quick start):
59
+
60
+ ```json
61
+ {
62
+ "$schema": "./node_modules/@dolphy-app/extension-api/dist/extension.schema.json",
63
+ "id": "acme.hello",
64
+ "version": "0.1.0",
65
+ "apiVersion": 1,
66
+ "name": "Hello command",
67
+ "description": "A command-palette command that shows a notification.",
68
+ "author": "your-github-login",
69
+ "tags": ["productivity"]
70
+ }
71
+ ```
72
+
73
+ `$schema` gives editors completion and checking. `id` is the identity of the
74
+ extension in the app and the catalog; it never changes after the first release.
75
+ Replace `your-github-login` in `author` with your GitHub login before you
76
+ publish. The manifest declares no commands, panels or settings: the code
77
+ registers them. The other keys are `platforms`, `minAppVersion`, `icon` and
78
+ `dependencies`. `main` and `client` are written by the build.
79
+
80
+ File `src/index.ts` (quick start):
81
+
82
+ ```ts
83
+ import { defineServer, notify } from '@dolphy-app/extension-sdk';
84
+
85
+ // runs in the extension host: every call registers a contribution
86
+ export const server = defineServer((s) => {
87
+ s.registerCommand({
88
+ id: 'acme.hello.hello',
89
+ title: { en: 'Say hello', ru: 'Поздороваться' },
90
+ run: () => notify('Hello from acme.hello!'),
91
+ });
92
+ });
93
+ ```
94
+
95
+ `src/index.ts` exports up to two entries. `server` runs in the extension host
96
+ (Node) and registers commands, exercise types, settings, event handlers,
97
+ schedules, importers and exporters; `client` runs in the app window and adds
98
+ panels, injected components, answer views, markdown renderers, themes and
99
+ client commands. This project has only `server`. The host calls it when it
100
+ loads the extension; if it throws, or does not finish in 10 seconds, the
101
+ extension shows `load-failed` in Settings → Extensions and registers nothing
102
+ (see [debugging](debugging.md)).
103
+
104
+ `notify` returns a notification to the app. A text the user sees is a
105
+ `LocalizedText`: a plain string, or `{ en, ru }` as here. An id is written in
106
+ the code and must be the extension id or start with `acme.hello.`; the host
107
+ refuses an id that is taken or does not carry the prefix.
108
+
109
+ When both entries exist, keep each in its own file and re-export them from
110
+ `src/index.ts` (the recipes do): `server` must not import `vue`, `vuetify` or a
111
+ component, `client` must not import `node:*` modules, and the build reports a
112
+ violation with the file and the rule.
113
+
114
+ File `test/index.test.ts` (quick start):
115
+
116
+ ```ts
117
+ import { createTestServer } from '@dolphy-app/extension-sdk/testing';
118
+ import { expect, it } from 'vitest';
119
+ import { server } from '../src/index.ts';
120
+
121
+ it('the hello command notifies', async () => {
122
+ const running = await createTestServer(server, { extensionId: 'acme.hello' });
123
+ expect(await running.commands.run('acme.hello.hello')).toEqual({
124
+ kind: 'notify',
125
+ text: 'Hello from acme.hello!',
126
+ });
127
+ await running.dispose();
128
+ });
129
+ ```
130
+
131
+ The test runs the command the way the host does, without the app:
132
+ `createTestServer` from `@dolphy-app/extension-sdk/testing` starts `server` on
133
+ in-memory storage, settings and library, and `running.commands.run` applies the
134
+ rules the host applies to a command result. `createTestClient` does the same for
135
+ `client` (see the [command and panel recipe](recipe-command-panel.md)).
136
+
137
+ ## 3. Build, check, test
138
+
139
+ ```sh
140
+ pnpm build # dolphy-ext build: writes dist-ext/acme.hello
141
+ pnpm validate # parses the built manifest the way the app does
142
+ pnpm lint # metadata and bundle checks the catalog review also runs
143
+ pnpm typecheck # tsc
144
+ pnpm test # vitest
145
+ ```
146
+
147
+ `pnpm build` turns `src/index.ts` into the files the app loads. With this
148
+ template that is `dist-ext/acme.hello/extension.json` and `main.mjs`; the built
149
+ manifest names it in `main`. A project that exports `client` also gets
150
+ `client.mjs` and the `client` key. Keep `dist-ext` out of git; the generated
151
+ `.gitignore` does it.
152
+
153
+ ## 4. Try it in the app
154
+
155
+ One command builds the project in watch mode and starts the installed Dolphy
156
+ app on the result:
157
+
158
+ ```sh
159
+ pnpm exec dolphy-ext dev
160
+ ```
161
+
162
+ The extension appears in Settings → Extensions with the origin "Development",
163
+ and the command appears in the command palette. Press Ctrl+C to stop the build
164
+ and the app. Quit a running Dolphy first: the app has one instance, a second
165
+ start does not pick up the extension directory. Where the app is looked up and
166
+ what to do when it is not found: [debugging](debugging.md), section 3.
167
+
168
+ To run the two parts yourself, start the build in watch mode with `pnpm dev`
169
+ (`dolphy-ext build --watch`) and start Dolphy with the variable
170
+ `DOLPHY_DEV_EXTENSIONS` set to the absolute path of `dist-ext` of your project.
171
+ It adds a developer root with the highest priority.
172
+
173
+ ```sh
174
+ # from a checkout of the Dolphy repository
175
+ DOLPHY_DEV_EXTENSIONS=/path/to/my-extension/dist-ext pnpm dev
176
+ ```
177
+
178
+ Save a file: the build writes the new files and the app applies them without a
179
+ restart. The window does not reload; the components of extensions in
180
+ development are recreated, so their state can be lost.
181
+
182
+ The extension code runs without restrictions: files, processes, threads and the
183
+ network are available. The test helpers run a handler in your process and do not
184
+ reproduce the host (time limits, the process boundary); try such code in the
185
+ app.
186
+
187
+ ## 5. Next
188
+
189
+ - Pick the recipe closest to your task and generate that template.
190
+ - Before a pull request to the extension catalog run `pnpm build`,
191
+ `pnpm validate`, `pnpm lint`, `pnpm typecheck` and `pnpm test`: all must pass.
192
+ The generated `.github/workflows/ci.yml` runs the same steps on every push.
193
+ - Write in `README.md` what the extension does; the catalog
194
+ review reads it.