@akanjs/cli 3.0.0-alpha.102 → 3.0.0-alpha.104
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/{abstractCompactor-byytxs22.js → abstractCompactor-mjh5ppsq.js} +1 -1
- package/{agent.command-v1a4746v.js → agent.command-d2gfxfck.js} +5 -5
- package/{application.command-fzdcnzhg.js → application.command-xgphqs7k.js} +5 -5
- package/{applicationBuildRunner-s4gsk728.js → applicationBuildRunner-3d5q1yn7.js} +2 -2
- package/{applicationReleasePackager-28v9vbzk.js → applicationReleasePackager-4yasxwv9.js} +1 -1
- package/buildBatch.proc.js +2 -2
- package/{capacitorApp-8d6d4zkm.js → capacitorApp-36rbr858.js} +2 -2
- package/{cloud.command-wygy2z05.js → cloud.command-abgkk73f.js} +9 -9
- package/{context.command-4vdt1aqq.js → context.command-ez8a0zyz.js} +13 -13
- package/{devTui-qnf1txdz.js → devTui-70cjjtaw.js} +1 -1
- package/{devTuiView-yzct2cq6.js → devTuiView-71wnkjft.js} +1 -1
- package/{guideline.command-p92dq6ts.js → guideline.command-xmqwx8zm.js} +11 -7
- package/guidelines/agentRule/agentRule.instruction.md +22 -24
- package/guidelines/conventions/conventions.instruction.md +40 -25
- package/guidelines/mcpRule/mcpRule.instruction.md +61 -39
- package/guidelines/ssrRule/ssrRule.instruction.md +15 -13
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -3
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +68 -3
- package/incrementalBuilder.proc.js +1 -1
- package/{index-p5neav9b.js → index-0vh9rgxz.js} +1 -1
- package/{index-gq44bqjt.js → index-259dsnz3.js} +2 -2
- package/{index-qsq3q8fy.js → index-2tv3fdx4.js} +21 -10
- package/{index-yks023mb.js → index-4c9ykbfq.js} +6 -6
- package/{index-qkk27bk6.js → index-4edzf1gr.js} +4 -4
- package/{index-yza7zkpt.js → index-600zhnve.js} +10 -10
- package/{index-srdssqj7.js → index-7737gxcp.js} +3 -3
- package/{index-zmjppp25.js → index-8pm30m73.js} +5 -5
- package/{index-0amdxycf.js → index-fz5q3gd7.js} +3 -3
- package/{index-fc1bh0a7.js → index-gyd220p2.js} +1 -1
- package/{index-5svjba73.js → index-j9yhg404.js} +1 -1
- package/{index-c69j78kk.js → index-kn1h7p1x.js} +1 -1
- package/{index-gtmz74tj.js → index-ktb5njgy.js} +2 -2
- package/{index-znynzt5w.js → index-mak3cd28.js} +1 -1
- package/{index-29sxqfaw.js → index-ne2qy0nk.js} +3 -3
- package/{index-hyywke0f.js → index-pnqbkf5p.js} +1 -1
- package/{index-8p8p0cf8.js → index-rwg2aa4f.js} +3 -3
- package/{index-7d6vny25.js → index-ws5e0j51.js} +2 -2
- package/{index-1yxtbmbj.js → index-wt7qrtd4.js} +1 -1
- package/{index-zz5jyd94.js → index-yecdrt9a.js} +9 -9
- package/{index-zxgr3s8s.js → index-yw4xj7pa.js} +4 -4
- package/index.js +19 -19
- package/{library.command-s4fszngs.js → library.command-nbsky672.js} +3 -3
- package/{localRegistry.command-gyw4da9z.js → localRegistry.command-namg61k1.js} +8 -8
- package/{module.command-6n7dwvav.js → module.command-29vqzrh8.js} +6 -6
- package/{package.command-gr82ypnr.js → package.command-axv0abmn.js} +3 -3
- package/package.json +2 -2
- package/{page.command-7gvgb3jb.js → page.command-vs9s3zrh.js} +3 -3
- package/{primitive.command-tk5ckw38.js → primitive.command-cbxmqbtc.js} +7 -7
- package/{quality.command-38v81baf.js → quality.command-gmbs7nws.js} +2 -2
- package/{repair.command-5xbwvsxp.js → repair.command-bqsjptsj.js} +5 -5
- package/{routeSourceValidator-ze6x894c.js → routeSourceValidator-apyqw7wk.js} +90 -17
- package/{scalar.command-0epwcr5k.js → scalar.command-6k1vhe9s.js} +5 -5
- package/{subspace.command-e2t36xb2.js → subspace.command-35a3t6vv.js} +3 -3
- package/templates/app/page/_index.tsx +3 -3
- package/templates/app/page/_layout.tsx +31 -32
- package/templates/appSample/page/task/[taskId]/_index.tsx +22 -27
- package/templates/appSample/page/task/[taskId]/edit.tsx +25 -30
- package/templates/appSample/page/task/_index.tsx +3 -7
- package/templates/appSample/page/task/_layout.tsx +4 -15
- package/templates/appSample/page/task/new.tsx +4 -10
- package/templates/crudPages/[__model__Id]/edit/page.tsx +26 -28
- package/templates/crudPages/[__model__Id]/page.tsx +20 -25
- package/templates/crudPages/new/page.tsx +24 -23
- package/templates/crudPages/page.tsx +25 -24
- package/templates/crudSinglePage/page.tsx +25 -24
- package/{workflow.command-kphbr7eh.js → workflow.command-nwpcdc8p.js} +10 -10
- package/{workspace.command-j1gatz3d.js → workspace.command-kah5jgg5.js} +20 -20
|
@@ -73,7 +73,7 @@ export const option = new AkanOption<ModulesOptions>().setMcp({
|
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
```typescript
|
|
76
|
-
// <model>.signal.ts — every one of these is an MCP tool
|
|
76
|
+
// <model>.signal.ts — every one of these is an MCP tool, with no `mcp:` option anywhere
|
|
77
77
|
export class TaskSlice extends slice(
|
|
78
78
|
srv.task,
|
|
79
79
|
{ guards: { root: Admin, get: SignedIn, cru: SignedIn } },
|
|
@@ -86,21 +86,26 @@ export class TaskSlice extends slice(
|
|
|
86
86
|
}),
|
|
87
87
|
) {}
|
|
88
88
|
|
|
89
|
-
export class TaskEndpoint extends endpoint(srv.task, ({ mutation
|
|
89
|
+
export class TaskEndpoint extends endpoint(srv.task, ({ mutation }) => ({
|
|
90
90
|
startTask: mutation(cnst.Task, { guards: [SignedIn] })
|
|
91
91
|
.param("taskId", ID)
|
|
92
92
|
.exec(async function (taskId) {
|
|
93
93
|
return await this.taskService.startTask(taskId);
|
|
94
94
|
}),
|
|
95
|
-
reviewTask: prompt({ guards: [SignedIn] })
|
|
96
|
-
.param("taskId", ID)
|
|
97
|
-
.exec(async function (taskId) {
|
|
98
|
-
const task = await this.taskService.getTask(taskId);
|
|
99
|
-
return [Msg.user(`Review this task and suggest next steps.`), Msg.resource(`akan://task/${taskId}`, task)];
|
|
100
|
-
}),
|
|
101
95
|
})) {}
|
|
102
96
|
```
|
|
103
97
|
|
|
98
|
+
```tsx
|
|
99
|
+
// page/task/[taskId]/_index.tsx — the screen, published to agents as the prompt `reviewTask`
|
|
100
|
+
export default page()
|
|
101
|
+
.param("taskId", ID, { desc: "The task to review." })
|
|
102
|
+
.prompt("reviewTask", "Review this task and suggest next steps.")
|
|
103
|
+
.render(async ({ taskId }) => {
|
|
104
|
+
const { taskView } = await fetch.viewTask(taskId);
|
|
105
|
+
return <Task.Zone.View view={taskView} />;
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
104
109
|
## Refusals And Wire Behaviour
|
|
105
110
|
- **`mcp: false` is read first**, because it is the one answer nobody had to derive — somebody stated it. Its
|
|
106
111
|
reason says "HTTP still serves it", because that is the sentence that stops the next reader treating it as a lock.
|
|
@@ -111,16 +116,13 @@ export class TaskEndpoint extends endpoint(srv.task, ({ mutation, prompt }) => (
|
|
|
111
116
|
**`light<Model>` read** — it returns the same document as `<Model>` under the same guards, in a shape trimmed
|
|
112
117
|
for a page's payload, so publishing both put two entries per model in a listing an agent pays for every turn
|
|
113
118
|
and gave it nothing but the word "light" to choose between them.
|
|
114
|
-
A `prompt` refuses two more, because its `arguments` is one string per name with no schema beside it: a **list
|
|
115
|
-
argument**, which could never carry a second value, and **any `Any` argument** — a tool leaves that out of its
|
|
116
|
-
schema, and a prompt has no schema to leave it out of.
|
|
117
119
|
- **The boot log sizes the catalogue and says which signals it went to** — `MCP catalogue: tools=48 … · listing
|
|
118
120
|
214KB` and `MCP catalogue cost: user 27/71KB · file 6/22KB · …`, heaviest first. Read it before narrowing
|
|
119
121
|
anything: the number is nobody's intuition, because MCP has no shared component section and forbids a `$ref`
|
|
120
122
|
across entries, so **every entry inlines the full schema of every model it mentions** and a listing is re-sent
|
|
121
123
|
whole to every agent that connects, before its first turn. Past 100KB it says so as a `warn`.
|
|
122
|
-
- **Every refusal is named in the boot log**: one `warn` per endpoint plus a `MCP catalogue: tools=…
|
|
123
|
-
count. Read that line first when a tool you expected is missing — and it is the *only* place the answer exists,
|
|
124
|
+
- **Every refusal is named in the boot log**: one `warn` per endpoint plus a `MCP catalogue: tools=…
|
|
125
|
+
resourceTemplates=…` count, and a `MCP page prompts: N (…)` line once the RSC worker has listed the pages. Read that line first when a tool you expected is missing — and it is the *only* place the answer exists,
|
|
124
126
|
because there is no absent opt-in to notice. The API explorer badges the same rule per endpoint (`MCP` /
|
|
125
127
|
`MCP refused`), from the same shared implementation the catalogue runs.
|
|
126
128
|
- **An `Any` argument is left out of the published schema** rather than described as `{}` — it tells a model
|
|
@@ -146,7 +148,8 @@ export class TaskEndpoint extends endpoint(srv.task, ({ mutation, prompt }) => (
|
|
|
146
148
|
doubling of what every model-returning tool costs a model. `option.setMcp({ legacyTextBlock: false })` — or
|
|
147
149
|
`AKAN_MCP_LEGACY_TEXT=false` — leaves a one-line pointer in the text block instead, for a deployment whose
|
|
148
150
|
clients read the structured half. A scalar return is unaffected: it has no structured half to point at, so it
|
|
149
|
-
ships as the value itself either way.
|
|
151
|
+
ships as the value itself either way. `resources/read` is unaffected too: `ReadResourceResult` has no
|
|
152
|
+
`structuredContent`, so a resource's text is its only channel and always carries the serialized value.
|
|
150
153
|
- **A request schema asks for a relation's id, and a response schema names a nested model.** `serialize` sends a
|
|
151
154
|
relation field as its id and the server never converts an object back, so publishing the related model's shape
|
|
152
155
|
asked an agent for what the document layer cannot take. On the response side every entry inlines the transitive
|
|
@@ -213,9 +216,9 @@ export class TaskEndpoint extends endpoint(srv.task, ({ mutation, prompt }) => (
|
|
|
213
216
|
which is in force, and warns when it is off.
|
|
214
217
|
- **A `resources/read` uri that does not decode** — a stray `%` — is `Unknown resource`, not a server failure.
|
|
215
218
|
- **A caller's own mistake is reported as one** and never as a server failure: an argument that is missing,
|
|
216
|
-
unparseable or **undeclared** comes back as `isError` naming it — `additionalProperties: false` travels in the
|
|
219
|
+
unparseable, outside its `enumOf`, or **undeclared** comes back as `isError` naming it — `additionalProperties: false` travels in the
|
|
217
220
|
published schema and nothing on the wire enforces it — and so does a document that is not there, as
|
|
218
|
-
`No <model> found for the arguments given.` A
|
|
221
|
+
`No <model> found for the arguments given.` A prompt, having no `isError` to carry a refusal, answers `-32602`.
|
|
219
222
|
Only a real failure logs a stack; an agent can drive the rest at will.
|
|
220
223
|
- **Three revisions are spoken**: the modern `2026-07-28` and the legacy `2025-11-25` / `2025-06-18`, which are
|
|
221
224
|
wire-identical over the POST-only surface this implements — a client whose proposal is not listed is told to
|
|
@@ -225,7 +228,7 @@ export class TaskEndpoint extends endpoint(srv.task, ({ mutation, prompt }) => (
|
|
|
225
228
|
- **A modern-era request mirrors `MCP-Protocol-Version` and `Mcp-Method` into headers** (plus `Mcp-Name` when the
|
|
226
229
|
body names one), and one that leaves a mirror out is refused just like one that contradicts the body: a gateway
|
|
227
230
|
rule keyed on a header never fires for the request that omitted it. Legacy requests are not checked. Capabilities
|
|
228
|
-
are derived from the catalogue
|
|
231
|
+
are derived from the catalogue; `prompts` is advertised when the build serves pages, since prompts are theirs.
|
|
229
232
|
- **An expired, wrongly-audienced or unverifiable bearer token is refused up front**, so an agent is told to
|
|
230
233
|
authenticate rather than that the tool does not exist. The signature is checked through `auth.verify`, which the
|
|
231
234
|
module that mints the tokens supplies (`libs/shared` does, from the app's signing secret); without a verifier the
|
|
@@ -313,23 +316,41 @@ delegates to lives in `akanjs/server` (`OAuthAuthorize`, `OAuthToken`, `OAuthReg
|
|
|
313
316
|
derived fallback is seeded from the app, environment and repo names and forges an admin token for anyone who
|
|
314
317
|
knows them. `AKAN_ALLOW_DERIVED_JWT_SECRET=1` accepts that risk explicitly.
|
|
315
318
|
|
|
316
|
-
## `prompt()`
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
`
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
319
|
+
## Page Prompts — `page().prompt()`
|
|
320
|
+
**A prompt is a screen.** There is no `prompt()` on `endpoint()`: the page file declares it —
|
|
321
|
+
`page().param("projectId", ID, { desc: "The project to brief." }).prompt("briefProjectTickets", "Brief the ticket
|
|
322
|
+
board of one project.").render(…)` — and that is the whole surface. `prompts/list` is every page carrying
|
|
323
|
+
`.prompt()`; its `.param()` stages are the required arguments and its `.search()` stages the optional ones, a list
|
|
324
|
+
argument is comma-separated on the wire and its description says so, and an `ID`/`Int`/`enumOf` value is checked by
|
|
325
|
+
the page's own declaration. **`prompts/get` runs the page's body** — root layouts, layouts, then the page's
|
|
326
|
+
`.render()` — in the RSC worker under the caller's bearer token, the way a tab would open it. Nothing is rendered:
|
|
327
|
+
the JSX comes back as an element tree nobody walks, no client component runs, and the screen's data footprint is
|
|
328
|
+
exactly the `fetch.*` queries its body made, with the arguments it made them with. The answer is the description as
|
|
329
|
+
the first message (priority 1), one embedded `resource` per query masked by that endpoint's return model and
|
|
330
|
+
addressed by the `akan://` uri the tool answers to (`akan://<toolKey>?args` for a custom read) — one document read
|
|
331
|
+
in two shapes, a layout's `project` beside the page's `lightProject`, travels once as the larger — and a closing
|
|
332
|
+
`Tools for this screen: …` line naming the published tools of the modules the screen fetched from, minus the reads
|
|
333
|
+
whose answers are attached above and filtered to what this caller may see — which is how a model reading a ticket
|
|
334
|
+
board is handed the ticket tools and not three hundred. Lists are cut to `promptBudget` (60,000 characters;
|
|
335
|
+
`option.setMcp({ promptBudget })`, `AKAN_MCP_PROMPT_BUDGET`), largest first, and the cut is said: `Attached the
|
|
336
|
+
first 20 of 341 rows of \`ticketListInProject\`; call it for the rest.` Only lists are cut — a single document rides
|
|
337
|
+
whole, so the budget is what the lists are trimmed to fit under, not a ceiling a screen of large documents cannot
|
|
338
|
+
pass. A layout's own queries (a project selector's `projectListInOrg`) are part of every screen under it and are
|
|
339
|
+
attached too. **No argument fallback**: a required argument left out answers one message — `No projectId was named for
|
|
340
|
+
"briefProjectTickets". Find it with \`projectListBySearch\`, then run this prompt again with projectId=<id>.` —
|
|
341
|
+
and runs nothing, because a prompt cannot re-run itself and context filled in on a guess would never be used. A
|
|
342
|
+
page that redirects (`getSelf({ unauthorize })`), or whose `fetch.*` a guard refuses with 401/403, answers the 401
|
|
343
|
+
credential challenge when the call carried no token and `This screen is not available to the signed-in account.`
|
|
344
|
+
when it did — the same sentence for both, so a refused id is never confirmed to exist; `router.notFound()` and a
|
|
345
|
+
404 from a query are `No screen exists for these arguments.`; anything else the body throws is `The page failed to
|
|
346
|
+
load.`, with the cause in the server log only. **The description is the only instruction.** `<Agent.Guide>` is for the in-page agent, whose
|
|
347
|
+
levers are the screen's controls, and is never read here, where the levers are the API — so write the description
|
|
348
|
+
in English, in the API's vocabulary, and put a computed rule (the legal status transitions) into it as a static
|
|
349
|
+
table the model combines with the attached state. Names match `^[A-Za-z0-9_-]{1,64}$` and are unique across pages;
|
|
350
|
+
two pages claiming one name fail the listing. An API-only build (`web: false`) publishes no prompts, and the
|
|
351
|
+
in-page chat lists none of them: its `/` menu is its six built-in commands. Pick which screens to publish by this
|
|
352
|
+
rule: a page taking no arguments is the most useful prompt, since MCP has no completion — one taking an id is for
|
|
353
|
+
when the id is already in hand.
|
|
333
354
|
|
|
334
355
|
## Progress Reporting
|
|
335
356
|
**`McpProgress.report(n, { total, message })`** reports progress from anywhere inside a call, a service or adapter
|
|
@@ -399,8 +420,8 @@ the same guards.** What stays closed is closed because no agent has a reason to
|
|
|
399
420
|
## Writing `instructions`
|
|
400
421
|
|
|
401
422
|
`*.abstract.md` and `<Agent.Guide>` never reach an MCP client. Exactly three texts do: `instructions`, every
|
|
402
|
-
published entry's `.desc()`, and a
|
|
403
|
-
model, and the catalogue is built once in one `language` and cached by clients.
|
|
423
|
+
published entry's `.desc()`, and a page's `.prompt()` description. All three are English, whatever the users speak
|
|
424
|
+
— the reader is a model, and the catalogue is built once in one `language` and cached by clients.
|
|
404
425
|
|
|
405
426
|
- **`instructions` is the workflow.** Five to eight sentences, in this order: what the app is and what its nouns
|
|
406
427
|
mean; **which tool to call first** and which ids the rest take; how a name becomes an id; what a write confirms
|
|
@@ -419,9 +440,10 @@ model, and the catalogue is built once in one `language` and cached by clients.
|
|
|
419
440
|
- **`.desc()` is the reason to pick one tool** — what it returns and when to use it, two clauses. An id argument's
|
|
420
441
|
`.desc()` names the tool that returns it (`From projectInOrg`). The generated CRUD borrows the model's `.of()`
|
|
421
442
|
label and `.desc()`, which is the only text those entries can carry.
|
|
422
|
-
-
|
|
423
|
-
an onboarding check.
|
|
424
|
-
by their
|
|
443
|
+
- **A page prompt is a workflow the user invokes** as a slash command, not one the model chooses — a weekly
|
|
444
|
+
report, an onboarding check. Its instruction is the `.prompt()` description alone and its attachments are what
|
|
445
|
+
the screen fetched, masked by their return models — so to change what a prompt hands the model, change what the
|
|
446
|
+
screen loads, and keep the description to the ask.
|
|
425
447
|
|
|
426
448
|
## Verifying An App
|
|
427
449
|
|
|
@@ -84,19 +84,21 @@ whole detail surface renders server-side wherever a route uses the `View` direct
|
|
|
84
84
|
sections actually need:
|
|
85
85
|
|
|
86
86
|
```tsx
|
|
87
|
-
export default
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
87
|
+
export default page()
|
|
88
|
+
.param("orgId", ID)
|
|
89
|
+
.render(async ({ orgId }) => {
|
|
90
|
+
const { taskInitInOrg, taskListInOrg } = fetch.initTaskInOrg(orgId);
|
|
91
|
+
const { orgView } = fetch.viewOrg(orgId);
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
<Org.Zone.Header view={orgView} />
|
|
95
|
+
<Task.Zone.Card init={taskInitInOrg} />
|
|
96
|
+
<Load.Stream of={taskListInOrg} fallback={<Loading.Skeleton active />}>
|
|
97
|
+
{(taskList) => taskList.map((task) => <Task.Unit.Row key={task.id} task={task} />)}
|
|
98
|
+
</Load.Stream>
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
});
|
|
100
102
|
```
|
|
101
103
|
|
|
102
104
|
Both queries leave immediately — splitting the result never serializes them — and each section renders behind its
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
- `apps/<app>/lib/<model>` holds database-backed domain modules, `lib/_<service>` service modules, and
|
|
6
6
|
`lib/__scalar/<scalar>` reusable value types. Module abstracts sit beside the code as `<name>.abstract.md`.
|
|
7
7
|
- `apps/<app>/page` holds server-side file-routed pages: `<routeName>.tsx` serves `/routeName`, a directory's
|
|
8
|
-
`_index.tsx` serves that directory, `_layout.tsx` nests a layout, and `[modelId]` is a dynamic segment.
|
|
8
|
+
`_index.tsx` serves that directory, `_layout.tsx` nests a layout, and `[modelId]` is a dynamic segment. Each route
|
|
9
|
+
file is one chain — `export default page()…render()`, or `layout()` / `rootLayout()` in a `_layout.tsx`.
|
|
9
10
|
|
|
10
11
|
The file roles inside a module — which file owns persistence, business logic, state, and each UI shape — are in
|
|
11
12
|
the convention set above under **Domain Module Conventions**.
|
|
@@ -258,6 +259,6 @@ core framework. The core framework gives you the composition points below; wire
|
|
|
258
259
|
- **Auto-generated CRUD and `serve()` service methods / lifecycle hooks do not receive session context.** If an
|
|
259
260
|
operation needs the acting user, expose a custom endpoint that takes it via `.with(CurrentUserId)` — never trust a
|
|
260
261
|
client-supplied user id.
|
|
261
|
-
- **SSR auth-gated pages: guard at the layout.** Check the session
|
|
262
|
-
absent, so nested pages never render for signed-out users.
|
|
262
|
+
- **SSR auth-gated pages: guard at the layout.** Check the session inside the `_layout.tsx` chain's `.render()` and
|
|
263
|
+
redirect when it is absent, so nested pages never render for signed-out users.
|
|
263
264
|
|
|
@@ -186,11 +186,11 @@ export class TaskFilter extends from(cnst.Task, (filter) => ({
|
|
|
186
186
|
})),
|
|
187
187
|
}))
|
|
188
188
|
|
|
189
|
-
// 4. In page —
|
|
190
|
-
export default async
|
|
189
|
+
// 4. In page — init the slice inside the page chain's render and hand the init to a Zone.
|
|
190
|
+
export default page().render(async () => {
|
|
191
191
|
const [{ taskInitInTodo }] = await Promise.all([fetch.initTaskInTodo()]);
|
|
192
192
|
return <Task.Zone.Card init={taskInitInTodo} sliceName="taskInTodo" />;
|
|
193
|
-
}
|
|
193
|
+
});
|
|
194
194
|
```
|
|
195
195
|
|
|
196
196
|
The slice name in code uses camelCase (`inTodo`). In dictionary and components it becomes `"taskInTodo"`.
|
|
@@ -328,6 +328,71 @@ export const Insight = ({ sliceName }: { sliceName: string }) => {
|
|
|
328
328
|
|
|
329
329
|
---
|
|
330
330
|
|
|
331
|
+
### Recipe 7: Migrating `page/` to the Route Chain
|
|
332
|
+
|
|
333
|
+
A route file used to be a default function beside named exports (`pageConfig`, `head`, `generateHead`, `Loading`,
|
|
334
|
+
`fonts`, …). It is now one chain, and the chain is also what publishes a screen to agents. The legacy shape still
|
|
335
|
+
loads and logs one warning per file at boot (`… uses the legacy route shape …`); migrate file by file, then run
|
|
336
|
+
`bun run akan typecheck <app>` and `bun run akan lint <app>`.
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
// before — page/(user)/project/[projectId]/_index.tsx
|
|
340
|
+
import { fetch, usePage } from "@apps/<app>/client";
|
|
341
|
+
import type { PageConfig } from "akanjs/client";
|
|
342
|
+
|
|
343
|
+
interface PageProps {
|
|
344
|
+
params: { projectId: string };
|
|
345
|
+
searchParams: { tab?: string };
|
|
346
|
+
}
|
|
347
|
+
export function generateHead({ params }: PageProps) {
|
|
348
|
+
return <title>{params.projectId}</title>;
|
|
349
|
+
}
|
|
350
|
+
export default async function Page({ params: { projectId }, searchParams }: PageProps) {
|
|
351
|
+
const { l } = usePage();
|
|
352
|
+
const { project } = await fetch.viewProject(projectId);
|
|
353
|
+
return <Project.Zone.View view={project} tab={searchParams.tab} />;
|
|
354
|
+
}
|
|
355
|
+
export const pageConfig = { transition: "stack" } satisfies PageConfig;
|
|
356
|
+
|
|
357
|
+
// after
|
|
358
|
+
import { fetch, usePage } from "@apps/<app>/client";
|
|
359
|
+
import { ID } from "akanjs/base";
|
|
360
|
+
import { page } from "akanjs/client";
|
|
361
|
+
|
|
362
|
+
export default page()
|
|
363
|
+
.param("projectId", ID, { desc: "The project to show." })
|
|
364
|
+
.search("tab", String)
|
|
365
|
+
.config({ transition: "stack" })
|
|
366
|
+
.head(({ projectId }) => <title>{projectId}</title>)
|
|
367
|
+
.prompt("briefProject", "Brief one project: its status and what is due.")
|
|
368
|
+
.render(async ({ projectId, tab }) => {
|
|
369
|
+
const { l } = usePage();
|
|
370
|
+
const { project } = await fetch.viewProject(projectId);
|
|
371
|
+
return <Project.Zone.View view={project} tab={tab} />;
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
| Legacy export | Chain stage |
|
|
376
|
+
|---|---|
|
|
377
|
+
| `interface PageProps { params }` + `[x]` folder | `.param("x", ID \| String \| Int \| …)` — one per `[x]` segment, every one of them, in path order |
|
|
378
|
+
| `searchParams.k` | `.search("k", Type)`; `[Type]` for a repeated key; the value arrives typed and optional |
|
|
379
|
+
| `params.lang` | `lang` — on every route's `.render()`, `.head()`, `.metadata()` and `.loading()` with no stage; `.param("lang")` is refused |
|
|
380
|
+
| `export const pageConfig = {…}` | `.config({…})` — `devOnly` stays a literal |
|
|
381
|
+
| `export const head = <…/>` / `generateHead({ params })` | `.head(<…/>)` / `.head(({ x }) => <…/>)` |
|
|
382
|
+
| `export const metadata` / `generateMetadata` | `.metadata({…})` / `.metadata((args) => …)` |
|
|
383
|
+
| `export function Loading()` | `.loading((args) => …)` |
|
|
384
|
+
| `export default async function Page({ params, searchParams })` | `.render(async ({ x, k }) => { …same body… })` — `async` only if it awaits |
|
|
385
|
+
| `_layout.tsx`: `export default function Layout({ children })` | `layout().render(({ children }) => …)` |
|
|
386
|
+
| `export function NotFound` / `export function Error` | `.notFound(fn)` / `.error(fn)` on `layout()` |
|
|
387
|
+
| root `_layout.tsx`: `fonts`, `theme`, `manifest`, `reconnect`, `wsConnect`, `layoutStyle`, `gaTrackingId` | `rootLayout().fonts([…]).theme("dark").manifest({…}).reconnect(false).wsConnect(false).layoutStyle("web").gaTrackingId("G-…")` |
|
|
388
|
+
| `prompt()` on `endpoint()` (removed) | `.prompt(name, description)` on the page that shows the same screen |
|
|
389
|
+
|
|
390
|
+
Rules the loader and `akan sync` enforce: a chain module has no other export; `page()` belongs in a page file and
|
|
391
|
+
`layout()` / `rootLayout()` in a `_layout.tsx`; a page names every `[x]` of its path, a layout may name a subset;
|
|
392
|
+
`.param()` and `.prompt()` names are string literals; `import "./styles.css"` stays the first line of a root layout;
|
|
393
|
+
`_overrides.tsx` is untouched. MCP: `prompts/get` now runs the page's body under the caller's token and hands the
|
|
394
|
+
model the description, what the page fetched, and the tools of those modules — see `mcpRule`.
|
|
395
|
+
|
|
331
396
|
### Data Flow Summary
|
|
332
397
|
|
|
333
398
|
For each business question, follow this chain:
|
|
@@ -947,6 +947,8 @@ class AsyncDefaultExportDetector {
|
|
|
947
947
|
if (ts.isExportAssignment(statement)) {
|
|
948
948
|
if (this.#isAsyncFunctionExpression(statement.expression))
|
|
949
949
|
return true;
|
|
950
|
+
if (ts.isCallExpression(statement.expression))
|
|
951
|
+
return true;
|
|
950
952
|
if (ts.isIdentifier(statement.expression))
|
|
951
953
|
defaultIdentifier = statement.expression.text;
|
|
952
954
|
continue;
|
|
@@ -1096,13 +1098,18 @@ async function writeGeneratedRootLayoutFile(opts) {
|
|
|
1096
1098
|
void st;
|
|
1097
1099
|
` : `import "@apps/${opts.appName}/client";
|
|
1098
1100
|
`;
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
+
const inheritedLabel = inheritedSourceAbsPath ? path4.relative(opts.appCwdPath, inheritedSourceAbsPath) : "";
|
|
1102
|
+
const inheritedImport = inheritedSourceSpecifier ? `import * as inheritedModule from ${JSON.stringify(inheritedSourceSpecifier)};
|
|
1103
|
+
const inheritedLayout = resolveRouteModule(inheritedModule as never, ${JSON.stringify(inheritedLabel)}).module as LayoutModule;
|
|
1104
|
+
` : `const inheritedLayout: LayoutModule = {};
|
|
1101
1105
|
`;
|
|
1102
1106
|
const prefix = routePrefixForSegments(opts.boundary.segments);
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
const userLayout = {};
|
|
1107
|
+
const userLabel = opts.boundary.sourceAbsPath ? path4.relative(opts.appCwdPath, opts.boundary.sourceAbsPath) : "";
|
|
1108
|
+
const userImport = sourceSpecifier ? `import * as userModule from ${JSON.stringify(sourceSpecifier)};
|
|
1109
|
+
const userLayout = resolveRouteModule(userModule as never, ${JSON.stringify(userLabel)}).module as LayoutModule;
|
|
1110
|
+
const UserLayout = userLayout.default as (props: LayoutProps) => ReactNode | Promise<ReactNode>;
|
|
1111
|
+
` : `const UserLayout = ({ children }: LayoutProps) => children;
|
|
1112
|
+
const userLayout: LayoutModule = {};
|
|
1106
1113
|
`;
|
|
1107
1114
|
const isAsyncUserLayout = opts.boundary.sourceAbsPath ? await AsyncDefaultExportDetector.detect(opts.boundary.sourceAbsPath) : false;
|
|
1108
1115
|
const userLayoutElement = "<UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>";
|
|
@@ -1114,8 +1121,9 @@ const userLayout = {};
|
|
|
1114
1121
|
` : "";
|
|
1115
1122
|
const layoutChild = isAsyncUserLayout ? "{layout}" : userLayoutElement;
|
|
1116
1123
|
const layoutReturn = isAsyncUserLayout ? "layout" : userLayoutElement;
|
|
1117
|
-
const source = opts.includeSystemProvider ? `import type { LayoutProps, PageProps } from "akanjs/client";
|
|
1118
|
-
import { loadFonts } from "akanjs/client";
|
|
1124
|
+
const source = opts.includeSystemProvider ? `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
|
|
1125
|
+
import { loadFonts, resolveRouteModule } from "akanjs/client";
|
|
1126
|
+
import type { ReactNode } from "react";
|
|
1119
1127
|
import { System } from "akanjs/ui";
|
|
1120
1128
|
import { env } from "@apps/${opts.appName}/env/env.client";
|
|
1121
1129
|
import { allDictionary } from ${JSON.stringify(dictMacroSpecifier)};
|
|
@@ -1166,7 +1174,9 @@ ${layoutBinding} return (
|
|
|
1166
1174
|
</System.Provider>
|
|
1167
1175
|
);
|
|
1168
1176
|
}
|
|
1169
|
-
` : `import type { LayoutProps, PageProps } from "akanjs/client";
|
|
1177
|
+
` : `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
|
|
1178
|
+
import { resolveRouteModule } from "akanjs/client";
|
|
1179
|
+
import type { ReactNode } from "react";
|
|
1170
1180
|
${inheritedImport}${userImport}
|
|
1171
1181
|
export async function generateHead(props: PageProps) {
|
|
1172
1182
|
if (userLayout.generateHead) return userLayout.generateHead(props);
|
|
@@ -3245,7 +3255,7 @@ class SysExecutor extends Executor {
|
|
|
3245
3255
|
class AppExecutor extends SysExecutor {
|
|
3246
3256
|
static #routeSourceValidator = null;
|
|
3247
3257
|
static async#getRouteSourceValidator() {
|
|
3248
|
-
AppExecutor.#routeSourceValidator ??= (await import("./routeSourceValidator-
|
|
3258
|
+
AppExecutor.#routeSourceValidator ??= (await import("./routeSourceValidator-apyqw7wk.js")).RouteSourceValidator;
|
|
3249
3259
|
return AppExecutor.#routeSourceValidator;
|
|
3250
3260
|
}
|
|
3251
3261
|
dist;
|
|
@@ -3419,7 +3429,8 @@ class AppExecutor extends SysExecutor {
|
|
|
3419
3429
|
validator.validateOverridesSourceExports(routeSource, absPath);
|
|
3420
3430
|
else {
|
|
3421
3431
|
const info = validator.validateRouteSourceExports(routeSource, absPath, parsed.kind, {
|
|
3422
|
-
rootLayout: isRootLayout
|
|
3432
|
+
rootLayout: isRootLayout,
|
|
3433
|
+
pattern: parsed.pattern
|
|
3423
3434
|
});
|
|
3424
3435
|
if (info.devOnly) {
|
|
3425
3436
|
devOnlyKeys.add(key);
|
|
@@ -3,22 +3,22 @@ import {
|
|
|
3
3
|
GlobalConfig,
|
|
4
4
|
CloudApi,
|
|
5
5
|
AiSession
|
|
6
|
-
} from "./index-
|
|
6
|
+
} from "./index-kn1h7p1x.js";
|
|
7
7
|
import {
|
|
8
8
|
PkgExecutor
|
|
9
|
-
} from "./index-
|
|
9
|
+
} from "./index-2tv3fdx4.js";
|
|
10
10
|
import {
|
|
11
11
|
script
|
|
12
|
-
} from "./index-
|
|
12
|
+
} from "./index-mak3cd28.js";
|
|
13
13
|
import {
|
|
14
14
|
ApplicationScript
|
|
15
|
-
} from "./index-
|
|
15
|
+
} from "./index-yecdrt9a.js";
|
|
16
16
|
import {
|
|
17
17
|
PackageScript
|
|
18
|
-
} from "./index-
|
|
18
|
+
} from "./index-ws5e0j51.js";
|
|
19
19
|
import {
|
|
20
20
|
CloudRunner
|
|
21
|
-
} from "./index-
|
|
21
|
+
} from "./index-yw4xj7pa.js";
|
|
22
22
|
|
|
23
23
|
// pkgs/@akanjs/cli/cloud/cloud.script.ts
|
|
24
24
|
import { Logger } from "akanjs/common";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
script
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-mak3cd28.js";
|
|
5
5
|
import {
|
|
6
6
|
AppExecutor,
|
|
7
7
|
LibExecutor,
|
|
8
8
|
ModuleExecutor
|
|
9
|
-
} from "./index-
|
|
9
|
+
} from "./index-2tv3fdx4.js";
|
|
10
10
|
import {
|
|
11
11
|
compactDiagnostics,
|
|
12
12
|
createPrimitiveWriteReport,
|
|
@@ -39,10 +39,10 @@ import {
|
|
|
39
39
|
hasSignalFactoryEntry,
|
|
40
40
|
insertSignalFactoryEntry,
|
|
41
41
|
addFieldUiPolicyForType
|
|
42
|
-
} from "./index-
|
|
42
|
+
} from "./index-gyd220p2.js";
|
|
43
43
|
import {
|
|
44
44
|
ModuleScript
|
|
45
|
-
} from "./index-
|
|
45
|
+
} from "./index-8pm30m73.js";
|
|
46
46
|
|
|
47
47
|
// pkgs/@akanjs/cli/primitive/primitive.script.ts
|
|
48
48
|
import { capitalize } from "akanjs/common";
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
AppExecutor
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-2tv3fdx4.js";
|
|
5
5
|
import {
|
|
6
6
|
jsonText,
|
|
7
7
|
createWorkflowBaselineSummary,
|
|
8
8
|
buildAkanModuleContextIndex,
|
|
9
9
|
createWorkflowStepRegistry,
|
|
10
10
|
toolingRolloutGate
|
|
11
|
-
} from "./index-
|
|
11
|
+
} from "./index-gyd220p2.js";
|
|
12
12
|
import {
|
|
13
13
|
buildResourceList,
|
|
14
14
|
codexMcpConfigPath,
|
|
@@ -19,33 +19,33 @@ import {
|
|
|
19
19
|
upsertCodexMcpServerBlock,
|
|
20
20
|
renderDoctorText,
|
|
21
21
|
AkanContextAnalyzer
|
|
22
|
-
} from "./index-
|
|
22
|
+
} from "./index-ktb5njgy.js";
|
|
23
23
|
import {
|
|
24
24
|
CommandContainer,
|
|
25
25
|
runner,
|
|
26
26
|
script
|
|
27
|
-
} from "./index-
|
|
27
|
+
} from "./index-mak3cd28.js";
|
|
28
28
|
import {
|
|
29
29
|
isPlaceholderAppId2
|
|
30
|
-
} from "./index-
|
|
30
|
+
} from "./index-wt7qrtd4.js";
|
|
31
31
|
import {
|
|
32
32
|
Prompter
|
|
33
33
|
} from "./index-h7yw2z9n.js";
|
|
34
34
|
import {
|
|
35
35
|
RepairRunner
|
|
36
|
-
} from "./index-
|
|
36
|
+
} from "./index-fz5q3gd7.js";
|
|
37
37
|
import {
|
|
38
38
|
WorkflowRunner
|
|
39
|
-
} from "./index-
|
|
39
|
+
} from "./index-7737gxcp.js";
|
|
40
40
|
import {
|
|
41
41
|
ModuleScript
|
|
42
|
-
} from "./index-
|
|
42
|
+
} from "./index-8pm30m73.js";
|
|
43
43
|
import {
|
|
44
44
|
PrimitiveScript
|
|
45
|
-
} from "./index-
|
|
45
|
+
} from "./index-4edzf1gr.js";
|
|
46
46
|
import {
|
|
47
47
|
ScalarScript
|
|
48
|
-
} from "./index-
|
|
48
|
+
} from "./index-rwg2aa4f.js";
|
|
49
49
|
import {
|
|
50
50
|
getMobileTargets
|
|
51
51
|
} from "./index-47pj3aee.js";
|
|
@@ -20,13 +20,13 @@ import {
|
|
|
20
20
|
renderWorkflowApply,
|
|
21
21
|
renderWorkflowValidation,
|
|
22
22
|
renderWorkflowRunArtifact
|
|
23
|
-
} from "./index-
|
|
23
|
+
} from "./index-gyd220p2.js";
|
|
24
24
|
import {
|
|
25
25
|
AkanContextAnalyzer
|
|
26
|
-
} from "./index-
|
|
26
|
+
} from "./index-ktb5njgy.js";
|
|
27
27
|
import {
|
|
28
28
|
runner
|
|
29
|
-
} from "./index-
|
|
29
|
+
} from "./index-mak3cd28.js";
|
|
30
30
|
|
|
31
31
|
// pkgs/@akanjs/cli/workflow/workflow.runner.ts
|
|
32
32
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
@@ -4,14 +4,14 @@ import {
|
|
|
4
4
|
} from "./index-q0y2jq9j.js";
|
|
5
5
|
import {
|
|
6
6
|
AiSession
|
|
7
|
-
} from "./index-
|
|
7
|
+
} from "./index-kn1h7p1x.js";
|
|
8
8
|
import {
|
|
9
9
|
ModuleExecutor
|
|
10
|
-
} from "./index-
|
|
10
|
+
} from "./index-2tv3fdx4.js";
|
|
11
11
|
import {
|
|
12
12
|
runner,
|
|
13
13
|
script
|
|
14
|
-
} from "./index-
|
|
14
|
+
} from "./index-mak3cd28.js";
|
|
15
15
|
import {
|
|
16
16
|
sourceFile,
|
|
17
17
|
moduleSourcePaths,
|
|
@@ -19,10 +19,10 @@ import {
|
|
|
19
19
|
createPassedPrimitiveReport,
|
|
20
20
|
bilingualLabelForField,
|
|
21
21
|
bilingualDescriptionForField
|
|
22
|
-
} from "./index-
|
|
22
|
+
} from "./index-gyd220p2.js";
|
|
23
23
|
import {
|
|
24
24
|
PageScript
|
|
25
|
-
} from "./index-
|
|
25
|
+
} from "./index-pnqbkf5p.js";
|
|
26
26
|
import {
|
|
27
27
|
Prompter
|
|
28
28
|
} from "./index-h7yw2z9n.js";
|
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
writeGeneratedSyncState,
|
|
6
6
|
createRepairReport,
|
|
7
7
|
renderRepairReport
|
|
8
|
-
} from "./index-
|
|
8
|
+
} from "./index-gyd220p2.js";
|
|
9
9
|
import {
|
|
10
10
|
AkanContextAnalyzer
|
|
11
|
-
} from "./index-
|
|
11
|
+
} from "./index-ktb5njgy.js";
|
|
12
12
|
import {
|
|
13
13
|
runner
|
|
14
|
-
} from "./index-
|
|
14
|
+
} from "./index-mak3cd28.js";
|
|
15
15
|
|
|
16
16
|
// pkgs/@akanjs/cli/repair/repair.runner.ts
|
|
17
17
|
var commandForShell = (command) => command.startsWith("akan ") ? `bun run ${command}` : command;
|