@softize/opus 13.0.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 (138) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/PROMOTED.md +46 -0
  3. package/README.md +28 -19
  4. package/bin/cli.mjs +87 -216
  5. package/bin/lib/cli-shared.mjs +131 -0
  6. package/bin/lib/db.mjs +16 -74
  7. package/bin/lib/gen-openapi.mjs +3 -3
  8. package/bin/lib/gen-runner.mjs +1 -1
  9. package/bin/lib/gen.mjs +14 -69
  10. package/bin/lib/mcp.mjs +3 -1
  11. package/bin/lib/seed.mjs +5 -62
  12. package/docs/ownership-vs-shadcn-lock.md +2 -3
  13. package/docs/protocol.md +7 -7
  14. package/docs/releasing.md +8 -2
  15. package/package.json +7 -3
  16. package/registry/templates/app/package.json +1 -1
  17. package/registry/templates/app/src/main.tsx +4 -4
  18. package/src/audit/drivers/console.ts +1 -0
  19. package/src/auth/drivers/better-auth.ts +1 -0
  20. package/src/auth/drivers/jwt.ts +1 -0
  21. package/src/cache/drivers/memory.ts +1 -0
  22. package/src/client/drivers/fetch.ts +2 -1
  23. package/src/core/actions.ts +6 -1
  24. package/src/core/audit.ts +9 -3
  25. package/src/core/contracts.ts +7 -0
  26. package/src/core/domain.ts +1 -1
  27. package/src/core/errors.ts +18 -15
  28. package/src/core/index.ts +4 -2
  29. package/src/core/package-version.ts +26 -0
  30. package/src/core/reactions.ts +1 -1
  31. package/src/core/runtime.ts +33 -23
  32. package/src/core/schedules.ts +1 -1
  33. package/src/core/types.ts +2 -2
  34. package/src/dsl/eval.ts +2 -2
  35. package/src/dsl/kysely.ts +2 -2
  36. package/src/dsl/loads.ts +1 -1
  37. package/src/dsl/parser.ts +5 -5
  38. package/src/events/drivers/mitt.ts +1 -0
  39. package/src/mcp/index.ts +2 -1
  40. package/src/observability/drivers/opentelemetry.ts +1 -0
  41. package/src/queue/drivers/bullmq.ts +3 -3
  42. package/src/scheduler/drivers/node-cron.ts +3 -2
  43. package/src/scheduler/every.ts +7 -7
  44. package/src/schema/openapi.ts +3 -3
  45. package/src/seed/index.ts +29 -0
  46. package/src/server/drivers/fastify.ts +5 -2
  47. package/src/server/drivers/node.ts +9 -6
  48. package/src/server/index.ts +3 -1
  49. package/src/storage/drivers/fs.ts +1 -0
  50. package/src/testing/index.ts +3 -3
  51. package/src/ui/components/patterns/confirm.tsx +2 -2
  52. package/src/ui/components/patterns/content-header.tsx +7 -1
  53. package/src/ui/components/patterns/data-state.tsx +1 -1
  54. package/src/ui/components/patterns/dock.tsx +20 -3
  55. package/src/ui/components/patterns/form.tsx +12 -8
  56. package/src/ui/components/patterns/list.tsx +4 -4
  57. package/src/ui/components/patterns/page.tsx +19 -1
  58. package/src/ui/components/patterns/shell-nav.tsx +10 -3
  59. package/src/ui/components/patterns/sidebar.tsx +17 -6
  60. package/src/ui/components/patterns/trigger.tsx +14 -16
  61. package/src/ui/components/patterns/view.tsx +26 -17
  62. package/src/ui/components/primitives/alert.tsx +11 -5
  63. package/src/ui/components/primitives/ask.tsx +3 -3
  64. package/src/ui/components/primitives/badge.tsx +11 -6
  65. package/src/ui/components/primitives/breadcrumb.tsx +2 -2
  66. package/src/ui/components/primitives/button.tsx +16 -3
  67. package/src/ui/components/primitives/calendar.tsx +28 -2
  68. package/src/ui/components/primitives/carousel.tsx +3 -3
  69. package/src/ui/components/primitives/chat.tsx +1 -1
  70. package/src/ui/components/primitives/checkbox.tsx +1 -1
  71. package/src/ui/components/primitives/command.tsx +2 -2
  72. package/src/ui/components/primitives/control.ts +12 -0
  73. package/src/ui/components/primitives/copyable.tsx +1 -1
  74. package/src/ui/components/primitives/dialog.tsx +12 -7
  75. package/src/ui/components/primitives/dot.tsx +5 -0
  76. package/src/ui/components/primitives/drawer.tsx +10 -3
  77. package/src/ui/components/primitives/field.tsx +3 -3
  78. package/src/ui/components/primitives/icon-picker.tsx +3 -1
  79. package/src/ui/components/primitives/input-group.tsx +1 -1
  80. package/src/ui/components/primitives/input-otp.tsx +1 -1
  81. package/src/ui/components/primitives/input.tsx +2 -2
  82. package/src/ui/components/primitives/progress.tsx +32 -3
  83. package/src/ui/components/primitives/radio-group.tsx +1 -1
  84. package/src/ui/components/primitives/resizable.tsx +3 -1
  85. package/src/ui/components/primitives/select.tsx +5 -5
  86. package/src/ui/components/primitives/slider.tsx +5 -1
  87. package/src/ui/components/primitives/sonner.tsx +3 -0
  88. package/src/ui/components/primitives/switch.tsx +1 -0
  89. package/src/ui/components/primitives/tabs.tsx +1 -0
  90. package/src/ui/components/primitives/textarea.tsx +1 -1
  91. package/src/ui/components/primitives/toggle.tsx +1 -1
  92. package/src/ui/components/primitives/tooltip.tsx +1 -0
  93. package/src/ui/docs/changelog.tsx +1 -1
  94. package/src/ui/docs/content/action-form.md +10 -3
  95. package/src/ui/docs/content/action-list.md +10 -1
  96. package/src/ui/docs/content/action-trigger.md +8 -1
  97. package/src/ui/docs/content/action-view.md +9 -2
  98. package/src/ui/docs/content/ask.md +11 -0
  99. package/src/ui/docs/content/calendar.md +13 -0
  100. package/src/ui/docs/content/card.md +26 -0
  101. package/src/ui/docs/content/chat.md +20 -0
  102. package/src/ui/docs/content/cli.md +71 -19
  103. package/src/ui/docs/content/composer.md +15 -0
  104. package/src/ui/docs/content/content.md +15 -0
  105. package/src/ui/docs/content/copyable.md +8 -0
  106. package/src/ui/docs/content/detail.md +19 -1
  107. package/src/ui/docs/content/dictionary-value.md +9 -2
  108. package/src/ui/docs/content/dock.md +8 -0
  109. package/src/ui/docs/content/dot.md +8 -0
  110. package/src/ui/docs/content/empty.md +2 -2
  111. package/src/ui/docs/content/getting-started.md +2 -2
  112. package/src/ui/docs/content/icon-picker.md +11 -0
  113. package/src/ui/docs/content/label.md +7 -0
  114. package/src/ui/docs/content/menu.md +6 -0
  115. package/src/ui/docs/content/metric-card.md +13 -0
  116. package/src/ui/docs/content/page.md +8 -0
  117. package/src/ui/docs/content/popover.md +6 -0
  118. package/src/ui/docs/content/progress.md +8 -11
  119. package/src/ui/docs/content/select.md +5 -5
  120. package/src/ui/docs/content/semantic-context.md +2 -2
  121. package/src/ui/docs/content/sidebar.md +14 -8
  122. package/src/ui/docs/content/skeleton.md +6 -0
  123. package/src/ui/docs/content/split.md +21 -0
  124. package/src/ui/docs/content/textarea.md +7 -0
  125. package/src/ui/docs/content/tokens.md +4 -4
  126. package/src/ui/docs/content/truncate.md +8 -0
  127. package/src/ui/docs/content/ui.md +14 -0
  128. package/src/ui/docs/doc-client.tsx +5 -5
  129. package/src/ui/docs/doc.tsx +26 -14
  130. package/src/ui/docs/registry.tsx +5 -5
  131. package/src/ui/docs/standalone.tsx +2 -2
  132. package/src/ui/drivers/react.tsx +17 -12
  133. package/src/ui/lib/action-errors.ts +45 -0
  134. package/src/ui/lib/zod-pt-br.ts +31 -4
  135. package/src/ui/meta.ts +3 -3
  136. package/src/ui/react.tsx +2 -0
  137. package/src/ui/theme.css +10 -8
  138. package/src/vite/design.ts +6 -18
@@ -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/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
@@ -73,9 +73,8 @@ upstream com quem se tem que ficar idêntico atrita com ter opinião.
73
73
  3. **Re-sync por diff sob demanda, com IA.** Em vez de "re-porta e o hash diz o que
74
74
  mudou", o fluxo é um **merge de 3 vias**: base (o `upstreamHash` gravado) → shadcn
75
75
  atual → nosso customizado. O Claude lê os dois diffs e reconcilia preservando o delta
76
- da casa. Vale uma skill/comando (`opus resync <componente>`) pra ser um gesto de uma
77
- linha, com teste + revisão como rede de segurança (o hash determinístico sai, o
78
- julgamento entra).
76
+ da casa. Hoje é um fluxo conduzido com IA, com teste + revisão como rede de segurança
77
+ (o hash determinístico sai, o julgamento entra); não existe comando `opus resync`.
79
78
 
80
79
  ## Tradeoffs honestos
81
80
 
package/docs/protocol.md CHANGED
@@ -424,21 +424,21 @@ handler: async (ctx, input) => {
424
424
  if (!deal) throw error({
425
425
  code: 'deal.archive.notFound',
426
426
  category: 'not_found',
427
- message: `Deal ${input.dealId} not found`,
427
+ message: `Negócio ${input.dealId} não encontrado`,
428
428
  i18nKey: 'deal.error.notFound',
429
429
  i18nParams: { id: input.dealId },
430
430
  })
431
431
  if (deal.archivedAt) throw error({
432
432
  code: 'deal.archive.alreadyArchived',
433
433
  category: 'conflict',
434
- message: 'Deal already archived',
434
+ message: 'Negócio arquivado',
435
435
  retriable: false,
436
436
  })
437
437
  // ...
438
438
  }
439
439
  ```
440
440
 
441
- Erros não-categorizados (ex: `throw new Error(...)` cru, ou exceção do driver do DB) são **normalizados** pelo runtime para `{ category: 'internal', code: 'internal.unhandled', severity: 'error', retriable: false, cause: <original> }`.
441
+ Erros não-categorizados (ex: `throw new Error(...)` cru, ou exceção do driver do DB) são **normalizados** pelo runtime para `{ category: 'internal', code: 'internal.unhandled', severity: 'error', retriable: false, message: 'Não foi possível concluir a operação. Tente novamente.', cause: <original> }`. A `message` é fixa: o texto da exceção original nunca chega à pessoa — fica no logger do runtime e em `cause` (só a mensagem; stack não vaza).
442
442
 
443
443
  ### Validation: caso especial
444
444
 
@@ -448,12 +448,12 @@ Erros de schema (input que não satisfaz Zod/ArkType) são **automaticamente** t
448
448
  {
449
449
  code: 'validation.invalid_input',
450
450
  category: 'validation',
451
- message: 'Input validation failed',
451
+ message: 'Dados de entrada inválidos',
452
452
  severity: 'warning',
453
453
  retriable: false,
454
454
  issues: [
455
- { path: 'dealId', code: 'required', message: 'dealId is required' },
456
- { path: 'priority', code: 'out_of_range', message: 'priority must be 1..5' },
455
+ { path: 'dealId', code: 'required', message: 'dealId é obrigatório' },
456
+ { path: 'priority', code: 'out_of_range', message: 'priority precisa estar entre 1 e 5' },
457
457
  ],
458
458
  }
459
459
  ```
@@ -540,7 +540,7 @@ authorize: async (ctx, input, loaded) => {
540
540
  if (loaded.deal.locked) return error({ // específico
541
541
  code: 'deal.archive.locked',
542
542
  category: 'authorization',
543
- message: 'Locked deals cannot be archived',
543
+ message: 'Negócios bloqueados não podem ser arquivados',
544
544
  })
545
545
  return ctx.can('deal:archive', loaded.deal) // delega
546
546
  }
package/docs/releasing.md CHANGED
@@ -53,17 +53,21 @@ pra testar publicação seria a guarda atrapalhando quem está experimentando.
53
53
 
54
54
  **Gate de qualidade**: depois do bump e da materialização, mas antes de publicar, roda
55
55
  `pnpm typecheck` + `pnpm test` + `pnpm copy:check` + `base copy check` e **aborta a release
56
- se qualquer um falhar**. O Opus declara `@softize/base ^2.1.0` em `dependencies`, pois usa
56
+ se qualquer um falhar**. O Opus declara `@softize/base ^2.2.1` em `dependencies`, pois usa
57
57
  suas APIs públicas de filesystem em runtime, e materializa os artefatos Base no próprio repo;
58
58
  a release não baixa uma política ad hoc. Como o
59
59
  Opus **ship source** (`.ts`, sem build), essa é a última barreira antes do tarball — sem ela,
60
60
  um `tsc` vermelho, inventário desatualizado ou violação da política vaza pro cliente. Emergência (evite):
61
61
  `pnpm release --skip-checks`.
62
62
 
63
+ O gate roda `pnpm test`, não `pnpm test:cov`: o threshold de 100% de cobertura em
64
+ `vitest.config.ts` é uma meta aspiracional e **não** faz parte do gate de release. Uma release
65
+ não é bloqueada por cobertura abaixo de 100%; a cobertura é acompanhada com `pnpm test:cov`.
66
+
63
67
  **Smoke do esqueleto**: depois do bump e antes do publish, o `release.sh` gera um app com
64
68
  `opus create`, instala o **tarball exato** que vai ser publicado e roda os gates dele
65
69
  (typecheck · test · `opus check` · `opus copy --check` · `base copy check` · manifest ·
66
- build). O template exige `@softize/base ^2.1.0`; publique a Base compatível antes do Opus.
70
+ build). O template exige `@softize/base ^2.2.1`; publique a Base compatível antes do Opus.
67
71
  O `minimumReleaseAgeExclude` do template inclui os dois pacotes, e o smoke executa os
68
72
  fragmentos de pre-push para provar que o layout pnpm instalado resolve ambos os CLIs.
69
73
  É o que pega o que typecheck+test não
@@ -93,6 +97,8 @@ sem `link:`/`file:`, que podem mascarar exports e peers.
93
97
  ```bash
94
98
  # terminal 1 — uma vez por máquina/sessão
95
99
  pnpm registry:up # Verdaccio em 127.0.0.1:6873
100
+ # `registry:up` lê `~/.config/verdaccio/config.yaml`, que não é versionado: cada estação
101
+ # mantém o seu, com `listen: 127.0.0.1:6873` (a porta que `release.sh --local` espera).
96
102
 
97
103
  # terminal 2 — no Opus
98
104
  pnpm version 8.7.0-rc.0 --no-git-tag-version # escolha uma versão ainda não publicada