@akanjs/cli 3.0.0-beta.3 → 3.0.0-beta.30

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.
Files changed (194) hide show
  1. package/.build-stamp +1 -1
  2. package/CodeAgent-xnfyyz8h.js +9 -0
  3. package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
  4. package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
  5. package/CodeTui-h66xb1fh.js +1457 -0
  6. package/README.md +3 -0
  7. package/TunnelShare-cwh9ezsm.js +9 -0
  8. package/{agent.command-d2gfxfck.js → agent.command-c4v7kjff.js} +8 -9
  9. package/akanCode.js +102 -0
  10. package/application.command-yzsswrz6.js +241 -0
  11. package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-qkt6spd5.js} +121 -77
  12. package/buildBatch.proc.js +120 -32
  13. package/cloud.command-v8h688jc.js +95 -0
  14. package/cloud.script-yct1sk4k.js +20 -0
  15. package/code.command-jff3tw7y.js +154 -0
  16. package/commandManifest.json +1 -1
  17. package/consoleToStderr-h5bryj94.js +7 -0
  18. package/context.command-y5sevk3c.js +48 -0
  19. package/context.runner-90hsmacr.js +21 -0
  20. package/{dependencyScanner-shs82ynp.js → dependencyScanner-06z2dmrr.js} +2 -2
  21. package/{devTui-40ggmd99.js → devTui-q7k7bakd.js} +122 -40
  22. package/fileSys-wzzrntkw.js +9 -0
  23. package/guideline.command-11cndccb.js +42 -0
  24. package/guidelines/agentRule/agentRule.instruction.md +209 -33
  25. package/guidelines/conventions/conventions.instruction.md +143 -41
  26. package/guidelines/fieldRule/fieldRule.instruction.md +1 -0
  27. package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
  28. package/guidelines/queryRule/queryRule.instruction.md +21 -7
  29. package/guidelines/runtimeRule/runtimeRule.instruction.md +268 -6
  30. package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
  31. package/guidelines/transportRule/transportRule.instruction.md +51 -0
  32. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +20 -0
  33. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
  34. package/incrementalBuilder.proc.js +564 -109
  35. package/{index-fz5q3gd7.js → index-0ncc0mbk.js} +10 -17
  36. package/index-167c77aq.js +69 -0
  37. package/{index-kcxmv48c.js → index-2ahj896a.js} +45 -79
  38. package/index-2wc41k5w.js +49 -0
  39. package/{index-j9yhg404.js → index-35mmm9ca.js} +4 -7
  40. package/{index-a64esmtb.js → index-376gpdds.js} +523 -1022
  41. package/index-3hx1z96c.js +87 -0
  42. package/{index-v0qh106m.js → index-3rh7thcx.js} +961 -892
  43. package/{index-hwkygn9s.js → index-48k96p55.js} +325 -36
  44. package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
  45. package/index-5rp0h6na.js +1119 -0
  46. package/index-6x9p8dpb.js +4300 -0
  47. package/index-7md2g9vw.js +6 -0
  48. package/index-8tv7356t.js +700 -0
  49. package/index-94s080qm.js +266 -0
  50. package/index-9dp031by.js +12 -0
  51. package/{index-ws5e0j51.js → index-a4s954kq.js} +43 -26
  52. package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
  53. package/{index-ne2qy0nk.js → index-bxmejydw.js} +12 -12
  54. package/index-c6eggbee.js +123 -0
  55. package/index-d2242ksy.js +99 -0
  56. package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
  57. package/{index-259dsnz3.js → index-gxamdf18.js} +5 -8
  58. package/{index-4edzf1gr.js → index-hf6f4xnb.js} +77 -150
  59. package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
  60. package/{index-gyd220p2.js → index-hmdq5pqm.js} +554 -577
  61. package/{index-600zhnve.js → index-hqd0h1pb.js} +75 -203
  62. package/index-kb16nzyt.js +997 -0
  63. package/index-mk3b9rk1.js +2729 -0
  64. package/{index-2tv3fdx4.js → index-n3jk2501.js} +1205 -1458
  65. package/index-p6pbz9n5.js +25 -0
  66. package/{index-zry5nz81.js → index-qcejqgts.js} +51 -30
  67. package/{index-7737gxcp.js → index-rtwhw4aq.js} +107 -241
  68. package/{index-pnqbkf5p.js → index-se643zvp.js} +6 -2
  69. package/index-tsb0f8ya.js +53 -0
  70. package/{index-ktb5njgy.js → index-tzc1gtrg.js} +49 -99
  71. package/index-wa5jbxj1.js +760 -0
  72. package/index-wfwgcnyc.js +187 -0
  73. package/{index-mak3cd28.js → index-xb7qhxa8.js} +137 -255
  74. package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
  75. package/index.js +23 -22
  76. package/{library.command-nbsky672.js → library.command-n9kp7yms.js} +4 -5
  77. package/{localRegistry.command-yeq57wjr.js → localRegistry.command-jx6kxt2x.js} +23 -42
  78. package/{module.command-29vqzrh8.js → module.command-q2xjf6cx.js} +8 -11
  79. package/{package.command-axv0abmn.js → package.command-q5a3nsw5.js} +6 -7
  80. package/package.json +15 -7
  81. package/{page.command-vs9s3zrh.js → page.command-ppenrxjn.js} +4 -5
  82. package/{primitive.command-cbxmqbtc.js → primitive.command-dvgg8nem.js} +9 -22
  83. package/prompter-76k56b37.js +8 -0
  84. package/{quality.command-gmbs7nws.js → quality.command-pz7kg3pc.js} +205 -377
  85. package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
  86. package/{repair.command-bqsjptsj.js → repair.command-a6py9xvz.js} +11 -35
  87. package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
  88. package/{scalar.command-6k1vhe9s.js → scalar.command-7f3xfpen.js} +7 -10
  89. package/skills/akan-code/SKILL.md +130 -0
  90. package/skills/akan-debug/SKILL.md +42 -0
  91. package/skills/akan-deps/SKILL.md +50 -0
  92. package/skills/akan-err/SKILL.md +39 -0
  93. package/skills/akan-module/SKILL.md +45 -0
  94. package/skills/akan-scaffold/SKILL.md +76 -0
  95. package/skills/akan-scalar/SKILL.md +52 -0
  96. package/skills/akan-service/SKILL.md +34 -0
  97. package/skills/akan-store/SKILL.md +67 -0
  98. package/skills/akan-ui/SKILL.md +63 -0
  99. package/skills/akan-validate/SKILL.md +44 -0
  100. package/{subspace.command-fwjfcts7.js → subspace.command-z7nqs508.js} +138 -67
  101. package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
  102. package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
  103. package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
  104. package/templates/appSample/lib/task/task.abstract.ts +12 -32
  105. package/templates/appSample/lib/task/task.signal.spec.ts +15 -18
  106. package/templates/appSample/lib/task/task.signal.test.ts +15 -1
  107. package/templates/appSample/lib/task/task.signal.ts +1 -1
  108. package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
  109. package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
  110. package/templates/client.ts +7 -30
  111. package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
  112. package/templates/crudPages/[__model__Id]/page.tsx +2 -1
  113. package/templates/crudPages/new/page.tsx +2 -1
  114. package/templates/crudPages/page.tsx +2 -1
  115. package/templates/crudSinglePage/page.tsx +2 -1
  116. package/templates/lib/__lib/lib.dictionary.ts +0 -2
  117. package/templates/lib/__lib/lib.document.ts +0 -2
  118. package/templates/lib/sig.ts +8 -30
  119. package/templates/localDev/docker-compose.yaml.template +3 -14
  120. package/templates/module/__model__.signal.ts +12 -4
  121. package/templates/pkgRoot/tsconfig.json.template +2 -7
  122. package/templates/server.ts +1 -3
  123. package/templates/service/__model__.abstract.md.template +8 -0
  124. package/templates/workspaceRoot/.gitignore.template +0 -1
  125. package/templates/workspaceRoot/package.json.template +1 -3
  126. package/tunnel.command-zy4wbcgt.js +129 -0
  127. package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
  128. package/typecheck.proc.js +1 -1
  129. package/workflow.command-f142t17s.js +79 -0
  130. package/{workspace.command-8s050m0h.js → workspace.command-gpk1149x.js} +77 -109
  131. package/abstractCompactor-mjh5ppsq.js +0 -101
  132. package/application.command-pj2m7584.js +0 -204
  133. package/applicationReleasePackager-4yasxwv9.js +0 -244
  134. package/capacitorApp-36rbr858.js +0 -12
  135. package/cloud.command-jshwdq22.js +0 -93
  136. package/context.command-ez8a0zyz.js +0 -80
  137. package/devTuiView-c08ccbat.js +0 -9
  138. package/getDirname-dr3jz1xg.js +0 -7
  139. package/guideline.command-xmqwx8zm.js +0 -359
  140. package/guidelines/agentRule/agentRule.generate.json +0 -31
  141. package/guidelines/componentRule/componentRule.generate.json +0 -31
  142. package/guidelines/conventions/conventions.generate.json +0 -25
  143. package/guidelines/cssRule/cssRule.generate.json +0 -31
  144. package/guidelines/docPageRule/docPageRule.generate.json +0 -31
  145. package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
  146. package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
  147. package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
  148. package/guidelines/enumConstant/enumConstant.generate.json +0 -31
  149. package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
  150. package/guidelines/fieldRule/fieldRule.generate.json +0 -31
  151. package/guidelines/framework/framework.generate.json +0 -32
  152. package/guidelines/mcpRule/mcpRule.generate.json +0 -35
  153. package/guidelines/modelConstant/modelConstant.generate.json +0 -32
  154. package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
  155. package/guidelines/modelDocument/modelDocument.generate.json +0 -31
  156. package/guidelines/modelService/modelService.generate.json +0 -31
  157. package/guidelines/modelSignal/modelSignal.generate.json +0 -31
  158. package/guidelines/modelStore/modelStore.generate.json +0 -31
  159. package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
  160. package/guidelines/modelUnit/modelUnit.generate.json +0 -31
  161. package/guidelines/modelUtil/modelUtil.generate.json +0 -31
  162. package/guidelines/modelView/modelView.generate.json +0 -31
  163. package/guidelines/modelZone/modelZone.generate.json +0 -31
  164. package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
  165. package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
  166. package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
  167. package/guidelines/queryRule/queryRule.generate.json +0 -31
  168. package/guidelines/recipeRule/recipeRule.generate.json +0 -31
  169. package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
  170. package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
  171. package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
  172. package/guidelines/scalarModule/scalarModule.generate.json +0 -31
  173. package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
  174. package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
  175. package/guidelines/ssrRule/ssrRule.generate.json +0 -31
  176. package/guidelines/transportRule/transportRule.generate.json +0 -31
  177. package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
  178. package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
  179. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
  180. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
  181. package/index-2hq52jq3.js +0 -465
  182. package/index-47pj3aee.js +0 -76
  183. package/index-6wwzs1bc.js +0 -7
  184. package/index-8pm30m73.js +0 -841
  185. package/index-kn1h7p1x.js +0 -560
  186. package/index-meh558jy.js +0 -65
  187. package/index-rwg2aa4f.js +0 -229
  188. package/index-w8kmx1b6.js +0 -2176
  189. package/index-wt7qrtd4.js +0 -1516
  190. package/templates/__scalar/__model__/__model__.abstract.md +0 -28
  191. package/templates/module/__model__.abstract.md +0 -28
  192. package/templates/service/__model__.abstract.md +0 -28
  193. package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +0 -62
  194. package/workflow.command-nwpcdc8p.js +0 -107
@@ -1,4 +1,4 @@
1
- # Runtime Rule — Serving, Processes, Logging, Image, Assets
1
+ # Runtime Rule — Serving, Processes, Logging, Image, Native Apps, Desktop Server, Assets
2
2
 
3
3
  How an Akan app is built, what it serves, how many processes it runs, where its logs go, and what ends up in
4
4
  its image. Everything here is declared in `akan.config.ts` or narrowed by an env at boot; none of it is reached
@@ -11,14 +11,14 @@ on; the other two are declared in `akan.config.ts` as **`web: true | false | { c
11
11
  narrowed again per deployment.
12
12
 
13
13
  ```ts
14
- const config: AppConfig = { web: { csr: false } }; // web without the mobile bundle
14
+ const config: AppConfig = { web: { csr: false } }; // web without the CSR bundle the native apps ship
15
15
  const config: AppConfig = { web: false }; // api only
16
16
  ```
17
17
 
18
18
  - **`web: { csr: false }`** drops the CSR build phase and the `/__csr` + `?csr=true` routes. The CSR bundle is
19
- what the Capacitor mobile build ships, so a web-only deployment never needs it — and an app that declares a
20
- `mobile` section is refused, because `akan build-ios` copies `dist/apps/<app>/csr/<target>.html` into the
21
- native project.
19
+ what the native apps ship, so a web-only deployment never needs it — and an app that declares a `native` section
20
+ is refused, because every native build copies `dist/apps/<app>/csr/<target>.html` into the target's web root,
21
+ `.akan/native/<target>/web`.
22
22
  - **`web: false`** is an API-only build: no base artifact, no pages or client bundles, no RSC worker
23
23
  entrypoint, and no `public/` in the image (the web router's catch-all is its only reader). Nothing under
24
24
  `page/` is served, including routes a lib contributed through `syncPageLibs`.
@@ -68,7 +68,7 @@ const config: AppConfig = { api: { prefix: "/backend", websocketPrefix: "/socket
68
68
  origin when the module graph initializes, before any component renders, so a React prop — a `System.Provider`
69
69
  value, anything out of `implicitRootLayout` — is always too late. The value rides the SSR classic bootstrap
70
70
  script instead, the one thing guaranteed to run ahead of every module script, and is omitted when it matches
71
- what the bundle already assumes. `akan.config.ts` is what a Capacitor bundle or a statically served CSR shell
71
+ what the bundle already assumes. `akan.config.ts` is what a native app bundle or a statically served CSR shell
72
72
  follows, because neither is rendered by a server that could tell it otherwise.
73
73
  - **A prefix is a path segment.** A blank value or a bare `/` is refused: `/` would be mounted ahead of the SSR
74
74
  catch-all and swallow every page route. A prefix whose first segment is a declared basePath is refused at
@@ -184,6 +184,268 @@ const config: AppConfig = { docker: "FROM oven/bun:1-slim\n…" }; // verbatim,
184
184
  - `AkanAppConfig.docker` is the resolved declaration; `AkanAppConfig.dockerfile` is the text `akan build` writes
185
185
  to `dist/apps/<app>/Dockerfile`.
186
186
 
187
+ ## Native Apps — The `native` Section
188
+
189
+ An app's iOS, Android and desktop (macOS, Windows, Linux) apps are the `native` section of its `akan.config.ts`.
190
+ Each ships the CSR bundle on the native runtime, so one set of pages serves the web and every app.
191
+
192
+ ```ts
193
+ const config: AppConfig = {
194
+ native: {
195
+ appName: "Board",
196
+ appId: "com.example.board", // or one per platform: { default, ios, android, macos, windows, linux }
197
+ version: "1.2.0",
198
+ buildNum: 12,
199
+ indexPath: "/explore", // the first page, and where a deep link's stack and a back with no history land
200
+ permissions: ["camera", "push"],
201
+ plugins: ["file-picker", "keep-awake"],
202
+ deepLinks: { schemes: ["board"], domains: ["board.example.com"] },
203
+ ios: { teamId: "ABCDE12345" }, // universal links for deepLinks.domains
204
+ android: { googleServices: "secrets/google-services.json", sha256CertFingerprints: ["…"] }, // FCM, app links
205
+ desktop: { server: true },
206
+ },
207
+ };
208
+ ```
209
+
210
+ - **Without `targets` the app has one target, `default`** — the usual case, with every setting in the section
211
+ itself; `native: { desktop: { server: true } }` alone is a whole desktop app that carries its server. Defaults:
212
+ the app's folder name for `appName` and `fileName`, an id made from the repository's and the app's names for
213
+ `appId`, `0.0.1` and `1`.
214
+ - **A target is another app made from the section.** It has the section's shape minus `targets` and overrides it
215
+ field by field: plain objects merge key by key — `ios`, `android`, `desktop`, `deepLinks`, `updates` and the
216
+ objects inside them — and every other value replaces the section's, a list included, so a target's `plugins` or
217
+ `permissions` is its whole list. An `icon` or `splash` object is one value. Once `targets` is declared, only the
218
+ targets it names are built:
219
+
220
+ ```ts
221
+ native: {
222
+ appId: "com.example.board",
223
+ plugins: ["file-picker"],
224
+ targets: {
225
+ board: {},
226
+ lobby: { appId: "com.example.lobby", plugins: ["file-picker", "keep-awake"], desktop: { recovery: "reload" } },
227
+ },
228
+ },
229
+ ```
230
+
231
+ - **`basePath` is the client a target opens.** An app with no basePaths leaves it out. Without `targets`, the one
232
+ target opens the basePath named like the app when there is one; in an app with basePaths, a target that names
233
+ none is a template, and `--target <basePath>` builds that client from it.
234
+ - Every native command takes `--target <name>`. `build-*`, `release-*`, `update-keygen` and `publish-update` also
235
+ take `all`; `start-*` and `pack-update` handle one target at a time.
236
+ - **Everything a build makes is under `apps/<app>/.akan/native/<target>/`**: `build/<platform>` (`build-ios`,
237
+ `build-android`, `build-desktop`), `dev/<platform>` (the dev builds `start-*` run), `updates` (what
238
+ `publish-update` signs and `pack-update` packs), `web` (the bundle the app loads) and `bin`. `akan start` leaves
239
+ the folder alone.
240
+ - **`start-ios` / `start-android` / `start-desktop` run a dev build that loads its pages from `akan start`.** It has
241
+ to be running — except for a desktop app that carries its server, which starts it when none of this checkout
242
+ answers. `--release true` runs a release build of the app's own bundle instead.
243
+ - **A desktop app builds only for the computer that builds it** — a `.app` on macOS, signed ad hoc or with the
244
+ development identity, and an unsigned folder on Windows and Linux. `build-desktop --installer true` adds a
245
+ per-user NSIS setup on Windows (`/S` installs silently). Distribution signing and notarization are not akan
246
+ commands yet.
247
+ - **`updates: { url, publicKey }` lets an installed app update itself** — the whole app on a desktop, the web
248
+ bundle on a phone. `akan update-keygen <app>` makes the signing key once
249
+ (`~/.akan/native/keys/<app id>.update.key`, or the path `AKAN_NATIVE_UPDATE_KEY` names) and prints the
250
+ `publicKey`. `akan publish-update <app>` builds and signs a release into `updates/`: upload that folder to `url`,
251
+ the manifests last. `akan pack-update` writes an unsigned phone bundle for whoever holds the key to sign. The
252
+ channel is the backend env the binary was built for unless `channel` names one, and a release that does not
253
+ come up is rolled back to the one before.
254
+ - **An unattended screen** — signage, a kiosk — takes `desktop.recovery: "reload"` (a crashed page loads again,
255
+ and the app relaunches when the webview ends), `desktop.window: { fullscreen, skipTaskbar }`,
256
+ `desktop.screenCapture: "auto"` (Windows answers `getDisplayMedia()` with the first screen, no picker) and
257
+ `android.autoplay` (media plays with sound without a tap).
258
+
259
+ ## A Desktop App's Server — `bin` And `trustedDependencies`
260
+
261
+ A desktop app whose `native` section, or target, declares `desktop: { server: true }` carries the app's server:
262
+ `akan build-desktop`, `start-desktop --release` and `publish-update` put the backend `akan build` made into the
263
+ app — everything the backend build wrote (the `.js`, `akan.build.json`, `private/`, a bundled package's `.node`,
264
+ `.wasm` or file asset) but the Dockerfile, the RSC worker, the console, `csr/` and `public/` — and install its
265
+ packages first with `bun install --production --prefer-offline`, before `akan build` runs. `start-desktop` without
266
+ `--release` starts `akan start` beside the app when no dev server of this checkout answers. The setting is the
267
+ app's backend, so it does not change under an installed app: an update whose release carries a server when the app
268
+ has none, or none when it has one, is refused, and switching means a reinstall.
269
+ A desktop app builds only for the computer it is built on, so every native addon's prebuild matches the one it runs
270
+ on. It runs as an API-only edge server on SQLite, on a loopback port the launcher picks — the last session's when
271
+ it is free, which is not a guarantee, so a provider that needs an exact redirect URI signs in through a cloud
272
+ server's adapter. It refuses a `Host` other than its own, but any program on the computer can still call that port:
273
+ guard its endpoints as a network server's. It runs in one process, so a `main.ts` or an env asking for replicas (`replica`,
274
+ `solo: false`, `AKAN_SOLO=false`, `AKAN_REPLICA`, `AKAN_COMMAND_TYPE=start`) does not boot in the app. Its data lives in the app's local data folder (`%LOCALAPPDATA%\<id>\server`
275
+ on Windows, never Roaming; `server-debug` for a `--debug` build). It trusts the OS certificate store, and of the
276
+ user's environment it takes a fixed set and nothing else: the system's own (`PATH`, `HOME`, the temp folder, `LANG`,
277
+ `TZ`, Windows' folders and `ComSpec`), the desktop session a `bin` tool needs for the screen or the audio server
278
+ (`DISPLAY`, `XAUTHORITY`, `WAYLAND_DISPLAY`, the session bus, `PULSE_*`, `XDG_*`), and the proxy and CA variables
279
+ (`HTTP(S)_PROXY`, `NO_PROXY`, `NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`).
280
+
281
+ **Everything it carries is on the user's computer, readable.** That is `private/` as it is — each lib's too, under
282
+ `private/libs/<lib>` — and the server env of the one environment the build is for (`--env`: `debug` for
283
+ `build-desktop`, `local` for `start-desktop --release`, `main` for `publish-update`, unless named): the build keeps
284
+ `env/env.server.<env>.ts` and swaps every other environment's file for exports that refuse to be read. The server env
285
+ each lib exports as its defaults (the lib's `env.server.testing.ts`, spread into every environment by
286
+ `env.server.type.ts`) ships as well. Keep a key in those files only if every user of the app may hold it; a secret the
287
+ server needs belongs to a cloud server its adapter calls. There is no `public/`: a file the server reads at runtime
288
+ goes in `private/` and is read from `AKAN_APP_DIR` (the folder `server.js` sits in), never from `process.cwd()`, which
289
+ is the app's data folder.
290
+
291
+ **Nothing from `docker` reaches it.** `preRuns`, `postRuns` and a whole Dockerfile install into a Linux image the
292
+ desktop app never runs in, and the build warns when an app has them and carries no `bin`. What the server needs
293
+ from them comes one of two ways:
294
+
295
+ ```ts
296
+ const config: AppConfig = {
297
+ trustedDependencies: ["rclnodejs"],
298
+ bin: {
299
+ ffmpeg: {
300
+ "darwin-arm64": {
301
+ url: "https://files.example.com/ffmpeg-7.1-lgpl-darwin-arm64.zip",
302
+ sha256: "…",
303
+ file: "ffmpeg-7.1/bin/ffmpeg",
304
+ },
305
+ "win32-x64": { url: "https://files.example.com/ffmpeg-7.1-lgpl-win64.zip", sha256: "…", file: "bin/ffmpeg.exe" },
306
+ "linux-x64": { path: "tools/linux-x64/ffmpeg" },
307
+ },
308
+ },
309
+ };
310
+ ```
311
+
312
+ - **An executable the desktop app spawns goes in `bin`** — its server's code or its native plugins, whether or not
313
+ it carries a server — keyed by the name the code spawns and then by
314
+ `${process.platform}-${process.arch}` (`darwin-arm64`, `darwin-x64`, `linux-arm64`, `linux-x64`, `win32-arm64`,
315
+ `win32-x64`). A source is `{ url, sha256, file? }` — downloaded when the app is built, refused unless it hashes
316
+ to `sha256` (so plain http is as safe as https), kept in `apps/<app>/.akan/cache/bin/<sha256>` — or
317
+ `{ path, file? }`, relative to the `akan.config.ts` that declares it. `file` is the executable inside an archive
318
+ (`.zip`, `.tar.gz`, `.tgz`, `.tar.xz`, `.tar.bz2`, `.tar`), unpacked with the OS's own `tar` (`unzip` for a zip
319
+ on Linux).
320
+ - Only the building computer's platform is fetched, and an entry without it fails the build instead of the user's
321
+ call. `build-desktop` and `start-desktop` copy the file into the app's `bin/` (keeping `.exe` on Windows), sign it
322
+ with the app on macOS, and put that folder first on the app's PATH. The server's `spawn("ffmpeg")` needs no change
323
+ and finds the carried file even in an app launched from the Finder, whose PATH is only
324
+ `/usr/bin:/bin:/usr/sbin:/sbin`. A native plugin finds the folder in `ctx.binDir`: Bun's own `spawn` and `which`
325
+ without `env` read the environment the app started with, so a plugin passes `env: process.env` to run one by name.
326
+ The server passes it too when it spawns a carried `bun build --compile` executable: the shell starts the server as
327
+ Bun with `BUN_BE_BUN=1`, which akanjs takes out of `process.env` at boot, and a child spawned without `env` still
328
+ inherits it and runs as Bun's own CLI instead of the tool.
329
+ - **One file per entry, so carry a static build.** A build that loads its own shared libraries — a `-shared`
330
+ archive, or Homebrew's ffmpeg with its 55 dylibs — runs on the computer that built it and nowhere else.
331
+ - **A lib's `bin` reaches only the apps that depend on it**, unlike its `docker` steps; the app's own entry of the
332
+ same name wins, and two libs that declare one name differently fail the build.
333
+ - **The image does not read `bin`.** Keep installing through `docker.preRuns` there.
334
+ - **A package that builds itself at install goes in `trustedDependencies`.** `bun install --production` runs no
335
+ dependency's install or postinstall script unless the package is trusted, so an addon with no prebuild arrives
336
+ unbuilt and fails at its first call. The list lands in the built `package.json`, so it applies to the image and to
337
+ the desktop server alike, and a lib's list reaches every app, like its `externalLibs`.
338
+
339
+ **Carry an LGPL ffmpeg.** A build configured with `--enable-nonfree` may not be redistributed at all — the macOS
340
+ binary npm's `ffmpeg-static` downloads is one — and a `--enable-gpl` build obliges you to offer its source. An LGPL
341
+ build has no `libx264`, so encode H.264 through the OS's encoder: `h264_videotoolbox` on macOS, `h264_mf` on
342
+ Windows, VAAPI or NVENC on Linux. Codec patents are a separate question to settle before shipping.
343
+
344
+ **A server bound to its machine stays a service.** A server that needs a whole environment — ROS, system services,
345
+ root to change the network or the clock — runs as a service on that machine (the image), and the desktop app's
346
+ target leaves `desktop.server` off, pinned to it with `AKAN_PUBLIC_SERVER_URL` at build time. A carried server runs
347
+ as the signed-in user and stops with the app, and so does what it started. On macOS and Linux the server leads its
348
+ own process group: quitting the app ends the group, and when the app was killed or crashed — even while the server
349
+ was still starting — the server sees its parent gone and ends the group itself, SIGTERM first and SIGKILL a second
350
+ later for whatever ignored it. On Windows the job object ends them all with the app. A process started `detached`
351
+ leaves the group, or the job, and keeps running.
352
+
353
+ **A file the user picks reaches the server as a grant, never as a copy or a path.** With `"file-picker"` in
354
+ `native.plugins`, `filePicker.pickFiles({ forServer: true })` (also `pickDirectory` and `saveFile`,
355
+ from `akanjs/client/native`) copies nothing, whatever the size: its FileRefs serve the originals for a preview, and
356
+ each result carries a `grant`. The page hands the grant to an endpoint, and the server exchanges it:
357
+
358
+ ```ts
359
+ const input = await NativeFile.resolve(grant, "read"); // "write" for a saveFile grant, "folder" for pickDirectory
360
+ const clip = await NativeFile.resolveIn(folderGrant, "day1/a.mp4"); // refused if it climbs out of the folder
361
+ ```
362
+
363
+ The carried server asks the shell that showed the dialog over its IPC channel, so it reaches what the user picked
364
+ and nothing else, and no page ever holds a path. Behind a dev build `akan start` checks the grant's signature
365
+ against a key in `~/.akan/native` instead, in `operationMode` local only; every other server refuses a grant.
366
+
367
+ **Devices belong to the shell, not the server.** Displays and their changes (`screen`), windows placed on them
368
+ (`window`), the system volume and mute (`volume`), global shortcuts (`global-shortcut`), keep-awake (`keep-awake`)
369
+ and launch at login (`autostart`) are native runtime plugins, each added to `native.plugins`. Every builtin plugin's
370
+ page API is
371
+ `akanjs/client/native/<id>` (`akanjs/client/native/window`, `…/screen`, `…/global-shortcut`), imported in a `webkit/`
372
+ hook; `volume` and `filePicker` also come from `akanjs/client/native` itself. A capability the shell
373
+ lacks is added there: an app's own plugin runs as Bun code in the plugin host and cannot add a native shell op.
374
+
375
+ ## Database Modes — `database` In `akan.config.ts`
376
+
377
+ | mode | database | cache · queue · pubsub | runs as |
378
+ |---|---|---|---|
379
+ | `single` | SQLite file | SQLite files (Solid) | one container |
380
+ | `multiple` | one SQLite file (WAL) on a host volume every container on that host opens | Redis | containers on one host |
381
+ | `cluster` | Postgres | Redis | pods on several servers |
382
+
383
+ - **`database: { modes: ["single", "cluster"] }` is what the build can run in**; the first is what `akan start`
384
+ runs. `akan build` carries the drivers of every declared mode (`multiple` → `bullmq`, `ioredis`; `cluster` adds
385
+ `postgres`).
386
+ - **A deployment names its mode with `AKAN_DATABASE_MODE`**, and only a declared one. With one declared mode it may
387
+ leave it out; with several it must name one — the fallback would be a SQLite file inside each pod. `akan start`,
388
+ `build`, `script` and `console` resolve it the same way from the shell. One image can serve an edge site in
389
+ `single` and a cloud cluster in `cluster`: declare both.
390
+ - **Where the data lives is the deployment's env, ahead of `env.server.ts`**: `SQLITE_DATABASE_PATH`,
391
+ `AKAN_SOLID_DB_PATH`, `POSTGRES_URL` (or `POSTGRES_HOST`/`PORT`/`DATABASE`/`USER`/`PASSWORD`),
392
+ `POSTGRES_INSIGHT_URL`, `LIBSQL_URL`. `REDIS_URI` is required outside local development. Pool size, SSL and
393
+ prepared statements ride the Postgres URL (`?max=20&ssl=require`, `prepare=false` behind PgBouncer).
394
+ - **Uploads**: a deployed `multiple`/`cluster` app uses object storage, or a volume every instance mounts with
395
+ `AKAN_STORAGE_SHARED=true`.
396
+ - **`runAdminSql` on Postgres reads as its own login role** holding base-column `SELECT` and nothing else:
397
+ `CREATE ROLE <name> LOGIN PASSWORD '…'` with no other role granted, `POSTGRES_INSIGHT_URL` logging in as it, and
398
+ `GRANT USAGE ON SCHEMA` from the DBA when the app does not own its schema. Every model table grants the role its
399
+ base columns at boot. Without it the console refuses on Postgres; `_doc` is never readable either way.
400
+ - **`akan dbup`** starts what the workspace's apps declare (`--mode multiple` is Redis, `cluster` adds Postgres).
401
+ **`akan db-export <app>` / `akan db-import <app>`** move every model table as stored rows through NDJSON files
402
+ (`--dir`, default `local/transfer`) between whatever the shell's `AKAN_DATABASE_MODE` points at; a rerun
403
+ replaces rows, search is rebuilt after, and sessions, queued jobs and uploaded files do not move.
404
+ - **`q.raw(sql)` is the dialect's own SQL** — `json_extract("_doc", '$.f')` on SQLite, `("_doc" #>> '{f}')` on
405
+ Postgres. An app that runs in both modes avoids it.
406
+
407
+ ## Operations — Snapshots, Restore And The `/_akan/ops` Channel
408
+
409
+ `GET /_akan/app/info` answers `{ appName, repoName, environment, operationMode }` on every deployment — the same
410
+ `AKAN_PUBLIC_*` values the client bundle already carries — so a control plane can check which app a host is
411
+ serving without a key. Everything else is behind the ops channel.
412
+
413
+ | surface | what it does |
414
+ |---|---|
415
+ | `bun main.js ops snapshot [--id] [--out] [--include-solid] [--json]` | `VACUUM INTO` a consistent copy, `integrity_check`, sha256, gzip, optional age encryption, `manifest.json` |
416
+ | `bun main.js ops restore <manifest.json> [--identity <file>]` | offline only: verify, stage, keep the old file as `<db>.pre-restore-<ts>` (its `-wal`/`-shm` beside it), swap |
417
+ | `GET /_akan/ops/info` | the public info plus `akanVersion`, `buildId`, `serverMode`, `databaseMode`, `solo`, `startedAt`, `replicaIdx` |
418
+ | `POST /_akan/ops/snapshot` `{ id, includeSolid?, uploadUrls: { main, solid?, manifest } }` | 202; a child process snapshots, then PUTs each file and the manifest **last** |
419
+ | `GET /_akan/ops/snapshot/:id` | `running` → `uploading` → `done` \| `failed`; kept in memory, so a restart answers 404 |
420
+
421
+ - **The entry is the image's own `main.js`** — a production image carries no `akan` CLI. `AkanApp.start()` hands
422
+ `argv[2] === "ops"` to the command before it boots anything. It reads the container's env — the `AKAN_PUBLIC_*`
423
+ identity and the database paths — so run it with that env (`docker compose exec`), never a bare shell's.
424
+ - **Restore refuses while an app answers `/_akan/app/health` on `PORT`** and names it by its `/_akan/app/info`;
425
+ `--force` is for a port some other app holds.
426
+ - **The channel exists only when `AKAN_OPS_PUBLIC_KEY` is set** (Ed25519: PEM, JWK, base64 SPKI or raw 32 bytes).
427
+ The control plane signs a short EdDSA JWS — `aud`, `iat`, `exp` at most 300s after `iat`, a `jti` the process
428
+ refuses twice — and the host holds only the public half, so a machine someone carries off mints nothing.
429
+ `aud` is `<appName>/<environment>`, or `<appName>/<environment>/<AKAN_OPS_INSTANCE>` when that is set, and then
430
+ only that: a token minted for the app as a whole would replay across every host sharing the name. 30 calls a
431
+ minute per process; every refusal is the same 401. A malformed key leaves the app serving with the channel off.
432
+ - **The app holds no storage credential.** It PUTs to the presigned URLs it was handed (`application/octet-stream`,
433
+ the manifest `application/json`), plain http only to loopback. The manifest landing is the commit marker.
434
+ - **Snapshots cover the SQLite modes only.** `single` copies the main file and, with `includeSolid`, the Solid file;
435
+ `multiple` copies the main file; `cluster` is refused. Paths follow the adaptors' env precedence
436
+ (`SQLITE_DATABASE_PATH`, `AKAN_SOLID_DB_PATH`, `AKAN_SQLITE_DIR`); a solo process uses the path its adaptor
437
+ actually opened, so a gateway deployment that moved the file in `env.server.ts` names it in the env too.
438
+ - **`AKAN_BACKUP_RECIPIENT=age1…[,age1…]` encrypts before upload** — age v1 X25519, so `age -d -i key.txt` opens
439
+ it. Restore takes `--identity` / `AKAN_BACKUP_IDENTITY_FILE`, or a file an operator already decrypted with
440
+ `age -d -o <name without .age>`.
441
+ - **The host keeps the newest `AKAN_OPS_SNAPSHOT_KEEP` (default 2) snapshots** under `AKAN_OPS_SNAPSHOT_DIR`
442
+ (default `<db dir>/snapshots/<id>/`) — the copy an edge still has while its uplink or the bucket is down.
443
+ - **`buildId` is `AKAN_BUILD_ID` when the deployment sets it**, else the git sha `akan build` wrote into
444
+ `akan.build.json` beside `main.js` (`-dirty` when the tree was).
445
+ - **`operationMode` does not gate any of this** — the key does. `edge` means a production host outside the
446
+ cluster: the console refuses it like `cloud`, and `init` / `interval` / `cron` take `operationMode: ["edge"]` to
447
+ run only there. Everything else behaves as `cloud`; `local` is a developer machine.
448
+
187
449
  ## Shipped Assets — `assets` In `akan.config.ts`
188
450
 
189
451
  `akan build` copies the whole `public/` tree into `dist`, lib assets dereferenced, and that copy is the image.
@@ -129,8 +129,10 @@ eight queries threw; per-section boundaries put each rejection at its own bounda
129
129
  soft-redirect script, so `getSelf({ unauthorize: "/signin" })` stays in `_layout.tsx` or the page body — never
130
130
  inside a streamed section.
131
131
 
132
- **⑥ Use named `ReactNode` slots, not just `children`.** `Layout.Navbar` accepts `title`, `back`, `left`, `right`,
133
- and `children`, so a client shell composes server-rendered content in five places instead of absorbing it.
132
+ **⑥ Use named `ReactNode` slots, not just `children`.** A client shell that takes one slot per region lets a
133
+ server component supply each of them, instead of absorbing the whole subtree into the bundle. `Layout.Navbar`
134
+ declares `title` / `left` / `right` alongside `back` and `children`, but its render body uses only `back` and
135
+ `children` — copy the shape, not that component.
134
136
 
135
137
  **⑦ Let the server do the derived work.** Display and predicate logic belongs on `Light<Model>` (`isNew()`,
136
138
  `canWrite(user?)`, `formatTimes()`), and enum→class lookups belong in a module-scope `as const` map. Both sides call
@@ -28,6 +28,57 @@ mutation is mounted under. `conventions` carries the invariants — this is the
28
28
  neither again, and cleanup that must happen either way registers for both. From a `message` handler it belongs to
29
29
  the socket and runs at close. A handler that throws is logged and never blocks the rest of the teardown.
30
30
 
31
+ ## Request Deadlines
32
+
33
+ - **`{ timeout: <ms> }` in an endpoint's signal option is the whole configuration surface**, and it reaches both
34
+ ends of the call. Server side, the `Timeout` middleware races the handler against it and answers
35
+ `base.error.gatewayTimeout` (504) once it is spent. Client side, `FetchSerializer` ships the value on the
36
+ serialized endpoint and `fetch.<endpoint>()` uses it as that request's `AbortSignal.timeout` budget, so the
37
+ browser does not abandon a call the server is still allowed to be working on.
38
+ - **The middleware is registered by default and has no default of its own.** An endpoint that declares no
39
+ `timeout` is passed straight through — a default here would put a deadline on every endpoint in the app that
40
+ nobody asked for, which is why the middleware used to be opt-in and, being opt-in, made the declared value
41
+ dead. It clears its timer in a `finally`: the race's loser is a pending timer per call otherwise, and with a
42
+ five-minute budget that is a five-minute hold on the event loop for a call that answered in a millisecond.
43
+ - **Without a declaration the client's own default applies — 30 seconds** (`DEFAULT_TIMEOUT_MS`,
44
+ `fetch/client/httpClient.ts`), and the server imposes nothing. Nothing else bounds a request: `Bun.serve` runs
45
+ with `idleTimeout: 0`, so a long call is only ever ended by one of these two.
46
+ - **Precedence is caller, then endpoint, then client default.** `fetch.x(…, { timeout: 5_000 })` overrides what
47
+ the endpoint declared; `{ timeout: false }` waits as long as the runtime will. An app moves its own default
48
+ with `fetch.instance.setTimeout(ms)` — `lib/useClient.ts` is generated, so the `timeout` option on
49
+ `FetchClient.build` belongs to the template that writes it, not to app code.
50
+ - **A file upload is never bounded**, on any of those paths: a large body on a slow uplink is a request that is
51
+ working, and the request body is the only thing the client can tell that from.
52
+ - **The deadline answers the caller; it does not undo the call.** `Promise.race` cannot cancel `next()`, so a
53
+ handler that timed out keeps running and keeps writing. Reach for a `timeout` to bound what a *caller* waits
54
+ for, not to bound an effect — an operation that must not half-happen needs its own idempotency or compensation.
55
+ - The failure is the same `base.error.gatewayTimeout` key whichever side gave up, so the user reads one sentence
56
+ in their own language either way. What differs is the status code — 504 from the server's middleware, 408 from
57
+ the client's own abort — and nothing downstream branches on that.
58
+
59
+ ## Cached Answers
60
+
61
+ - **`{ cache: <ms> }` in an endpoint's signal option is the whole cache surface.** The lookup is a step of every
62
+ call and stands aside unless an endpoint declares one.
63
+ - **Only a `query` that takes no internal argument may carry one.** The handler's inputs are its declared
64
+ arguments plus its internal ones, so an endpoint with none of the latter answers the same thing to everyone who
65
+ may read it — which is the only answer a shared entry can hold. An endpoint that takes `.with(Self)` answers
66
+ per caller, and one entry would be one caller's answer handed to the next; that endpoint and every `mutation`
67
+ are named once in the log and left uncached rather than silently ignored.
68
+ - **The guards run before every hit.** The lookup sits after the guards and the internal arguments, inside the
69
+ call, so every middleware — the account resolution a lib registers among them — has already run and a hit
70
+ reaches only a caller the guards admitted. It used to be a middleware ahead of the lib's, and a guard reading
71
+ the account refused a signed-in caller on every hit. Shared is not public.
72
+ - **The entry is the handler's result, not the response.** The lookup wraps the handler only, so `resolveReturn`
73
+ still runs per call on the cached value — field masking, `hidden`/`secret` stripping and relation resolution are
74
+ never cached, and a relation still costs its own load. The cache saves the handler, not the serialization.
75
+ - **A cache backend that is down does not take the endpoint down.** A failed read is warned and the call runs
76
+ uncached; a failed write is warned and dropped.
77
+ - The default middleware chain is `Logging → Timeout → <lib middlewares>`, and the call inside it runs guards →
78
+ internal arguments → cache → handler: a hung cache backend is bounded by the same deadline as the handler it
79
+ stands in for. **`Retry` was removed** — an automatic re-run of an endpoint
80
+ whose failure it cannot classify replays whatever the first attempt already did.
81
+
31
82
  ## Binary Pubsub
32
83
 
33
84
  - **`pubsub(Binary)` sends its payload in a websocket binary frame**, skipping the JSON `{ type: "pub" }`
@@ -7,6 +7,8 @@
7
7
  - `apps/<app>/page` holds server-side file-routed pages: `<routeName>.tsx` serves `/routeName`, a directory's
8
8
  `_index.tsx` serves that directory, `_layout.tsx` nests a layout, and `[modelId]` is a dynamic segment. Each route
9
9
  file is one chain — `export default page()…render()`, or `layout()` / `rootLayout()` in a `_layout.tsx`.
10
+ - The `native` section of `akan.config.ts` makes the app an iOS, Android and desktop app built from the same pages,
11
+ and `apps/<app>/native/<id>` holds a native plugin the app owns.
10
12
 
11
13
  The file roles inside a module — which file owns persistence, business logic, state, and each UI shape — are in
12
14
  the convention set above under **Domain Module Conventions**.
@@ -112,6 +114,22 @@ For the default generated app, start with:
112
114
  akan start <%= appName %>
113
115
  ```
114
116
 
117
+ ### Native Apps — iOS, Android, Desktop
118
+
119
+ `native: { desktop: { server: true } }` in `akan.config.ts` is all a desktop app that carries its own server needs.
120
+ A `start-*` command loads its pages from `akan start`, so run that first; a desktop app that carries its server
121
+ starts it itself. The full contract — targets, updates, what the carried server ships — is
122
+ `akan guideline show runtimeRule`.
123
+
124
+ ```bash
125
+ akan start-desktop <%= appName %> # Run the desktop app on this computer
126
+ akan start-ios <%= appName %> # Run it in a simulator or on an iPhone (start-android alike)
127
+ akan build-desktop <%= appName %> # Build it for this computer into .akan/native/<target>/build
128
+ akan build-desktop <%= appName %> --installer true # Windows: add a per-user setup program
129
+ akan update-keygen <%= appName %> # Make the update signing key once; print its public key
130
+ akan publish-update <%= appName %> # Build and sign a desktop release to upload to updates.url
131
+ ```
132
+
115
133
  ### The Essential Loop: Workflow -> Sync -> Check
116
134
 
117
135
  Almost every Akan.js change follows this pattern. **Missing sync or repair is the #1 cause of agent confusion.**
@@ -200,6 +218,8 @@ final fallback when no CLI command covers the change.
200
218
  | Add a React hook or browser helper | `webkit/` → camelCase `.ts` with `"use client"` | `akan sync <name>` |
201
219
  | Add a server-only guard, middleware, or adaptor | `srvkit/` → PascalCase `.ts` | `akan sync <name>` |
202
220
  | Add a pure helper (no DOM, no server API) | `common/` → camelCase `.ts` | `akan sync <name>` |
221
+ | Make the app an iOS, Android or desktop app | `akan.config.ts` → `native: { … }`; `desktop: { server: true }` puts the server in the desktop app | `akan start-desktop <name>` (or `start-ios` / `start-android`) |
222
+ | Add a device capability the framework lacks | `native/<id>/` → `native-plugin.json`, `src/index.ts` (page API, used from a `webkit/` hook), `src/desktop.ts`, Kotlin, Swift | the platform's `akan start-*` |
203
223
 
204
224
  ## Workflow Recipes
205
225
 
@@ -376,15 +376,16 @@ export default page()
376
376
  |---|---|
377
377
  | `interface PageProps { params }` + `[x]` folder | `.param("x", ID \| String \| Int \| …)` — one per `[x]` segment, every one of them, in path order |
378
378
  | `searchParams.k` | `.search("k", Type)`; `[Type]` for a repeated key; the value arrives typed and optional |
379
- | `params.lang` | `lang` — on every route's `.render()`, `.head()`, `.metadata()` and `.loading()` with no stage; `.param("lang")` is refused |
379
+ | `params.lang` | `lang` — on every route's `.render()`, `.head()` and `.loading()` with no stage; `.param("lang")` is refused |
380
380
  | `export const pageConfig = {…}` | `.config({…})` — `devOnly` stays a literal |
381
381
  | `export const head = <…/>` / `generateHead({ params })` | `.head(<…/>)` / `.head(({ x }) => <…/>)` |
382
- | `export const metadata` / `generateMetadata` | `.metadata({…})` / `.metadata((args) => …)` |
382
+ | `export const metadata` / `generateMetadata` | `.head(<><title>…</title><meta name="description" content="…" /></>)` — there is no object form |
383
383
  | `export function Loading()` | `.loading((args) => …)` |
384
384
  | `export default async function Page({ params, searchParams })` | `.render(async ({ x, k }) => { …same body… })` — `async` only if it awaits |
385
385
  | `_layout.tsx`: `export default function Layout({ children })` | `layout().render(({ children }) => …)` |
386
386
  | `export function NotFound` / `export function Error` | `.notFound(fn)` / `.error(fn)` on `layout()` |
387
- | root `_layout.tsx`: `fonts`, `theme`, `manifest`, `reconnect`, `wsConnect`, `layoutStyle`, `gaTrackingId` | `rootLayout().fonts([…]).theme("dark").manifest({…}).reconnect(false).wsConnect(false).layoutStyle("web").gaTrackingId("G-…")` |
387
+ | root `_layout.tsx`: `fonts`, `theme`, `manifest`, `reconnect`, `wsConnect`, `layoutStyle` | `rootLayout().fonts([…]).theme("dark").manifest({…}).reconnect(false).wsConnect(false).layoutStyle("web")` |
388
+ | root `_layout.tsx`: `gaTrackingId` | none — render the app's own `"use client"` analytics component from the root layout's `.render()` |
388
389
  | `prompt()` on `endpoint()` (removed) | `.prompt(name, description)` on the page that shows the same screen |
389
390
 
390
391
  Rules the loader and `akan sync` enforce: a chain module has no other export; `page()` belongs in a page file and
@@ -393,6 +394,68 @@ Rules the loader and `akan sync` enforce: a chain module has no other export; `p
393
394
  `_overrides.tsx` is untouched. MCP: `prompts/get` now runs the page's body under the caller's token and hands the
394
395
  model the description, what the page fetched, and the tools of those modules — see `mcpRule`.
395
396
 
397
+ ---
398
+
399
+ ### Recipe 8: Migrating the LLM Adaptor and the Removed `--ai` Commands
400
+
401
+ Two changes land together. The runtime LLM seam now names a wire rather than a vendor, and the CLI's AI editor is
402
+ gone. They touch different files and neither one is optional — the first breaks a build or, worse, degrades
403
+ silently; the second breaks a script that calls a command that no longer exists.
404
+
405
+ #### The adaptor, in `lib/option.ts` and `env/env.server.*.ts`
406
+
407
+ `DeepseekLlm` is deleted. `OpenaiLlm` is the default and speaks the chat-completions dialect to whatever `host`
408
+ names, so DeepSeek, Groq, Together, OpenRouter, Ollama and a self-hosted vLLM are the same class with a different
409
+ `host` and `model`. `AnthropicLlm` is unchanged.
410
+
411
+ ```ts
412
+ // before — env/env.server.local.ts
413
+ export const env: ModulesOptions = {
414
+ ...libEnv,
415
+ llm: { apiKey: process.env.DEEPSEEK_KEY },
416
+ };
417
+
418
+ // after — the default model and host are gone, so name both
419
+ export const env: ModulesOptions = {
420
+ ...libEnv,
421
+ llm: { apiKey: process.env.DEEPSEEK_KEY, model: "deepseek-v4-flash", host: "https://api.deepseek.com" },
422
+ };
423
+ ```
424
+
425
+ | Before | After | What happens if you skip it |
426
+ |---|---|---|
427
+ | `setLlm({ apiKey })`, relying on the DeepSeek default | `setLlm({ apiKey, model, host })` | The app boots, the log warns once, and the chat answers `llmUnavailable` to every turn |
428
+ | `applyAdaptor(LlmAdaptorRole, DeepseekLlm)` | `OpenaiLlm` + `host: "https://api.deepseek.com"` | Build error — the class is gone |
429
+ | `OpenaiLlm` pointed at a **named** `host`, reading images | add `accepts: { image: true }` to `setLlm` | **Silent**: images reach the model as a note saying they could not be read |
430
+ | An adaptor of your own throwing `agent.error.<vendor>RequestFailed` | `new Err("agent.error.llmRequestFailed", { provider, status, reason })` | The chat prints the raw key instead of a sentence |
431
+ | Provider settings in a second `option.use({ myLlmOption })` | put them in `setLlm` and read `use<MyLlmOption>()` | Nothing breaks; the second channel is no longer needed |
432
+
433
+ `OpenaiLlm` claims vision for its default host (`https://api.openai.com/v1`) and for nothing else, because a host
434
+ you named is a gateway it knows nothing about — and bytes handed to a model that cannot decode them kill the whole
435
+ turn on a 400, where text-only degrades them to a note. Declare `accepts` whenever you set `host`.
436
+
437
+ `LlmOption` is now a floor rather than the whole shape: `setLlm` carries whatever else it is handed to the role, so
438
+ an adaptor of your own declares `interface MyLlmOption extends LlmOption { region: string }`, reads it with
439
+ `use<MyLlmOption>()`, and stops needing a config channel beside `setLlm`.
440
+
441
+ #### The CLI, in scripts and CI
442
+
443
+ `AiSession` and every command that drove it are removed. There is no replacement command — the workspace's agent
444
+ surface (`akan context`, the MCP tools, `akan guideline show`) is what feeds a coding agent now.
445
+
446
+ | Removed | What to do instead |
447
+ |---|---|
448
+ | `akan create-module <name> --ai`, `akan create-scalar <name> --ai` | Drop the flag. The command scaffolds the same files it always did; write the constant and dictionary yourself or let a coding agent do it against `get_guideline modelConstant` / `modelDictionary` |
449
+ | `akan compact <app-or-lib>` | Edit the `*.abstract.md` by hand. `akan quality scan` still warns past 300 lines |
450
+ | `akan set-llm`, `akan reset-llm`, `akan ask` | Nothing. The runtime LLM is `option.setLlm(...)` and has never been this config |
451
+ | `akan generate-instruction`, `akan update-instruction`, `akan generate-document`, `akan reapply-instruction` | Nothing. `akan guideline list` / `show` still read the bundled guidelines |
452
+ | `import { AiSession } from "@akanjs/devkit/aiEditor"` | Nothing — the module is gone |
453
+
454
+ `~/.akan/config.json` keeps an `llm` entry if it had one. It is ignored; delete it, because it holds an API key.
455
+
456
+ Then: `bun run akan typecheck <app>`, `bun run akan lint <app>`, and one agent turn in the browser to confirm the
457
+ provider answers.
458
+
396
459
  ### Data Flow Summary
397
460
 
398
461
  For each business question, follow this chain: