@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,311 @@
1
+ /**
2
+ * gen — orquestra o comando `opus gen`.
3
+ *
4
+ * Fluxo:
5
+ * 1. Resolve config path (--config ou ./opus.config.ts).
6
+ * 2. Spawn `tsx <package>/bin/lib/gen-runner.mjs <configPath>` capturando
7
+ * stdout. Runner emite linha JSON com payload serializado.
8
+ * 3. Constrói manifest, openapi, docs, client-stubs.
9
+ * 4. Escreve em <output>/manifest.json, openapi.json, docs/*.md,
10
+ * client-stubs/*.ts.
11
+ * 5. Poda órfãos: .md/.ts que rodada anterior gerou e esta não gerou saem
12
+ * do disco (docs/ é a superfície de spec que o AGENTE lê — doc órfã
13
+ * anuncia endpoint que já dá 404).
14
+ *
15
+ * Defaults:
16
+ * - config: ./opus.config.ts (no cwd)
17
+ * - output: o que o config disser, ou ./.gen
18
+ *
19
+ * Flag `--force`: sobrescreve arquivos existentes (default sobrescreve
20
+ * silenciosamente — opus gen é idempotente e o output mora num dir
21
+ * dedicado, então não há valor real em bloquear sem --force. Mantemos a
22
+ * flag pra paridade com `add`).
23
+ */
24
+
25
+ import { promises as fs } from 'node:fs'
26
+ import path from 'node:path'
27
+ import { fileURLToPath } from 'node:url'
28
+ import { execFile } from 'node:child_process'
29
+ import { promisify } from 'node:util'
30
+
31
+ import { buildManifest } from './gen-manifest.mjs'
32
+ import { buildOpenAPI } from './gen-openapi.mjs'
33
+ import { buildDocs } from './gen-docs.mjs'
34
+ import { buildClientStubs } from './gen-stubs.mjs'
35
+ import { buildDictStubs } from './gen-dicts.mjs'
36
+
37
+ const execFileAsync = promisify(execFile)
38
+
39
+ const __filename = fileURLToPath(import.meta.url)
40
+ const __dirname = path.dirname(__filename)
41
+ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..')
42
+ const RUNNER_PATH = path.join(__dirname, 'gen-runner.mjs')
43
+
44
+ const COLORS = {
45
+ info: '\x1b[36m',
46
+ success: '\x1b[32m',
47
+ error: '\x1b[31m',
48
+ warn: '\x1b[33m',
49
+ dim: '\x1b[2m',
50
+ }
51
+ const RESET = '\x1b[0m'
52
+
53
+ function log(level, msg) {
54
+ console.log(`${COLORS[level] ?? ''}${msg}${RESET}`)
55
+ }
56
+
57
+ // =============================================================================
58
+ // Comando
59
+ // =============================================================================
60
+
61
+ export async function cmdGen(flags) {
62
+ const cwd = process.cwd()
63
+ const configRel = flags.config ?? 'opus.config.ts'
64
+ const configPath = path.isAbsolute(configRel)
65
+ ? configRel
66
+ : path.resolve(cwd, configRel)
67
+
68
+ if (!(await fileExists(configPath))) {
69
+ log('error', `opus.config.ts não encontrado em ${configPath}`)
70
+ log(
71
+ 'dim',
72
+ ' Cria um arquivo na raiz do projeto exportando `{ domains: [...] }` como default,',
73
+ )
74
+ log('dim', ' ou passa o path via --config <path>.')
75
+ process.exit(1)
76
+ }
77
+
78
+ log('info', `→ Carregando ${path.relative(cwd, configPath)}...`)
79
+
80
+ const payload = await loadConfig(configPath)
81
+
82
+ // Output: flag > config > default.
83
+ const outputDir = flags.output ?? payload.output ?? '.gen'
84
+ const outputAbs = path.isAbsolute(outputDir)
85
+ ? outputDir
86
+ : path.resolve(cwd, outputDir)
87
+
88
+ const manifest = buildManifest(payload)
89
+ const openapi = buildOpenAPI(manifest)
90
+ const docs = buildDocs(manifest)
91
+ const stubs = buildClientStubs(manifest)
92
+ const dictStubs = buildDictStubs(manifest)
93
+
94
+ await fs.mkdir(outputAbs, { recursive: true })
95
+ await fs.mkdir(path.join(outputAbs, 'docs'), { recursive: true })
96
+ await fs.mkdir(path.join(outputAbs, 'client-stubs'), { recursive: true })
97
+ if (dictStubs.length > 0) {
98
+ await fs.mkdir(path.join(outputAbs, 'client-stubs', 'dicts'), {
99
+ recursive: true,
100
+ })
101
+ }
102
+
103
+ await writeJson(path.join(outputAbs, 'manifest.json'), manifest, flags.force)
104
+ await writeJson(path.join(outputAbs, 'openapi.json'), openapi, flags.force)
105
+
106
+ for (const doc of docs) {
107
+ await writeText(path.join(outputAbs, 'docs', doc.filename), doc.content, flags.force)
108
+ }
109
+ for (const stub of stubs) {
110
+ await writeText(
111
+ path.join(outputAbs, 'client-stubs', stub.filename),
112
+ stub.content,
113
+ flags.force,
114
+ )
115
+ }
116
+ for (const dict of dictStubs) {
117
+ await writeText(
118
+ path.join(outputAbs, 'client-stubs', 'dicts', dict.filename),
119
+ dict.content,
120
+ flags.force,
121
+ )
122
+ }
123
+
124
+ const pruned = [
125
+ ...(await pruneOrphans(path.join(outputAbs, 'docs'), new Set(docs.map((d) => d.filename)), ['.md'])).map(
126
+ (f) => `docs/${f}`,
127
+ ),
128
+ ...(await pruneOrphans(path.join(outputAbs, 'client-stubs'), new Set(stubs.map((s) => s.filename)), ['.ts'])).map(
129
+ (f) => `client-stubs/${f}`,
130
+ ),
131
+ ...(
132
+ await pruneOrphans(path.join(outputAbs, 'client-stubs', 'dicts'), new Set(dictStubs.map((d) => d.filename)), ['.ts'])
133
+ ).map((f) => `client-stubs/dicts/${f}`),
134
+ ]
135
+
136
+ log('success', `✓ Gerado em ${path.relative(cwd, outputAbs)}`)
137
+ log('dim', ` manifest.json`)
138
+ log('dim', ` openapi.json`)
139
+ log('dim', ` docs/ (${docs.length} arquivo${docs.length === 1 ? '' : 's'})`)
140
+ log(
141
+ 'dim',
142
+ ` client-stubs/ (${stubs.length} arquivo${stubs.length === 1 ? '' : 's'})`,
143
+ )
144
+ if (dictStubs.length > 0) {
145
+ log(
146
+ 'dim',
147
+ ` client-stubs/dicts/ (${dictStubs.length} arquivo${dictStubs.length === 1 ? '' : 's'})`,
148
+ )
149
+ }
150
+ if (pruned.length > 0) {
151
+ log('dim', ` podado${pruned.length === 1 ? '' : 's'} ${pruned.length} órfão${pruned.length === 1 ? '' : 's'}: ${pruned.join(', ')}`)
152
+ }
153
+
154
+ // Auditoria de doc: o `description` é a fonte de entendimento (negócio + contrato) e
155
+ // flui pro manifest/lente. O gen vê a descrição RESOLVIDA (concatenada inclusive) —
156
+ // por isso a checagem mora aqui, não no opus check estático. Avisa, não reprova.
157
+ auditDocs(manifest, cwd)
158
+ }
159
+
160
+ /** Lista as actions sem `description` no manifest (o doc de negócio — a fonte). */
161
+ function auditDocs(manifest, cwd) {
162
+ const missing = []
163
+ const walk = (d) => {
164
+ for (const a of d.actions ?? []) {
165
+ const doc = typeof a.description === 'string' ? a.description.trim() : ''
166
+ if (doc.length === 0) missing.push(a.name)
167
+ }
168
+ for (const sub of d.subdomains ?? []) walk(sub)
169
+ }
170
+ for (const d of manifest.domains ?? []) walk(d)
171
+ if (missing.length === 0) {
172
+ log('success', ' ✓ doc: todas as actions documentadas (spec abrange tudo).')
173
+ return
174
+ }
175
+ log('warn', `\n ⚠ doc: ${missing.length} action(s) sem \`description\` (o doc de negócio — a fonte):`)
176
+ for (const name of missing) log('dim', ` • ${name}`)
177
+ }
178
+
179
+ // =============================================================================
180
+ // Runner spawn
181
+ // =============================================================================
182
+
183
+ async function loadConfig(configPath) {
184
+ // tsx mora em <package>/node_modules/.bin/tsx — usamos absoluto pra
185
+ // evitar depender de PATH do consumer.
186
+ const tsxBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', 'tsx')
187
+ const hasLocalTsx = await fileExists(tsxBin)
188
+
189
+ // Args do runner: <runner.mjs> <configPath>. tsx aceita .mjs também
190
+ // (passthrough) e nos permite resolver TS na cadeia de imports.
191
+ const args = [RUNNER_PATH, configPath]
192
+ const cmd = hasLocalTsx ? tsxBin : 'tsx'
193
+
194
+ try {
195
+ // Buffer grande pra acomodar manifests gordos.
196
+ const { stdout, stderr } = await execFileAsync(cmd, args, {
197
+ encoding: 'utf8',
198
+ maxBuffer: 64 * 1024 * 1024,
199
+ env: { ...process.env },
200
+ })
201
+
202
+ // Runner emite a linha JSON na ÚLTIMA linha de stdout. Qualquer log do
203
+ // consumer durante o import vai pro inicio — varremos de trás pra
204
+ // frente.
205
+ const line = lastJsonLine(stdout)
206
+ if (line === null) {
207
+ log('error', 'Runner não emitiu payload JSON válido.')
208
+ if (stderr.length > 0) log('dim', stderr.trim())
209
+ if (stdout.length > 0) log('dim', stdout.trim())
210
+ process.exit(1)
211
+ }
212
+ const parsed = JSON.parse(line)
213
+ if (parsed.ok !== true) {
214
+ log('error', `Falha ao carregar config: ${parsed.error}`)
215
+ process.exit(1)
216
+ }
217
+ return parsed.payload
218
+ } catch (err) {
219
+ log('error', `Falha ao executar tsx: ${err.message}`)
220
+ if (typeof err.stderr === 'string' && err.stderr.length > 0) {
221
+ log('dim', err.stderr.trim())
222
+ }
223
+ if (typeof err.stdout === 'string' && err.stdout.length > 0) {
224
+ log('dim', err.stdout.trim())
225
+ }
226
+ process.exit(1)
227
+ }
228
+ }
229
+
230
+ function lastJsonLine(stdout) {
231
+ const lines = stdout.split('\n').filter((l) => l.trim().length > 0)
232
+ for (let i = lines.length - 1; i >= 0; i--) {
233
+ const trimmed = lines[i].trim()
234
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
235
+ return trimmed
236
+ }
237
+ }
238
+ return null
239
+ }
240
+
241
+ // =============================================================================
242
+ // FS helpers
243
+ // =============================================================================
244
+
245
+ async function fileExists(p) {
246
+ try {
247
+ await fs.access(p)
248
+ return true
249
+ } catch {
250
+ return false
251
+ }
252
+ }
253
+
254
+ async function writeJson(p, value, _force) {
255
+ await fs.writeFile(p, JSON.stringify(value, null, 2) + '\n', 'utf8')
256
+ }
257
+
258
+ /**
259
+ * Poda órfãos de um dir gerenciado pelo gen: arquivo com extensão gerada (`exts`)
260
+ * que NÃO saiu desta rodada (`keep`) é removido. Só olha arquivos do próprio dir
261
+ * (subdirs têm chamada própria) e só as extensões geradas — qualquer outra coisa
262
+ * que o consumer tenha posto lá fica intacta. Exportada pra teste.
263
+ */
264
+ export async function pruneOrphans(dir, keep, exts) {
265
+ let entries
266
+ try {
267
+ entries = await fs.readdir(dir, { withFileTypes: true })
268
+ } catch {
269
+ return [] // dir nem existe → nada a podar
270
+ }
271
+ const removed = []
272
+ for (const e of entries) {
273
+ if (!e.isFile()) continue
274
+ if (!exts.some((x) => e.name.endsWith(x))) continue
275
+ if (keep.has(e.name)) continue
276
+ await fs.unlink(path.join(dir, e.name))
277
+ removed.push(e.name)
278
+ }
279
+ return removed.sort()
280
+ }
281
+
282
+ async function writeText(p, content, _force) {
283
+ await fs.writeFile(p, content, 'utf8')
284
+ }
285
+
286
+ // =============================================================================
287
+ // Help
288
+ // =============================================================================
289
+
290
+ export function helpGen() {
291
+ console.log(`
292
+ @softize/opus gen
293
+
294
+ Lê opus.config.ts do consumer e gera:
295
+ - <output>/manifest.json Spec serializada (JSON Schema dos inputs/outputs)
296
+ - <output>/openapi.json OpenAPI 3.1 derivada do manifest
297
+ - <output>/docs/<domain>.md Doc human-readable por domínio
298
+ - <output>/client-stubs/<domain>.ts Stubs com schema Zod reconstruído pro frontend
299
+ - <output>/client-stubs/dicts/<domain>.ts Dicts (t.dict({...})) prontos pro frontend
300
+
301
+ Flags:
302
+ --config <path> Caminho do opus.config.ts. Default: ./opus.config.ts
303
+ --output <path> Pasta de saída. Default: do config (ou ./.gen)
304
+ --force, -f Sobrescreve existente (atualmente sempre sobrescreve)
305
+
306
+ Exemplos:
307
+ npx @softize/opus gen
308
+ npx @softize/opus gen --config ./apps/api/opus.config.ts
309
+ npx @softize/opus gen --output ./custom/gen
310
+ `)
311
+ }