@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,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Stop da base Opus: quando o turno termina, roda o `opus check` dos apps que a
|
|
3
|
+
* sessão MEXEU (arquivos alterados no git) e BLOQUEIA o fim do turno se houver violação
|
|
4
|
+
* — exit 2 devolve os findings ao agente, que se corrige na hora. O gate da casa deixa
|
|
5
|
+
* de depender de alguém lembrar de rodar.
|
|
6
|
+
*
|
|
7
|
+
* Fail-open por princípio: sem git, sem bin do opus, crash ou timeout do check → exit 0
|
|
8
|
+
* (infra nunca trava a sessão). Só bloqueia em violação REAL (a linha `✗ opus check`).
|
|
9
|
+
* Escopo por app alterado de propósito: violação pré-existente em app não tocado não
|
|
10
|
+
* pode travar o turno dos outros.
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from 'node:child_process'
|
|
13
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
14
|
+
import { dirname, join } from 'node:path'
|
|
15
|
+
|
|
16
|
+
let event = {}
|
|
17
|
+
try {
|
|
18
|
+
event = JSON.parse(readFileSync(0, 'utf8'))
|
|
19
|
+
} catch {
|
|
20
|
+
/* Sem stdin legível → segue com defaults. */
|
|
21
|
+
}
|
|
22
|
+
// Anti-loop: este Stop já foi bloqueado por hook antes → deixa parar.
|
|
23
|
+
if (event.stop_hook_active === true) process.exit(0)
|
|
24
|
+
|
|
25
|
+
const root = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
|
|
26
|
+
|
|
27
|
+
// Arquivos alterados (staged + unstaged + untracked). Sem git → fail-open.
|
|
28
|
+
let changed = []
|
|
29
|
+
try {
|
|
30
|
+
changed = execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' })
|
|
31
|
+
.split('\n')
|
|
32
|
+
.map((l) => l.slice(3).trim())
|
|
33
|
+
.filter((l) => l.length > 0)
|
|
34
|
+
// Rename (`a -> b`): o caminho que interessa é o destino.
|
|
35
|
+
.map((l) => (l.includes(' -> ') ? l.split(' -> ')[1] : l))
|
|
36
|
+
} catch {
|
|
37
|
+
process.exit(0)
|
|
38
|
+
}
|
|
39
|
+
if (changed.length === 0) process.exit(0)
|
|
40
|
+
|
|
41
|
+
// Apps opus-based = diretórios com o marcador opus.json (raiz inclusa).
|
|
42
|
+
const SKIP = new Set(['node_modules', 'dist', 'build', 'coverage', '.next'])
|
|
43
|
+
function findApps(dir, depth) {
|
|
44
|
+
const acc = existsSync(join(dir, 'opus.json')) ? [dir] : []
|
|
45
|
+
if (depth === 0) return acc
|
|
46
|
+
let entries = []
|
|
47
|
+
try {
|
|
48
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
49
|
+
} catch {
|
|
50
|
+
return acc
|
|
51
|
+
}
|
|
52
|
+
for (const e of entries) {
|
|
53
|
+
if (!e.isDirectory() || e.name.startsWith('.') || SKIP.has(e.name)) continue
|
|
54
|
+
acc.push(...findApps(join(dir, e.name), depth - 1))
|
|
55
|
+
}
|
|
56
|
+
return acc
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Só os apps que a sessão tocou (app mais específico primeiro, pra um app aninhado
|
|
60
|
+
// não cair no ancestral).
|
|
61
|
+
const apps = findApps(root, 3).sort((a, b) => b.length - a.length)
|
|
62
|
+
const touched = new Set()
|
|
63
|
+
for (const rel of changed) {
|
|
64
|
+
const app = apps.find((a) => a === root || join(root, rel).startsWith(`${a}/`))
|
|
65
|
+
if (app !== undefined) touched.add(app)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Bin do opus pro app: sobe do app até a raiz (repo simples / hoisted) e, se não achar,
|
|
70
|
+
* desce UM nível (app composto — pacotes api/web dentro do app, o pnpm instala o bin
|
|
71
|
+
* no node_modules de cada pacote). Sem bin → fail-open (o app fica de fora).
|
|
72
|
+
*/
|
|
73
|
+
function opusBinFor(appDir) {
|
|
74
|
+
let dir = appDir
|
|
75
|
+
for (;;) {
|
|
76
|
+
const bin = join(dir, 'node_modules', '.bin', 'opus')
|
|
77
|
+
if (existsSync(bin)) return bin
|
|
78
|
+
if (dir === root || dirname(dir) === dir) break
|
|
79
|
+
dir = dirname(dir)
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
for (const e of readdirSync(appDir, { withFileTypes: true })) {
|
|
83
|
+
if (!e.isDirectory() || e.name.startsWith('.') || SKIP.has(e.name)) continue
|
|
84
|
+
const bin = join(appDir, e.name, 'node_modules', '.bin', 'opus')
|
|
85
|
+
if (existsSync(bin)) return bin
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
/* Ilegível → sem bin. */
|
|
89
|
+
}
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const failures = []
|
|
94
|
+
for (const app of touched) {
|
|
95
|
+
const bin = opusBinFor(app)
|
|
96
|
+
if (bin === null) continue
|
|
97
|
+
try {
|
|
98
|
+
execFileSync(bin, ['check'], { cwd: app, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 45_000 })
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// Sem cor: o destinatário é o agente, não um terminal.
|
|
101
|
+
const out = `${err.stdout ?? ''}${err.stderr ?? ''}`.replace(/\x1b\[[0-9;]*m/g, '')
|
|
102
|
+
// Violação de verdade tem a linha ✗ e exit 1; qualquer outra falha é infra → segue.
|
|
103
|
+
if (err.status === 1 && out.includes('✗ opus check')) {
|
|
104
|
+
const rel = app === root ? '.' : app.slice(root.length + 1)
|
|
105
|
+
failures.push(`# ${rel}\n${out.trim()}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (failures.length === 0) process.exit(0)
|
|
111
|
+
process.stderr.write(
|
|
112
|
+
`O opus check reprovou nos apps alterados — corrija as violações antes de encerrar o turno:\n\n${failures.join('\n\n')}\n`,
|
|
113
|
+
)
|
|
114
|
+
process.exit(2)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-action
|
|
3
|
+
description: Cria uma opus action no formato canônico (passa no opus check por construção). Use ao implementar uma nova action/endpoint num projeto Opus-based.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# create-action
|
|
7
|
+
|
|
8
|
+
Procedimento pra criar uma **opus action** padronizada. Gera o esqueleto certo e
|
|
9
|
+
deixa você só com a lógica — sem decorar convenção, sem reprovar no `opus check`.
|
|
10
|
+
|
|
11
|
+
> Língua: **código em inglês; comentários/UI em pt-BR** (ver `code-style.md`).
|
|
12
|
+
|
|
13
|
+
## Passo a passo
|
|
14
|
+
|
|
15
|
+
1. **Decida** `<resource>.<verb>` e o `kind`:
|
|
16
|
+
- `list` — lista paginada · `view` — leitura de um recurso
|
|
17
|
+
- `form` — mutação com campos de UI · `simple` — mutação atômica
|
|
18
|
+
- Namespacing por sub-área é ok: `ticket.sla.check`.
|
|
19
|
+
|
|
20
|
+
2. **Gere o esqueleto** (formato canônico, ordem dos campos correta):
|
|
21
|
+
```bash
|
|
22
|
+
node scaffold.mjs <resource> <verb> <kind> # ex.: ticket create form
|
|
23
|
+
```
|
|
24
|
+
Cole o resultado em `src/domains/<resource>/actions/<verb>.ts`.
|
|
25
|
+
|
|
26
|
+
3. **Preencha**: `input`/`output` (zod + `t.*` pra tipos lógicos), `summary`/`label`
|
|
27
|
+
(pt-BR), e o `handler` (a lógica). Não reordene os campos.
|
|
28
|
+
|
|
29
|
+
4. **Exporte no barrel** `src/domains/<resource>/actions/index.ts` e garanta que o
|
|
30
|
+
domínio está registrado no `src/runtime.ts` (`defineDomain` + `runtime.register`).
|
|
31
|
+
|
|
32
|
+
5. **Front (quando aplicável)**: invoque via `@softize/opus/client` + `ui/react`
|
|
33
|
+
(`useLookupAction`/`useTriggerAction`/`useFormAction`/`useViewAction`) — **não**
|
|
34
|
+
escreva fetch/tipos na mão.
|
|
35
|
+
|
|
36
|
+
6. **Valide**: `opus check src` → tem que dar **0 violação**. Rode antes do handoff.
|
|
37
|
+
|
|
38
|
+
## O contrato é o `actions.md`
|
|
39
|
+
|
|
40
|
+
Kinds, ordem canônica dos campos e garantias vivem na página Actions (`/docs/actions`;
|
|
41
|
+
`node_modules/@softize/opus/src/ui/docs/content/actions.md`) — não reproduza aqui. O
|
|
42
|
+
`scaffold.mjs` já emite a ordem certa e o `opus check` cobra; esta skill é o
|
|
43
|
+
PROCEDIMENTO, não a referência.
|
|
44
|
+
|
|
45
|
+
Checklist rápido do que o `opus check` reprova (antes do handoff):
|
|
46
|
+
- `name` = `<resource>(.<sub>)*.<verb>`, minúsculo, ponto.
|
|
47
|
+
- `kind` ∈ `simple|form|list|view`.
|
|
48
|
+
- Campos na ordem canônica (do scaffold; ver `actions.md`).
|
|
49
|
+
- `defineAction` é **`export const`**.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffold de opus action — gera o esqueleto canônico (passa no `opus check` por
|
|
3
|
+
* construção: action-name, kind, field-order, export). O agente preenche a lógica.
|
|
4
|
+
*
|
|
5
|
+
* Uso: node scaffold.mjs <resource> <verb> [kind=simple]
|
|
6
|
+
* ex.: node scaffold.mjs widget list list → const widgetList (widget.list)
|
|
7
|
+
* node scaffold.mjs user set-password simple
|
|
8
|
+
*
|
|
9
|
+
* Comentários em pt-BR (UI/comentários em PT; código em inglês — ver code-style.md).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1)
|
|
13
|
+
const camel = (verb) => verb.split('-').map((p, i) => (i === 0 ? p : cap(p))).join('')
|
|
14
|
+
/** const name = resource + PascalVerb. Ex.: user + set-password → userSetPassword. */
|
|
15
|
+
const constName = (resource, verb) => resource + verb.split('-').map(cap).join('')
|
|
16
|
+
|
|
17
|
+
const HEAD = `import { z } from 'zod'
|
|
18
|
+
import { defineAction } from '@softize/opus/core'
|
|
19
|
+
`
|
|
20
|
+
|
|
21
|
+
/** Gera o conteúdo .ts canônico da action pro kind. */
|
|
22
|
+
export function actionTemplate({ resource, verb, kind = 'simple' }) {
|
|
23
|
+
const cn = constName(resource, verb)
|
|
24
|
+
const name = `${resource}.${verb}`
|
|
25
|
+
|
|
26
|
+
if (kind === 'list') {
|
|
27
|
+
return `${HEAD}
|
|
28
|
+
export const ${cn} = defineAction({
|
|
29
|
+
name: '${name}',
|
|
30
|
+
kind: 'list',
|
|
31
|
+
summary: 'TODO: o que lista (pt-BR)',
|
|
32
|
+
input: z.object({
|
|
33
|
+
// TODO: filtros / paginação
|
|
34
|
+
}),
|
|
35
|
+
output: z.object({
|
|
36
|
+
// TODO: shape de um item
|
|
37
|
+
id: z.string(),
|
|
38
|
+
}),
|
|
39
|
+
paginate: 'cursor',
|
|
40
|
+
handler: async (_ctx, _input) => {
|
|
41
|
+
// TODO: buscar
|
|
42
|
+
return { items: [], cursor: { next: null }, total: 0 }
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (kind === 'form') {
|
|
49
|
+
return `${HEAD}
|
|
50
|
+
export const ${cn} = defineAction({
|
|
51
|
+
name: '${name}',
|
|
52
|
+
kind: 'form',
|
|
53
|
+
label: 'TODO: rótulo (pt-BR)',
|
|
54
|
+
messages: { success: 'TODO', error: 'TODO' },
|
|
55
|
+
input: z.object({
|
|
56
|
+
// TODO: campos do form
|
|
57
|
+
}),
|
|
58
|
+
output: z.object({ id: z.string() }),
|
|
59
|
+
fields: {
|
|
60
|
+
// TODO: { campo: { label: 'pt-BR' } }
|
|
61
|
+
},
|
|
62
|
+
handler: async (_ctx, _input) => {
|
|
63
|
+
// TODO: criar/editar
|
|
64
|
+
return { id: '' }
|
|
65
|
+
},
|
|
66
|
+
invalidates: ['${resource}.list'],
|
|
67
|
+
})
|
|
68
|
+
`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (kind === 'view') {
|
|
72
|
+
return `${HEAD}
|
|
73
|
+
export const ${cn} = defineAction({
|
|
74
|
+
name: '${name}',
|
|
75
|
+
kind: 'view',
|
|
76
|
+
summary: 'TODO: o que retorna (pt-BR)',
|
|
77
|
+
input: z.object({ id: z.string() }),
|
|
78
|
+
output: z.object({
|
|
79
|
+
// TODO: shape do recurso
|
|
80
|
+
id: z.string(),
|
|
81
|
+
}),
|
|
82
|
+
handler: async (_ctx, input) => {
|
|
83
|
+
// TODO: carregar
|
|
84
|
+
return { id: input.id }
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// simple (default)
|
|
91
|
+
return `${HEAD}
|
|
92
|
+
export const ${cn} = defineAction({
|
|
93
|
+
name: '${name}',
|
|
94
|
+
kind: 'simple',
|
|
95
|
+
label: 'TODO: rótulo (pt-BR)',
|
|
96
|
+
messages: { success: 'TODO', error: 'TODO' },
|
|
97
|
+
input: z.object({ id: z.string() }),
|
|
98
|
+
output: z.object({ ok: z.boolean() }),
|
|
99
|
+
handler: async (_ctx, _input) => {
|
|
100
|
+
// TODO: mutação
|
|
101
|
+
return { ok: true }
|
|
102
|
+
},
|
|
103
|
+
invalidates: ['${resource}.list'],
|
|
104
|
+
})
|
|
105
|
+
`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Nome do arquivo sugerido (kebab do verb). */
|
|
109
|
+
export function actionFileName(verb) {
|
|
110
|
+
return `${verb}.ts`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// CLI: imprime o template no stdout (a skill diz onde colar).
|
|
114
|
+
const isMain = import.meta.url === `file://${process.argv[1]}`
|
|
115
|
+
if (isMain) {
|
|
116
|
+
const [resource, verb, kind] = process.argv.slice(2)
|
|
117
|
+
if (!resource || !verb) {
|
|
118
|
+
console.error('uso: node scaffold.mjs <resource> <verb> [kind=simple|form|list|view]')
|
|
119
|
+
process.exit(1)
|
|
120
|
+
}
|
|
121
|
+
process.stdout.write(actionTemplate({ resource, verb, kind }))
|
|
122
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@softize:registry=https://registry.softize.com.br/
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="pt-BR">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
|
+
<title>{{APP_NAME}}</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opus.config.ts — consumido pelo `opus gen` (manifest/openapi/docs) e, quando o app
|
|
3
|
+
* ganhar banco, pelo `opus db` (adicione `entities`, a factory `database` e o `schema`
|
|
4
|
+
* — o script SQL idempotente que o `opus db migrate` aplica).
|
|
5
|
+
* `domains` = os domínios registrados: a fonte da spec — o `opus gen` projeta o
|
|
6
|
+
* manifest.json commitado (o CI cobra a frescura via manifest:check).
|
|
7
|
+
*/
|
|
8
|
+
import { tasksDomain } from './src/domains/tasks/index.ts'
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
domains: [tasksDomain],
|
|
12
|
+
entities: [],
|
|
13
|
+
naming: 'snake',
|
|
14
|
+
migrations: './migrations',
|
|
15
|
+
output: '.opus',
|
|
16
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{APP_NAME}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"dev:design": "vite --mode design --configLoader runner",
|
|
8
|
+
"build": "tsc --noEmit && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"format": "prettier --write src",
|
|
13
|
+
"format:check": "prettier --check src",
|
|
14
|
+
"manifest": "opus gen",
|
|
15
|
+
"manifest:check": "opus gen && git diff --exit-code .opus/manifest.json"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@hookform/resolvers": "^3.0.0",
|
|
19
|
+
"@softize/opus": "^{{OPUS_VERSION}}",
|
|
20
|
+
"@tanstack/react-query": "^5.0.0",
|
|
21
|
+
"clsx": "^2.1.1",
|
|
22
|
+
"lucide-react": "^0.460.0",
|
|
23
|
+
"react": "^19.0.0",
|
|
24
|
+
"react-dom": "^19.0.0",
|
|
25
|
+
"react-hook-form": "^7.0.0",
|
|
26
|
+
"tailwind-merge": "^3.3.1",
|
|
27
|
+
"zod": "^3.24.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@tailwindcss/vite": "^4.1.0",
|
|
31
|
+
"@types/node": "^22.0.0",
|
|
32
|
+
"@types/react": "^19.0.0",
|
|
33
|
+
"@types/react-dom": "^19.0.0",
|
|
34
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
35
|
+
"prettier": "^3.3.0",
|
|
36
|
+
"tailwindcss": "^4.1.0",
|
|
37
|
+
"tsx": "^4.19.0",
|
|
38
|
+
"tw-animate-css": "^1.2.5",
|
|
39
|
+
"typescript": "^5.6.0",
|
|
40
|
+
"vite": "^6.0.0",
|
|
41
|
+
"vitest": "^2.1.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Permissão de build scripts (o pnpm bloqueia por default): o postinstall do
|
|
2
|
+
# @softize/opus roda o `opus setup` (pin + bloco gerenciado do CLAUDE.md) a cada
|
|
3
|
+
# install; o esbuild (vite) compila o binário nativo.
|
|
4
|
+
allowBuilds:
|
|
5
|
+
'@softize/opus': true
|
|
6
|
+
esbuild: true
|
|
7
|
+
|
|
8
|
+
# A base é do nosso registry (confiada): fora da quarentena de release do pnpm —
|
|
9
|
+
# senão um bump nas primeiras 24h da versão resolveria versão velha.
|
|
10
|
+
minimumReleaseAgeExclude:
|
|
11
|
+
- '@softize/opus'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { z } from 'zod'
|
|
2
|
+
import { Badge, Card, useListAction } from '@softize/opus/ui/react'
|
|
3
|
+
import { Task, taskList } from './domains/tasks/actions/list.ts'
|
|
4
|
+
|
|
5
|
+
type TaskItem = z.infer<typeof Task>
|
|
6
|
+
|
|
7
|
+
/** Página inicial do esqueleto: prova a fundação (tema + componentes do Opus) e o
|
|
8
|
+
* domínio-exemplo. Troque tudo — este arquivo é seu. */
|
|
9
|
+
export function App(): React.ReactElement {
|
|
10
|
+
// O front consome o CONTRATO via /api — o plugin opusDesign (vite.config.ts)
|
|
11
|
+
// serve o backend no próprio dev server: dev normal executa o handler real;
|
|
12
|
+
// `pnpm dev:design` responde com o mockHandler, isolado de qualquer backend.
|
|
13
|
+
const { items, isLoading } = useListAction<TaskItem>(taskList)
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<main className="flex min-h-full items-center justify-center bg-background p-6">
|
|
17
|
+
<Card className="w-full max-w-md p-6">
|
|
18
|
+
<h1 className="mb-1 text-lg font-semibold">{'{{APP_NAME}}'}</h1>
|
|
19
|
+
<p className="mb-4 text-sm text-muted-foreground">
|
|
20
|
+
Esqueleto criado pelo opus create. A spec vive nas declarações do domínio.
|
|
21
|
+
</p>
|
|
22
|
+
{isLoading ? (
|
|
23
|
+
<p className="text-sm text-muted-foreground">Carregando…</p>
|
|
24
|
+
) : (
|
|
25
|
+
<ul className="space-y-2">
|
|
26
|
+
{items.map((t) => (
|
|
27
|
+
<li key={t.id} className="flex items-center justify-between gap-2 text-sm">
|
|
28
|
+
<span>{t.title}</span>
|
|
29
|
+
<Badge variant={t.done ? 'default' : 'secondary'}>{t.done ? 'Feita' : 'Aberta'}</Badge>
|
|
30
|
+
</li>
|
|
31
|
+
))}
|
|
32
|
+
</ul>
|
|
33
|
+
)}
|
|
34
|
+
</Card>
|
|
35
|
+
</main>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { runAction } from '@softize/opus/testing'
|
|
3
|
+
import { Task, taskList, taskListImpl } from './list.ts'
|
|
4
|
+
|
|
5
|
+
// O harness roda o MESMO pipeline do runtime (valida input, cobra auth, valida
|
|
6
|
+
// output) — teste pelo contrato, não chamando o handler à mão.
|
|
7
|
+
describe('task.list', () => {
|
|
8
|
+
it('o bound (handler real) lista as fixtures com o shape do contrato', async () => {
|
|
9
|
+
const { output } = await runAction(taskListImpl, {}, { user: { id: 'u1' } })
|
|
10
|
+
expect(output.total).toBe(2)
|
|
11
|
+
expect(output.items[0]).toMatchObject({ id: '1', done: true })
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('sem usuário, barra: a action não é pública', async () => {
|
|
15
|
+
await expect(runAction(taskListImpl, {})).rejects.toMatchObject({ code: 'auth.unauthenticated' })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('modo design (o que o `pnpm dev:design` serve)', () => {
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
// OPUS_MODE é global do processo — limpa pra não vazar entre testes.
|
|
21
|
+
delete process.env['OPUS_MODE']
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('o contrato puro responde pelo mockHandler, com dado válido pro schema', async () => {
|
|
25
|
+
process.env['OPUS_MODE'] = 'design'
|
|
26
|
+
const { output } = await runAction(taskList, {}, { user: { id: 'u1' } })
|
|
27
|
+
expect(output.items.length).toBeGreaterThan(0)
|
|
28
|
+
// `list` pula a validação de output no pipeline — afirme item a item.
|
|
29
|
+
for (const item of output.items) {
|
|
30
|
+
expect(Task.safeParse(item).success).toBe(true)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { bindAction, defineContract } from '@softize/opus/core'
|
|
3
|
+
import { fakeMany } from '@softize/opus/testing'
|
|
4
|
+
|
|
5
|
+
/** Item do domínio-exemplo — o schema alimenta contrato, mock e testes. */
|
|
6
|
+
export const Task = z.object({ id: z.string(), title: z.string(), done: z.boolean() })
|
|
7
|
+
|
|
8
|
+
// O split canônico contrato↔handler: o CONTRATO é declarativo e roda nos dois
|
|
9
|
+
// lados (a SPA consome via useListAction); o BINDING é server-only. Em
|
|
10
|
+
// `pnpm dev:design` o mockHandler responde (dado fake do próprio schema);
|
|
11
|
+
// no dev normal e em prod, o handler real do binding assume.
|
|
12
|
+
export const taskList = defineContract({
|
|
13
|
+
name: 'task.list',
|
|
14
|
+
kind: 'list',
|
|
15
|
+
summary: 'Lista as tarefas do exemplo.',
|
|
16
|
+
description:
|
|
17
|
+
'Lista as tarefas do domínio-exemplo (fixtures em memória). Referência do formato ' +
|
|
18
|
+
'canônico de uma action list — contrato separado do handler, paginação por cursor.',
|
|
19
|
+
input: z.object({}),
|
|
20
|
+
output: Task,
|
|
21
|
+
paginate: 'cursor',
|
|
22
|
+
mockHandler: async () => ({ items: fakeMany(Task, 5), cursor: { next: null }, total: 5 }),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// Fixture em memória — o domínio-exemplo não tem banco; troque pelo seu repositório.
|
|
26
|
+
const TASKS = [
|
|
27
|
+
{ id: '1', title: 'Conhecer a base Opus.', done: true },
|
|
28
|
+
{ id: '2', title: 'Modelar o primeiro domínio real.', done: false },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
export const taskListImpl = bindAction(taskList, {
|
|
32
|
+
handler: async () => ({ items: TASKS, cursor: { next: null }, total: TASKS.length }),
|
|
33
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineDomain } from '@softize/opus/core'
|
|
2
|
+
import { taskListImpl } from './actions/list.ts'
|
|
3
|
+
|
|
4
|
+
/** Domínio-exemplo do esqueleto. Troque pelo seu domínio real — a skill
|
|
5
|
+
* `create-action` gera actions novas no formato canônico. O domínio registra
|
|
6
|
+
* o BOUND (contrato + handler); o contrato puro é o que a web consome. */
|
|
7
|
+
export const tasksDomain = defineDomain({
|
|
8
|
+
name: 'task',
|
|
9
|
+
description:
|
|
10
|
+
'Domínio-exemplo do esqueleto: uma lista de tarefas em memória. Serve de referência ' +
|
|
11
|
+
'do formato canônico (defineDomain + contrato↔handler com description = a spec).',
|
|
12
|
+
actions: { taskList: taskListImpl },
|
|
13
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* Tailwind v4 (CSS-first): o entry vem do Opus (Tailwind + tokens + @theme).
|
|
2
|
+
Sobrescreva vars aqui pra dar identidade ao projeto. */
|
|
3
|
+
@import '@softize/opus/ui/theme.css';
|
|
4
|
+
|
|
5
|
+
/* Os componentes do Opus vivem em node_modules — o Tailwind os ignora por padrão;
|
|
6
|
+
aponta a fonte explicitamente pra gerar as classes que eles usam. */
|
|
7
|
+
@source '../node_modules/@softize/opus/src/ui/**/*.{ts,tsx}';
|
|
8
|
+
|
|
9
|
+
@layer base {
|
|
10
|
+
* {
|
|
11
|
+
@apply border-border;
|
|
12
|
+
}
|
|
13
|
+
html,
|
|
14
|
+
body,
|
|
15
|
+
#root {
|
|
16
|
+
height: 100%;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
4
|
+
import { TbdlibProvider, TooltipProvider } from '@softize/opus/ui/react'
|
|
5
|
+
import { fetchClient } from '@softize/opus/client/fetch'
|
|
6
|
+
import { App } from './App.tsx'
|
|
7
|
+
import './index.css'
|
|
8
|
+
|
|
9
|
+
// O trio de providers do padrão da casa — os hooks (useTriggerAction/useFormAction…)
|
|
10
|
+
// exigem QueryClient + Tbdlib, e qualquer Tooltip da base exige o TooltipProvider.
|
|
11
|
+
const queryClient = new QueryClient()
|
|
12
|
+
// Same-origin (/api). Quando o app ganhar server próprio, aponte o baseUrl pra ele.
|
|
13
|
+
const client = fetchClient({ baseUrl: '' })
|
|
14
|
+
|
|
15
|
+
createRoot(document.getElementById('root')!).render(
|
|
16
|
+
<StrictMode>
|
|
17
|
+
<QueryClientProvider client={queryClient}>
|
|
18
|
+
<TbdlibProvider client={client}>
|
|
19
|
+
<TooltipProvider>
|
|
20
|
+
<App />
|
|
21
|
+
</TooltipProvider>
|
|
22
|
+
</TbdlibProvider>
|
|
23
|
+
</QueryClientProvider>
|
|
24
|
+
</StrictMode>,
|
|
25
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "Bundler",
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noUnusedLocals": true,
|
|
12
|
+
"noUnusedParameters": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"verbatimModuleSyntax": true,
|
|
16
|
+
"baseUrl": ".",
|
|
17
|
+
"paths": { "@/*": ["./src/*"] }
|
|
18
|
+
},
|
|
19
|
+
"include": ["src", "opus.config.ts"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineConfig, type PluginOption } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Modo design (`vite --mode design`, script dev:design): o runtime Opus sobe DENTRO do
|
|
8
|
+
* dev server e os mockHandler dos contratos respondem o `/api` — preview autossuficiente,
|
|
9
|
+
* isolado de prod (o plugin desliga o `server.proxy` sozinho; prefixo ex-proxy sem
|
|
10
|
+
* cobertura responde 503, nunca vaza pra porta real). Quando o app ganhar um backend real,
|
|
11
|
+
* o `dev` normal proxeia pra ele (adicione `server.proxy`); o preview de design segue nos
|
|
12
|
+
* mocks. O entry default do plugin é `./opus.config.ts` (a fonte da spec deste app).
|
|
13
|
+
*
|
|
14
|
+
* Import DINÂMICO NO TOP-LEVEL, de propósito, e em duas condições que andam juntas: (1) o
|
|
15
|
+
* @softize/opus embarca TS source e o config loader padrão (esbuild bundle — o que build e
|
|
16
|
+
* preview usam) externaliza um import ESTÁTICO, que o node ≥22 recusa (type-stripping sob
|
|
17
|
+
* node_modules, ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING) — por isso o plugin só carrega
|
|
18
|
+
* no dev:design, que roda com `--configLoader runner`, cujo pipeline transforma o TS; (2) o
|
|
19
|
+
* runner FECHA depois de avaliar o módulo, então o import fica no top-level (await), não
|
|
20
|
+
* dentro da função de config. O `apply` do próprio plugin re-confere o mode — o argv aqui é
|
|
21
|
+
* só quem evita o load fora do design.
|
|
22
|
+
*/
|
|
23
|
+
const DESIGN_RUN =
|
|
24
|
+
process.argv.includes('--mode=design') ||
|
|
25
|
+
process.argv.some((arg, i) => (arg === '--mode' || arg === '-m') && process.argv[i + 1] === 'design')
|
|
26
|
+
|
|
27
|
+
async function designPlugin(): Promise<PluginOption> {
|
|
28
|
+
const { opusDesign } = await import('@softize/opus/vite')
|
|
29
|
+
return opusDesign()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const designPlugins: PluginOption[] = DESIGN_RUN ? [await designPlugin()] : []
|
|
33
|
+
|
|
34
|
+
export default defineConfig({
|
|
35
|
+
plugins: [tailwindcss(), react(), ...designPlugins],
|
|
36
|
+
base: '/',
|
|
37
|
+
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },
|
|
38
|
+
server: {
|
|
39
|
+
// PORT do ambiente (um orquestrador de preview pode injetar); local, cai no default.
|
|
40
|
+
port: Number(process.env.PORT ?? 5173),
|
|
41
|
+
// Dev server pode rodar atrás de um proxy de preview (host arbitrário) — libera por
|
|
42
|
+
// padrão (é dev). Restrinja com VITE_ALLOWED_HOSTS=".seu-dominio.com" se quiser.
|
|
43
|
+
allowedHosts: process.env.VITE_ALLOWED_HOSTS?.split(',').filter(Boolean) ?? true,
|
|
44
|
+
},
|
|
45
|
+
build: { outDir: 'dist', emptyOutDir: true },
|
|
46
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@softize:registry=https://registry.softize.com.br/
|