@akanjs/cli 3.0.0-alpha.9 → 3.0.0-alpha.90
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/README.ko.md +1 -1
- package/README.md +1 -1
- package/{abstractCompactor-e5yq60qz.js → abstractCompactor-cefxzggj.js} +3 -5
- package/{agent.command-f0gqkfgz.js → agent.command-r93wmq7m.js} +11 -13
- package/{application.command-x903m2qq.js → application.command-5nqfqgdn.js} +62 -38
- package/{applicationBuildRunner-ha3y8ew8.js → applicationBuildRunner-b21m07fs.js} +23 -20
- package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-3wh58n2m.js} +8 -9
- package/buildBatch.proc.js +12 -14
- package/capacitorApp-gzcx6txb.js +12 -0
- package/{cloud.command-zs6kyf83.js → cloud.command-g0pk0k60.js} +15 -18
- package/commandManifest.json +1 -1
- package/{context.command-fmepgjzg.js → context.command-qd8kqrnh.js} +19 -23
- package/dependencyScanner-shs82ynp.js +8 -0
- package/devTui-2xf0meaq.js +552 -0
- package/devTuiView-7jy8f30g.js +9 -0
- package/getDirname-dr3jz1xg.js +7 -0
- package/{guideline.command-n6sw0a0c.js → guideline.command-f38x8jhw.js} +16 -18
- package/guidelines/agentRule/agentRule.generate.json +31 -0
- package/guidelines/agentRule/agentRule.instruction.md +533 -0
- package/guidelines/conventions/conventions.instruction.md +406 -197
- package/guidelines/cssRule/cssRule.instruction.md +7 -0
- package/guidelines/framework/framework.generate.json +3 -2
- package/guidelines/framework/framework.instruction.md +1 -9
- package/guidelines/mcpRule/mcpRule.generate.json +25 -0
- package/guidelines/mcpRule/mcpRule.instruction.md +217 -0
- package/guidelines/modelStore/modelStore.instruction.md +12 -0
- package/guidelines/modelTemplate/modelTemplate.instruction.md +5 -0
- package/guidelines/queryRule/queryRule.generate.json +31 -0
- package/guidelines/queryRule/queryRule.instruction.md +143 -0
- package/guidelines/runtimeRule/runtimeRule.generate.json +25 -0
- package/guidelines/runtimeRule/runtimeRule.instruction.md +213 -0
- package/guidelines/ssrRule/ssrRule.instruction.md +49 -5
- package/guidelines/transportRule/transportRule.generate.json +31 -0
- package/guidelines/transportRule/transportRule.instruction.md +60 -0
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +51 -760
- package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +33 -0
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +402 -0
- package/incrementalBuilder.proc.js +26 -25
- package/{index-774gcx29.js → index-0emb9eat.js} +41 -34
- package/index-3d9y066c.js +76 -0
- package/{index-3vta1tqa.js → index-3vvbv974.js} +32 -32
- package/{index-76rn3g2c.js → index-47pj3aee.js} +4 -4
- package/{index-y4vkyhvv.js → index-5xdqxbn9.js} +14 -14
- package/{index-1577bej2.js → index-6wwzs1bc.js} +2 -2
- package/{index-wnp7hwq7.js → index-92zztqy1.js} +7 -7
- package/{index-jhpa13zj.js → index-9bwhxrqf.js} +57 -57
- package/{index-bjpxzr6s.js → index-9wtbw98m.js} +6 -3
- package/{index-fgc8r6dj.js → index-a5vmm6x4.js} +3 -1
- package/{index-xys926f2.js → index-by5gpp8x.js} +4 -4
- package/{index-kdh76j26.js → index-cezy7yb3.js} +100 -32
- package/{index-62955kn5.js → index-d61rcfa2.js} +758 -127
- package/{index-82jrh4hj.js → index-deekbtkj.js} +13 -13
- package/{index-rxkpk7n4.js → index-e82dyeqy.js} +4 -4
- package/{index-qzdjd4ct.js → index-h759vjfa.js} +18 -18
- package/{index-j37qq1f2.js → index-h7yw2z9n.js} +7 -7
- package/{index-qb61h0k7.js → index-jydtyzva.js} +1098 -1360
- package/{index-46tjzh6s.js → index-kcxmv48c.js} +29 -29
- package/{index-4m0bphac.js → index-kfnr6c19.js} +31 -30
- package/{index-xmmzf150.js → index-mpmyqm1y.js} +1 -1
- package/{index-xjd3rqzj.js → index-n68jx79w.js} +6 -15
- package/{index-0cj2zxbm.js → index-n85fer99.js} +12 -11
- package/{index-779ske1y.js → index-pgsswppj.js} +27 -27
- package/index-pvb4vdwr.js +159 -0
- package/{index-67546d0j.js → index-q0y2jq9j.js} +14 -2
- package/{index-z8ptfkzv.js → index-q8sn827n.js} +969 -240
- package/{index-5sn5w2mt.js → index-r50qen0z.js} +94 -61
- package/{index-eyyf2149.js → index-rntcdss1.js} +36 -37
- package/{index-gcv8ncfh.js → index-sjak5v5b.js} +16 -12
- package/{index-j9cxndc0.js → index-w8kmx1b6.js} +20 -11
- package/{index-jf2ya7r2.js → index-y74k5anj.js} +8 -8
- package/{index-d43t8af2.js → index-zkvpaggp.js} +58 -25
- package/index.js +22 -25
- package/{library.command-cv9gdbmg.js → library.command-em103wtn.js} +11 -10
- package/{localRegistry.command-d8fg36zc.js → localRegistry.command-gz8apn4y.js} +20 -23
- package/{module.command-qyr1s9cn.js → module.command-451pztkc.js} +13 -16
- package/{package.command-wsjx0k47.js → package.command-rfhyf38t.js} +8 -10
- package/package.json +5 -7
- package/{page.command-rdzg65rm.js → page.command-kt19da5g.js} +7 -9
- package/{primitive.command-89g38b53.js → primitive.command-pgy69ere.js} +13 -16
- package/{quality.command-0cs2z92d.js → quality.command-gpe2vqb5.js} +226 -105
- package/recipeScanner-fpagtgc1.js +11 -0
- package/{repair.command-6fphxfwj.js → repair.command-2sbrz187.js} +12 -14
- package/{routeSourceValidator-e4jmbfcx.js → routeSourceValidator-ze6x894c.js} +0 -2
- package/{scalar.command-xfer69s6.js → scalar.command-jcf4e5sv.js} +11 -14
- package/subspace.command-kqzqtk0p.js +755 -0
- package/templates/app/env/env.client.debug.ts.template +1 -5
- package/templates/app/env/env.client.develop.ts.template +1 -5
- package/templates/app/env/env.client.local.ts.template +1 -5
- package/templates/app/env/env.client.main.ts.template +1 -5
- package/templates/app/env/env.client.testing.ts.template +1 -5
- package/templates/app/env/env.client.type.ts +2 -6
- package/templates/app/env/env.server.debug.ts.template +1 -5
- package/templates/app/env/env.server.develop.ts.template +1 -5
- package/templates/app/env/env.server.local.ts.template +1 -5
- package/templates/app/env/env.server.main.ts.template +1 -5
- package/templates/app/main.ts +3 -1
- package/templates/appSample/lib/task/task.signal.ts +11 -6
- package/templates/appSample/srvkit/AuthGuard.ts +9 -5
- package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
- package/templates/crudPages/[__model__Id]/edit/page.tsx +5 -5
- package/templates/crudPages/[__model__Id]/page.tsx +10 -13
- package/templates/crudPages/new/page.tsx +5 -5
- package/templates/crudPages/page.tsx +9 -9
- package/templates/crudSinglePage/page.tsx +11 -7
- package/templates/env/_env.server.type.ts +7 -5
- package/templates/lib/db.ts +1 -1
- package/templates/lib/dict.ts +1 -1
- package/templates/lib/sig.ts +1 -1
- package/templates/lib/srv.ts +6 -2
- package/templates/module/__model__.dictionary.ts +4 -1
- package/templates/module/__model__.signal.ts +2 -3
- package/templates/module/__model__.store.ts +1 -2
- package/templates/workspaceRoot/.gitignore.template +0 -1
- package/templates/workspaceRoot/.husky/pre-commit.template +16 -0
- package/templates/workspaceRoot/.vscode/settings.json.template +1 -1
- package/templates/workspaceRoot/AGENTS.md.template +4 -0
- package/templates/workspaceRoot/CLAUDE.md.template +19 -0
- package/templates/workspaceRoot/biome.json.template +2 -1
- package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +10 -14
- package/templates/workspaceRoot/docs/GENERATED.md.template +12 -10
- package/templates/workspaceRoot/package.json.template +2 -4
- package/typeChecker-3xgrzc2v.js +7 -0
- package/typecheck.proc.js +4 -5
- package/{workflow.command-2ygfqt4f.js → workflow.command-m4z25a2p.js} +22 -25
- package/{workspace.command-88m0zs64.js → workspace.command-dqgj9zbk.js} +96 -81
- package/capacitorApp-9xswcgx7.js +0 -61
- package/dependencyScanner-f6grrep3.js +0 -9
- package/getDirname-m9s0cmhq.js +0 -8
- package/index-mxvakhsm.js +0 -66
- package/index-r24hmh0q.js +0 -4
- package/recipeScanner-tghkzrrt.js +0 -12
- package/typeChecker-kravn7ns.js +0 -8
- /package/{index-0wae5ebk.js → index-meh558jy.js} +0 -0
- /package/{index-ss469dec.js → index-nz8prx9g.js} +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Workspace Recipes",
|
|
3
|
+
"description": "Worked step-by-step recipes for common Akan changes, plus the auto-generated signal/service/store/document API reference",
|
|
4
|
+
"group": "core",
|
|
5
|
+
"codegenPriority": 5,
|
|
6
|
+
"deprecated": false,
|
|
7
|
+
"scans": [
|
|
8
|
+
{
|
|
9
|
+
"type": "example",
|
|
10
|
+
"description": "Module files a recipe walks through",
|
|
11
|
+
"path": "{apps,libs}/*/lib/*/*.{constant,document,service,signal,store}.ts",
|
|
12
|
+
"sample": 5
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"update": {
|
|
16
|
+
"filePath": "./workspaceRecipes.instruction.md",
|
|
17
|
+
"contents": [
|
|
18
|
+
"add a field",
|
|
19
|
+
"inject a dependency",
|
|
20
|
+
"create a slice",
|
|
21
|
+
"create a mutation",
|
|
22
|
+
"internal triggers",
|
|
23
|
+
"insights",
|
|
24
|
+
"auto-generated API reference"
|
|
25
|
+
],
|
|
26
|
+
"rules": [
|
|
27
|
+
"Use current Akan APIs only",
|
|
28
|
+
"Every recipe names the files to edit and the order to edit them in",
|
|
29
|
+
"Keep the <%= appName %> placeholder so the rendered guide names this workspace's app",
|
|
30
|
+
"Never duplicate a rule that workspaceOnboarding already states — link to it instead"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# Workspace Recipes — Worked Examples And The Auto-Generated API
|
|
2
|
+
|
|
3
|
+
Step-by-step recipes for the most frequent Akan.js changes, and the reference of what `akan sync` generates for
|
|
4
|
+
you at each layer. `workspaceOnboarding` carries the index and the rules; this is the worked code behind them.
|
|
5
|
+
Read the whole recipe before editing, and change only the sections it names — do not rewrite a file.
|
|
6
|
+
|
|
7
|
+
## Workflow Recipes
|
|
8
|
+
|
|
9
|
+
Concrete step-by-step recipes for the most frequent Akan.js changes. Each recipe shows which files to edit
|
|
10
|
+
and in what order. The code examples reference the `task` module in `apps/<app>/lib/task/` as a
|
|
11
|
+
template; replace `task` with your model name and `Task` with your PascalCase model name.
|
|
12
|
+
|
|
13
|
+
When editing a file, always read the existing content first. Only change the relevant sections — do not
|
|
14
|
+
rewrite the entire file.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
### Recipe 1: Adding a New Field to a Model
|
|
19
|
+
|
|
20
|
+
**Files to edit (in order):** `constant.ts` → `dictionary.ts` → `Template.tsx` → `Unit.tsx` → `akan sync`
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// 1. apps/<app>/lib/<model>/<model>.constant.ts
|
|
24
|
+
// Add field to the Input class. Use field() builder with optional defaults
|
|
25
|
+
export class TaskInput extends via((field) => ({
|
|
26
|
+
title: field(String),
|
|
27
|
+
priority: field(TaskPriority, { default: "medium" }), // NEW FIELD
|
|
28
|
+
})) {}
|
|
29
|
+
|
|
30
|
+
// If the new field should appear in list views, also add it to LightTask:
|
|
31
|
+
export class LightTask extends via(TaskObject, ["title", "priority", "status", "due"] as const, () => ({})) {}
|
|
32
|
+
|
|
33
|
+
// 2. apps/<app>/lib/<model>/<model>.dictionary.ts
|
|
34
|
+
// Add i18n labels for the new field (and its enum values if any).
|
|
35
|
+
// Labels are [en, ko] pairs, and nearly every one also carries a .desc([en, ko]).
|
|
36
|
+
.model<Task>((t) => ({
|
|
37
|
+
priority: t(["Priority", "우선순위"]).desc(["How urgent the task is", "할 일의 긴급도"]),
|
|
38
|
+
}))
|
|
39
|
+
.enum<TaskPriority>("taskPriority", (t) => ({
|
|
40
|
+
low: t(["Low", "낮음"]),
|
|
41
|
+
medium: t(["Medium", "보통"]),
|
|
42
|
+
high: t(["High", "높음"]),
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
// 3. apps/<app>/lib/<model>/<Model>.Template.tsx
|
|
46
|
+
// Add a form field using st.do.setXxxOnYyy (auto-generated setter)
|
|
47
|
+
const form = st.use.taskForm();
|
|
48
|
+
<Field.ToggleSelect
|
|
49
|
+
label={l("task.priority")}
|
|
50
|
+
items={cnst.TaskPriority}
|
|
51
|
+
value={form.priority}
|
|
52
|
+
onChange={st.do.setPriorityOnTask}
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
// 4. apps/<app>/lib/<model>/<Model>.Unit.tsx
|
|
56
|
+
// Display the new field in card/list views
|
|
57
|
+
<Badge variant={task.priority === "high" ? "error" : task.priority === "medium" ? "warning" : "default"}>
|
|
58
|
+
{task.priority}
|
|
59
|
+
</Badge>
|
|
60
|
+
|
|
61
|
+
// 5. Regenerate barrels
|
|
62
|
+
// akan sync <name>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### Recipe 2: Injecting a Dependency into a Service
|
|
68
|
+
|
|
69
|
+
Three patterns: injecting an **external adapter** (`use<>()`), another **module's service** (`service<>()`),
|
|
70
|
+
or a **predefined framework adapter** (`plug()`). A field named `<refName>Service` resolves to the service
|
|
71
|
+
registered under `<refName>` — the `Service`/`Signal` suffix is required and stripped to derive the lookup key.
|
|
72
|
+
|
|
73
|
+
> **For a new adapter you own, prefer the `adapt()` shape in pattern C over the `option.ts` registration in
|
|
74
|
+
> pattern A.** An `adapt()` class self-registers and is injected with `plug(TheClass)`, so it never touches
|
|
75
|
+
> `option.ts`. Pattern A is the legacy constructor-style shape: recognise it, keep it working, and migrate one
|
|
76
|
+
> only when you are already changing it.
|
|
77
|
+
|
|
78
|
+
> `apps/<app>/lib/option.ts` is a **user-owned** file scaffolded once — edit it to register adapters/DI. Unlike the
|
|
79
|
+
> barrels (`cnst.ts`, `db.ts`, `srv.ts`, …) it is **not** overwritten by `akan sync`, so your `.use(...)` registrations
|
|
80
|
+
> are safe.
|
|
81
|
+
|
|
82
|
+
**A. Adapter injection via `use<>()` (for external clients / global singletons)**
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// 1. Create the adapter class in apps/<app>/srvkit/
|
|
86
|
+
// apps/<app>/srvkit/EmailClient.ts
|
|
87
|
+
export class EmailClient {
|
|
88
|
+
constructor(readonly apiKey: string) {}
|
|
89
|
+
async send(opts: { to: string; subject: string; body: string }) { /* ... */ }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 2. Register in apps/<app>/lib/option.ts
|
|
93
|
+
export const option = new AkanOption()
|
|
94
|
+
.use((options) => ({
|
|
95
|
+
emailClient: new EmailClient(options.mailerApiKey),
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
// 3. Inject via use<>() in <model>.service.ts
|
|
99
|
+
export class TaskService extends serve(db.task, ({ use }) => ({
|
|
100
|
+
emailClient: use<EmailClient>(),
|
|
101
|
+
})) {
|
|
102
|
+
async _postCreate(task: cnst.Task) {
|
|
103
|
+
await this.emailClient.send({ to: "...", subject: "Task Created", body: `Task "${task.title}" created.` });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**B. Cross-module service injection via `service<>()` (for other Akan services)**
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// In <model>.service.ts — inject another module's service
|
|
112
|
+
import * as srv from "../srv";
|
|
113
|
+
|
|
114
|
+
export class TaskService extends serve(db.task, ({ service }) => ({
|
|
115
|
+
notiService: service<srv.NotiService>(),
|
|
116
|
+
})) {
|
|
117
|
+
async _postCreate(task: cnst.Task) {
|
|
118
|
+
await this.notiService.send("info", `Task "${task.title}" created`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**C. Predefined framework adapter injection via `plug()` (storage, cache, queue, schedule, …)**
|
|
124
|
+
|
|
125
|
+
Akan ships predefined adapter roles from `akanjs/service`: `StorageAdaptorRole`, `CacheAdaptorRole`,
|
|
126
|
+
`QueueAdaptorRole`, `ScheduleAdaptorRole`, `DatabaseAdaptorRole`, `WebsocketAdaptorRole`,
|
|
127
|
+
`LoggingAdaptorRole`, `CompressAdaptorRole`. `plug()` injects the concrete adapter bound to that role (the
|
|
128
|
+
default `StorageAdaptor` binding is `BlobStorage`). `plug()` also accepts a concrete adapter class directly.
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// In <model>.service.ts — inject the framework storage adapter by role
|
|
132
|
+
import { plug, serve, StorageAdaptorRole } from "akanjs/service";
|
|
133
|
+
|
|
134
|
+
export class TaskService extends serve(db.task, ({ plug }) => ({
|
|
135
|
+
storage: plug(StorageAdaptorRole),
|
|
136
|
+
})) {
|
|
137
|
+
async attach(taskId: string, path: string, localPath: string) {
|
|
138
|
+
// BlobStorage returns a URL under blobStorage.urlPrefix (default "/api/localFile/getBlob").
|
|
139
|
+
return await this.storage.uploadDataFromLocal({ path, localPath });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
For a custom adapter class (not a predefined role), pass the class itself, e.g. `ipfsApi: plug(IpfsApi)`.
|
|
145
|
+
Injecting a file/image field is usually simpler than calling
|
|
146
|
+
storage directly: declare `image: field(File).optional()` (or `images: field([File])`) on the model and let the
|
|
147
|
+
store's generated `upload<Field>On<Model>(fileList)` action handle the upload. Add `{ cascade: "removeRef" }` to that
|
|
148
|
+
field when the file belongs to the model alone, and removing the model removes the file and its stored object.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### Recipe 3: Creating and Using a Slice
|
|
153
|
+
|
|
154
|
+
A Slice is a named, filtered data view. Add file entries and connect from a page.
|
|
155
|
+
|
|
156
|
+
> **Silent failure — a slice `exec` must return a query descriptor, never an executed list.**
|
|
157
|
+
> Return `this.taskService.queryByStatuses(...)` (the `query<Filter>` builder), **not**
|
|
158
|
+
> `this.taskService.listByStatuses(...)` / `listBy...(...)` (which returns a `Promise<Doc[]>`).
|
|
159
|
+
> Returning an array type-checks but throws at runtime during insight aggregation with the opaque
|
|
160
|
+
> `Error: Unknown document field path: 0`. If you see that error, your slice is returning a list, not a query.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// 1. apps/<app>/lib/<model>/<model>.signal.ts — Define the slice
|
|
164
|
+
export class TaskSlice extends slice(srv.task, { guards: { root: Admin, get: SignedIn, cru: SignedIn } }, (init) => ({
|
|
165
|
+
inTodo: init({ guards: [SignedIn] })
|
|
166
|
+
.search("statuses", [cnst.TaskStatus])
|
|
167
|
+
.exec(function (statuses?) {
|
|
168
|
+
// ✅ query<Filter> — a query descriptor. ❌ listByStatuses(...) returns an array and fails at runtime.
|
|
169
|
+
return this.taskService.queryByStatuses(statuses ?? ["todo", "inProgress"]);
|
|
170
|
+
}),
|
|
171
|
+
})) {}
|
|
172
|
+
|
|
173
|
+
// 2. apps/<app>/lib/<model>/<model>.document.ts — Add query filter for slice
|
|
174
|
+
export class TaskFilter extends from(cnst.Task, (filter) => ({
|
|
175
|
+
query: {
|
|
176
|
+
byStatuses: filter()
|
|
177
|
+
.arg("statuses", [cnst.TaskStatus])
|
|
178
|
+
.query((statuses) => ({ status: { $in: statuses } })),
|
|
179
|
+
},
|
|
180
|
+
})) {}
|
|
181
|
+
|
|
182
|
+
// 3. apps/<app>/lib/<model>/<model>.dictionary.ts — Slice labels
|
|
183
|
+
.slice<TaskSlice>((fn) => ({
|
|
184
|
+
inTodo: fn(["Tasks In Todo", "할 일"]).arg((t) => ({
|
|
185
|
+
statuses: t(["Statuses", "상태"]),
|
|
186
|
+
})),
|
|
187
|
+
}))
|
|
188
|
+
|
|
189
|
+
// 4. In page — Init the slice in an async Page and hand the init to a Zone.
|
|
190
|
+
export default async function Page() {
|
|
191
|
+
const [{ taskInitInTodo }] = await Promise.all([fetch.initTaskInTodo()]);
|
|
192
|
+
return <Task.Zone.Card init={taskInitInTodo} sliceName="taskInTodo" />;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The slice name in code uses camelCase (`inTodo`). In dictionary and components it becomes `"taskInTodo"`.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### Recipe 4: Creating a Mutation Endpoint (with Status Workflow)
|
|
201
|
+
|
|
202
|
+
**Files to edit (in order):** `document.ts` → `service.ts` → `signal.ts` → `dictionary.ts` → `store.ts` → `Util.tsx`
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// 1. <model>.document.ts — Document chain method with state validation
|
|
206
|
+
export class TaskDocument extends by(cnst.Task) {
|
|
207
|
+
start() {
|
|
208
|
+
if (this.status !== "todo") throw new Err("task.error.cannotStartFromNonTodo");
|
|
209
|
+
this.status = "inProgress";
|
|
210
|
+
return this; // Return this for chaining: task.start().save()
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 2. <model>.service.ts — Service method wrapping document
|
|
215
|
+
async startTask(taskId: string) {
|
|
216
|
+
const task = await this.getTask(taskId);
|
|
217
|
+
return task.start().save();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 3. <model>.signal.ts — Mutation endpoint
|
|
221
|
+
export class TaskEndpoint extends endpoint(srv.task, ({ mutation }) => ({
|
|
222
|
+
startTask: mutation(cnst.Task, { guards: [SignedIn] })
|
|
223
|
+
.param("taskId", ID)
|
|
224
|
+
.exec(async function (taskId) {
|
|
225
|
+
return await this.taskService.startTask(taskId);
|
|
226
|
+
}),
|
|
227
|
+
})) {}
|
|
228
|
+
|
|
229
|
+
// 4. <model>.dictionary.ts — Endpoint + error labels
|
|
230
|
+
.endpoint<TaskEndpoint>((fn) => ({
|
|
231
|
+
startTask: fn(["Start Task", "작업시작"])
|
|
232
|
+
.arg((t) => ({ taskId: t(["Task ID", "할 일 ID"]) })),
|
|
233
|
+
}))
|
|
234
|
+
.error({
|
|
235
|
+
cannotStartFromNonTodo: ["Task can only start from todo status", "할 일 상태에서만 시작 가능"],
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// 5. <model>.store.ts — Client-side action with toast feedback
|
|
239
|
+
async startTask(taskId: string) {
|
|
240
|
+
msg.loading("task.startTaskLoading", { key: "startTask" });
|
|
241
|
+
const task = await fetch.startTask(taskId);
|
|
242
|
+
this.setTask(task); // Auto-generated: updates task state in store
|
|
243
|
+
msg.success("task.startTaskSuccess", { key: "startTask" });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 6. <Model>.Util.tsx — Reusable button component
|
|
247
|
+
export const Start = ({ taskId }: { taskId: string }) => (
|
|
248
|
+
<button className={buttonRecipe({ variant: "primary", size: "xs" })} onClick={() => st.do.startTask(taskId)}>
|
|
249
|
+
Start
|
|
250
|
+
</button>
|
|
251
|
+
);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### Recipe 5: Internal Triggers — Interval & Cron
|
|
257
|
+
|
|
258
|
+
Server-side background jobs. Defined in `internal()` signal, implemented in service.
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// 1. <model>.signal.ts — Define triggers
|
|
262
|
+
export class TaskInternal extends internal(srv.task, ({ interval, cron }) => ({
|
|
263
|
+
cleanupStaleTasks: interval(10000).exec(async function () {
|
|
264
|
+
await this.taskService.cleanupStaleTasks();
|
|
265
|
+
}),
|
|
266
|
+
|
|
267
|
+
dailyDigest: cron("0 0 * * *").exec(async function () {
|
|
268
|
+
await this.taskService.sendDailyDigest();
|
|
269
|
+
}),
|
|
270
|
+
})) {}
|
|
271
|
+
|
|
272
|
+
// 2. <model>.service.ts — Implement the logic
|
|
273
|
+
async cleanupStaleTasks() {
|
|
274
|
+
const weekAgo = dayjs().subtract(7, "day").toDate();
|
|
275
|
+
const stale = await this.taskModel.listDueBefore(weekAgo);
|
|
276
|
+
for (const task of stale) {
|
|
277
|
+
await task.remove();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Available trigger types:**
|
|
283
|
+
- `interval(ms)` — runs repeatedly at the given interval
|
|
284
|
+
- `cron("min hour dom month dow")` — runs on a schedule
|
|
285
|
+
- `initialize()` — runs once on service startup
|
|
286
|
+
- `process(Return).msg(Type)` — message queue consumer
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
### Recipe 6: Creating an Insight (Aggregation / Dashboard Stats)
|
|
291
|
+
|
|
292
|
+
Insights display aggregated statistics across model data.
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// 1. <model>.constant.ts — Insight class with accumulate rules
|
|
296
|
+
export class TaskInsight extends via(Task, (field) => ({
|
|
297
|
+
totalCount: field(Int, { default: 0, accumulate: {} }),
|
|
298
|
+
completedCount: field(Int, { default: 0, accumulate: { status: "completed" } }),
|
|
299
|
+
})) {}
|
|
300
|
+
|
|
301
|
+
// 2. <model>.dictionary.ts — Insight field labels
|
|
302
|
+
.insight<TaskInsight>((t) => ({
|
|
303
|
+
totalCount: t(["Total Tasks", "전체 할 일"]),
|
|
304
|
+
completedCount: t(["Completed", "완료됨"]),
|
|
305
|
+
}))
|
|
306
|
+
|
|
307
|
+
// 3. <Model>.View.tsx — Display component consuming an Insight model
|
|
308
|
+
export const Stats = ({ taskInsight }: { taskInsight: cnst.TaskInsight }) => (
|
|
309
|
+
<div className="grid grid-cols-2 gap-4">
|
|
310
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
311
|
+
<div className="text-muted-foreground text-sm">{l("task.totalCount")}</div>
|
|
312
|
+
<div className="font-bold text-2xl text-primary">{taskInsight.totalCount}</div>
|
|
313
|
+
</div>
|
|
314
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
315
|
+
<div className="text-muted-foreground text-sm">{l("task.completedCount")}</div>
|
|
316
|
+
<div className="font-bold text-2xl text-success">{taskInsight.completedCount}</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// 4. <Model>.Zone.tsx — Mount in page via Load.Insight bound to a slice
|
|
322
|
+
export const Insight = ({ sliceName }: { sliceName: string }) => {
|
|
323
|
+
const insight = st.slice[sliceName].use.taskInsight();
|
|
324
|
+
if (!insight) return null;
|
|
325
|
+
return <Task.View.Stats taskInsight={insight} />;
|
|
326
|
+
};
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
### Data Flow Summary
|
|
332
|
+
|
|
333
|
+
For each business question, follow this chain:
|
|
334
|
+
|
|
335
|
+
| Question | File Pattern |
|
|
336
|
+
|----------|-------------|
|
|
337
|
+
| What fields does it have? | `constant.ts` — `via()` layers: Input → Object → Light → Model → Insight |
|
|
338
|
+
| How is it stored/searched? | `document.ts` — `from()` filters + `by()` document methods + `into()` model |
|
|
339
|
+
| What business rule should run? | `service.ts` — `serve()` with DI (`use`, `service`, `plug`, `env`, `memory`) |
|
|
340
|
+
| What should a page call? | `signal.ts` — `internal()` (jobs), `endpoint()` (APIs), `slice()` (data views) |
|
|
341
|
+
| What client state is shared? | `store.ts` — `store()` with auto-generated form/insight state + custom actions |
|
|
342
|
+
| What should users see? | `View.tsx` + `Zone.tsx` (detail/container), `Template.tsx` (forms), `Unit.tsx` (cards), `Util.tsx` (buttons) |
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
## Auto-Generated API Reference
|
|
346
|
+
|
|
347
|
+
akan sync automatically generates APIs across all layers. Only write custom logic — never hand-write what the framework generates.
|
|
348
|
+
|
|
349
|
+
### Signal — Endpoint Auto-Generation
|
|
350
|
+
|
|
351
|
+
| Auto-Generated | Signature | Description |
|
|
352
|
+
|---------------|-----------|-------------|
|
|
353
|
+
| `view[Model](id)` | `fetch.viewTask(id)` | Fetch single model for detail view |
|
|
354
|
+
| `edit[Model](id)` | `fetch.editTask(id)` | Fetch model for edit view |
|
|
355
|
+
| `merge[Model](id, data)` | `fetch.mergeTask(data)` | Create (no id) or update (with id) model |
|
|
356
|
+
| `[model]List[Suffix](args, skip, limit, sort)` | `fetch.taskListInTodo(args)` | Paginated list from slice filter |
|
|
357
|
+
| `[model]Insight[Suffix](args)` | `fetch.taskInsightInTodo(args)` | Aggregated insight from slice query |
|
|
358
|
+
| `init[Model][Suffix](args)` | `fetch.initTaskInTodo(args)` | Initialize slice with list + insight |
|
|
359
|
+
|
|
360
|
+
**Rule**: Only define `query()`, `mutation()`, `message()`, `pubsub()` endpoints manually when the endpoint needs custom business logic. Standard CRUD is already auto-generated.
|
|
361
|
+
|
|
362
|
+
### Service — CRUD & Query Auto-Generation
|
|
363
|
+
|
|
364
|
+
| Auto-Generated | Description |
|
|
365
|
+
|---------------|-------------|
|
|
366
|
+
| `this.<model>Model` | Auto-injected model adaptor |
|
|
367
|
+
| `get<Model>(id)`, `load<Model>(id)` | Single document lookup |
|
|
368
|
+
| `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`) |
|
|
369
|
+
| `list<Query>(args)`, `find<Query>(args)`, `pick<Query>(args)` | Filter-based queries |
|
|
370
|
+
| `exists<Query>(args)`, `count<Query>(args)`, `insight<Query>(args)` | Filter-based helpers |
|
|
371
|
+
| `_preCreate`, `_postCreate`, `_preUpdate`, `_postUpdate`, `_preRemove`, `_postRemove` | Lifecycle hooks (override to add logic) |
|
|
372
|
+
|
|
373
|
+
**Rule**: Use `_preCreate`/`_postCreate` lifecycle hooks for side effects (e.g., push workHistory entries). Write custom service methods only for multi-model orchestration.
|
|
374
|
+
|
|
375
|
+
### Store — State & Action Auto-Generation
|
|
376
|
+
|
|
377
|
+
| Auto-Generated | Description |
|
|
378
|
+
|---------------|-------------|
|
|
379
|
+
| `[model]` (cached full model), `[model]Loading`, `[model]Form`, `[model]Modal` | Base model states |
|
|
380
|
+
| `create[Model](data)`, `update[Model](id, data)`, `remove[Model](id)` | CRUD actions |
|
|
381
|
+
| `new[Model](partial)`, `edit[Model](model)`, `view[Model](model)` | Form/view state actions |
|
|
382
|
+
| `[slice]List`, `[slice]InitList`, `[slice]Insight`, `[slice]Selection` | Slice states |
|
|
383
|
+
| `init[Slice](args)`, `refresh[Slice]()`, `setPageOf[Slice](page)` | Slice actions |
|
|
384
|
+
| `set[Field]On[Model](value)` | Auto-setters for each model field |
|
|
385
|
+
|
|
386
|
+
**Rule**: Write custom store actions only for toast messages (`msg.loading`/`msg.success`) or multi-step workflows. State fields and CRUD actions are already auto-generated.
|
|
387
|
+
|
|
388
|
+
### Document — Filter Query Auto-Generation
|
|
389
|
+
|
|
390
|
+
| Auto-Generated (from Filter definition) | Description |
|
|
391
|
+
|----------------------------------------|-------------|
|
|
392
|
+
| `list[Query](args)`, `listIds[Query](args)` | List documents matching filter |
|
|
393
|
+
| `find[Query](args)`, `findId[Query](args)` | Find one (null if not found) |
|
|
394
|
+
| `pick[Query](args)`, `pickId[Query](args)` | Find one (throw if not found) |
|
|
395
|
+
| `exists[Query](args)`, `count[Query](args)` | Existence check and count |
|
|
396
|
+
| `insight[Query](args)`, `query[Query](args)` | Insight and raw query |
|
|
397
|
+
| `remove[Query](args)`, `removeOne[Query](args)` | Query-level soft remove — all matches, or the newest one (`createdAt` desc, not caller-chosen) |
|
|
398
|
+
| `update[Query](args).set(patch)`, `updateOne[Query](args).set(patch)` | Query-level update — the patch lands on a terminal `.set()`, because a filter's trailing args may be optional |
|
|
399
|
+
|
|
400
|
+
**Rule**: Define `Filter` with `.query()` conditions in `document.ts`. akan sync auto-generates all 14 query helper methods per filter. Write `Document` chain methods only for state transitions with validation.
|
|
401
|
+
|
|
402
|
+
**The four query-level writes fire no hooks**, so no `_pre`/`_postRemove` and no cascade run — same as `updateManyByQuery`. Reach for them when the model carries no removal side effect; otherwise remove documents one at a time. A filter keyed after its own model (filter `chat` on model `chat`) is rejected at boot, because `removeChat`/`updateChat` would otherwise shadow the generated single-document CRUD.
|
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
CodegenLock,
|
|
4
|
+
WorkspaceExecutor,
|
|
5
|
+
AppExecutor
|
|
6
|
+
} from "./index-q8sn827n.js";
|
|
7
|
+
import"./index-q0y2jq9j.js";
|
|
8
|
+
import"./index-6wwzs1bc.js";
|
|
5
9
|
import {
|
|
10
|
+
GraphClientEntryDiscovery,
|
|
11
|
+
RouteClientBuilder,
|
|
6
12
|
AutoImportSync,
|
|
7
13
|
DevChangePlanner,
|
|
8
|
-
DevGeneratedIndexSync
|
|
9
|
-
|
|
10
|
-
RouteClientBuilder
|
|
11
|
-
} from "./index-j9cxndc0.js";
|
|
14
|
+
DevGeneratedIndexSync
|
|
15
|
+
} from "./index-w8kmx1b6.js";
|
|
12
16
|
import {
|
|
13
17
|
HmrWatcher,
|
|
14
18
|
WatchRootResolver
|
|
15
|
-
} from "./index-
|
|
19
|
+
} from "./index-9wtbw98m.js";
|
|
16
20
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from "./index-z8ptfkzv.js";
|
|
20
|
-
import"./index-mxvakhsm.js";
|
|
21
|
-
import"./index-1577bej2.js";
|
|
22
|
-
import"./index-67546d0j.js";
|
|
23
|
-
import"./index-r24hmh0q.js";
|
|
21
|
+
prepareDevWatchBatch
|
|
22
|
+
} from "./index-a5vmm6x4.js";
|
|
24
23
|
|
|
25
24
|
// pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts
|
|
26
25
|
import path2 from "path";
|
|
@@ -68,13 +67,13 @@ class BuildBatchRunner {
|
|
|
68
67
|
env: process.env,
|
|
69
68
|
stdio: ["ignore", "inherit", "inherit"],
|
|
70
69
|
serialization: "advanced",
|
|
71
|
-
ipc: (
|
|
72
|
-
if (!
|
|
70
|
+
ipc: (message) => {
|
|
71
|
+
if (!message || typeof message !== "object")
|
|
73
72
|
return;
|
|
74
|
-
if (
|
|
75
|
-
result =
|
|
73
|
+
if (message.type === "build-batch-result")
|
|
74
|
+
result = message.data;
|
|
76
75
|
else
|
|
77
|
-
onMessage(
|
|
76
|
+
onMessage(message);
|
|
78
77
|
}
|
|
79
78
|
});
|
|
80
79
|
const exitCode = await proc.exited;
|
|
@@ -250,7 +249,8 @@ class IncrementalBuilder {
|
|
|
250
249
|
return;
|
|
251
250
|
this.#shuttingDown = true;
|
|
252
251
|
const started = Date.now();
|
|
253
|
-
this.#logger.
|
|
252
|
+
this.#logger.debug(`shutdown requested (${reason}); draining ${this.#inFlight} work item(s)`);
|
|
253
|
+
this.#watcher?.stop();
|
|
254
254
|
if (this.#cssRebuildTimer) {
|
|
255
255
|
clearTimeout(this.#cssRebuildTimer);
|
|
256
256
|
this.#cssRebuildTimer = null;
|
|
@@ -263,7 +263,7 @@ class IncrementalBuilder {
|
|
|
263
263
|
return;
|
|
264
264
|
});
|
|
265
265
|
const flushed = await BuilderChannel.drain();
|
|
266
|
-
this.#logger.
|
|
266
|
+
this.#logger.debug(`drained in ${Date.now() - started}ms${flushed ? ` after flushing ${flushed} ipc write(s)` : ""}; exiting for recycle`);
|
|
267
267
|
process.exit(0);
|
|
268
268
|
}
|
|
269
269
|
get shuttingDown() {
|
|
@@ -353,20 +353,21 @@ class IncrementalBuilder {
|
|
|
353
353
|
if (rawKinds.size === 0)
|
|
354
354
|
return;
|
|
355
355
|
const generation = ++this.#generation;
|
|
356
|
-
const autoImport = await this.#
|
|
356
|
+
const [autoImport, indexSync] = await CodegenLock.run(this.#app.workspace.workspaceRoot, `hmr-batch:${this.#app.name}`, async () => {
|
|
357
|
+
const auto = await this.#autoImportSync.syncForBatch(batch.files);
|
|
358
|
+
return [auto, await this.#generatedIndexSync.syncForBatch(batch.files)];
|
|
359
|
+
});
|
|
357
360
|
for (const error of autoImport.errors)
|
|
358
361
|
this.#logger.error(error);
|
|
359
362
|
if (autoImport.changedFiles.length > 0)
|
|
360
363
|
this.#logger.verbose(`[auto-import] inserted imports into ${autoImport.changedFiles.length} file(s)`);
|
|
361
|
-
const indexSync = await this.#generatedIndexSync.syncForBatch(batch.files);
|
|
362
364
|
await this.#watcher?.absorb([...autoImport.changedFiles, ...indexSync.changedFiles]);
|
|
363
|
-
const { files, kinds, expandedBatch, event, hasSyncErrors } = prepareDevWatchBatch({
|
|
365
|
+
const { files, kinds, expandedBatch, devPlan, event, hasSyncErrors } = prepareDevWatchBatch({
|
|
364
366
|
generation,
|
|
365
367
|
batch,
|
|
366
368
|
indexSync,
|
|
367
369
|
changePlanner: this.#changePlanner
|
|
368
370
|
});
|
|
369
|
-
const devPlan = event.devPlan;
|
|
370
371
|
this.#logger.verbose(`[hmr] batch generation=${generation} kinds=${kinds.join(",")} files=${files.length} generated=${indexSync.changedFiles.length} roles=${devPlan.roles.join(",") || "(none)"} actions=${devPlan.actions.join(",") || "(none)"}`);
|
|
371
372
|
for (const error of indexSync.errors)
|
|
372
373
|
this.#logger.error(error);
|
|
@@ -1,38 +1,45 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
-
workflowRunArtifactPath,
|
|
4
|
-
workflowSyncDir
|
|
5
|
-
} from "./index-rxkpk7n4.js";
|
|
6
|
-
import {
|
|
7
|
-
AppExecutor,
|
|
8
|
-
LibExecutor,
|
|
9
|
-
appRootAllowedDirs,
|
|
10
|
-
appRootAllowedFiles,
|
|
11
3
|
extractBlockVersion,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from "./index-xys926f2.js";
|
|
4
|
+
readDevkitVersion,
|
|
5
|
+
rootAllowedFiles,
|
|
6
|
+
rootAllowedDirs,
|
|
7
|
+
isScannedRootEntry,
|
|
8
|
+
AppExecutor,
|
|
9
|
+
LibExecutor
|
|
10
|
+
} from "./index-q8sn827n.js";
|
|
20
11
|
import {
|
|
21
12
|
FileSys
|
|
22
|
-
} from "./index-
|
|
13
|
+
} from "./index-q0y2jq9j.js";
|
|
14
|
+
import {
|
|
15
|
+
collectRecipeSources2,
|
|
16
|
+
scanRecipes2,
|
|
17
|
+
findInlineRecipeDuplicates2
|
|
18
|
+
} from "./index-by5gpp8x.js";
|
|
19
|
+
import {
|
|
20
|
+
workflowRunArtifactPath,
|
|
21
|
+
workflowSyncDir
|
|
22
|
+
} from "./index-e82dyeqy.js";
|
|
23
23
|
|
|
24
24
|
// pkgs/@akanjs/devkit/akanContext.ts
|
|
25
25
|
import { readdir } from "fs/promises";
|
|
26
26
|
import path from "path";
|
|
27
27
|
import { capitalize } from "akanjs/common";
|
|
28
|
-
var
|
|
28
|
+
var guidelineResourceUri = (name) => `akan://guidelines/${name}`;
|
|
29
|
+
var staticResourceList = [
|
|
29
30
|
{ uri: "akan://docs/framework", name: "Akan framework guide", mimeType: "text/markdown" },
|
|
30
|
-
{ uri: "akan://guidelines/framework", name: "Framework guideline", mimeType: "text/markdown" },
|
|
31
|
-
{ uri: "akan://guidelines/modelSignal", name: "Model signal guideline", mimeType: "text/markdown" },
|
|
32
31
|
{ uri: "akan://workspace/summary", name: "Workspace summary", mimeType: "application/json" },
|
|
33
32
|
{ uri: "akan://workspace/apps", name: "Workspace apps", mimeType: "application/json" },
|
|
34
33
|
{ uri: "akan://workspace/modules", name: "Workspace modules", mimeType: "application/json" }
|
|
35
34
|
];
|
|
35
|
+
var buildResourceList = (guidelineNames) => [
|
|
36
|
+
...staticResourceList,
|
|
37
|
+
...guidelineNames.map((name) => ({
|
|
38
|
+
uri: guidelineResourceUri(name),
|
|
39
|
+
name: `${name} guideline`,
|
|
40
|
+
mimeType: "text/markdown"
|
|
41
|
+
}))
|
|
42
|
+
];
|
|
36
43
|
var cursorMcpConfigPath = ".cursor/mcp.json";
|
|
37
44
|
var claudeMcpConfigPath = ".mcp.json";
|
|
38
45
|
var codexMcpConfigPath = ".codex/config.toml";
|
|
@@ -404,19 +411,19 @@ class AkanContextAnalyzer {
|
|
|
404
411
|
repairAction("generated", "akan repair generated --app <app-or-lib>", "Refresh generated Akan files.", true),
|
|
405
412
|
repairAction("format", "akan repair format --target <app-or-lib-or-pkg>", "Run the formatter/linter repair path.", true)
|
|
406
413
|
];
|
|
407
|
-
for (const
|
|
408
|
-
const
|
|
409
|
-
for (const entry of await safeReadDir(
|
|
410
|
-
if (!
|
|
414
|
+
for (const sys of [...context.apps, ...context.libs]) {
|
|
415
|
+
const sysPath = path.join(workspace.workspaceRoot, sys.path);
|
|
416
|
+
for (const entry of await safeReadDir(sysPath)) {
|
|
417
|
+
if (!isScannedRootEntry(sys.type, entry.name))
|
|
411
418
|
continue;
|
|
412
|
-
const allowed = entry.isDirectory() ?
|
|
419
|
+
const allowed = entry.isDirectory() ? rootAllowedDirs[sys.type].has(entry.name) : rootAllowedFiles[sys.type].has(entry.name);
|
|
413
420
|
if (!allowed) {
|
|
414
|
-
const action = repairAction("module-shape", `akan repair module-shape --app ${
|
|
421
|
+
const action = repairAction("module-shape", `akan repair module-shape --app ${sys.name}`, `Review ${sys.type} root shape and remove or move the unknown entry.`, false);
|
|
415
422
|
diagnostics.push({
|
|
416
423
|
severity: "error",
|
|
417
|
-
code:
|
|
418
|
-
path: `${
|
|
419
|
-
message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in
|
|
424
|
+
code: `${sys.type}-root-unknown-entry`,
|
|
425
|
+
path: `${sys.path}/${entry.name}`,
|
|
426
|
+
message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in ${sys.type} root: ${sys.path}/${entry.name}`,
|
|
420
427
|
repairActions: [action]
|
|
421
428
|
});
|
|
422
429
|
repairActions.push(action);
|
|
@@ -486,10 +493,10 @@ class AkanContextAnalyzer {
|
|
|
486
493
|
}
|
|
487
494
|
}
|
|
488
495
|
for (const sys of [...context.apps, ...context.libs]) {
|
|
489
|
-
const sources = await
|
|
496
|
+
const sources = await collectRecipeSources2(path.join(workspace.workspaceRoot, sys.path, "ui"), "ui");
|
|
490
497
|
if (sources.length === 0)
|
|
491
498
|
continue;
|
|
492
|
-
const recipes =
|
|
499
|
+
const recipes = scanRecipes2(sources);
|
|
493
500
|
const files = [];
|
|
494
501
|
const glob = new Bun.Glob("**/*.tsx");
|
|
495
502
|
for await (const abs of glob.scan({ cwd: path.join(workspace.workspaceRoot, sys.path), absolute: true })) {
|
|
@@ -500,7 +507,7 @@ class AkanContextAnalyzer {
|
|
|
500
507
|
content: await Bun.file(abs).text().catch(() => "")
|
|
501
508
|
});
|
|
502
509
|
}
|
|
503
|
-
const duplicates =
|
|
510
|
+
const duplicates = findInlineRecipeDuplicates2(recipes, files);
|
|
504
511
|
if (duplicates.length === 0)
|
|
505
512
|
continue;
|
|
506
513
|
const preview = duplicates.slice(0, 3).map((duplicate) => `${path.relative(workspace.workspaceRoot, duplicate.path)}:${duplicate.line}`).join(", ");
|
|
@@ -511,7 +518,7 @@ class AkanContextAnalyzer {
|
|
|
511
518
|
message: `${sys.name}: ${duplicates.length} inline className(s) re-author a recipe fingerprint (${preview}${duplicates.length > 3 ? ", \u2026" : ""}) \u2014 consume the recipe instead.`
|
|
512
519
|
});
|
|
513
520
|
}
|
|
514
|
-
const scanNames = async (uiDirPath, basename) => new Set(
|
|
521
|
+
const scanNames = async (uiDirPath, basename) => new Set(scanRecipes2(await collectRecipeSources2(uiDirPath, "ui", basename)).map((info) => info.name));
|
|
515
522
|
const declaredByImport = new Map;
|
|
516
523
|
declaredByImport.set("akanjs/ui", await scanNames(path.join(workspace.workspaceRoot, "pkgs/akanjs/ui"), "recipe"));
|
|
517
524
|
for (const sys of [...context.apps, ...context.libs])
|
|
@@ -626,4 +633,4 @@ class AkanContextAnalyzer {
|
|
|
626
633
|
}
|
|
627
634
|
}
|
|
628
635
|
|
|
629
|
-
export {
|
|
636
|
+
export { buildResourceList, codexMcpConfigPath, akanMcpInstallTargets, akanMcpInstallConfigPaths, createAkanMcpServer, createAkanCodexMcpServerBlock, upsertCodexMcpServerBlock, renderDoctorText, AkanContextAnalyzer };
|