@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.
- package/.build-stamp +1 -1
- package/{CodeAgent-n3tzjm91.js → CodeAgent-fee0pnwp.js} +2 -2
- package/{CodeTui-fnm9ft4d.js → CodeTui-xh71b2k8.js} +1 -1
- package/TunnelShare-s7bpe55e.js +14 -0
- package/{agent.command-3qxf4xcj.js → agent.command-tge9rvmt.js} +5 -5
- package/akanCode.js +2 -2
- package/{application.command-zfja7x4r.js → application.command-j5b8yrcf.js} +35 -9
- package/{applicationBuildRunner-vh4j6vdv.js → applicationBuildRunner-s41mgz21.js} +21 -7
- package/buildBatch.proc.js +9 -9
- package/{cloud.command-fcc8e8tp.js → cloud.command-q9srng6e.js} +13 -13
- package/cloud.script-vfsd4gxs.js +24 -0
- package/{code.command-130bdmhe.js → code.command-n6r6zt97.js} +6 -6
- package/commandManifest.json +1 -1
- package/{context.command-0qx447mb.js → context.command-1je84w1b.js} +16 -16
- package/context.runner-xg0ngz93.js +25 -0
- package/{guideline.command-cv5bmrx2.js → guideline.command-0e0sx2vg.js} +2 -2
- package/guidelines/conventions/conventions.instruction.md +42 -13
- package/guidelines/mcpRule/mcpRule.instruction.md +88 -47
- package/guidelines/pageRule/pageRule.instruction.md +53 -8
- package/guidelines/runtimeRule/runtimeRule.instruction.md +8 -1
- package/guidelines/ssrRule/ssrRule.instruction.md +8 -5
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +1 -1
- package/incrementalBuilder.proc.js +3 -3
- package/{index-fmzcs8v6.js → index-1bhv9dh4.js} +4 -4
- package/{index-qtfmtcrw.js → index-1xqq3va9.js} +71 -18
- package/{index-86jt2a0e.js → index-3xhak9e0.js} +1 -1
- package/{index-tamvyvj3.js → index-4fe4qqgn.js} +3 -3
- package/{index-g1mbppa9.js → index-551gh8h2.js} +5 -5
- package/{index-trpasv9q.js → index-5m0s0gdv.js} +1 -1
- package/index-6mprvdt8.js +566 -0
- package/{index-192ha4xh.js → index-6nqnqzv1.js} +1 -1
- package/{index-dye8pnxx.js → index-73esn3y2.js} +1 -1
- package/{index-4mhy6qme.js → index-7xy30jev.js} +4 -4
- package/{index-1xj2t7cb.js → index-85h3dhe0.js} +7 -7
- package/{index-axs61rna.js → index-8abyrfw8.js} +5 -4
- package/{index-1p98exm2.js → index-94jq9cbz.js} +2 -2
- package/{index-cpb3nvma.js → index-9h0z0tjr.js} +2 -2
- package/{index-hsadfb6c.js → index-c9rfak6r.js} +7 -7
- package/{index-w44cy4vs.js → index-cgyqx6q1.js} +2 -2
- package/{index-d72hfmdm.js → index-cq5x7a9k.js} +8 -8
- package/{index-j237xtew.js → index-deckcwbg.js} +2 -2
- package/{index-vk0pa0mn.js → index-ex35dg39.js} +2 -1
- package/{index-35hc95h5.js → index-f9118j70.js} +1 -1
- package/{index-jw1zm273.js → index-g5px5pxd.js} +53 -9
- package/{index-va06nr10.js → index-gxqvev8w.js} +52 -12
- package/{index-fyq1z6kt.js → index-hagw9c94.js} +3 -3
- package/{index-pydbnfez.js → index-p44jnjfr.js} +2 -2
- package/{index-awbdfcf2.js → index-rp9anyeb.js} +4 -4
- package/{index-j4mkhz93.js → index-t8npm331.js} +3 -3
- package/{index-5edzv8fn.js → index-tbmtmkhx.js} +2 -2
- package/{index-1djcjcre.js → index-vyh8kemm.js} +3 -3
- package/{index-qwa5k1j8.js → index-x7bny3wv.js} +1 -1
- package/index.js +22 -22
- package/{library.command-erp2f56n.js → library.command-ys9s0mtc.js} +3 -3
- package/{localRegistry.command-hj3p16p5.js → localRegistry.command-kmp3sd47.js} +12 -12
- package/{module.command-gs2r615e.js → module.command-5sx9453y.js} +5 -5
- package/{package.command-j10r76ax.js → package.command-s0d3417k.js} +3 -3
- package/package.json +2 -2
- package/{page.command-4h4a8pnh.js → page.command-zsa03pmc.js} +3 -3
- package/{pipeline.command-a2nrk26m.js → pipeline.command-j8h6qsha.js} +2 -2
- package/{primitive.command-w6t0asqz.js → primitive.command-n2978nmw.js} +6 -6
- package/{projectTypecheck-f534enqj.js → projectTypecheck-qg2jf80j.js} +1 -1
- package/{quality.command-77x7vbbx.js → quality.command-7kgz46st.js} +2 -2
- package/{repair.command-y6sstev7.js → repair.command-2sszcckw.js} +5 -5
- package/{routeSourceValidator-7ycnfz0e.js → routeSourceValidator-6jd0q6r6.js} +47 -14
- package/{scalar.command-20b8cy0k.js → scalar.command-230jrhzf.js} +4 -4
- package/{subspace.command-baz73jpk.js → subspace.command-y5v4ypd3.js} +8 -8
- package/templates/migration/__migrationId__.ts +23 -0
- package/templates/server.ts +8 -1
- package/{tunnel.command-2kw8vt9g.js → tunnel.command-ja29zghd.js} +7 -7
- package/{workflow.command-2djwmpqf.js → workflow.command-0eg8czt5.js} +10 -10
- package/{workspace.command-jqs7vjhe.js → workspace.command-zcym1dk5.js} +24 -24
- package/TunnelShare-661cxz5e.js +0 -14
- package/cloud.script-1xt3tnts.js +0 -24
- package/context.runner-1vsjztjg.js +0 -25
- 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
|
|
44
|
-
is published with `visibility: ["app"]`:
|
|
45
|
-
|
|
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
|
|
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 `.
|
|
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.
|
|
353
|
-
|
|
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
|
|
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 —
|
|
383
|
-
|
|
384
|
-
the
|
|
385
|
-
|
|
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
|
|
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
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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,
|
|
413
|
-
|
|
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
|
|
418
|
-
`<Agent.Chat />` reads: `st.tool`, the form setters, `fill<Model>Form`, `
|
|
419
|
-
`highlight` — and, to a host that takes it, the
|
|
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,
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
507
|
-
tools it explains. Exactly
|
|
508
|
-
page's `.prompt()`
|
|
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
|
|
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()` /
|
|
29
|
-
a layout; `.fonts()` / `.theme()` / `.manifest()` / `.reconnect()` / `.wsConnect()` /
|
|
30
|
-
`.viewport()` on the root layout; `.prompt()` on a page. A named export beside the
|
|
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
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
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.**
|
|
158
|
-
|
|
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 `.
|
|
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-
|
|
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-
|
|
22
|
+
} from "./index-cgyqx6q1.js";
|
|
23
23
|
import {
|
|
24
24
|
ServerGraphFile
|
|
25
|
-
} from "./index-
|
|
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-
|
|
5
|
+
} from "./index-g5px5pxd.js";
|
|
6
6
|
import {
|
|
7
7
|
ModuleExecutor
|
|
8
|
-
} from "./index-
|
|
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-
|
|
16
|
+
} from "./index-5m0s0gdv.js";
|
|
17
17
|
import {
|
|
18
18
|
PageScript
|
|
19
|
-
} from "./index-
|
|
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-
|
|
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-
|
|
52
|
+
} from "./index-gxqvev8w.js";
|
|
50
53
|
import {
|
|
51
54
|
DesktopServerStage,
|
|
52
55
|
UpdateUploader
|
|
53
|
-
} from "./index-
|
|
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-
|
|
65
|
+
} from "./index-551gh8h2.js";
|
|
63
66
|
import {
|
|
64
67
|
LibraryScript
|
|
65
|
-
} from "./index-
|
|
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-
|
|
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-
|
|
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
|
|
1942
|
-
|
|
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
|
|
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
|
|
2052
|
-
|
|
2053
|
-
|
|
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-
|
|
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-
|
|
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);
|