@akanjs/cli 3.0.0-alpha.8 → 3.0.0-alpha.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/.build-stamp +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/{abstractCompactor-e5yq60qz.js → abstractCompactor-pwc6d6yk.js} +3 -5
  5. package/{agent.command-pggr92jj.js → agent.command-180hh0ye.js} +11 -12
  6. package/{application.command-692yap1d.js → application.command-gmcgen9y.js} +59 -35
  7. package/{applicationBuildRunner-ha3y8ew8.js → applicationBuildRunner-tavpx3fv.js} +22 -20
  8. package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-ktjt0erc.js} +8 -9
  9. package/buildBatch.proc.js +12 -13
  10. package/capacitorApp-kwa67kzs.js +12 -0
  11. package/{cloud.command-wqfsyzv2.js → cloud.command-gpq7qxrc.js} +15 -17
  12. package/commandManifest.json +1 -1
  13. package/{context.command-9bxtdn0w.js → context.command-dk2m2sjx.js} +19 -22
  14. package/dependencyScanner-c64m6fxp.js +8 -0
  15. package/fleet.command-3bkyyd3a.js +836 -0
  16. package/getDirname-dr3jz1xg.js +7 -0
  17. package/{guideline.command-nfqahxex.js → guideline.command-w6ddv13r.js} +16 -17
  18. package/guidelines/agentRule/agentRule.generate.json +31 -0
  19. package/guidelines/agentRule/agentRule.instruction.md +525 -0
  20. package/guidelines/conventions/conventions.generate.json +25 -0
  21. package/guidelines/conventions/conventions.instruction.md +847 -0
  22. package/guidelines/cssRule/cssRule.instruction.md +7 -0
  23. package/guidelines/framework/framework.generate.json +3 -2
  24. package/guidelines/framework/framework.instruction.md +1 -9
  25. package/guidelines/mcpRule/mcpRule.generate.json +25 -0
  26. package/guidelines/mcpRule/mcpRule.instruction.md +217 -0
  27. package/guidelines/queryRule/queryRule.generate.json +31 -0
  28. package/guidelines/queryRule/queryRule.instruction.md +143 -0
  29. package/guidelines/runtimeRule/runtimeRule.generate.json +25 -0
  30. package/guidelines/runtimeRule/runtimeRule.instruction.md +176 -0
  31. package/guidelines/ssrRule/ssrRule.instruction.md +49 -5
  32. package/guidelines/transportRule/transportRule.generate.json +31 -0
  33. package/guidelines/transportRule/transportRule.instruction.md +60 -0
  34. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +24 -0
  35. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +263 -0
  36. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +33 -0
  37. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +402 -0
  38. package/incrementalBuilder.proc.js +21 -22
  39. package/{index-4hek879p.js → index-1kzsn94d.js} +1 -1
  40. package/{index-p3f4q532.js → index-3c9n3nt8.js} +31 -30
  41. package/index-3d9y066c.js +76 -0
  42. package/{index-76rn3g2c.js → index-47pj3aee.js} +4 -4
  43. package/{index-gex7c8kd.js → index-5n0hf8r9.js} +65 -38
  44. package/{index-mssqn2sq.js → index-5y7qba1c.js} +79 -33
  45. package/index-6wwzs1bc.js +7 -0
  46. package/{index-wnp7hwq7.js → index-92zztqy1.js} +7 -7
  47. package/{index-fgc8r6dj.js → index-a5vmm6x4.js} +3 -1
  48. package/{index-azv0xjb5.js → index-a8h184wr.js} +15 -11
  49. package/{index-67546d0j.js → index-ayy5ecax.js} +1 -1
  50. package/{index-xhgtkekh.js → index-b2nt0w0k.js} +27 -27
  51. package/{index-xys926f2.js → index-by5gpp8x.js} +4 -4
  52. package/{index-bz2sfzzt.js → index-cd5x18qh.js} +32 -32
  53. package/{index-bjpxzr6s.js → index-ctphds8v.js} +1 -1
  54. package/{index-w6wbb612.js → index-dq6wq1tv.js} +94 -61
  55. package/{index-js368gk9.js → index-ee8z8adx.js} +13 -13
  56. package/{index-5dw57f0d.js → index-f7wewj2v.js} +155 -76
  57. package/{index-6rhabeev.js → index-ft1sfjz0.js} +36 -25
  58. package/{index-7myvn0f0.js → index-h7yw2z9n.js} +7 -7
  59. package/{index-5pmx7bkg.js → index-j9vnq8tr.js} +18 -18
  60. package/{index-vg0sj9nc.js → index-k1n0h7dk.js} +840 -201
  61. package/{index-vtj3zps1.js → index-k86zc21w.js} +57 -57
  62. package/{index-46tjzh6s.js → index-mfm35dhh.js} +29 -29
  63. package/{index-xjd3rqzj.js → index-n68jx79w.js} +6 -15
  64. package/{index-ztpw544k.js → index-qcracvc9.js} +8 -8
  65. package/{index-2c3e2xh8.js → index-saheq2bs.js} +4 -4
  66. package/{index-gmy45vz2.js → index-v0s24t8j.js} +14 -14
  67. package/{index-0cj2zxbm.js → index-wet2ytnr.js} +12 -11
  68. package/{index-j9cxndc0.js → index-x23688ar.js} +12 -7
  69. package/index-xnhf8tr9.js +109 -0
  70. package/{index-a1e0j63x.js → index-xxe90kqy.js} +58 -25
  71. package/{index-qb61h0k7.js → index-z3c9mmw4.js} +1098 -1360
  72. package/index.js +22 -24
  73. package/{library.command-md9715jz.js → library.command-wpedct6c.js} +11 -9
  74. package/{localRegistry.command-exkbamyj.js → localRegistry.command-pjdxa01g.js} +20 -22
  75. package/{module.command-w5aaddxk.js → module.command-8n4kct3e.js} +13 -15
  76. package/{package.command-yfkrcy0z.js → package.command-fcbd0b1d.js} +8 -9
  77. package/package.json +5 -7
  78. package/{page.command-echbavp4.js → page.command-x4379grq.js} +7 -8
  79. package/{primitive.command-9fbhspw2.js → primitive.command-8etmnqhq.js} +13 -15
  80. package/{quality.command-3pnt9yep.js → quality.command-hfx7z8yr.js} +226 -104
  81. package/recipeScanner-fpagtgc1.js +11 -0
  82. package/{repair.command-carj4vax.js → repair.command-j0fs5qkz.js} +12 -13
  83. package/{routeSourceValidator-e4jmbfcx.js → routeSourceValidator-ze6x894c.js} +0 -2
  84. package/{scalar.command-51ryzypc.js → scalar.command-2emk89eh.js} +11 -13
  85. package/templates/app/env/env.client.debug.ts.template +1 -5
  86. package/templates/app/env/env.client.develop.ts.template +1 -5
  87. package/templates/app/env/env.client.local.ts.template +1 -5
  88. package/templates/app/env/env.client.main.ts.template +1 -5
  89. package/templates/app/env/env.client.testing.ts.template +1 -5
  90. package/templates/app/env/env.client.type.ts +2 -6
  91. package/templates/app/env/env.server.debug.ts.template +1 -5
  92. package/templates/app/env/env.server.develop.ts.template +1 -5
  93. package/templates/app/env/env.server.local.ts.template +1 -5
  94. package/templates/app/env/env.server.main.ts.template +1 -5
  95. package/templates/app/main.ts +3 -1
  96. package/templates/appSample/lib/task/task.signal.ts +11 -6
  97. package/templates/appSample/srvkit/AuthGuard.ts +9 -5
  98. package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
  99. package/templates/crudPages/[__model__Id]/edit/page.tsx +6 -6
  100. package/templates/crudPages/[__model__Id]/page.tsx +10 -13
  101. package/templates/crudPages/new/page.tsx +6 -6
  102. package/templates/crudPages/page.tsx +9 -9
  103. package/templates/crudSinglePage/page.tsx +11 -7
  104. package/templates/env/_env.server.type.ts +7 -5
  105. package/templates/lib/db.ts +1 -1
  106. package/templates/lib/dict.ts +1 -1
  107. package/templates/lib/sig.ts +1 -1
  108. package/templates/lib/srv.ts +6 -2
  109. package/templates/module/__Model__.Util.tsx +1 -1
  110. package/templates/module/__Model__.Zone.tsx +3 -5
  111. package/templates/module/__model__.dictionary.ts +4 -1
  112. package/templates/module/__model__.signal.ts +2 -3
  113. package/templates/module/__model__.store.ts +1 -2
  114. package/templates/workspaceRoot/.gitignore.template +0 -1
  115. package/templates/workspaceRoot/AGENTS.md.template +4 -972
  116. package/templates/workspaceRoot/CLAUDE.md.template +19 -0
  117. package/templates/workspaceRoot/biome.json.template +4 -244
  118. package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +10 -14
  119. package/templates/workspaceRoot/docs/GENERATED.md.template +12 -10
  120. package/templates/workspaceRoot/package.json.template +2 -4
  121. package/typeChecker-3xgrzc2v.js +7 -0
  122. package/typecheck.proc.js +4 -5
  123. package/{workflow.command-91wmvm8k.js → workflow.command-cvpmzwjg.js} +21 -23
  124. package/{workspace.command-wa10mfd9.js → workspace.command-mbb5s3wx.js} +108 -91
  125. package/capacitorApp-p0z5yab2.js +0 -60
  126. package/dependencyScanner-f6grrep3.js +0 -9
  127. package/index-mxvakhsm.js +0 -66
  128. package/index-r24hmh0q.js +0 -4
  129. package/recipeScanner-tghkzrrt.js +0 -12
  130. package/templates/workspaceRoot/.cursor/rules/akan-page-routing.mdc.template +0 -43
  131. package/templates/workspaceRoot/.cursor/rules/akan-sync-conventions.mdc.template +0 -34
  132. package/templates/workspaceRoot/.cursor/rules/application-test-commands.mdc.template +0 -16
  133. package/templates/workspaceRoot/.cursor/rules/change-scope.mdc.template +0 -12
  134. package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +0 -18
  135. package/templates/workspaceRoot/.cursor/rules/coding-comments.mdc.template +0 -25
  136. package/templates/workspaceRoot/.cursor/rules/coding-style.mdc.template +0 -44
  137. package/templates/workspaceRoot/.cursor/rules/domain-module-conventions.mdc.template +0 -17
  138. package/templates/workspaceRoot/.cursor/rules/format-lint.mdc.template +0 -12
  139. package/templates/workspaceRoot/.cursor/rules/lint-enforced-rules.mdc.template +0 -19
  140. package/templates/workspaceRoot/.cursor/rules/module-file-playbook.mdc.template +0 -62
  141. package/templates/workspaceRoot/.cursor/rules/naming-conventions.mdc.template +0 -15
  142. package/templates/workspaceRoot/.cursor/rules/react-component-style.mdc.template +0 -21
  143. package/templates/workspaceRoot/.cursor/rules/repo-overview.mdc.template +0 -17
  144. package/templates/workspaceRoot/.cursor/rules/scalar-modeling.mdc.template +0 -99
  145. package/templates/workspaceRoot/.cursor/rules/secrets-and-env.mdc.template +0 -13
  146. package/templates/workspaceRoot/.cursor/rules/service-signal-conventions.mdc.template +0 -83
  147. package/templates/workspaceRoot/.cursor/rules/srvkit-adapters-and-guards.mdc.template +0 -50
  148. package/templates/workspaceRoot/.cursor/rules/ssr-first.mdc.template +0 -59
  149. package/templates/workspaceRoot/.cursor/rules/typescript-imports.mdc.template +0 -16
  150. package/typeChecker-kravn7ns.js +0 -8
  151. /package/{index-0wae5ebk.js → index-meh558jy.js} +0 -0
  152. /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,25 +1,24 @@
1
1
  // @bun
2
2
  import {
3
- prepareDevWatchBatch
4
- } from "./index-fgc8r6dj.js";
3
+ WorkspaceExecutor,
4
+ AppExecutor
5
+ } from "./index-k1n0h7dk.js";
6
+ import"./index-ayy5ecax.js";
7
+ import"./index-6wwzs1bc.js";
5
8
  import {
9
+ GraphClientEntryDiscovery,
10
+ RouteClientBuilder,
6
11
  AutoImportSync,
7
12
  DevChangePlanner,
8
- DevGeneratedIndexSync,
9
- GraphClientEntryDiscovery,
10
- RouteClientBuilder
11
- } from "./index-j9cxndc0.js";
13
+ DevGeneratedIndexSync
14
+ } from "./index-x23688ar.js";
12
15
  import {
13
16
  HmrWatcher,
14
17
  WatchRootResolver
15
- } from "./index-bjpxzr6s.js";
18
+ } from "./index-ctphds8v.js";
16
19
  import {
17
- AppExecutor,
18
- WorkspaceExecutor
19
- } from "./index-vg0sj9nc.js";
20
- import"./index-mxvakhsm.js";
21
- import"./index-67546d0j.js";
22
- import"./index-r24hmh0q.js";
20
+ prepareDevWatchBatch
21
+ } from "./index-a5vmm6x4.js";
23
22
 
24
23
  // pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts
25
24
  import path2 from "path";
@@ -67,13 +66,13 @@ class BuildBatchRunner {
67
66
  env: process.env,
68
67
  stdio: ["ignore", "inherit", "inherit"],
69
68
  serialization: "advanced",
70
- ipc: (message2) => {
71
- if (!message2 || typeof message2 !== "object")
69
+ ipc: (message) => {
70
+ if (!message || typeof message !== "object")
72
71
  return;
73
- if (message2.type === "build-batch-result")
74
- result = message2.data;
72
+ if (message.type === "build-batch-result")
73
+ result = message.data;
75
74
  else
76
- onMessage(message2);
75
+ onMessage(message);
77
76
  }
78
77
  });
79
78
  const exitCode = await proc.exited;
@@ -249,7 +248,8 @@ class IncrementalBuilder {
249
248
  return;
250
249
  this.#shuttingDown = true;
251
250
  const started = Date.now();
252
- this.#logger.info(`shutdown requested (${reason}); draining ${this.#inFlight} work item(s)`);
251
+ this.#logger.debug(`shutdown requested (${reason}); draining ${this.#inFlight} work item(s)`);
252
+ this.#watcher?.stop();
253
253
  if (this.#cssRebuildTimer) {
254
254
  clearTimeout(this.#cssRebuildTimer);
255
255
  this.#cssRebuildTimer = null;
@@ -262,7 +262,7 @@ class IncrementalBuilder {
262
262
  return;
263
263
  });
264
264
  const flushed = await BuilderChannel.drain();
265
- this.#logger.info(`drained in ${Date.now() - started}ms${flushed ? ` after flushing ${flushed} ipc write(s)` : ""}; exiting for recycle`);
265
+ this.#logger.debug(`drained in ${Date.now() - started}ms${flushed ? ` after flushing ${flushed} ipc write(s)` : ""}; exiting for recycle`);
266
266
  process.exit(0);
267
267
  }
268
268
  get shuttingDown() {
@@ -359,13 +359,12 @@ class IncrementalBuilder {
359
359
  this.#logger.verbose(`[auto-import] inserted imports into ${autoImport.changedFiles.length} file(s)`);
360
360
  const indexSync = await this.#generatedIndexSync.syncForBatch(batch.files);
361
361
  await this.#watcher?.absorb([...autoImport.changedFiles, ...indexSync.changedFiles]);
362
- const { files, kinds, expandedBatch, event, hasSyncErrors } = prepareDevWatchBatch({
362
+ const { files, kinds, expandedBatch, devPlan, event, hasSyncErrors } = prepareDevWatchBatch({
363
363
  generation,
364
364
  batch,
365
365
  indexSync,
366
366
  changePlanner: this.#changePlanner
367
367
  });
368
- const devPlan = event.devPlan;
369
368
  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)"}`);
370
369
  for (const error of indexSync.errors)
371
370
  this.#logger.error(error);
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  runner,
4
4
  script
5
- } from "./index-6rhabeev.js";
5
+ } from "./index-ft1sfjz0.js";
6
6
 
7
7
  // pkgs/@akanjs/cli/page/page.runner.ts
8
8
  class PageRunner extends runner("page") {
@@ -1,27 +1,20 @@
1
1
  // @bun
2
2
  import {
3
- AiSession,
4
- CloudApi,
3
+ getDefaultHostConfig,
5
4
  GlobalConfig,
6
- getDefaultHostConfig
7
- } from "./index-0cj2zxbm.js";
5
+ CloudApi,
6
+ AiSession
7
+ } from "./index-wet2ytnr.js";
8
8
  import {
9
- openBrowser
10
- } from "./index-5dw57f0d.js";
9
+ WorkspaceExecutor,
10
+ AppExecutor
11
+ } from "./index-k1n0h7dk.js";
11
12
  import {
12
13
  runner
13
- } from "./index-6rhabeev.js";
14
+ } from "./index-ft1sfjz0.js";
14
15
  import {
15
- AppExecutor,
16
- WorkspaceExecutor
17
- } from "./index-vg0sj9nc.js";
18
-
19
- // pkgs/@akanjs/cli/cloud/cloud.runner.ts
20
- import path from "path";
21
- import { confirm, input, select } from "@inquirer/prompts";
22
- import { Logger, sleep } from "akanjs/common";
23
- import chalk from "chalk";
24
- import * as QRcode from "qrcode";
16
+ openBrowser
17
+ } from "./index-f7wewj2v.js";
25
18
 
26
19
  // pkgs/@akanjs/cli/npmRegistry.ts
27
20
  var defaultNpmRegistry = "https://registry.npmjs.org";
@@ -40,6 +33,11 @@ async function getLatestPackageVersion(packageName, tag = "latest", registryUrl)
40
33
  }
41
34
 
42
35
  // pkgs/@akanjs/cli/cloud/cloud.runner.ts
36
+ import path from "path";
37
+ import { confirm, input, select } from "@inquirer/prompts";
38
+ import { Logger, sleep } from "akanjs/common";
39
+ import chalk from "chalk";
40
+ import * as QRcode from "qrcode";
43
41
  var addRemoteEnvServerValue = "__addRemoteEnvServer";
44
42
  var removeRemoteEnvServerValue = "__removeRemoteEnvServer";
45
43
 
@@ -100,8 +98,8 @@ class CloudRunner extends runner("cloud") {
100
98
  const selectedName = await select({
101
99
  message: "Select the remote env server",
102
100
  choices: [
103
- ...serverEntries.map(([name, config2]) => ({
104
- name: `${name} (${config2.username ? `${config2.username}@` : ""}${config2.host}${config2.port ? `:${config2.port}` : ""})`,
101
+ ...serverEntries.map(([name, config]) => ({
102
+ name: `${name} (${config.username ? `${config.username}@` : ""}${config.host}${config.port ? `:${config.port}` : ""})`,
105
103
  value: name
106
104
  })),
107
105
  { name: "Add new remote server", value: addRemoteEnvServerValue },
@@ -205,12 +203,12 @@ ${chalk.green("\u27A4")} Authentication Required`));
205
203
  const MAX_RETRY = 300;
206
204
  for (let i = 0;i < MAX_RETRY; i++) {
207
205
  const accessToken = await cloudApi.getRemoteAuthToken(remoteId);
208
- const self2 = await cloudApi.getRemoteSelf();
209
- if (accessToken && self2) {
210
- await GlobalConfig.setHostConfig({ host: config.host, auth: { accessToken, self: self2 } });
206
+ const self = await cloudApi.getRemoteSelf();
207
+ if (accessToken && self) {
208
+ await GlobalConfig.setHostConfig({ host: config.host, auth: { accessToken, self } });
211
209
  Logger.rawLog(chalk.green(`\r\u2713 Authentication successful!`));
212
210
  Logger.rawLog(chalk.green.bold(`
213
- \u2728 Welcome aboard, ${self2.nickname ?? "anonymous"}!`));
211
+ \u2728 Welcome aboard, ${self.nickname ?? "anonymous"}!`));
214
212
  Logger.rawLog(chalk.dim(`You're now ready to use Akan CLI!
215
213
  `));
216
214
  return true;
@@ -255,12 +253,12 @@ ${chalk.green("\u27A4")} Authentication Required`));
255
253
  const isOfficialRelease = !devPatchVersion;
256
254
  const targetVersionPrefix = isOfficialRelease ? `${majorVersion}.${minorVersion}` : `${majorVersion}.${minorVersion}.${patchVersion}`;
257
255
  const tag = distTag ?? (isOfficialRelease ? "latest" : patchVersion.split("-").at(1) ?? "dev");
258
- const getNextVersion = async (prefix, tag2) => {
256
+ const getNextVersion = async (prefix, tag) => {
259
257
  try {
260
- const latestPublishedVersion2 = await getLatestPackageVersion("akanjs", tag2, registry);
261
- const latestPatch = latestPublishedVersion2.startsWith(prefix) ? parseInt(latestPublishedVersion2.split(".").at(-1) ?? "-1") : -1;
262
- const nextVersion2 = `${prefix}.${latestPatch + 1}`;
263
- return { nextVersion: nextVersion2, latestPublishedVersion: latestPublishedVersion2 };
258
+ const latestPublishedVersion = await getLatestPackageVersion("akanjs", tag, registry);
259
+ const latestPatch = latestPublishedVersion.startsWith(prefix) ? parseInt(latestPublishedVersion.split(".").at(-1) ?? "-1") : -1;
260
+ const nextVersion = `${prefix}.${latestPatch + 1}`;
261
+ return { nextVersion, latestPublishedVersion };
264
262
  } catch {
265
263
  return { nextVersion: `${prefix}.0`, latestPublishedVersion: null };
266
264
  }
@@ -285,7 +283,10 @@ ${chalk.green("\u27A4")} Authentication Required`));
285
283
  return;
286
284
  }
287
285
  }
288
- await Promise.all(akanPkgs.map(async (library) => {
286
+ Logger.info("Logging in to npm...");
287
+ await workspace.spawn("npm", ["login"], { stdio: "inherit" });
288
+ Logger.info("Logged in to npm");
289
+ for (const library of akanPkgs) {
289
290
  Logger.info(`Publishing ${library}@${nextVersion} to ${registry ?? "npm"}...`);
290
291
  await workspace.spawn("npm", ["publish", "--tag", tag, ...this.#getRegistryArgs(registry), ...this.#getLocalRegistryAuthArgs(registry)], {
291
292
  cwd: path.join(workspace.workspaceRoot, "dist/pkgs", library),
@@ -293,7 +294,7 @@ ${chalk.green("\u27A4")} Authentication Required`));
293
294
  stdio: "inherit"
294
295
  });
295
296
  Logger.info(`${library}@${nextVersion} is published to ${registry ?? "npm"}`);
296
- }));
297
+ }
297
298
  Logger.info(`All libraries are published to ${registry ?? "npm"}`);
298
299
  }
299
300
  async update(workspace, tag = "latest", { registryUrl } = {}) {