@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.
- package/CHANGELOG.md +47 -0
- package/PROMOTED.md +46 -0
- package/README.md +28 -19
- package/bin/cli.mjs +87 -216
- package/bin/lib/cli-shared.mjs +131 -0
- package/bin/lib/db.mjs +16 -74
- package/bin/lib/gen-openapi.mjs +3 -3
- package/bin/lib/gen-runner.mjs +1 -1
- package/bin/lib/gen.mjs +14 -69
- package/bin/lib/mcp.mjs +3 -1
- package/bin/lib/seed.mjs +5 -62
- package/docs/ownership-vs-shadcn-lock.md +2 -3
- package/docs/protocol.md +7 -7
- package/docs/releasing.md +8 -2
- package/package.json +7 -3
- package/registry/templates/app/package.json +1 -1
- package/registry/templates/app/src/main.tsx +4 -4
- package/src/audit/drivers/console.ts +1 -0
- package/src/auth/drivers/better-auth.ts +1 -0
- package/src/auth/drivers/jwt.ts +1 -0
- package/src/cache/drivers/memory.ts +1 -0
- package/src/client/drivers/fetch.ts +2 -1
- package/src/core/actions.ts +6 -1
- package/src/core/audit.ts +9 -3
- package/src/core/contracts.ts +7 -0
- package/src/core/domain.ts +1 -1
- package/src/core/errors.ts +18 -15
- package/src/core/index.ts +4 -2
- package/src/core/package-version.ts +26 -0
- package/src/core/reactions.ts +1 -1
- package/src/core/runtime.ts +33 -23
- package/src/core/schedules.ts +1 -1
- package/src/core/types.ts +2 -2
- package/src/dsl/eval.ts +2 -2
- package/src/dsl/kysely.ts +2 -2
- package/src/dsl/loads.ts +1 -1
- package/src/dsl/parser.ts +5 -5
- package/src/events/drivers/mitt.ts +1 -0
- package/src/mcp/index.ts +2 -1
- package/src/observability/drivers/opentelemetry.ts +1 -0
- package/src/queue/drivers/bullmq.ts +3 -3
- package/src/scheduler/drivers/node-cron.ts +3 -2
- package/src/scheduler/every.ts +7 -7
- package/src/schema/openapi.ts +3 -3
- package/src/seed/index.ts +29 -0
- package/src/server/drivers/fastify.ts +5 -2
- package/src/server/drivers/node.ts +9 -6
- package/src/server/index.ts +3 -1
- package/src/storage/drivers/fs.ts +1 -0
- package/src/testing/index.ts +3 -3
- package/src/ui/components/patterns/confirm.tsx +2 -2
- package/src/ui/components/patterns/content-header.tsx +7 -1
- package/src/ui/components/patterns/data-state.tsx +1 -1
- package/src/ui/components/patterns/dock.tsx +20 -3
- package/src/ui/components/patterns/form.tsx +12 -8
- package/src/ui/components/patterns/list.tsx +4 -4
- package/src/ui/components/patterns/page.tsx +19 -1
- package/src/ui/components/patterns/shell-nav.tsx +10 -3
- package/src/ui/components/patterns/sidebar.tsx +17 -6
- package/src/ui/components/patterns/trigger.tsx +14 -16
- package/src/ui/components/patterns/view.tsx +26 -17
- package/src/ui/components/primitives/alert.tsx +11 -5
- package/src/ui/components/primitives/ask.tsx +3 -3
- package/src/ui/components/primitives/badge.tsx +11 -6
- package/src/ui/components/primitives/breadcrumb.tsx +2 -2
- package/src/ui/components/primitives/button.tsx +16 -3
- package/src/ui/components/primitives/calendar.tsx +28 -2
- package/src/ui/components/primitives/carousel.tsx +3 -3
- package/src/ui/components/primitives/chat.tsx +1 -1
- package/src/ui/components/primitives/checkbox.tsx +1 -1
- package/src/ui/components/primitives/command.tsx +2 -2
- package/src/ui/components/primitives/control.ts +12 -0
- package/src/ui/components/primitives/copyable.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +12 -7
- package/src/ui/components/primitives/dot.tsx +5 -0
- package/src/ui/components/primitives/drawer.tsx +10 -3
- package/src/ui/components/primitives/field.tsx +3 -3
- package/src/ui/components/primitives/icon-picker.tsx +3 -1
- package/src/ui/components/primitives/input-group.tsx +1 -1
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/input.tsx +2 -2
- package/src/ui/components/primitives/progress.tsx +32 -3
- package/src/ui/components/primitives/radio-group.tsx +1 -1
- package/src/ui/components/primitives/resizable.tsx +3 -1
- package/src/ui/components/primitives/select.tsx +5 -5
- package/src/ui/components/primitives/slider.tsx +5 -1
- package/src/ui/components/primitives/sonner.tsx +3 -0
- package/src/ui/components/primitives/switch.tsx +1 -0
- package/src/ui/components/primitives/tabs.tsx +1 -0
- package/src/ui/components/primitives/textarea.tsx +1 -1
- package/src/ui/components/primitives/toggle.tsx +1 -1
- package/src/ui/components/primitives/tooltip.tsx +1 -0
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/action-form.md +10 -3
- package/src/ui/docs/content/action-list.md +10 -1
- package/src/ui/docs/content/action-trigger.md +8 -1
- package/src/ui/docs/content/action-view.md +9 -2
- package/src/ui/docs/content/ask.md +11 -0
- package/src/ui/docs/content/calendar.md +13 -0
- package/src/ui/docs/content/card.md +26 -0
- package/src/ui/docs/content/chat.md +20 -0
- package/src/ui/docs/content/cli.md +71 -19
- package/src/ui/docs/content/composer.md +15 -0
- package/src/ui/docs/content/content.md +15 -0
- package/src/ui/docs/content/copyable.md +8 -0
- package/src/ui/docs/content/detail.md +19 -1
- package/src/ui/docs/content/dictionary-value.md +9 -2
- package/src/ui/docs/content/dock.md +8 -0
- package/src/ui/docs/content/dot.md +8 -0
- package/src/ui/docs/content/empty.md +2 -2
- package/src/ui/docs/content/getting-started.md +2 -2
- package/src/ui/docs/content/icon-picker.md +11 -0
- package/src/ui/docs/content/label.md +7 -0
- package/src/ui/docs/content/menu.md +6 -0
- package/src/ui/docs/content/metric-card.md +13 -0
- package/src/ui/docs/content/page.md +8 -0
- package/src/ui/docs/content/popover.md +6 -0
- package/src/ui/docs/content/progress.md +8 -11
- package/src/ui/docs/content/select.md +5 -5
- package/src/ui/docs/content/semantic-context.md +2 -2
- package/src/ui/docs/content/sidebar.md +14 -8
- package/src/ui/docs/content/skeleton.md +6 -0
- package/src/ui/docs/content/split.md +21 -0
- package/src/ui/docs/content/textarea.md +7 -0
- package/src/ui/docs/content/tokens.md +4 -4
- package/src/ui/docs/content/truncate.md +8 -0
- package/src/ui/docs/content/ui.md +14 -0
- package/src/ui/docs/doc-client.tsx +5 -5
- package/src/ui/docs/doc.tsx +26 -14
- package/src/ui/docs/registry.tsx +5 -5
- package/src/ui/docs/standalone.tsx +2 -2
- package/src/ui/drivers/react.tsx +17 -12
- package/src/ui/lib/action-errors.ts +45 -0
- package/src/ui/lib/zod-pt-br.ts +31 -4
- package/src/ui/meta.ts +3 -3
- package/src/ui/react.tsx +2 -0
- package/src/ui/theme.css +10 -8
- 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
|
-
|
|
16
|
+
import { log, resolveConfig, runJsonRunner } from './cli-shared.mjs'
|
|
21
17
|
|
|
22
|
-
const
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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
|
-
|
|
192
|
-
|
|
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
|
|
203
|
+
npx @softize/opus db scaffold --config ./apps/api/opus.config.ts
|
|
262
204
|
`)
|
|
263
205
|
}
|
package/bin/lib/gen-openapi.mjs
CHANGED
|
@@ -82,15 +82,15 @@ function buildResponses(action) {
|
|
|
82
82
|
const successStatus = String(action.successStatus ?? 200)
|
|
83
83
|
return {
|
|
84
84
|
[successStatus]: {
|
|
85
|
-
description: '
|
|
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('
|
|
93
|
-
'5XX': errorResponse('
|
|
92
|
+
'4XX': errorResponse('Erro do cliente', action.errors),
|
|
93
|
+
'5XX': errorResponse('Erro do servidor'),
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
package/bin/lib/gen-runner.mjs
CHANGED
|
@@ -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('
|
|
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`:
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
194
|
-
//
|
|
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
|
|
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
|
|
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:
|
|
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 {
|
|
6
|
+
import { readProjectFile } from '@softize/base/project-path'
|
|
10
7
|
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
144
|
-
|
|
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.
|
|
77
|
-
|
|
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: `
|
|
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: '
|
|
434
|
+
message: 'Negócio já 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: '
|
|
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
|
|
456
|
-
{ path: 'priority', code: 'out_of_range', message: 'priority
|
|
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: '
|
|
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
|
|
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
|
|
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
|