@softize/opus 8.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1616 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/cli.mjs +528 -0
- package/bin/lib/check.mjs +307 -0
- package/bin/lib/components.mjs +151 -0
- package/bin/lib/create.mjs +208 -0
- package/bin/lib/db-check-runner.mjs +86 -0
- package/bin/lib/db-migrate-runner.mjs +89 -0
- package/bin/lib/db-scaffold-runner.mjs +84 -0
- package/bin/lib/db.mjs +261 -0
- package/bin/lib/docs-include.mjs +48 -0
- package/bin/lib/gen-dicts.mjs +134 -0
- package/bin/lib/gen-docs.mjs +288 -0
- package/bin/lib/gen-manifest.mjs +102 -0
- package/bin/lib/gen-openapi.mjs +195 -0
- package/bin/lib/gen-runner.mjs +472 -0
- package/bin/lib/gen-stubs.mjs +463 -0
- package/bin/lib/gen.mjs +311 -0
- package/bin/lib/init.mjs +514 -0
- package/bin/lib/introspect.mjs +107 -0
- package/bin/lib/mcp.mjs +85 -0
- package/bin/lib/postinstall.mjs +56 -0
- package/docs/chat-event-protocol.md +85 -0
- package/docs/code-style.md +16 -0
- package/docs/data-layer.md +246 -0
- package/docs/ownership-vs-shadcn-lock.md +102 -0
- package/docs/protocol.md +2053 -0
- package/docs/releasing.md +110 -0
- package/docs/shellnav.md +131 -0
- package/package.json +338 -0
- package/registry/hooks/hooks.json +26 -0
- package/registry/hooks/link-memory-on-start.mjs +46 -0
- package/registry/hooks/opus-check-on-stop.mjs +114 -0
- package/registry/skills/create-action/SKILL.md +49 -0
- package/registry/skills/create-action/scaffold.mjs +122 -0
- package/registry/templates/app/_gitignore +3 -0
- package/registry/templates/app/_npmrc +1 -0
- package/registry/templates/app/_opus/_gitignore +5 -0
- package/registry/templates/app/_prettierrc.json +6 -0
- package/registry/templates/app/index.html +13 -0
- package/registry/templates/app/opus.config.ts +16 -0
- package/registry/templates/app/package.json +43 -0
- package/registry/templates/app/pnpm-workspace.yaml +11 -0
- package/registry/templates/app/public/favicon.svg +4 -0
- package/registry/templates/app/src/App.tsx +37 -0
- package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
- package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
- package/registry/templates/app/src/domains/tasks/index.ts +13 -0
- package/registry/templates/app/src/index.css +18 -0
- package/registry/templates/app/src/main.tsx +25 -0
- package/registry/templates/app/tsconfig.json +20 -0
- package/registry/templates/app/vite.config.ts +46 -0
- package/registry/templates/monorepo/_gitignore +3 -0
- package/registry/templates/monorepo/_npmrc +1 -0
- package/registry/templates/monorepo/package.json +9 -0
- package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
- package/src/ai/ask.ts +64 -0
- package/src/ai/drivers/anthropic.ts +309 -0
- package/src/ai/index.ts +17 -0
- package/src/audit/drivers/console.ts +117 -0
- package/src/audit/drivers/pg.ts +172 -0
- package/src/audit/index.ts +51 -0
- package/src/auth/drivers/better-auth.ts +103 -0
- package/src/auth/drivers/jwt.ts +188 -0
- package/src/auth/index.ts +9 -0
- package/src/client/drivers/fetch.ts +202 -0
- package/src/client/index.ts +22 -0
- package/src/core/actions.ts +110 -0
- package/src/core/audit.ts +239 -0
- package/src/core/contracts.ts +137 -0
- package/src/core/domain.ts +310 -0
- package/src/core/errors.ts +181 -0
- package/src/core/index.ts +174 -0
- package/src/core/logical-type.ts +31 -0
- package/src/core/reactions.ts +81 -0
- package/src/core/runtime.ts +1167 -0
- package/src/core/schedules.ts +41 -0
- package/src/core/types.ts +1356 -0
- package/src/data/drivers/kysely.ts +389 -0
- package/src/data/index.ts +10 -0
- package/src/data/readonly-pool.ts +160 -0
- package/src/dsl/eval.ts +136 -0
- package/src/dsl/index.ts +29 -0
- package/src/dsl/kysely.ts +230 -0
- package/src/dsl/loads.ts +123 -0
- package/src/dsl/parser.ts +423 -0
- package/src/dsl/types.ts +113 -0
- package/src/events/drivers/mitt.ts +70 -0
- package/src/events/index.ts +9 -0
- package/src/log/drivers/pino.ts +57 -0
- package/src/log/index.ts +9 -0
- package/src/mcp/index.ts +62 -0
- package/src/queue/drivers/bullmq.ts +190 -0
- package/src/queue/index.ts +9 -0
- package/src/scheduler/drivers/node-cron.ts +93 -0
- package/src/scheduler/every.ts +45 -0
- package/src/scheduler/index.ts +9 -0
- package/src/schema/drivers/zod.ts +765 -0
- package/src/schema/entity.ts +439 -0
- package/src/schema/format/locale.ts +144 -0
- package/src/schema/index.ts +65 -0
- package/src/schema/openapi.ts +302 -0
- package/src/schema/scaffold.ts +160 -0
- package/src/server/drivers/fastify.ts +224 -0
- package/src/server/drivers/node.ts +386 -0
- package/src/server/index.ts +142 -0
- package/src/storage/drivers/fs.ts +90 -0
- package/src/storage/drivers/s3.ts +117 -0
- package/src/storage/index.ts +27 -0
- package/src/testing/fake.ts +298 -0
- package/src/testing/index.ts +324 -0
- package/src/ui/components/patterns/action-form-card.tsx +48 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
- package/src/ui/components/patterns/app-shell.tsx +227 -0
- package/src/ui/components/patterns/confirm.tsx +226 -0
- package/src/ui/components/patterns/data-state.tsx +75 -0
- package/src/ui/components/patterns/form-dialog.tsx +64 -0
- package/src/ui/components/patterns/form.tsx +584 -0
- package/src/ui/components/patterns/list.tsx +1488 -0
- package/src/ui/components/patterns/page.tsx +46 -0
- package/src/ui/components/patterns/section-shell.tsx +246 -0
- package/src/ui/components/patterns/shell-nav.tsx +150 -0
- package/src/ui/components/patterns/sidebar.tsx +89 -0
- package/src/ui/components/patterns/split.tsx +93 -0
- package/src/ui/components/patterns/trigger.tsx +196 -0
- package/src/ui/components/patterns/view.tsx +84 -0
- package/src/ui/components/primitives/accordion.tsx +64 -0
- package/src/ui/components/primitives/alert-dialog.tsx +190 -0
- package/src/ui/components/primitives/alert.tsx +116 -0
- package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
- package/src/ui/components/primitives/avatar.tsx +107 -0
- package/src/ui/components/primitives/badge.tsx +37 -0
- package/src/ui/components/primitives/breadcrumb.tsx +109 -0
- package/src/ui/components/primitives/button-group.tsx +83 -0
- package/src/ui/components/primitives/button.tsx +102 -0
- package/src/ui/components/primitives/calendar.tsx +218 -0
- package/src/ui/components/primitives/card.tsx +56 -0
- package/src/ui/components/primitives/carousel.tsx +239 -0
- package/src/ui/components/primitives/chat.tsx +407 -0
- package/src/ui/components/primitives/checkbox.tsx +30 -0
- package/src/ui/components/primitives/collapsible.tsx +31 -0
- package/src/ui/components/primitives/command.tsx +182 -0
- package/src/ui/components/primitives/composer.tsx +121 -0
- package/src/ui/components/primitives/copyable.tsx +50 -0
- package/src/ui/components/primitives/dialog.tsx +147 -0
- package/src/ui/components/primitives/drawer.tsx +141 -0
- package/src/ui/components/primitives/empty.tsx +104 -0
- package/src/ui/components/primitives/field.tsx +246 -0
- package/src/ui/components/primitives/icon-picker.tsx +180 -0
- package/src/ui/components/primitives/input-group.tsx +168 -0
- package/src/ui/components/primitives/input-otp.tsx +75 -0
- package/src/ui/components/primitives/input.tsx +72 -0
- package/src/ui/components/primitives/item.tsx +193 -0
- package/src/ui/components/primitives/kbd.tsx +28 -0
- package/src/ui/components/primitives/label.tsx +22 -0
- package/src/ui/components/primitives/markdown.tsx +35 -0
- package/src/ui/components/primitives/menu.tsx +255 -0
- package/src/ui/components/primitives/pagination.tsx +127 -0
- package/src/ui/components/primitives/popover.tsx +87 -0
- package/src/ui/components/primitives/progress.tsx +29 -0
- package/src/ui/components/primitives/radio-group.tsx +43 -0
- package/src/ui/components/primitives/resizable.tsx +51 -0
- package/src/ui/components/primitives/scroll-area.tsx +56 -0
- package/src/ui/components/primitives/select.tsx +479 -0
- package/src/ui/components/primitives/separator.tsx +26 -0
- package/src/ui/components/primitives/skeleton.tsx +13 -0
- package/src/ui/components/primitives/slider.tsx +61 -0
- package/src/ui/components/primitives/sonner.tsx +46 -0
- package/src/ui/components/primitives/spinner.tsx +29 -0
- package/src/ui/components/primitives/switch.tsx +33 -0
- package/src/ui/components/primitives/table.tsx +114 -0
- package/src/ui/components/primitives/tabs.tsx +104 -0
- package/src/ui/components/primitives/textarea.tsx +18 -0
- package/src/ui/components/primitives/toggle-group.tsx +81 -0
- package/src/ui/components/primitives/toggle.tsx +45 -0
- package/src/ui/components/primitives/tooltip.tsx +55 -0
- package/src/ui/components/primitives/truncate.tsx +49 -0
- package/src/ui/docs/DocBrowser.tsx +90 -0
- package/src/ui/docs/changelog.tsx +80 -0
- package/src/ui/docs/content/accordion.md +86 -0
- package/src/ui/docs/content/action-form-card.md +24 -0
- package/src/ui/docs/content/action-form-dialog.md +30 -0
- package/src/ui/docs/content/action-form.md +125 -0
- package/src/ui/docs/content/action-list-dialog.md +68 -0
- package/src/ui/docs/content/action-list.md +194 -0
- package/src/ui/docs/content/action-trigger.md +72 -0
- package/src/ui/docs/content/action-view.md +47 -0
- package/src/ui/docs/content/actions.md +138 -0
- package/src/ui/docs/content/ai.md +112 -0
- package/src/ui/docs/content/alert-dialog.md +73 -0
- package/src/ui/docs/content/alert.md +69 -0
- package/src/ui/docs/content/app-shell.md +155 -0
- package/src/ui/docs/content/aspect-ratio.md +66 -0
- package/src/ui/docs/content/audit.md +84 -0
- package/src/ui/docs/content/auth.md +70 -0
- package/src/ui/docs/content/avatar.md +94 -0
- package/src/ui/docs/content/badge.md +48 -0
- package/src/ui/docs/content/breadcrumb.md +87 -0
- package/src/ui/docs/content/button-group.md +71 -0
- package/src/ui/docs/content/button.md +60 -0
- package/src/ui/docs/content/calendar.md +62 -0
- package/src/ui/docs/content/card.md +49 -0
- package/src/ui/docs/content/carousel.md +85 -0
- package/src/ui/docs/content/chat.md +69 -0
- package/src/ui/docs/content/checkbox.md +75 -0
- package/src/ui/docs/content/cli.md +58 -0
- package/src/ui/docs/content/collapsible.md +64 -0
- package/src/ui/docs/content/command.md +56 -0
- package/src/ui/docs/content/composer.md +50 -0
- package/src/ui/docs/content/confirm.md +120 -0
- package/src/ui/docs/content/copyable.md +30 -0
- package/src/ui/docs/content/customization.md +110 -0
- package/src/ui/docs/content/cycle.md +34 -0
- package/src/ui/docs/content/data-state.md +47 -0
- package/src/ui/docs/content/data.md +99 -0
- package/src/ui/docs/content/dialog.md +60 -0
- package/src/ui/docs/content/drawer.md +55 -0
- package/src/ui/docs/content/empty.md +66 -0
- package/src/ui/docs/content/events.md +61 -0
- package/src/ui/docs/content/field.md +58 -0
- package/src/ui/docs/content/getting-started.md +109 -0
- package/src/ui/docs/content/icon-picker.md +51 -0
- package/src/ui/docs/content/input-group.md +78 -0
- package/src/ui/docs/content/input-otp.md +72 -0
- package/src/ui/docs/content/input.md +78 -0
- package/src/ui/docs/content/item.md +84 -0
- package/src/ui/docs/content/kbd.md +62 -0
- package/src/ui/docs/content/label.md +32 -0
- package/src/ui/docs/content/log.md +55 -0
- package/src/ui/docs/content/markdown.md +41 -0
- package/src/ui/docs/content/mcp.md +44 -0
- package/src/ui/docs/content/menu.md +114 -0
- package/src/ui/docs/content/microcopy.md +83 -0
- package/src/ui/docs/content/page.md +34 -0
- package/src/ui/docs/content/pagination.md +99 -0
- package/src/ui/docs/content/popover.md +49 -0
- package/src/ui/docs/content/progress.md +69 -0
- package/src/ui/docs/content/queue.md +62 -0
- package/src/ui/docs/content/radio-group.md +77 -0
- package/src/ui/docs/content/resizable.md +86 -0
- package/src/ui/docs/content/router.md +56 -0
- package/src/ui/docs/content/runtime.md +77 -0
- package/src/ui/docs/content/scheduler.md +66 -0
- package/src/ui/docs/content/scroll-area.md +89 -0
- package/src/ui/docs/content/section-shell.md +121 -0
- package/src/ui/docs/content/select.md +342 -0
- package/src/ui/docs/content/separator.md +33 -0
- package/src/ui/docs/content/sidebar.md +38 -0
- package/src/ui/docs/content/skeleton.md +34 -0
- package/src/ui/docs/content/slider.md +64 -0
- package/src/ui/docs/content/spinner.md +37 -0
- package/src/ui/docs/content/split.md +33 -0
- package/src/ui/docs/content/storage.md +69 -0
- package/src/ui/docs/content/switch.md +69 -0
- package/src/ui/docs/content/table.md +102 -0
- package/src/ui/docs/content/tabs.md +94 -0
- package/src/ui/docs/content/testing.md +89 -0
- package/src/ui/docs/content/textarea.md +30 -0
- package/src/ui/docs/content/toast.md +67 -0
- package/src/ui/docs/content/toggle-group.md +81 -0
- package/src/ui/docs/content/toggle.md +72 -0
- package/src/ui/docs/content/tokens.md +171 -0
- package/src/ui/docs/content/tooltip.md +50 -0
- package/src/ui/docs/content/truncate.md +37 -0
- package/src/ui/docs/content/ui.md +40 -0
- package/src/ui/docs/content/upgrading.md +48 -0
- package/src/ui/docs/doc-client.tsx +214 -0
- package/src/ui/docs/doc.tsx +301 -0
- package/src/ui/docs/folder.tsx +149 -0
- package/src/ui/docs/index.ts +21 -0
- package/src/ui/docs/markdown.tsx +130 -0
- package/src/ui/docs/md-raw.d.ts +4 -0
- package/src/ui/docs/plugin.ts +104 -0
- package/src/ui/docs/registry.tsx +424 -0
- package/src/ui/docs/standalone.tsx +107 -0
- package/src/ui/drivers/react.tsx +627 -0
- package/src/ui/index.ts +92 -0
- package/src/ui/lib/cn.ts +10 -0
- package/src/ui/lib/zod-pt-br.ts +38 -0
- package/src/ui/meta.ts +412 -0
- package/src/ui/react.tsx +235 -0
- package/src/ui/router.ts +96 -0
- package/src/ui/theme.css +234 -0
- package/src/vite/design.ts +652 -0
- package/src/vite/index.ts +8 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* db-check-runner — subprocess via `tsx` que roda o drift-check.
|
|
4
|
+
*
|
|
5
|
+
* Por que subprocess: o CLI principal é `.mjs` puro (Node sem loader TS) e o
|
|
6
|
+
* drift-check precisa (a) importar TS do consumer (`opus.config.ts` + entidades)
|
|
7
|
+
* e (b) abrir conexão viva com o banco. Tudo isso roda aqui dentro do tsx.
|
|
8
|
+
*
|
|
9
|
+
* Contrato do `opus.config.ts` pros comandos `db`:
|
|
10
|
+
* - `entities?: EntityConfig[]` — entidades explícitas (opcional)
|
|
11
|
+
* - `domains?: DomainConfig[]` — entidades coletadas de `domain.models`
|
|
12
|
+
* - `database: () => Kysely | Promise<Kysely>` — factory LAZY do banco.
|
|
13
|
+
* É factory (não instância) de propósito: o `gen` importa o mesmo config
|
|
14
|
+
* sem nunca chamar `database()`, então não abre conexão.
|
|
15
|
+
*
|
|
16
|
+
* Protocolo de saída (igual gen-runner): última linha de stdout é um JSON
|
|
17
|
+
* `{ ok: true, findings }` ou `{ ok: false, error }`. Resto é debug.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
import { pathToFileURL } from 'node:url'
|
|
22
|
+
|
|
23
|
+
import { isEntityConfig } from '../../src/schema/entity.ts'
|
|
24
|
+
import { kyselyDriftCheck } from '../../src/data/drivers/kysely.ts'
|
|
25
|
+
|
|
26
|
+
function emit(obj) {
|
|
27
|
+
process.stdout.write(`\n${JSON.stringify(obj)}\n`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function collectEntities(domain, out) {
|
|
31
|
+
if (domain === null || typeof domain !== 'object') return
|
|
32
|
+
const models = domain.models
|
|
33
|
+
if (models !== undefined && models !== null && typeof models === 'object') {
|
|
34
|
+
for (const value of Object.values(models)) {
|
|
35
|
+
if (isEntityConfig(value)) out.push(value)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(domain.subdomains)) {
|
|
39
|
+
for (const sub of domain.subdomains) collectEntities(sub, out)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function main() {
|
|
44
|
+
const configPath = process.argv[2]
|
|
45
|
+
if (typeof configPath !== 'string') {
|
|
46
|
+
emit({ ok: false, error: 'config path ausente' })
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const mod = await import(pathToFileURL(path.resolve(configPath)).href)
|
|
51
|
+
const config = mod.default ?? mod
|
|
52
|
+
|
|
53
|
+
const entities = []
|
|
54
|
+
if (Array.isArray(config.entities)) {
|
|
55
|
+
for (const e of config.entities) if (isEntityConfig(e)) entities.push(e)
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(config.domains)) {
|
|
58
|
+
for (const d of config.domains) collectEntities(d, entities)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (entities.length === 0) {
|
|
62
|
+
emit({ ok: false, error: 'nenhuma entidade encontrada (config.entities ou domain.models)' })
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof config.database !== 'function') {
|
|
67
|
+
emit({
|
|
68
|
+
ok: false,
|
|
69
|
+
error:
|
|
70
|
+
'config.database() ausente — exporte uma factory lazy que retorna um Kysely (ex: `database: () => new Kysely({...})`)',
|
|
71
|
+
})
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const db = await config.database()
|
|
76
|
+
try {
|
|
77
|
+
const findings = await kyselyDriftCheck(db, entities, { naming: config.naming })
|
|
78
|
+
emit({ ok: true, findings })
|
|
79
|
+
} finally {
|
|
80
|
+
if (db !== null && typeof db.destroy === 'function') await db.destroy()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
main().catch((err) => {
|
|
85
|
+
emit({ ok: false, error: err instanceof Error ? err.message : String(err) })
|
|
86
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* db-migrate-runner — subprocess via `tsx` que aplica o SCHEMA IDEMPOTENTE.
|
|
4
|
+
*
|
|
5
|
+
* O padrão da casa não é migration versionada: é UM script SQL evolutivo
|
|
6
|
+
* (config.schema, ex. `src/db/schema.sql`) re-rodável — CREATE IF NOT EXISTS +
|
|
7
|
+
* guards `DO $$ IF EXISTS` cobrem nascer do zero E upgrade de prod no mesmo
|
|
8
|
+
* artefato. Depois de aplicar, roda o drift-check (entidade ↔ banco) na mesma
|
|
9
|
+
* conexão: migrar e continuar divergente é estado quebrado que o deploy precisa
|
|
10
|
+
* ver na hora. (A era Kysely Migrator foi aposentada — o formato .ts de
|
|
11
|
+
* migration gerava DDL camelCase errado e ninguém o usava.)
|
|
12
|
+
*
|
|
13
|
+
* argv: [config path]
|
|
14
|
+
* Saída: última linha JSON `{ ok, schema?, findings?, error? }` (resto é debug).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
import { promises as fs } from 'node:fs'
|
|
19
|
+
import { pathToFileURL } from 'node:url'
|
|
20
|
+
import { sql } from 'kysely'
|
|
21
|
+
|
|
22
|
+
import { isEntityConfig } from '../../src/schema/entity.ts'
|
|
23
|
+
import { kyselyDriftCheck } from '../../src/data/drivers/kysely.ts'
|
|
24
|
+
|
|
25
|
+
function emit(obj) {
|
|
26
|
+
process.stdout.write(`\n${JSON.stringify(obj)}\n`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function collectEntities(domain, out) {
|
|
30
|
+
if (domain === null || typeof domain !== 'object') return
|
|
31
|
+
const models = domain.models
|
|
32
|
+
if (models !== undefined && models !== null && typeof models === 'object') {
|
|
33
|
+
for (const value of Object.values(models)) if (isEntityConfig(value)) out.push(value)
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(domain.subdomains)) for (const sub of domain.subdomains) collectEntities(sub, out)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
const configPath = process.argv[2]
|
|
40
|
+
if (typeof configPath !== 'string') {
|
|
41
|
+
emit({ ok: false, error: 'config path ausente' })
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const mod = await import(pathToFileURL(path.resolve(configPath)).href)
|
|
46
|
+
const config = mod.default ?? mod
|
|
47
|
+
|
|
48
|
+
if (typeof config.database !== 'function') {
|
|
49
|
+
emit({ ok: false, error: 'config.database() ausente — exporte uma factory que retorna um Kysely' })
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const configDir = path.dirname(path.resolve(configPath))
|
|
54
|
+
const schemaPath = path.resolve(configDir, config.schema ?? 'db/schema.sql')
|
|
55
|
+
let schemaSql
|
|
56
|
+
try {
|
|
57
|
+
schemaSql = await fs.readFile(schemaPath, 'utf8')
|
|
58
|
+
} catch {
|
|
59
|
+
emit({
|
|
60
|
+
ok: false,
|
|
61
|
+
error: `schema não encontrado em ${schemaPath} — aponte via \`schema\` no opus.config.ts (script SQL idempotente).`,
|
|
62
|
+
})
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Entidades pro drift-check pós-apply (explícitas ou via domains).
|
|
67
|
+
const entities = []
|
|
68
|
+
if (Array.isArray(config.entities)) for (const e of config.entities) if (isEntityConfig(e)) entities.push(e)
|
|
69
|
+
if (Array.isArray(config.domains)) for (const d of config.domains) collectEntities(d, entities)
|
|
70
|
+
|
|
71
|
+
const db = await config.database()
|
|
72
|
+
try {
|
|
73
|
+
// Um shot, sem parâmetros → protocolo simples do pg aceita multi-statement,
|
|
74
|
+
// igual ao pool.query(SQL) que este runner substitui.
|
|
75
|
+
await sql.raw(schemaSql).execute(db)
|
|
76
|
+
const findings = entities.length > 0 ? await kyselyDriftCheck(db, entities) : []
|
|
77
|
+
emit({
|
|
78
|
+
ok: true,
|
|
79
|
+
schema: schemaPath,
|
|
80
|
+
findings: findings.map((f) => ({ table: f.table, kind: f.kind, message: f.message })),
|
|
81
|
+
})
|
|
82
|
+
} finally {
|
|
83
|
+
if (db !== null && typeof db.destroy === 'function') await db.destroy()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main().catch((err) => {
|
|
88
|
+
emit({ ok: false, error: err instanceof Error ? err.message : String(err) })
|
|
89
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* db-scaffold-runner — subprocess via `tsx` que gera um rascunho de migration.
|
|
4
|
+
*
|
|
5
|
+
* Roda o drift-check (precisa de banco) → gera o corpo da migration (puro) →
|
|
6
|
+
* escreve `migrations/<timestamp>_scaffold.ts`. Best-effort; humano revisa.
|
|
7
|
+
*
|
|
8
|
+
* argv: [config path]
|
|
9
|
+
* Saída: última linha JSON `{ ok, file?, statements?, notes?, error? }`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import { promises as fs } from 'node:fs'
|
|
14
|
+
import { pathToFileURL } from 'node:url'
|
|
15
|
+
|
|
16
|
+
import { isEntityConfig } from '../../src/schema/entity.ts'
|
|
17
|
+
import { scaffoldMigration, scaffoldFileContent } from '../../src/schema/scaffold.ts'
|
|
18
|
+
import { kyselyDriftCheck } from '../../src/data/drivers/kysely.ts'
|
|
19
|
+
|
|
20
|
+
function emit(obj) {
|
|
21
|
+
process.stdout.write(`\n${JSON.stringify(obj)}\n`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function collectEntities(domain, out) {
|
|
25
|
+
if (domain === null || typeof domain !== 'object') return
|
|
26
|
+
const models = domain.models
|
|
27
|
+
if (models !== undefined && models !== null && typeof models === 'object') {
|
|
28
|
+
for (const value of Object.values(models)) if (isEntityConfig(value)) out.push(value)
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(domain.subdomains)) for (const sub of domain.subdomains) collectEntities(sub, out)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function timestamp() {
|
|
34
|
+
return new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
const configPath = process.argv[2]
|
|
39
|
+
if (typeof configPath !== 'string') {
|
|
40
|
+
emit({ ok: false, error: 'config path ausente' })
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const mod = await import(pathToFileURL(path.resolve(configPath)).href)
|
|
45
|
+
const config = mod.default ?? mod
|
|
46
|
+
|
|
47
|
+
const entities = []
|
|
48
|
+
if (Array.isArray(config.entities)) for (const e of config.entities) if (isEntityConfig(e)) entities.push(e)
|
|
49
|
+
if (Array.isArray(config.domains)) for (const d of config.domains) collectEntities(d, entities)
|
|
50
|
+
if (entities.length === 0) {
|
|
51
|
+
emit({ ok: false, error: 'nenhuma entidade encontrada (config.entities ou domain.models)' })
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
if (typeof config.database !== 'function') {
|
|
55
|
+
emit({ ok: false, error: 'config.database() ausente — exporte uma factory que retorna um Kysely' })
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const db = await config.database()
|
|
60
|
+
let result
|
|
61
|
+
try {
|
|
62
|
+
const findings = await kyselyDriftCheck(db, entities)
|
|
63
|
+
result = scaffoldMigration(entities, findings)
|
|
64
|
+
} finally {
|
|
65
|
+
if (db !== null && typeof db.destroy === 'function') await db.destroy()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (result === null) {
|
|
69
|
+
emit({ ok: true, file: null })
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const configDir = path.dirname(path.resolve(configPath))
|
|
74
|
+
const migrationFolder = path.resolve(configDir, config.migrations ?? 'migrations')
|
|
75
|
+
await fs.mkdir(migrationFolder, { recursive: true })
|
|
76
|
+
const file = path.join(migrationFolder, `${timestamp()}_scaffold.ts`)
|
|
77
|
+
await fs.writeFile(file, scaffoldFileContent(result), 'utf8')
|
|
78
|
+
|
|
79
|
+
emit({ ok: true, file, statements: result.statements, notes: result.notes })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
main().catch((err) => {
|
|
83
|
+
emit({ ok: false, error: err instanceof Error ? err.message : String(err) })
|
|
84
|
+
})
|
package/bin/lib/db.mjs
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* db — grupo de comandos de banco (`opus db <verbo>`).
|
|
3
|
+
*
|
|
4
|
+
* `db check` (drift-check entidade ↔ banco), `db migrate` (aplica o SCHEMA
|
|
5
|
+
* IDEMPOTENTE — o padrão da casa: um script SQL evolutivo re-rodável, não
|
|
6
|
+
* migration versionada — e cobra o drift na sequência) e `db scaffold`
|
|
7
|
+
* (rascunho-referência a partir do diff). Diferente do `opus check` (estático,
|
|
8
|
+
* source-only), os comandos `db` carregam o `opus.config.ts` e abrem conexão —
|
|
9
|
+
* por isso rodam via `tsx` num runner isolado (mesma infra do `gen`).
|
|
10
|
+
* Ver `docs/data-layer.md`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { promises as fs } from 'node:fs'
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import { execFile } from 'node:child_process'
|
|
17
|
+
import { promisify } from 'node:util'
|
|
18
|
+
|
|
19
|
+
const execFileAsync = promisify(execFile)
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
22
|
+
const __dirname = path.dirname(__filename)
|
|
23
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..', '..')
|
|
24
|
+
|
|
25
|
+
const COLORS = {
|
|
26
|
+
info: '\x1b[36m',
|
|
27
|
+
success: '\x1b[32m',
|
|
28
|
+
error: '\x1b[31m',
|
|
29
|
+
warn: '\x1b[33m',
|
|
30
|
+
dim: '\x1b[2m',
|
|
31
|
+
}
|
|
32
|
+
const RESET = '\x1b[0m'
|
|
33
|
+
|
|
34
|
+
function log(level, msg) {
|
|
35
|
+
console.log(`${COLORS[level] ?? ''}${msg}${RESET}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// Dispatch
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
export async function cmdDb(rest, flags) {
|
|
43
|
+
const [sub] = rest
|
|
44
|
+
if (flags.help || sub === undefined || sub === 'help') {
|
|
45
|
+
helpDb()
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
if (sub === 'check') {
|
|
49
|
+
await cmdDbCheck(flags)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
if (sub === 'migrate') {
|
|
53
|
+
await cmdDbMigrate(rest[1], flags)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
if (sub === 'scaffold') {
|
|
57
|
+
await cmdDbScaffold(flags)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
log('error', `Subcomando db desconhecido: ${sub}`)
|
|
61
|
+
helpDb()
|
|
62
|
+
process.exit(1)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Config
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
async function resolveConfig(flags) {
|
|
70
|
+
const cwd = process.cwd()
|
|
71
|
+
const configRel = flags.config ?? 'opus.config.ts'
|
|
72
|
+
const configPath = path.isAbsolute(configRel) ? configRel : path.resolve(cwd, configRel)
|
|
73
|
+
if (!(await fileExists(configPath))) {
|
|
74
|
+
log('error', `opus.config.ts não encontrado em ${configPath}`)
|
|
75
|
+
log('dim', ' Passa o path via --config <path> ou cria um na raiz do projeto.')
|
|
76
|
+
process.exit(1)
|
|
77
|
+
}
|
|
78
|
+
return { cwd, configPath }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// db check
|
|
83
|
+
// =============================================================================
|
|
84
|
+
|
|
85
|
+
async function cmdDbCheck(flags) {
|
|
86
|
+
const { cwd, configPath } = await resolveConfig(flags)
|
|
87
|
+
log('info', `→ drift-check via ${path.relative(cwd, configPath)}...`)
|
|
88
|
+
|
|
89
|
+
const result = await runRunner('db-check-runner.mjs', configPath)
|
|
90
|
+
if (result.ok !== true) {
|
|
91
|
+
log('error', `Falha no drift-check: ${result.error}`)
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const findings = result.findings ?? []
|
|
96
|
+
if (findings.length === 0) {
|
|
97
|
+
log('success', '✓ db check: schema do banco em dia com as entidades.')
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const byTable = new Map()
|
|
102
|
+
for (const f of findings) {
|
|
103
|
+
if (!byTable.has(f.table)) byTable.set(f.table, [])
|
|
104
|
+
byTable.get(f.table).push(f)
|
|
105
|
+
}
|
|
106
|
+
for (const [table, list] of byTable) {
|
|
107
|
+
log('warn', `\n${table}`)
|
|
108
|
+
for (const f of list) {
|
|
109
|
+
console.log(` [${f.kind}] ${f.message}`)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
log('error', `\n✗ db check: ${findings.length} divergência(s) em ${byTable.size} tabela(s).`)
|
|
113
|
+
process.exit(1)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// =============================================================================
|
|
117
|
+
// db migrate — aplica o schema idempotente + drift-check na sequência
|
|
118
|
+
// =============================================================================
|
|
119
|
+
|
|
120
|
+
async function cmdDbMigrate(sub2, flags) {
|
|
121
|
+
if (sub2 === 'down') {
|
|
122
|
+
// O padrão da casa é schema EVOLUTIVO (um script idempotente), não migration
|
|
123
|
+
// versionada — não existe "voltar uma": rollback = editar o script e re-rodar
|
|
124
|
+
// (catástrofe = snapshot pré-deploy). A era Kysely Migrator foi aposentada.
|
|
125
|
+
log('error', 'db migrate down foi aposentado: o schema é um script idempotente, sem histórico a reverter.')
|
|
126
|
+
log('dim', ' Rollback = editar o schema e re-rodar `opus db migrate`; catástrofe = snapshot pré-deploy.')
|
|
127
|
+
process.exit(1)
|
|
128
|
+
}
|
|
129
|
+
const { cwd, configPath } = await resolveConfig(flags)
|
|
130
|
+
log('info', `→ db migrate via ${path.relative(cwd, configPath)}...`)
|
|
131
|
+
|
|
132
|
+
const result = await runRunner('db-migrate-runner.mjs', configPath)
|
|
133
|
+
if (result.ok !== true) {
|
|
134
|
+
log('error', `✗ db migrate: ${result.error}`)
|
|
135
|
+
process.exit(1)
|
|
136
|
+
}
|
|
137
|
+
log('success', `✓ schema aplicado (${path.relative(cwd, result.schema)}).`)
|
|
138
|
+
|
|
139
|
+
// Drift-check embutido: migrou mas diverge das entidades = o schema ficou pra trás
|
|
140
|
+
// da declaração — estado quebrado que o deploy precisa ver AQUI, não depois.
|
|
141
|
+
const findings = result.findings ?? []
|
|
142
|
+
if (findings.length === 0) {
|
|
143
|
+
log('success', '✓ db migrate: banco em dia com as entidades.')
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
const byTable = new Map()
|
|
147
|
+
for (const f of findings) {
|
|
148
|
+
if (!byTable.has(f.table)) byTable.set(f.table, [])
|
|
149
|
+
byTable.get(f.table).push(f)
|
|
150
|
+
}
|
|
151
|
+
for (const [table, list] of byTable) {
|
|
152
|
+
log('warn', `\n${table}`)
|
|
153
|
+
for (const f of list) console.log(` [${f.kind}] ${f.message}`)
|
|
154
|
+
}
|
|
155
|
+
log('error', `\n✗ db migrate: schema aplicado, mas ${findings.length} divergência(s) com as entidades — atualize o script.`)
|
|
156
|
+
process.exit(1)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// =============================================================================
|
|
160
|
+
// db scaffold
|
|
161
|
+
// =============================================================================
|
|
162
|
+
|
|
163
|
+
async function cmdDbScaffold(flags) {
|
|
164
|
+
const { cwd, configPath } = await resolveConfig(flags)
|
|
165
|
+
log('info', `→ db scaffold via ${path.relative(cwd, configPath)}...`)
|
|
166
|
+
|
|
167
|
+
const result = await runRunner('db-scaffold-runner.mjs', configPath)
|
|
168
|
+
if (result.ok !== true) {
|
|
169
|
+
log('error', `Falha no scaffold: ${result.error}`)
|
|
170
|
+
process.exit(1)
|
|
171
|
+
}
|
|
172
|
+
if (result.file === null || result.file === undefined) {
|
|
173
|
+
log('success', '✓ db scaffold: nada a gerar (schema em dia).')
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
log('success', `✓ db scaffold: ${path.relative(cwd, result.file)} (${result.statements} statement(s)).`)
|
|
177
|
+
const notes = result.notes ?? []
|
|
178
|
+
if (notes.length > 0) {
|
|
179
|
+
log('warn', `\n ${notes.length} ponto(s) pra revisar à mão (gerados como // TODO):`)
|
|
180
|
+
for (const n of notes) console.log(` ! ${n}`)
|
|
181
|
+
}
|
|
182
|
+
log('dim', '\n Rascunho — revise antes de `opus db migrate`.')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// =============================================================================
|
|
186
|
+
// Runner spawn (via tsx)
|
|
187
|
+
// =============================================================================
|
|
188
|
+
|
|
189
|
+
async function runRunner(runnerFile, configPath, extraArgs = []) {
|
|
190
|
+
const tsxBin = path.join(PACKAGE_ROOT, 'node_modules', '.bin', 'tsx')
|
|
191
|
+
const hasLocalTsx = await fileExists(tsxBin)
|
|
192
|
+
const runnerPath = path.join(__dirname, runnerFile)
|
|
193
|
+
const cmd = hasLocalTsx ? tsxBin : 'tsx'
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const { stdout } = await execFileAsync(cmd, [runnerPath, configPath, ...extraArgs], {
|
|
197
|
+
encoding: 'utf8',
|
|
198
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
199
|
+
env: { ...process.env },
|
|
200
|
+
})
|
|
201
|
+
const line = lastJsonLine(stdout)
|
|
202
|
+
if (line === null) return { ok: false, error: 'runner não emitiu JSON' }
|
|
203
|
+
return JSON.parse(line)
|
|
204
|
+
} catch (err) {
|
|
205
|
+
const detail =
|
|
206
|
+
typeof err.stderr === 'string' && err.stderr.trim().length > 0
|
|
207
|
+
? err.stderr.trim()
|
|
208
|
+
: err.message
|
|
209
|
+
return { ok: false, error: detail }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function lastJsonLine(stdout) {
|
|
214
|
+
const lines = stdout.split('\n').filter((l) => l.trim().length > 0)
|
|
215
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
216
|
+
const trimmed = lines[i].trim()
|
|
217
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) return trimmed
|
|
218
|
+
}
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function fileExists(p) {
|
|
223
|
+
try {
|
|
224
|
+
await fs.access(p)
|
|
225
|
+
return true
|
|
226
|
+
} catch {
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// =============================================================================
|
|
232
|
+
// Help
|
|
233
|
+
// =============================================================================
|
|
234
|
+
|
|
235
|
+
export function helpDb() {
|
|
236
|
+
console.log(`
|
|
237
|
+
@softize/opus db — comandos de banco
|
|
238
|
+
|
|
239
|
+
db check Compara o schema do banco com as entidades (defineEntity) — drift-check.
|
|
240
|
+
Read-only. Exit ≠ 0 se divergir. Gate de CI/deploy.
|
|
241
|
+
db migrate Aplica o SCHEMA IDEMPOTENTE (config \`schema\`, script SQL evolutivo:
|
|
242
|
+
IF NOT EXISTS + guards — nasce do zero E faz upgrade no mesmo artefato,
|
|
243
|
+
re-rodável) e roda o drift-check na sequência. Exit ≠ 0 se divergir.
|
|
244
|
+
db scaffold Gera um rascunho kysely a partir do diff — REFERÊNCIA pra escrever o
|
|
245
|
+
SQL no schema (a verdade é o script; revise à mão).
|
|
246
|
+
|
|
247
|
+
Flags:
|
|
248
|
+
--config <path> Caminho do opus.config.ts. Default: ./opus.config.ts
|
|
249
|
+
|
|
250
|
+
O opus.config.ts precisa expor, pros comandos db:
|
|
251
|
+
database: () => Kysely factory LAZY do banco (gen não a chama)
|
|
252
|
+
entities | domains entidades (explícitas ou via domain.models)
|
|
253
|
+
schema?: string script SQL idempotente (default: ./db/schema.sql)
|
|
254
|
+
migrations?: string pasta dos rascunhos do scaffold (default: ./migrations)
|
|
255
|
+
|
|
256
|
+
Exemplos:
|
|
257
|
+
npx @softize/opus db check
|
|
258
|
+
npx @softize/opus db migrate
|
|
259
|
+
npx @softize/opus db migrate down --config ./apps/api/opus.config.ts
|
|
260
|
+
`)
|
|
261
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* docs-include — transclusão de páginas da doc pra dentro de SKILLS na materialização.
|
|
3
|
+
*
|
|
4
|
+
* O modelo da casa: a DOC é a fonte do conhecimento (humano lê no site; viaja no
|
|
5
|
+
* pacote); a SKILL é o gatilho do agente. Ponteiro não garante consulta — transclusão
|
|
6
|
+
* garante: a skill declara `<!-- opus-doc: <página>.md -->` e quem MATERIALIZA (o
|
|
7
|
+
* prepare do admin; o caminho vivo do Maestro) expande o conteúdo da página pra dentro
|
|
8
|
+
* do corpo, que o mecanismo de skill carrega no contexto. Edita-se a página, nunca a
|
|
9
|
+
* cópia — fonte única, entrega derivada (o mesmo padrão do manifest).
|
|
10
|
+
*
|
|
11
|
+
* Página inexistente = ERRO (deploy para — skill apontando doc morta é defeito, igual
|
|
12
|
+
* ao gate de frontmatter). A expansão tira o frontmatter e o H1 da página (a skill já
|
|
13
|
+
* titula a própria seção).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from 'node:fs'
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
19
|
+
|
|
20
|
+
const CONTENT_DIR = path.resolve(
|
|
21
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
22
|
+
'..',
|
|
23
|
+
'..',
|
|
24
|
+
'src',
|
|
25
|
+
'ui',
|
|
26
|
+
'docs',
|
|
27
|
+
'content',
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const MARKER_RE = /^[ \t]*<!--\s*opus-doc:\s*([\w./-]+)\s*-->[ \t]*$/gm
|
|
31
|
+
|
|
32
|
+
/** Expande os markers `<!-- opus-doc: x.md -->` de um corpo de skill. */
|
|
33
|
+
export function expandDocIncludes(md) {
|
|
34
|
+
return md.replace(MARKER_RE, (_marker, file) => {
|
|
35
|
+
const name = path.basename(String(file)) // só o nome — sem traversal.
|
|
36
|
+
const full = path.join(CONTENT_DIR, name)
|
|
37
|
+
let raw
|
|
38
|
+
try {
|
|
39
|
+
raw = readFileSync(full, 'utf8')
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error(`opus-doc include: a página "${name}" não existe em ${CONTENT_DIR} — corrija a skill.`)
|
|
42
|
+
}
|
|
43
|
+
return raw
|
|
44
|
+
.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '')
|
|
45
|
+
.replace(/^\s*# .*\r?\n/, '')
|
|
46
|
+
.trim()
|
|
47
|
+
})
|
|
48
|
+
}
|