@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,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * db-check-runner — subprocess via `tsx` que roda o drift-check.
4
+ *
5
+ * Por que subprocess: o CLI principal é `.mjs` puro (Node sem loader TS) e o
6
+ * drift-check precisa (a) importar TS do consumer (`opus.config.ts` + entidades)
7
+ * e (b) abrir conexão viva com o banco. Tudo isso roda aqui dentro do tsx.
8
+ *
9
+ * Contrato do `opus.config.ts` pros comandos `db`:
10
+ * - `entities?: EntityConfig[]` — entidades explícitas (opcional)
11
+ * - `domains?: DomainConfig[]` — entidades coletadas de `domain.models`
12
+ * - `database: () => Kysely | Promise<Kysely>` — factory LAZY do banco.
13
+ * É factory (não instância) de propósito: o `gen` importa o mesmo config
14
+ * sem nunca chamar `database()`, então não abre conexão.
15
+ *
16
+ * Protocolo de saída (igual gen-runner): última linha de stdout é um JSON
17
+ * `{ ok: true, findings }` ou `{ ok: false, error }`. Resto é debug.
18
+ */
19
+
20
+ import path from 'node:path'
21
+ import { pathToFileURL } from 'node:url'
22
+
23
+ import { isEntityConfig } from '../../src/schema/entity.ts'
24
+ import { kyselyDriftCheck } from '../../src/data/drivers/kysely.ts'
25
+
26
+ function emit(obj) {
27
+ process.stdout.write(`\n${JSON.stringify(obj)}\n`)
28
+ }
29
+
30
+ function collectEntities(domain, out) {
31
+ if (domain === null || typeof domain !== 'object') return
32
+ const models = domain.models
33
+ if (models !== undefined && models !== null && typeof models === 'object') {
34
+ for (const value of Object.values(models)) {
35
+ if (isEntityConfig(value)) out.push(value)
36
+ }
37
+ }
38
+ if (Array.isArray(domain.subdomains)) {
39
+ for (const sub of domain.subdomains) collectEntities(sub, out)
40
+ }
41
+ }
42
+
43
+ async function main() {
44
+ const configPath = process.argv[2]
45
+ if (typeof configPath !== 'string') {
46
+ emit({ ok: false, error: 'config path ausente' })
47
+ return
48
+ }
49
+
50
+ const mod = await import(pathToFileURL(path.resolve(configPath)).href)
51
+ const config = mod.default ?? mod
52
+
53
+ const entities = []
54
+ if (Array.isArray(config.entities)) {
55
+ for (const e of config.entities) if (isEntityConfig(e)) entities.push(e)
56
+ }
57
+ if (Array.isArray(config.domains)) {
58
+ for (const d of config.domains) collectEntities(d, entities)
59
+ }
60
+
61
+ if (entities.length === 0) {
62
+ emit({ ok: false, error: 'nenhuma entidade encontrada (config.entities ou domain.models)' })
63
+ return
64
+ }
65
+
66
+ if (typeof config.database !== 'function') {
67
+ emit({
68
+ ok: false,
69
+ error:
70
+ 'config.database() ausente — exporte uma factory lazy que retorna um Kysely (ex: `database: () => new Kysely({...})`)',
71
+ })
72
+ return
73
+ }
74
+
75
+ const db = await config.database()
76
+ try {
77
+ const findings = await kyselyDriftCheck(db, entities, { naming: config.naming })
78
+ emit({ ok: true, findings })
79
+ } finally {
80
+ if (db !== null && typeof db.destroy === 'function') await db.destroy()
81
+ }
82
+ }
83
+
84
+ main().catch((err) => {
85
+ emit({ ok: false, error: err instanceof Error ? err.message : String(err) })
86
+ })
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * db-migrate-runner — subprocess via `tsx` que aplica o SCHEMA IDEMPOTENTE.
4
+ *
5
+ * O padrão da casa não é migration versionada: é UM script SQL evolutivo
6
+ * (config.schema, ex. `src/db/schema.sql`) re-rodável — CREATE IF NOT EXISTS +
7
+ * guards `DO $$ IF EXISTS` cobrem nascer do zero E upgrade de prod no mesmo
8
+ * artefato. Depois de aplicar, roda o drift-check (entidade ↔ banco) na mesma
9
+ * conexão: migrar e continuar divergente é estado quebrado que o deploy precisa
10
+ * ver na hora. (A era Kysely Migrator foi aposentada — o formato .ts de
11
+ * migration gerava DDL camelCase errado e ninguém o usava.)
12
+ *
13
+ * argv: [config path]
14
+ * Saída: última linha JSON `{ ok, schema?, findings?, error? }` (resto é debug).
15
+ */
16
+
17
+ import path from 'node:path'
18
+ import { promises as fs } from 'node:fs'
19
+ import { pathToFileURL } from 'node:url'
20
+ import { sql } from 'kysely'
21
+
22
+ import { isEntityConfig } from '../../src/schema/entity.ts'
23
+ import { kyselyDriftCheck } from '../../src/data/drivers/kysely.ts'
24
+
25
+ function emit(obj) {
26
+ process.stdout.write(`\n${JSON.stringify(obj)}\n`)
27
+ }
28
+
29
+ function collectEntities(domain, out) {
30
+ if (domain === null || typeof domain !== 'object') return
31
+ const models = domain.models
32
+ if (models !== undefined && models !== null && typeof models === 'object') {
33
+ for (const value of Object.values(models)) if (isEntityConfig(value)) out.push(value)
34
+ }
35
+ if (Array.isArray(domain.subdomains)) for (const sub of domain.subdomains) collectEntities(sub, out)
36
+ }
37
+
38
+ async function main() {
39
+ const configPath = process.argv[2]
40
+ if (typeof configPath !== 'string') {
41
+ emit({ ok: false, error: 'config path ausente' })
42
+ return
43
+ }
44
+
45
+ const mod = await import(pathToFileURL(path.resolve(configPath)).href)
46
+ const config = mod.default ?? mod
47
+
48
+ if (typeof config.database !== 'function') {
49
+ emit({ ok: false, error: 'config.database() ausente — exporte uma factory que retorna um Kysely' })
50
+ return
51
+ }
52
+
53
+ const configDir = path.dirname(path.resolve(configPath))
54
+ const schemaPath = path.resolve(configDir, config.schema ?? 'db/schema.sql')
55
+ let schemaSql
56
+ try {
57
+ schemaSql = await fs.readFile(schemaPath, 'utf8')
58
+ } catch {
59
+ emit({
60
+ ok: false,
61
+ error: `schema não encontrado em ${schemaPath} — aponte via \`schema\` no opus.config.ts (script SQL idempotente).`,
62
+ })
63
+ return
64
+ }
65
+
66
+ // Entidades pro drift-check pós-apply (explícitas ou via domains).
67
+ const entities = []
68
+ if (Array.isArray(config.entities)) for (const e of config.entities) if (isEntityConfig(e)) entities.push(e)
69
+ if (Array.isArray(config.domains)) for (const d of config.domains) collectEntities(d, entities)
70
+
71
+ const db = await config.database()
72
+ try {
73
+ // Um shot, sem parâmetros → protocolo simples do pg aceita multi-statement,
74
+ // igual ao pool.query(SQL) que este runner substitui.
75
+ await sql.raw(schemaSql).execute(db)
76
+ const findings = entities.length > 0 ? await kyselyDriftCheck(db, entities) : []
77
+ emit({
78
+ ok: true,
79
+ schema: schemaPath,
80
+ findings: findings.map((f) => ({ table: f.table, kind: f.kind, message: f.message })),
81
+ })
82
+ } finally {
83
+ if (db !== null && typeof db.destroy === 'function') await db.destroy()
84
+ }
85
+ }
86
+
87
+ main().catch((err) => {
88
+ emit({ ok: false, error: err instanceof Error ? err.message : String(err) })
89
+ })
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * db-scaffold-runner — subprocess via `tsx` que gera um rascunho de migration.
4
+ *
5
+ * Roda o drift-check (precisa de banco) → gera o corpo da migration (puro) →
6
+ * escreve `migrations/<timestamp>_scaffold.ts`. Best-effort; humano revisa.
7
+ *
8
+ * argv: [config path]
9
+ * Saída: última linha JSON `{ ok, file?, statements?, notes?, error? }`.
10
+ */
11
+
12
+ import path from 'node:path'
13
+ import { promises as fs } from 'node:fs'
14
+ import { pathToFileURL } from 'node:url'
15
+
16
+ import { isEntityConfig } from '../../src/schema/entity.ts'
17
+ import { scaffoldMigration, scaffoldFileContent } from '../../src/schema/scaffold.ts'
18
+ import { kyselyDriftCheck } from '../../src/data/drivers/kysely.ts'
19
+
20
+ function emit(obj) {
21
+ process.stdout.write(`\n${JSON.stringify(obj)}\n`)
22
+ }
23
+
24
+ function collectEntities(domain, out) {
25
+ if (domain === null || typeof domain !== 'object') return
26
+ const models = domain.models
27
+ if (models !== undefined && models !== null && typeof models === 'object') {
28
+ for (const value of Object.values(models)) if (isEntityConfig(value)) out.push(value)
29
+ }
30
+ if (Array.isArray(domain.subdomains)) for (const sub of domain.subdomains) collectEntities(sub, out)
31
+ }
32
+
33
+ function timestamp() {
34
+ return new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14)
35
+ }
36
+
37
+ async function main() {
38
+ const configPath = process.argv[2]
39
+ if (typeof configPath !== 'string') {
40
+ emit({ ok: false, error: 'config path ausente' })
41
+ return
42
+ }
43
+
44
+ const mod = await import(pathToFileURL(path.resolve(configPath)).href)
45
+ const config = mod.default ?? mod
46
+
47
+ const entities = []
48
+ if (Array.isArray(config.entities)) for (const e of config.entities) if (isEntityConfig(e)) entities.push(e)
49
+ if (Array.isArray(config.domains)) for (const d of config.domains) collectEntities(d, entities)
50
+ if (entities.length === 0) {
51
+ emit({ ok: false, error: 'nenhuma entidade encontrada (config.entities ou domain.models)' })
52
+ return
53
+ }
54
+ if (typeof config.database !== 'function') {
55
+ emit({ ok: false, error: 'config.database() ausente — exporte uma factory que retorna um Kysely' })
56
+ return
57
+ }
58
+
59
+ const db = await config.database()
60
+ let result
61
+ try {
62
+ const findings = await kyselyDriftCheck(db, entities)
63
+ result = scaffoldMigration(entities, findings)
64
+ } finally {
65
+ if (db !== null && typeof db.destroy === 'function') await db.destroy()
66
+ }
67
+
68
+ if (result === null) {
69
+ emit({ ok: true, file: null })
70
+ return
71
+ }
72
+
73
+ const configDir = path.dirname(path.resolve(configPath))
74
+ const migrationFolder = path.resolve(configDir, config.migrations ?? 'migrations')
75
+ await fs.mkdir(migrationFolder, { recursive: true })
76
+ const file = path.join(migrationFolder, `${timestamp()}_scaffold.ts`)
77
+ await fs.writeFile(file, scaffoldFileContent(result), 'utf8')
78
+
79
+ emit({ ok: true, file, statements: result.statements, notes: result.notes })
80
+ }
81
+
82
+ main().catch((err) => {
83
+ emit({ ok: false, error: err instanceof Error ? err.message : String(err) })
84
+ })
package/bin/lib/db.mjs ADDED
@@ -0,0 +1,261 @@
1
+ /**
2
+ * db — grupo de comandos de banco (`opus db <verbo>`).
3
+ *
4
+ * `db check` (drift-check entidade ↔ banco), `db migrate` (aplica o SCHEMA
5
+ * IDEMPOTENTE — o padrão da casa: um script SQL evolutivo re-rodável, não
6
+ * migration versionada — e cobra o drift na sequência) e `db scaffold`
7
+ * (rascunho-referência a partir do diff). Diferente do `opus check` (estático,
8
+ * source-only), os comandos `db` carregam o `opus.config.ts` e abrem conexão —
9
+ * por isso rodam via `tsx` num runner isolado (mesma infra do `gen`).
10
+ * Ver `docs/data-layer.md`.
11
+ */
12
+
13
+ import { promises as fs } from 'node:fs'
14
+ import path from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+ import { execFile } from 'node:child_process'
17
+ import { promisify } from 'node:util'
18
+
19
+ const execFileAsync = promisify(execFile)
20
+
21
+ const __filename = fileURLToPath(import.meta.url)
22
+ const __dirname = path.dirname(__filename)
23
+ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..')
24
+
25
+ const COLORS = {
26
+ info: '\x1b[36m',
27
+ success: '\x1b[32m',
28
+ error: '\x1b[31m',
29
+ warn: '\x1b[33m',
30
+ dim: '\x1b[2m',
31
+ }
32
+ const RESET = '\x1b[0m'
33
+
34
+ function log(level, msg) {
35
+ console.log(`${COLORS[level] ?? ''}${msg}${RESET}`)
36
+ }
37
+
38
+ // =============================================================================
39
+ // Dispatch
40
+ // =============================================================================
41
+
42
+ export async function cmdDb(rest, flags) {
43
+ const [sub] = rest
44
+ if (flags.help || sub === undefined || sub === 'help') {
45
+ helpDb()
46
+ return
47
+ }
48
+ if (sub === 'check') {
49
+ await cmdDbCheck(flags)
50
+ return
51
+ }
52
+ if (sub === 'migrate') {
53
+ await cmdDbMigrate(rest[1], flags)
54
+ return
55
+ }
56
+ if (sub === 'scaffold') {
57
+ await cmdDbScaffold(flags)
58
+ return
59
+ }
60
+ log('error', `Subcomando db desconhecido: ${sub}`)
61
+ helpDb()
62
+ process.exit(1)
63
+ }
64
+
65
+ // =============================================================================
66
+ // Config
67
+ // =============================================================================
68
+
69
+ async function resolveConfig(flags) {
70
+ const cwd = process.cwd()
71
+ const configRel = flags.config ?? 'opus.config.ts'
72
+ const configPath = path.isAbsolute(configRel) ? configRel : path.resolve(cwd, configRel)
73
+ if (!(await fileExists(configPath))) {
74
+ log('error', `opus.config.ts não encontrado em ${configPath}`)
75
+ log('dim', ' Passa o path via --config <path> ou cria um na raiz do projeto.')
76
+ process.exit(1)
77
+ }
78
+ return { cwd, configPath }
79
+ }
80
+
81
+ // =============================================================================
82
+ // db check
83
+ // =============================================================================
84
+
85
+ async function cmdDbCheck(flags) {
86
+ const { cwd, configPath } = await resolveConfig(flags)
87
+ log('info', `→ drift-check via ${path.relative(cwd, configPath)}...`)
88
+
89
+ const result = await runRunner('db-check-runner.mjs', configPath)
90
+ if (result.ok !== true) {
91
+ log('error', `Falha no drift-check: ${result.error}`)
92
+ process.exit(1)
93
+ }
94
+
95
+ const findings = result.findings ?? []
96
+ if (findings.length === 0) {
97
+ log('success', '✓ db check: schema do banco em dia com as entidades.')
98
+ return
99
+ }
100
+
101
+ const byTable = new Map()
102
+ for (const f of findings) {
103
+ if (!byTable.has(f.table)) byTable.set(f.table, [])
104
+ byTable.get(f.table).push(f)
105
+ }
106
+ for (const [table, list] of byTable) {
107
+ log('warn', `\n${table}`)
108
+ for (const f of list) {
109
+ console.log(` [${f.kind}] ${f.message}`)
110
+ }
111
+ }
112
+ log('error', `\n✗ db check: ${findings.length} divergência(s) em ${byTable.size} tabela(s).`)
113
+ process.exit(1)
114
+ }
115
+
116
+ // =============================================================================
117
+ // db migrate — aplica o schema idempotente + drift-check na sequência
118
+ // =============================================================================
119
+
120
+ async function cmdDbMigrate(sub2, flags) {
121
+ if (sub2 === 'down') {
122
+ // O padrão da casa é schema EVOLUTIVO (um script idempotente), não migration
123
+ // versionada — não existe "voltar uma": rollback = editar o script e re-rodar
124
+ // (catástrofe = snapshot pré-deploy). A era Kysely Migrator foi aposentada.
125
+ log('error', 'db migrate down foi aposentado: o schema é um script idempotente, sem histórico a reverter.')
126
+ log('dim', ' Rollback = editar o schema e re-rodar `opus db migrate`; catástrofe = snapshot pré-deploy.')
127
+ process.exit(1)
128
+ }
129
+ const { cwd, configPath } = await resolveConfig(flags)
130
+ log('info', `→ db migrate via ${path.relative(cwd, configPath)}...`)
131
+
132
+ const result = await runRunner('db-migrate-runner.mjs', configPath)
133
+ if (result.ok !== true) {
134
+ log('error', `✗ db migrate: ${result.error}`)
135
+ process.exit(1)
136
+ }
137
+ log('success', `✓ schema aplicado (${path.relative(cwd, result.schema)}).`)
138
+
139
+ // Drift-check embutido: migrou mas diverge das entidades = o schema ficou pra trás
140
+ // da declaração — estado quebrado que o deploy precisa ver AQUI, não depois.
141
+ const findings = result.findings ?? []
142
+ if (findings.length === 0) {
143
+ log('success', '✓ db migrate: banco em dia com as entidades.')
144
+ return
145
+ }
146
+ const byTable = new Map()
147
+ for (const f of findings) {
148
+ if (!byTable.has(f.table)) byTable.set(f.table, [])
149
+ byTable.get(f.table).push(f)
150
+ }
151
+ for (const [table, list] of byTable) {
152
+ log('warn', `\n${table}`)
153
+ for (const f of list) console.log(` [${f.kind}] ${f.message}`)
154
+ }
155
+ log('error', `\n✗ db migrate: schema aplicado, mas ${findings.length} divergência(s) com as entidades — atualize o script.`)
156
+ process.exit(1)
157
+ }
158
+
159
+ // =============================================================================
160
+ // db scaffold
161
+ // =============================================================================
162
+
163
+ async function cmdDbScaffold(flags) {
164
+ const { cwd, configPath } = await resolveConfig(flags)
165
+ log('info', `→ db scaffold via ${path.relative(cwd, configPath)}...`)
166
+
167
+ const result = await runRunner('db-scaffold-runner.mjs', configPath)
168
+ if (result.ok !== true) {
169
+ log('error', `Falha no scaffold: ${result.error}`)
170
+ process.exit(1)
171
+ }
172
+ if (result.file === null || result.file === undefined) {
173
+ log('success', '✓ db scaffold: nada a gerar (schema em dia).')
174
+ return
175
+ }
176
+ log('success', `✓ db scaffold: ${path.relative(cwd, result.file)} (${result.statements} statement(s)).`)
177
+ const notes = result.notes ?? []
178
+ if (notes.length > 0) {
179
+ log('warn', `\n ${notes.length} ponto(s) pra revisar à mão (gerados como // TODO):`)
180
+ for (const n of notes) console.log(` ! ${n}`)
181
+ }
182
+ log('dim', '\n Rascunho — revise antes de `opus db migrate`.')
183
+ }
184
+
185
+ // =============================================================================
186
+ // Runner spawn (via tsx)
187
+ // =============================================================================
188
+
189
+ async function runRunner(runnerFile, configPath, extraArgs = []) {
190
+ const tsxBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', 'tsx')
191
+ const hasLocalTsx = await fileExists(tsxBin)
192
+ const runnerPath = path.join(__dirname, runnerFile)
193
+ const cmd = hasLocalTsx ? tsxBin : 'tsx'
194
+
195
+ try {
196
+ const { stdout } = await execFileAsync(cmd, [runnerPath, configPath, ...extraArgs], {
197
+ encoding: 'utf8',
198
+ maxBuffer: 16 * 1024 * 1024,
199
+ env: { ...process.env },
200
+ })
201
+ const line = lastJsonLine(stdout)
202
+ if (line === null) return { ok: false, error: 'runner não emitiu JSON' }
203
+ return JSON.parse(line)
204
+ } catch (err) {
205
+ const detail =
206
+ typeof err.stderr === 'string' && err.stderr.trim().length > 0
207
+ ? err.stderr.trim()
208
+ : err.message
209
+ return { ok: false, error: detail }
210
+ }
211
+ }
212
+
213
+ function lastJsonLine(stdout) {
214
+ const lines = stdout.split('\n').filter((l) => l.trim().length > 0)
215
+ for (let i = lines.length - 1; i >= 0; i--) {
216
+ const trimmed = lines[i].trim()
217
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) return trimmed
218
+ }
219
+ return null
220
+ }
221
+
222
+ async function fileExists(p) {
223
+ try {
224
+ await fs.access(p)
225
+ return true
226
+ } catch {
227
+ return false
228
+ }
229
+ }
230
+
231
+ // =============================================================================
232
+ // Help
233
+ // =============================================================================
234
+
235
+ export function helpDb() {
236
+ console.log(`
237
+ @softize/opus db — comandos de banco
238
+
239
+ db check Compara o schema do banco com as entidades (defineEntity) — drift-check.
240
+ Read-only. Exit ≠ 0 se divergir. Gate de CI/deploy.
241
+ db migrate Aplica o SCHEMA IDEMPOTENTE (config \`schema\`, script SQL evolutivo:
242
+ IF NOT EXISTS + guards — nasce do zero E faz upgrade no mesmo artefato,
243
+ re-rodável) e roda o drift-check na sequência. Exit ≠ 0 se divergir.
244
+ db scaffold Gera um rascunho kysely a partir do diff — REFERÊNCIA pra escrever o
245
+ SQL no schema (a verdade é o script; revise à mão).
246
+
247
+ Flags:
248
+ --config <path> Caminho do opus.config.ts. Default: ./opus.config.ts
249
+
250
+ O opus.config.ts precisa expor, pros comandos db:
251
+ database: () => Kysely factory LAZY do banco (gen não a chama)
252
+ entities | domains entidades (explícitas ou via domain.models)
253
+ schema?: string script SQL idempotente (default: ./db/schema.sql)
254
+ migrations?: string pasta dos rascunhos do scaffold (default: ./migrations)
255
+
256
+ Exemplos:
257
+ npx @softize/opus db check
258
+ npx @softize/opus db migrate
259
+ npx @softize/opus db migrate down --config ./apps/api/opus.config.ts
260
+ `)
261
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * docs-include — transclusão de páginas da doc pra dentro de SKILLS na materialização.
3
+ *
4
+ * O modelo da casa: a DOC é a fonte do conhecimento (humano lê no site; viaja no
5
+ * pacote); a SKILL é o gatilho do agente. Ponteiro não garante consulta — transclusão
6
+ * garante: a skill declara `<!-- opus-doc: <página>.md -->` e quem MATERIALIZA (o
7
+ * prepare do admin; o caminho vivo do Maestro) expande o conteúdo da página pra dentro
8
+ * do corpo, que o mecanismo de skill carrega no contexto. Edita-se a página, nunca a
9
+ * cópia — fonte única, entrega derivada (o mesmo padrão do manifest).
10
+ *
11
+ * Página inexistente = ERRO (deploy para — skill apontando doc morta é defeito, igual
12
+ * ao gate de frontmatter). A expansão tira o frontmatter e o H1 da página (a skill já
13
+ * titula a própria seção).
14
+ */
15
+
16
+ import { readFileSync } from 'node:fs'
17
+ import path from 'node:path'
18
+ import { fileURLToPath } from 'node:url'
19
+
20
+ const CONTENT_DIR = path.resolve(
21
+ path.dirname(fileURLToPath(import.meta.url)),
22
+ '..',
23
+ '..',
24
+ 'src',
25
+ 'ui',
26
+ 'docs',
27
+ 'content',
28
+ )
29
+
30
+ const MARKER_RE = /^[ \t]*<!--\s*opus-doc:\s*([\w./-]+)\s*-->[ \t]*$/gm
31
+
32
+ /** Expande os markers `<!-- opus-doc: x.md -->` de um corpo de skill. */
33
+ export function expandDocIncludes(md) {
34
+ return md.replace(MARKER_RE, (_marker, file) => {
35
+ const name = path.basename(String(file)) // só o nome — sem traversal.
36
+ const full = path.join(CONTENT_DIR, name)
37
+ let raw
38
+ try {
39
+ raw = readFileSync(full, 'utf8')
40
+ } catch {
41
+ throw new Error(`opus-doc include: a página "${name}" não existe em ${CONTENT_DIR} — corrija a skill.`)
42
+ }
43
+ return raw
44
+ .replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '')
45
+ .replace(/^\s*# .*\r?\n/, '')
46
+ .trim()
47
+ })
48
+ }