@akanjs/cli 3.0.0-beta.8 → 3.0.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.
Files changed (224) hide show
  1. package/.build-stamp +1 -1
  2. package/CodeAgent-2vmp9qe0.js +9 -0
  3. package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
  4. package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
  5. package/CodeTui-5ed6bg2a.js +1457 -0
  6. package/README.ko.md +83 -28
  7. package/README.md +70 -27
  8. package/TunnelShare-cwh9ezsm.js +9 -0
  9. package/{agent.command-0q17pnrs.js → agent.command-gn73rhe7.js} +8 -9
  10. package/akanCode.js +102 -0
  11. package/application.command-e968t0hw.js +246 -0
  12. package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-64taxseg.js} +121 -77
  13. package/buildBatch.proc.js +120 -32
  14. package/cloud.command-0jm301rr.js +95 -0
  15. package/cloud.script-nemnzjpv.js +20 -0
  16. package/code.command-18kcm7hq.js +154 -0
  17. package/commandManifest.json +1 -1
  18. package/consoleToStderr-h5bryj94.js +7 -0
  19. package/context.command-trd9bn18.js +48 -0
  20. package/context.runner-y5hwh673.js +21 -0
  21. package/{dependencyScanner-shs82ynp.js → dependencyScanner-06z2dmrr.js} +2 -2
  22. package/{devTui-m1atgkc9.js → devTui-q7k7bakd.js} +122 -40
  23. package/fileSys-wzzrntkw.js +9 -0
  24. package/guideline.command-bdmdn2de.js +42 -0
  25. package/guidelines/agentRule/agentRule.instruction.md +154 -26
  26. package/guidelines/conventions/conventions.instruction.md +134 -48
  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 +291 -6
  30. package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
  31. package/guidelines/transportRule/transportRule.instruction.md +10 -8
  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-0wpybxtr.js +2729 -0
  36. package/{index-kcxmv48c.js → index-2ahj896a.js} +45 -79
  37. package/index-2wc41k5w.js +49 -0
  38. package/index-3hx1z96c.js +87 -0
  39. package/{index-2tv3fdx4.js → index-3w94e7n3.js} +1228 -1458
  40. package/{index-hwkygn9s.js → index-48k96p55.js} +325 -36
  41. package/index-4npz05nm.js +187 -0
  42. package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
  43. package/index-6he6gvfg.js +4300 -0
  44. package/index-7md2g9vw.js +6 -0
  45. package/{index-04bk599g.js → index-93j60em0.js} +10 -17
  46. package/index-94s080qm.js +266 -0
  47. package/index-9dp031by.js +12 -0
  48. package/{index-ktb5njgy.js → index-9z771ptz.js} +49 -99
  49. package/{index-y6n8nmny.js → index-abxpehf8.js} +75 -203
  50. package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
  51. package/{index-9077wzd2.js → index-by9pxkr0.js} +43 -26
  52. package/index-c6eggbee.js +123 -0
  53. package/index-d2242ksy.js +99 -0
  54. package/index-drehwpdt.js +1119 -0
  55. package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
  56. package/{index-ffvk10zz.js → index-f3c78nd6.js} +107 -241
  57. package/index-gbggbqcp.js +1058 -0
  58. package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
  59. package/index-hsmtysz4.js +53 -0
  60. package/{index-j9yhg404.js → index-jmrn54tt.js} +4 -7
  61. package/{index-a64esmtb.js → index-kcdj1z5v.js} +523 -1022
  62. package/{index-d43r08tf.js → index-kqabytj7.js} +77 -150
  63. package/{index-vyvre9en.js → index-mbxbhay5.js} +6 -2
  64. package/{index-vxtad5bd.js → index-mgxpx3f2.js} +5 -8
  65. package/index-p6pbz9n5.js +25 -0
  66. package/{index-sj5vxr8f.js → index-p993efkq.js} +981 -897
  67. package/{index-2xp0kfkt.js → index-pmhqttv3.js} +37 -25
  68. package/index-tqm7jjcn.js +700 -0
  69. package/{index-0g7r8xz3.js → index-vpj42xwt.js} +114 -232
  70. package/index-wa5jbxj1.js +760 -0
  71. package/{index-k2j1hc4r.js → index-wprnsjms.js} +12 -12
  72. package/{index-gyd220p2.js → index-y3mphr1m.js} +554 -577
  73. package/index-yg7jhz8x.js +69 -0
  74. package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
  75. package/index.js +23 -22
  76. package/{library.command-qzzbj6d8.js → library.command-9h749a1x.js} +4 -5
  77. package/{localRegistry.command-vyrgg4xw.js → localRegistry.command-2bqqhxyz.js} +23 -43
  78. package/{module.command-rq5hq3ax.js → module.command-nv8nzgft.js} +8 -12
  79. package/{package.command-d9hd6v9n.js → package.command-mq4bggp6.js} +6 -7
  80. package/package.json +15 -7
  81. package/{page.command-126d56te.js → page.command-j639n707.js} +4 -5
  82. package/{primitive.command-taxd1538.js → primitive.command-kz6849th.js} +9 -23
  83. package/prompter-76k56b37.js +8 -0
  84. package/{quality.command-6gsna0a3.js → quality.command-4hyabwkb.js} +205 -377
  85. package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
  86. package/{repair.command-tq2dgy72.js → repair.command-a9awpbqr.js} +11 -35
  87. package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
  88. package/{scalar.command-3bcmrrkv.js → scalar.command-ma4fpm9m.js} +7 -11
  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-eddzqpx3.js → subspace.command-3k2pmrs4.js} +53 -62
  101. package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
  102. package/templates/app/page/_index.tsx +257 -258
  103. package/templates/app/page/_layout.tsx +5 -1
  104. package/templates/app/page/_overrides.tsx +12 -0
  105. package/templates/app/page/styles.css.template +316 -14
  106. package/templates/app/public/apple-touch-icon.png +0 -0
  107. package/templates/app/public/favicon.ico +0 -0
  108. package/templates/app/public/jelly/cloud.webp +0 -0
  109. package/templates/app/public/jelly/comet.webp +0 -0
  110. package/templates/app/public/jelly/moon.webp +0 -0
  111. package/templates/app/public/jelly/planet.webp +0 -0
  112. package/templates/app/public/jelly/rocket.webp +0 -0
  113. package/templates/app/public/jelly/star.webp +0 -0
  114. package/templates/app/public/jelly/sugar.webp +0 -0
  115. package/templates/app/public/logo.png +0 -0
  116. package/templates/app/ui/Recipe/index.ts +9 -0
  117. package/templates/app/ui/Recipe/jellyButton.ts +63 -0
  118. package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
  119. package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
  120. package/templates/appSample/lib/task/Task.Unit.tsx +1 -1
  121. package/templates/appSample/lib/task/Task.Util.tsx +10 -13
  122. package/templates/appSample/lib/task/Task.View.tsx +2 -2
  123. package/templates/appSample/lib/task/Task.Zone.tsx +5 -2
  124. package/templates/appSample/lib/task/task.abstract.ts +12 -32
  125. package/templates/appSample/lib/task/task.signal.spec.ts +1 -15
  126. package/templates/appSample/lib/task/task.signal.test.ts +15 -1
  127. package/templates/appSample/lib/task/task.signal.ts +10 -8
  128. package/templates/appSample/page/task/[taskId]/_index.tsx +6 -6
  129. package/templates/appSample/page/task/[taskId]/edit.tsx +5 -4
  130. package/templates/appSample/page/task/_index.tsx +4 -3
  131. package/templates/appSample/page/task/_layout.tsx +1 -1
  132. package/templates/appSample/page/task/new.tsx +5 -4
  133. package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
  134. package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
  135. package/templates/appSample/ui/GlobalLoading.tsx +2 -1
  136. package/templates/appSample/ui/QuantityControl.tsx +3 -3
  137. package/templates/client.ts +7 -30
  138. package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
  139. package/templates/crudPages/[__model__Id]/page.tsx +2 -1
  140. package/templates/crudPages/new/page.tsx +2 -1
  141. package/templates/crudPages/page.tsx +2 -1
  142. package/templates/crudSinglePage/page.tsx +2 -1
  143. package/templates/lib/__lib/lib.dictionary.ts +0 -2
  144. package/templates/lib/__lib/lib.document.ts +0 -2
  145. package/templates/lib/sig.ts +8 -30
  146. package/templates/localDev/docker-compose.yaml.template +3 -14
  147. package/templates/module/__model__.signal.ts +12 -4
  148. package/templates/pkgRoot/tsconfig.json.template +2 -7
  149. package/templates/server.ts +1 -3
  150. package/templates/service/__model__.abstract.md.template +8 -0
  151. package/templates/workspaceRoot/.gitignore.template +0 -1
  152. package/templates/workspaceRoot/AGENTS.md.template +6 -0
  153. package/templates/workspaceRoot/package.json.template +1 -3
  154. package/tunnel.command-xwr46b66.js +129 -0
  155. package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
  156. package/typecheck.proc.js +1 -1
  157. package/workflow.command-t7wqe4h7.js +79 -0
  158. package/{workspace.command-06wvstgr.js → workspace.command-4derhzrh.js} +79 -109
  159. package/abstractCompactor-gmkc0z8j.js +0 -102
  160. package/application.command-18y0s485.js +0 -204
  161. package/applicationReleasePackager-4yasxwv9.js +0 -244
  162. package/capacitorApp-36rbr858.js +0 -12
  163. package/cloud.command-av97ndh6.js +0 -94
  164. package/cloud.script-cm11xjdz.js +0 -20
  165. package/context.command-5v37ef3n.js +0 -81
  166. package/devTuiView-0hc2emhe.js +0 -9
  167. package/getDirname-dr3jz1xg.js +0 -7
  168. package/guideline.command-4pz9g1rt.js +0 -360
  169. package/guidelines/agentRule/agentRule.generate.json +0 -31
  170. package/guidelines/componentRule/componentRule.generate.json +0 -31
  171. package/guidelines/conventions/conventions.generate.json +0 -25
  172. package/guidelines/cssRule/cssRule.generate.json +0 -31
  173. package/guidelines/docPageRule/docPageRule.generate.json +0 -31
  174. package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
  175. package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
  176. package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
  177. package/guidelines/enumConstant/enumConstant.generate.json +0 -31
  178. package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
  179. package/guidelines/fieldRule/fieldRule.generate.json +0 -31
  180. package/guidelines/framework/framework.generate.json +0 -32
  181. package/guidelines/mcpRule/mcpRule.generate.json +0 -35
  182. package/guidelines/modelConstant/modelConstant.generate.json +0 -32
  183. package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
  184. package/guidelines/modelDocument/modelDocument.generate.json +0 -31
  185. package/guidelines/modelService/modelService.generate.json +0 -31
  186. package/guidelines/modelSignal/modelSignal.generate.json +0 -31
  187. package/guidelines/modelStore/modelStore.generate.json +0 -31
  188. package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
  189. package/guidelines/modelUnit/modelUnit.generate.json +0 -31
  190. package/guidelines/modelUtil/modelUtil.generate.json +0 -31
  191. package/guidelines/modelView/modelView.generate.json +0 -31
  192. package/guidelines/modelZone/modelZone.generate.json +0 -31
  193. package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
  194. package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
  195. package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
  196. package/guidelines/queryRule/queryRule.generate.json +0 -31
  197. package/guidelines/recipeRule/recipeRule.generate.json +0 -31
  198. package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
  199. package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
  200. package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
  201. package/guidelines/scalarModule/scalarModule.generate.json +0 -31
  202. package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
  203. package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
  204. package/guidelines/ssrRule/ssrRule.generate.json +0 -31
  205. package/guidelines/transportRule/transportRule.generate.json +0 -31
  206. package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
  207. package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
  208. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
  209. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
  210. package/index-47pj3aee.js +0 -76
  211. package/index-5ystnsd5.js +0 -841
  212. package/index-6wwzs1bc.js +0 -7
  213. package/index-a17x49vg.js +0 -229
  214. package/index-m0xccmmm.js +0 -228
  215. package/index-meh558jy.js +0 -65
  216. package/index-rtxxan88.js +0 -471
  217. package/index-te54x9zm.js +0 -338
  218. package/index-w8kmx1b6.js +0 -2176
  219. package/index-wt7qrtd4.js +0 -1516
  220. package/templates/__scalar/__model__/__model__.abstract.md +0 -28
  221. package/templates/module/__model__.abstract.md +0 -28
  222. package/templates/service/__model__.abstract.md +0 -28
  223. package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +0 -62
  224. package/workflow.command-gtdnzvbz.js +0 -108
@@ -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,291 @@ 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
+ - **A platform section may name its own `indexPath`**, which wins on that platform: `indexPath: "/mobile"` with
232
+ `desktop: { indexPath: "/" }` opens the phones on `/mobile` and the desktop app on `/`, from one target, so no
233
+ command needs `--target`. It moves the dev build's first page and the `indexPath` its bundle carries alike.
234
+ - **`basePath` is the client a target opens.** An app with no basePaths leaves it out. Without `targets`, the one
235
+ target opens the basePath named like the app when there is one; in an app with basePaths, a target that names
236
+ none is a template, and `--target <basePath>` builds that client from it.
237
+ - Every native command takes `--target <name>`. `build-*`, `release-*`, `update-keygen` and `publish-update` also
238
+ take `all`; `start-*` and `pack-update` handle one target at a time.
239
+ - **Everything a build makes is under `apps/<app>/.akan/native/<target>/`**: `build/<platform>` (`build-ios`,
240
+ `build-android`, `build-desktop`), `dev/<platform>` (the dev builds `start-*` run), `updates` (what
241
+ `publish-update` signs and `pack-update` packs), `web` (the bundle the app loads) and `bin`. `akan start` leaves
242
+ the folder alone.
243
+ - **`start-ios` / `start-android` / `start-desktop` run a dev build that loads its pages from `akan start`.** It has
244
+ to be running — except for a desktop app that carries its server, which starts it when none of this checkout
245
+ answers. `--release true` runs a release build of the app's own bundle instead.
246
+ - **A desktop app builds only for the computer that builds it** — a `.app` on macOS, and an unsigned folder on
247
+ Windows and Linux. `build-desktop --installer true` adds what a person downloads: a per-user NSIS setup on Windows
248
+ (`/S` installs silently), a dmg on macOS, an AppImage on Linux (it needs `mksquashfs`; an AppImage cannot update
249
+ itself, so a release with `updates` ships a new one). `--arch arm64|x64` builds a Windows or Linux app for the
250
+ other CPU of that OS: the Rust library, Bun's executable, the server's `bun install --cpu` and each `bin` file
251
+ follow it, and a server addon with no binary for that CPU stops the build. A macOS app is Apple silicon only —
252
+ Intel Macs are not a target, so `--arch x64` on macOS is refused.
253
+ - **A macOS release for download is signed with a Developer ID and notarized, from the environment.**
254
+ `AKAN_NATIVE_MACOS_IDENTITY` names a keychain identity, or `AKAN_NATIVE_MACOS_CERTIFICATE` +
255
+ `_CERTIFICATE_PASSWORD` a `.p12` the build imports into a keychain of its own (a CI runner);
256
+ `AKAN_NATIVE_MACOS_NOTARY_KEY` + `_KEY_ID` + `_ISSUER` (an App Store Connect API key) or `_NOTARY_PROFILE` turn on
257
+ notarization and stapling of the app and the dmg. Every Mach-O file — the server's addons and `bin` included — is
258
+ signed inside out with the hardened runtime and a timestamp; the executable gets Bun's JIT entitlements and the
259
+ camera's or microphone's when a usage text asks for them, and `desktop.entitlements` adds the app's own.
260
+ `publish-update` signs with the same identity, since the updater checks the installed app's signature. Without a
261
+ Developer ID the build warns, because Gatekeeper blocks a downloaded copy.
262
+ - **A Windows release is signed with Authenticode, from the environment.** `AKAN_NATIVE_WINDOWS_CERTIFICATE` +
263
+ `_CERTIFICATE_PASSWORD` (a `.pfx`), `AKAN_NATIVE_WINDOWS_THUMBPRINT` (a certificate in the store, a token's or an
264
+ HSM's), or `AKAN_NATIVE_WINDOWS_SIGN_COMMAND` (a JSON array run per file with `{file}`, e.g. signtool with Azure
265
+ Trusted Signing's dlib) signs every PE file of the app — the server's `.node` and `bin` included — the setup program
266
+ and the uninstaller it writes, SHA-256 with a timestamp (`AKAN_NATIVE_WINDOWS_TIMESTAMP_URL`).
267
+ - **`updates: { url, publicKey }` lets an installed app update itself** — the whole app on a desktop, the web
268
+ bundle on a phone. `akan update-keygen <app>` makes the signing key once
269
+ (`~/.akan/native/keys/<app id>.update.key`, or the path `AKAN_NATIVE_UPDATE_KEY` names) and prints the
270
+ `publicKey`. `akan publish-update <app>` builds and signs a release into `updates/`: upload that folder to `url`,
271
+ the manifests last. `akan pack-update` writes an unsigned phone bundle for whoever holds the key to sign. The
272
+ channel is the backend env the binary was built for unless `channel` names one, and a release that does not
273
+ come up is rolled back to the one before.
274
+ - **An unattended screen** — signage, a kiosk — takes `desktop.recovery: "reload"` (a crashed page loads again,
275
+ and the app relaunches when the webview ends), `desktop.window: { fullscreen, skipTaskbar }`,
276
+ `desktop.screenCapture: "auto"` (Windows answers `getDisplayMedia()` with the first screen, no picker) and
277
+ `android.autoplay` (media plays with sound without a tap).
278
+
279
+ ## A Desktop App's Server — `bin` And `trustedDependencies`
280
+
281
+ A desktop app whose `native` section, or target, declares `desktop: { server: true }` carries the app's server:
282
+ `akan build-desktop`, `start-desktop --release` and `publish-update` put the backend `akan build` made into the
283
+ app — everything the backend build wrote (the `.js`, `akan.build.json`, `private/`, a bundled package's `.node`,
284
+ `.wasm` or file asset) but the Dockerfile, the RSC worker, the console, `csr/` and `public/` — and install its
285
+ packages first with `bun install --production --prefer-offline`, before `akan build` runs. `start-desktop` without
286
+ `--release` starts `akan start` beside the app when no dev server of this checkout answers. The setting is the
287
+ app's backend, so it does not change under an installed app: an update whose release carries a server when the app
288
+ has none, or none when it has one, is refused, and switching means a reinstall.
289
+ `desktop: { server: { omit: ["rclnodejs"] } }` carries the server without packages only the image needs — an addon
290
+ tied to the image's system, code only a process the app never starts loads — and whatever only they pull in; the
291
+ image keeps them, and the server's own bundle must not import them. A package another dependency still installs
292
+ stops the build, naming who needs it. So does a native addon that would not load on a user's computer: no binary
293
+ for the app's OS and CPU, a link outside the system, or a `binding.gyp` its install never compiled. It runs as an API-only edge server on SQLite, on a loopback port the launcher picks — the last session's when
294
+ it is free, which is not a guarantee, so a provider that needs an exact redirect URI signs in through a cloud
295
+ server's adapter. It refuses a `Host` other than its own, but any program on the computer can still call that port:
296
+ guard its endpoints as a network server's. It runs in one process, so a `main.ts` or an env asking for replicas (`replica`,
297
+ `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`
298
+ on Windows, never Roaming; `server-debug` for a `--debug` build). It trusts the OS certificate store, and of the
299
+ user's environment it takes a fixed set and nothing else: the system's own (`PATH`, `HOME`, the temp folder, `LANG`,
300
+ `TZ`, Windows' folders and `ComSpec`), the desktop session a `bin` tool needs for the screen or the audio server
301
+ (`DISPLAY`, `XAUTHORITY`, `WAYLAND_DISPLAY`, the session bus, `PULSE_*`, `XDG_*`), and the proxy and CA variables
302
+ (`HTTP(S)_PROXY`, `NO_PROXY`, `NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`).
303
+
304
+ **Everything it carries is on the user's computer, readable.** That is `private/` as it is — each lib's too, under
305
+ `private/libs/<lib>` — and the server env of the one environment the build is for (`--env`: `debug` for
306
+ `build-desktop`, `local` for `start-desktop --release`, `main` for `publish-update`, unless named): the build keeps
307
+ `env/env.server.<env>.ts` and swaps every other environment's file for exports that refuse to be read. The server env
308
+ each lib exports as its defaults (the lib's `env.server.testing.ts`, spread into every environment by
309
+ `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
310
+ server needs belongs to a cloud server its adapter calls. There is no `public/`: a file the server reads at runtime
311
+ goes in `private/` and is read from `AKAN_APP_DIR` (the folder `server.js` sits in), never from `process.cwd()`, which
312
+ is the app's data folder.
313
+
314
+ **Nothing from `docker` reaches it.** `preRuns`, `postRuns` and a whole Dockerfile install into a Linux image the
315
+ desktop app never runs in, and the build warns when an app has them and carries no `bin`. What the server needs
316
+ from them comes one of two ways:
317
+
318
+ ```ts
319
+ const config: AppConfig = {
320
+ trustedDependencies: ["rclnodejs"],
321
+ bin: {
322
+ ffmpeg: {
323
+ "darwin-arm64": {
324
+ url: "https://files.example.com/ffmpeg-7.1-lgpl-darwin-arm64.zip",
325
+ sha256: "…",
326
+ file: "ffmpeg-7.1/bin/ffmpeg",
327
+ },
328
+ "win32-x64": { url: "https://files.example.com/ffmpeg-7.1-lgpl-win64.zip", sha256: "…", file: "bin/ffmpeg.exe" },
329
+ "linux-x64": { path: "tools/linux-x64/ffmpeg" },
330
+ },
331
+ },
332
+ };
333
+ ```
334
+
335
+ - **An executable the desktop app spawns goes in `bin`** — its server's code or its native plugins, whether or not
336
+ it carries a server — keyed by the name the code spawns and then by
337
+ `${process.platform}-${process.arch}` (`darwin-arm64`, `darwin-x64`, `linux-arm64`, `linux-x64`, `win32-arm64`,
338
+ `win32-x64`). A source is `{ url, sha256, file? }` — downloaded when the app is built, refused unless it hashes
339
+ to `sha256` (so plain http is as safe as https), kept in `apps/<app>/.akan/cache/bin/<sha256>` — or
340
+ `{ path, file? }`, relative to the `akan.config.ts` that declares it. `file` is the executable inside an archive
341
+ (`.zip`, `.tar.gz`, `.tgz`, `.tar.xz`, `.tar.bz2`, `.tar`), unpacked with the OS's own `tar` (`unzip` for a zip
342
+ on Linux).
343
+ - Only the building computer's platform is fetched, and an entry without it fails the build instead of the user's
344
+ call. `build-desktop` and `start-desktop` copy the file into the app's `bin/` (keeping `.exe` on Windows), sign it
345
+ with the app on macOS, and put that folder first on the app's PATH. The server's `spawn("ffmpeg")` needs no change
346
+ and finds the carried file even in an app launched from the Finder, whose PATH is only
347
+ `/usr/bin:/bin:/usr/sbin:/sbin`. A native plugin finds the folder in `ctx.binDir`: Bun's own `spawn` and `which`
348
+ without `env` read the environment the app started with, so a plugin passes `env: process.env` to run one by name.
349
+ The server passes it too when it spawns a carried `bun build --compile` executable: the shell starts the server as
350
+ Bun with `BUN_BE_BUN=1`, which akanjs takes out of `process.env` at boot, and a child spawned without `env` still
351
+ inherits it and runs as Bun's own CLI instead of the tool.
352
+ - **One file per entry, so carry a static build.** A build that loads its own shared libraries — a `-shared`
353
+ archive, or Homebrew's ffmpeg with its 55 dylibs — runs on the computer that built it and nowhere else.
354
+ - **A lib's `bin` reaches only the apps that depend on it**, unlike its `docker` steps; the app's own entry of the
355
+ same name wins, and two libs that declare one name differently fail the build.
356
+ - **The image does not read `bin`.** Keep installing through `docker.preRuns` there.
357
+ - **A package that builds itself at install goes in `trustedDependencies`.** `bun install --production` runs no
358
+ dependency's install or postinstall script unless the package is trusted, so an addon with no prebuild arrives
359
+ unbuilt and fails at its first call. The list lands in the built `package.json`, so it applies to the image and to
360
+ the desktop server alike, and a lib's list reaches every app, like its `externalLibs`.
361
+
362
+ **Carry an LGPL ffmpeg.** A build configured with `--enable-nonfree` may not be redistributed at all — the macOS
363
+ binary npm's `ffmpeg-static` downloads is one — and a `--enable-gpl` build obliges you to offer its source. An LGPL
364
+ build has no `libx264`, so encode H.264 through the OS's encoder: `h264_videotoolbox` on macOS, `h264_mf` on
365
+ Windows, VAAPI or NVENC on Linux. Codec patents are a separate question to settle before shipping.
366
+
367
+ **A server bound to its machine stays a service.** A server that needs a whole environment — ROS, system services,
368
+ root to change the network or the clock — runs as a service on that machine (the image), and the desktop app's
369
+ target leaves `desktop.server` off, pinned to it with `AKAN_PUBLIC_SERVER_URL` at build time. A carried server runs
370
+ as the signed-in user and stops with the app, and so does what it started. On macOS and Linux the server leads its
371
+ own process group: quitting the app ends the group, and when the app was killed or crashed — even while the server
372
+ was still starting — the server sees its parent gone and ends the group itself, SIGTERM first and SIGKILL a second
373
+ later for whatever ignored it. On Windows the job object ends them all with the app. A process started `detached`
374
+ leaves the group, or the job, and keeps running.
375
+
376
+ **A file the user picks reaches the server as a grant, never as a copy or a path.** With `"file-picker"` in
377
+ `native.plugins`, `filePicker.pickFiles({ forServer: true })` (also `pickDirectory` and `saveFile`,
378
+ from `akanjs/client/native`) copies nothing, whatever the size: its FileRefs serve the originals for a preview, and
379
+ each result carries a `grant`. The page hands the grant to an endpoint, and the server exchanges it:
380
+
381
+ ```ts
382
+ const input = await NativeFile.resolve(grant, "read"); // "write" for a saveFile grant, "folder" for pickDirectory
383
+ const clip = await NativeFile.resolveIn(folderGrant, "day1/a.mp4"); // refused if it climbs out of the folder
384
+ ```
385
+
386
+ The carried server asks the shell that showed the dialog over its IPC channel, so it reaches what the user picked
387
+ and nothing else, and no page ever holds a path. Behind a dev build `akan start` checks the grant's signature
388
+ against a key in `~/.akan/native` instead, in `operationMode` local only; every other server refuses a grant.
389
+
390
+ **Devices belong to the shell, not the server.** Displays and their changes (`screen`), windows placed on them
391
+ (`window`), the system volume and mute (`volume`), global shortcuts (`global-shortcut`), keep-awake (`keep-awake`)
392
+ and launch at login (`autostart`) are native runtime plugins, each added to `native.plugins`. Every builtin plugin's
393
+ page API is
394
+ `akanjs/client/native/<id>` (`akanjs/client/native/window`, `…/screen`, `…/global-shortcut`), imported in a `webkit/`
395
+ hook; `volume` and `filePicker` also come from `akanjs/client/native` itself. A capability the shell
396
+ lacks is added there: an app's own plugin runs as Bun code in the plugin host and cannot add a native shell op.
397
+
398
+ ## Database Modes — `database` In `akan.config.ts`
399
+
400
+ | mode | database | cache · queue · pubsub | runs as |
401
+ |---|---|---|---|
402
+ | `single` | SQLite file | SQLite files (Solid) | one container |
403
+ | `multiple` | one SQLite file (WAL) on a host volume every container on that host opens | Redis | containers on one host |
404
+ | `cluster` | Postgres | Redis | pods on several servers |
405
+
406
+ - **`database: { modes: ["single", "cluster"] }` is what the build can run in**; the first is what `akan start`
407
+ runs. `akan build` carries the drivers of every declared mode (`multiple` → `bullmq`, `ioredis`; `cluster` adds
408
+ `postgres`).
409
+ - **A deployment names its mode with `AKAN_DATABASE_MODE`**, and only a declared one. With one declared mode it may
410
+ leave it out; with several it must name one — the fallback would be a SQLite file inside each pod. `akan start`,
411
+ `build`, `script` and `console` resolve it the same way from the shell. One image can serve an edge site in
412
+ `single` and a cloud cluster in `cluster`: declare both.
413
+ - **Where the data lives is the deployment's env, ahead of `env.server.ts`**: `SQLITE_DATABASE_PATH`,
414
+ `AKAN_SOLID_DB_PATH`, `POSTGRES_URL` (or `POSTGRES_HOST`/`PORT`/`DATABASE`/`USER`/`PASSWORD`),
415
+ `POSTGRES_INSIGHT_URL`, `LIBSQL_URL`. `REDIS_URI` is required outside local development. Pool size, SSL and
416
+ prepared statements ride the Postgres URL (`?max=20&ssl=require`, `prepare=false` behind PgBouncer).
417
+ - **Uploads**: a deployed `multiple`/`cluster` app uses object storage, or a volume every instance mounts with
418
+ `AKAN_STORAGE_SHARED=true`.
419
+ - **`runAdminSql` on Postgres reads as its own login role** holding base-column `SELECT` and nothing else:
420
+ `CREATE ROLE <name> LOGIN PASSWORD '…'` with no other role granted, `POSTGRES_INSIGHT_URL` logging in as it, and
421
+ `GRANT USAGE ON SCHEMA` from the DBA when the app does not own its schema. Every model table grants the role its
422
+ base columns at boot. Without it the console refuses on Postgres; `_doc` is never readable either way.
423
+ - **`akan dbup`** starts what the workspace's apps declare (`--mode multiple` is Redis, `cluster` adds Postgres).
424
+ **`akan db-export <app>` / `akan db-import <app>`** move every model table as stored rows through NDJSON files
425
+ (`--dir`, default `local/transfer`) between whatever the shell's `AKAN_DATABASE_MODE` points at; a rerun
426
+ replaces rows, search is rebuilt after, and sessions, queued jobs and uploaded files do not move.
427
+ - **`q.raw(sql)` is the dialect's own SQL** — `json_extract("_doc", '$.f')` on SQLite, `("_doc" #>> '{f}')` on
428
+ Postgres. An app that runs in both modes avoids it.
429
+
430
+ ## Operations — Snapshots, Restore And The `/_akan/ops` Channel
431
+
432
+ `GET /_akan/app/info` answers `{ appName, repoName, environment, operationMode }` on every deployment — the same
433
+ `AKAN_PUBLIC_*` values the client bundle already carries — so a control plane can check which app a host is
434
+ serving without a key. Everything else is behind the ops channel.
435
+
436
+ | surface | what it does |
437
+ |---|---|
438
+ | `bun main.js ops snapshot [--id] [--out] [--include-solid] [--json]` | `VACUUM INTO` a consistent copy, `integrity_check`, sha256, gzip, optional age encryption, `manifest.json` |
439
+ | `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 |
440
+ | `GET /_akan/ops/info` | the public info plus `akanVersion`, `buildId`, `serverMode`, `databaseMode`, `solo`, `startedAt`, `replicaIdx` |
441
+ | `POST /_akan/ops/snapshot` `{ id, includeSolid?, uploadUrls: { main, solid?, manifest } }` | 202; a child process snapshots, then PUTs each file and the manifest **last** |
442
+ | `GET /_akan/ops/snapshot/:id` | `running` → `uploading` → `done` \| `failed`; kept in memory, so a restart answers 404 |
443
+
444
+ - **The entry is the image's own `main.js`** — a production image carries no `akan` CLI. `AkanApp.start()` hands
445
+ `argv[2] === "ops"` to the command before it boots anything. It reads the container's env — the `AKAN_PUBLIC_*`
446
+ identity and the database paths — so run it with that env (`docker compose exec`), never a bare shell's.
447
+ - **Restore refuses while an app answers `/_akan/app/health` on `PORT`** and names it by its `/_akan/app/info`;
448
+ `--force` is for a port some other app holds.
449
+ - **The channel exists only when `AKAN_OPS_PUBLIC_KEY` is set** (Ed25519: PEM, JWK, base64 SPKI or raw 32 bytes).
450
+ The control plane signs a short EdDSA JWS — `aud`, `iat`, `exp` at most 300s after `iat`, a `jti` the process
451
+ refuses twice — and the host holds only the public half, so a machine someone carries off mints nothing.
452
+ `aud` is `<appName>/<environment>`, or `<appName>/<environment>/<AKAN_OPS_INSTANCE>` when that is set, and then
453
+ only that: a token minted for the app as a whole would replay across every host sharing the name. 30 calls a
454
+ minute per process; every refusal is the same 401. A malformed key leaves the app serving with the channel off.
455
+ - **The app holds no storage credential.** It PUTs to the presigned URLs it was handed (`application/octet-stream`,
456
+ the manifest `application/json`), plain http only to loopback. The manifest landing is the commit marker.
457
+ - **Snapshots cover the SQLite modes only.** `single` copies the main file and, with `includeSolid`, the Solid file;
458
+ `multiple` copies the main file; `cluster` is refused. Paths follow the adaptors' env precedence
459
+ (`SQLITE_DATABASE_PATH`, `AKAN_SOLID_DB_PATH`, `AKAN_SQLITE_DIR`); a solo process uses the path its adaptor
460
+ actually opened, so a gateway deployment that moved the file in `env.server.ts` names it in the env too.
461
+ - **`AKAN_BACKUP_RECIPIENT=age1…[,age1…]` encrypts before upload** — age v1 X25519, so `age -d -i key.txt` opens
462
+ it. Restore takes `--identity` / `AKAN_BACKUP_IDENTITY_FILE`, or a file an operator already decrypted with
463
+ `age -d -o <name without .age>`.
464
+ - **The host keeps the newest `AKAN_OPS_SNAPSHOT_KEEP` (default 2) snapshots** under `AKAN_OPS_SNAPSHOT_DIR`
465
+ (default `<db dir>/snapshots/<id>/`) — the copy an edge still has while its uplink or the bucket is down.
466
+ - **`buildId` is `AKAN_BUILD_ID` when the deployment sets it**, else the git sha `akan build` wrote into
467
+ `akan.build.json` beside `main.js` (`-dirty` when the tree was).
468
+ - **`operationMode` does not gate any of this** — the key does. `edge` means a production host outside the
469
+ cluster: the console refuses it like `cloud`, and `init` / `interval` / `cron` take `operationMode: ["edge"]` to
470
+ run only there. Everything else behaves as `cloud`; `local` is a developer machine.
471
+
187
472
  ## Shipped Assets — `assets` In `akan.config.ts`
188
473
 
189
474
  `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
@@ -58,23 +58,25 @@ mutation is mounted under. `conventions` carries the invariants — this is the
58
58
 
59
59
  ## Cached Answers
60
60
 
61
- - **`{ cache: <ms> }` in an endpoint's signal option is the whole cache surface.** The `Cache` middleware is
62
- registered by default and stands aside unless an endpoint declares one — it used to be opt-in and to ignore the
63
- declared value for a hard-coded 60 seconds, which is the same shape the `timeout` bug had.
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.
64
63
  - **Only a `query` that takes no internal argument may carry one.** The handler's inputs are its declared
65
64
  arguments plus its internal ones, so an endpoint with none of the latter answers the same thing to everyone who
66
65
  may read it — which is the only answer a shared entry can hold. An endpoint that takes `.with(Self)` answers
67
66
  per caller, and one entry would be one caller's answer handed to the next; that endpoint and every `mutation`
68
67
  are named once in the log and left uncached rather than silently ignored.
69
- - **The guards run on every hit.** A cache hit skips `next()`, and `next()` is what runs them, so the middleware
70
- calls `context.checkGuards()` itself before handing the entry over. Shared is not public.
71
- - **The entry is the handler's result, not the response.** The middleware wraps execution, so `resolveReturn`
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`
72
73
  still runs per call on the cached value — field masking, `hidden`/`secret` stripping and relation resolution are
73
74
  never cached, and a relation still costs its own load. The cache saves the handler, not the serialization.
74
75
  - **A cache backend that is down does not take the endpoint down.** A failed read is warned and the call runs
75
76
  uncached; a failed write is warned and dropped.
76
- - The default middleware chain is `Logging → Timeout → Cache → handler`: a hung cache backend is bounded by the
77
- same deadline as the handler it stands in for. **`Retry` was removed** — an automatic re-run of an endpoint
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
78
80
  whose failure it cannot classify replays whatever the first attempt already did.
79
81
 
80
82
  ## Binary Pubsub
@@ -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: