@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
@@ -0,0 +1,195 @@
1
+ /**
2
+ * gen-openapi — gera OpenAPI 3.1 a partir do manifest serializado.
3
+ *
4
+ * Não reusa `src/schema/openapi.ts` direto pq aquele assume `ActionDef`
5
+ * vivo (com Zod schemas instanciados). Aqui já temos JSON Schema serializado
6
+ * pelo runner. Replicamos a convenção de rota e o envelope `ok/data/meta`
7
+ * pra manter contrato consistente.
8
+ *
9
+ * Convenção (mesma de src/schema/openapi.ts):
10
+ * simple/form → POST /api/{name-com-slashes}
11
+ * list/view → GET /api/{name-com-slashes}
12
+ */
13
+
14
+ import { flattenManifestActions } from './gen-manifest.mjs'
15
+
16
+ export function buildOpenAPI(manifest, options = {}) {
17
+ const {
18
+ info = {
19
+ title: 'Generated by @softize/opus',
20
+ version: manifest.opusVersion,
21
+ },
22
+ servers,
23
+ apiPrefix = '/api',
24
+ } = options
25
+
26
+ const paths = {}
27
+ for (const action of flattenManifestActions(manifest)) {
28
+ if (action.internal === true) continue
29
+ const { path, method } = routeFor(action, apiPrefix)
30
+ const op = buildOperation(action, method)
31
+ paths[path] = paths[path] ?? {}
32
+ paths[path][method] = op
33
+ }
34
+
35
+ const spec = {
36
+ openapi: '3.1.0',
37
+ info,
38
+ paths,
39
+ components: { schemas: {} },
40
+ }
41
+ if (Array.isArray(servers) && servers.length > 0) spec.servers = servers
42
+ return spec
43
+ }
44
+
45
+ function routeFor(action, apiPrefix) {
46
+ const segments = action.name.split('.').map(encodeURIComponent)
47
+ const path = `${apiPrefix}/${segments.join('/')}`
48
+ const method =
49
+ action.kind === 'simple' || action.kind === 'form' ? 'post' : 'get'
50
+ return { path, method }
51
+ }
52
+
53
+ function buildOperation(action, method) {
54
+ const op = {
55
+ operationId: action.name,
56
+ responses: buildResponses(action),
57
+ }
58
+ if (typeof action.summary === 'string') op.summary = action.summary
59
+ if (typeof action.description === 'string') op.description = action.description
60
+ if (Array.isArray(action.tags) && action.tags.length > 0) op.tags = action.tags
61
+
62
+ const inputSchema = action.input
63
+
64
+ if (method === 'post' && inputSchema !== null) {
65
+ op.requestBody = {
66
+ required: true,
67
+ content: { 'application/json': { schema: inputSchema } },
68
+ }
69
+ } else if (method === 'get') {
70
+ const params = buildQueryParams(inputSchema)
71
+ if (params.length > 0) op.parameters = params
72
+ }
73
+
74
+ if (action.public !== true) {
75
+ op.security = [{ bearerAuth: [] }]
76
+ }
77
+
78
+ return op
79
+ }
80
+
81
+ function buildResponses(action) {
82
+ const successStatus = String(action.successStatus ?? 200)
83
+ return {
84
+ [successStatus]: {
85
+ description: 'Success',
86
+ content: {
87
+ 'application/json': {
88
+ schema: wrapEnvelope(action.output, action.kind),
89
+ },
90
+ },
91
+ },
92
+ '4XX': errorResponse('Client error', action.errors),
93
+ '5XX': errorResponse('Server error'),
94
+ }
95
+ }
96
+
97
+ function wrapEnvelope(outputSchema, kind) {
98
+ const data =
99
+ kind === 'list'
100
+ ? {
101
+ type: 'object',
102
+ properties: {
103
+ items: { type: 'array', items: outputSchema ?? {} },
104
+ cursor: {
105
+ type: 'object',
106
+ properties: {
107
+ next: { type: ['string', 'null'] },
108
+ prev: { type: ['string', 'null'] },
109
+ },
110
+ required: ['next'],
111
+ },
112
+ total: { type: 'integer' },
113
+ },
114
+ required: ['items', 'cursor'],
115
+ }
116
+ : (outputSchema ?? {})
117
+
118
+ return {
119
+ type: 'object',
120
+ properties: {
121
+ ok: { const: true },
122
+ data,
123
+ meta: metaSchema(),
124
+ },
125
+ required: ['ok', 'data', 'meta'],
126
+ }
127
+ }
128
+
129
+ function errorResponse(description, errors) {
130
+ const codes =
131
+ Array.isArray(errors) && errors.length > 0
132
+ ? { enum: errors.map((e) => e.code) }
133
+ : { type: 'string' }
134
+ return {
135
+ description,
136
+ content: {
137
+ 'application/json': {
138
+ schema: {
139
+ type: 'object',
140
+ properties: {
141
+ ok: { const: false },
142
+ error: {
143
+ type: 'object',
144
+ properties: {
145
+ code: codes,
146
+ category: { type: 'string' },
147
+ message: { type: 'string' },
148
+ severity: { enum: ['warning', 'error', 'fatal'] },
149
+ retriable: { type: 'boolean' },
150
+ },
151
+ required: ['code', 'category', 'message', 'severity', 'retriable'],
152
+ },
153
+ meta: metaSchema(),
154
+ },
155
+ required: ['ok', 'error', 'meta'],
156
+ },
157
+ },
158
+ },
159
+ }
160
+ }
161
+
162
+ function metaSchema() {
163
+ return {
164
+ type: 'object',
165
+ properties: {
166
+ actionId: { type: 'string' },
167
+ action: { type: 'string' },
168
+ durationMs: { type: 'number' },
169
+ requestId: { type: 'string' },
170
+ cached: { type: 'boolean' },
171
+ },
172
+ required: ['actionId', 'action', 'durationMs'],
173
+ }
174
+ }
175
+
176
+ function buildQueryParams(inputSchema) {
177
+ if (
178
+ inputSchema === null ||
179
+ typeof inputSchema !== 'object' ||
180
+ typeof inputSchema.properties !== 'object' ||
181
+ inputSchema.properties === null
182
+ ) {
183
+ return []
184
+ }
185
+ const props = inputSchema.properties
186
+ const required = new Set(
187
+ Array.isArray(inputSchema.required) ? inputSchema.required : [],
188
+ )
189
+ return Object.entries(props).map(([name, schema]) => ({
190
+ name,
191
+ in: 'query',
192
+ required: required.has(name),
193
+ schema,
194
+ }))
195
+ }
@@ -0,0 +1,472 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gen-runner — subprocess executado via `tsx` pra carregar e serializar
4
+ * o `opus.config.ts` do consumer.
5
+ *
6
+ * Por que subprocess: o CLI principal roda como `.mjs` puro (Node sem
7
+ * loader TS). Pra avaliar TypeScript do consumer com `import`, precisamos
8
+ * de `tsx` no caminho. Spawn isolado mantém o CLI puro e evita poluir o
9
+ * process do usuário.
10
+ *
11
+ * Protocolo:
12
+ * Entrada via argv:
13
+ * [0] node
14
+ * [1] gen-runner.mjs
15
+ * [2] config path absoluto (.ts/.mts/.mjs/.js)
16
+ *
17
+ * Saída:
18
+ * stdout: única linha JSON com `{ ok: true, payload }` ou
19
+ * `{ ok: false, error }`. Tudo que não for essa linha é debug
20
+ * (vai pra stderr).
21
+ *
22
+ * `payload` shape (ver gen-manifest.mjs pra consumidor):
23
+ * {
24
+ * opusVersion: string,
25
+ * sourceConfigDir: string, // dirname absoluto do opus.config.ts
26
+ * output: string, // pasta de saída declarada no config
27
+ * domains: SerializedDomain[]
28
+ * }
29
+ *
30
+ * SerializedDomain:
31
+ * {
32
+ * name, dicts, actions, reactions, schedules, subdomains,
33
+ * hasRepository, hasService, hasModels
34
+ * }
35
+ *
36
+ * SerializedAction (todos os ActionDef preservam suas strings):
37
+ * {
38
+ * name, kind, summary, description, label, title, icon, color, tags,
39
+ * permission: string | string[] | null, // requires fiel (lista = ANY)
40
+ * authorize: '<function>' | string?, // DSL string preservada
41
+ * input: JSONSchema, // zod-to-json-schema
42
+ * output: JSONSchema, // zod-to-json-schema
43
+ * fields?: Record<string, FieldSpecSerialized>,
44
+ * filters?, sort?, paginate?, text?, periods?, // search
45
+ * projection?, expand?, // view
46
+ * background?: boolean,
47
+ * emits?: string[],
48
+ * invalidates?: string[] | '<function>',
49
+ * successStatus?: number,
50
+ * public?: boolean,
51
+ * internal?: boolean,
52
+ * rateLimit?: { window, max },
53
+ * confirm?: ConfirmSpec,
54
+ * messages?, examples?, errors?,
55
+ * sourceModule?: string | null // path absoluto onde a action foi declarada (best-effort)
56
+ * }
57
+ *
58
+ * Closures não serializáveis (authorize fn, handler, etc) viram a string
59
+ * literal `"<function>"`. Mantém o nome do campo no payload pra docs e
60
+ * stubs poderem indicar que existe lógica.
61
+ */
62
+
63
+ import path from 'node:path'
64
+ import { pathToFileURL, fileURLToPath } from 'node:url'
65
+
66
+ const __filename = fileURLToPath(import.meta.url)
67
+ const __dirname = path.dirname(__filename)
68
+ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..')
69
+
70
+ // =============================================================================
71
+ // Entrada
72
+ // =============================================================================
73
+
74
+ async function main() {
75
+ const configPath = process.argv[2]
76
+ if (typeof configPath !== 'string' || configPath.length === 0) {
77
+ return emitError('Missing config path argv[2]')
78
+ }
79
+
80
+ const configAbs = path.resolve(configPath)
81
+ const configUrl = pathToFileURL(configAbs).href
82
+
83
+ // Importa opus (a partir do PACKAGE_ROOT, não do consumer) — precisamos
84
+ // do `flattenDomain` + helpers internos pra serializar.
85
+ const opusCoreUrl = pathToFileURL(
86
+ path.join(PACKAGE_ROOT, 'src', 'core', 'index.ts'),
87
+ ).href
88
+ const { flattenDomain } = await import(opusCoreUrl)
89
+
90
+ // Acessar logical type meta dos dicts.
91
+ const opusSchemaUrl = pathToFileURL(
92
+ path.join(PACKAGE_ROOT, 'src', 'schema', 'index.ts'),
93
+ ).href
94
+ const opusSchema = await import(opusSchemaUrl)
95
+
96
+ // Pra serializar Zod → JSON Schema reusa o caminho que `toOpenAPISpec`
97
+ // usa. Importação direta do npm pq é dep regular da opus.
98
+ const { zodToJsonSchema } = await import('zod-to-json-schema')
99
+
100
+ const pkgUrl = pathToFileURL(path.join(PACKAGE_ROOT, 'package.json')).href
101
+ const pkg = await import(pkgUrl, { with: { type: 'json' } }).then(
102
+ (m) => m.default,
103
+ )
104
+
105
+ // Import dinâmico do config do consumer.
106
+ const mod = await import(configUrl)
107
+ const cfg = mod.default ?? mod.config ?? mod
108
+ if (cfg === undefined || cfg === null || typeof cfg !== 'object') {
109
+ return emitError(
110
+ `Config em ${configAbs} não exporta um objeto default ou named "config"`,
111
+ )
112
+ }
113
+
114
+ if (!Array.isArray(cfg.domains)) {
115
+ return emitError(
116
+ `Config em ${configAbs} precisa de \`domains: DomainConfig[]\` no export default`,
117
+ )
118
+ }
119
+
120
+ const outputDir = typeof cfg.output === 'string' ? cfg.output : '.gen'
121
+
122
+ const ctx = {
123
+ flattenDomain,
124
+ toJsonSchema: (schema) =>
125
+ zodToJsonSchema(schema, { target: 'openApi3', $refStrategy: 'none' }),
126
+ sourceConfigDir: path.dirname(configAbs),
127
+ }
128
+
129
+ const domains = cfg.domains.map((d) => serializeDomain(d, ctx))
130
+
131
+ const payload = {
132
+ opusVersion: pkg.version,
133
+ sourceConfigDir: ctx.sourceConfigDir,
134
+ output: outputDir,
135
+ domains,
136
+ }
137
+
138
+ emit({ ok: true, payload })
139
+ }
140
+
141
+ // =============================================================================
142
+ // Serialização
143
+ // =============================================================================
144
+
145
+ function serializeDomain(domain, ctx) {
146
+ // `entities` é o nome preferido; `models` é o legado (alias). Lê os dois.
147
+ const entitySrc = domain.entities ?? domain.models
148
+ const entityList = serializeEntities(entitySrc)
149
+ return {
150
+ name: domain.name,
151
+ description: nullable(domain.description),
152
+ hasRepository: domain.repository !== undefined,
153
+ hasService: domain.service !== undefined,
154
+ // Compat: nomes ainda expostos; `entities` traz a estrutura+doc completa.
155
+ hasModels: entityList.length > 0,
156
+ modelNames: entityList.map((e) => e.name),
157
+ entities: entityList,
158
+ dicts: serializeDicts(domain.dicts),
159
+ actions: Array.from(iterateActions(domain.actions), (a) =>
160
+ serializeAction(a, ctx),
161
+ ),
162
+ reactions: Array.isArray(domain.reactions)
163
+ ? domain.reactions.map(serializeReaction)
164
+ : [],
165
+ schedules: Array.isArray(domain.schedules)
166
+ ? domain.schedules.map(serializeSchedule)
167
+ : [],
168
+ subdomains: Array.isArray(domain.subdomains)
169
+ ? domain.subdomains.map((s) => serializeDomain(s, ctx))
170
+ : [],
171
+ }
172
+ }
173
+
174
+ /** Serializa as entidades (`defineEntity`) com campos + docs — a fonte que vai pro
175
+ * manifest/lente. Ignora valores que não parecem EntityConfig (name+fields). */
176
+ function serializeEntities(src) {
177
+ if (src === undefined || src === null || typeof src !== 'object') return []
178
+ const out = []
179
+ for (const [key, ent] of Object.entries(src)) {
180
+ if (ent === null || typeof ent !== 'object' || typeof ent.fields !== 'object') continue
181
+ const fields = []
182
+ for (const [fname, f] of Object.entries(ent.fields)) {
183
+ const col = f?.column ?? {}
184
+ fields.push({
185
+ name: fname,
186
+ logicalType: f?.meta?.logicalType ?? null,
187
+ nullable: col.nullable === true,
188
+ pk: col.pk === true,
189
+ references: col.references ?? null,
190
+ doc: col.doc ?? null,
191
+ })
192
+ }
193
+ out.push({
194
+ name: typeof ent.name === 'string' ? ent.name : key,
195
+ description: typeof ent.description === 'string' ? ent.description : null,
196
+ table: typeof ent.table === 'string' ? ent.table : null,
197
+ fields,
198
+ relations: ent.relations ?? null,
199
+ })
200
+ }
201
+ return out
202
+ }
203
+
204
+ function serializeDicts(dicts) {
205
+ if (dicts === undefined || dicts === null || typeof dicts !== 'object') {
206
+ return {}
207
+ }
208
+ const out = {}
209
+ for (const [name, dict] of Object.entries(dicts)) {
210
+ if (dict === null || typeof dict !== 'object') continue
211
+ // DictType expõe `meta.params.entries`. Fallback: tenta `.keys()` +
212
+ // `.metaFor(k)` caso seja shape custom.
213
+ const meta = dict.meta
214
+ if (
215
+ meta !== undefined &&
216
+ meta !== null &&
217
+ meta.logicalType === 'dict' &&
218
+ meta.params !== undefined
219
+ ) {
220
+ const params = meta.params
221
+ const keys = Array.isArray(params.keys) ? params.keys : []
222
+ const entries =
223
+ params.entries !== undefined && params.entries !== null
224
+ ? params.entries
225
+ : {}
226
+ const values = {}
227
+ for (const k of keys) {
228
+ values[k] = entries[k] ?? null
229
+ }
230
+ // `doc` = entendimento do vocabulário inteiro (o que esse dict representa).
231
+ out[name] = { keys, values, doc: typeof params.doc === 'string' ? params.doc : null }
232
+ continue
233
+ }
234
+ // Fallback: dict-like com `keys()` + `metaFor()`.
235
+ if (
236
+ typeof dict.keys === 'function' &&
237
+ typeof dict.metaFor === 'function'
238
+ ) {
239
+ const keys = dict.keys()
240
+ const values = {}
241
+ for (const k of keys) values[k] = dict.metaFor(k)
242
+ out[name] = { keys, values }
243
+ continue
244
+ }
245
+ // Não-reconhecível: registra placeholder pra não silenciar.
246
+ out[name] = { keys: [], values: {}, unknownShape: true }
247
+ }
248
+ return out
249
+ }
250
+
251
+ function serializeAction(action, ctx) {
252
+ const base = {
253
+ name: action.name,
254
+ kind: action.kind,
255
+ summary: nullable(action.summary),
256
+ description: nullable(action.description),
257
+ label: nullable(action.label),
258
+ title: nullable(action.title),
259
+ icon: nullable(action.icon),
260
+ color: nullable(action.color),
261
+ tags: Array.isArray(action.tags) ? action.tags : [],
262
+ permission: derivePermission(action),
263
+ authorize: serializeAuthorize(action.authorize),
264
+ public: action.public === true,
265
+ internal: action.internal === true,
266
+ successStatus: action.successStatus,
267
+ input: safeToJsonSchema(action.input, ctx),
268
+ output: safeToJsonSchema(action.output, ctx),
269
+ invalidates: serializeInvalidates(action.invalidates),
270
+ confirm: action.confirm ?? null,
271
+ rateLimit:
272
+ action.rateLimit !== undefined
273
+ ? { window: action.rateLimit.window, max: action.rateLimit.max }
274
+ : null,
275
+ messages: action.messages ?? null,
276
+ examples: Array.isArray(action.examples) ? action.examples : [],
277
+ errors: Array.isArray(action.errors) ? action.errors : [],
278
+ sourceModule: lookupSourceModule(action, ctx),
279
+ }
280
+
281
+ if (action.kind === 'simple' || action.kind === 'form') {
282
+ base.emits = Array.isArray(action.emits) ? action.emits : []
283
+ base.background =
284
+ action.background !== undefined &&
285
+ action.background !== null &&
286
+ action.background.enabled === true
287
+ }
288
+
289
+ if (action.kind === 'form') {
290
+ base.fields = serializeFields(action.fields)
291
+ }
292
+
293
+ if (action.kind === 'list') {
294
+ base.filters = serializeFilters(action.filters)
295
+ base.sort = action.sort ?? null
296
+ base.paginate = action.paginate ?? null
297
+ base.text = action.text ?? null
298
+ base.periods = Array.isArray(action.periods) ? action.periods : []
299
+ }
300
+
301
+ if (action.kind === 'view') {
302
+ base.projection = Array.isArray(action.projection) ? action.projection : []
303
+ base.expand = action.expand ?? null
304
+ }
305
+
306
+ return base
307
+ }
308
+
309
+ function serializeFields(fields) {
310
+ if (fields === undefined || fields === null || typeof fields !== 'object') {
311
+ return {}
312
+ }
313
+ const out = {}
314
+ for (const [name, spec] of Object.entries(fields)) {
315
+ if (spec === null || typeof spec !== 'object') continue
316
+ out[name] = {
317
+ label: spec.label ?? null,
318
+ placeholder: spec.placeholder ?? null,
319
+ hint: spec.hint ?? null,
320
+ default: spec.default ?? null,
321
+ mask: spec.mask ?? null,
322
+ depends: spec.depends ?? null,
323
+ group: spec.group ?? null,
324
+ order: spec.order ?? null,
325
+ widget: spec.widget ?? null,
326
+ options: spec.options ?? null,
327
+ hasShowWhen: typeof spec.showWhen === 'function',
328
+ hasRequireWhen: typeof spec.requireWhen === 'function',
329
+ aiDescription: spec.aiDescription ?? null,
330
+ }
331
+ }
332
+ return out
333
+ }
334
+
335
+ function serializeFilters(filters) {
336
+ if (filters === undefined || filters === null || typeof filters !== 'object') {
337
+ return {}
338
+ }
339
+ const out = {}
340
+ for (const [name, spec] of Object.entries(filters)) {
341
+ if (spec === null || typeof spec !== 'object') continue
342
+ out[name] = {
343
+ label: spec.label ?? null,
344
+ placeholder: spec.placeholder ?? null,
345
+ type: spec.type ?? null,
346
+ multiple: spec.multiple === true,
347
+ mode: spec.mode ?? null,
348
+ operators: Array.isArray(spec.operators) ? spec.operators : null,
349
+ path: spec.path ?? null,
350
+ section: spec.section ?? null,
351
+ depends: spec.depends ?? null,
352
+ options: spec.options ?? null,
353
+ aiDescription: spec.aiDescription ?? null,
354
+ }
355
+ }
356
+ return out
357
+ }
358
+
359
+ function serializeReaction(reaction) {
360
+ return {
361
+ name: reaction.name,
362
+ on: reaction.on,
363
+ description: nullable(reaction.description),
364
+ tags: Array.isArray(reaction.tags) ? reaction.tags : [],
365
+ handler: '<function>',
366
+ retry: reaction.retry ?? null,
367
+ timeout: reaction.timeout ?? null,
368
+ concurrency: reaction.concurrency ?? null,
369
+ hasDedup: typeof reaction.dedup === 'function',
370
+ hasAuthorize: typeof reaction.authorize === 'function',
371
+ }
372
+ }
373
+
374
+ function serializeSchedule(sched) {
375
+ return {
376
+ name: sched.name,
377
+ action: sched.action,
378
+ cron: nullable(sched.cron),
379
+ every: nullable(sched.every),
380
+ timezone: nullable(sched.timezone),
381
+ enabled: sched.enabled !== false,
382
+ description: nullable(sched.description),
383
+ tags: Array.isArray(sched.tags) ? sched.tags : [],
384
+ input:
385
+ typeof sched.input === 'function' ? '<function>' : (sched.input ?? null),
386
+ }
387
+ }
388
+
389
+ // =============================================================================
390
+ // Helpers
391
+ // =============================================================================
392
+
393
+ function* iterateActions(actions) {
394
+ if (actions === undefined) return
395
+ if (Array.isArray(actions)) {
396
+ for (const a of actions) yield a
397
+ return
398
+ }
399
+ for (const value of Object.values(actions)) {
400
+ if (Array.isArray(value)) {
401
+ for (const a of value) yield a
402
+ } else {
403
+ yield value
404
+ }
405
+ }
406
+ }
407
+
408
+ function derivePermission(action) {
409
+ // Projeção FIEL do `requires`: lista sai lista (semântica ANY) — projetar só a
410
+ // 1ª string faria a spec mentir por omissão pra quem lê o manifest.
411
+ const req = action.requires
412
+ if (typeof req === 'string') return req
413
+ if (Array.isArray(req)) {
414
+ const strs = req.filter((r) => typeof r === 'string')
415
+ return strs.length > 0 ? strs : null
416
+ }
417
+ return null
418
+ }
419
+
420
+ function serializeAuthorize(authorize) {
421
+ if (authorize === undefined || authorize === null) return null
422
+ // DSL: pode chegar como string em frameworks (versão futura). Hoje é fn.
423
+ if (typeof authorize === 'string') return authorize
424
+ if (typeof authorize === 'function') return '<function>'
425
+ return null
426
+ }
427
+
428
+ function serializeInvalidates(inv) {
429
+ if (inv === undefined || inv === null) return null
430
+ if (Array.isArray(inv)) return inv
431
+ if (typeof inv === 'function') return '<function>'
432
+ return null
433
+ }
434
+
435
+ function safeToJsonSchema(schema, ctx) {
436
+ if (schema === undefined || schema === null) return null
437
+ try {
438
+ return ctx.toJsonSchema(schema)
439
+ } catch (err) {
440
+ return { __unserializable: true, reason: err.message }
441
+ }
442
+ }
443
+
444
+ function lookupSourceModule(_action, _ctx) {
445
+ // Não há mecanismo confiável pra extrair o source module a partir do
446
+ // ActionDef em runtime (ESM não expõe). Reservamos o campo pra futura
447
+ // integração com `import.meta.url` no defineAction.
448
+ return null
449
+ }
450
+
451
+ function nullable(value) {
452
+ if (value === undefined || value === null) return null
453
+ if (typeof value === 'string' && value.length === 0) return null
454
+ return value
455
+ }
456
+
457
+ // =============================================================================
458
+ // IPC
459
+ // =============================================================================
460
+
461
+ function emit(payload) {
462
+ process.stdout.write(JSON.stringify(payload) + '\n')
463
+ }
464
+
465
+ function emitError(message) {
466
+ emit({ ok: false, error: message })
467
+ process.exit(1)
468
+ }
469
+
470
+ main().catch((err) => {
471
+ emitError(`runner crash: ${err.stack ?? err.message ?? String(err)}`)
472
+ })