@akanjs/cli 3.0.0-beta.9 → 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.
- package/.build-stamp +1 -1
- package/CodeAgent-2vmp9qe0.js +9 -0
- package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
- package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
- package/CodeTui-5ed6bg2a.js +1457 -0
- package/README.ko.md +83 -28
- package/README.md +70 -27
- package/TunnelShare-cwh9ezsm.js +9 -0
- package/{agent.command-0q17pnrs.js → agent.command-gn73rhe7.js} +8 -9
- package/akanCode.js +102 -0
- package/application.command-e968t0hw.js +246 -0
- package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-64taxseg.js} +121 -77
- package/buildBatch.proc.js +120 -32
- package/cloud.command-0jm301rr.js +95 -0
- package/cloud.script-nemnzjpv.js +20 -0
- package/code.command-18kcm7hq.js +154 -0
- package/commandManifest.json +1 -1
- package/consoleToStderr-h5bryj94.js +7 -0
- package/context.command-trd9bn18.js +48 -0
- package/context.runner-y5hwh673.js +21 -0
- package/{dependencyScanner-shs82ynp.js → dependencyScanner-06z2dmrr.js} +2 -2
- package/{devTui-m1atgkc9.js → devTui-q7k7bakd.js} +122 -40
- package/fileSys-wzzrntkw.js +9 -0
- package/guideline.command-bdmdn2de.js +42 -0
- package/guidelines/agentRule/agentRule.instruction.md +154 -26
- package/guidelines/conventions/conventions.instruction.md +134 -48
- package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
- package/guidelines/queryRule/queryRule.instruction.md +21 -7
- package/guidelines/runtimeRule/runtimeRule.instruction.md +291 -6
- package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
- package/guidelines/transportRule/transportRule.instruction.md +10 -8
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +20 -0
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
- package/incrementalBuilder.proc.js +564 -109
- package/index-0wpybxtr.js +2729 -0
- package/{index-kcxmv48c.js → index-2ahj896a.js} +45 -79
- package/index-2wc41k5w.js +49 -0
- package/index-3hx1z96c.js +87 -0
- package/{index-2tv3fdx4.js → index-3w94e7n3.js} +1228 -1458
- package/{index-hwkygn9s.js → index-48k96p55.js} +325 -36
- package/index-4npz05nm.js +187 -0
- package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
- package/index-6he6gvfg.js +4300 -0
- package/index-7md2g9vw.js +6 -0
- package/{index-04bk599g.js → index-93j60em0.js} +10 -17
- package/index-94s080qm.js +266 -0
- package/index-9dp031by.js +12 -0
- package/{index-ktb5njgy.js → index-9z771ptz.js} +49 -99
- package/{index-y6n8nmny.js → index-abxpehf8.js} +75 -203
- package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
- package/{index-9077wzd2.js → index-by9pxkr0.js} +43 -26
- package/index-c6eggbee.js +123 -0
- package/index-d2242ksy.js +99 -0
- package/index-drehwpdt.js +1119 -0
- package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
- package/{index-ffvk10zz.js → index-f3c78nd6.js} +107 -241
- package/index-gbggbqcp.js +1058 -0
- package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
- package/index-hsmtysz4.js +53 -0
- package/{index-j9yhg404.js → index-jmrn54tt.js} +4 -7
- package/{index-a64esmtb.js → index-kcdj1z5v.js} +523 -1022
- package/{index-d43r08tf.js → index-kqabytj7.js} +77 -150
- package/{index-vyvre9en.js → index-mbxbhay5.js} +6 -2
- package/{index-vxtad5bd.js → index-mgxpx3f2.js} +5 -8
- package/index-p6pbz9n5.js +25 -0
- package/{index-sj5vxr8f.js → index-p993efkq.js} +981 -897
- package/{index-2xp0kfkt.js → index-pmhqttv3.js} +37 -25
- package/index-tqm7jjcn.js +700 -0
- package/{index-0g7r8xz3.js → index-vpj42xwt.js} +114 -232
- package/index-wa5jbxj1.js +760 -0
- package/{index-k2j1hc4r.js → index-wprnsjms.js} +12 -12
- package/{index-gyd220p2.js → index-y3mphr1m.js} +554 -577
- package/index-yg7jhz8x.js +69 -0
- package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
- package/index.js +23 -22
- package/{library.command-qzzbj6d8.js → library.command-9h749a1x.js} +4 -5
- package/{localRegistry.command-vyrgg4xw.js → localRegistry.command-2bqqhxyz.js} +23 -43
- package/{module.command-rq5hq3ax.js → module.command-nv8nzgft.js} +8 -12
- package/{package.command-d9hd6v9n.js → package.command-mq4bggp6.js} +6 -7
- package/package.json +15 -7
- package/{page.command-126d56te.js → page.command-j639n707.js} +4 -5
- package/{primitive.command-taxd1538.js → primitive.command-kz6849th.js} +9 -23
- package/prompter-76k56b37.js +8 -0
- package/{quality.command-6gsna0a3.js → quality.command-4hyabwkb.js} +205 -377
- package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
- package/{repair.command-tq2dgy72.js → repair.command-a9awpbqr.js} +11 -35
- package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
- package/{scalar.command-3bcmrrkv.js → scalar.command-ma4fpm9m.js} +7 -11
- package/skills/akan-code/SKILL.md +130 -0
- package/skills/akan-debug/SKILL.md +42 -0
- package/skills/akan-deps/SKILL.md +50 -0
- package/skills/akan-err/SKILL.md +39 -0
- package/skills/akan-module/SKILL.md +45 -0
- package/skills/akan-scaffold/SKILL.md +76 -0
- package/skills/akan-scalar/SKILL.md +52 -0
- package/skills/akan-service/SKILL.md +34 -0
- package/skills/akan-store/SKILL.md +67 -0
- package/skills/akan-ui/SKILL.md +63 -0
- package/skills/akan-validate/SKILL.md +44 -0
- package/{subspace.command-eddzqpx3.js → subspace.command-3k2pmrs4.js} +53 -62
- package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
- package/templates/app/page/_index.tsx +257 -258
- package/templates/app/page/_layout.tsx +5 -1
- package/templates/app/page/_overrides.tsx +12 -0
- package/templates/app/page/styles.css.template +316 -14
- package/templates/app/public/apple-touch-icon.png +0 -0
- package/templates/app/public/favicon.ico +0 -0
- package/templates/app/public/jelly/cloud.webp +0 -0
- package/templates/app/public/jelly/comet.webp +0 -0
- package/templates/app/public/jelly/moon.webp +0 -0
- package/templates/app/public/jelly/planet.webp +0 -0
- package/templates/app/public/jelly/rocket.webp +0 -0
- package/templates/app/public/jelly/star.webp +0 -0
- package/templates/app/public/jelly/sugar.webp +0 -0
- package/templates/app/public/logo.png +0 -0
- package/templates/app/ui/Recipe/index.ts +9 -0
- package/templates/app/ui/Recipe/jellyButton.ts +63 -0
- package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
- package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
- package/templates/appSample/lib/task/Task.Unit.tsx +1 -1
- package/templates/appSample/lib/task/Task.Util.tsx +10 -13
- package/templates/appSample/lib/task/Task.View.tsx +2 -2
- package/templates/appSample/lib/task/Task.Zone.tsx +5 -2
- package/templates/appSample/lib/task/task.abstract.ts +12 -32
- package/templates/appSample/lib/task/task.signal.spec.ts +1 -15
- package/templates/appSample/lib/task/task.signal.test.ts +15 -1
- package/templates/appSample/lib/task/task.signal.ts +10 -8
- package/templates/appSample/page/task/[taskId]/_index.tsx +6 -6
- package/templates/appSample/page/task/[taskId]/edit.tsx +5 -4
- package/templates/appSample/page/task/_index.tsx +4 -3
- package/templates/appSample/page/task/_layout.tsx +1 -1
- package/templates/appSample/page/task/new.tsx +5 -4
- package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
- package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
- package/templates/appSample/ui/GlobalLoading.tsx +2 -1
- package/templates/appSample/ui/QuantityControl.tsx +3 -3
- package/templates/client.ts +7 -30
- package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
- package/templates/crudPages/[__model__Id]/page.tsx +2 -1
- package/templates/crudPages/new/page.tsx +2 -1
- package/templates/crudPages/page.tsx +2 -1
- package/templates/crudSinglePage/page.tsx +2 -1
- package/templates/lib/__lib/lib.dictionary.ts +0 -2
- package/templates/lib/__lib/lib.document.ts +0 -2
- package/templates/lib/sig.ts +8 -30
- package/templates/localDev/docker-compose.yaml.template +3 -14
- package/templates/module/__model__.signal.ts +12 -4
- package/templates/pkgRoot/tsconfig.json.template +2 -7
- package/templates/server.ts +1 -3
- package/templates/service/__model__.abstract.md.template +8 -0
- package/templates/workspaceRoot/.gitignore.template +0 -1
- package/templates/workspaceRoot/AGENTS.md.template +6 -0
- package/templates/workspaceRoot/package.json.template +1 -3
- package/tunnel.command-xwr46b66.js +129 -0
- package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
- package/typecheck.proc.js +1 -1
- package/workflow.command-t7wqe4h7.js +79 -0
- package/{workspace.command-06wvstgr.js → workspace.command-4derhzrh.js} +79 -109
- package/abstractCompactor-gmkc0z8j.js +0 -102
- package/application.command-18y0s485.js +0 -204
- package/applicationReleasePackager-4yasxwv9.js +0 -244
- package/capacitorApp-36rbr858.js +0 -12
- package/cloud.command-av97ndh6.js +0 -94
- package/cloud.script-cm11xjdz.js +0 -20
- package/context.command-5v37ef3n.js +0 -81
- package/devTuiView-0hc2emhe.js +0 -9
- package/getDirname-dr3jz1xg.js +0 -7
- package/guideline.command-4pz9g1rt.js +0 -360
- package/guidelines/agentRule/agentRule.generate.json +0 -31
- package/guidelines/componentRule/componentRule.generate.json +0 -31
- package/guidelines/conventions/conventions.generate.json +0 -25
- package/guidelines/cssRule/cssRule.generate.json +0 -31
- package/guidelines/docPageRule/docPageRule.generate.json +0 -31
- package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
- package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
- package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
- package/guidelines/enumConstant/enumConstant.generate.json +0 -31
- package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
- package/guidelines/fieldRule/fieldRule.generate.json +0 -31
- package/guidelines/framework/framework.generate.json +0 -32
- package/guidelines/mcpRule/mcpRule.generate.json +0 -35
- package/guidelines/modelConstant/modelConstant.generate.json +0 -32
- package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
- package/guidelines/modelDocument/modelDocument.generate.json +0 -31
- package/guidelines/modelService/modelService.generate.json +0 -31
- package/guidelines/modelSignal/modelSignal.generate.json +0 -31
- package/guidelines/modelStore/modelStore.generate.json +0 -31
- package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
- package/guidelines/modelUnit/modelUnit.generate.json +0 -31
- package/guidelines/modelUtil/modelUtil.generate.json +0 -31
- package/guidelines/modelView/modelView.generate.json +0 -31
- package/guidelines/modelZone/modelZone.generate.json +0 -31
- package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
- package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
- package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
- package/guidelines/queryRule/queryRule.generate.json +0 -31
- package/guidelines/recipeRule/recipeRule.generate.json +0 -31
- package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
- package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
- package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
- package/guidelines/scalarModule/scalarModule.generate.json +0 -31
- package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
- package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
- package/guidelines/ssrRule/ssrRule.generate.json +0 -31
- package/guidelines/transportRule/transportRule.generate.json +0 -31
- package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
- package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
- package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
- package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
- package/index-47pj3aee.js +0 -76
- package/index-5ystnsd5.js +0 -841
- package/index-6wwzs1bc.js +0 -7
- package/index-a17x49vg.js +0 -229
- package/index-m0xccmmm.js +0 -228
- package/index-meh558jy.js +0 -65
- package/index-rtxxan88.js +0 -471
- package/index-te54x9zm.js +0 -338
- package/index-w8kmx1b6.js +0 -2176
- package/index-wt7qrtd4.js +0 -1516
- package/templates/__scalar/__model__/__model__.abstract.md +0 -28
- package/templates/module/__model__.abstract.md +0 -28
- package/templates/service/__model__.abstract.md +0 -28
- package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +0 -62
- 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
|
|
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
|
|
20
|
-
|
|
21
|
-
native
|
|
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
|
|
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`.**
|
|
133
|
-
|
|
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
|
|
62
|
-
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
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 →
|
|
77
|
-
|
|
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()
|
|
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` | `.
|
|
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
|
|
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:
|