@akanjs/cli 3.0.0-beta.2 → 3.0.0-beta.20
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-awv258wb.js +9 -0
- package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
- package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
- package/CodeTui-1nqh35ez.js +1457 -0
- package/README.md +3 -0
- package/TunnelShare-scf4e4pd.js +9 -0
- package/{agent.command-d2gfxfck.js → agent.command-1s9ag7sb.js} +8 -9
- package/akanCode.js +102 -0
- package/{application.command-pj2m7584.js → application.command-49hwy6va.js} +51 -78
- package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-ybyp97pv.js} +63 -64
- package/{applicationReleasePackager-4yasxwv9.js → applicationReleasePackager-esnrphw8.js} +40 -60
- package/buildBatch.proc.js +19 -24
- package/{capacitorApp-36rbr858.js → capacitorApp-1synz4en.js} +3 -4
- package/cloud.command-d7rqtzt9.js +94 -0
- package/cloud.script-bkbpcr96.js +19 -0
- package/code.command-nn48xzej.js +154 -0
- package/commandManifest.json +1 -1
- package/consoleToStderr-h5bryj94.js +7 -0
- package/context.command-s3mcmvpa.js +48 -0
- package/context.runner-k99nb5pk.js +21 -0
- package/{dependencyScanner-shs82ynp.js → dependencyScanner-ye2e6pdz.js} +2 -2
- package/{devTui-40ggmd99.js → devTui-q7k7bakd.js} +122 -40
- package/fileSys-5ngz93ga.js +9 -0
- package/guideline.command-sct4qnt5.js +42 -0
- package/guidelines/agentRule/agentRule.instruction.md +206 -25
- package/guidelines/conventions/conventions.instruction.md +79 -24
- package/guidelines/fieldRule/fieldRule.instruction.md +1 -0
- package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
- package/guidelines/queryRule/queryRule.instruction.md +17 -7
- package/guidelines/runtimeRule/runtimeRule.instruction.md +74 -0
- package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
- package/guidelines/transportRule/transportRule.instruction.md +51 -0
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
- package/incrementalBuilder.proc.js +31 -48
- package/{index-h7yw2z9n.js → index-0bq14vr1.js} +8 -14
- package/index-13ffe9wp.js +69 -0
- package/index-22d57v4c.js +266 -0
- package/{index-fz5q3gd7.js → index-31yr441d.js} +10 -17
- package/index-3hx1z96c.js +87 -0
- package/index-3sq321ak.js +53 -0
- package/{index-kcxmv48c.js → index-3t7g9e4r.js} +45 -79
- package/{index-q0y2jq9j.js → index-5b6tavv3.js} +6 -3
- package/{index-47pj3aee.js → index-5e0n4hqx.js} +11 -12
- package/{index-259dsnz3.js → index-5pc1qrbv.js} +5 -8
- package/index-7md2g9vw.js +6 -0
- package/index-8mna0947.js +187 -0
- package/{index-zry5nz81.js → index-8sf7wmdr.js} +51 -30
- package/{index-2tv3fdx4.js → index-9d9bgcx4.js} +342 -765
- package/index-9dp031by.js +12 -0
- package/{index-ne2qy0nk.js → index-ahemdr5n.js} +12 -12
- package/{index-ws5e0j51.js → index-as7xmyz8.js} +40 -24
- package/index-axzjaqve.js +1118 -0
- package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
- package/index-c6eggbee.js +123 -0
- package/index-d2242ksy.js +99 -0
- package/{index-a64esmtb.js → index-ddxvdd1p.js} +169 -305
- package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
- package/{index-pnqbkf5p.js → index-dzf3r49a.js} +6 -2
- package/{index-w8kmx1b6.js → index-fgq75haa.js} +198 -290
- package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
- package/{index-hwkygn9s.js → index-khpfpcsd.js} +302 -33
- package/{index-wt7qrtd4.js → index-mxav6j3c.js} +93 -232
- package/{index-gyd220p2.js → index-myhc3c1j.js} +554 -577
- package/{index-600zhnve.js → index-nv5976x3.js} +56 -197
- package/index-p6pbz9n5.js +25 -0
- package/{index-4edzf1gr.js → index-przybvy5.js} +77 -150
- package/index-smme4r47.js +49 -0
- package/{index-mak3cd28.js → index-w2zdcw3k.js} +137 -255
- package/index-wa5jbxj1.js +760 -0
- package/{index-v0qh106m.js → index-ww4z397j.js} +527 -723
- package/{index-j9yhg404.js → index-xqjwd384.js} +4 -7
- package/{index-7737gxcp.js → index-xqpzbab5.js} +107 -241
- package/index-xsjh6nzh.js +2729 -0
- package/{index-ktb5njgy.js → index-ygn77a5w.js} +47 -99
- package/index-ztp3e73t.js +700 -0
- package/index.js +23 -22
- package/{library.command-nbsky672.js → library.command-1gx04jgn.js} +4 -5
- package/{localRegistry.command-yeq57wjr.js → localRegistry.command-zndxj1q6.js} +22 -42
- package/{module.command-29vqzrh8.js → module.command-3waddk95.js} +8 -11
- package/{package.command-axv0abmn.js → package.command-zrc7br1y.js} +6 -7
- package/package.json +14 -5
- package/{page.command-vs9s3zrh.js → page.command-vpjgardh.js} +4 -5
- package/{primitive.command-cbxmqbtc.js → primitive.command-sz84tk4t.js} +9 -22
- package/prompter-ptaa3nk0.js +8 -0
- package/{quality.command-gmbs7nws.js → quality.command-g869hafs.js} +203 -376
- package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
- package/{repair.command-bqsjptsj.js → repair.command-chsnjvvv.js} +11 -35
- package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
- package/{scalar.command-6k1vhe9s.js → scalar.command-2v21newx.js} +7 -10
- 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-fwjfcts7.js → subspace.command-ckcbwfp1.js} +138 -67
- package/templates/__scalar/__model__/__model__.abstract.md.template +8 -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.abstract.ts +12 -32
- package/templates/appSample/lib/task/task.signal.spec.ts +15 -18
- package/templates/appSample/lib/task/task.signal.test.ts +15 -1
- package/templates/appSample/lib/task/task.signal.ts +1 -1
- package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
- package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
- 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 +0 -2
- package/templates/service/__model__.abstract.md.template +8 -0
- package/tunnel.command-kd2yr4fx.js +129 -0
- package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
- package/typecheck.proc.js +1 -1
- package/workflow.command-44252cqv.js +79 -0
- package/{workspace.command-8s050m0h.js → workspace.command-fqcztt5p.js} +77 -109
- package/abstractCompactor-mjh5ppsq.js +0 -101
- package/cloud.command-jshwdq22.js +0 -93
- package/context.command-ez8a0zyz.js +0 -80
- package/devTuiView-c08ccbat.js +0 -9
- package/getDirname-dr3jz1xg.js +0 -7
- package/guideline.command-xmqwx8zm.js +0 -359
- 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-2hq52jq3.js +0 -465
- package/index-6wwzs1bc.js +0 -7
- package/index-8pm30m73.js +0 -841
- package/index-kn1h7p1x.js +0 -560
- package/index-meh558jy.js +0 -65
- package/index-rwg2aa4f.js +0 -229
- 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/workflow.command-nwpcdc8p.js +0 -107
|
@@ -49,7 +49,7 @@ with it. `conventions` carries the invariants — this is the full contract behi
|
|
|
49
49
|
`findBy<Filter>(...args, { select: { secretField: true } })` takes it inside the option object. The shapes do not
|
|
50
50
|
swap: `{ select: … }` handed to the facade projects a field named `select`, which no model has, and the read comes
|
|
51
51
|
back empty with no error. This is the only way to read a `field.secret(...)` value, which is otherwise stripped.
|
|
52
|
-
- **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`)
|
|
52
|
+
- **Hydrated vs raw:** server queries return hydrated `cnst.<Model>` instances (with `set`/`save`/`refresh`), and so do client fetch results — `FetchClient` parses every return with `crystalize` defaulting to **true** (`pkgs/akanjs/fetch/client/fetchClient.ts`). The raw `GetStateObject` shape (functions stripped, `pkgs/akanjs/base/types.ts`) is what the `<ref>Obj` / `<ref>ObjList` / `<ref>ObjInsight` handles carry, because those pass `crystalize: false` on purpose so the value can cross the RSC boundary as a client prop.
|
|
53
53
|
- Every filter generates fourteen methods: `list` · `listIds` · `find` · `findId` · `pick` · `pickId` · `exists` ·
|
|
54
54
|
`count` · `insight` · `query` · **`remove`** · **`removeOne`** · **`update`** · **`updateOne`**. The last four are
|
|
55
55
|
query-level writes — one atomic UPDATE, **no hooks**, and therefore no `_postRemove` and no cascade. Use them on a
|
|
@@ -90,14 +90,15 @@ with it. `conventions` carries the invariants — this is the full contract behi
|
|
|
90
90
|
- **A slice endpoint never reaches "unspecified".** The resolver fills `latest` before the query is built, so a
|
|
91
91
|
client asking for the score order has to name `relevance`; leaving `sort` off gets `latest`, not relevance.
|
|
92
92
|
- Scope a search with `columns` (`q.search(text, { columns: ["title"] })`) and re-weight with `weights`, a tuple of
|
|
93
|
-
finite numbers positional over `["title", "desc", "tag", "filter"]
|
|
93
|
+
finite, non-negative numbers positional over `["title", "desc", "tag", "filter"]` — Postgres ranks by weight
|
|
94
|
+
class and has no reading for a negative one.
|
|
94
95
|
|
|
95
96
|
|
|
96
97
|
## Text Search Fields — the `text` role
|
|
97
98
|
|
|
98
99
|
- A field joins the full-text index by declaring one of five roles: `field(String, { text: "title" })`, and likewise
|
|
99
100
|
`"desc"`, `"tag"`, `"thumb"`, `"filter"`. Nothing else opts a field in, and there is no per-model switch.
|
|
100
|
-
- Pick the role by what the value *is*, because
|
|
101
|
+
- Pick the role by what the value *is*, because the rank weights them positionally (`title` 10, `tag` 3, `desc` 1,
|
|
101
102
|
`filter` 0): `title` is the one line a human scans for, `desc` is prose, `tag` is a keyword list, `filter` is a
|
|
102
103
|
scoping value (status, owner, role) that must be matchable but must never outrank a real title hit.
|
|
103
104
|
- `thumb` is mirrored for rendering a hit and is **not** indexed — never expect it to match.
|
|
@@ -114,15 +115,24 @@ with it. `conventions` carries the invariants — this is the full contract behi
|
|
|
114
115
|
- Declaring roles is all the wiring there is. Mirror rows are maintained by SQL triggers — not document hooks —
|
|
115
116
|
because `updateOneByQuery` and friends fire no hooks, and most searchable-field mutations go through exactly that
|
|
116
117
|
path.
|
|
117
|
-
- Search runs on
|
|
118
|
+
- Search runs in every database mode: fts5 on SQLite/libSQL, a weighted `tsvector` under a GIN index on Postgres.
|
|
119
|
+
For the same text the same documents match — punctuation splits words the way unicode61 does, and a phrase never
|
|
120
|
+
runs from one column into the next — but the order may differ, since Postgres ranks without document frequency.
|
|
121
|
+
Assert on the match set, never on a Postgres order the weights alone do not decide.
|
|
122
|
+
- Postgres reads `AKAN_SEARCH_TOKENIZER` as `unicode61 [remove_diacritics 0|1|2]` (`simple`, with `unaccent` unless
|
|
123
|
+
`0`) or `trigram [case_sensitive 0|1]` (`pg_trgm`); any other spelling is refused. The extension is created when
|
|
124
|
+
the role may, otherwise a role that may runs `CREATE EXTENSION`. It indexes the first 20,000 characters of
|
|
125
|
+
`title`, `tag` and `filter` and 200,000 of `desc` — a tsvector refuses more than 1MB, and the refusal would fail
|
|
126
|
+
the model write. A database whose `LC_CTYPE` is `C` keeps non-ASCII case apart; use a UTF-8 ctype.
|
|
118
127
|
- `AKAN_SEARCH_ENABLED=0` switches the index off process-wide; unset means on. It never deletes mirror data, and
|
|
119
128
|
re-enabling reconciles every ref. **Give every process the same value** — a process cannot drop triggers for models
|
|
120
129
|
it does not mount, so a mixed fleet leaves stale triggers behind.
|
|
121
130
|
- The tokenizer is `AKAN_SEARCH_TOKENIZER` (or `database.search.tokenizer`, which wins), defaulting to
|
|
122
131
|
`unicode61 remove_diacritics 2`. Changing it rebuilds the index from the mirror on the next boot — the model
|
|
123
|
-
tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The schema
|
|
133
|
+
is ensured in one write turn — a SQLite write transaction, a Postgres schema lock — so of a fleet restarted at
|
|
134
|
+
once the first rebuilds and the rest find it current. A SQLite process waits for that turn up to its
|
|
135
|
+
`busy_timeout`, so stagger the restart when the mirror is large.
|
|
126
136
|
|
|
127
137
|
## Cascade Remove — the `cascade` option
|
|
128
138
|
|
|
@@ -184,6 +184,80 @@ 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
|
+
## Database Modes — `database` In `akan.config.ts`
|
|
188
|
+
|
|
189
|
+
| mode | database | cache · queue · pubsub | runs as |
|
|
190
|
+
|---|---|---|---|
|
|
191
|
+
| `single` | SQLite file | SQLite files (Solid) | one container |
|
|
192
|
+
| `multiple` | one SQLite file (WAL) on a host volume every container on that host opens | Redis | containers on one host |
|
|
193
|
+
| `cluster` | Postgres | Redis | pods on several servers |
|
|
194
|
+
|
|
195
|
+
- **`database: { modes: ["single", "cluster"] }` is what the build can run in**; the first is what `akan start`
|
|
196
|
+
runs. `akan build` carries the drivers of every declared mode (`multiple` → `bullmq`, `ioredis`; `cluster` adds
|
|
197
|
+
`postgres`).
|
|
198
|
+
- **A deployment names its mode with `AKAN_DATABASE_MODE`**, and only a declared one. With one declared mode it may
|
|
199
|
+
leave it out; with several it must name one — the fallback would be a SQLite file inside each pod. `akan start`,
|
|
200
|
+
`build`, `script` and `console` resolve it the same way from the shell. One image can serve an edge site in
|
|
201
|
+
`single` and a cloud cluster in `cluster`: declare both.
|
|
202
|
+
- **Where the data lives is the deployment's env, ahead of `env.server.ts`**: `SQLITE_DATABASE_PATH`,
|
|
203
|
+
`AKAN_SOLID_DB_PATH`, `POSTGRES_URL` (or `POSTGRES_HOST`/`PORT`/`DATABASE`/`USER`/`PASSWORD`),
|
|
204
|
+
`POSTGRES_INSIGHT_URL`, `LIBSQL_URL`. `REDIS_URI` is required outside local development. Pool size, SSL and
|
|
205
|
+
prepared statements ride the Postgres URL (`?max=20&ssl=require`, `prepare=false` behind PgBouncer).
|
|
206
|
+
- **Uploads**: a deployed `multiple`/`cluster` app uses object storage, or a volume every instance mounts with
|
|
207
|
+
`AKAN_STORAGE_SHARED=true`.
|
|
208
|
+
- **`runAdminSql` on Postgres reads as its own login role** holding base-column `SELECT` and nothing else:
|
|
209
|
+
`CREATE ROLE <name> LOGIN PASSWORD '…'` with no other role granted, `POSTGRES_INSIGHT_URL` logging in as it, and
|
|
210
|
+
`GRANT USAGE ON SCHEMA` from the DBA when the app does not own its schema. Every model table grants the role its
|
|
211
|
+
base columns at boot. Without it the console refuses on Postgres; `_doc` is never readable either way.
|
|
212
|
+
- **`akan dbup`** starts what the workspace's apps declare (`--mode multiple` is Redis, `cluster` adds Postgres).
|
|
213
|
+
**`akan db-export <app>` / `akan db-import <app>`** move every model table as stored rows through NDJSON files
|
|
214
|
+
(`--dir`, default `local/transfer`) between whatever the shell's `AKAN_DATABASE_MODE` points at; a rerun
|
|
215
|
+
replaces rows, search is rebuilt after, and sessions, queued jobs and uploaded files do not move.
|
|
216
|
+
- **`q.raw(sql)` is the dialect's own SQL** — `json_extract("_doc", '$.f')` on SQLite, `("_doc" #>> '{f}')` on
|
|
217
|
+
Postgres. An app that runs in both modes avoids it.
|
|
218
|
+
|
|
219
|
+
## Operations — Snapshots, Restore And The `/_akan/ops` Channel
|
|
220
|
+
|
|
221
|
+
`GET /_akan/app/info` answers `{ appName, repoName, environment, operationMode }` on every deployment — the same
|
|
222
|
+
`AKAN_PUBLIC_*` values the client bundle already carries — so a control plane can check which app a host is
|
|
223
|
+
serving without a key. Everything else is behind the ops channel.
|
|
224
|
+
|
|
225
|
+
| surface | what it does |
|
|
226
|
+
|---|---|
|
|
227
|
+
| `bun main.js ops snapshot [--id] [--out] [--include-solid] [--json]` | `VACUUM INTO` a consistent copy, `integrity_check`, sha256, gzip, optional age encryption, `manifest.json` |
|
|
228
|
+
| `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 |
|
|
229
|
+
| `GET /_akan/ops/info` | the public info plus `akanVersion`, `buildId`, `serverMode`, `databaseMode`, `solo`, `startedAt`, `replicaIdx` |
|
|
230
|
+
| `POST /_akan/ops/snapshot` `{ id, includeSolid?, uploadUrls: { main, solid?, manifest } }` | 202; a child process snapshots, then PUTs each file and the manifest **last** |
|
|
231
|
+
| `GET /_akan/ops/snapshot/:id` | `running` → `uploading` → `done` \| `failed`; kept in memory, so a restart answers 404 |
|
|
232
|
+
|
|
233
|
+
- **The entry is the image's own `main.js`** — a production image carries no `akan` CLI. `AkanApp.start()` hands
|
|
234
|
+
`argv[2] === "ops"` to the command before it boots anything. It reads the container's env — the `AKAN_PUBLIC_*`
|
|
235
|
+
identity and the database paths — so run it with that env (`docker compose exec`), never a bare shell's.
|
|
236
|
+
- **Restore refuses while an app answers `/_akan/app/health` on `PORT`** and names it by its `/_akan/app/info`;
|
|
237
|
+
`--force` is for a port some other app holds.
|
|
238
|
+
- **The channel exists only when `AKAN_OPS_PUBLIC_KEY` is set** (Ed25519: PEM, JWK, base64 SPKI or raw 32 bytes).
|
|
239
|
+
The control plane signs a short EdDSA JWS — `aud`, `iat`, `exp` at most 300s after `iat`, a `jti` the process
|
|
240
|
+
refuses twice — and the host holds only the public half, so a machine someone carries off mints nothing.
|
|
241
|
+
`aud` is `<appName>/<environment>`, or `<appName>/<environment>/<AKAN_OPS_INSTANCE>` when that is set, and then
|
|
242
|
+
only that: a token minted for the app as a whole would replay across every host sharing the name. 30 calls a
|
|
243
|
+
minute per process; every refusal is the same 401. A malformed key leaves the app serving with the channel off.
|
|
244
|
+
- **The app holds no storage credential.** It PUTs to the presigned URLs it was handed (`application/octet-stream`,
|
|
245
|
+
the manifest `application/json`), plain http only to loopback. The manifest landing is the commit marker.
|
|
246
|
+
- **Snapshots cover the SQLite modes only.** `single` copies the main file and, with `includeSolid`, the Solid file;
|
|
247
|
+
`multiple` copies the main file; `cluster` is refused. Paths follow the adaptors' env precedence
|
|
248
|
+
(`SQLITE_DATABASE_PATH`, `AKAN_SOLID_DB_PATH`, `AKAN_SQLITE_DIR`); a solo process uses the path its adaptor
|
|
249
|
+
actually opened, so a gateway deployment that moved the file in `env.server.ts` names it in the env too.
|
|
250
|
+
- **`AKAN_BACKUP_RECIPIENT=age1…[,age1…]` encrypts before upload** — age v1 X25519, so `age -d -i key.txt` opens
|
|
251
|
+
it. Restore takes `--identity` / `AKAN_BACKUP_IDENTITY_FILE`, or a file an operator already decrypted with
|
|
252
|
+
`age -d -o <name without .age>`.
|
|
253
|
+
- **The host keeps the newest `AKAN_OPS_SNAPSHOT_KEEP` (default 2) snapshots** under `AKAN_OPS_SNAPSHOT_DIR`
|
|
254
|
+
(default `<db dir>/snapshots/<id>/`) — the copy an edge still has while its uplink or the bucket is down.
|
|
255
|
+
- **`buildId` is `AKAN_BUILD_ID` when the deployment sets it**, else the git sha `akan build` wrote into
|
|
256
|
+
`akan.build.json` beside `main.js` (`-dirty` when the tree was).
|
|
257
|
+
- **`operationMode` does not gate any of this** — the key does. `edge` means a production host outside the
|
|
258
|
+
cluster: the console refuses it like `cloud`, and `init` / `interval` / `cron` take `operationMode: ["edge"]` to
|
|
259
|
+
run only there. Everything else behaves as `cloud`; `local` is a developer machine.
|
|
260
|
+
|
|
187
261
|
## Shipped Assets — `assets` In `akan.config.ts`
|
|
188
262
|
|
|
189
263
|
`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
|
|
@@ -28,6 +28,57 @@ mutation is mounted under. `conventions` carries the invariants — this is the
|
|
|
28
28
|
neither again, and cleanup that must happen either way registers for both. From a `message` handler it belongs to
|
|
29
29
|
the socket and runs at close. A handler that throws is logged and never blocks the rest of the teardown.
|
|
30
30
|
|
|
31
|
+
## Request Deadlines
|
|
32
|
+
|
|
33
|
+
- **`{ timeout: <ms> }` in an endpoint's signal option is the whole configuration surface**, and it reaches both
|
|
34
|
+
ends of the call. Server side, the `Timeout` middleware races the handler against it and answers
|
|
35
|
+
`base.error.gatewayTimeout` (504) once it is spent. Client side, `FetchSerializer` ships the value on the
|
|
36
|
+
serialized endpoint and `fetch.<endpoint>()` uses it as that request's `AbortSignal.timeout` budget, so the
|
|
37
|
+
browser does not abandon a call the server is still allowed to be working on.
|
|
38
|
+
- **The middleware is registered by default and has no default of its own.** An endpoint that declares no
|
|
39
|
+
`timeout` is passed straight through — a default here would put a deadline on every endpoint in the app that
|
|
40
|
+
nobody asked for, which is why the middleware used to be opt-in and, being opt-in, made the declared value
|
|
41
|
+
dead. It clears its timer in a `finally`: the race's loser is a pending timer per call otherwise, and with a
|
|
42
|
+
five-minute budget that is a five-minute hold on the event loop for a call that answered in a millisecond.
|
|
43
|
+
- **Without a declaration the client's own default applies — 30 seconds** (`DEFAULT_TIMEOUT_MS`,
|
|
44
|
+
`fetch/client/httpClient.ts`), and the server imposes nothing. Nothing else bounds a request: `Bun.serve` runs
|
|
45
|
+
with `idleTimeout: 0`, so a long call is only ever ended by one of these two.
|
|
46
|
+
- **Precedence is caller, then endpoint, then client default.** `fetch.x(…, { timeout: 5_000 })` overrides what
|
|
47
|
+
the endpoint declared; `{ timeout: false }` waits as long as the runtime will. An app moves its own default
|
|
48
|
+
with `fetch.instance.setTimeout(ms)` — `lib/useClient.ts` is generated, so the `timeout` option on
|
|
49
|
+
`FetchClient.build` belongs to the template that writes it, not to app code.
|
|
50
|
+
- **A file upload is never bounded**, on any of those paths: a large body on a slow uplink is a request that is
|
|
51
|
+
working, and the request body is the only thing the client can tell that from.
|
|
52
|
+
- **The deadline answers the caller; it does not undo the call.** `Promise.race` cannot cancel `next()`, so a
|
|
53
|
+
handler that timed out keeps running and keeps writing. Reach for a `timeout` to bound what a *caller* waits
|
|
54
|
+
for, not to bound an effect — an operation that must not half-happen needs its own idempotency or compensation.
|
|
55
|
+
- The failure is the same `base.error.gatewayTimeout` key whichever side gave up, so the user reads one sentence
|
|
56
|
+
in their own language either way. What differs is the status code — 504 from the server's middleware, 408 from
|
|
57
|
+
the client's own abort — and nothing downstream branches on that.
|
|
58
|
+
|
|
59
|
+
## Cached Answers
|
|
60
|
+
|
|
61
|
+
- **`{ cache: <ms> }` in an endpoint's signal option is the whole cache surface.** The lookup is a step of every
|
|
62
|
+
call and stands aside unless an endpoint declares one.
|
|
63
|
+
- **Only a `query` that takes no internal argument may carry one.** The handler's inputs are its declared
|
|
64
|
+
arguments plus its internal ones, so an endpoint with none of the latter answers the same thing to everyone who
|
|
65
|
+
may read it — which is the only answer a shared entry can hold. An endpoint that takes `.with(Self)` answers
|
|
66
|
+
per caller, and one entry would be one caller's answer handed to the next; that endpoint and every `mutation`
|
|
67
|
+
are named once in the log and left uncached rather than silently ignored.
|
|
68
|
+
- **The guards run before every hit.** The lookup sits after the guards and the internal arguments, inside the
|
|
69
|
+
call, so every middleware — the account resolution a lib registers among them — has already run and a hit
|
|
70
|
+
reaches only a caller the guards admitted. It used to be a middleware ahead of the lib's, and a guard reading
|
|
71
|
+
the account refused a signed-in caller on every hit. Shared is not public.
|
|
72
|
+
- **The entry is the handler's result, not the response.** The lookup wraps the handler only, so `resolveReturn`
|
|
73
|
+
still runs per call on the cached value — field masking, `hidden`/`secret` stripping and relation resolution are
|
|
74
|
+
never cached, and a relation still costs its own load. The cache saves the handler, not the serialization.
|
|
75
|
+
- **A cache backend that is down does not take the endpoint down.** A failed read is warned and the call runs
|
|
76
|
+
uncached; a failed write is warned and dropped.
|
|
77
|
+
- The default middleware chain is `Logging → Timeout → <lib middlewares>`, and the call inside it runs guards →
|
|
78
|
+
internal arguments → cache → handler: a hung cache backend is bounded by the same deadline as the handler it
|
|
79
|
+
stands in for. **`Retry` was removed** — an automatic re-run of an endpoint
|
|
80
|
+
whose failure it cannot classify replays whatever the first attempt already did.
|
|
81
|
+
|
|
31
82
|
## Binary Pubsub
|
|
32
83
|
|
|
33
84
|
- **`pubsub(Binary)` sends its payload in a websocket binary frame**, skipping the JSON `{ type: "pub" }`
|
|
@@ -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:
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
import {
|
|
3
|
+
ApplicationBuildReporter,
|
|
4
|
+
HmrWatcher,
|
|
5
|
+
WatchRootResolver
|
|
6
|
+
} from "./index-khpfpcsd.js";
|
|
2
7
|
import {
|
|
3
8
|
CodegenLock,
|
|
4
9
|
WorkspaceExecutor,
|
|
5
10
|
AppExecutor
|
|
6
|
-
} from "./index-
|
|
7
|
-
import"./index-
|
|
8
|
-
import"./index-6wwzs1bc.js";
|
|
11
|
+
} from "./index-9d9bgcx4.js";
|
|
12
|
+
import"./index-5b6tavv3.js";
|
|
9
13
|
import {
|
|
10
14
|
GraphClientEntryDiscovery,
|
|
11
15
|
RouteClientBuilder,
|
|
12
16
|
AutoImportSync,
|
|
13
17
|
DevChangePlanner,
|
|
14
18
|
DevGeneratedIndexSync
|
|
15
|
-
} from "./index-
|
|
16
|
-
import {
|
|
17
|
-
HmrWatcher,
|
|
18
|
-
WatchRootResolver
|
|
19
|
-
} from "./index-hwkygn9s.js";
|
|
19
|
+
} from "./index-fgq75haa.js";
|
|
20
20
|
import {
|
|
21
21
|
prepareDevWatchBatch
|
|
22
22
|
} from "./index-a5vmm6x4.js";
|
|
@@ -30,12 +30,6 @@ import path from "path";
|
|
|
30
30
|
import { Logger } from "akanjs/common";
|
|
31
31
|
|
|
32
32
|
class BuildBatchRunner {
|
|
33
|
-
static #entryCandidates = (workspaceRoot) => [
|
|
34
|
-
path.join(workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
|
|
35
|
-
path.join(workspaceRoot, "node_modules/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
|
|
36
|
-
path.join(import.meta.dir, "buildBatch.proc.js"),
|
|
37
|
-
path.join(import.meta.dir, "buildBatch.proc.ts")
|
|
38
|
-
];
|
|
39
33
|
#logger = new Logger("BuildBatchRunner");
|
|
40
34
|
#entry = null;
|
|
41
35
|
#workspaceRoot;
|
|
@@ -47,7 +41,12 @@ class BuildBatchRunner {
|
|
|
47
41
|
async#resolveEntry() {
|
|
48
42
|
if (this.#entry)
|
|
49
43
|
return this.#entry;
|
|
50
|
-
const candidates =
|
|
44
|
+
const candidates = [
|
|
45
|
+
path.join(this.#workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
|
|
46
|
+
path.join(this.#workspaceRoot, "node_modules/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
|
|
47
|
+
path.join(import.meta.dir, "buildBatch.proc.js"),
|
|
48
|
+
path.join(import.meta.dir, "buildBatch.proc.ts")
|
|
49
|
+
];
|
|
51
50
|
for (const candidate of candidates) {
|
|
52
51
|
if (!await Bun.file(candidate).exists())
|
|
53
52
|
continue;
|
|
@@ -173,6 +172,7 @@ class IncrementalBuilder {
|
|
|
173
172
|
app: this.#app,
|
|
174
173
|
routeId: msg.routeId,
|
|
175
174
|
seeds: msg.seeds,
|
|
175
|
+
graphSeeds: msg.graphSeeds,
|
|
176
176
|
artifact: this.#artifact,
|
|
177
177
|
knownEntries: new Set(msg.knownEntries),
|
|
178
178
|
discovery: this.#discovery
|
|
@@ -195,7 +195,7 @@ class IncrementalBuilder {
|
|
|
195
195
|
}
|
|
196
196
|
};
|
|
197
197
|
} catch (err) {
|
|
198
|
-
const errMsg = err
|
|
198
|
+
const errMsg = ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot);
|
|
199
199
|
this.#logger.error(`build-route failed routeId=${msg.routeId}: ${errMsg}`);
|
|
200
200
|
this.#sendBuildStatus("route", { generation: msg.generation, ok: false, files: msg.seeds, message: errMsg });
|
|
201
201
|
return { type: "build-route-res", id: msg.id, ok: false, error: errMsg };
|
|
@@ -335,20 +335,19 @@ class IncrementalBuilder {
|
|
|
335
335
|
}, 150);
|
|
336
336
|
}
|
|
337
337
|
async installWatcher() {
|
|
338
|
-
const artifactDir = this.#artifactDir;
|
|
339
338
|
const roots = await new WatchRootResolver(this.#app).resolve();
|
|
340
339
|
const watcher = new HmrWatcher({
|
|
341
340
|
roots,
|
|
342
341
|
logger: this.#logger,
|
|
343
342
|
onBatch: async (batch) => {
|
|
344
|
-
await this.#enqueueWork("hmr-batch", async () => this.#handleWatchBatch(
|
|
343
|
+
await this.#enqueueWork("hmr-batch", async () => this.#handleWatchBatch(batch));
|
|
345
344
|
}
|
|
346
345
|
});
|
|
347
346
|
await watcher.start();
|
|
348
347
|
this.#watcher = watcher;
|
|
349
348
|
this.#logger.verbose(`watching ${roots.length} roots`);
|
|
350
349
|
}
|
|
351
|
-
async#handleWatchBatch(
|
|
350
|
+
async#handleWatchBatch(batch) {
|
|
352
351
|
const rawKinds = new Set(batch.kinds);
|
|
353
352
|
if (rawKinds.size === 0)
|
|
354
353
|
return;
|
|
@@ -401,7 +400,7 @@ class IncrementalBuilder {
|
|
|
401
400
|
}
|
|
402
401
|
const needs = [];
|
|
403
402
|
if (kinds.includes("code") && rebuildClient) {
|
|
404
|
-
if (this.#
|
|
403
|
+
if (this.#csrActive)
|
|
405
404
|
needs.push("csr");
|
|
406
405
|
else
|
|
407
406
|
this.#logger.verbose(`csr-rebundle skipped; request /__csr or ?csr=true (or set AKAN_DEV_CSR_REBUILD=1) to enable per-save CSR rebuilds`);
|
|
@@ -416,13 +415,10 @@ class IncrementalBuilder {
|
|
|
416
415
|
this.#logger.verbose(`css-rebuild scheduled generation=${generation}`);
|
|
417
416
|
}
|
|
418
417
|
}
|
|
419
|
-
async#runBatch({
|
|
420
|
-
generation,
|
|
421
|
-
needs,
|
|
422
|
-
changedFiles
|
|
423
|
-
}) {
|
|
418
|
+
async#runBatch(work) {
|
|
419
|
+
const { generation, needs, changedFiles } = work;
|
|
424
420
|
const started = Date.now();
|
|
425
|
-
const result = await this.#batchRunner.run(await this.#batchRequest(
|
|
421
|
+
const result = await this.#batchRunner.run(await this.#batchRequest(work), (msg) => BuilderChannel.emit(msg));
|
|
426
422
|
if (result.optimizedFonts)
|
|
427
423
|
this.#optimizedFonts = result.optimizedFonts;
|
|
428
424
|
if (result.cssAssets)
|
|
@@ -436,11 +432,7 @@ class IncrementalBuilder {
|
|
|
436
432
|
this.#logger.verbose(`css-rebuild checked (${Date.now() - started}ms)`);
|
|
437
433
|
return result;
|
|
438
434
|
}
|
|
439
|
-
async#batchRequest({
|
|
440
|
-
generation,
|
|
441
|
-
needs,
|
|
442
|
-
changedFiles
|
|
443
|
-
}) {
|
|
435
|
+
async#batchRequest({ generation, needs, changedFiles }) {
|
|
444
436
|
return {
|
|
445
437
|
appName: this.#app.name,
|
|
446
438
|
workspaceRoot: this.#app.workspace.workspaceRoot,
|
|
@@ -505,9 +497,6 @@ class IncrementalBuilder {
|
|
|
505
497
|
await BuilderChannel.send({ type: "build-csr-res", id: msg.id, ok: true });
|
|
506
498
|
});
|
|
507
499
|
}
|
|
508
|
-
#shouldRebuildCsr() {
|
|
509
|
-
return this.#csrActive;
|
|
510
|
-
}
|
|
511
500
|
static #csrArmedByEnv() {
|
|
512
501
|
return process.env.AKAN_DEV_CSR_REBUILD === "1";
|
|
513
502
|
}
|
|
@@ -606,23 +595,17 @@ class IncrementalBuilder {
|
|
|
606
595
|
builder.shutdown(msg.reason);
|
|
607
596
|
return;
|
|
608
597
|
}
|
|
609
|
-
if (msg.type
|
|
598
|
+
if (msg.type !== "build-route" && msg.type !== "build-csr")
|
|
599
|
+
return;
|
|
600
|
+
if (!builder || builder.shuttingDown) {
|
|
610
601
|
const error = builder?.shuttingDown ? recyclingError : bootingError;
|
|
611
|
-
|
|
612
|
-
BuilderChannel.emit({ type: "build-route-res", id: msg.id, ok: false, error });
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
builder.handleBuildRoute(msg);
|
|
602
|
+
BuilderChannel.emit({ type: `${msg.type}-res`, id: msg.id, ok: false, error });
|
|
616
603
|
return;
|
|
617
604
|
}
|
|
618
|
-
if (msg.type === "build-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
BuilderChannel.emit({ type: "build-csr-res", id: msg.id, ok: false, error });
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
605
|
+
if (msg.type === "build-route")
|
|
606
|
+
builder.handleBuildRoute(msg);
|
|
607
|
+
else
|
|
624
608
|
builder.handleBuildCsr(msg);
|
|
625
|
-
}
|
|
626
609
|
});
|
|
627
610
|
process.on("disconnect", () => {
|
|
628
611
|
logger.warn("host IPC channel closed; exiting builder");
|
|
@@ -647,7 +630,7 @@ class IncrementalBuilder {
|
|
|
647
630
|
await builder.rearmCsrFromEnv();
|
|
648
631
|
}
|
|
649
632
|
}
|
|
650
|
-
IncrementalBuilder.main().catch((err) => {
|
|
633
|
+
await IncrementalBuilder.main().catch((err) => {
|
|
651
634
|
console.error(err);
|
|
652
635
|
process.exit(1);
|
|
653
636
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
getDirname2
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-5b6tavv3.js";
|
|
5
5
|
|
|
6
6
|
// pkgs/@akanjs/devkit/prompter.ts
|
|
7
7
|
import fsPromise from "fs/promises";
|
|
8
8
|
import { input, select } from "@inquirer/prompts";
|
|
9
|
-
class
|
|
9
|
+
class Prompter2 {
|
|
10
10
|
static async#getGuidelineRoot() {
|
|
11
11
|
const dirname = getDirname2(import.meta.url);
|
|
12
12
|
const candidates = [`${dirname}/guidelines`, `${dirname}/../cli/guidelines`];
|
|
@@ -19,27 +19,21 @@ class Prompter {
|
|
|
19
19
|
return candidates[0];
|
|
20
20
|
}
|
|
21
21
|
static async selectGuideline() {
|
|
22
|
-
const guideNames = await
|
|
22
|
+
const guideNames = await Prompter2.listGuidelines();
|
|
23
23
|
return await select({
|
|
24
24
|
message: "Select a guideline",
|
|
25
25
|
choices: guideNames.map((name) => ({ name, value: name }))
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
static async listGuidelines() {
|
|
29
|
-
const guidelineRoot = await
|
|
29
|
+
const guidelineRoot = await Prompter2.#getGuidelineRoot();
|
|
30
30
|
return (await fsPromise.readdir(guidelineRoot)).filter((name) => !name.startsWith("_")).sort();
|
|
31
31
|
}
|
|
32
|
-
static async getGuideJson(guideName) {
|
|
33
|
-
const guidelineRoot = await Prompter.#getGuidelineRoot();
|
|
34
|
-
const filePath = `${guidelineRoot}/${guideName}/${guideName}.generate.json`;
|
|
35
|
-
const guideJson = await fsPromise.readFile(filePath, "utf-8");
|
|
36
|
-
return JSON.parse(guideJson);
|
|
37
|
-
}
|
|
38
32
|
static async getInstruction(guideName) {
|
|
39
|
-
const guideNames = await
|
|
33
|
+
const guideNames = await Prompter2.listGuidelines();
|
|
40
34
|
if (!guideNames.includes(guideName))
|
|
41
35
|
throw new Error(`Unknown guideline "${guideName}". Available: ${guideNames.join(", ")}`);
|
|
42
|
-
const guidelineRoot = await
|
|
36
|
+
const guidelineRoot = await Prompter2.#getGuidelineRoot();
|
|
43
37
|
return await fsPromise.readFile(`${guidelineRoot}/${guideName}/${guideName}.instruction.md`, "utf-8");
|
|
44
38
|
}
|
|
45
39
|
static async getUpdateRequest(guideName) {
|
|
@@ -67,7 +61,7 @@ ${request}
|
|
|
67
61
|
`;
|
|
68
62
|
}
|
|
69
63
|
async getDocumentation(guideName) {
|
|
70
|
-
const guidelineRoot = await
|
|
64
|
+
const guidelineRoot = await Prompter2.#getGuidelineRoot();
|
|
71
65
|
const filePath = `${guidelineRoot}/${guideName}/${guideName}.instruction.md`;
|
|
72
66
|
const document = await fsPromise.readFile(filePath, "utf-8");
|
|
73
67
|
return `\`\`\`markdown
|
|
@@ -77,4 +71,4 @@ ${document}
|
|
|
77
71
|
}
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
export {
|
|
74
|
+
export { Prompter2 };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
jsonText
|
|
4
|
+
} from "./index-myhc3c1j.js";
|
|
5
|
+
import {
|
|
6
|
+
akanMcpInstallTargets
|
|
7
|
+
} from "./index-ygn77a5w.js";
|
|
8
|
+
import {
|
|
9
|
+
script
|
|
10
|
+
} from "./index-w2zdcw3k.js";
|
|
11
|
+
import {
|
|
12
|
+
ContextRunner2
|
|
13
|
+
} from "./index-nv5976x3.js";
|
|
14
|
+
|
|
15
|
+
// pkgs/@akanjs/cli/context/context.script.ts
|
|
16
|
+
import { Logger } from "akanjs/common";
|
|
17
|
+
var resolveMcpInstallTargets = (target) => {
|
|
18
|
+
if (!target || target === "all")
|
|
19
|
+
return [...akanMcpInstallTargets];
|
|
20
|
+
if (akanMcpInstallTargets.includes(target))
|
|
21
|
+
return [target];
|
|
22
|
+
throw new Error(`Unknown MCP install target: ${target}. Use cursor, claude, codex, or all.`);
|
|
23
|
+
};
|
|
24
|
+
var mcpTargetLabels = {
|
|
25
|
+
cursor: "Cursor",
|
|
26
|
+
claude: "Claude Code",
|
|
27
|
+
codex: "Codex"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
class ContextScript extends script("context", [ContextRunner2]) {
|
|
31
|
+
async context(workspace, options = {}) {
|
|
32
|
+
Logger.rawLog(await this.contextRunner.getContext(workspace, options));
|
|
33
|
+
}
|
|
34
|
+
async doctor(workspace, options = {}) {
|
|
35
|
+
Logger.rawLog(await this.contextRunner.doctor(workspace, options));
|
|
36
|
+
}
|
|
37
|
+
async mcpInstall(workspace, target, { force = false, mode = "apply" } = {}) {
|
|
38
|
+
const targets = resolveMcpInstallTargets(target);
|
|
39
|
+
const written = [];
|
|
40
|
+
for (const t of targets) {
|
|
41
|
+
const configPath = await this.contextRunner.installMcp(workspace, t, { force, mode });
|
|
42
|
+
written.push(`${mcpTargetLabels[t]}: ${configPath}`);
|
|
43
|
+
}
|
|
44
|
+
Logger.rawLog(`Akan MCP server installed (${mode} mode):
|
|
45
|
+
${written.map((line) => `- ${line}`).join(`
|
|
46
|
+
`)}`);
|
|
47
|
+
}
|
|
48
|
+
async mcp(workspace, { mode = "apply" } = {}) {
|
|
49
|
+
await this.contextRunner.runMcp(workspace, { mode });
|
|
50
|
+
}
|
|
51
|
+
async mcpCall(workspace, tool, {
|
|
52
|
+
mode = "readonly",
|
|
53
|
+
args = null,
|
|
54
|
+
format = "json"
|
|
55
|
+
} = {}) {
|
|
56
|
+
let parsedArgs = {};
|
|
57
|
+
if (args) {
|
|
58
|
+
const parsed = JSON.parse(args);
|
|
59
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
60
|
+
throw new Error("MCP call args must be a JSON object.");
|
|
61
|
+
}
|
|
62
|
+
parsedArgs = parsed;
|
|
63
|
+
}
|
|
64
|
+
const result = await this.contextRunner.callMcpTool(workspace, tool, parsedArgs, { mode });
|
|
65
|
+
Logger.rawLog(format === "json" ? jsonText(result) : String(result));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { ContextScript };
|