@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,652 @@
1
+ /**
2
+ * opusDesign() — plugin vite que sobe o runtime Opus DENTRO do dev server e
3
+ * serve `/api` in-process no preview de design.
4
+ *
5
+ * Por quê: o preview de design precisa ser AUTOSSUFICIENTE e isolado de prod.
6
+ * Antes, o dev server proxeava `/api` pra um backend real — o que vaza escrita
7
+ * pra prod e exige infra de pé pra desenhar tela. Com o plugin, `vite --mode
8
+ * design` monta o runtime (driver `nodeServer`) no próprio dev server: os
9
+ * `mockHandler` dos contratos respondem (`OPUS_MODE=design`), qualquer proxy
10
+ * configurado é DESLIGADO (prefixo ex-proxy sem cobertura responde 503 em
11
+ * envelope, nunca vaza), e o Maestro sonda `GET /__opus/design` pra saber que
12
+ * o preview está montado.
13
+ *
14
+ * Invariante: o runtime de design é request/response — reactions e schedules
15
+ * dos entries são IGNORADOS (não registrados), senão o `start()` exige
16
+ * eventbus/scheduler adapters que o design não tem por que montar.
17
+ *
18
+ * Uso (vite.config.ts do app):
19
+ * import { opusDesign } from '@softize/opus/vite'
20
+ * export default defineConfig({ plugins: [react(), opusDesign()] })
21
+ * // vite --mode design
22
+ */
23
+
24
+ import { readdirSync, readFileSync } from 'node:fs'
25
+ import type { IncomingMessage, ServerResponse } from 'node:http'
26
+ import path from 'node:path'
27
+ import type { Plugin, ViteDevServer } from 'vite'
28
+ import {
29
+ createRuntime,
30
+ error,
31
+ flattenDomain,
32
+ isDomainConfig,
33
+ isReaction,
34
+ isSchedule,
35
+ type ActionDef,
36
+ type AuthAdapter,
37
+ type ErrorInput,
38
+ type LoaderResolver,
39
+ type Runtime,
40
+ type RuntimeSetup,
41
+ type User,
42
+ } from '../core/index.ts'
43
+ import { mittEvents } from '../events/drivers/mitt.ts'
44
+ import { nodeServer, type NodeServerHandle } from '../server/drivers/node.ts'
45
+ import { serializeResult } from '../server/index.ts'
46
+ // Import relativo de propósito: `memStorage` é harness de teste, não superfície
47
+ // pública de runtime — o plugin o reusa sem promover export novo.
48
+ import { memStorage } from '../testing/index.ts'
49
+
50
+ // =============================================================================
51
+ // Options
52
+ // =============================================================================
53
+
54
+ export type DesignStubHandler = (
55
+ req: IncomingMessage,
56
+ res: ServerResponse,
57
+ ) => void | Promise<void>
58
+
59
+ export interface OpusDesignOptions {
60
+ /**
61
+ * Entry(s) com os registráveis do design. Default './opus.config.ts'.
62
+ *
63
+ * Entry ESTÁTICO: módulo que default-exporta `{ domains: DomainConfig[] }`
64
+ * (shape do opus.config) OU um array de DomainConfig/ActionDef/contratos.
65
+ *
66
+ * Entry GLOB (contém `*`, ex.: 'src/domains/*\/contract.ts'): expandido a
67
+ * CADA rebuild — domínio novo entra no ar ao salvar o arquivo, sem editar
68
+ * entry nenhum. Dos módulos casados entram os exports (default e named) com
69
+ * shape de registrável; schemas/helpers exportados junto ficam de fora.
70
+ * Pode misturar estáticos e globs no array.
71
+ */
72
+ entry?: string | string[]
73
+
74
+ /** Prefixo do path. Default '/api'. */
75
+ apiPrefix?: string
76
+
77
+ /**
78
+ * Sessão fixa do preview. Default `{ id: 'design' }` — `execute()` exige
79
+ * user pra action não-public. `null` explícito simula anônimo.
80
+ */
81
+ user?: User | null
82
+
83
+ /** Tenant da sessão de design. Default null. */
84
+ tenantId?: string | null
85
+
86
+ /** Resolvers da DSL `loads` — domínio bound com `loads` falha ANTES do mock sem resolver. */
87
+ loaders?: Record<string, LoaderResolver>
88
+
89
+ /**
90
+ * Handlers raw por prefixo (podem streamar SSE). Mais-específico vence:
91
+ * um stub em '/api/auth' sombreia o '/api' do runtime.
92
+ */
93
+ stubs?: Record<string, DesignStubHandler>
94
+
95
+ /**
96
+ * Monta o /api também no dev NORMAL (handler real, sem OPUS_MODE).
97
+ * Default false — devBackend puro não é design (sem sonda, proxy intacto).
98
+ */
99
+ devBackend?: boolean
100
+
101
+ /** Overrides do setup do runtime de design (adapters extras etc). */
102
+ runtime?: Partial<RuntimeSetup>
103
+ }
104
+
105
+ // =============================================================================
106
+ // designAuth
107
+ // =============================================================================
108
+
109
+ /**
110
+ * AuthAdapter de design: ignora o request e devolve a sessão fixa do preview,
111
+ * com `can` liberado — autorização de verdade é preocupação do backend real.
112
+ */
113
+ export function designAuth(
114
+ opts: { user?: User | null; tenantId?: string | null } = {},
115
+ ): AuthAdapter {
116
+ const user = opts.user === undefined ? { id: 'design' } : opts.user
117
+ const tenantId = opts.tenantId ?? null
118
+ return {
119
+ name: 'design',
120
+ kind: 'auth',
121
+ resolveContext: async () => ({ user, tenantId, can: () => true }),
122
+ }
123
+ }
124
+
125
+ // =============================================================================
126
+ // Plugin
127
+ // =============================================================================
128
+
129
+ const PROBE_PATH = '/__opus/design'
130
+
131
+ // Paths default que o mountEndpoints do driver monta — o middleware precisa
132
+ // deixá-los chegar no handler (moram fora do apiPrefix).
133
+ const RUNTIME_ENDPOINT_PATHS = new Set(['/health', '/ready', '/openapi.json'])
134
+
135
+ interface DesignState {
136
+ handle: NodeServerHandle
137
+ runtime: Runtime
138
+ actions: ActionDef[]
139
+ }
140
+
141
+ export function opusDesign(options: OpusDesignOptions = {}): Plugin {
142
+ const apiPrefix = options.apiPrefix ?? '/api'
143
+ const entries = Array.isArray(options.entry)
144
+ ? options.entry
145
+ : [options.entry ?? './opus.config.ts']
146
+ const devBackend = options.devBackend === true
147
+ // Mais-específico primeiro: prefixo mais longo vence.
148
+ const stubEntries = Object.entries(options.stubs ?? {}).sort(
149
+ (a, b) => b[0].length - a[0].length,
150
+ )
151
+ const opusVersion = readOpusVersion()
152
+
153
+ let exProxyPrefixes: string[] = []
154
+ let current: Promise<DesignState> | undefined
155
+ let stale = false
156
+
157
+ return {
158
+ name: 'opus-design',
159
+
160
+ apply(_config, env) {
161
+ return env.command === 'serve' && (env.mode === 'design' || devBackend)
162
+ },
163
+
164
+ config(config, env) {
165
+ // O pacote embarca TS source: externalizado no SSR, o node recusa TS sob
166
+ // node_modules — e o entry importa @softize/opus. noExternal faz o vite
167
+ // transformar o pacote no ssrLoadModule (vale pro dev normal com devBackend).
168
+ const ssr = (config.ssr ??= {})
169
+ if (ssr.noExternal !== true) {
170
+ const prev =
171
+ ssr.noExternal === undefined
172
+ ? []
173
+ : Array.isArray(ssr.noExternal)
174
+ ? ssr.noExternal
175
+ : [ssr.noExternal]
176
+ ssr.noExternal = [...prev, '@softize/opus']
177
+ }
178
+
179
+ if (env.mode !== 'design') return
180
+ // 1) Guarda os prefixos que iam pra proxy — sem cobertura viram 503 explícito.
181
+ exProxyPrefixes = Object.keys(config.server?.proxy ?? {})
182
+ // 2) Mata o proxy: fecha o vazamento preview→prod pra QUALQUER prefixo.
183
+ if (config.server?.proxy !== undefined) config.server.proxy = undefined
184
+ // 3) O execute() roteia handler×mockHandler lendo OPUS_MODE (isOpusDesignMode);
185
+ // com o runtime local, o preview fica autossuficiente.
186
+ process.env.OPUS_MODE = 'design'
187
+ },
188
+
189
+ configureServer(server) {
190
+ const isDesign = server.config.mode === 'design'
191
+ const logger = server.config.logger
192
+ // Um rebuild-por-404 por ciclo de staleness (rearma quando o watcher marca
193
+ // stale de novo) — cura o "domínio novo dá 404" sem virar rebuild-storm.
194
+ let unknownRetryDone = false
195
+
196
+ const boot = async (): Promise<DesignState> => {
197
+ const registrables: unknown[] = []
198
+ for (const entry of entries) {
199
+ if (isGlobPattern(entry)) {
200
+ registrables.push(...(await loadGlobEntry(server, entry)))
201
+ } else {
202
+ registrables.push(...(await loadEntry(server, entry)))
203
+ }
204
+ }
205
+ const { actions, dropped } = collectActions(registrables)
206
+
207
+ const handle = nodeServer({ apiPrefix })
208
+ const runtime = createRuntime({
209
+ server: handle.adapter,
210
+ auth: designAuth({ user: options.user, tenantId: options.tenantId }),
211
+ eventBus: mittEvents(),
212
+ storage: memStorage(),
213
+ // SEM `data` de propósito: design não toca ctx.db — handler real sem
214
+ // mock falhando é o sinal certo de que falta mockar.
215
+ ...options.runtime,
216
+ config: { env: 'development', dev: true, ...options.runtime?.config },
217
+ })
218
+ if (options.loaders !== undefined) runtime.registerLoaders(options.loaders)
219
+ runtime.register(actions)
220
+
221
+ if (dropped > 0) {
222
+ logger.info(
223
+ `[opus-design] ${dropped} reaction(s)/schedule(s) ignorada(s) — o runtime de design é request/response.`,
224
+ )
225
+ }
226
+ if (isDesign) {
227
+ for (const action of actions) {
228
+ if (action.mockHandler === undefined) {
229
+ logger.warn(
230
+ `[opus-design] Action "${action.name}" sem mockHandler — em design cai no handler real e pode falhar sem adapters.`,
231
+ )
232
+ }
233
+ }
234
+ }
235
+
236
+ await runtime.start()
237
+ handle.adapter.mountEndpoints({ health: true, ready: true, openapi: true })
238
+ return { handle, runtime, actions }
239
+ }
240
+
241
+ // Promise-mutex: requests em voo aguardam o MESMO boot; rebuild (stale)
242
+ // descarta o runtime anterior antes de reconstruir — register/start não
243
+ // são re-entrantes.
244
+ const ensureBoot = (): Promise<DesignState> => {
245
+ if (current !== undefined && !stale) return current
246
+ stale = false
247
+ const previous = current
248
+ current = (async () => {
249
+ if (previous !== undefined) {
250
+ const old = await previous.catch(() => undefined)
251
+ if (old !== undefined) await old.runtime.dispose().catch(() => undefined)
252
+ }
253
+ try {
254
+ return await boot()
255
+ } catch (err) {
256
+ // Falha fica retriable: a próxima request tenta reconstruir.
257
+ stale = true
258
+ logger.error(`[opus-design] Boot do runtime de design falhou: ${String(err)}`)
259
+ throw err
260
+ }
261
+ })()
262
+ current.catch(() => undefined)
263
+ return current
264
+ }
265
+
266
+ // Boot eager: o custo de carregar o entry acontece no subir do server,
267
+ // não na primeira request.
268
+ void ensureBoot().catch(() => undefined)
269
+
270
+ // HMR — dois critérios; a próxima request reconstrói do zero (lazy):
271
+ // change: arquivo com módulo no graph (ou o próprio entry) invalida;
272
+ // add/unlink: QUALQUER arquivo sob o root (fora node_modules/.git) —
273
+ // arquivo novo nunca importado não está no graph, e é exatamente ele
274
+ // que um entry glob precisa enxergar. Over-invalidation é barata.
275
+ const entryPaths = new Set(
276
+ entries
277
+ .filter((entry) => !isGlobPattern(entry))
278
+ .map((entry) => path.resolve(server.config.root, entry)),
279
+ )
280
+ // Specs dos entries glob. O watcher do vite só cobre o root, mas um glob
281
+ // pode apontar pra FORA dele (ex.: '../../services/*/contract.ts', caso GB):
282
+ // mandamos o chokidar observar o PREFIXO ESTÁTICO de cada glob — o mesmo que
283
+ // o expandGlob anda, nunca o mundo — pra change/unlink lá também invalidar.
284
+ const globSpecs = entries
285
+ .filter(isGlobPattern)
286
+ .map((pattern) => globSpec(server.config.root, pattern))
287
+ for (const { prefixDir } of globSpecs) server.watcher.add(prefixDir)
288
+ const matchesGlob = (file: string): boolean => {
289
+ const rel = path.relative(server.config.root, file).split(path.sep).join('/')
290
+ return globSpecs.some((spec) => spec.re.test(rel))
291
+ }
292
+ const markStale = (): void => {
293
+ if (current === undefined) return
294
+ stale = true
295
+ unknownRetryDone = false
296
+ }
297
+ const onChange = (file: string): void => {
298
+ const mods = server.moduleGraph.getModulesByFile(file)
299
+ const known = (mods !== undefined && mods.size > 0) || entryPaths.has(file)
300
+ // Um mock casando um glob pra fora do root pode não estar no module graph —
301
+ // o match do glob é o sinal (editar o mock invalida o runtime de design).
302
+ if (!known && !matchesGlob(file)) return
303
+ markStale()
304
+ }
305
+ const onAddOrUnlink = (file: string): void => {
306
+ // Casou um glob (in ou out do root): invalida — unlink de contrato some do
307
+ // rebuild seguinte (sem action fantasma); add entra. Precede a checagem de
308
+ // root, que rejeita `../` — senão a remoção lá fora nunca invalidaria.
309
+ if (matchesGlob(file)) {
310
+ markStale()
311
+ return
312
+ }
313
+ const rel = path.relative(server.config.root, file)
314
+ if (rel.startsWith('..') || path.isAbsolute(rel)) return
315
+ const segments = rel.split(path.sep)
316
+ if (segments.includes('node_modules') || segments.includes('.git')) return
317
+ markStale()
318
+ }
319
+ server.watcher.on('change', onChange)
320
+ server.watcher.on('add', onAddOrUnlink)
321
+ server.watcher.on('unlink', onAddOrUnlink)
322
+
323
+ const respondProbe = async (res: ServerResponse): Promise<void> => {
324
+ const payload: Record<string, unknown> = {
325
+ design: true,
326
+ opus: opusVersion,
327
+ stubs: stubEntries.map(([prefix]) => ({ prefix })),
328
+ }
329
+ try {
330
+ const state = await ensureBoot()
331
+ const unmocked = state.actions.filter((a) => a.mockHandler === undefined)
332
+ payload.runtime = {
333
+ prefix: apiPrefix,
334
+ actions: state.actions.length,
335
+ mocked: state.actions.length - unmocked.length,
336
+ unmocked: unmocked.map((a) => a.name),
337
+ }
338
+ } catch (err) {
339
+ payload.error = String(err)
340
+ }
341
+ res.statusCode = 200
342
+ res.setHeader('content-type', 'application/json; charset=utf-8')
343
+ res.setHeader('cache-control', 'no-store')
344
+ res.end(JSON.stringify(payload))
345
+ }
346
+
347
+ // Registrado DIRETO no body do hook: no vite 5/6 esses middlewares rodam
348
+ // antes do proxy interno e do transform.
349
+ server.middlewares.use((req, res, next) => {
350
+ const pathname = new URL(req.url ?? '/', 'http://localhost').pathname
351
+
352
+ // 1. Stubs primeiro (mais-específico primeiro) — um stub em /api/auth
353
+ // sombreia o /api do runtime.
354
+ for (const [prefix, fn] of stubEntries) {
355
+ if (matchesPrefix(pathname, prefix)) {
356
+ if (isDesign) res.setHeader('x-opus-design', '1')
357
+ try {
358
+ Promise.resolve(fn(req, res)).catch(next)
359
+ } catch (err) {
360
+ next(err)
361
+ }
362
+ return
363
+ }
364
+ }
365
+
366
+ // 2. Protocolo: /api + endpoints do runtime. Aguarda o boot.
367
+ if (matchesPrefix(pathname, apiPrefix) || RUNTIME_ENDPOINT_PATHS.has(pathname)) {
368
+ void (async () => {
369
+ let state: DesignState
370
+ try {
371
+ state = await ensureBoot()
372
+ } catch (err) {
373
+ sendEnvelope(res, 503, {
374
+ code: 'design.boot_failed',
375
+ category: 'dependency',
376
+ message: `Design runtime failed to boot: ${String(err)}`,
377
+ })
378
+ return
379
+ }
380
+
381
+ const method = req.method ?? 'GET'
382
+ if (matchesPrefix(pathname, apiPrefix) && !state.handle.hasRoute(method, pathname)) {
383
+ // 404 que se cura: action desconhecida pode ser só staleness que o
384
+ // watcher não viu (domínio recém-criado). Força UM rebuild por
385
+ // ciclo (mutex = ensureBoot) e re-tenta o match uma vez.
386
+ if (!unknownRetryDone) {
387
+ unknownRetryDone = true
388
+ stale = true
389
+ }
390
+ state = (await ensureBoot().catch(() => undefined)) ?? state
391
+ if (!state.handle.hasRoute(method, pathname)) {
392
+ sendEnvelope(res, 404, {
393
+ code: 'design.action_not_found',
394
+ category: 'not_found',
395
+ message:
396
+ `Rota ${method} ${pathname} não registrada no runtime do opusDesign — ` +
397
+ `confira se o contrato está exportado pelo entry (${entries.join(', ')}); ` +
398
+ `domínio novo exige registro lá (ou use entry glob).`,
399
+ })
400
+ return
401
+ }
402
+ }
403
+
404
+ if (isDesign) res.setHeader('x-opus-design', '1')
405
+ state.handle.handler(req, res, next)
406
+ })()
407
+ return
408
+ }
409
+
410
+ // 3. Sonda — o contrato com o Maestro (mínimo garantido: design true).
411
+ if (isDesign && req.method === 'GET' && pathname === PROBE_PATH) {
412
+ void respondProbe(res)
413
+ return
414
+ }
415
+
416
+ // 4. Prefixo ex-proxy sem cobertura: falha explícita em envelope,
417
+ // nunca vazamento pra prod.
418
+ if (isDesign) {
419
+ const orphan = exProxyPrefixes.find((p) => matchesPrefix(pathname, p))
420
+ if (orphan !== undefined) {
421
+ sendEnvelope(res, 503, {
422
+ code: 'design.no_backend',
423
+ category: 'dependency',
424
+ message: `No backend for "${orphan}" in design mode — add a stub or serve it from the opus runtime`,
425
+ })
426
+ return
427
+ }
428
+ }
429
+
430
+ next()
431
+ })
432
+ },
433
+
434
+ async closeBundle() {
435
+ const state = current === undefined ? undefined : await current.catch(() => undefined)
436
+ current = undefined
437
+ if (state !== undefined) await state.runtime.dispose().catch(() => undefined)
438
+ },
439
+ }
440
+ }
441
+
442
+ // =============================================================================
443
+ // Entry loading
444
+ // =============================================================================
445
+
446
+ /**
447
+ * Carrega um entry pelo pipeline SSR do vite — respeita aliases e TS do app.
448
+ * Isolado nesta função de propósito: no vite 7 o sucessor é
449
+ * `server.environments.ssr` (module runner); quando o piso de versão subir,
450
+ * a troca acontece só aqui.
451
+ */
452
+ async function loadEntry(server: ViteDevServer, entry: string): Promise<unknown[]> {
453
+ const mod = (await server.ssrLoadModule(normalizeEntryUrl(entry))) as Record<string, unknown>
454
+ const root = mod.default ?? mod.config
455
+ if (Array.isArray(root)) return root
456
+ if (
457
+ typeof root === 'object' &&
458
+ root !== null &&
459
+ Array.isArray((root as { domains?: unknown }).domains)
460
+ ) {
461
+ return (root as { domains: unknown[] }).domains
462
+ }
463
+ if (isDomainConfig(root)) return [root]
464
+ throw error({
465
+ code: 'design.invalid_entry',
466
+ category: 'internal',
467
+ message: `Entry "${entry}" must default-export { domains: [...] } or an array of registrables`,
468
+ })
469
+ }
470
+
471
+ function normalizeEntryUrl(entry: string): string {
472
+ if (path.isAbsolute(entry)) return entry
473
+ return `/${entry.replace(/^\.\//, '')}`
474
+ }
475
+
476
+ function isGlobPattern(entry: string): boolean {
477
+ return entry.includes('*')
478
+ }
479
+
480
+ /**
481
+ * Entry por glob: expande o padrão A CADA boot — domínio novo entra no rebuild
482
+ * seguinte sem editar entry nenhum (o watcher de `add` já invalida na criação
483
+ * do arquivo). Cada match carrega pelo pipeline SSR; dos exports entram só os
484
+ * com shape de registrável (schemas/helpers exportados junto ficam de fora).
485
+ */
486
+ async function loadGlobEntry(server: ViteDevServer, pattern: string): Promise<unknown[]> {
487
+ const out = new Set<unknown>()
488
+ for (const file of expandGlob(server.config.root, pattern)) {
489
+ const mod = (await server.ssrLoadModule(file)) as Record<string, unknown>
490
+ for (const item of registrablesOfModule(mod)) out.add(item)
491
+ }
492
+ return [...out]
493
+ }
494
+
495
+ /**
496
+ * Matcher próprio de propósito (sem dep nova): `*` casa um segmento (sem '/'),
497
+ * `**` casa qualquer profundidade. Cobre o entry por pasta de domínio; padrão
498
+ * exótico não suportado = aponte entries estáticos.
499
+ */
500
+ function globToRegExp(pattern: string): RegExp {
501
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&')
502
+ // `\u0000` como placeholder do `**`: nunca ocorre num path real.
503
+ const body = escaped
504
+ .replace(/\*\*/g, '\u0000')
505
+ .replace(/\*/g, '[^/]*')
506
+ .replace(/\u0000/g, '.*')
507
+ return new RegExp(`^${body}$`)
508
+ }
509
+
510
+ interface GlobSpec {
511
+ /** Regex que casa o path RELATIVO ao root (posix), inclusive `../` pra fora. */
512
+ re: RegExp
513
+ /** Prefixo estático do padrão (absoluto): `path.join(root, ...estáticos)`. É o
514
+ * ponto de partida do walk E o caminho que o watcher observa. */
515
+ prefixDir: string
516
+ }
517
+
518
+ /**
519
+ * O par (regex, prefixo estático) de um glob relativo ao `root`. Isolado pra ser
520
+ * reusado: o `expandGlob` anda a partir do prefixo, e o `configureServer` manda o
521
+ * chokidar observar o MESMO prefixo (globs pra fora do root inclusos) — um cálculo
522
+ * só decide o que carregar E o que vigiar, sem observar o mundo.
523
+ */
524
+ function globSpec(root: string, pattern: string): GlobSpec {
525
+ const normalized = pattern.replace(/^\.\//, '').replace(/^\/+/, '')
526
+ const re = globToRegExp(normalized)
527
+ const staticSegments: string[] = []
528
+ for (const segment of normalized.split('/')) {
529
+ if (segment.includes('*')) break
530
+ staticSegments.push(segment)
531
+ }
532
+ return { re, prefixDir: path.join(root, ...staticSegments) }
533
+ }
534
+
535
+ function expandGlob(root: string, pattern: string): string[] {
536
+ const { re, prefixDir } = globSpec(root, pattern)
537
+ const matches: string[] = []
538
+ const walk = (dir: string): void => {
539
+ let dirents
540
+ try {
541
+ dirents = readdirSync(dir, { withFileTypes: true })
542
+ } catch {
543
+ return
544
+ }
545
+ for (const dirent of dirents) {
546
+ if (dirent.name === 'node_modules' || dirent.name === '.git') continue
547
+ const full = path.join(dir, dirent.name)
548
+ if (dirent.isDirectory()) {
549
+ walk(full)
550
+ } else if (re.test(path.relative(root, full).split(path.sep).join('/'))) {
551
+ matches.push(full)
552
+ }
553
+ }
554
+ }
555
+ walk(prefixDir)
556
+ return matches.sort()
557
+ }
558
+
559
+ const ACTION_KINDS = new Set(['simple', 'form', 'list', 'view'])
560
+
561
+ /** Registrável por shape: domínio, reaction/schedule (contados e dropados no
562
+ * collect) ou action/contrato (`name` + `kind` canônico). */
563
+ function isRegistrable(value: unknown): boolean {
564
+ if (typeof value !== 'object' || value === null) return false
565
+ if (isDomainConfig(value) || isReaction(value) || isSchedule(value)) return true
566
+ const candidate = value as { name?: unknown; kind?: unknown }
567
+ return (
568
+ typeof candidate.name === 'string' &&
569
+ typeof candidate.kind === 'string' &&
570
+ ACTION_KINDS.has(candidate.kind)
571
+ )
572
+ }
573
+
574
+ /** Colhe registráveis de um módulo do glob: named e default; arrays e o shape
575
+ * `{ domains }` são desdobrados; o resto (schemas, helpers) fica de fora. */
576
+ function registrablesOfModule(mod: Record<string, unknown>): unknown[] {
577
+ const out = new Set<unknown>()
578
+ const add = (value: unknown): void => {
579
+ if (Array.isArray(value)) {
580
+ for (const item of value) add(item)
581
+ return
582
+ }
583
+ if (isRegistrable(value)) {
584
+ out.add(value)
585
+ return
586
+ }
587
+ if (typeof value === 'object' && value !== null && Array.isArray((value as { domains?: unknown }).domains)) {
588
+ add((value as { domains: unknown[] }).domains)
589
+ }
590
+ }
591
+ for (const value of Object.values(mod)) add(value)
592
+ return [...out]
593
+ }
594
+
595
+ /** Achata os registráveis em actions; reactions/schedules ficam de fora (contados). */
596
+ function collectActions(items: unknown[]): { actions: ActionDef[]; dropped: number } {
597
+ const actions: ActionDef[] = []
598
+ let dropped = 0
599
+ for (const item of items) {
600
+ if (typeof item !== 'object' || item === null) continue
601
+ if (isDomainConfig(item)) {
602
+ const flat = flattenDomain(item)
603
+ actions.push(...flat.actions)
604
+ dropped += flat.reactions.length + flat.schedules.length
605
+ continue
606
+ }
607
+ if (isReaction(item) || isSchedule(item)) {
608
+ dropped += 1
609
+ continue
610
+ }
611
+ // ActionDef ou contrato (sem handler): em design o mockHandler responde.
612
+ actions.push(item as ActionDef)
613
+ }
614
+ return { actions, dropped }
615
+ }
616
+
617
+ // =============================================================================
618
+ // Helpers
619
+ // =============================================================================
620
+
621
+ function matchesPrefix(pathname: string, prefix: string): boolean {
622
+ return pathname === prefix || pathname.startsWith(`${prefix}/`)
623
+ }
624
+
625
+ /** Envelope opus com `meta` completo — o fetch client só reconhece envelope com `meta`. */
626
+ function sendEnvelope(res: ServerResponse, status: number, input: ErrorInput): void {
627
+ res.statusCode = status
628
+ res.setHeader('content-type', 'application/json; charset=utf-8')
629
+ res.end(
630
+ JSON.stringify(
631
+ serializeResult({
632
+ ok: false,
633
+ error: error(input),
634
+ meta: { actionId: crypto.randomUUID(), action: 'opus.design', durationMs: 0 },
635
+ }),
636
+ ),
637
+ )
638
+ }
639
+
640
+ function readOpusVersion(): string {
641
+ // `import.meta.url` sobrevive ao bundling do vite.config (o vite injeta a
642
+ // URL original do arquivo), então o package.json resolve do source real.
643
+ try {
644
+ const pkg = JSON.parse(
645
+ readFileSync(new URL('../../package.json', import.meta.url), 'utf8'),
646
+ ) as { version?: string }
647
+ return pkg.version ?? '0.0.0'
648
+ /* v8 ignore next 3 — defensivo; o package.json viaja junto do source */
649
+ } catch {
650
+ return '0.0.0'
651
+ }
652
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @softize/opus/vite — plugins vite do Opus.
3
+ *
4
+ * `opusDesign()` sobe o runtime em modo design dentro do dev server (preview
5
+ * isolado de prod); `designAuth()` é o AuthAdapter de sessão fixa que ele usa.
6
+ */
7
+ export { designAuth, opusDesign } from './design.ts'
8
+ export type { DesignStubHandler, OpusDesignOptions } from './design.ts'