@akanjs/cli 3.1.0-rc.20 → 3.1.0-rc.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.build-stamp +1 -1
  2. package/{CodeAgent-n3tzjm91.js → CodeAgent-fee0pnwp.js} +2 -2
  3. package/{CodeTui-fnm9ft4d.js → CodeTui-xh71b2k8.js} +1 -1
  4. package/TunnelShare-s7bpe55e.js +14 -0
  5. package/{agent.command-3qxf4xcj.js → agent.command-tge9rvmt.js} +5 -5
  6. package/akanCode.js +2 -2
  7. package/{application.command-zfja7x4r.js → application.command-j5b8yrcf.js} +35 -9
  8. package/{applicationBuildRunner-vh4j6vdv.js → applicationBuildRunner-s41mgz21.js} +21 -7
  9. package/buildBatch.proc.js +9 -9
  10. package/{cloud.command-fcc8e8tp.js → cloud.command-q9srng6e.js} +13 -13
  11. package/cloud.script-vfsd4gxs.js +24 -0
  12. package/{code.command-130bdmhe.js → code.command-n6r6zt97.js} +6 -6
  13. package/commandManifest.json +1 -1
  14. package/{context.command-0qx447mb.js → context.command-1je84w1b.js} +16 -16
  15. package/context.runner-xg0ngz93.js +25 -0
  16. package/{guideline.command-cv5bmrx2.js → guideline.command-0e0sx2vg.js} +2 -2
  17. package/guidelines/conventions/conventions.instruction.md +42 -13
  18. package/guidelines/mcpRule/mcpRule.instruction.md +88 -47
  19. package/guidelines/pageRule/pageRule.instruction.md +53 -8
  20. package/guidelines/runtimeRule/runtimeRule.instruction.md +8 -1
  21. package/guidelines/ssrRule/ssrRule.instruction.md +8 -5
  22. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +1 -1
  23. package/incrementalBuilder.proc.js +3 -3
  24. package/{index-fmzcs8v6.js → index-1bhv9dh4.js} +4 -4
  25. package/{index-qtfmtcrw.js → index-1xqq3va9.js} +71 -18
  26. package/{index-86jt2a0e.js → index-3xhak9e0.js} +1 -1
  27. package/{index-tamvyvj3.js → index-4fe4qqgn.js} +3 -3
  28. package/{index-g1mbppa9.js → index-551gh8h2.js} +5 -5
  29. package/{index-trpasv9q.js → index-5m0s0gdv.js} +1 -1
  30. package/index-6mprvdt8.js +566 -0
  31. package/{index-192ha4xh.js → index-6nqnqzv1.js} +1 -1
  32. package/{index-dye8pnxx.js → index-73esn3y2.js} +1 -1
  33. package/{index-4mhy6qme.js → index-7xy30jev.js} +4 -4
  34. package/{index-1xj2t7cb.js → index-85h3dhe0.js} +7 -7
  35. package/{index-axs61rna.js → index-8abyrfw8.js} +5 -4
  36. package/{index-1p98exm2.js → index-94jq9cbz.js} +2 -2
  37. package/{index-cpb3nvma.js → index-9h0z0tjr.js} +2 -2
  38. package/{index-hsadfb6c.js → index-c9rfak6r.js} +7 -7
  39. package/{index-w44cy4vs.js → index-cgyqx6q1.js} +2 -2
  40. package/{index-d72hfmdm.js → index-cq5x7a9k.js} +8 -8
  41. package/{index-j237xtew.js → index-deckcwbg.js} +2 -2
  42. package/{index-vk0pa0mn.js → index-ex35dg39.js} +2 -1
  43. package/{index-35hc95h5.js → index-f9118j70.js} +1 -1
  44. package/{index-jw1zm273.js → index-g5px5pxd.js} +53 -9
  45. package/{index-va06nr10.js → index-gxqvev8w.js} +52 -12
  46. package/{index-fyq1z6kt.js → index-hagw9c94.js} +3 -3
  47. package/{index-pydbnfez.js → index-p44jnjfr.js} +2 -2
  48. package/{index-awbdfcf2.js → index-rp9anyeb.js} +4 -4
  49. package/{index-j4mkhz93.js → index-t8npm331.js} +3 -3
  50. package/{index-5edzv8fn.js → index-tbmtmkhx.js} +2 -2
  51. package/{index-1djcjcre.js → index-vyh8kemm.js} +3 -3
  52. package/{index-qwa5k1j8.js → index-x7bny3wv.js} +1 -1
  53. package/index.js +22 -22
  54. package/{library.command-erp2f56n.js → library.command-ys9s0mtc.js} +3 -3
  55. package/{localRegistry.command-hj3p16p5.js → localRegistry.command-kmp3sd47.js} +12 -12
  56. package/{module.command-gs2r615e.js → module.command-5sx9453y.js} +5 -5
  57. package/{package.command-j10r76ax.js → package.command-s0d3417k.js} +3 -3
  58. package/package.json +2 -2
  59. package/{page.command-4h4a8pnh.js → page.command-zsa03pmc.js} +3 -3
  60. package/{pipeline.command-a2nrk26m.js → pipeline.command-j8h6qsha.js} +2 -2
  61. package/{primitive.command-w6t0asqz.js → primitive.command-n2978nmw.js} +6 -6
  62. package/{projectTypecheck-f534enqj.js → projectTypecheck-qg2jf80j.js} +1 -1
  63. package/{quality.command-77x7vbbx.js → quality.command-7kgz46st.js} +2 -2
  64. package/{repair.command-y6sstev7.js → repair.command-2sszcckw.js} +5 -5
  65. package/{routeSourceValidator-7ycnfz0e.js → routeSourceValidator-6jd0q6r6.js} +47 -14
  66. package/{scalar.command-20b8cy0k.js → scalar.command-230jrhzf.js} +4 -4
  67. package/{subspace.command-baz73jpk.js → subspace.command-y5v4ypd3.js} +8 -8
  68. package/templates/migration/__migrationId__.ts +23 -0
  69. package/templates/server.ts +8 -1
  70. package/{tunnel.command-2kw8vt9g.js → tunnel.command-ja29zghd.js} +7 -7
  71. package/{workflow.command-2djwmpqf.js → workflow.command-0eg8czt5.js} +10 -10
  72. package/{workspace.command-jqs7vjhe.js → workspace.command-zcym1dk5.js} +24 -24
  73. package/TunnelShare-661cxz5e.js +0 -14
  74. package/cloud.script-1xt3tnts.js +0 -24
  75. package/context.runner-1vsjztjg.js +0 -25
  76. package/index-665zp9rs.js +0 -271
@@ -40,9 +40,9 @@ export class FileSlice extends slice(srv.file, {
40
40
  `update`, `remove`), same fallbacks (`create`/`update`/`remove` inherit `cru`), and the same scope: it narrows
41
41
  the root slice and the generated CRUD, and never reaches a named slice or a custom endpoint. Those declare
42
42
  their own `mcp` beside their own `guards`. The bare `mcp: false` is every key at once, still within that scope.
43
- - **A screen keeps reaching it.** To a host that renders MCP Apps views, an endpoint refused for `mcp: false` alone
44
- is published with `visibility: ["app"]`: the view the page tool opens may call it, the model is never shown it
45
- (below). Every other refusal still holds there.
43
+ - **A view keeps reaching it.** To a host that renders MCP Apps views, an endpoint refused for `mcp: false` alone
44
+ is published with `visibility: ["app"]`: an app's view may call it, the model is never shown it (below). Every
45
+ other refusal still holds there.
46
46
  - **`mcp: true` is the default and is a no-op**; write it only to say out loud that an entry is deliberately on
47
47
  the shelf. It does not override a shape refusal (an `Any` return stays refused) and it cannot publish something
48
48
  the guard rules keep out.
@@ -203,7 +203,8 @@ export default page()
203
203
  - The `readOnly` / `destructive` / `idempotent` hints a client renders are derived from the endpoint type and key
204
204
  and are not configurable. Clients are told to distrust hints; they are never a gate.
205
205
  - **`AKAN_MCP_READONLY=true` is the read-only-deployment valve, not the exposure switch.** It drops every mutation
206
- whatever it declared, and reports each one in the boot log like any other refusal.
206
+ whatever it declared, and reports each one in the boot log like any other refusal. It also drops each `.prompt()`
207
+ page's text tool, whose run renders the whole page; the prompts, the `.app()` tools and their views stay.
207
208
  - OAuth resource metadata is published at `/.well-known/oauth-protected-resource` (and at that path plus the mount
208
209
  path, the spelling most clients try first). `auth.authorizationServers` / `AKAN_MCP_AUTH_SERVERS`,
209
210
  `auth.scopes` / `AKAN_MCP_SCOPES`, `auth.resource` / `AKAN_MCP_RESOURCE` and `auth.verify` configure it;
@@ -248,9 +249,9 @@ export default page()
248
249
  wire-identical over the POST-only surface this implements — a client whose proposal is not listed is told to
249
250
  disconnect. An unknown proposal is answered at whichever end of that list it is closer to, and an unimplemented
250
251
  method answers `404` to a modern client but `200` to a legacy one, whose era spends `404` on "your session is
251
- gone". **While a `.prompt()` page may show as a view only the legacy pair is spoken**: `server/discover` answers
252
+ gone". **While an `.app()` may show as a view only the legacy pair is spoken**: `server/discover` answers
252
253
  `-32601` and `initialize` settles on `2025-11-25`, because claude.ai renders a view only for a server it reached
253
- through `initialize`. An app that renders no view, or whose RSC worker lists no `.prompt()` page, speaks all three.
254
+ through `initialize`. An app that renders no view, or whose RSC worker lists no `.app()`, speaks all three.
254
255
  `modern: true` / `AKAN_MCP_MODERN=true` offers `2026-07-28` anyway; `false` withholds it everywhere.
255
256
  - **A modern-era request mirrors `MCP-Protocol-Version` and `Mcp-Method` into headers** (plus `Mcp-Name` when the
256
257
  body names one), and one that leaves a mirror out is refused just like one that contradicts the body: a gateway
@@ -349,8 +350,10 @@ delegates to lives in `akanjs/server` (`OAuthAuthorize`, `OAuthToken`, `OAuthReg
349
350
  board of one project.").render(…)` — and that is the whole surface. `prompts/list` is every page carrying
350
351
  `.prompt()`; its `.param()` stages are the required arguments and its `.search()` stages the optional ones, a list
351
352
  argument is comma-separated on the wire and its description says so, and an `ID`/`Int`/`enumOf` value is checked by
352
- the page's own declaration. **`prompts/get` runs the page's body** — root layouts, layouts, then the page's
353
- `.render()` — in the RSC worker under the caller's bearer token, the way a tab would open it. Nothing is rendered:
353
+ the page's own declaration. `prompts/list` shows only the screens whose route `.guard()`s admit the caller; a listing
354
+ knows no arguments, so a guard that reads one admits there and is checked again on the call. **`prompts/get` runs the
355
+ route's guards, then the page's body** — root layouts, layouts, then the page's `.render()` — in the RSC worker under
356
+ the caller's bearer token, the way a tab would open it. Nothing is rendered:
354
357
  the JSX comes back as an element tree nobody walks, no client component runs, and the screen's data footprint is
355
358
  exactly the `fetch.*` queries its body made, with the arguments it made them with. The answer is the description as
356
359
  the first message (priority 1), one embedded `resource` per query masked by that endpoint's return model and
@@ -366,74 +369,109 @@ pass. A layout's own queries (a project selector's `projectListInOrg`) are part
366
369
  attached too. **No argument fallback**: a required argument left out answers one message — `No projectId was named for
367
370
  "briefProjectTickets". Find it with \`projectListBySearch\`, then run this prompt again with projectId=<id>.` —
368
371
  and runs nothing, because a prompt cannot re-run itself and context filled in on a guess would never be used. A
369
- page that redirects (`getSelf({ unauthorize })`), or whose `fetch.*` a guard refuses with 401/403, answers the 401
372
+ page that redirects (a `.guard()`, `getSelf({ unauthorize })`), or whose `fetch.*` a guard refuses with 401/403, answers the 401
370
373
  credential challenge when the call carried no token and `This screen is not available to the signed-in account.`
371
374
  when it did — the same sentence for both, so a refused id is never confirmed to exist; `router.notFound()` and a
372
375
  404 from a query are `No screen exists for these arguments.`; anything else the body throws is `The page failed to
373
376
  load.`, with the cause in the server log only. **The description is the only instruction.** `<Agent.Guide>` is for the in-page agent, whose
374
377
  levers are the screen's controls, and is never read here, where the levers are the API — so write the description
375
378
  in English, in the API's vocabulary, and put a computed rule (the legal status transitions) into it as a static
376
- table the model combines with the attached state. Names match `^[A-Za-z0-9_-]{1,64}$` and are unique across pages;
377
- two pages claiming one name fail the listing. An API-only build (`web: false`) publishes no prompts, and the
379
+ table the model combines with the attached state. Names match `^[A-Za-z0-9_-]{1,64}$` and share one namespace with
380
+ every page's `.app()`s; two pages claiming one name fail the listing. An API-only build (`web: false`) publishes no prompts, and the
378
381
  in-page chat lists none of them: its `/` menu is its six built-in commands. Pick which screens to publish by this
379
382
  rule: a page taking no arguments is the most useful prompt, since MCP has no completion — one taking an id is for
380
383
  when the id is already in hand.
381
384
 
382
- ## MCP Apps — The Page, Rendered In The Chat
383
- **A `.prompt()` page is also a tool**, of the same name and arguments, marked `readOnlyHint: false`, since running
384
- the page makes its own `fetch.*` calls, mutations included. Its result is the
385
- prompt's briefing plus `Open this screen in a browser: <url>` — what a client without the
386
- `io.modelcontextprotocol/ui` extension (Claude Code, Codex) reads. A host that declares the extension (claude.ai,
387
- Claude Desktop, ChatGPT, VS Code, Goose) is also given `_meta.ui.resourceUri` (`ui://<app>/app`, or
388
- `ui://<app>/app/<basePath>`) and renders the screen in place of the result: the app's CSR bundle, one HTML document,
389
- in a sandboxed iframe started on the page the tool ran. Its first queries are answered from that run — carried in
390
- the result's `_meta`, the one part no host shows the model — so the first paint costs no round trip; every later
391
- call is relayed by the host as `tools/call` under the host's token. Nothing is declared for any of it.
385
+ ## MCP Apps — One Component Of A Page, Drawn In The Chat
386
+ **`page().app(name, description, load)` publishes one component of a screen.** `load` takes the page's typed
387
+ arguments and `lang` and returns the element — usually a `Zone` fed by unawaited `fetch.*` fields — and on the web that
388
+ element reaches `.render()` as the argument named `name`, so the page draws it where it belongs:
392
389
 
390
+ ```tsx
391
+ export default page()
392
+ .param("ticketId", ID, { desc: "The ticket to show." })
393
+ .app("showTicket", "Show one ticket with its status and assignee.", ({ ticketId }) => {
394
+ const { ticketView } = fetch.viewTicket(ticketId);
395
+ return <Ticket.Zone.View view={ticketView} />;
396
+ })
397
+ .render(({ ticketId, showTicket }) => (
398
+ <div className="flex flex-col gap-4">
399
+ {showTicket}
400
+ <Ticket.Util.Close ticketId={ticketId} />
401
+ </div>
402
+ ));
403
+ ```
404
+
405
+ - **Each app is a read-only tool** of its name, whose arguments are the page's `.param()` (required) and `.search()`
406
+ (optional) stages, marked `readOnlyHint: true`. A call checks the arguments, runs the route's `.guard()`s, then the
407
+ app's `load` alone — no layout body, no `.render()` — and answers the briefing a prompt would give for what that load
408
+ fetched, plus `Open the full screen: <url>`: what a client without the `io.modelcontextprotocol/ui` extension
409
+ (Claude Code, Codex) reads. A refusal is a tool error with the prompt's sentences. Several apps may sit on one page;
410
+ names share one namespace with the prompts, and an endpoint keeps its name over an app's, with a boot warning.
411
+ - **`load` may only query.** A mutation, a file upload, a room subscription or a message sent while it runs throws
412
+ `base.error.mutationInApp` before anything is sent — on the server through every `await`, in a browser for its
413
+ synchronous part. Writes stay in `.render()` around the element (the `Close` above), and gating stays in
414
+ `.guard()`: a `getSelf({ unauthorize })` inside `load` would redirect a view.
415
+ - **A host that declares the extension** (claude.ai, Claude Desktop, ChatGPT, VS Code, Goose) is also given
416
+ `_meta.ui.resourceUri` (`ui://<app>/apps/<name>`) and draws the app's own view in place of the result: one HTML
417
+ document per app, which `akan build` writes to `mcp-apps/<name>.html` beside `csr/` and `akan start` writes to
418
+ `.akan/artifact/mcp-apps/` when a host first reads it. The view runs `load` again with the route values the result
419
+ names in `structuredContent`, and its first queries are answered from that call — carried in the result's `_meta`,
420
+ the one part no host shows the model — so the first paint costs no round trip; every later call is relayed by the
421
+ host as `tools/call` under the host's token.
422
+ - **The view is the component, not the page.** It bundles the page with `.render()`, `.head()`, `.loading()` and
423
+ `.guard()` blanked, so it carries what the loads reach and nothing else, and draws the element inside the
424
+ `_overrides.tsx` over the page and the root layout's `McpAppRoot` (`System.App`: its theme, fonts and providers, with
425
+ no frame, router or websocket). An `_overrides.tsx` the web draws outside the root layout (on its folder, or on one
426
+ above it with no layout of its own) wraps `McpAppRoot`, so the toasts and approvals it draws take it too. No layout
427
+ body runs there, so a layout's `<Agent.Guide>` never reaches the host; one inside the element does. A view never
428
+ moves: a link or `router.push` to one of the app's screens opens that screen's web page through the host (basePath
429
+ kept), a fragment of its own screen scrolls it, and `router.back()` / `router.refresh()` do nothing. A host loads one
430
+ file, so every `lazy(() => import(…))` the loads render is inlined; the view build marks a `const X = lazy(…)` pure,
431
+ so a barrel's, the page's own that only `.render()` drew, or an override manifest's that nothing renders is dropped,
432
+ while a `lazy()` called anywhere else stays.
393
433
  - **What a view may call** is every published tool, and every endpoint kept off the shelf by `mcp: false` alone or
394
434
  the generated `light<Model>` read, listed with `visibility: ["app"]` — which a host offers to the view and never to
395
435
  the model; a `Person` guard still refuses one. Visibility is a host's UX, not authorization: the guards decide, as
396
436
  everywhere. A client that did not declare the extension is listed neither those tools nor the `ui://` resources, a
397
- call to one answers `Unknown tool`, and a page tool's result carries none of the view's data for it.
437
+ call to one answers `Unknown tool`, and an app tool's result carries none of the view's data for it.
398
438
  - **The capability is read per request.** A legacy client declares it once at `initialize` and is issued an
399
439
  `Mcp-Session-Id` (`ui.<nonce>.<signature>`) that stores nothing: the app signs it, and an id the app did not sign is
400
440
  served as a client without views. With `modern: true` a modern request carries the capability in `_meta`. A client
401
441
  that declares nothing is issued no session, exactly as before.
402
- - **A read-only deployment has no MCP Apps.** With `readOnly` / `AKAN_MCP_READONLY` there are no page tools, app-only
403
- tools, views or UI sessions, since running a page makes its own `fetch.*` calls, mutations included.
404
- - **The view is the page, not the app around it.** Each layout above it still runs — guards, providers,
405
- `<Agent.Guide>` — but what it draws around `{children}` is hidden, with its frame chrome (`Layout.Navbar`,
406
- `TopInset`, `BottomInset`, `BottomTab`), the `topInset` / `bottomInset` it reserves and the tools its controls
407
- publish. A layout that draws no page, like a gate, is the screen then and is shown. The screen the view opened on
408
- has no back button: there is nothing behind it.
442
+ - **A read-only deployment keeps its apps.** With `readOnly` / `AKAN_MCP_READONLY` the app tools, their resources and
443
+ views stay, since a load may only query, and the result says so in `_meta`; the view refuses a write before it
444
+ reaches the host with `base.error.readOnlyDeployment`, toasted in the person's language. A `.prompt()` page's text
445
+ tool is dropped, since its run renders the whole page; the prompt stays.
446
+ - **`.prompt()` is a briefing, never a view.** Its page is also a text tool of the same name, marked
447
+ `readOnlyHint: false`, since running the page makes its own `fetch.*` calls, mutations included. To have a host draw
448
+ part of that screen, declare an `.app()` beside it.
409
449
  - **The view has no network.** `connect-src` stays closed; the app's own origin is opened for what a page loads by
410
450
  URL — fonts (served with `Access-Control-Allow-Origin: *`) and stored images — and every root-relative URL the
411
451
  page writes is pointed at it, since a host may block the document's `<base>` (Claude does).
412
- Cookies and Web Storage are in-memory, history is in-memory, and the websocket, live sync, pubsub, the reconnect
413
- overlay and `<Agent.Chat />` are off. A path link routes inside the view; any other link opens through the host.
452
+ Cookies and Web Storage are in-memory, and the websocket, live sync, pubsub, the reconnect overlay and
453
+ `<Agent.Chat />` are off.
414
454
  - **The view looks like the host.** Its neutrals (`--background`, `--foreground`, `--muted`, `--border`, `--ring`) and
415
455
  `--font-sans` / `--font-mono` follow the host's theme variables, its theme follows the host's, and so does its
416
456
  language when the app speaks it; the app's brand colors and radii stay its own.
417
- - **The host's model drives the screen too.** The view offers the host the screen's own tools — the surface
418
- `<Agent.Chat />` reads: `st.tool`, the form setters, `fill<Model>Form`, `navigate` / `readScreen` / `readState` /
419
- `highlight` — and, to a host that takes it, the screen's context (route, live state, `<Agent.Guide>`) through
457
+ - **The host's model drives the component too.** The view offers the host the tools the element publishes — the
458
+ surface `<Agent.Chat />` reads: `st.tool`, the form setters, `fill<Model>Form`, `readScreen` / `readState` /
459
+ `highlight` — and, to a host that takes it, the view's context (route, live state, `<Agent.Guide>`) through
420
460
  `ui/update-model-context` whenever it moves. A tool that declares `confirm` asks the person in the view through
421
461
  the `AgentApproval` slot, whatever the host asks on its own; a card tool, which a person answers in a chat, is not
422
462
  offered.
423
463
  - **A person tapping in the view acts with the agent's token**, so `isAgentCall` is true and a `Person` act is
424
464
  refused there. That act belongs on the web page the result links to. The view holds no token: it is handed the
425
- caller's claims, so `getSelf({ unauthorize })` in a layout renders the screen instead of a sign-in page, and each
426
- call it makes is relayed and judged by the guards.
465
+ caller's claims, and each call it makes is relayed and judged by the guards.
427
466
  - **A view's call answers what a browser would get**, in `_meta` beside the masked result: the value with its
428
467
  `visual` fields, or the domain `Err` with its key and data, toasted in the user's language. A refusal and a crash
429
- stay generalized.
430
- - **What a document load would do, the view does in place.** `router.refresh()` renders the route afresh, a
431
- download (`<a download>`, a data table's export) goes to a host that saves files, and a full navigation off the
432
- app shows a notice instead of a blank frame.
433
- - **A page named like an endpoint stays a prompt only**, published or not, and the boot log says so: the endpoint
434
- keeps its name. A refused screen answers as a tool error (`isError`), which the model reads.
435
- - `option.setMcp({ apps: false })` / `AKAN_MCP_APPS=false` keeps every page a prompt only. `web: { csr: false }`
436
- publishes the tool with no view, and an API-only build (`web: false`) has no pages to publish.
468
+ stay generalized. A load that throws, or a read that fails while the element draws, shows what the call would have
469
+ answered: `No screen exists for these arguments.` for a 404 (a record removed since the call), `This screen is not
470
+ available to the signed-in account.` for a 401 or 403, and `The page failed to load.` for anything else; a download
471
+ (`<a download>`, a data table's export) goes to a host that saves files.
472
+ - `option.setMcp({ apps: false })` / `AKAN_MCP_APPS=false` publishes no page tools: no app is published and every
473
+ prompt stays a prompt only. `web: { csr: false }` publishes the app tools with no view, and an API-only build
474
+ (`web: false`) has no pages to publish.
437
475
 
438
476
  ## Progress Reporting
439
477
  **`McpProgress.report(n, { total, message })`** reports progress from anywhere inside a call, a service or adapter
@@ -503,9 +541,9 @@ the same guards.** What stays closed is closed because no agent has a reason to
503
541
 
504
542
  ## Writing `instructions`
505
543
 
506
- `*.abstract.md` never reaches an MCP client, and `<Agent.Guide>` only through an MCP Apps view, beside the screen's own
507
- tools it explains. Exactly three texts reach every client: `instructions`, every published entry's `.desc()`, and a
508
- page's `.prompt()` description. All three are English, whatever the users speak
544
+ `*.abstract.md` never reaches an MCP client, and `<Agent.Guide>` only through an MCP App's view, from inside the app's
545
+ element, beside the tools it explains. Exactly these texts reach every client: `instructions`, every published entry's
546
+ `.desc()`, and a page's `.prompt()` and `.app()` descriptions. All are English, whatever the users speak
509
547
  — the reader is a model, and the catalogue is built once in one `language` and cached by clients.
510
548
 
511
549
  - **`instructions` is the workflow.** Five to eight sentences, in this order: what the app is and what its nouns
@@ -529,6 +567,9 @@ page's `.prompt()` description. All three are English, whatever the users speak
529
567
  report, an onboarding check. Its instruction is the `.prompt()` description alone and its attachments are what
530
568
  the screen fetched, masked by their return models — so to change what a prompt hands the model, change what the
531
569
  screen loads, and keep the description to the ask.
570
+ - **An app is a tool the model picks**, like a read endpoint: its description says what the component shows and for
571
+ which arguments (`Show one ticket with its status and assignee.`), and the model reads the briefing of what its load
572
+ fetched whether or not the host draws the view.
532
573
 
533
574
  ## Verifying An App
534
575
 
@@ -1,7 +1,7 @@
1
1
  # Page Rule — Route Files, The Route Chain, Heads, Lib Pages
2
2
 
3
- What a file under `page/` may be, how a route declares its arguments and settings, how heads merge, what a CSR
4
- bundle shows of a root layout, and how a lib's routes reach an app. `conventions` carries the invariants and the page
3
+ What a file under `page/` may be, how a route declares its arguments and settings, how a route guards itself, how
4
+ heads merge, what a CSR bundle shows of a root layout, and how a lib's routes reach an app. `conventions` carries the invariants and the page
5
5
  body shape — this is the full contract behind them. Before changing route behavior, check
6
6
  `pkgs/akanjs/server/routeTreeBuilder.ts` and nearby routes for the expected pattern.
7
7
 
@@ -25,15 +25,16 @@ body shape — this is the full contract behind them. Before changing route beha
25
25
 
26
26
  - **A route file exports one thing**: `export default page()…render(…)` for a page, `layout()…render(…)` for a
27
27
  `_layout.tsx`, `rootLayout()…render(…)` for the app's (or a basePath's) root `_layout.tsx`. Every route setting is
28
- a stage of the chain — `.param()`, `.search()`, `.config()`, `.head()`, `.loading()`; `.notFound()` / `.error()` on
29
- a layout; `.fonts()` / `.theme()` / `.manifest()` / `.reconnect()` / `.wsConnect()` / `.layoutStyle()` /
30
- `.viewport()` on the root layout; `.prompt()` on a page. A named export beside the chain, or `page()` in a
31
- `_layout.tsx`, fails the build.
28
+ a stage of the chain — `.param()`, `.search()`, `.config()`, `.guard()`, `.head()`, `.loading()`; `.notFound()` /
29
+ `.error()` on a layout; `.fonts()` / `.theme()` / `.manifest()` / `.reconnect()` / `.wsConnect()` /
30
+ `.layoutStyle()` / `.viewport()` on the root layout; `.prompt()` and `.app()` on a page. A named export beside the
31
+ chain, or `page()` in a `_layout.tsx`, fails the build.
32
32
  - **A page names every `[x]` segment of its path with `.param("x", Type)`** and reads a query key only through
33
33
  `.search("k", Type)`. Values arrive typed — `ID`/`String` → string, `Int`/`Float` → number, `Boolean`, `Date` →
34
34
  Dayjs, an `enumOf` class → its union, `[T]` → array — a path value the type refuses answers not-found, and a
35
35
  search value that fails is dropped. A layout may declare a subset. Names are string literals: `akan sync` reads
36
- them off the source and refuses a `[projectId]` folder whose page declares no `.param("projectId")`.
36
+ them off the source and refuses a `[projectId]` folder whose page declares no `.param("projectId")`, so the chain
37
+ is the `export default` expression itself — one bound to a variable first is refused.
37
38
  - **`lang` is never declared**: every route sits under `/:lang`, and the locale segment reaches every stage as
38
39
  `lang`.
39
40
  - **On a web page's first render `st.use.searchParams()` is still the server's empty snapshot**, synced right after
@@ -41,6 +42,50 @@ body shape — this is the full contract behind them. Before changing route beha
41
42
  - There is no `loader=` / `render=` page prop and no `PageProps` interface: the chain declares the arguments and
42
43
  `.render()` receives them flat, typed, with `lang` on every route and `children` added for a layout.
43
44
 
45
+ ## Route Guards — `.guard()`
46
+
47
+ - **`.guard(fn)` decides who may open a route before any of it renders.** Every chain takes it, once: a second
48
+ `.guard()` throws when the module loads. `fn` receives the route's typed arguments and `lang` (a layout's gets no
49
+ `children`) and may be `async`. It refuses by moving the router — `router.redirect(href)`, `router.notFound()` — or
50
+ with `getSelf({ unauthorize })`; `signedIn()` from `@libs/shared/webkit` is that last one, called inside an arrow
51
+ (`.guard(() => signedIn())`), because `.guard(signedIn)` would hand the route's arguments to `unauthorize`.
52
+ - **On the server every guard of the chain runs before any layout body starts**, root layout first and before the
53
+ route's arguments are checked, so a redirect is a real 307 even after an awaited query, and a not-found is a 404 —
54
+ where a `getSelf({ unauthorize })` inside a layout body after I/O redirects late, inside the stream. A query in a
55
+ guard that the server refuses with 403 or 404 (an owner check) answers not-found, so the page never confirms the
56
+ record exists. A refusal's not-found keeps only the layouts above the refusing guard, and a path no page matches
57
+ asks the guards of the layouts its not-found would sit under first.
58
+ - **On a client navigation a page's layers wait for its guards.** The verdict is kept by what the guards read (a
59
+ query only the page reads keeps it) and lasts while a layer of that location is on screen. Only the page in front
60
+ asks — never a parked one, nor the one under it during a swipe back; back in front, a cached page included, it asks
61
+ again in the background while what it drew stays, and a refusal moves the router and blanks it. A guard that moves
62
+ the router ends the chain and the page draws nothing; a refused or failed check runs again when its layer is
63
+ retried. Root layouts are drawn once for every page and are not gated
64
+ themselves; their guards still run in each page's chain, and a legacy page under them waits too. `guard` is a stage
65
+ only: a legacy module that exports one, a root layout's included, fails to load and fails `akan build`.
66
+ - **MCP runs them too**: a prompt or an app tool runs its route's guards before anything else, and `prompts/list` and
67
+ the page tools in `tools/list` show only the screens whose guards admit the caller (a listing knows no arguments,
68
+ so a guard that reads one admits there and is checked again on the call).
69
+ - **A route guard is not authorization for data.** It keeps a person off a screen and redirects early; every query
70
+ the screen makes is still judged by that endpoint's own `guards`. Write both: the signal guard is the gate, the
71
+ route guard is the courtesy that answers before the screen draws.
72
+
73
+ ## MCP Apps — `.app()`
74
+
75
+ - **`.app(name, description, load)` publishes one component of a page as an MCP App**: a read-only tool of that name,
76
+ whose arguments are the page's `.param()` and `.search()` stages, and a single-file view of its own. `load` receives
77
+ the page's typed arguments and `lang` and returns the element (usually a `Zone` fed by unawaited `fetch.*` fields);
78
+ on the web that element reaches `.render()` as the argument named `name`, so the page places it.
79
+ - A page may declare several. A name matches `^[A-Za-z0-9_-]{1,64}$`, is a string literal (the build reads it off the
80
+ source), shares one namespace with every page's prompts and apps, and needs an English description. A name
81
+ `.render()` already receives (`lang`, a `.param()` / `.search()` name, another app) fails to compile and throws when
82
+ the module loads, as does an app named like the page's `.prompt()`.
83
+ - **`load` may only query** (`base.error.mutationInApp` otherwise): keep writes in `.render()` around the element, and
84
+ gating in `.guard()`. The view runs `load` with no layout body and no `.render()`: it gets the root layout's theme,
85
+ fonts and framework providers through the generated `McpAppRoot` and the `_overrides.tsx` over the page (one the web
86
+ draws outside the root layout wraps `McpAppRoot`, its toasts included), and nothing a layout body provides. The
87
+ chain is the `export default` expression itself: one bound to a name fails the build. Full contract: `mcpRule`.
88
+
44
89
  ## Heads
45
90
 
46
91
  - **`.head()` takes JSX — `<title>`, `<meta>`, `<link>` — or a function of the route's args returning it.** One that
@@ -58,7 +103,7 @@ body shape — this is the full contract behind them. Before changing route beha
58
103
  second screen beside the frame. Its components still mount and their effects run (`<Auth.User />` works).
59
104
  - Anything a person must see from a root layout — an overlay, a gate, a banner — renders through
60
105
  `createPortal(…, document.body)`, as `<Agent.Chat />` and the dialogs already do. A non-root `_layout.tsx` has no
61
- such limit.
106
+ such limit. A gate belongs in the root layout's `.guard()`, which runs in every page's chain.
62
107
 
63
108
  ## Lib Pages — `syncPageLibs`
64
109
 
@@ -585,6 +585,8 @@ serving without a key. Everything else is behind the ops channel.
585
585
  | `GET /_akan/ops/info` | the public info plus `akanVersion`, `buildId`, `serverMode`, `databaseMode`, `solo`, `startedAt`, `replicaIdx` |
586
586
  | `POST /_akan/ops/snapshot` `{ id, includeSolid?, uploadUrls: { main, solid?, manifest } }` | 202; a child process snapshots, then PUTs each file and the manifest **last** |
587
587
  | `GET /_akan/ops/snapshot/:id` | `running` → `uploading` → `done` \| `failed`; kept in memory, so a restart answers 404 |
588
+ | `POST /_akan/ops/migrate` `{ id, action, target?, params?, expect?, items? }` | 202; a child runs `ops migrate <action> … --json` for the token's `sub` |
589
+ | `GET /_akan/ops/migrate/:id` | `running` → `done` (`exitCode`, and `result`: the line `--json` printed last) \| `failed` (it printed none) |
588
590
 
589
591
  - **The entry is the image's own `main.js`** — a production image carries no `akan` CLI. `AkanApp.start()` hands
590
592
  `argv[2] === "ops"` to the command before it boots anything. It reads the container's env — the `AKAN_PUBLIC_*`
@@ -593,10 +595,15 @@ serving without a key. Everything else is behind the ops channel.
593
595
  `--force` is for a port some other app holds.
594
596
  - **The channel exists only when `AKAN_OPS_PUBLIC_KEY` is set** (Ed25519: PEM, JWK, base64 SPKI or raw 32 bytes).
595
597
  The control plane signs a short EdDSA JWS — `aud`, `iat`, `exp` at most 300s after `iat`, a `jti` the process
596
- refuses twice — and the host holds only the public half, so a machine someone carries off mints nothing.
598
+ refuses twice, and optionally `sub`, whom it acts for — and the host holds only the public half, so a machine
599
+ someone carries off mints nothing.
597
600
  `aud` is `<appName>/<environment>`, or `<appName>/<environment>/<AKAN_OPS_INSTANCE>` when that is set, and then
598
601
  only that: a token minted for the app as a whole would replay across every host sharing the name. 30 calls a
599
602
  minute per process; every refusal is the same 401. A malformed key leaves the app serving with the channel off.
603
+ - **A migrate job runs for the token's `sub`**, which the run records as its actor: apply and rollback refuse a
604
+ token without one, and a body naming `actor` is refused. Each option reaches the command as `--name=value`. One
605
+ job runs at a time per process (409 meanwhile); the run's own lease keeps two hosts on one database apart. Jobs
606
+ live in memory, so after a restart read the outcome from `list`. Polls count toward the 30 a minute.
600
607
  - **The app holds no storage credential.** It PUTs to the presigned URLs it was handed (`application/octet-stream`,
601
608
  the manifest `application/json`), plain http only to loopback. The manifest landing is the commit marker.
602
609
  - **Snapshots cover the SQLite modes only.** `single` copies the main file and, with `includeSolid`, the Solid file;
@@ -142,9 +142,10 @@ eight queries threw; per-section boundaries put each rejection at its own bounda
142
142
  is a real 3xx only when it was raised before any I/O was awaited, the body's own or a layout's above it (a layout that
143
143
  awaits `fetch.viewOrg` makes every page under it late); after one — `getSelf({ unauthorize: "/signin" })` fetches —
144
144
  it reaches the browser as an HTTP 200 that navigates in the page, which a crawler or `curl` does not
145
- follow. Either way no markup below the redirect is sent. A route that must answer a real 3xx after I/O — a gate an
146
- external client hits, a moved URL — takes `.config({ ssr: "block" })`: the response waits for the whole render.
147
- Never redirect from inside a streamed section.
145
+ follow. Either way no markup below the redirect is sent. A route's `.guard()` runs before any layout body starts,
146
+ so a redirect from it is a real 3xx even after the guard's own awaited query: gate there. A route that must answer a
147
+ real 3xx from its body after I/O — a moved URL found by a query — takes `.config({ ssr: "block" })`: the response
148
+ waits for the whole render. Never redirect from inside a streamed section.
148
149
 
149
150
  **⑥ Use named `ReactNode` slots, not just `children`.** A client shell that takes one slot per region lets a
150
151
  server component supply each of them, instead of absorbing the whole subtree into the bundle. `Layout.Navbar`
@@ -154,8 +155,10 @@ is the shape: `title` / `left` / `right` beside `back` and `children`, each fill
154
155
  `canWrite(user?)`, `formatTimes()`), and enum→class lookups belong in a module-scope `as const` map. Both sides call
155
156
  the same method, so a client component that exists only to compute a label is markup in the wrong place.
156
157
 
157
- **⑧ Gate auth on the server.** `getSelf({ unauthorize: "/signin" })` in `_layout.tsx` redirects before any of the
158
- page's markup is rendered. A client-side check costs a hydration round-trip and flashes the wrong UI first.
158
+ **⑧ Gate auth on the server.** `.guard(() => signedIn())` (`@libs/shared/webkit`) on `_layout.tsx` runs before any
159
+ of the route renders, so the redirect is a real 3xx; `getSelf({ unauthorize: "/signin" })` in a layout body still
160
+ redirects before any of the page's markup, in-page after I/O. A client-side check costs a hydration round-trip and
161
+ flashes the wrong UI first.
159
162
 
160
163
  **⑨ Prefer CSS over client state for pure visibility.** A `data-*` attribute plus `group-data-[…]` variants (see
161
164
  `libs/util/ui/Grid/*`) or `<details>`/`<summary>` keeps both branches server-rendered. Reach for `useState` when the
@@ -451,7 +451,7 @@ export default page()
451
451
 
452
452
  Rules the loader and `akan sync` enforce: a chain module has no other export; `page()` belongs in a page file and
453
453
  `layout()` / `rootLayout()` in a `_layout.tsx`; a page names every `[x]` of its path, a layout may name a subset;
454
- `.param()` and `.prompt()` names are string literals; `import "./styles.css"` stays the first line of a root layout;
454
+ `.param()`, `.prompt()` and `.app()` names are string literals; `import "./styles.css"` stays the first line of a root layout;
455
455
  `_overrides.tsx` is untouched. MCP: `prompts/get` now runs the page's body under the caller's token and hands the
456
456
  model the description, what the page fetched, and the tools of those modules — see `mcpRule`.
457
457
 
@@ -8,7 +8,7 @@ import {
8
8
  CodegenLock,
9
9
  WorkspaceExecutor,
10
10
  AppExecutor
11
- } from "./index-va06nr10.js";
11
+ } from "./index-gxqvev8w.js";
12
12
  import"./index-52w0fvh8.js";
13
13
  import {
14
14
  GraphClientEntryDiscovery,
@@ -19,10 +19,10 @@ import {
19
19
  DevChangePlanner,
20
20
  DevGeneratedIndexSync,
21
21
  SsrDevBundler
22
- } from "./index-w44cy4vs.js";
22
+ } from "./index-cgyqx6q1.js";
23
23
  import {
24
24
  ServerGraphFile
25
- } from "./index-192ha4xh.js";
25
+ } from "./index-6nqnqzv1.js";
26
26
  import {
27
27
  prepareDevWatchBatch
28
28
  } from "./index-a5vmm6x4.js";
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  runner,
4
4
  script
5
- } from "./index-jw1zm273.js";
5
+ } from "./index-g5px5pxd.js";
6
6
  import {
7
7
  ModuleExecutor
8
- } from "./index-va06nr10.js";
8
+ } from "./index-gxqvev8w.js";
9
9
  import {
10
10
  sourceFile,
11
11
  moduleSourcePaths,
@@ -13,10 +13,10 @@ import {
13
13
  createPassedPrimitiveReport,
14
14
  bilingualLabelForField,
15
15
  bilingualDescriptionForField
16
- } from "./index-trpasv9q.js";
16
+ } from "./index-5m0s0gdv.js";
17
17
  import {
18
18
  PageScript
19
- } from "./index-86jt2a0e.js";
19
+ } from "./index-3xhak9e0.js";
20
20
  import {
21
21
  pluralizeName
22
22
  } from "./index-nz8prx9g.js";
@@ -39,18 +39,21 @@ import {
39
39
  import {
40
40
  runner,
41
41
  script,
42
+ markCliErrorPrinted,
42
43
  assertPromptable
43
- } from "./index-jw1zm273.js";
44
+ } from "./index-g5px5pxd.js";
44
45
  import {
46
+ AppInfo,
47
+ CommandExecutionError,
45
48
  WorkspaceExecutor,
46
49
  AppExecutor,
47
50
  LibExecutor,
48
51
  PkgExecutor
49
- } from "./index-va06nr10.js";
52
+ } from "./index-gxqvev8w.js";
50
53
  import {
51
54
  DesktopServerStage,
52
55
  UpdateUploader
53
- } from "./index-vk0pa0mn.js";
56
+ } from "./index-ex35dg39.js";
54
57
  import {
55
58
  resolveMobileTargets,
56
59
  resolveAppId
@@ -59,17 +62,17 @@ import {
59
62
  NativePublicEnv,
60
63
  UpdatePackFolder,
61
64
  NativeApp
62
- } from "./index-g1mbppa9.js";
65
+ } from "./index-551gh8h2.js";
63
66
  import {
64
67
  LibraryScript
65
- } from "./index-pydbnfez.js";
68
+ } from "./index-p44jnjfr.js";
66
69
  import {
67
70
  stripAnsi
68
71
  } from "./index-7md2g9vw.js";
69
72
  import {
70
73
  SlicePlanner,
71
74
  formatSlicePlan
72
- } from "./index-35hc95h5.js";
75
+ } from "./index-f9118j70.js";
73
76
  import {
74
77
  openBrowser
75
78
  } from "./index-9dp031by.js";
@@ -1896,7 +1899,7 @@ ${uniqueCandidates.map((candidate) => ` - ${candidate}`).join(`
1896
1899
 
1897
1900
  // pkgs/@akanjs/cli/application/application.runner.ts
1898
1901
  import { Logger as Logger3, toError } from "akanjs/common";
1899
- var loadBuildRunner = async () => (await import("./applicationBuildRunner-vh4j6vdv.js")).ApplicationBuildRunner;
1902
+ var loadBuildRunner = async () => (await import("./applicationBuildRunner-s41mgz21.js")).ApplicationBuildRunner;
1900
1903
  var loadPrompts = async () => await import("@inquirer/prompts");
1901
1904
 
1902
1905
  class ApplicationRunner extends runner("application") {
@@ -1938,8 +1941,12 @@ class ApplicationRunner extends runner("application") {
1938
1941
  const scriptPath = `script/${scriptName}.ts`;
1939
1942
  if (!await app.exists(scriptPath))
1940
1943
  throw new Error(`Script file not found: apps/${app.name}/${scriptPath}`);
1941
- await app.spawn("bun", [scriptPath], {
1942
- env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "script", ...await app.getDatabaseModeEnv() }),
1944
+ await this.#spawnFromRoot(app, "script", [`${app.cwdPath}/${scriptPath}`]);
1945
+ }
1946
+ async#spawnFromRoot(app, commandType, args) {
1947
+ await app.spawn("bun", args, {
1948
+ cwd: app.workspace.workspaceRoot,
1949
+ env: app.getCommandEnv({ AKAN_COMMAND_TYPE: commandType, ...await app.getDatabaseModeEnv() }),
1943
1950
  stdio: "inherit"
1944
1951
  });
1945
1952
  }
@@ -2026,10 +2033,7 @@ try {
2026
2033
  await server.stop();
2027
2034
  }
2028
2035
  `;
2029
- await app.spawn("bun", ["-e", code], {
2030
- env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "console", ...await app.getDatabaseModeEnv() }),
2031
- stdio: "inherit"
2032
- });
2036
+ await this.#spawnFromRoot(app, "console", ["-e", code]);
2033
2037
  }
2034
2038
  async transferDatabase(app, direction, dir) {
2035
2039
  const serverPath = `${app.cwdPath}/server.ts`;
@@ -2048,16 +2052,55 @@ try {
2048
2052
  await server.stop();
2049
2053
  }
2050
2054
  `;
2051
- await app.spawn("bun", ["-e", code], {
2052
- env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "script", ...await app.getDatabaseModeEnv() }),
2053
- stdio: "inherit"
2055
+ await this.#spawnFromRoot(app, "script", ["-e", code]);
2056
+ }
2057
+ async migrate(app, argv) {
2058
+ const serverPath = `${app.cwdPath}/server.ts`;
2059
+ if (!await app.exists("server.ts"))
2060
+ throw new Error(`Server file not found: apps/${app.name}/server.ts`);
2061
+ const code = `
2062
+ const argv = ${JSON.stringify(argv)};
2063
+ let loaded;
2064
+ try {
2065
+ loaded = await import(${JSON.stringify(serverPath)});
2066
+ } catch (error) {
2067
+ console.error(error);
2068
+ const { MigrationCommand } = await import("akanjs/server");
2069
+ process.exitCode = await MigrationCommand.print(MigrationCommand.failure(null, error), MigrationCommand.jsonOf(argv));
2070
+ }
2071
+ if (loaded) process.exitCode = await loaded.migrations.command(loaded.server, argv, ${JSON.stringify(`akan migrate ${app.name}`)});
2072
+ `;
2073
+ try {
2074
+ await this.#spawnFromRoot(app, "script", ["-e", code]);
2075
+ } catch (error) {
2076
+ if (!(error instanceof CommandExecutionError) || error.code === null)
2077
+ throw error;
2078
+ process.exitCode = error.code;
2079
+ }
2080
+ }
2081
+ async createMigration(app, migrationName, now = new Date) {
2082
+ const words = migrationName.split(/[^A-Za-z0-9]+/).filter(Boolean);
2083
+ const name = words.map((word, idx) => (idx ? word.charAt(0).toUpperCase() : word.charAt(0).toLowerCase()) + word.slice(1)).join("");
2084
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(name))
2085
+ throw new Error(`"${migrationName}" is no migration name; name what it does in camelCase, e.g. bizLicenseOwnership`);
2086
+ const migrationId = `${now.toISOString().replace(/\D/g, "").slice(0, 12)}_${name}`;
2087
+ const filePath = `${AppInfo.migrationDir}/${migrationId}.ts`;
2088
+ if (await app.exists(filePath))
2089
+ throw new Error(`apps/${app.name}/${filePath} already exists`);
2090
+ const title = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
2091
+ await app.applyTemplate({
2092
+ basePath: AppInfo.migrationDir,
2093
+ template: "migration",
2094
+ dict: { migrationId, title: title.charAt(0).toUpperCase() + title.slice(1) },
2095
+ overwrite: false
2054
2096
  });
2097
+ return migrationId;
2055
2098
  }
2056
2099
  async typecheck(app, options = {}) {
2057
2100
  await new (await loadBuildRunner())(app).typecheck(options);
2058
2101
  }
2059
2102
  async typecheckLibrary(lib, options = {}) {
2060
- const { ProjectTypecheck } = await import("./projectTypecheck-f534enqj.js");
2103
+ const { ProjectTypecheck } = await import("./projectTypecheck-qg2jf80j.js");
2061
2104
  const root = lib.workspace.workspaceRoot;
2062
2105
  await new ProjectTypecheck(lib.cwdPath, root, { ...process.env, AKAN_WORKSPACE_ROOT: root }).run([
2063
2106
  "../../server.ts",
@@ -3339,6 +3382,14 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
3339
3382
  await app.scanSync();
3340
3383
  await this.applicationRunner.runConsole(app);
3341
3384
  }
3385
+ async migrate(app, argv) {
3386
+ await app.scanSync();
3387
+ await this.applicationRunner.migrate(app, argv);
3388
+ }
3389
+ async createMigration(app, migrationName) {
3390
+ const migrationId = await this.applicationRunner.createMigration(app, migrationName);
3391
+ Logger6.rawLog(`Created apps/${app.name}/script/migration/${migrationId}.ts; plan it with \`akan migrate ${app.name} plan ${migrationId}\``);
3392
+ }
3342
3393
  async logs(app, options) {
3343
3394
  await this.applicationRunner.runLogs(app, options);
3344
3395
  }
@@ -3359,6 +3410,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
3359
3410
  } catch (error) {
3360
3411
  Logger6.rawLog(`${app.name} build failed in dist/apps/${app.name}`, undefined, "error");
3361
3412
  Logger6.rawLog(ApplicationBuildReporter.formatError(error), undefined, "error");
3413
+ markCliErrorPrinted(error);
3362
3414
  throw error;
3363
3415
  }
3364
3416
  }
@@ -3381,6 +3433,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
3381
3433
  } catch (error) {
3382
3434
  spinner.fail(`${exec.name} typecheck failed`);
3383
3435
  Logger6.rawLog(ApplicationBuildReporter.formatError(error), undefined, "error");
3436
+ markCliErrorPrinted(error);
3384
3437
  throw error;
3385
3438
  }
3386
3439
  }
@@ -3635,7 +3688,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
3635
3688
  await Promise.all(shares.map(async (share) => await share.close()));
3636
3689
  }, "Abandoning the tunnel release; the shares expire on their own.");
3637
3690
  await Promise.all(apps.map(async (app) => {
3638
- const { TunnelShare } = await import("./TunnelShare-661cxz5e.js");
3691
+ const { TunnelShare } = await import("./TunnelShare-s7bpe55e.js");
3639
3692
  const share = await TunnelShare.open(app, { workspace: app.workspace });
3640
3693
  shares.push(share);
3641
3694
  urls.set(app.name, share.url);