@softize/opus 12.6.3 → 12.7.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 +18 -1
- package/README.md +20 -2
- package/bin/cli.mjs +10 -1
- package/bin/lib/gen-manifest.mjs +1 -0
- package/bin/lib/gen-runner.mjs +29 -1
- package/bin/lib/seed-runner.mjs +152 -0
- package/bin/lib/seed.mjs +229 -0
- package/docs/adr/0002-structured-seeds-are-declared-and-bound.md +130 -0
- package/docs/protocol.md +10 -1
- package/docs/seeds.md +132 -0
- package/package.json +5 -1
- package/registry/instructions/opus.md +4 -0
- package/registry/skills/apply-opus-seed/SKILL.md +45 -0
- package/registry/skills/apply-opus-seed/agents/openai.yaml +4 -0
- package/registry/skills/apply-opus-seed/references/evaluations.md +8 -0
- package/registry/skills/create-opus-seed/SKILL.md +55 -0
- package/registry/skills/create-opus-seed/agents/openai.yaml +4 -0
- package/registry/skills/create-opus-seed/references/contract.md +16 -0
- package/registry/skills/create-opus-seed/references/evaluations.md +8 -0
- package/registry/skills/create-opus-seed/scripts/scaffold.mjs +78 -0
- package/registry/skills/implement-opus-change/SKILL.md +3 -1
- package/src/core/index.ts +2 -0
- package/src/core/types.ts +16 -1
- package/src/seed/index.ts +391 -0
- package/src/ui/components/patterns/form.tsx +131 -19
- package/src/ui/components/primitives/detail.tsx +113 -0
- package/src/ui/docs/content/action-form.md +10 -0
- package/src/ui/docs/content/cli.md +17 -0
- package/src/ui/docs/content/detail.md +38 -0
- package/src/ui/docs/registry.tsx +2 -0
- package/src/ui/drivers/react.tsx +3 -2
- package/src/ui/lib/object-schema.ts +36 -0
- package/src/ui/meta.ts +7 -1
- package/src/ui/react.tsx +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
|
7
7
|
`opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
|
|
8
8
|
mudança cobra do seu código.
|
|
9
9
|
|
|
10
|
+
## 12.7.0 — 2026-08-29
|
|
11
|
+
|
|
12
|
+
Seeds persistentes ganham uma porta própria em `@softize/opus/seed`: declaração compartilhável,
|
|
13
|
+
binding server-only, perfis, dependências, métricas esperadas e escopos permitidos. A CLI passa a
|
|
14
|
+
listar, validar, planejar, aplicar e verificar esses datasets por `opus seed`, bloqueia qualquer
|
|
15
|
+
operação com banco sem ambiente seguro e atestação do alvo real, e reprova comandos locais
|
|
16
|
+
paralelos ou compostos no gate.
|
|
17
|
+
|
|
18
|
+
O registry inclui as skills `create-opus-seed` e `apply-opus-seed`. O manifest projeta somente a
|
|
19
|
+
declaração; conexão e funções do binding permanecem server-only. A mudança é aditiva. Projetos que
|
|
20
|
+
adotarem a superfície devem migrar scripts `seed*`/`db:seed*` ou fazê-los delegar para a CLI.
|
|
21
|
+
|
|
22
|
+
`DetailField` e `DetailGroup` oferecem a composição canônica para dados somente leitura, com
|
|
23
|
+
labels, valores React ricos, grade responsiva, moldura externa e divisórias internas opcionais.
|
|
24
|
+
`ActionForm` também passa a renderizar `widget: 'toggle-group'` de forma declarativa e aceita
|
|
25
|
+
conteúdo rico nas opções pelo mesmo contrato usado pelos demais campos. O novo `FieldWidget` tipa
|
|
26
|
+
os overrides reconhecidos pelo renderer. As mudanças são aditivas.
|
|
27
|
+
|
|
10
28
|
## 12.6.3 — 2026-08-26
|
|
11
29
|
|
|
12
30
|
A skill `build-opus-ui` passa a orientar operacionalmente os patterns entregues em 12.6.0 e
|
|
@@ -46,7 +64,6 @@ Os patterns existentes passam a reutilizar os primitives canônicos: `ActionForm
|
|
|
46
64
|
`FieldGroup`, descrições e erros associados ao controle; `ActionList` usa a moldura de `Table` e
|
|
47
65
|
os estados seguros de `DataState`, preservando a tentativa de recuperação; e a confirmação de uma
|
|
48
66
|
`ActionTrigger` destrutiva mantém o tratamento destrutivo mesmo quando o gatilho visual é discreto.
|
|
49
|
-
|
|
50
67
|
## 12.5.5 — 2026-08-25
|
|
51
68
|
|
|
52
69
|
Só documentação: leva ao pacote a correção de nota que a 12.5.4 recebeu no repositório depois de
|
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ Um pacote (`@softize/opus`), uma versão. Cada categoria abaixo é um **subpath
|
|
|
35
35
|
| `@softize/opus/queue` | `/bullmq` | Background job execution |
|
|
36
36
|
| `@softize/opus/events` | `/mitt` | EventBus (in-process) |
|
|
37
37
|
| `@softize/opus/scheduler` | `/node-cron` | Schedule adapter (ação iniciada por tempo) |
|
|
38
|
+
| `@softize/opus/seed` | — | Declaração e binding de datasets verificáveis operados pela CLI |
|
|
38
39
|
|
|
39
40
|
Drivers via subpath ESM (estilo Drizzle): `@softize/opus/server/fastify`, `@softize/opus/data/kysely`.
|
|
40
41
|
|
|
@@ -82,12 +83,29 @@ src/
|
|
|
82
83
|
core/ schema/ server/ client/ ui/
|
|
83
84
|
data/ auth/ audit/ log/ queue/ events/ scheduler/ dsl/
|
|
84
85
|
registry/ # scaffolds + skills Opus — geração e conhecimento do SDK, não runtime
|
|
85
|
-
bin/ # CLI (opus setup/gen/copy/check/introspect/mcp) + libs
|
|
86
|
+
bin/ # CLI (opus setup/gen/copy/check/seed/introspect/mcp) + libs
|
|
86
87
|
docs/
|
|
87
88
|
protocol.md # contrato completo (16 seções)
|
|
88
|
-
data-layer.md · releasing.md · code-style.md
|
|
89
|
+
data-layer.md · seeds.md · releasing.md · code-style.md
|
|
89
90
|
```
|
|
90
91
|
|
|
92
|
+
## Seeds de projeto
|
|
93
|
+
|
|
94
|
+
Datasets persistentes de desenvolvimento, teste e demonstração usam `defineSeed` + `bindSeed` e
|
|
95
|
+
ficam registrados em `opus.config.ts`. A CLI descobre e valida sem abrir conexão, exige escopo
|
|
96
|
+
explícito para operar e bloqueia produção:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pnpm exec opus seed list
|
|
100
|
+
pnpm exec opus seed check
|
|
101
|
+
pnpm exec opus seed plan customers.scenarios --profile smoke --scope local
|
|
102
|
+
pnpm exec opus seed apply customers.scenarios --profile smoke --scope local
|
|
103
|
+
pnpm exec opus seed verify customers.scenarios --profile smoke --scope local
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
O contrato não oferece reset/truncate, e `apply` precisa convergir quando repetido. Veja
|
|
107
|
+
[docs/seeds.md](docs/seeds.md).
|
|
108
|
+
|
|
91
109
|
## Status
|
|
92
110
|
|
|
93
111
|
- **v0**: 12 superfícies, 430 tests, 100% coverage.
|
package/bin/cli.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import { createMonorepo, createProject } from './lib/create.mjs'
|
|
|
31
31
|
import { initProject, repoRootOf, setupUiFoundation } from './lib/init.mjs'
|
|
32
32
|
import { introspect } from './lib/introspect.mjs'
|
|
33
33
|
import { materializeOpus } from './lib/materialize.mjs'
|
|
34
|
+
import { cmdSeed } from './lib/seed.mjs'
|
|
34
35
|
|
|
35
36
|
const __filename = fileURLToPath(import.meta.url)
|
|
36
37
|
const __dirname = path.dirname(__filename)
|
|
@@ -393,7 +394,7 @@ function parseArgs(argv) {
|
|
|
393
394
|
const positional = []
|
|
394
395
|
const flags = { force: false, help: false }
|
|
395
396
|
// Flags com valor — pega o próximo arg.
|
|
396
|
-
const VALUE_FLAGS = new Set(['--config', '--output'])
|
|
397
|
+
const VALUE_FLAGS = new Set(['--config', '--output', '--profile', '--scope'])
|
|
397
398
|
for (let i = 0; i < args.length; i++) {
|
|
398
399
|
const arg = args[i]
|
|
399
400
|
if (arg === '--force' || arg === '-f') {
|
|
@@ -455,6 +456,7 @@ Comandos:
|
|
|
455
456
|
check [dir] Valida as convenções das actions (régua de padrão; exit ≠ 0 se violar)
|
|
456
457
|
pre-push Gate Git: freshness dos artefatos + convenções das actions
|
|
457
458
|
db <verbo> Comandos de banco. v1: db check (drift-check entidade ↔ banco)
|
|
459
|
+
seed <verbo> Lista, valida, planeja, aplica e verifica seeds estruturados
|
|
458
460
|
introspect [dir] Modelo da estrutura (actions/reactions/schedules + wiring); --json
|
|
459
461
|
mcp Server MCP (introspect/check/scaffold de action) — agentes via mcp_config
|
|
460
462
|
help Mostra esta mensagem
|
|
@@ -464,6 +466,8 @@ Flags:
|
|
|
464
466
|
--check Não escreve; falha se o inventário de copy estiver ausente/desatualizado
|
|
465
467
|
--config <path> (gen) Caminho do opus.config.ts
|
|
466
468
|
--output <path> (gen) Pasta de saída
|
|
469
|
+
--profile <name> (seed) Perfil do dataset
|
|
470
|
+
--scope <name> (seed) Escopo explícito dos dados
|
|
467
471
|
|
|
468
472
|
Exemplos:
|
|
469
473
|
npx @softize/opus add action-form
|
|
@@ -606,6 +610,11 @@ Flags:
|
|
|
606
610
|
return
|
|
607
611
|
}
|
|
608
612
|
|
|
613
|
+
if (command === 'seed') {
|
|
614
|
+
await cmdSeed(rest, flags)
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
|
|
609
618
|
if (command === 'introspect') {
|
|
610
619
|
await cmdIntrospect(rest[0], flags)
|
|
611
620
|
return
|
package/bin/lib/gen-manifest.mjs
CHANGED
package/bin/lib/gen-runner.mjs
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
* opusVersion: string,
|
|
25
25
|
* sourceConfigDir: string, // dirname absoluto do opus.config.ts
|
|
26
26
|
* output: string, // pasta de saída declarada no config
|
|
27
|
-
* domains: SerializedDomain[]
|
|
27
|
+
* domains: SerializedDomain[],
|
|
28
|
+
* seeds: SerializedSeed[]
|
|
28
29
|
* }
|
|
29
30
|
*
|
|
30
31
|
* SerializedDomain:
|
|
@@ -87,6 +88,12 @@ async function main() {
|
|
|
87
88
|
).href
|
|
88
89
|
const { flattenDomain } = await import(opusCoreUrl)
|
|
89
90
|
|
|
91
|
+
const opusSeedUrl = pathToFileURL(
|
|
92
|
+
path.join(PACKAGE_ROOT, 'src', 'seed', 'index.ts'),
|
|
93
|
+
).href
|
|
94
|
+
const { checkSeedRegistry, isSeedDefinition, publicSeedDefinition } =
|
|
95
|
+
await import(opusSeedUrl)
|
|
96
|
+
|
|
90
97
|
// Acessar logical type meta dos dicts.
|
|
91
98
|
const opusSchemaUrl = pathToFileURL(
|
|
92
99
|
path.join(PACKAGE_ROOT, 'src', 'schema', 'index.ts'),
|
|
@@ -127,17 +134,38 @@ async function main() {
|
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
const domains = cfg.domains.map((d) => serializeDomain(d, ctx))
|
|
137
|
+
if (cfg.seeds !== undefined && !Array.isArray(cfg.seeds)) {
|
|
138
|
+
throw new Error('config.seeds inválido — use um array de declarações registradas')
|
|
139
|
+
}
|
|
140
|
+
const registeredSeeds = cfg.seeds ?? []
|
|
141
|
+
const seedCheck = checkSeedRegistry(registeredSeeds)
|
|
142
|
+
if (!seedCheck.ok) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`config.seeds inválido — ${seedCheck.diagnostics.map((diagnostic) => diagnostic.message).join('; ')}`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
const seeds = registeredSeeds.map((seed) =>
|
|
148
|
+
serializeSeed(seed, isSeedDefinition, publicSeedDefinition),
|
|
149
|
+
)
|
|
130
150
|
|
|
131
151
|
const payload = {
|
|
132
152
|
opusVersion: pkg.version,
|
|
133
153
|
sourceConfigDir: ctx.sourceConfigDir,
|
|
134
154
|
output: outputDir,
|
|
135
155
|
domains,
|
|
156
|
+
seeds,
|
|
136
157
|
}
|
|
137
158
|
|
|
138
159
|
emit({ ok: true, payload })
|
|
139
160
|
}
|
|
140
161
|
|
|
162
|
+
function serializeSeed(seed, isSeedDefinition, publicSeedDefinition) {
|
|
163
|
+
if (!isSeedDefinition(seed)) {
|
|
164
|
+
throw new Error('config.seeds contém uma declaração inválida')
|
|
165
|
+
}
|
|
166
|
+
return publicSeedDefinition(seed)
|
|
167
|
+
}
|
|
168
|
+
|
|
141
169
|
// =============================================================================
|
|
142
170
|
// Serialização
|
|
143
171
|
// =============================================================================
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Carrega seeds TypeScript do consumidor num subprocesso isolado via tsx. */
|
|
3
|
+
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
assertSeedExecutionAllowed,
|
|
9
|
+
assertSeedExpectedMetrics,
|
|
10
|
+
checkSeedRegistry,
|
|
11
|
+
publicSeedDefinition,
|
|
12
|
+
resolveSeedProfile,
|
|
13
|
+
validateSeedPlan,
|
|
14
|
+
validateSeedReport,
|
|
15
|
+
} from '../../src/seed/index.ts'
|
|
16
|
+
|
|
17
|
+
function emit(value) {
|
|
18
|
+
process.stdout.write(`\n${JSON.stringify(value)}\n`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function publicDefinition(seed) {
|
|
22
|
+
return publicSeedDefinition(seed)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function main() {
|
|
26
|
+
const [configArg, command, name, requestedProfile, requestedScope] = process.argv.slice(2)
|
|
27
|
+
if (typeof configArg !== 'string') throw new Error('config path ausente')
|
|
28
|
+
|
|
29
|
+
const module = await import(pathToFileURL(path.resolve(configArg)).href)
|
|
30
|
+
const config = module.default ?? module.config ?? module
|
|
31
|
+
const seeds = config.seeds
|
|
32
|
+
if (!Array.isArray(seeds) || seeds.length === 0) {
|
|
33
|
+
throw new Error('nenhum seed registrado em config.seeds')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const check = checkSeedRegistry(seeds)
|
|
37
|
+
const targetDiagnostics =
|
|
38
|
+
typeof config.assertSeedTarget === 'function'
|
|
39
|
+
? []
|
|
40
|
+
: [
|
|
41
|
+
{
|
|
42
|
+
code: 'seed.missing_target_assertion',
|
|
43
|
+
seed: 'opus.config.ts',
|
|
44
|
+
message:
|
|
45
|
+
'opus.config.ts precisa de assertSeedTarget para atestar a conexão antes dos bindings',
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
if (command === 'check') {
|
|
49
|
+
emit({
|
|
50
|
+
ok: check.ok && targetDiagnostics.length === 0,
|
|
51
|
+
diagnostics: [...check.diagnostics, ...targetDiagnostics],
|
|
52
|
+
seeds: seeds.length,
|
|
53
|
+
})
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
if (!check.ok) {
|
|
57
|
+
emit({ ok: false, diagnostics: check.diagnostics })
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
if (command === 'list') {
|
|
61
|
+
emit({ ok: true, seeds: check.ordered.map(publicDefinition) })
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
if (targetDiagnostics.length > 0) {
|
|
65
|
+
emit({ ok: false, diagnostics: targetDiagnostics })
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
if (command !== 'plan' && command !== 'apply' && command !== 'verify') {
|
|
69
|
+
throw new Error(`operação de seed desconhecida: ${String(command)}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const seed = check.ordered.find((candidate) => candidate.name === name)
|
|
73
|
+
if (seed === undefined) {
|
|
74
|
+
throw new Error(`seed ${String(name)} não encontrado`)
|
|
75
|
+
}
|
|
76
|
+
const profile = resolveSeedProfile(seed, requestedProfile || undefined)
|
|
77
|
+
const scope = requestedScope || undefined
|
|
78
|
+
const executionSeeds = dependencyClosure(check.ordered, seed)
|
|
79
|
+
const selections = executionSeeds.map((candidate) => ({
|
|
80
|
+
seed: candidate,
|
|
81
|
+
profile: candidate.name === seed.name ? profile : resolveSeedProfile(candidate),
|
|
82
|
+
}))
|
|
83
|
+
for (const selection of selections) assertSeedExecutionAllowed(selection.seed, scope)
|
|
84
|
+
|
|
85
|
+
const database = typeof config.database === 'function' ? await config.database() : null
|
|
86
|
+
try {
|
|
87
|
+
const targetAttested = await config.assertSeedTarget({ database, scope })
|
|
88
|
+
if (targetAttested !== true) {
|
|
89
|
+
throw new Error('assertSeedTarget precisa retornar true após atestar o destino real')
|
|
90
|
+
}
|
|
91
|
+
if (command === 'plan') {
|
|
92
|
+
const steps = []
|
|
93
|
+
for (const selection of selections) {
|
|
94
|
+
const plan = validateSeedPlan(await selection.seed.binding.plan({
|
|
95
|
+
database,
|
|
96
|
+
profile: selection.profile,
|
|
97
|
+
scope,
|
|
98
|
+
}))
|
|
99
|
+
steps.push({ seed: publicDefinition(selection.seed), profile: selection.profile, plan })
|
|
100
|
+
}
|
|
101
|
+
emit({
|
|
102
|
+
ok: true,
|
|
103
|
+
command,
|
|
104
|
+
seed: publicDefinition(seed),
|
|
105
|
+
profile,
|
|
106
|
+
scope,
|
|
107
|
+
plan: steps.at(-1).plan,
|
|
108
|
+
steps,
|
|
109
|
+
})
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
const steps = []
|
|
113
|
+
for (const selection of selections) {
|
|
114
|
+
const report = validateSeedReport(await selection.seed.binding[command]({
|
|
115
|
+
database,
|
|
116
|
+
profile: selection.profile,
|
|
117
|
+
scope,
|
|
118
|
+
}))
|
|
119
|
+
if (command === 'verify') {
|
|
120
|
+
assertSeedExpectedMetrics(selection.seed, selection.profile, report)
|
|
121
|
+
}
|
|
122
|
+
steps.push({ seed: publicDefinition(selection.seed), profile: selection.profile, report })
|
|
123
|
+
}
|
|
124
|
+
emit({
|
|
125
|
+
ok: true,
|
|
126
|
+
command,
|
|
127
|
+
seed: publicDefinition(seed),
|
|
128
|
+
profile,
|
|
129
|
+
scope,
|
|
130
|
+
report: steps.at(-1).report,
|
|
131
|
+
steps,
|
|
132
|
+
})
|
|
133
|
+
} finally {
|
|
134
|
+
if (database !== null && typeof database.destroy === 'function') await database.destroy()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function dependencyClosure(ordered, target) {
|
|
139
|
+
const required = new Set()
|
|
140
|
+
const byName = new Map(ordered.map((seed) => [seed.name, seed]))
|
|
141
|
+
const collect = (seed) => {
|
|
142
|
+
if (required.has(seed.name)) return
|
|
143
|
+
for (const dependency of seed.dependsOn ?? []) collect(byName.get(dependency))
|
|
144
|
+
required.add(seed.name)
|
|
145
|
+
}
|
|
146
|
+
collect(target)
|
|
147
|
+
return ordered.filter((seed) => required.has(seed.name))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
main().catch((error) => {
|
|
151
|
+
emit({ ok: false, error: error instanceof Error ? error.message : String(error) })
|
|
152
|
+
})
|
package/bin/lib/seed.mjs
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/** Grupo `opus seed`: descoberta, gate e execução segura dos seeds do projeto. */
|
|
2
|
+
|
|
3
|
+
import { execFile } from 'node:child_process'
|
|
4
|
+
import { promises as fs } from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { promisify } from 'node:util'
|
|
8
|
+
|
|
9
|
+
import { canonicalProjectDirectory, readProjectFile, safeProjectPath } from '@softize/base/project-path'
|
|
10
|
+
|
|
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, '..', '..')
|
|
15
|
+
|
|
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
|
+
}
|
|
28
|
+
|
|
29
|
+
export function findAdHocSeedScripts(scripts = {}) {
|
|
30
|
+
return Object.entries(scripts).flatMap(([name, command]) => {
|
|
31
|
+
const isSeedName = /^seed(?::|$)/.test(name) || /^db:seed(?::|$)/.test(name)
|
|
32
|
+
const hasShellComposition =
|
|
33
|
+
typeof command === 'string' && /(?:&&|\|\||[;&|<>`\n\r]|\$\()/.test(command)
|
|
34
|
+
const delegatesToOpus =
|
|
35
|
+
typeof command === 'string' &&
|
|
36
|
+
!hasShellComposition &&
|
|
37
|
+
/^(?:npx |pnpm exec )?opus seed (?:list|check|plan|apply|verify)(?:\s|$)/.test(command.trim())
|
|
38
|
+
return isSeedName && !delegatesToOpus ? [{ name, command }] : []
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function cmdSeed(rest, flags) {
|
|
43
|
+
const [command, name] = rest
|
|
44
|
+
if (flags.help || command === undefined || command === 'help') {
|
|
45
|
+
helpSeed()
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
if (!['list', 'check', 'plan', 'apply', 'verify'].includes(command)) {
|
|
49
|
+
fail(flags, `Subcomando seed desconhecido: ${command}`)
|
|
50
|
+
if (!flags.json) helpSeed()
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (['plan', 'apply', 'verify'].includes(command) && typeof name !== 'string') {
|
|
54
|
+
fail(flags, `Uso: opus seed ${command} <nome> --scope <escopo>`)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let resolved
|
|
59
|
+
try {
|
|
60
|
+
resolved = resolveConfig(flags)
|
|
61
|
+
} catch (error) {
|
|
62
|
+
fail(flags, error instanceof Error ? error.message : String(error))
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
const { cwd, configPath } = resolved
|
|
66
|
+
const result = await runRunner(configPath, [
|
|
67
|
+
command,
|
|
68
|
+
name ?? '',
|
|
69
|
+
flags.profile ?? '',
|
|
70
|
+
flags.scope ?? process.env.OPUS_SEED_SCOPE ?? '',
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
if (command === 'check') {
|
|
74
|
+
const scriptFindings = readSeedScriptFindings(cwd)
|
|
75
|
+
const runnerFindings = result.error === undefined ? [] : [{
|
|
76
|
+
code: 'seed.invalid_registry',
|
|
77
|
+
seed: 'opus.config.ts',
|
|
78
|
+
message: result.error,
|
|
79
|
+
}]
|
|
80
|
+
const diagnostics = [...(result.diagnostics ?? []), ...runnerFindings, ...scriptFindings]
|
|
81
|
+
const checked = { ...result, ok: result.ok === true && diagnostics.length === 0, diagnostics }
|
|
82
|
+
if (flags.json) {
|
|
83
|
+
console.log(JSON.stringify(checked, null, 2))
|
|
84
|
+
} else if (checked.ok) {
|
|
85
|
+
log('success', `✓ seed check: ${checked.seeds} seed(s) estruturado(s); registro consistente.`)
|
|
86
|
+
} else {
|
|
87
|
+
printDiagnostics(diagnostics)
|
|
88
|
+
log('error', `✗ seed check: ${diagnostics.length} violação(ões).`)
|
|
89
|
+
}
|
|
90
|
+
if (!checked.ok) process.exitCode = 1
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (result.ok !== true) {
|
|
95
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2))
|
|
96
|
+
else {
|
|
97
|
+
printDiagnostics(result.diagnostics ?? [])
|
|
98
|
+
log('error', `✗ opus seed ${command}: ${result.error ?? 'registro inconsistente'}`)
|
|
99
|
+
}
|
|
100
|
+
process.exitCode = 1
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (flags.json) {
|
|
105
|
+
console.log(JSON.stringify(result, null, 2))
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (command === 'list') printList(result.seeds)
|
|
109
|
+
else if (command === 'plan') printPlan(result)
|
|
110
|
+
else printReport(result)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function fail(flags, message) {
|
|
114
|
+
if (flags.json) console.log(JSON.stringify({ ok: false, error: message }, null, 2))
|
|
115
|
+
else log('error', `✗ opus seed: ${message}`)
|
|
116
|
+
process.exitCode = 1
|
|
117
|
+
}
|
|
118
|
+
|
|
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
|
+
function readSeedScriptFindings(cwd) {
|
|
128
|
+
const packageFile = readProjectFile(cwd, 'package.json', { allowMissing: true })
|
|
129
|
+
if (!packageFile.exists) return []
|
|
130
|
+
let parsed
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(packageFile.content)
|
|
133
|
+
} catch {
|
|
134
|
+
return [{ code: 'seed.invalid_package', seed: 'package.json', message: 'package.json não é JSON válido' }]
|
|
135
|
+
}
|
|
136
|
+
return findAdHocSeedScripts(parsed.scripts).map(({ name, command }) => ({
|
|
137
|
+
code: 'seed.ad_hoc_script',
|
|
138
|
+
seed: name,
|
|
139
|
+
message: `Script ${name} não delega para opus seed: ${String(command)}`,
|
|
140
|
+
}))
|
|
141
|
+
}
|
|
142
|
+
|
|
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
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function printList(seeds) {
|
|
162
|
+
log('info', `\nSeeds estruturados (${seeds.length}):`)
|
|
163
|
+
for (const seed of seeds) {
|
|
164
|
+
console.log(` ${seed.name}@${seed.version} — ${seed.description}`)
|
|
165
|
+
console.log(` perfis: ${Object.keys(seed.profiles).join(', ')}; default: ${seed.defaultProfile}`)
|
|
166
|
+
console.log(` escopos: ${seed.safety.scopes.join(', ')}`)
|
|
167
|
+
}
|
|
168
|
+
console.log('')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function printPlan(result) {
|
|
172
|
+
log('info', `→ ${result.seed.name}@${result.seed.version} [${result.profile}] em ${result.scope}`)
|
|
173
|
+
for (const step of result.steps) {
|
|
174
|
+
console.log(` ${step.seed.name} [${step.profile}] — ${step.plan.summary}`)
|
|
175
|
+
for (const operation of step.plan.operations) console.log(` - ${operation}`)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function printReport(result) {
|
|
180
|
+
log('success', `✓ seed ${result.command}: ${result.seed.name}@${result.seed.version} [${result.profile}]`)
|
|
181
|
+
for (const step of result.steps) {
|
|
182
|
+
console.log(` ${step.seed.name} [${step.profile}] — ${step.report.summary}`)
|
|
183
|
+
for (const [metric, value] of Object.entries(step.report.metrics)) console.log(` ${metric}: ${value}`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function printDiagnostics(diagnostics) {
|
|
188
|
+
for (const diagnostic of diagnostics) log('warn', `[${diagnostic.code}] ${diagnostic.message}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
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
|
+
export function helpSeed() {
|
|
210
|
+
console.log(`
|
|
211
|
+
@softize/opus seed — datasets estruturados do projeto
|
|
212
|
+
|
|
213
|
+
seed list Lista seeds, perfis, versões e escopos sem abrir o banco.
|
|
214
|
+
seed check Valida bindings, dependências, ciclos e scripts paralelos.
|
|
215
|
+
seed plan <nome> Mostra as operações previstas sem escrever.
|
|
216
|
+
seed apply <nome> Aplica o perfil de forma convergente.
|
|
217
|
+
seed verify <nome> Confere métricas e invariantes do perfil.
|
|
218
|
+
|
|
219
|
+
Flags:
|
|
220
|
+
--config <path> Caminho interno ao projeto para opus.config.ts.
|
|
221
|
+
--profile <nome> Perfil; usa defaultProfile quando omitido.
|
|
222
|
+
--scope <escopo> Escopo explícito dos dados. Alternativa: OPUS_SEED_SCOPE.
|
|
223
|
+
--json Retorna o resultado estruturado.
|
|
224
|
+
|
|
225
|
+
\`plan\`, \`apply\` e \`verify\` só abrem conexão com NODE_ENV=development ou NODE_ENV=test.
|
|
226
|
+
O contrato não possui reset nem truncate; aplicação repetida precisa convergir para o mesmo estado
|
|
227
|
+
observável.
|
|
228
|
+
`)
|
|
229
|
+
}
|