@softize/opus 8.6.6

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 (286) hide show
  1. package/CHANGELOG.md +1616 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -0
  4. package/bin/cli.mjs +528 -0
  5. package/bin/lib/check.mjs +307 -0
  6. package/bin/lib/components.mjs +151 -0
  7. package/bin/lib/create.mjs +208 -0
  8. package/bin/lib/db-check-runner.mjs +86 -0
  9. package/bin/lib/db-migrate-runner.mjs +89 -0
  10. package/bin/lib/db-scaffold-runner.mjs +84 -0
  11. package/bin/lib/db.mjs +261 -0
  12. package/bin/lib/docs-include.mjs +48 -0
  13. package/bin/lib/gen-dicts.mjs +134 -0
  14. package/bin/lib/gen-docs.mjs +288 -0
  15. package/bin/lib/gen-manifest.mjs +102 -0
  16. package/bin/lib/gen-openapi.mjs +195 -0
  17. package/bin/lib/gen-runner.mjs +472 -0
  18. package/bin/lib/gen-stubs.mjs +463 -0
  19. package/bin/lib/gen.mjs +311 -0
  20. package/bin/lib/init.mjs +514 -0
  21. package/bin/lib/introspect.mjs +107 -0
  22. package/bin/lib/mcp.mjs +85 -0
  23. package/bin/lib/postinstall.mjs +56 -0
  24. package/docs/chat-event-protocol.md +85 -0
  25. package/docs/code-style.md +16 -0
  26. package/docs/data-layer.md +246 -0
  27. package/docs/ownership-vs-shadcn-lock.md +102 -0
  28. package/docs/protocol.md +2053 -0
  29. package/docs/releasing.md +110 -0
  30. package/docs/shellnav.md +131 -0
  31. package/package.json +338 -0
  32. package/registry/hooks/hooks.json +26 -0
  33. package/registry/hooks/link-memory-on-start.mjs +46 -0
  34. package/registry/hooks/opus-check-on-stop.mjs +114 -0
  35. package/registry/skills/create-action/SKILL.md +49 -0
  36. package/registry/skills/create-action/scaffold.mjs +122 -0
  37. package/registry/templates/app/_gitignore +3 -0
  38. package/registry/templates/app/_npmrc +1 -0
  39. package/registry/templates/app/_opus/_gitignore +5 -0
  40. package/registry/templates/app/_prettierrc.json +6 -0
  41. package/registry/templates/app/index.html +13 -0
  42. package/registry/templates/app/opus.config.ts +16 -0
  43. package/registry/templates/app/package.json +43 -0
  44. package/registry/templates/app/pnpm-workspace.yaml +11 -0
  45. package/registry/templates/app/public/favicon.svg +4 -0
  46. package/registry/templates/app/src/App.tsx +37 -0
  47. package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
  48. package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
  49. package/registry/templates/app/src/domains/tasks/index.ts +13 -0
  50. package/registry/templates/app/src/index.css +18 -0
  51. package/registry/templates/app/src/main.tsx +25 -0
  52. package/registry/templates/app/tsconfig.json +20 -0
  53. package/registry/templates/app/vite.config.ts +46 -0
  54. package/registry/templates/monorepo/_gitignore +3 -0
  55. package/registry/templates/monorepo/_npmrc +1 -0
  56. package/registry/templates/monorepo/package.json +9 -0
  57. package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
  58. package/src/ai/ask.ts +64 -0
  59. package/src/ai/drivers/anthropic.ts +309 -0
  60. package/src/ai/index.ts +17 -0
  61. package/src/audit/drivers/console.ts +117 -0
  62. package/src/audit/drivers/pg.ts +172 -0
  63. package/src/audit/index.ts +51 -0
  64. package/src/auth/drivers/better-auth.ts +103 -0
  65. package/src/auth/drivers/jwt.ts +188 -0
  66. package/src/auth/index.ts +9 -0
  67. package/src/client/drivers/fetch.ts +202 -0
  68. package/src/client/index.ts +22 -0
  69. package/src/core/actions.ts +110 -0
  70. package/src/core/audit.ts +239 -0
  71. package/src/core/contracts.ts +137 -0
  72. package/src/core/domain.ts +310 -0
  73. package/src/core/errors.ts +181 -0
  74. package/src/core/index.ts +174 -0
  75. package/src/core/logical-type.ts +31 -0
  76. package/src/core/reactions.ts +81 -0
  77. package/src/core/runtime.ts +1167 -0
  78. package/src/core/schedules.ts +41 -0
  79. package/src/core/types.ts +1356 -0
  80. package/src/data/drivers/kysely.ts +389 -0
  81. package/src/data/index.ts +10 -0
  82. package/src/data/readonly-pool.ts +160 -0
  83. package/src/dsl/eval.ts +136 -0
  84. package/src/dsl/index.ts +29 -0
  85. package/src/dsl/kysely.ts +230 -0
  86. package/src/dsl/loads.ts +123 -0
  87. package/src/dsl/parser.ts +423 -0
  88. package/src/dsl/types.ts +113 -0
  89. package/src/events/drivers/mitt.ts +70 -0
  90. package/src/events/index.ts +9 -0
  91. package/src/log/drivers/pino.ts +57 -0
  92. package/src/log/index.ts +9 -0
  93. package/src/mcp/index.ts +62 -0
  94. package/src/queue/drivers/bullmq.ts +190 -0
  95. package/src/queue/index.ts +9 -0
  96. package/src/scheduler/drivers/node-cron.ts +93 -0
  97. package/src/scheduler/every.ts +45 -0
  98. package/src/scheduler/index.ts +9 -0
  99. package/src/schema/drivers/zod.ts +765 -0
  100. package/src/schema/entity.ts +439 -0
  101. package/src/schema/format/locale.ts +144 -0
  102. package/src/schema/index.ts +65 -0
  103. package/src/schema/openapi.ts +302 -0
  104. package/src/schema/scaffold.ts +160 -0
  105. package/src/server/drivers/fastify.ts +224 -0
  106. package/src/server/drivers/node.ts +386 -0
  107. package/src/server/index.ts +142 -0
  108. package/src/storage/drivers/fs.ts +90 -0
  109. package/src/storage/drivers/s3.ts +117 -0
  110. package/src/storage/index.ts +27 -0
  111. package/src/testing/fake.ts +298 -0
  112. package/src/testing/index.ts +324 -0
  113. package/src/ui/components/patterns/action-form-card.tsx +48 -0
  114. package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
  115. package/src/ui/components/patterns/app-shell.tsx +227 -0
  116. package/src/ui/components/patterns/confirm.tsx +226 -0
  117. package/src/ui/components/patterns/data-state.tsx +75 -0
  118. package/src/ui/components/patterns/form-dialog.tsx +64 -0
  119. package/src/ui/components/patterns/form.tsx +584 -0
  120. package/src/ui/components/patterns/list.tsx +1488 -0
  121. package/src/ui/components/patterns/page.tsx +46 -0
  122. package/src/ui/components/patterns/section-shell.tsx +246 -0
  123. package/src/ui/components/patterns/shell-nav.tsx +150 -0
  124. package/src/ui/components/patterns/sidebar.tsx +89 -0
  125. package/src/ui/components/patterns/split.tsx +93 -0
  126. package/src/ui/components/patterns/trigger.tsx +196 -0
  127. package/src/ui/components/patterns/view.tsx +84 -0
  128. package/src/ui/components/primitives/accordion.tsx +64 -0
  129. package/src/ui/components/primitives/alert-dialog.tsx +190 -0
  130. package/src/ui/components/primitives/alert.tsx +116 -0
  131. package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
  132. package/src/ui/components/primitives/avatar.tsx +107 -0
  133. package/src/ui/components/primitives/badge.tsx +37 -0
  134. package/src/ui/components/primitives/breadcrumb.tsx +109 -0
  135. package/src/ui/components/primitives/button-group.tsx +83 -0
  136. package/src/ui/components/primitives/button.tsx +102 -0
  137. package/src/ui/components/primitives/calendar.tsx +218 -0
  138. package/src/ui/components/primitives/card.tsx +56 -0
  139. package/src/ui/components/primitives/carousel.tsx +239 -0
  140. package/src/ui/components/primitives/chat.tsx +407 -0
  141. package/src/ui/components/primitives/checkbox.tsx +30 -0
  142. package/src/ui/components/primitives/collapsible.tsx +31 -0
  143. package/src/ui/components/primitives/command.tsx +182 -0
  144. package/src/ui/components/primitives/composer.tsx +121 -0
  145. package/src/ui/components/primitives/copyable.tsx +50 -0
  146. package/src/ui/components/primitives/dialog.tsx +147 -0
  147. package/src/ui/components/primitives/drawer.tsx +141 -0
  148. package/src/ui/components/primitives/empty.tsx +104 -0
  149. package/src/ui/components/primitives/field.tsx +246 -0
  150. package/src/ui/components/primitives/icon-picker.tsx +180 -0
  151. package/src/ui/components/primitives/input-group.tsx +168 -0
  152. package/src/ui/components/primitives/input-otp.tsx +75 -0
  153. package/src/ui/components/primitives/input.tsx +72 -0
  154. package/src/ui/components/primitives/item.tsx +193 -0
  155. package/src/ui/components/primitives/kbd.tsx +28 -0
  156. package/src/ui/components/primitives/label.tsx +22 -0
  157. package/src/ui/components/primitives/markdown.tsx +35 -0
  158. package/src/ui/components/primitives/menu.tsx +255 -0
  159. package/src/ui/components/primitives/pagination.tsx +127 -0
  160. package/src/ui/components/primitives/popover.tsx +87 -0
  161. package/src/ui/components/primitives/progress.tsx +29 -0
  162. package/src/ui/components/primitives/radio-group.tsx +43 -0
  163. package/src/ui/components/primitives/resizable.tsx +51 -0
  164. package/src/ui/components/primitives/scroll-area.tsx +56 -0
  165. package/src/ui/components/primitives/select.tsx +479 -0
  166. package/src/ui/components/primitives/separator.tsx +26 -0
  167. package/src/ui/components/primitives/skeleton.tsx +13 -0
  168. package/src/ui/components/primitives/slider.tsx +61 -0
  169. package/src/ui/components/primitives/sonner.tsx +46 -0
  170. package/src/ui/components/primitives/spinner.tsx +29 -0
  171. package/src/ui/components/primitives/switch.tsx +33 -0
  172. package/src/ui/components/primitives/table.tsx +114 -0
  173. package/src/ui/components/primitives/tabs.tsx +104 -0
  174. package/src/ui/components/primitives/textarea.tsx +18 -0
  175. package/src/ui/components/primitives/toggle-group.tsx +81 -0
  176. package/src/ui/components/primitives/toggle.tsx +45 -0
  177. package/src/ui/components/primitives/tooltip.tsx +55 -0
  178. package/src/ui/components/primitives/truncate.tsx +49 -0
  179. package/src/ui/docs/DocBrowser.tsx +90 -0
  180. package/src/ui/docs/changelog.tsx +80 -0
  181. package/src/ui/docs/content/accordion.md +86 -0
  182. package/src/ui/docs/content/action-form-card.md +24 -0
  183. package/src/ui/docs/content/action-form-dialog.md +30 -0
  184. package/src/ui/docs/content/action-form.md +125 -0
  185. package/src/ui/docs/content/action-list-dialog.md +68 -0
  186. package/src/ui/docs/content/action-list.md +194 -0
  187. package/src/ui/docs/content/action-trigger.md +72 -0
  188. package/src/ui/docs/content/action-view.md +47 -0
  189. package/src/ui/docs/content/actions.md +138 -0
  190. package/src/ui/docs/content/ai.md +112 -0
  191. package/src/ui/docs/content/alert-dialog.md +73 -0
  192. package/src/ui/docs/content/alert.md +69 -0
  193. package/src/ui/docs/content/app-shell.md +155 -0
  194. package/src/ui/docs/content/aspect-ratio.md +66 -0
  195. package/src/ui/docs/content/audit.md +84 -0
  196. package/src/ui/docs/content/auth.md +70 -0
  197. package/src/ui/docs/content/avatar.md +94 -0
  198. package/src/ui/docs/content/badge.md +48 -0
  199. package/src/ui/docs/content/breadcrumb.md +87 -0
  200. package/src/ui/docs/content/button-group.md +71 -0
  201. package/src/ui/docs/content/button.md +60 -0
  202. package/src/ui/docs/content/calendar.md +62 -0
  203. package/src/ui/docs/content/card.md +49 -0
  204. package/src/ui/docs/content/carousel.md +85 -0
  205. package/src/ui/docs/content/chat.md +69 -0
  206. package/src/ui/docs/content/checkbox.md +75 -0
  207. package/src/ui/docs/content/cli.md +58 -0
  208. package/src/ui/docs/content/collapsible.md +64 -0
  209. package/src/ui/docs/content/command.md +56 -0
  210. package/src/ui/docs/content/composer.md +50 -0
  211. package/src/ui/docs/content/confirm.md +120 -0
  212. package/src/ui/docs/content/copyable.md +30 -0
  213. package/src/ui/docs/content/customization.md +110 -0
  214. package/src/ui/docs/content/cycle.md +34 -0
  215. package/src/ui/docs/content/data-state.md +47 -0
  216. package/src/ui/docs/content/data.md +99 -0
  217. package/src/ui/docs/content/dialog.md +60 -0
  218. package/src/ui/docs/content/drawer.md +55 -0
  219. package/src/ui/docs/content/empty.md +66 -0
  220. package/src/ui/docs/content/events.md +61 -0
  221. package/src/ui/docs/content/field.md +58 -0
  222. package/src/ui/docs/content/getting-started.md +109 -0
  223. package/src/ui/docs/content/icon-picker.md +51 -0
  224. package/src/ui/docs/content/input-group.md +78 -0
  225. package/src/ui/docs/content/input-otp.md +72 -0
  226. package/src/ui/docs/content/input.md +78 -0
  227. package/src/ui/docs/content/item.md +84 -0
  228. package/src/ui/docs/content/kbd.md +62 -0
  229. package/src/ui/docs/content/label.md +32 -0
  230. package/src/ui/docs/content/log.md +55 -0
  231. package/src/ui/docs/content/markdown.md +41 -0
  232. package/src/ui/docs/content/mcp.md +44 -0
  233. package/src/ui/docs/content/menu.md +114 -0
  234. package/src/ui/docs/content/microcopy.md +83 -0
  235. package/src/ui/docs/content/page.md +34 -0
  236. package/src/ui/docs/content/pagination.md +99 -0
  237. package/src/ui/docs/content/popover.md +49 -0
  238. package/src/ui/docs/content/progress.md +69 -0
  239. package/src/ui/docs/content/queue.md +62 -0
  240. package/src/ui/docs/content/radio-group.md +77 -0
  241. package/src/ui/docs/content/resizable.md +86 -0
  242. package/src/ui/docs/content/router.md +56 -0
  243. package/src/ui/docs/content/runtime.md +77 -0
  244. package/src/ui/docs/content/scheduler.md +66 -0
  245. package/src/ui/docs/content/scroll-area.md +89 -0
  246. package/src/ui/docs/content/section-shell.md +121 -0
  247. package/src/ui/docs/content/select.md +342 -0
  248. package/src/ui/docs/content/separator.md +33 -0
  249. package/src/ui/docs/content/sidebar.md +38 -0
  250. package/src/ui/docs/content/skeleton.md +34 -0
  251. package/src/ui/docs/content/slider.md +64 -0
  252. package/src/ui/docs/content/spinner.md +37 -0
  253. package/src/ui/docs/content/split.md +33 -0
  254. package/src/ui/docs/content/storage.md +69 -0
  255. package/src/ui/docs/content/switch.md +69 -0
  256. package/src/ui/docs/content/table.md +102 -0
  257. package/src/ui/docs/content/tabs.md +94 -0
  258. package/src/ui/docs/content/testing.md +89 -0
  259. package/src/ui/docs/content/textarea.md +30 -0
  260. package/src/ui/docs/content/toast.md +67 -0
  261. package/src/ui/docs/content/toggle-group.md +81 -0
  262. package/src/ui/docs/content/toggle.md +72 -0
  263. package/src/ui/docs/content/tokens.md +171 -0
  264. package/src/ui/docs/content/tooltip.md +50 -0
  265. package/src/ui/docs/content/truncate.md +37 -0
  266. package/src/ui/docs/content/ui.md +40 -0
  267. package/src/ui/docs/content/upgrading.md +48 -0
  268. package/src/ui/docs/doc-client.tsx +214 -0
  269. package/src/ui/docs/doc.tsx +301 -0
  270. package/src/ui/docs/folder.tsx +149 -0
  271. package/src/ui/docs/index.ts +21 -0
  272. package/src/ui/docs/markdown.tsx +130 -0
  273. package/src/ui/docs/md-raw.d.ts +4 -0
  274. package/src/ui/docs/plugin.ts +104 -0
  275. package/src/ui/docs/registry.tsx +424 -0
  276. package/src/ui/docs/standalone.tsx +107 -0
  277. package/src/ui/drivers/react.tsx +627 -0
  278. package/src/ui/index.ts +92 -0
  279. package/src/ui/lib/cn.ts +10 -0
  280. package/src/ui/lib/zod-pt-br.ts +38 -0
  281. package/src/ui/meta.ts +412 -0
  282. package/src/ui/react.tsx +235 -0
  283. package/src/ui/router.ts +96 -0
  284. package/src/ui/theme.css +234 -0
  285. package/src/vite/design.ts +652 -0
  286. package/src/vite/index.ts +8 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Softize
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # Opus
2
+
3
+ > **Status:** v0 — pacote único `@softize/opus` com subpath exports (core + adapters). Uma versão, sem semver por módulo.
4
+
5
+ End-to-end action protocol for TypeScript. Declare once — input, authorization, execution, audit, feedback — and let adapters materialize it across UI, client, server, and log.
6
+
7
+ **O Opus não é framework.** Não tem ciclo de vida próprio. É o contrato comum que todas as camadas falam.
8
+
9
+ ## Filosofia
10
+
11
+ - **Pipeline declarado, não escrita declarada.** Action é qualquer unidade que flui pelo pipeline (validate → load → authorize → execute → audit → return), independente de mudar estado. Cobre write (form, simple) e read estruturado (list, view).
12
+ - **Três primitivas declarativas.** Action (humano/AI dispara) + Reaction (evento dispara) + Schedule (tempo dispara). Fecha o ciclo de ação proativa.
13
+ - **Provenance rastreável.** Toda execução carrega origem estruturada (http, schedule, reaction, ai-agent, background, ...) com cadeia preservada via `originalProvenance`. Audit registra a árvore de causalidade do user até a action final.
14
+ - **Kind como discriminator.** v0 suporta `simple`, `form`, `list`, `view`.
15
+ - **Contrato, não feature.** Tudo que entra no core padroniza forma; tudo que faz trabalho usa lib externa.
16
+ - **Fail-closed por default.** Action sem `authorize` é negada. Audit default-on.
17
+ - **Adapters plugáveis.** Cada categoria é uma superfície (subpath) com interface fechada e drivers trocáveis.
18
+ - **Doc é fonte da verdade.** `docs/protocol.md` — 16 seções fechadas (15 + glossário).
19
+
20
+ ## Superfícies
21
+
22
+ Um pacote (`@softize/opus`), uma versão. Cada categoria abaixo é um **subpath ESM**, não um pacote separado.
23
+
24
+ | Superfície | Drivers | Propósito |
25
+ |---|---|---|
26
+ | `@softize/opus` (core) | — | Protocolo, `defineAction`, `defineEntity`, runtime |
27
+ | `@softize/opus/schema` | `/zod`, `/openapi` | Logical types (`t.*`) + OpenAPI generator |
28
+ | `@softize/opus/server` | `/fastify` | HTTP transport + endpoint mount |
29
+ | `@softize/opus/client` | `/fetch` | Client adapter pra invocar actions |
30
+ | `@softize/opus/ui` | `/react` | Componentes + Provider + hooks (`useAction`, `useListAction`) |
31
+ | `@softize/opus/data` | `/kysely` | Data adapter (`ctx.db`, `ctx.repo`) |
32
+ | `@softize/opus/auth` | `/jwt`, `/better-auth` | Auth adapter (`ctx.user`, `ctx.can`) |
33
+ | `@softize/opus/audit` | `/console`, `/pg` | Audit sinks |
34
+ | `@softize/opus/log` | `/pino` | Logger adapter (console default no core) |
35
+ | `@softize/opus/queue` | `/bullmq` | Background job execution |
36
+ | `@softize/opus/events` | `/mitt` | EventBus (in-process) |
37
+ | `@softize/opus/scheduler` | `/node-cron` | Schedule adapter (ação iniciada por tempo) |
38
+
39
+ Drivers via subpath ESM (estilo Drizzle): `@softize/opus/server/fastify`, `@softize/opus/data/kysely`.
40
+
41
+ ## Quick start
42
+
43
+ ```bash
44
+ pnpm add @softize/opus zod fastify
45
+ ```
46
+
47
+ ```ts
48
+ import Fastify from 'fastify'
49
+ import { z } from 'zod'
50
+ import { createRuntime, defineAction } from '@softize/opus'
51
+ import { t } from '@softize/opus/schema/zod'
52
+ import { fastifyServer } from '@softize/opus/server/fastify'
53
+
54
+ const archiveDeal = defineAction({
55
+ name: 'deal.archive',
56
+ kind: 'simple',
57
+ input: z.object({ dealId: z.string() }),
58
+ output: z.object({ archivedAt: t.datetime() }),
59
+ authorize: (ctx) => ctx.can('deal:archive'),
60
+ handler: async (_ctx, input) => ({ archivedAt: new Date().toISOString() }),
61
+ })
62
+
63
+ const app = Fastify()
64
+ const server = fastifyServer({ app })
65
+ const runtime = createRuntime({ server })
66
+
67
+ runtime.register([archiveDeal])
68
+ await runtime.start()
69
+ server.mountEndpoints({ openapi: true })
70
+
71
+ await app.listen({ port: 3000 })
72
+ // POST http://localhost:3000/api/deal/archive
73
+ // GET http://localhost:3000/openapi.json
74
+ // GET http://localhost:3000/health
75
+ // GET http://localhost:3000/ready
76
+ ```
77
+
78
+ ## Estrutura
79
+
80
+ ```
81
+ src/
82
+ core/ schema/ server/ client/ ui/
83
+ data/ auth/ audit/ log/ queue/ events/ scheduler/ dsl/
84
+ registry/ # scaffolds + skills (create-action) — geração, não runtime
85
+ bin/ # CLI (opus setup/gen/check/introspect/mcp) + libs
86
+ docs/
87
+ protocol.md # contrato completo (16 seções)
88
+ data-layer.md · releasing.md · code-style.md
89
+ ```
90
+
91
+ ## Status
92
+
93
+ - **v0**: 12 superfícies, 430 tests, 100% coverage.
94
+ - **v1+**: drivers adicionais (Hono, Drizzle, ArkType, Vue, Inngest, Redis events), camada de entidades (`defineEntity`), Conversya migration plan.
95
+
96
+ ## Desenvolvimento
97
+
98
+ ```bash
99
+ pnpm install # instala deps
100
+ pnpm typecheck # tsc --noEmit
101
+ pnpm test # vitest run
102
+ pnpm test:cov # com coverage report
103
+ ```
104
+
105
+ ## Publicar
106
+
107
+ Publicado no registry compartilhado da Softize (`registry.softize.com.br`), não no
108
+ npm público. `pnpm release [patch|minor|major|x.y.z]` bumpa + publica. Passo a passo
109
+ (verificar, testar local, auth) em [docs/releasing.md](docs/releasing.md).
110
+
111
+ ## Licença
112
+
113
+ A decidir. Ver `docs/protocol.md` — Decisões em aberto.
package/bin/cli.mjs ADDED
@@ -0,0 +1,528 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @softize/opus CLI — copia templates do registry pro consumer.
4
+ *
5
+ * Uso:
6
+ * npx @softize/opus add <name> # copia registry/<name>.tsx
7
+ * npx @softize/opus add <name> --force # sobrescreve se existe
8
+ * npx @softize/opus list # lista templates disponíveis
9
+ * npx @softize/opus gen # gera manifest/openapi/docs/stubs
10
+ *
11
+ * Lê components.json do consumer pra resolver path destino (mesma config
12
+ * do shadcn). Default: <cwd>/src/components/action/<name>.tsx.
13
+ */
14
+
15
+ import { promises as fs } from 'node:fs'
16
+ import path from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
18
+
19
+ import { cmdGen, helpGen } from './lib/gen.mjs'
20
+ import { cmdDb } from './lib/db.mjs'
21
+ import { scanDir, emptyScanVerdict } from './lib/check.mjs'
22
+ import { createMonorepo, createProject } from './lib/create.mjs'
23
+ import { initProject, setupUiFoundation } from './lib/init.mjs'
24
+ import { introspect } from './lib/introspect.mjs'
25
+
26
+ const __filename = fileURLToPath(import.meta.url)
27
+ const __dirname = path.dirname(__filename)
28
+ const PACKAGE_ROOT = path.resolve(__dirname, '..')
29
+ const REGISTRY_DIR = path.join(PACKAGE_ROOT, 'registry')
30
+
31
+ // =============================================================================
32
+ // Helpers
33
+ // =============================================================================
34
+
35
+ function log(level, msg) {
36
+ const colors = {
37
+ info: '\x1b[36m',
38
+ success: '\x1b[32m',
39
+ error: '\x1b[31m',
40
+ warn: '\x1b[33m',
41
+ }
42
+ const reset = '\x1b[0m'
43
+ console.log(`${colors[level]}${msg}${reset}`)
44
+ }
45
+
46
+ async function fileExists(p) {
47
+ try {
48
+ await fs.access(p)
49
+ return true
50
+ } catch {
51
+ return false
52
+ }
53
+ }
54
+
55
+ async function loadComponentsJson(cwd) {
56
+ const candidate = path.join(cwd, 'components.json')
57
+ if (!(await fileExists(candidate))) {
58
+ return null
59
+ }
60
+ try {
61
+ const raw = await fs.readFile(candidate, 'utf-8')
62
+ return JSON.parse(raw)
63
+ } catch (err) {
64
+ log('warn', `components.json existe mas não é JSON válido: ${err.message}`)
65
+ return null
66
+ }
67
+ }
68
+
69
+ function resolveAlias(alias, fallback) {
70
+ if (typeof alias !== 'string') return fallback
71
+ // Pega só a parte após "@/" pq path é relativo a src/.
72
+ // Ex: "@/components" → "components".
73
+ if (alias.startsWith('@/')) {
74
+ return alias.slice(2)
75
+ }
76
+ return alias
77
+ }
78
+
79
+ async function resolveDestination(cwd, name) {
80
+ const components = await loadComponentsJson(cwd)
81
+ if (components === null) {
82
+ log(
83
+ 'warn',
84
+ 'Sem components.json. Usando default ./src/components/action/. Roda `npx shadcn init` se quiser configurar.',
85
+ )
86
+ return path.join(cwd, 'src/components/action', `${name}.tsx`)
87
+ }
88
+ const aliasComponents =
89
+ components.aliases?.components ?? '@/components'
90
+ const componentsRel = resolveAlias(aliasComponents, 'components')
91
+ return path.join(cwd, 'src', componentsRel, 'action', `${name}.tsx`)
92
+ }
93
+
94
+ async function listTemplates() {
95
+ if (!(await fileExists(REGISTRY_DIR))) {
96
+ log('error', `Registry não encontrado em ${REGISTRY_DIR}.`)
97
+ process.exit(1)
98
+ }
99
+ const files = await fs.readdir(REGISTRY_DIR)
100
+ return files
101
+ .filter((f) => f.endsWith('.tsx') || f.endsWith('.ts'))
102
+ .map((f) => f.replace(/\.(tsx|ts)$/, ''))
103
+ .sort()
104
+ }
105
+
106
+ // =============================================================================
107
+ // Commands
108
+ // =============================================================================
109
+
110
+ async function cmdList() {
111
+ const templates = await listTemplates()
112
+ if (templates.length === 0) {
113
+ log(
114
+ 'info',
115
+ '\nSem templates de cópia (os componentes de UI agora são LIB, não se copiam):\n' +
116
+ ' • importe de `@softize/opus/ui/react` (Button, ActionForm, DropdownMenu…)\n' +
117
+ ' • descubra o catálogo via `opus mcp` → tool `opus_list_components`\n',
118
+ )
119
+ return
120
+ }
121
+ log('info', `\n@softize/opus templates disponíveis:\n`)
122
+ for (const t of templates) {
123
+ console.log(` ${t}`)
124
+ }
125
+ console.log('')
126
+ log('info', `Adicionar: npx @softize/opus add <nome>\n`)
127
+ }
128
+
129
+ async function cmdAdd(name, options) {
130
+ if (typeof name !== 'string' || name.length === 0) {
131
+ log('error', 'Uso: npx @softize/opus add <name>')
132
+ process.exit(1)
133
+ }
134
+
135
+ const templates = await listTemplates()
136
+ if (!templates.includes(name)) {
137
+ log('error', `Template "${name}" não existe.`)
138
+ console.log(`\nDisponíveis: ${templates.join(', ')}\n`)
139
+ process.exit(1)
140
+ }
141
+
142
+ const cwd = process.cwd()
143
+ const src = path.join(REGISTRY_DIR, `${name}.tsx`)
144
+ const dest = await resolveDestination(cwd, name)
145
+
146
+ if (await fileExists(dest)) {
147
+ if (options.force !== true) {
148
+ log(
149
+ 'warn',
150
+ `Já existe em ${path.relative(cwd, dest)} — passa --force pra sobrescrever.`,
151
+ )
152
+ process.exit(1)
153
+ }
154
+ log('warn', `Sobrescrevendo ${path.relative(cwd, dest)}`)
155
+ }
156
+
157
+ await fs.mkdir(path.dirname(dest), { recursive: true })
158
+ const content = await fs.readFile(src, 'utf-8')
159
+ await fs.writeFile(dest, content, 'utf-8')
160
+
161
+ log('success', `✓ Criado ${path.relative(cwd, dest)}`)
162
+ console.log(
163
+ '\n Edita à vontade — esse arquivo é seu agora. Próximas runs com --force\n sobrescrevem suas mudanças.\n',
164
+ )
165
+ }
166
+
167
+ async function cmdCheck(dir) {
168
+ const root = path.resolve(process.cwd(), dir ?? '.')
169
+ const rel = path.relative(process.cwd(), root) || '.'
170
+ const { files, actions, contracts, findings, hasMarker } = await scanDir(root)
171
+
172
+ if (actions === 0) {
173
+ const verdict = emptyScanVerdict({ hasMarker, contracts })
174
+ // Projeto MARCADO (opus.json) sem actions passa vacuamente — server/lib/app sem
175
+ // domínio ainda. Sem marcador, "0 actions" segue sintoma (diretório errado ou
176
+ // padrão que o check não enxerga), não aprovação.
177
+ if (verdict.ok) {
178
+ log('success', `✓ opus check: ${verdict.message}`)
179
+ return
180
+ }
181
+ log('error', `✗ opus check: nenhuma action encontrada em ${rel} (${files} arquivos .ts); ${verdict.message}`)
182
+ process.exit(1)
183
+ }
184
+ const viaContrato = contracts > 0 ? ` (${contracts} contrato(s))` : ''
185
+ if (findings.length === 0) {
186
+ log('success', `✓ opus check: ${actions} action(s)${viaContrato}, 0 violação — padrão ok.`)
187
+ return
188
+ }
189
+
190
+ const byFile = new Map()
191
+ for (const f of findings) {
192
+ if (!byFile.has(f.file)) byFile.set(f.file, [])
193
+ byFile.get(f.file).push(f)
194
+ }
195
+ for (const [file, list] of byFile) {
196
+ log('warn', `\n${file}`)
197
+ for (const f of list.sort((a, b) => a.line - b.line)) {
198
+ console.log(` ${String(f.line).padStart(4)}: [${f.rule}] ${f.action} — ${f.message}`)
199
+ }
200
+ }
201
+ log('error', `\n✗ opus check: ${findings.length} violação(ões) em ${byFile.size}/${actions} action(s).`)
202
+ process.exit(1)
203
+ }
204
+
205
+ async function cmdCreate(dir, flags) {
206
+ if (!dir) {
207
+ log('error', 'Uso: opus create <dir> — o nome do diretório vira o nome do app (--monorepo: a raiz do workspace).')
208
+ process.exit(1)
209
+ }
210
+ const target = path.resolve(process.cwd(), dir)
211
+ const name = path.basename(target)
212
+
213
+ if (flags.monorepo) {
214
+ const r = await createMonorepo(REGISTRY_DIR, target, name)
215
+ log('success', `✓ opus create: raiz do monorepo "${name}" criada (base v${r.version}).`)
216
+ console.log(`\n ${r.files.length} arquivos (workspace apps/* + packages/*, registry, allowBuilds).`)
217
+ console.log(`
218
+ Próximos passos:
219
+ cd ${dir}
220
+ git init && git add -A && git commit -m "chore: raiz do monorepo"
221
+ opus create apps/<seu-app> # o modo app detecta o workspace
222
+ `)
223
+ return
224
+ }
225
+
226
+ const r = await createProject(REGISTRY_DIR, target, name)
227
+ const modeLabel = r.mode === 'app' ? 'app no monorepo (workspace detectado)' : 'repo standalone'
228
+ log('success', `✓ opus create: app "${name}" criado (base v${r.version}, ${modeLabel}).`)
229
+ console.log(`\n ${r.files.length} arquivos do esqueleto + setup (${r.setup.created.join(', ')}).`)
230
+ const warnings = [...r.warnings, ...r.setup.warnings]
231
+ if (warnings.length) {
232
+ console.log('\n falta plugar (à mão, pra não sobrescrever o seu):')
233
+ for (const w of warnings) console.log(` ! ${w}`)
234
+ }
235
+ if (r.mode === 'app') {
236
+ console.log(`
237
+ Próximos passos (no monorepo):
238
+ pnpm install
239
+ pnpm --filter ${name} test && pnpm --filter ${name} manifest
240
+
241
+ Depois: cadastro do app no admin (root/run) → sessão no Maestro.
242
+ `)
243
+ } else {
244
+ console.log(`
245
+ Próximos passos:
246
+ cd ${dir}
247
+ git init && git add -A && git commit -m "chore: esqueleto opus"
248
+ pnpm install
249
+ pnpm test && pnpm exec opus check src && pnpm manifest
250
+
251
+ Depois: repo no GitHub + cadastro no admin (workspace/repo/app/agentes) → sessão no Maestro.
252
+ `)
253
+ }
254
+ }
255
+
256
+ async function cmdInit() {
257
+ const cwd = process.cwd()
258
+ const r = await initProject(REGISTRY_DIR, cwd)
259
+ log('success', `✓ opus setup: app v${r.version} ${r.wasInitialized ? 'atualizado' : 'inicializado'}.`)
260
+ if (r.created.length) {
261
+ console.log('\n criados (per-app):')
262
+ for (const c of r.created) console.log(` + ${c}`)
263
+ }
264
+ if (r.synced.length) {
265
+ console.log('\n atualizados (bloco gerenciado):')
266
+ for (const c of r.synced) console.log(` ~ ${c}`)
267
+ }
268
+ if (r.warnings.length) {
269
+ console.log('\n falta plugar (à mão, pra não sobrescrever o seu):')
270
+ for (const w of r.warnings) console.log(` ! ${w}`)
271
+ }
272
+
273
+ // Fundação de UI (só apps web): preset + tema + flags. Cria o que falta, avisa o resto.
274
+ const ui = await setupUiFoundation(cwd)
275
+ if (ui.applicable) {
276
+ if (ui.created.length) {
277
+ console.log('\n fundação de UI criada:')
278
+ for (const c of ui.created) console.log(` + ${c}`)
279
+ }
280
+ if (ui.warnings.length) {
281
+ console.log('\n fundação de UI — falta plugar (à mão, pra não sobrescrever o seu):')
282
+ for (const w of ui.warnings) console.log(` ! ${w}`)
283
+ }
284
+ if (!ui.created.length && !ui.warnings.length) {
285
+ console.log('\n fundação de UI: ok (preset + tema + flags já plugados).')
286
+ }
287
+ }
288
+
289
+ console.log(
290
+ '\n Per-app: opus.json (pin) + CLAUDE.md. A spec vive nas declarações (description → manifest).\n' +
291
+ ' A camada-base (.claude agents+skills) é REPO-LEVEL — o Maestro a materializa.\n' +
292
+ ' Gate de build: opus check.\n',
293
+ )
294
+ }
295
+
296
+ async function cmdIntrospect(dir, flags) {
297
+ const root = path.resolve(process.cwd(), dir ?? '.')
298
+ const model = await introspect(root)
299
+ if (flags.json) {
300
+ console.log(JSON.stringify(model, null, 2))
301
+ return
302
+ }
303
+ const rel = path.relative(process.cwd(), root) || '.'
304
+ log('info', `\nopus introspect — ${rel}`)
305
+ console.log(` actions: ${model.actions.length}`)
306
+ console.log(` reactions: ${model.reactions.length}`)
307
+ console.log(` schedules: ${model.schedules.length}`)
308
+ if (model.wiring.length > 0) {
309
+ log('info', '\nwiring:')
310
+ for (const e of model.wiring) {
311
+ if (e.kind === 'schedule') console.log(` ⏱ ${e.from} → ${e.to} (${e.via})`)
312
+ else console.log(` ⚡ ${e.from} → ${e.to} (evento: ${e.via})`)
313
+ }
314
+ }
315
+ console.log('')
316
+ }
317
+
318
+ // =============================================================================
319
+ // Entry
320
+ // =============================================================================
321
+
322
+ function parseArgs(argv) {
323
+ const args = argv.slice(2)
324
+ const positional = []
325
+ const flags = { force: false, help: false }
326
+ // Flags com valor — pega o próximo arg.
327
+ const VALUE_FLAGS = new Set(['--config', '--output'])
328
+ for (let i = 0; i < args.length; i++) {
329
+ const arg = args[i]
330
+ if (arg === '--force' || arg === '-f') {
331
+ flags.force = true
332
+ continue
333
+ }
334
+ if (arg === '--help' || arg === '-h') {
335
+ flags.help = true
336
+ continue
337
+ }
338
+ if (arg === '--check') {
339
+ flags.check = true
340
+ continue
341
+ }
342
+ if (arg === '--json') {
343
+ flags.json = true
344
+ continue
345
+ }
346
+ if (arg === '--monorepo') {
347
+ flags.monorepo = true
348
+ continue
349
+ }
350
+ if (VALUE_FLAGS.has(arg)) {
351
+ const next = args[i + 1]
352
+ if (typeof next !== 'string' || next.startsWith('-')) {
353
+ log('error', `Flag ${arg} precisa de valor`)
354
+ process.exit(1)
355
+ }
356
+ // Strip leading "--" pra usar como key direto.
357
+ flags[arg.slice(2)] = next
358
+ i++
359
+ continue
360
+ }
361
+ if (arg.startsWith('--')) {
362
+ log('warn', `Flag desconhecida: ${arg}`)
363
+ continue
364
+ }
365
+ positional.push(arg)
366
+ }
367
+ return { positional, flags }
368
+ }
369
+
370
+ async function main() {
371
+ const { positional, flags } = parseArgs(process.argv)
372
+ const [command, ...rest] = positional
373
+
374
+ if (command === undefined || command === 'help') {
375
+ console.log(`
376
+ @softize/opus CLI
377
+
378
+ Comandos:
379
+ create <dir> Scaffolda um app opus-based canônico (vite+react+ui+domínio-exemplo);
380
+ detecta workspace (app em monorepo); --monorepo cria a RAIZ do workspace
381
+ setup Bootstrap PER-APP: grava opus.json + CLAUDE.md (se faltam)
382
+ add <name> Copia template do registry pro consumer
383
+ list Lista templates disponíveis
384
+ gen Gera manifest/openapi/docs/stubs a partir do opus.config.ts
385
+ check [dir] Valida as convenções das actions (régua de padrão; exit ≠ 0 se violar)
386
+ db <verbo> Comandos de banco. v1: db check (drift-check entidade ↔ banco)
387
+ introspect [dir] Modelo da estrutura (actions/reactions/schedules + wiring); --json
388
+ mcp Server MCP (introspect/check/create-action) — agentes via mcp_config
389
+ help Mostra esta mensagem
390
+
391
+ Flags:
392
+ --force, -f Sobrescreve arquivo existente
393
+ --config <path> (gen) Caminho do opus.config.ts
394
+ --output <path> (gen) Pasta de saída
395
+
396
+ Exemplos:
397
+ npx @softize/opus add action-form
398
+ npx @softize/opus add action-list --force
399
+ npx @softize/opus list
400
+ npx @softize/opus gen --config ./apps/api/opus.config.ts
401
+ `)
402
+ return
403
+ }
404
+
405
+ if (command === 'create') {
406
+ if (flags.help) {
407
+ console.log(`
408
+ @softize/opus create <dir>
409
+
410
+ Scaffolda um app opus-based CANÔNICO no diretório <dir> (recusa dir não-vazio).
411
+ O esqueleto versiona com a base — nasce com os pré-requisitos plugados:
412
+ protocolo opus.config.ts + domínio-exemplo + manifest/scripts (+ teste verde)
413
+ UI vite + react + tailwind v4 CSS-first (tema + @source do Opus)
414
+ Maestro dev server na porta injetada (PORT) + allowedHosts do preview
415
+ dia zero opus.json, CLAUDE.md (bloco), .claude/memory/, CI de fábrica
416
+
417
+ Depois: git init && pnpm install && pnpm test — e cadastre repo/app no admin.
418
+ `)
419
+ return
420
+ }
421
+ await cmdCreate(rest[0], flags)
422
+ return
423
+ }
424
+
425
+ if (command === 'setup') {
426
+ if (flags.help) {
427
+ console.log(`
428
+ @softize/opus setup
429
+
430
+ Bootstrap PER-APP de um projeto pra usar a base (idempotente):
431
+ opus.json marcador com a versão da base pinada (fonte da verdade)
432
+ CLAUDE.md pointer do protocolo — criado só se faltar (não sobrescreve)
433
+
434
+ Fundação de UI (só apps web — preset Tailwind + tema + flags do Opus):
435
+ tailwind.config criado se faltar; senão avisa pra estender o preset
436
+ tema/tsconfig/dep — avisa o que falta plugar (não edita seus arquivos: sem clobber)
437
+
438
+ A camada-base (agents/skills) é REPO-LEVEL — materializada pelo MAESTRO (o regente),
439
+ NÃO pelo setup. Roda manual ou via postinstall.
440
+ `)
441
+ return
442
+ }
443
+ await cmdInit()
444
+ return
445
+ }
446
+
447
+ if (command === 'list') {
448
+ await cmdList()
449
+ return
450
+ }
451
+
452
+ if (command === 'add') {
453
+ if (flags.help) {
454
+ console.log(`
455
+ @softize/opus add <name>
456
+
457
+ Copia o template registry/<name>.tsx pra src/components/action/<name>.tsx
458
+ (ou ao alias resolvido via components.json).
459
+
460
+ Flags:
461
+ --force, -f Sobrescreve existente
462
+ `)
463
+ return
464
+ }
465
+ const [name] = rest
466
+ await cmdAdd(name, flags)
467
+ return
468
+ }
469
+
470
+ if (command === 'gen') {
471
+ if (flags.help) {
472
+ helpGen()
473
+ return
474
+ }
475
+ await cmdGen(flags)
476
+ return
477
+ }
478
+
479
+ if (command === 'check') {
480
+ if (flags.help) {
481
+ console.log(`
482
+ @softize/opus check [dir]
483
+
484
+ Valida as convenções das actions (estático, TS compiler API). Enxerga
485
+ \`defineAction\` e o split \`defineContract\`+\`bindAction\`:
486
+ action-name — <resource>.<verb> (minúsculo, ponto)
487
+ kind — simple|form|list|view
488
+ field-order — identidade→docs→input/output→authorize→handler→comportamento
489
+ export — defineAction/defineContract/bindAction como \`export const\`
490
+ requires-sem-authorize — \`requires\` é declarativo (o runtime não o executa);
491
+ action com requires precisa de authorize (no contrato
492
+ ou no binding — o join contrato↔bind é cross-file)
493
+
494
+ Exit ≠ 0 se houver violação OU se não achar action nenhuma (gate vazio não
495
+ passa verde). Default dir: cwd.
496
+ `)
497
+ return
498
+ }
499
+ await cmdCheck(rest[0])
500
+ return
501
+ }
502
+
503
+ if (command === 'db') {
504
+ await cmdDb(rest, flags)
505
+ return
506
+ }
507
+
508
+ if (command === 'introspect') {
509
+ await cmdIntrospect(rest[0], flags)
510
+ return
511
+ }
512
+
513
+ if (command === 'mcp') {
514
+ // stdout é o canal do protocolo MCP — NÃO logar aqui.
515
+ const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js')
516
+ const { buildServer } = await import('./lib/mcp.mjs')
517
+ await buildServer().connect(new StdioServerTransport())
518
+ return // processo segue vivo pelo listener do stdin
519
+ }
520
+
521
+ log('error', `Comando desconhecido: ${command}`)
522
+ process.exit(1)
523
+ }
524
+
525
+ main().catch((err) => {
526
+ log('error', `Erro: ${err.message}`)
527
+ process.exit(1)
528
+ })