@softize/opus 12.11.0 → 13.1.0

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 (213) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/PROMOTED.md +46 -0
  3. package/README.md +28 -19
  4. package/bin/cli.mjs +87 -216
  5. package/bin/lib/check.mjs +2 -7
  6. package/bin/lib/cli-shared.mjs +131 -0
  7. package/bin/lib/copy.mjs +1 -5
  8. package/bin/lib/db.mjs +16 -74
  9. package/bin/lib/gen-openapi.mjs +3 -3
  10. package/bin/lib/gen-runner.mjs +1 -1
  11. package/bin/lib/gen.mjs +14 -69
  12. package/bin/lib/mcp.mjs +3 -1
  13. package/bin/lib/seed.mjs +5 -62
  14. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +93 -10
  15. package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
  16. package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
  17. package/docs/ownership-vs-shadcn-lock.md +2 -3
  18. package/docs/protocol.md +7 -7
  19. package/docs/radius-scale.md +1 -1
  20. package/docs/releasing.md +8 -2
  21. package/package.json +7 -3
  22. package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
  23. package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
  24. package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
  25. package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
  26. package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
  27. package/registry/templates/app/package.json +1 -1
  28. package/registry/templates/app/src/main.tsx +4 -4
  29. package/src/audit/drivers/console.ts +1 -0
  30. package/src/auth/drivers/better-auth.ts +1 -0
  31. package/src/auth/drivers/jwt.ts +1 -0
  32. package/src/cache/drivers/memory.ts +1 -0
  33. package/src/client/drivers/fetch.ts +2 -1
  34. package/src/core/actions.ts +6 -1
  35. package/src/core/audit.ts +9 -3
  36. package/src/core/contracts.ts +7 -0
  37. package/src/core/domain.ts +1 -1
  38. package/src/core/errors.ts +18 -15
  39. package/src/core/index.ts +4 -2
  40. package/src/core/package-version.ts +26 -0
  41. package/src/core/reactions.ts +1 -1
  42. package/src/core/runtime.ts +33 -23
  43. package/src/core/schedules.ts +1 -1
  44. package/src/core/types.ts +2 -2
  45. package/src/dsl/eval.ts +2 -2
  46. package/src/dsl/kysely.ts +2 -2
  47. package/src/dsl/loads.ts +1 -1
  48. package/src/dsl/parser.ts +5 -5
  49. package/src/events/drivers/mitt.ts +1 -0
  50. package/src/mcp/index.ts +2 -1
  51. package/src/observability/drivers/opentelemetry.ts +1 -0
  52. package/src/queue/drivers/bullmq.ts +3 -3
  53. package/src/scheduler/drivers/node-cron.ts +3 -2
  54. package/src/scheduler/every.ts +7 -7
  55. package/src/schema/openapi.ts +3 -3
  56. package/src/seed/index.ts +29 -0
  57. package/src/server/drivers/fastify.ts +5 -2
  58. package/src/server/drivers/node.ts +9 -6
  59. package/src/server/index.ts +3 -1
  60. package/src/storage/drivers/fs.ts +1 -0
  61. package/src/testing/index.ts +3 -3
  62. package/src/ui/components/patterns/confirm.tsx +142 -42
  63. package/src/ui/components/patterns/content-header.tsx +7 -1
  64. package/src/ui/components/patterns/data-state.tsx +1 -1
  65. package/src/ui/components/patterns/dock.tsx +20 -3
  66. package/src/ui/components/patterns/form.tsx +12 -8
  67. package/src/ui/components/patterns/list.tsx +36 -41
  68. package/src/ui/components/patterns/page-state.tsx +2 -2
  69. package/src/ui/components/patterns/page.tsx +19 -1
  70. package/src/ui/components/patterns/shell-nav.tsx +10 -3
  71. package/src/ui/components/patterns/sidebar.tsx +43 -32
  72. package/src/ui/components/patterns/trigger.tsx +39 -38
  73. package/src/ui/components/patterns/view.tsx +26 -17
  74. package/src/ui/components/primitives/alert.tsx +14 -8
  75. package/src/ui/components/primitives/ask.tsx +3 -3
  76. package/src/ui/components/primitives/badge.tsx +11 -6
  77. package/src/ui/components/primitives/breadcrumb.tsx +2 -2
  78. package/src/ui/components/primitives/button.tsx +16 -3
  79. package/src/ui/components/primitives/calendar.tsx +28 -2
  80. package/src/ui/components/primitives/carousel.tsx +3 -3
  81. package/src/ui/components/primitives/chat.tsx +1 -1
  82. package/src/ui/components/primitives/checkbox.tsx +1 -1
  83. package/src/ui/components/primitives/command.tsx +2 -2
  84. package/src/ui/components/primitives/control.ts +12 -0
  85. package/src/ui/components/primitives/copyable.tsx +1 -1
  86. package/src/ui/components/primitives/dialog.tsx +202 -40
  87. package/src/ui/components/primitives/dot.tsx +5 -0
  88. package/src/ui/components/primitives/drawer.tsx +18 -8
  89. package/src/ui/components/primitives/empty.tsx +3 -3
  90. package/src/ui/components/primitives/field.tsx +3 -3
  91. package/src/ui/components/primitives/icon-picker.tsx +3 -1
  92. package/src/ui/components/primitives/input-group.tsx +1 -1
  93. package/src/ui/components/primitives/input-otp.tsx +1 -1
  94. package/src/ui/components/primitives/input.tsx +2 -2
  95. package/src/ui/components/primitives/item.tsx +3 -3
  96. package/src/ui/components/primitives/progress.tsx +32 -3
  97. package/src/ui/components/primitives/radio-group.tsx +1 -1
  98. package/src/ui/components/primitives/resizable.tsx +3 -1
  99. package/src/ui/components/primitives/select.tsx +5 -5
  100. package/src/ui/components/primitives/slider.tsx +5 -1
  101. package/src/ui/components/primitives/sonner.tsx +190 -8
  102. package/src/ui/components/primitives/switch.tsx +1 -0
  103. package/src/ui/components/primitives/tabs.tsx +1 -0
  104. package/src/ui/components/primitives/textarea.tsx +1 -1
  105. package/src/ui/components/primitives/toggle.tsx +1 -1
  106. package/src/ui/components/primitives/tooltip.tsx +1 -0
  107. package/src/ui/docs/DocBrowser.tsx +102 -23
  108. package/src/ui/docs/changelog.tsx +1 -1
  109. package/src/ui/docs/content/accordion.md +22 -16
  110. package/src/ui/docs/content/action-form-card.md +8 -8
  111. package/src/ui/docs/content/action-form-dialog.md +9 -9
  112. package/src/ui/docs/content/action-form.md +37 -36
  113. package/src/ui/docs/content/action-list-dialog.md +11 -6
  114. package/src/ui/docs/content/action-list.md +73 -39
  115. package/src/ui/docs/content/action-trigger.md +29 -15
  116. package/src/ui/docs/content/action-view.md +17 -10
  117. package/src/ui/docs/content/actions.md +9 -9
  118. package/src/ui/docs/content/ai.md +3 -3
  119. package/src/ui/docs/content/alert.md +14 -12
  120. package/src/ui/docs/content/ask.md +11 -0
  121. package/src/ui/docs/content/aspect-ratio.md +4 -4
  122. package/src/ui/docs/content/audit.md +2 -2
  123. package/src/ui/docs/content/auth.md +3 -3
  124. package/src/ui/docs/content/avatar.md +34 -14
  125. package/src/ui/docs/content/badge.md +3 -3
  126. package/src/ui/docs/content/breadcrumb.md +13 -8
  127. package/src/ui/docs/content/button.md +81 -6
  128. package/src/ui/docs/content/calendar.md +18 -5
  129. package/src/ui/docs/content/card.md +27 -1
  130. package/src/ui/docs/content/carousel.md +16 -11
  131. package/src/ui/docs/content/chat.md +23 -3
  132. package/src/ui/docs/content/checkbox.md +7 -7
  133. package/src/ui/docs/content/cli.md +74 -22
  134. package/src/ui/docs/content/collapsible.md +8 -8
  135. package/src/ui/docs/content/command.md +16 -8
  136. package/src/ui/docs/content/composer.md +17 -2
  137. package/src/ui/docs/content/content.md +17 -2
  138. package/src/ui/docs/content/copyable.md +12 -3
  139. package/src/ui/docs/content/customization.md +5 -5
  140. package/src/ui/docs/content/cycle.md +3 -3
  141. package/src/ui/docs/content/data-state.md +11 -12
  142. package/src/ui/docs/content/data.md +26 -33
  143. package/src/ui/docs/content/detail.md +22 -4
  144. package/src/ui/docs/content/dialog.md +339 -31
  145. package/src/ui/docs/content/dictionary-value.md +17 -10
  146. package/src/ui/docs/content/dock.md +11 -3
  147. package/src/ui/docs/content/dot.md +8 -0
  148. package/src/ui/docs/content/drawer.md +27 -14
  149. package/src/ui/docs/content/empty-value.md +2 -2
  150. package/src/ui/docs/content/empty.md +19 -12
  151. package/src/ui/docs/content/events.md +4 -4
  152. package/src/ui/docs/content/field.md +34 -12
  153. package/src/ui/docs/content/getting-started.md +3 -3
  154. package/src/ui/docs/content/icon-picker.md +19 -4
  155. package/src/ui/docs/content/input-otp.md +20 -12
  156. package/src/ui/docs/content/input.md +121 -9
  157. package/src/ui/docs/content/item.md +27 -13
  158. package/src/ui/docs/content/kbd.md +19 -11
  159. package/src/ui/docs/content/label.md +12 -3
  160. package/src/ui/docs/content/log.md +4 -4
  161. package/src/ui/docs/content/markdown.md +7 -6
  162. package/src/ui/docs/content/mcp.md +13 -15
  163. package/src/ui/docs/content/menu.md +40 -16
  164. package/src/ui/docs/content/metric-card.md +13 -0
  165. package/src/ui/docs/content/observability.md +2 -2
  166. package/src/ui/docs/content/page.md +59 -6
  167. package/src/ui/docs/content/pagination.md +22 -17
  168. package/src/ui/docs/content/popover.md +22 -8
  169. package/src/ui/docs/content/progress.md +15 -16
  170. package/src/ui/docs/content/queue.md +5 -5
  171. package/src/ui/docs/content/radio-group.md +20 -12
  172. package/src/ui/docs/content/router.md +11 -6
  173. package/src/ui/docs/content/scheduler.md +4 -5
  174. package/src/ui/docs/content/scroll-area.md +12 -7
  175. package/src/ui/docs/content/select.md +47 -34
  176. package/src/ui/docs/content/semantic-context.md +2 -2
  177. package/src/ui/docs/content/separator.md +5 -5
  178. package/src/ui/docs/content/sidebar.md +329 -54
  179. package/src/ui/docs/content/skeleton.md +9 -2
  180. package/src/ui/docs/content/slider.md +8 -7
  181. package/src/ui/docs/content/spinner.md +8 -8
  182. package/src/ui/docs/content/split.md +29 -5
  183. package/src/ui/docs/content/storage.md +6 -8
  184. package/src/ui/docs/content/switch.md +8 -7
  185. package/src/ui/docs/content/table.md +13 -3
  186. package/src/ui/docs/content/tabs.md +28 -14
  187. package/src/ui/docs/content/testing.md +9 -11
  188. package/src/ui/docs/content/textarea.md +12 -4
  189. package/src/ui/docs/content/toast.md +47 -13
  190. package/src/ui/docs/content/toggle.md +75 -7
  191. package/src/ui/docs/content/tokens.md +7 -7
  192. package/src/ui/docs/content/tooltip.md +19 -11
  193. package/src/ui/docs/content/truncate.md +15 -8
  194. package/src/ui/docs/content/ui.md +24 -9
  195. package/src/ui/docs/content/upgrading.md +7 -8
  196. package/src/ui/docs/doc-client.tsx +5 -5
  197. package/src/ui/docs/doc.tsx +26 -14
  198. package/src/ui/docs/registry.tsx +25 -42
  199. package/src/ui/docs/standalone.tsx +2 -2
  200. package/src/ui/drivers/react.tsx +17 -12
  201. package/src/ui/lib/action-errors.ts +45 -0
  202. package/src/ui/lib/zod-pt-br.ts +31 -4
  203. package/src/ui/meta.ts +65 -95
  204. package/src/ui/react.tsx +17 -16
  205. package/src/ui/theme.css +60 -8
  206. package/src/vite/design.ts +6 -18
  207. package/src/ui/components/primitives/alert-dialog.tsx +0 -192
  208. package/src/ui/docs/content/alert-dialog.md +0 -73
  209. package/src/ui/docs/content/button-group.md +0 -71
  210. package/src/ui/docs/content/confirm.md +0 -120
  211. package/src/ui/docs/content/input-group.md +0 -79
  212. package/src/ui/docs/content/page-state.md +0 -45
  213. package/src/ui/docs/content/toggle-group.md +0 -81
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Utilitários compartilhados pelos comandos do CLI `opus`.
3
+ *
4
+ * Concentra o que mais de um comando repetia: log colorido no terminal, teste de
5
+ * existência de arquivo, resolução do `opus.config.ts` do consumer e o spawn do `tsx`
6
+ * que carrega esse config TypeScript num runner isolado (`gen`, `db` e `seed` usam a
7
+ * mesma infra: o runner emite uma linha JSON na última linha de stdout).
8
+ */
9
+
10
+ import { execFile } from 'node:child_process'
11
+ import { promises as fs } from 'node:fs'
12
+ import path from 'node:path'
13
+ import { fileURLToPath } from 'node:url'
14
+ import { promisify } from 'node:util'
15
+
16
+ import { canonicalProjectDirectory, safeProjectPath } from '@softize/base/project-path'
17
+
18
+ const execFileAsync = promisify(execFile)
19
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
20
+
21
+ export const CONFIG_FILE = 'opus.config.ts'
22
+
23
+ // =============================================================================
24
+ // Log
25
+ // =============================================================================
26
+
27
+ const COLORS = {
28
+ info: '\x1b[36m',
29
+ success: '\x1b[32m',
30
+ error: '\x1b[31m',
31
+ warn: '\x1b[33m',
32
+ dim: '\x1b[2m',
33
+ }
34
+ const RESET = '\x1b[0m'
35
+
36
+ /** Escreve uma linha colorida em stdout. Nível desconhecido sai sem cor. */
37
+ export function log(level, message) {
38
+ console.log(`${COLORS[level] ?? ''}${message}${RESET}`)
39
+ }
40
+
41
+ // =============================================================================
42
+ // FS
43
+ // =============================================================================
44
+
45
+ export async function fileExists(file) {
46
+ try {
47
+ await fs.access(file)
48
+ return true
49
+ } catch {
50
+ return false
51
+ }
52
+ }
53
+
54
+ // =============================================================================
55
+ // Config do consumer
56
+ // =============================================================================
57
+
58
+ /**
59
+ * Resolve o `opus.config.ts` a partir de `flags.config` (default: `./opus.config.ts`),
60
+ * contido no projeto. Lança `Error` com uma mensagem única quando o arquivo não existe;
61
+ * cada comando decide o canal (texto ou JSON) e o código de saída.
62
+ */
63
+ export function resolveConfig(flags, cwd = process.cwd()) {
64
+ const root = canonicalProjectDirectory(cwd)
65
+ const config = safeProjectPath(root, flags.config ?? CONFIG_FILE)
66
+ if (!config.exists) {
67
+ throw new Error(
68
+ `${CONFIG_FILE} não encontrado em ${config.path}. ` +
69
+ 'Passe o caminho com --config <path> ou crie o arquivo na raiz do projeto.',
70
+ )
71
+ }
72
+ return { cwd: root, configPath: config.path }
73
+ }
74
+
75
+ // =============================================================================
76
+ // Runner via tsx
77
+ // =============================================================================
78
+
79
+ /**
80
+ * Binário do tsx: o empacotado em `<package>/node_modules/.bin/tsx` quando existe
81
+ * (independe do PATH do consumer); senão confia no `tsx` do PATH.
82
+ */
83
+ export async function resolveTsxBin() {
84
+ const local = path.join(PACKAGE_ROOT, 'node_modules', '.bin', 'tsx')
85
+ return (await fileExists(local)) ? local : 'tsx'
86
+ }
87
+
88
+ /**
89
+ * Executa `tsx <runnerPath> ...args` capturando stdout/stderr em utf8 e herdando o
90
+ * ambiente. Rejeita como o `execFile` (o erro traz `stdout` e `stderr`) quando o
91
+ * processo falha.
92
+ */
93
+ export async function spawnTsx(runnerPath, args, { maxBuffer = 16 * 1024 * 1024 } = {}) {
94
+ const command = await resolveTsxBin()
95
+ return execFileAsync(command, [runnerPath, ...args], {
96
+ encoding: 'utf8',
97
+ maxBuffer,
98
+ env: { ...process.env },
99
+ })
100
+ }
101
+
102
+ /**
103
+ * Última linha de stdout que parece um objeto JSON. Logs do consumer durante o
104
+ * import do config vão para o início; o runner emite o payload por último.
105
+ */
106
+ export function lastJsonLine(stdout) {
107
+ const lines = stdout.split('\n').filter((line) => line.trim().length > 0)
108
+ for (let index = lines.length - 1; index >= 0; index--) {
109
+ const line = lines[index].trim()
110
+ if (line.startsWith('{') && line.endsWith('}')) return line
111
+ }
112
+ return null
113
+ }
114
+
115
+ /**
116
+ * Roda um runner que responde `{ ok, ... }` em JSON. Nunca rejeita: falha de spawn ou
117
+ * ausência de payload viram `{ ok: false, error }` para o comando reportar.
118
+ */
119
+ export async function runJsonRunner(runnerPath, args, options) {
120
+ try {
121
+ const { stdout } = await spawnTsx(runnerPath, args, options)
122
+ const line = lastJsonLine(stdout)
123
+ return line === null ? { ok: false, error: 'runner não emitiu JSON' } : JSON.parse(line)
124
+ } catch (error) {
125
+ const detail =
126
+ typeof error.stderr === 'string' && error.stderr.trim().length > 0
127
+ ? error.stderr.trim()
128
+ : error.message
129
+ return { ok: false, error: detail }
130
+ }
131
+ }
package/bin/lib/copy.mjs CHANGED
@@ -71,10 +71,6 @@ const JSX_CHILD_ROLES = new Map([
71
71
  ['ActionFormCard', 'message'],
72
72
  ['ActionFormDialog', 'message'],
73
73
  ['AlertDescription', 'message'],
74
- ['AlertDialogAction', 'button'],
75
- ['AlertDialogCancel', 'button'],
76
- ['AlertDialogDescription', 'dialog-body'],
77
- ['AlertDialogTitle', 'title'],
78
74
  ['AlertTitle', 'title'],
79
75
  ['Badge', 'badge'],
80
76
  ['BreadcrumbLink', 'breadcrumb'],
@@ -215,7 +211,7 @@ const JSX_PROP_STYLE = new Map([
215
211
  ])
216
212
 
217
213
  const JSX_PORTAL_BOUNDARIES = new Set([
218
- 'AlertDialogContent', 'DialogContent', 'DrawerContent', 'MenuContent', 'MenuSubContent',
214
+ 'DialogContent', 'DrawerContent', 'MenuContent', 'MenuSubContent',
219
215
  'PopoverContent', 'TooltipContent',
220
216
  ])
221
217
 
package/bin/lib/db.mjs CHANGED
@@ -10,31 +10,12 @@
10
10
  * Ver `docs/data-layer.md`.
11
11
  */
12
12
 
13
- import { promises as fs } from 'node:fs'
14
13
  import path from 'node:path'
15
14
  import { fileURLToPath } from 'node:url'
16
- import { execFile } from 'node:child_process'
17
- import { promisify } from 'node:util'
18
- import { canonicalProjectDirectory, safeProjectPath } from '@softize/base/project-path'
19
15
 
20
- const execFileAsync = promisify(execFile)
16
+ import { log, resolveConfig, runJsonRunner } from './cli-shared.mjs'
21
17
 
22
- const __filename = fileURLToPath(import.meta.url)
23
- const __dirname = path.dirname(__filename)
24
- const PACKAGE_ROOT = path.resolve(__dirname, '..', '..')
25
-
26
- const COLORS = {
27
- info: '\x1b[36m',
28
- success: '\x1b[32m',
29
- error: '\x1b[31m',
30
- warn: '\x1b[33m',
31
- dim: '\x1b[2m',
32
- }
33
- const RESET = '\x1b[0m'
34
-
35
- function log(level, msg) {
36
- console.log(`${COLORS[level] ?? ''}${msg}${RESET}`)
37
- }
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
38
19
 
39
20
  // =============================================================================
40
21
  // Dispatch
@@ -67,17 +48,13 @@ export async function cmdDb(rest, flags) {
67
48
  // Config
68
49
  // =============================================================================
69
50
 
70
- async function resolveConfig(flags) {
71
- const cwd = canonicalProjectDirectory(process.cwd())
72
- const configRel = flags.config ?? 'opus.config.ts'
73
- const config = safeProjectPath(cwd, configRel)
74
- const configPath = config.path
75
- if (!config.exists) {
76
- log('error', `opus.config.ts não encontrado em ${configPath}`)
77
- log('dim', ' Passa o path via --config <path> ou cria um na raiz do projeto.')
51
+ function resolveConfigOrExit(flags) {
52
+ try {
53
+ return resolveConfig(flags)
54
+ } catch (error) {
55
+ log('error', error instanceof Error ? error.message : String(error))
78
56
  process.exit(1)
79
57
  }
80
- return { cwd, configPath }
81
58
  }
82
59
 
83
60
  // =============================================================================
@@ -85,7 +62,7 @@ async function resolveConfig(flags) {
85
62
  // =============================================================================
86
63
 
87
64
  async function cmdDbCheck(flags) {
88
- const { cwd, configPath } = await resolveConfig(flags)
65
+ const { cwd, configPath } = resolveConfigOrExit(flags)
89
66
  log('info', `→ drift-check via ${path.relative(cwd, configPath)}...`)
90
67
 
91
68
  const result = await runRunner('db-check-runner.mjs', configPath)
@@ -128,7 +105,7 @@ async function cmdDbMigrate(sub2, flags) {
128
105
  log('dim', ' Rollback = editar o schema e re-rodar `opus db migrate`; catástrofe = snapshot pré-deploy.')
129
106
  process.exit(1)
130
107
  }
131
- const { cwd, configPath } = await resolveConfig(flags)
108
+ const { cwd, configPath } = resolveConfigOrExit(flags)
132
109
  log('info', `→ db migrate via ${path.relative(cwd, configPath)}...`)
133
110
 
134
111
  const result = await runRunner('db-migrate-runner.mjs', configPath)
@@ -163,7 +140,7 @@ async function cmdDbMigrate(sub2, flags) {
163
140
  // =============================================================================
164
141
 
165
142
  async function cmdDbScaffold(flags) {
166
- const { cwd, configPath } = await resolveConfig(flags)
143
+ const { cwd, configPath } = resolveConfigOrExit(flags)
167
144
  log('info', `→ db scaffold via ${path.relative(cwd, configPath)}...`)
168
145
 
169
146
  const result = await runRunner('db-scaffold-runner.mjs', configPath)
@@ -188,46 +165,8 @@ async function cmdDbScaffold(flags) {
188
165
  // Runner spawn (via tsx)
189
166
  // =============================================================================
190
167
 
191
- async function runRunner(runnerFile, configPath, extraArgs = []) {
192
- const tsxBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', 'tsx')
193
- const hasLocalTsx = await fileExists(tsxBin)
194
- const runnerPath = path.join(__dirname, runnerFile)
195
- const cmd = hasLocalTsx ? tsxBin : 'tsx'
196
-
197
- try {
198
- const { stdout } = await execFileAsync(cmd, [runnerPath, configPath, ...extraArgs], {
199
- encoding: 'utf8',
200
- maxBuffer: 16 * 1024 * 1024,
201
- env: { ...process.env },
202
- })
203
- const line = lastJsonLine(stdout)
204
- if (line === null) return { ok: false, error: 'runner não emitiu JSON' }
205
- return JSON.parse(line)
206
- } catch (err) {
207
- const detail =
208
- typeof err.stderr === 'string' && err.stderr.trim().length > 0
209
- ? err.stderr.trim()
210
- : err.message
211
- return { ok: false, error: detail }
212
- }
213
- }
214
-
215
- function lastJsonLine(stdout) {
216
- const lines = stdout.split('\n').filter((l) => l.trim().length > 0)
217
- for (let i = lines.length - 1; i >= 0; i--) {
218
- const trimmed = lines[i].trim()
219
- if (trimmed.startsWith('{') && trimmed.endsWith('}')) return trimmed
220
- }
221
- return null
222
- }
223
-
224
- async function fileExists(p) {
225
- try {
226
- await fs.access(p)
227
- return true
228
- } catch {
229
- return false
230
- }
168
+ function runRunner(runnerFile, configPath, extraArgs = []) {
169
+ return runJsonRunner(path.join(__dirname, runnerFile), [configPath, ...extraArgs])
231
170
  }
232
171
 
233
172
  // =============================================================================
@@ -245,9 +184,12 @@ export function helpDb() {
245
184
  re-rodável) e roda o drift-check na sequência. Exit ≠ 0 se divergir.
246
185
  db scaffold Gera um rascunho kysely a partir do diff — REFERÊNCIA pra escrever o
247
186
  SQL no schema (a verdade é o script; revise à mão).
187
+ db migrate down Aposentado: encerra com erro. O schema é um script idempotente sem
188
+ histórico a reverter; rollback = editar o script e rodar db migrate.
248
189
 
249
190
  Flags:
250
191
  --config <path> Caminho interno ao projeto para opus.config.ts. Default: ./opus.config.ts
192
+ --help, -h Mostra esta mensagem
251
193
 
252
194
  O opus.config.ts precisa expor, pros comandos db:
253
195
  database: () => Kysely factory LAZY do banco (gen não a chama)
@@ -258,6 +200,6 @@ O opus.config.ts precisa expor, pros comandos db:
258
200
  Exemplos:
259
201
  npx @softize/opus db check
260
202
  npx @softize/opus db migrate
261
- npx @softize/opus db migrate down --config ./apps/api/opus.config.ts
203
+ npx @softize/opus db scaffold --config ./apps/api/opus.config.ts
262
204
  `)
263
205
  }
@@ -82,15 +82,15 @@ function buildResponses(action) {
82
82
  const successStatus = String(action.successStatus ?? 200)
83
83
  return {
84
84
  [successStatus]: {
85
- description: 'Success',
85
+ description: 'Sucesso',
86
86
  content: {
87
87
  'application/json': {
88
88
  schema: wrapEnvelope(action.output, action.kind),
89
89
  },
90
90
  },
91
91
  },
92
- '4XX': errorResponse('Client error', action.errors),
93
- '5XX': errorResponse('Server error'),
92
+ '4XX': errorResponse('Erro do cliente', action.errors),
93
+ '5XX': errorResponse('Erro do servidor'),
94
94
  }
95
95
  }
96
96
 
@@ -75,7 +75,7 @@ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..')
75
75
  async function main() {
76
76
  const configPath = process.argv[2]
77
77
  if (typeof configPath !== 'string' || configPath.length === 0) {
78
- return emitError('Missing config path argv[2]')
78
+ return emitError('Caminho do config ausente (argv[2])')
79
79
  }
80
80
 
81
81
  const configAbs = path.resolve(configPath)
package/bin/lib/gen.mjs CHANGED
@@ -16,17 +16,13 @@
16
16
  * - config: ./opus.config.ts (no cwd)
17
17
  * - output: o que o config disser, ou ./.gen
18
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`).
19
+ * Flag `--force`: aceita por compatibilidade, sem efeito — o gen sempre
20
+ * sobrescreve (é idempotente e o output mora num dir dedicado, então não
21
+ * há valor real em bloquear a escrita).
23
22
  */
24
23
 
25
- import { promises as fs } from 'node:fs'
26
24
  import path from 'node:path'
27
25
  import { fileURLToPath } from 'node:url'
28
- import { execFile } from 'node:child_process'
29
- import { promisify } from 'node:util'
30
26
  import {
31
27
  canonicalProjectDirectory,
32
28
  ensureProjectDirectory,
@@ -42,46 +38,25 @@ import { buildOpenAPI } from './gen-openapi.mjs'
42
38
  import { buildDocs } from './gen-docs.mjs'
43
39
  import { buildClientStubs } from './gen-stubs.mjs'
44
40
  import { buildDictStubs } from './gen-dicts.mjs'
45
-
46
- const execFileAsync = promisify(execFile)
41
+ import { lastJsonLine, log, resolveConfig, spawnTsx } from './cli-shared.mjs'
47
42
 
48
43
  const __filename = fileURLToPath(import.meta.url)
49
44
  const __dirname = path.dirname(__filename)
50
- const PACKAGE_ROOT = path.resolve(__dirname, '..', '..')
51
45
  const RUNNER_PATH = path.join(__dirname, 'gen-runner.mjs')
52
46
 
53
- const COLORS = {
54
- info: '\x1b[36m',
55
- success: '\x1b[32m',
56
- error: '\x1b[31m',
57
- warn: '\x1b[33m',
58
- dim: '\x1b[2m',
59
- }
60
- const RESET = '\x1b[0m'
61
-
62
- function log(level, msg) {
63
- console.log(`${COLORS[level] ?? ''}${msg}${RESET}`)
64
- }
65
-
66
47
  // =============================================================================
67
48
  // Comando
68
49
  // =============================================================================
69
50
 
70
51
  export async function cmdGen(flags) {
71
- const cwd = canonicalProjectDirectory(process.cwd())
72
- const configRel = flags.config ?? 'opus.config.ts'
73
- const config = safeProjectPath(cwd, configRel)
74
- const configPath = config.path
75
-
76
- if (!config.exists) {
77
- log('error', `opus.config.ts não encontrado em ${configPath}`)
78
- log(
79
- 'dim',
80
- ' Cria um arquivo na raiz do projeto exportando `{ domains: [...] }` como default,',
81
- )
82
- log('dim', ' ou passa o path via --config <path>.')
52
+ let resolved
53
+ try {
54
+ resolved = resolveConfig(flags)
55
+ } catch (error) {
56
+ log('error', error instanceof Error ? error.message : String(error))
83
57
  process.exit(1)
84
58
  }
59
+ const { cwd, configPath } = resolved
85
60
 
86
61
  log('info', `→ Carregando ${path.relative(cwd, configPath)}...`)
87
62
 
@@ -190,22 +165,12 @@ function auditDocs(manifest, cwd) {
190
165
  // =============================================================================
191
166
 
192
167
  async function loadConfig(configPath) {
193
- // tsx mora em <package>/node_modules/.bin/tsx usamos absoluto pra
194
- // evitar depender de PATH do consumer.
195
- const tsxBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', 'tsx')
196
- const hasLocalTsx = await fileExists(tsxBin)
197
-
198
- // Args do runner: <runner.mjs> <configPath>. tsx aceita .mjs também
199
- // (passthrough) e nos permite resolver TS na cadeia de imports.
200
- const args = [RUNNER_PATH, configPath]
201
- const cmd = hasLocalTsx ? tsxBin : 'tsx'
202
-
168
+ // tsx aceita .mjs também (passthrough) e nos permite resolver TS na cadeia
169
+ // de imports do config.
203
170
  try {
204
171
  // Buffer grande pra acomodar manifests gordos.
205
- const { stdout, stderr } = await execFileAsync(cmd, args, {
206
- encoding: 'utf8',
172
+ const { stdout, stderr } = await spawnTsx(RUNNER_PATH, [configPath], {
207
173
  maxBuffer: 64 * 1024 * 1024,
208
- env: { ...process.env },
209
174
  })
210
175
 
211
176
  // Runner emite a linha JSON na ÚLTIMA linha de stdout. Qualquer log do
@@ -236,30 +201,10 @@ async function loadConfig(configPath) {
236
201
  }
237
202
  }
238
203
 
239
- function lastJsonLine(stdout) {
240
- const lines = stdout.split('\n').filter((l) => l.trim().length > 0)
241
- for (let i = lines.length - 1; i >= 0; i--) {
242
- const trimmed = lines[i].trim()
243
- if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
244
- return trimmed
245
- }
246
- }
247
- return null
248
- }
249
-
250
204
  // =============================================================================
251
205
  // FS helpers
252
206
  // =============================================================================
253
207
 
254
- async function fileExists(p) {
255
- try {
256
- await fs.access(p)
257
- return true
258
- } catch {
259
- return false
260
- }
261
- }
262
-
263
208
  function ensureProjectTree(root, requested) {
264
209
  let cursor = '.'
265
210
  for (const segment of requested.split(path.sep).filter((item) => item !== '' && item !== '.')) {
@@ -332,7 +277,7 @@ Lê opus.config.ts do consumer e gera:
332
277
  Flags:
333
278
  --config <path> Caminho interno ao projeto para opus.config.ts. Default: ./opus.config.ts
334
279
  --output <path> Pasta interna ao projeto. Default: do config (ou ./.gen)
335
- --force, -f Sobrescreve existente (atualmente sempre sobrescreve)
280
+ --force, -f Aceita por compatibilidade; sem efeito, o gen sempre sobrescreve a saída
336
281
 
337
282
  Exemplos:
338
283
  npx @softize/opus gen
package/bin/lib/mcp.mjs CHANGED
@@ -11,6 +11,7 @@
11
11
  * opus_get_component — detalhe de um componente pelo nome
12
12
  */
13
13
 
14
+ import { createRequire } from 'node:module'
14
15
  import path from 'node:path'
15
16
  import { z } from 'zod'
16
17
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
@@ -23,10 +24,11 @@ const asText = (data) => ({
23
24
  content: [{ type: 'text', text: typeof data === 'string' ? data : JSON.stringify(data, null, 2) }],
24
25
  })
25
26
  const dirOf = (d) => path.resolve(process.cwd(), d ?? '.')
27
+ const { version: OPUS_VERSION } = createRequire(import.meta.url)('../../package.json')
26
28
 
27
29
  /** Monta o McpServer com as tools da base. Exportado pra testar (transport in-memory). */
28
30
  export function buildServer() {
29
- const server = new McpServer({ name: 'opus', version: '0.0.0' })
31
+ const server = new McpServer({ name: 'opus', version: OPUS_VERSION })
30
32
 
31
33
  server.registerTool(
32
34
  'opus_introspect',
package/bin/lib/seed.mjs CHANGED
@@ -1,30 +1,13 @@
1
1
  /** Grupo `opus seed`: descoberta, gate e execução segura dos seeds do projeto. */
2
2
 
3
- import { execFile } from 'node:child_process'
4
- import { promises as fs } from 'node:fs'
5
3
  import path from 'node:path'
6
4
  import { fileURLToPath } from 'node:url'
7
- import { promisify } from 'node:util'
8
5
 
9
- import { canonicalProjectDirectory, readProjectFile, safeProjectPath } from '@softize/base/project-path'
6
+ import { readProjectFile } from '@softize/base/project-path'
10
7
 
11
- const execFileAsync = promisify(execFile)
12
- const __filename = fileURLToPath(import.meta.url)
13
- const __dirname = path.dirname(__filename)
14
- const PACKAGE_ROOT = path.resolve(__dirname, '..', '..')
8
+ import { log, resolveConfig, runJsonRunner } from './cli-shared.mjs'
15
9
 
16
- const COLORS = {
17
- info: '\x1b[36m',
18
- success: '\x1b[32m',
19
- error: '\x1b[31m',
20
- warn: '\x1b[33m',
21
- dim: '\x1b[2m',
22
- }
23
- const RESET = '\x1b[0m'
24
-
25
- function log(level, message) {
26
- console.log(`${COLORS[level] ?? ''}${message}${RESET}`)
27
- }
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
28
11
 
29
12
  export function findAdHocSeedScripts(scripts = {}) {
30
13
  return Object.entries(scripts).flatMap(([name, command]) => {
@@ -116,14 +99,6 @@ function fail(flags, message) {
116
99
  process.exitCode = 1
117
100
  }
118
101
 
119
- function resolveConfig(flags) {
120
- const cwd = canonicalProjectDirectory(process.cwd())
121
- const configRel = flags.config ?? 'opus.config.ts'
122
- const config = safeProjectPath(cwd, configRel)
123
- if (!config.exists) throw new Error(`opus.config.ts não encontrado em ${config.path}`)
124
- return { cwd, configPath: config.path }
125
- }
126
-
127
102
  function readSeedScriptFindings(cwd) {
128
103
  const packageFile = readProjectFile(cwd, 'package.json', { allowMissing: true })
129
104
  if (!packageFile.exists) return []
@@ -140,22 +115,8 @@ function readSeedScriptFindings(cwd) {
140
115
  }))
141
116
  }
142
117
 
143
- async function runRunner(configPath, extraArgs) {
144
- const tsxBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', 'tsx')
145
- const command = (await fileExists(tsxBin)) ? tsxBin : 'tsx'
146
- const runnerPath = path.join(__dirname, 'seed-runner.mjs')
147
- try {
148
- const { stdout } = await execFileAsync(command, [runnerPath, configPath, ...extraArgs], {
149
- encoding: 'utf8',
150
- maxBuffer: 16 * 1024 * 1024,
151
- env: { ...process.env },
152
- })
153
- const line = lastJsonLine(stdout)
154
- return line === null ? { ok: false, error: 'runner não emitiu JSON' } : JSON.parse(line)
155
- } catch (error) {
156
- const detail = typeof error.stderr === 'string' && error.stderr.trim() ? error.stderr.trim() : error.message
157
- return { ok: false, error: detail }
158
- }
118
+ function runRunner(configPath, extraArgs) {
119
+ return runJsonRunner(path.join(__dirname, 'seed-runner.mjs'), [configPath, ...extraArgs])
159
120
  }
160
121
 
161
122
  function printList(seeds) {
@@ -188,24 +149,6 @@ function printDiagnostics(diagnostics) {
188
149
  for (const diagnostic of diagnostics) log('warn', `[${diagnostic.code}] ${diagnostic.message}`)
189
150
  }
190
151
 
191
- function lastJsonLine(stdout) {
192
- const lines = stdout.split('\n').filter((line) => line.trim())
193
- for (let index = lines.length - 1; index >= 0; index--) {
194
- const line = lines[index].trim()
195
- if (line.startsWith('{') && line.endsWith('}')) return line
196
- }
197
- return null
198
- }
199
-
200
- async function fileExists(file) {
201
- try {
202
- await fs.access(file)
203
- return true
204
- } catch {
205
- return false
206
- }
207
- }
208
-
209
152
  export function helpSeed() {
210
153
  console.log(`
211
154
  @softize/opus seed — datasets estruturados do projeto