@akanjs/cli 2.3.11-rc.6 → 2.3.11-rc.8
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/incrementalBuilder.proc.js +239 -34
- package/index.js +285 -55
- package/package.json +2 -2
- package/templates/appSample/lib/task/task.service.ts +1 -1
- package/templates/module/__Model__.Template.tsx +4 -5
- package/templates/module/__Model__.Unit.tsx +1 -1
- package/templates/module/__Model__.View.tsx +1 -1
- package/templates/module/__Model__.Zone.tsx +5 -3
- package/templates/module/__model__.constant.ts +2 -1
- package/templates/module/__model__.dictionary.ts +3 -1
- package/templates/workspaceRoot/.gitignore.template +2 -0
- package/templates/workspaceRoot/AGENTS.md.template +55 -3
- package/templates/workspaceRoot/biome.json.template +4 -0
- package/templates/workspaceRoot/docs/GENERATED.md.template +0 -1
- package/templates/workspaceRoot/package.json.template +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/cli",
|
|
3
|
-
"version": "2.3.11-rc.
|
|
3
|
+
"version": "2.3.11-rc.8",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@langchain/openai": "^1.4.6",
|
|
35
35
|
"@tailwindcss/node": "^4.3.0",
|
|
36
36
|
"@trapezedev/project": "^7.1.4",
|
|
37
|
-
"akanjs": "2.3.11-rc.
|
|
37
|
+
"akanjs": "2.3.11-rc.8",
|
|
38
38
|
"chalk": "^5.6.2",
|
|
39
39
|
"commander": "^14.0.3",
|
|
40
40
|
"daisyui": "5.5.23",
|
|
@@ -10,7 +10,7 @@ import * as db from "../db";
|
|
|
10
10
|
// Convention: <module>.service.ts — business logic orchestration for a database module.
|
|
11
11
|
// Extends serve(db.<module>, depsCallback) from akanjs/service — binds to the DB model, receives DI deps.
|
|
12
12
|
// Auto-generated by akan sync (do not write manually):
|
|
13
|
-
// getTask(id),
|
|
13
|
+
// getTask(id), createTask(data), updateTask(id, data), removeTask(id),
|
|
14
14
|
// listByStatus(status), searchDocs(text), and all filter+query methods from document.ts.
|
|
15
15
|
// Manual below: lifecycle hooks, custom business logic methods.
|
|
16
16
|
// Registered by akan sync into srv.ts barrel.
|
|
@@ -11,7 +11,7 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
|
|
|
11
11
|
content: `
|
|
12
12
|
"use client";
|
|
13
13
|
import { Field, Layout } from "akanjs/ui";
|
|
14
|
-
import {
|
|
14
|
+
import { st, usePage } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
|
|
15
15
|
|
|
16
16
|
interface GeneralProps {
|
|
17
17
|
className?: string;
|
|
@@ -23,10 +23,9 @@ export const General = ({ className }: GeneralProps) => {
|
|
|
23
23
|
return (
|
|
24
24
|
<Layout.Template className={className}>
|
|
25
25
|
<Field.Text
|
|
26
|
-
label={l("${dict.model}.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
onChange={st.do.setIdOn${dict.Model}}
|
|
26
|
+
label={l("${dict.model}.name")}
|
|
27
|
+
value={${dict.model}Form.name}
|
|
28
|
+
onChange={st.do.setNameOn${dict.Model}}
|
|
30
29
|
/>
|
|
31
30
|
</Layout.Template>
|
|
32
31
|
);
|
|
@@ -17,7 +17,7 @@ export const Card = ({ ${dict.model}, href }: ModelProps<"${dict.model}", cnst.L
|
|
|
17
17
|
const { l } = usePage();
|
|
18
18
|
return (
|
|
19
19
|
<Link href={href} className="w-full">
|
|
20
|
-
<div>{l("${dict.model}.
|
|
20
|
+
<div>{l("${dict.model}.name")}: {${dict.model}.name}</div>
|
|
21
21
|
</Link>
|
|
22
22
|
);
|
|
23
23
|
};
|
|
@@ -20,7 +20,7 @@ export const General = ({ className, ${dict.model} }: GeneralProps) => {
|
|
|
20
20
|
const { l } = usePage();
|
|
21
21
|
return (
|
|
22
22
|
<div className={clsx("w-full", className)}>
|
|
23
|
-
<div>{l("${dict.model}.
|
|
23
|
+
<div>{l("${dict.model}.name")}: {${dict.model}.name}</div>
|
|
24
24
|
</div>
|
|
25
25
|
);
|
|
26
26
|
};
|
|
@@ -11,7 +11,9 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
|
|
|
11
11
|
content: `
|
|
12
12
|
"use client";
|
|
13
13
|
import { Load } from "akanjs/ui";
|
|
14
|
-
|
|
14
|
+
// Alias the domain namespace so the Card/View exports below never collide with the model name
|
|
15
|
+
// (a model literally named "card" or "view" would otherwise shadow this import).
|
|
16
|
+
import { type cnst, ${dict.Model} as ${dict.Model}Domain } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
|
|
15
17
|
import type { ClientInit, ClientView, SliceMeta } from "akanjs/fetch";
|
|
16
18
|
|
|
17
19
|
interface CardProps {
|
|
@@ -25,7 +27,7 @@ export const Card = ({ className, init, slice }: CardProps) => {
|
|
|
25
27
|
className={className}
|
|
26
28
|
init={init}
|
|
27
29
|
renderItem={(${dict.model}) => (
|
|
28
|
-
<${dict.Model}.Unit.Card key={${dict.model}.id} href={\`/${dict.model}/\${${dict.model}.id}\`} ${dict.model}={${dict.model}} />
|
|
30
|
+
<${dict.Model}Domain.Unit.Card key={${dict.model}.id} href={\`/${dict.model}/\${${dict.model}.id}\`} ${dict.model}={${dict.model}} />
|
|
29
31
|
)}
|
|
30
32
|
/>
|
|
31
33
|
);
|
|
@@ -36,7 +38,7 @@ interface ViewProps {
|
|
|
36
38
|
view: ClientView<"${dict.model}", cnst.${dict.Model}>;
|
|
37
39
|
}
|
|
38
40
|
export const View = ({ view }: ViewProps) => {
|
|
39
|
-
return <Load.View view={view} renderView={(${dict.model}) => <${dict.Model}.View.General ${dict.model}={${dict.model}} />} />;
|
|
41
|
+
return <Load.View view={view} renderView={(${dict.model}) => <${dict.Model}Domain.View.General ${dict.model}={${dict.model}} />} />;
|
|
40
42
|
};
|
|
41
43
|
`,
|
|
42
44
|
};
|
|
@@ -10,11 +10,12 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
|
|
|
10
10
|
import { via } from "akanjs/constant";
|
|
11
11
|
|
|
12
12
|
export class ${dict.Model}Input extends via((field) => ({
|
|
13
|
+
name: field(String),
|
|
13
14
|
})) {}
|
|
14
15
|
|
|
15
16
|
export class ${dict.Model}Object extends via(${dict.Model}Input, (field) => ({})) {}
|
|
16
17
|
|
|
17
|
-
export class Light${dict.Model} extends via(${dict.Model}Object, [] as const, (resolve) => ({})) {}
|
|
18
|
+
export class Light${dict.Model} extends via(${dict.Model}Object, ["name"] as const, (resolve) => ({})) {}
|
|
18
19
|
|
|
19
20
|
export class ${dict.Model} extends via(${dict.Model}Object, Light${dict.Model}, (resolve) => ({})) {}
|
|
20
21
|
|
|
@@ -24,7 +24,9 @@ export const dictionary = modelDictionary(["en", "ko"])
|
|
|
24
24
|
.of((t) =>
|
|
25
25
|
t(["${modelLabelEn}", "${modelLabelKo}"]).desc(["${modelDescEn}", "${modelDescKo}"])
|
|
26
26
|
)
|
|
27
|
-
.model<${dict.Model}>((t) => ({
|
|
27
|
+
.model<${dict.Model}>((t) => ({
|
|
28
|
+
name: t(["Name", "이름"]),
|
|
29
|
+
}))
|
|
28
30
|
.insight<${dict.Model}Insight>((t) => ({}))
|
|
29
31
|
.slice<${dict.Model}Slice>((fn) => ({
|
|
30
32
|
inPublic: fn(["${dict.Model} In Public", "${dict.Model} 공개"]).arg((t) => ({})),
|
|
@@ -28,7 +28,6 @@ Common generated files include:
|
|
|
28
28
|
- `*/lib/cnst.ts`
|
|
29
29
|
- `*/lib/db.ts`
|
|
30
30
|
- `*/lib/dict.ts`
|
|
31
|
-
- `*/lib/option.ts`
|
|
32
31
|
- `*/lib/sig.ts`
|
|
33
32
|
- `*/lib/srv.ts`
|
|
34
33
|
- `*/lib/st.ts`
|
|
@@ -112,6 +111,12 @@ akan test <%= appName %>
|
|
|
112
111
|
akan build <%= appName %>
|
|
113
112
|
```
|
|
114
113
|
|
|
114
|
+
**Verify endpoints with signal tests, not raw HTTP.** The canonical way to check a query/mutation/slice
|
|
115
|
+
contract is an in-memory signal test (`<model>.signal.test.ts`), using the test fetch harness
|
|
116
|
+
(`getOrSetupSignalTestFetch`) — it is fast, needs no running server, and exercises `fetch.*`, `view/edit/merge<Model>`,
|
|
117
|
+
and slice `init`/`list`/`insight` directly. Prefer it over `curl`: the dev gateway locale-prefixes routes (`/en/...`),
|
|
118
|
+
so hand-rolled HTTP calls against a raw path can redirect unexpectedly. See `akan test <%= appName %>`.
|
|
119
|
+
|
|
115
120
|
### Other Frequently Used Commands
|
|
116
121
|
|
|
117
122
|
```bash
|
|
@@ -230,6 +235,7 @@ final fallback when no CLI command covers the change.
|
|
|
230
235
|
| Add a pure workflow / integration (e.g., Payment, Email) | `lib/_<service>/` → service, signal, store, dictionary, abstract | `akan sync <name>` |
|
|
231
236
|
| Add a reusable value type (e.g., Address, WorkHistory) | `lib/__scalar/<type>/` → constant, dictionary, abstract | `akan sync <name>` |
|
|
232
237
|
| Create a new URL-visitable page | `page/` → `_index.tsx`, `_layout.tsx`, `[param]/_index.tsx` | Rebuild (akan start auto-detects) |
|
|
238
|
+
| Change the app color theme / design tokens | `apps/<app>/page/styles.css` → edit the daisyUI `@plugin "daisyui/theme"` blocks (`light`/`dark`, the `--color-*` variables) | akan start hot-reloads |
|
|
233
239
|
| Add a form or reusable UI component | `ui/` → PascalCase `.tsx` with `"use client"` if needed | `akan sync <name>` |
|
|
234
240
|
| Add a React hook or browser helper | `webkit/` → camelCase `.ts` with `"use client"` | `akan sync <name>` |
|
|
235
241
|
| Add a server-only guard, middleware, or adaptor | `srvkit/` → PascalCase `.ts` | `akan sync <name>` |
|
|
@@ -247,6 +253,7 @@ final fallback when no CLI command covers the change.
|
|
|
247
253
|
| Skip running `akan sync` after deleting a file | Deleted files remain referenced in barrel exports, causing import errors everywhere. | Run `akan sync <name>` after every file add, remove, or rename |
|
|
248
254
|
| Use "use client" or `useState`/`useEffect` in pages/*.tsx, *.Unit.tsx, and *.View.tsx files | Server code cannot use React hooks. Wrap in a separate `"use client"` component. | Move hook logic to `webkit/` or a `"use client"` UI component |
|
|
249
255
|
| Use `<a>` tag for internal navigation between pages | Akan.js uses `<Link>` from `akanjs/ui` for client-side navigation — avoids full page reloads. | `import { Link } from "akanjs/ui"` and use `<Link href="/task">...</Link>` |
|
|
256
|
+
| Name a custom `Endpoint`/`Slice` like a generated CRUD op — `create<Model>`, `update<Model>`, `remove<Model>`, `view<Model>`, `edit<Model>`, `merge<Model>` | These names are already auto-generated. A collision can pass sync/typecheck/build and only fail at runtime. | Pick a distinct verb, e.g. `startTask`/`archiveTask`, never `createTask` for a custom endpoint |
|
|
250
257
|
|
|
251
258
|
## Generated File Tracker (Quick Reference)
|
|
252
259
|
|
|
@@ -257,7 +264,6 @@ These files are regenerated by `akan sync` and overwritten on every sync. **Do n
|
|
|
257
264
|
| `*/lib/cnst.ts` | All `*/lib/*/**.constant.ts` | Barrel for all constants |
|
|
258
265
|
| `*/lib/db.ts` | All `*/lib/<model>/*.document.ts` | Barrel for all document models |
|
|
259
266
|
| `*/lib/dict.ts` | All `*/lib/*/**.dictionary.ts` | Barrel for all dictionaries |
|
|
260
|
-
| `*/lib/option.ts` | Generated option helpers | Option helper entry |
|
|
261
267
|
| `*/lib/sig.ts` | All `*/lib/**/**.signal.ts` | Barrel for all signals |
|
|
262
268
|
| `*/lib/srv.ts` | All `*/lib/**/**.service.ts` | Barrel for all services |
|
|
263
269
|
| `*/lib/st.ts` | All `*/lib/**/**.store.ts` | Barrel for all stores |
|
|
@@ -338,6 +344,10 @@ Three patterns: injecting an **external adapter** (`use<>()`), another **module'
|
|
|
338
344
|
or a **predefined framework adapter** (`plug()`). A field named `<refName>Service` resolves to the service
|
|
339
345
|
registered under `<refName>` — the `Service`/`Signal` suffix is required and stripped to derive the lookup key.
|
|
340
346
|
|
|
347
|
+
> `apps/<app>/lib/option.ts` is a **user-owned** file scaffolded once — edit it to register adapters/DI. Unlike the
|
|
348
|
+
> barrels (`cnst.ts`, `db.ts`, `srv.ts`, …) it is **not** overwritten by `akan sync`, so your `.use(...)` registrations
|
|
349
|
+
> are safe.
|
|
350
|
+
|
|
341
351
|
**A. Adapter injection via `use<>()` (for external clients / global singletons)**
|
|
342
352
|
|
|
343
353
|
```typescript
|
|
@@ -411,12 +421,19 @@ store's generated `upload<Field>On<Model>(fileList)` action handle the upload.
|
|
|
411
421
|
|
|
412
422
|
A Slice is a named, filtered data view. Add file entries and connect from a page.
|
|
413
423
|
|
|
424
|
+
> **Silent failure — a slice `exec` must return a query descriptor, never an executed list.**
|
|
425
|
+
> Return `this.taskService.queryByStatuses(...)` (the `query<Filter>` builder), **not**
|
|
426
|
+
> `this.taskService.listByStatuses(...)` / `listBy...(...)` (which returns a `Promise<Doc[]>`).
|
|
427
|
+
> Returning an array type-checks but throws at runtime during insight aggregation with the opaque
|
|
428
|
+
> `Error: Unknown document field path: 0`. If you see that error, your slice is returning a list, not a query.
|
|
429
|
+
|
|
414
430
|
```typescript
|
|
415
431
|
// 1. apps/<app>/lib/<model>/<model>.signal.ts — Define the slice
|
|
416
432
|
export class TaskSlice extends slice(srv.task, (init) => ({
|
|
417
433
|
inTodo: init()
|
|
418
434
|
.search("statuses", [cnst.TaskStatus])
|
|
419
435
|
.exec(function (statuses?) {
|
|
436
|
+
// ✅ query<Filter> — a query descriptor. ❌ listByStatuses(...) returns an array and fails at runtime.
|
|
420
437
|
return this.taskService.queryByStatuses(statuses ?? ["todo", "inProgress"]);
|
|
421
438
|
}),
|
|
422
439
|
})) {}
|
|
@@ -595,6 +612,41 @@ For each business question, follow this chain:
|
|
|
595
612
|
| What client state is shared? | `store.ts` — `store()` with auto-generated form/insight state + custom actions |
|
|
596
613
|
| What should users see? | `View.tsx` + `Zone.tsx` (detail/container), `Template.tsx` (forms), `Unit.tsx` (cards), `Util.tsx` (buttons) |
|
|
597
614
|
|
|
615
|
+
## Modeling & Query Gotchas
|
|
616
|
+
|
|
617
|
+
A short list of things the type system does not always catch:
|
|
618
|
+
|
|
619
|
+
- **Slices return a query, not a list.** A slice `exec` must return `this.<model>Service.query<Filter>(...)`, never
|
|
620
|
+
a `list<Filter>(...)` / `listBy...(...)` array. Returning an array type-checks but fails at runtime with
|
|
621
|
+
`Unknown document field path: 0`. (See Recipe 3.)
|
|
622
|
+
- **Custom endpoint names must not collide with generated CRUD.** `create/update/remove/view/edit/merge<Model>`
|
|
623
|
+
already exist. A collision can build green and fail only at runtime — pick a distinct verb.
|
|
624
|
+
- **Numbers are `Int` or `Float`, never `Number`.** `field(Number)` / `.body("x", Number)` fail to typecheck. Use
|
|
625
|
+
`Int` for counts, `Float` for decimals.
|
|
626
|
+
- **Array fields use `field([T])`.** e.g. `tags: field([String])`, `images: field([File])` — not `field(String)` with
|
|
627
|
+
a suffix.
|
|
628
|
+
- **Reading a secret field needs an explicit select.** `field(...).secret()` values (e.g. `passwordHash`) are stripped
|
|
629
|
+
from query results by default. Fetch them with `{ select: { <field>: true } }`, e.g.
|
|
630
|
+
`this.userModel.pickById(id, { select: { passwordHash: true } })`.
|
|
631
|
+
|
|
632
|
+
## Current User, Guards & Auth-Gated Pages
|
|
633
|
+
|
|
634
|
+
Built-in user authentication (session / JWT / password hashing) ships as a separate Akan auth library, not in the
|
|
635
|
+
core framework. The core framework gives you the composition points below; wire the auth library through them.
|
|
636
|
+
|
|
637
|
+
- **Guards** attach at the signal declaration, not per-method:
|
|
638
|
+
`endpoint(srv.task, { guards: { root: SignedIn } }, ({ mutation }) => ({...}))` or
|
|
639
|
+
`slice(srv.task, { guards: { root: SignedIn, get: Public, cru: Public } }, ...)`. `Public` always allows; other
|
|
640
|
+
guards implement the `Guard` interface in `srvkit/` (server-only) and read the request context.
|
|
641
|
+
- **Read the current user inside a custom endpoint** by injecting an `InternalArg` with `.with(...)`:
|
|
642
|
+
`mutation(cnst.Task).with(CurrentUserId).exec(async function (currentUserId) { ... })`. The `Guard` /
|
|
643
|
+
`InternalArg` helpers live in `srvkit/` and read `context.getHttpContext().req.user`.
|
|
644
|
+
- **Auto-generated CRUD and `serve()` service methods / lifecycle hooks do not receive session context.** If an
|
|
645
|
+
operation needs the acting user, expose a custom endpoint that takes it via `.with(CurrentUserId)` — never trust a
|
|
646
|
+
client-supplied user id.
|
|
647
|
+
- **SSR auth-gated pages: guard at the layout.** Check the session in the `_layout.tsx` loader and redirect when it is
|
|
648
|
+
absent, so nested pages never render for signed-out users.
|
|
649
|
+
|
|
598
650
|
## Auto-Generated API Reference
|
|
599
651
|
|
|
600
652
|
akan sync automatically generates APIs across all layers. Only write custom logic — never hand-write what the framework generates.
|
|
@@ -618,7 +670,7 @@ akan sync automatically generates APIs across all layers. Only write custom logi
|
|
|
618
670
|
|---------------|-------------|
|
|
619
671
|
| `this.<model>Model` | Auto-injected model adaptor |
|
|
620
672
|
| `get<Model>(id)`, `load<Model>(id)` | Single document lookup |
|
|
621
|
-
| `
|
|
673
|
+
| `create<Model>(data)`, `update<Model>(id, data)`, `remove<Model>(id)` | CRUD operations — named after the model, e.g. `createTask`/`updateTask`/`removeTask` (there is no literal `createModel`) |
|
|
622
674
|
| `list<Query>(args)`, `find<Query>(args)`, `pick<Query>(args)` | Filter-based queries |
|
|
623
675
|
| `exists<Query>(args)`, `count<Query>(args)`, `insight<Query>(args)` | Filter-based helpers |
|
|
624
676
|
| `_preCreate`, `_postCreate`, `_preUpdate`, `_postUpdate`, `_preRemove`, `_postRemove` | Lifecycle hooks (override to add logic) |
|
|
@@ -147,6 +147,10 @@
|
|
|
147
147
|
"includes": ["**/*.constant.ts", "**/*.document.ts", "**/*.service.ts", "**/*.store.ts"],
|
|
148
148
|
"plugins": ["./node_modules/@akanjs/devkit/lint/no-js-private-class-method.grit"]
|
|
149
149
|
},
|
|
150
|
+
{
|
|
151
|
+
"includes": ["**/*.signal.ts"],
|
|
152
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-redeclare-predefined-endpoint.grit"]
|
|
153
|
+
},
|
|
150
154
|
{
|
|
151
155
|
"includes": [
|
|
152
156
|
"**/*.constant.ts",
|
|
@@ -17,7 +17,6 @@ sync or build can overwrite local changes.
|
|
|
17
17
|
| `apps/*/lib/cnst.ts` | Re-exports constants and model shapes from module `*.constant.ts` files. |
|
|
18
18
|
| `apps/*/lib/db.ts` | Re-exports database models from module `*.document.ts` files. |
|
|
19
19
|
| `apps/*/lib/dict.ts` | Re-exports dictionaries from module `*.dictionary.ts` files. |
|
|
20
|
-
| `apps/*/lib/option.ts` | Re-exports generated option helpers. |
|
|
21
20
|
| `apps/*/lib/srv.ts` | Re-exports services from module `*.service.ts` files. |
|
|
22
21
|
| `apps/*/lib/sig.ts` | Re-exports endpoints, slices, and internals from module `*.signal.ts` files. |
|
|
23
22
|
| `apps/*/lib/st.ts` | Re-exports stores from module `*.store.ts` files. |
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"agent:setup": "akan agent install all --force",
|
|
14
14
|
"agent:doctor": "akan doctor --strict --format json",
|
|
15
15
|
"agent:context": "akan context --format json",
|
|
16
|
-
"agent:mcp:readonly": "akan mcp-install
|
|
17
|
-
"agent:mcp:plan": "akan mcp-install
|
|
18
|
-
"agent:mcp:apply": "akan mcp-install
|
|
16
|
+
"agent:mcp:readonly": "akan mcp-install all --mode readonly --force",
|
|
17
|
+
"agent:mcp:plan": "akan mcp-install all --mode plan --force",
|
|
18
|
+
"agent:mcp:apply": "akan mcp-install all --mode apply --force",
|
|
19
19
|
"agent:workflows": "akan workflow list",
|
|
20
20
|
"agent:sample:context": "akan context --format markdown --app <%= appName %> --module project",
|
|
21
21
|
"agent:sample:service": "akan create-service billing <%= appName %> --format json",
|