@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
package/bin/lib/init.mjs
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opus setup — faz um projeto "leigo" passar a CONHECER a base. PER-APP, idempotente:
|
|
3
|
+
* • grava o marcador `opus.json` (versão da base pinada) — fonte da verdade
|
|
4
|
+
* que torna o projeto auto-anunciável (Maestro/CI/agente leem isso);
|
|
5
|
+
* • cria `CLAUDE.md` SE faltar; se existir, atualiza SÓ o bloco gerenciado
|
|
6
|
+
* (`<!-- opus:base -->…<!-- /opus:base -->`) — fora dele o arquivo é do projeto
|
|
7
|
+
* e nunca é tocado; arquivo antigo sem marcadores fica 100% intocado.
|
|
8
|
+
*
|
|
9
|
+
* NÃO materializa a camada-base (`.claude`) — isso é REPO-LEVEL, do MAESTRO (pull do
|
|
10
|
+
* admin). Re-rodar atualiza pin + bloco. Roda manual (`opus setup`) ou via postinstall.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { promises as fs } from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
const MARKER = "opus.json";
|
|
17
|
+
|
|
18
|
+
async function exists(p) {
|
|
19
|
+
try {
|
|
20
|
+
await fs.access(p);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readJson(p) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(await fs.readFile(p, "utf-8"));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Raiz do repo (sobe até achar `.git`); sem repo → null (o chamador decide o fallback). */
|
|
36
|
+
async function repoRootOf(dir) {
|
|
37
|
+
let cur = path.resolve(dir);
|
|
38
|
+
for (;;) {
|
|
39
|
+
if (await exists(path.join(cur, ".git"))) return cur;
|
|
40
|
+
const up = path.dirname(cur);
|
|
41
|
+
if (up === cur) return null;
|
|
42
|
+
cur = up;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** CI de fábrica do projeto (só se faltar — depois é seu): roda o que existir em cada
|
|
47
|
+
* package via --if-present, então cresce junto com o projeto sem editar o workflow. */
|
|
48
|
+
function checkWorkflowTemplate() {
|
|
49
|
+
return `name: check
|
|
50
|
+
|
|
51
|
+
# Gate de PR/push (gerado pelo \`opus setup\` — edite à vontade, é seu).
|
|
52
|
+
# Roda o que existir em cada package: typecheck, test e manifest:check.
|
|
53
|
+
on:
|
|
54
|
+
push:
|
|
55
|
+
branches: [main]
|
|
56
|
+
pull_request: {}
|
|
57
|
+
workflow_dispatch: {}
|
|
58
|
+
|
|
59
|
+
jobs:
|
|
60
|
+
check:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v4
|
|
64
|
+
- uses: pnpm/action-setup@v4
|
|
65
|
+
with:
|
|
66
|
+
version: 11
|
|
67
|
+
- uses: actions/setup-node@v4
|
|
68
|
+
with:
|
|
69
|
+
node-version: 22
|
|
70
|
+
cache: pnpm
|
|
71
|
+
- run: pnpm install --frozen-lockfile
|
|
72
|
+
- name: Typecheck
|
|
73
|
+
run: pnpm -r --if-present run typecheck
|
|
74
|
+
- name: Testes
|
|
75
|
+
run: pnpm -r --if-present run test
|
|
76
|
+
- name: Formatação
|
|
77
|
+
run: pnpm -r --if-present run format:check
|
|
78
|
+
# Manifest commitado tem que estar fresco (spec defasada = diff que mente).
|
|
79
|
+
- name: Manifest fresco
|
|
80
|
+
run: pnpm -r --if-present run manifest:check
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const BASE_OPEN = "<!-- opus:base -->";
|
|
85
|
+
const BASE_CLOSE = "<!-- /opus:base -->";
|
|
86
|
+
const BASE_BLOCK_RE = /<!-- opus:base -->[\s\S]*?<!-- \/opus:base -->/;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Bloco GERENCIADO do CLAUDE.md — o miolo que o setup mantém em dia quando o Opus
|
|
90
|
+
* evolui (era only-if-missing: template melhorava e projeto antigo nunca via). Fora dos
|
|
91
|
+
* marcadores o arquivo é do projeto e o setup nunca toca. Sem número de versão no texto
|
|
92
|
+
* de propósito: a versão mora no `opus.json` (nada se duplica).
|
|
93
|
+
*/
|
|
94
|
+
export function claudeMdBaseBlock() {
|
|
95
|
+
return `${BASE_OPEN}
|
|
96
|
+
# Opus
|
|
97
|
+
|
|
98
|
+
Este projeto usa o **Opus** (\`@softize/opus\`), um SDK/protocolo de actions; a versão
|
|
99
|
+
fica pinada em \`opus.json\`.
|
|
100
|
+
|
|
101
|
+
## Antes de mexer
|
|
102
|
+
- **A camada \`.claude/\` é REPO-LEVEL, materializada — não editar à mão.** Skills, agents
|
|
103
|
+
e hooks vivem na raiz do repo, postos pelo **orquestrador da base** (o Opus semeia as
|
|
104
|
+
skills do framework; agents e método são o seu setup). Num monorepo o agente enxerga o
|
|
105
|
+
repo todo (libs compartilhadas inclusas).
|
|
106
|
+
- **Seu, deste app (per-app):** \`opus.json\` (pin), este \`CLAUDE.md\` (fora do bloco) e o código.
|
|
107
|
+
- **A spec vive nas DECLARAÇÕES:** cada action/entidade carrega o \`description\` (o doc
|
|
108
|
+
de negócio — a fonte); o \`opus gen\` projeta no manifest. Doc colado na declaração não
|
|
109
|
+
defasa (a \`domain.md\` à parte foi aposentada).
|
|
110
|
+
- **Gate de build:** \`opus check\` (valida as convenções das actions — defineAction e
|
|
111
|
+
o split defineContract+bindAction; exit ≠ 0 se violar OU se não achar action nenhuma).
|
|
112
|
+
Rode antes de commitar/buildar.
|
|
113
|
+
- **Atualizar o Opus:** depois do bump, leia \`node_modules/@softize/opus/CHANGELOG.md\`
|
|
114
|
+
(breaking = seção **Breaking**, com a migração) e rode os gates — eles apontam o
|
|
115
|
+
que a mudança cobra do código.
|
|
116
|
+
- **Estado/contratos ao vivo:** \`opus mcp\` (introspect/check/create-action).
|
|
117
|
+
- **Nada se duplica:** estado/contratos+negócio → declarações (\`description\`) → manifest ·
|
|
118
|
+
quem executa → agents · como se faz uma tarefa → skills. Regra duplicada em vez de
|
|
119
|
+
apontada é defeito — aponte.
|
|
120
|
+
|
|
121
|
+
> Bloco gerenciado pelo \`opus setup\` — acompanha o Opus. O que você escrever fora
|
|
122
|
+
> dele é seu; o setup nunca toca.
|
|
123
|
+
${BASE_CLOSE}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// =============================================================================
|
|
127
|
+
// Fundação de UI (apps web) — preset Tailwind + tema + flags
|
|
128
|
+
// =============================================================================
|
|
129
|
+
|
|
130
|
+
const TAILWIND_CONFIGS = [
|
|
131
|
+
"tailwind.config.js",
|
|
132
|
+
"tailwind.config.cjs",
|
|
133
|
+
"tailwind.config.mjs",
|
|
134
|
+
"tailwind.config.ts",
|
|
135
|
+
];
|
|
136
|
+
const ENTRY_FILES = [
|
|
137
|
+
"src/main.tsx",
|
|
138
|
+
"src/main.ts",
|
|
139
|
+
"src/index.tsx",
|
|
140
|
+
"src/index.ts",
|
|
141
|
+
"src/main.jsx",
|
|
142
|
+
"src/index.jsx",
|
|
143
|
+
];
|
|
144
|
+
/**
|
|
145
|
+
* Major do Tailwind do consumidor. A v4 é CSS-first: NÃO tem (nem quer) `tailwind.config.js`
|
|
146
|
+
* — tema e escaneamento moram no CSS (`@import 'tailwindcss'`, `@theme`, `@source`). Semear
|
|
147
|
+
* um config v3 ali cria um arquivo que a build ignora e que engana quem for lê-lo.
|
|
148
|
+
*
|
|
149
|
+
* A pergunta real é "que Tailwind essa build roda?", e quem responde isso é o que está
|
|
150
|
+
* INSTALADO — não o range declarado. Ler o range primeiro erraria justo onde dói: `catalog:`
|
|
151
|
+
* (monorepo pnpm), `workspace:*`, `latest`, `*` e `file:../x` não têm dígito nenhum, cairiam
|
|
152
|
+
* no caminho v3 e o app v4 ganharia de volta o config fantasma. E `>=3` declarado pode ter
|
|
153
|
+
* resolvido 4.x. Como o `setup` roda no postinstall, o `node_modules` já está lá.
|
|
154
|
+
* Só quando não há nada instalado o range entra como palpite.
|
|
155
|
+
*/
|
|
156
|
+
async function tailwindMajor(projectDir, pkg) {
|
|
157
|
+
// SOBE a árvore, como a resolução do Node: num monorepo com hoisting (npm/yarn
|
|
158
|
+
// workspaces, pnpm com `shamefully-hoist`) o tailwind mora só na raiz, e olhar apenas a
|
|
159
|
+
// pasta do app falharia — justamente onde moram os ranges que o fallback não sabe ler.
|
|
160
|
+
//
|
|
161
|
+
// A subida é feita à mão em vez de `createRequire().resolve()` de propósito: a resolução
|
|
162
|
+
// do Node é interceptada por runner de teste (sob vitest ela acha o tailwind do repo do
|
|
163
|
+
// Opus a partir de um diretório em /tmp), o que tornaria esta função não-testável e o
|
|
164
|
+
// resultado dependente de quem chama.
|
|
165
|
+
let instalada = null;
|
|
166
|
+
for (let dir = path.resolve(projectDir); ; dir = path.dirname(dir)) {
|
|
167
|
+
instalada = await readJson(
|
|
168
|
+
path.join(dir, "node_modules", "tailwindcss", "package.json"),
|
|
169
|
+
);
|
|
170
|
+
if (instalada !== null || path.dirname(dir) === dir) break;
|
|
171
|
+
}
|
|
172
|
+
const m =
|
|
173
|
+
typeof instalada?.version === "string"
|
|
174
|
+
? instalada.version.match(/^(\d+)\./)
|
|
175
|
+
: null;
|
|
176
|
+
if (m) return Number(m[1]);
|
|
177
|
+
|
|
178
|
+
const deps = {
|
|
179
|
+
...(pkg?.dependencies ?? {}),
|
|
180
|
+
...(pkg?.devDependencies ?? {}),
|
|
181
|
+
...(pkg?.peerDependencies ?? {}),
|
|
182
|
+
...(pkg?.optionalDependencies ?? {}),
|
|
183
|
+
};
|
|
184
|
+
// `@tailwindcss/vite` e `@tailwindcss/postcss` só existem na v4 — sinal direto.
|
|
185
|
+
if (deps["@tailwindcss/vite"] || deps["@tailwindcss/postcss"]) return 4;
|
|
186
|
+
const range = deps["tailwindcss"];
|
|
187
|
+
if (typeof range !== "string") return null;
|
|
188
|
+
// O major é o 1º número do range, com ou sem ponto depois: `^4.1.0`, `~4`, `>=4`, `4`.
|
|
189
|
+
// Protocolo sem número (`catalog:`, `workspace:*`, `latest`, `*`) → null, e o chamador
|
|
190
|
+
// mantém o caminho v3 — sem nada instalado nem declarado, seria palpite.
|
|
191
|
+
const r = range.match(/(\d+)/);
|
|
192
|
+
return r ? Number(r[1]) : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** tailwind.config pro consumidor: preset do Opus + glob da fonte dos componentes.
|
|
196
|
+
* Glob aponta pro pacote instalado (node_modules) — caso do projeto-cliente. */
|
|
197
|
+
function tailwindConfigTemplate() {
|
|
198
|
+
return `import preset from '@softize/opus/ui/preset'
|
|
199
|
+
|
|
200
|
+
/** @type {import('tailwindcss').Config} */
|
|
201
|
+
export default {
|
|
202
|
+
// Cores/raio/darkMode/animação vêm do preset do Opus. Estenda em \`theme.extend\`.
|
|
203
|
+
presets: [preset],
|
|
204
|
+
content: [
|
|
205
|
+
'./index.html',
|
|
206
|
+
'./src/**/*.{ts,tsx}',
|
|
207
|
+
// Escaneia a fonte dos componentes do Opus pra gerar as classes que usam.
|
|
208
|
+
'./node_modules/@softize/opus/src/ui/**/*.{ts,tsx}',
|
|
209
|
+
],
|
|
210
|
+
plugins: [],
|
|
211
|
+
}
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Pastas cujo CSS é fixture/exemplo, não a folha que a build usa. */
|
|
216
|
+
const CSS_IGNORADAS = new Set([
|
|
217
|
+
"__tests__",
|
|
218
|
+
"tests",
|
|
219
|
+
"test",
|
|
220
|
+
"stories",
|
|
221
|
+
"__mocks__",
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* TODO CSS de `src/`, do mais raso pro mais fundo, com o conteúdo lido. Varrer em vez de
|
|
226
|
+
* casar uma lista de nomes: `index.css` cobre o caso comum, mas `src/styles/app.css` e
|
|
227
|
+
* nome próprio são igualmente legítimos, e não achar o arquivo vira aviso falso mandando
|
|
228
|
+
* criar o que já existe (foi o defeito da versão anterior, só que espelhado).
|
|
229
|
+
* Não desce em `node_modules`, pasta oculta nem pasta de teste.
|
|
230
|
+
*/
|
|
231
|
+
async function collectCss(projectDir, dir = "src", profundidade = 0) {
|
|
232
|
+
if (profundidade > 3) return [];
|
|
233
|
+
let entradas;
|
|
234
|
+
try {
|
|
235
|
+
entradas = await fs.readdir(path.join(projectDir, dir), {
|
|
236
|
+
withFileTypes: true,
|
|
237
|
+
});
|
|
238
|
+
} catch {
|
|
239
|
+
return []; // sem `src/` → nada a varrer.
|
|
240
|
+
}
|
|
241
|
+
const aqui = [];
|
|
242
|
+
const abaixo = [];
|
|
243
|
+
for (const e of entradas.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
244
|
+
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
245
|
+
if (e.isDirectory() && CSS_IGNORADAS.has(e.name)) continue;
|
|
246
|
+
const rel = `${dir}/${e.name}`;
|
|
247
|
+
if (e.isDirectory()) {
|
|
248
|
+
abaixo.push(...(await collectCss(projectDir, rel, profundidade + 1)));
|
|
249
|
+
} else if (e.name.endsWith(".css")) {
|
|
250
|
+
aqui.push({
|
|
251
|
+
file: rel,
|
|
252
|
+
text: await fs.readFile(path.join(projectDir, rel), "utf-8"),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Arquivo raso antes de arquivo fundo: `src/index.css` ganha de `src/assets/reset.css`,
|
|
257
|
+
// que ordenaria antes se a lista fosse só alfabética (diretório vem antes do irmão).
|
|
258
|
+
return [...aqui, ...abaixo];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* O CSS de ENTRADA: o que importa o tema do Opus, senão o que puxa o Tailwind, senão o
|
|
263
|
+
* mais raso. Num app com reset + entry, é esse que responde a pergunta.
|
|
264
|
+
*/
|
|
265
|
+
function pickCss(achados) {
|
|
266
|
+
if (achados.length === 0) return null;
|
|
267
|
+
return (
|
|
268
|
+
achados.find((c) => c.text.includes("@softize/opus/ui/theme.css")) ??
|
|
269
|
+
achados.find((c) => /@import\s+['"]tailwindcss['"]/.test(c.text)) ??
|
|
270
|
+
achados[0]
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Planta a fundação de UI num app web — idempotente, SEM clobber: cria o que falta,
|
|
276
|
+
* AVISA o que diverge (não edita arquivo do usuário). Skip se não for app web.
|
|
277
|
+
* @returns {Promise<{applicable:boolean, created:string[], warnings:string[]}>}
|
|
278
|
+
*/
|
|
279
|
+
export async function setupUiFoundation(projectDir) {
|
|
280
|
+
const hasIndexHtml = await exists(path.join(projectDir, "index.html"));
|
|
281
|
+
let twConfig = null;
|
|
282
|
+
for (const c of TAILWIND_CONFIGS) {
|
|
283
|
+
if (await exists(path.join(projectDir, c))) {
|
|
284
|
+
twConfig = c;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Nem app web (sem tailwind config nem index.html) → fundação de UI não se aplica.
|
|
289
|
+
if (!twConfig && !hasIndexHtml)
|
|
290
|
+
return { applicable: false, created: [], warnings: [] };
|
|
291
|
+
|
|
292
|
+
const created = [];
|
|
293
|
+
const warnings = [];
|
|
294
|
+
const pkg = await readJson(path.join(projectDir, "package.json"));
|
|
295
|
+
const major = await tailwindMajor(projectDir, pkg);
|
|
296
|
+
const v4 = major !== null && major >= 4;
|
|
297
|
+
const todosCss = v4 ? await collectCss(projectDir) : [];
|
|
298
|
+
const css = pickCss(todosCss);
|
|
299
|
+
|
|
300
|
+
// 1. tailwind.config — só faz sentido até a v3. Na v4 o config foi substituído pelo CSS,
|
|
301
|
+
// então criar o arquivo semearia um artefato que a build ignora.
|
|
302
|
+
if (v4) {
|
|
303
|
+
// O equivalente v4 do `content` glob: o Tailwind pula node_modules, e os componentes do
|
|
304
|
+
// Opus moram lá — sem o `@source` as classes deles não são geradas e a tela sai crua.
|
|
305
|
+
if (css === null) {
|
|
306
|
+
warnings.push(
|
|
307
|
+
"crie o CSS de entrada com `@import '@softize/opus/ui/theme.css'` + `@source '../node_modules/@softize/opus/src/ui/**/*.{ts,tsx}'`.",
|
|
308
|
+
);
|
|
309
|
+
} else if (!css.text.includes("@softize/opus/src/ui")) {
|
|
310
|
+
warnings.push(
|
|
311
|
+
`${css.file}: aponte a fonte dos componentes — \`@source '../node_modules/@softize/opus/src/ui/**/*.{ts,tsx}'\` (o Tailwind v4 ignora node_modules).`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
// Config presente numa build v4: ou o app o pluga por `@config`, ou o arquivo é inerte
|
|
315
|
+
// — e provavelmente foi ESTE setup que o plantou, antes de saber distinguir as duas
|
|
316
|
+
// gerações. Avisar é o que fecha o ciclo pra quem já tomou o config fantasma.
|
|
317
|
+
//
|
|
318
|
+
// O `@config` é procurado em TODOS os CSS, não só no de entrada: ele pode morar num
|
|
319
|
+
// parcial, e errar aqui manda apagar um arquivo EM USO — o pior aviso falso possível.
|
|
320
|
+
const plugado = todosCss.some((c) => /@config\s*['"]/.test(c.text));
|
|
321
|
+
if (twConfig !== null && !plugado) {
|
|
322
|
+
warnings.push(
|
|
323
|
+
`${twConfig}: o Tailwind v4 ignora este arquivo (a menos que você o pluge com \`@config\` no CSS). Se veio do \`opus setup\`, pode apagar — na v4 o tema e o \`@source\` moram no CSS.`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
} else if (!twConfig) {
|
|
327
|
+
await fs.writeFile(
|
|
328
|
+
path.join(projectDir, "tailwind.config.js"),
|
|
329
|
+
tailwindConfigTemplate(),
|
|
330
|
+
);
|
|
331
|
+
created.push("tailwind.config.js");
|
|
332
|
+
} else {
|
|
333
|
+
const txt = await fs.readFile(path.join(projectDir, twConfig), "utf-8");
|
|
334
|
+
if (!txt.includes("@softize/opus/ui/preset")) {
|
|
335
|
+
warnings.push(
|
|
336
|
+
`${twConfig}: estenda o preset — \`import preset from '@softize/opus/ui/preset'\` + \`presets: [preset]\` + glob \`./node_modules/@softize/opus/src/ui/**/*.{ts,tsx}\` no content.`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 2. Tema canônico — na v4 entra por `@import` no CSS, até a v3 por `import` no entry.
|
|
342
|
+
// Procurar só no entry .tsx dava aviso FALSO em todo app v4, que faz certo pelo CSS.
|
|
343
|
+
if (css !== null) {
|
|
344
|
+
if (!css.text.includes("@softize/opus/ui/theme.css")) {
|
|
345
|
+
warnings.push(
|
|
346
|
+
`${css.file}: importe o tema — \`@import '@softize/opus/ui/theme.css';\` no topo.`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
let entry = null;
|
|
351
|
+
for (const e of ENTRY_FILES) {
|
|
352
|
+
if (await exists(path.join(projectDir, e))) {
|
|
353
|
+
entry = e;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// App v4 sem CSS de entrada já foi avisado no passo 1 — não repete.
|
|
358
|
+
if (entry !== null && !v4) {
|
|
359
|
+
const txt = await fs.readFile(path.join(projectDir, entry), "utf-8");
|
|
360
|
+
if (!txt.includes("@softize/opus/ui/theme.css")) {
|
|
361
|
+
warnings.push(
|
|
362
|
+
`${entry}: importe o tema — \`import '@softize/opus/ui/theme.css'\` (antes do seu CSS).`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
} else if (entry === null && !v4) {
|
|
366
|
+
warnings.push(
|
|
367
|
+
"importe `@softize/opus/ui/theme.css` no seu entry (ex.: src/main.tsx).",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 3. allowImportingTsExtensions (o Opus exporta source .ts/.tsx; o consumidor compila).
|
|
373
|
+
const tsconfigPath = path.join(projectDir, "tsconfig.json");
|
|
374
|
+
if (await exists(tsconfigPath)) {
|
|
375
|
+
const txt = await fs.readFile(tsconfigPath, "utf-8");
|
|
376
|
+
if (!txt.includes("allowImportingTsExtensions")) {
|
|
377
|
+
warnings.push(
|
|
378
|
+
'tsconfig.json: adicione `"allowImportingTsExtensions": true`.',
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 4. dep @softize/opus (o `pkg` já foi lido no passo 1, pra saber a geração do Tailwind).
|
|
384
|
+
const deps = {
|
|
385
|
+
...(pkg?.dependencies ?? {}),
|
|
386
|
+
...(pkg?.devDependencies ?? {}),
|
|
387
|
+
};
|
|
388
|
+
if (!deps["@softize/opus"]) {
|
|
389
|
+
warnings.push(
|
|
390
|
+
"package.json: falta a dep `@softize/opus` — rode `pnpm add @softize/opus`.",
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return { applicable: true, created, warnings };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Inicializa/atualiza um projeto pra usar a base.
|
|
399
|
+
* @returns {Promise<{version:string, created:string[], synced:string[], warnings:string[], wasInitialized:boolean}>}
|
|
400
|
+
*/
|
|
401
|
+
/** Anexa padrões ao .git/info/exclude do repo (exclusão local, fora do .gitignore). */
|
|
402
|
+
async function gitExclude(repoRoot, patterns) {
|
|
403
|
+
const excludePath = path.join(repoRoot, ".git", "info", "exclude");
|
|
404
|
+
if (!(await exists(path.join(repoRoot, ".git")))) return;
|
|
405
|
+
let cur = "";
|
|
406
|
+
try {
|
|
407
|
+
cur = await fs.readFile(excludePath, "utf-8");
|
|
408
|
+
} catch {
|
|
409
|
+
/* exclude novo. */
|
|
410
|
+
}
|
|
411
|
+
const have = new Set(cur.split("\n").map((l) => l.trim()));
|
|
412
|
+
const add = patterns.filter((p) => !have.has(p));
|
|
413
|
+
if (add.length === 0) return;
|
|
414
|
+
await fs.mkdir(path.dirname(excludePath), { recursive: true });
|
|
415
|
+
await fs.writeFile(
|
|
416
|
+
excludePath,
|
|
417
|
+
`${cur}${cur.endsWith("\n") || cur === "" ? "" : "\n"}${add.join("\n")}\n`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Semeia as skills DO PACOTE (registry/skills) em `.claude/skills/` do repo, sem
|
|
423
|
+
* clobber (o Maestro é o reconciliador quando o projeto for regido). Untracked por
|
|
424
|
+
* design: projeção da base, não código do projeto — vai pro info/exclude.
|
|
425
|
+
*/
|
|
426
|
+
async function seedRegistrySkills(registryDir, repoRoot) {
|
|
427
|
+
const from = path.join(registryDir, "skills");
|
|
428
|
+
if (!(await exists(from))) return 0;
|
|
429
|
+
let seeded = 0;
|
|
430
|
+
for (const slug of await fs.readdir(from)) {
|
|
431
|
+
const src = path.join(from, slug);
|
|
432
|
+
if (!(await fs.stat(src)).isDirectory()) continue;
|
|
433
|
+
const dest = path.join(repoRoot, ".claude", "skills", slug);
|
|
434
|
+
if (await exists(dest)) continue;
|
|
435
|
+
await fs.cp(src, dest, { recursive: true });
|
|
436
|
+
seeded += 1;
|
|
437
|
+
}
|
|
438
|
+
if (seeded > 0) await gitExclude(repoRoot, [".claude/skills/"]);
|
|
439
|
+
return seeded;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export async function initProject(registryDir, projectDir) {
|
|
443
|
+
const created = [];
|
|
444
|
+
const synced = [];
|
|
445
|
+
const pkg = await readJson(path.join(registryDir, "..", "package.json"));
|
|
446
|
+
const version = pkg?.version ?? "0.0.0";
|
|
447
|
+
|
|
448
|
+
// Marcador (sempre regravado — é da base; pina a versão).
|
|
449
|
+
const markerPath = path.join(projectDir, MARKER);
|
|
450
|
+
const prev = await readJson(markerPath);
|
|
451
|
+
await fs.writeFile(
|
|
452
|
+
markerPath,
|
|
453
|
+
JSON.stringify({ base: "@softize/opus", baseVersion: version }, null, 2) +
|
|
454
|
+
"\n",
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
// CLAUDE.md — cria se faltar; se existir, atualiza SÓ o bloco gerenciado (o resto é
|
|
458
|
+
// do projeto). Arquivo antigo sem marcadores = 100% do projeto, intocado. A domain.md
|
|
459
|
+
// foi APOSENTADA (13/06/2026): a spec vive nas declarações (`description`).
|
|
460
|
+
const claudePath = path.join(projectDir, "CLAUDE.md");
|
|
461
|
+
const block = claudeMdBaseBlock();
|
|
462
|
+
if (!(await exists(claudePath))) {
|
|
463
|
+
await fs.writeFile(claudePath, `${block}\n`);
|
|
464
|
+
created.push("CLAUDE.md");
|
|
465
|
+
} else {
|
|
466
|
+
const cur = await fs.readFile(claudePath, "utf-8");
|
|
467
|
+
if (BASE_BLOCK_RE.test(cur)) {
|
|
468
|
+
// Replacement por função: o bloco tem `$` em potencial (template) — literal, sem
|
|
469
|
+
// os padrões especiais do replace.
|
|
470
|
+
const next = cur.replace(BASE_BLOCK_RE, () => block);
|
|
471
|
+
if (next !== cur) {
|
|
472
|
+
await fs.writeFile(claudePath, next);
|
|
473
|
+
synced.push("CLAUDE.md");
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Dia zero REPO-level — mas só do que é DO PROJETO (tracked, viaja no git):
|
|
479
|
+
// • semente da memória versionada (política da skill memory; sem o dir, o hook
|
|
480
|
+
// link-memory-on-start no-opa e a sessão roda amnésica);
|
|
481
|
+
// • CI de fábrica (check.yml) se faltar.
|
|
482
|
+
// A camada-base do ADMIN (agents + skills de metodologia) segue sendo do MAESTRO;
|
|
483
|
+
// as skills DO PACOTE (registry/skills) entram aqui: esqueleto standalone não pode
|
|
484
|
+
// nascer sem o que o próprio tarball carrega (projeção untracked, git-excluded —
|
|
485
|
+
// o Maestro re-materializa por cima quando reger o projeto).
|
|
486
|
+
const repoRoot = (await repoRootOf(projectDir)) ?? projectDir;
|
|
487
|
+
const seededSkills = await seedRegistrySkills(registryDir, repoRoot);
|
|
488
|
+
if (seededSkills > 0)
|
|
489
|
+
created.push(`.claude/skills/ (${seededSkills} da base, untracked)`);
|
|
490
|
+
const memDir = path.join(repoRoot, ".claude", "memory");
|
|
491
|
+
if (!(await exists(memDir))) {
|
|
492
|
+
await fs.mkdir(memDir, { recursive: true });
|
|
493
|
+
await fs.writeFile(path.join(memDir, "MEMORY.md"), "");
|
|
494
|
+
created.push(".claude/memory/ (repo)");
|
|
495
|
+
}
|
|
496
|
+
const ciPath = path.join(repoRoot, ".github", "workflows", "check.yml");
|
|
497
|
+
if (!(await exists(ciPath))) {
|
|
498
|
+
await fs.mkdir(path.dirname(ciPath), { recursive: true });
|
|
499
|
+
await fs.writeFile(ciPath, checkWorkflowTemplate());
|
|
500
|
+
created.push(".github/workflows/check.yml (repo)");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Avisos de dia zero (sem clobber — igual à fundação de UI): o que falta plugar à mão.
|
|
504
|
+
const warnings = [];
|
|
505
|
+
const appPkg = await readJson(path.join(projectDir, "package.json"));
|
|
506
|
+
if (appPkg !== null && typeof appPkg.scripts?.test !== "string") {
|
|
507
|
+
warnings.push(
|
|
508
|
+
"package.json: sem script `test` — o gate de entrega não confere a suíte sem ele.",
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (!prev) created.unshift(MARKER);
|
|
513
|
+
return { version, created, synced, warnings, wasInitialized: prev !== null };
|
|
514
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opus introspect — modelo da estrutura de um projeto Opus (estático, TS compiler API).
|
|
3
|
+
*
|
|
4
|
+
* Fonte ÚNICA da estrutura: actions + reactions + schedules + wiring. É o que o MCP
|
|
5
|
+
* vai expor e o Maestro vai renderizar (hoje o Maestro tem scanner próprio, mais rico
|
|
6
|
+
* — models/dicts/relations; a unificação migra o Maestro pra cá). Passo 1: o Opus
|
|
7
|
+
* passa a OWN a introspecção dos 3 primitivos + a causalidade (emits→on, schedule→action).
|
|
8
|
+
*
|
|
9
|
+
* opus introspect [dir] [--json]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { promises as fs } from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import ts from 'typescript'
|
|
15
|
+
import { walkTsFiles } from './check.mjs'
|
|
16
|
+
|
|
17
|
+
function prop(obj, key, sf) {
|
|
18
|
+
const p = obj.properties.find((x) => ts.isPropertyAssignment(x) && x.name && x.name.getText(sf) === key)
|
|
19
|
+
return p ? p.initializer : undefined
|
|
20
|
+
}
|
|
21
|
+
function asString(init) {
|
|
22
|
+
return init && ts.isStringLiteral(init) ? init.text : null
|
|
23
|
+
}
|
|
24
|
+
function asStringArray(init) {
|
|
25
|
+
if (!init) return []
|
|
26
|
+
if (ts.isStringLiteral(init)) return [init.text]
|
|
27
|
+
if (ts.isArrayLiteralExpression(init)) return init.elements.filter(ts.isStringLiteral).map((e) => e.text)
|
|
28
|
+
return []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Extrai actions/reactions/schedules de um source. Puro/sintático — testável. */
|
|
32
|
+
export function parseStructure(fileName, sourceText) {
|
|
33
|
+
const sf = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
34
|
+
const out = { actions: [], reactions: [], schedules: [] }
|
|
35
|
+
const line = (n) => sf.getLineAndCharacterOfPosition(n.getStart(sf)).line + 1
|
|
36
|
+
|
|
37
|
+
function visit(node) {
|
|
38
|
+
if (
|
|
39
|
+
ts.isCallExpression(node) &&
|
|
40
|
+
ts.isIdentifier(node.expression) &&
|
|
41
|
+
node.arguments.length > 0 &&
|
|
42
|
+
ts.isObjectLiteralExpression(node.arguments[0])
|
|
43
|
+
) {
|
|
44
|
+
const fn = node.expression.text
|
|
45
|
+
const obj = node.arguments[0]
|
|
46
|
+
if (fn === 'defineAction') {
|
|
47
|
+
out.actions.push({
|
|
48
|
+
name: asString(prop(obj, 'name', sf)),
|
|
49
|
+
kind: asString(prop(obj, 'kind', sf)),
|
|
50
|
+
emits: asStringArray(prop(obj, 'emits', sf)),
|
|
51
|
+
line: line(node),
|
|
52
|
+
})
|
|
53
|
+
} else if (fn === 'defineReaction') {
|
|
54
|
+
out.reactions.push({
|
|
55
|
+
name: asString(prop(obj, 'name', sf)),
|
|
56
|
+
on: asStringArray(prop(obj, 'on', sf)),
|
|
57
|
+
line: line(node),
|
|
58
|
+
})
|
|
59
|
+
} else if (fn === 'defineSchedule') {
|
|
60
|
+
out.schedules.push({
|
|
61
|
+
name: asString(prop(obj, 'name', sf)),
|
|
62
|
+
action: asString(prop(obj, 'action', sf)),
|
|
63
|
+
cron: asString(prop(obj, 'cron', sf)),
|
|
64
|
+
every: asString(prop(obj, 'every', sf)),
|
|
65
|
+
line: line(node),
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
ts.forEachChild(node, visit)
|
|
70
|
+
}
|
|
71
|
+
visit(sf)
|
|
72
|
+
return out
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Deriva o wiring (causalidade) do modelo: emits→on (evento) e schedule→action. */
|
|
76
|
+
export function deriveWiring(model) {
|
|
77
|
+
const edges = []
|
|
78
|
+
// schedule → action
|
|
79
|
+
for (const s of model.schedules) {
|
|
80
|
+
if (s.action) edges.push({ kind: 'schedule', from: s.name, to: s.action, via: s.cron ?? s.every ?? 'tempo' })
|
|
81
|
+
}
|
|
82
|
+
// evento: action.emits → reaction.on
|
|
83
|
+
for (const a of model.actions) {
|
|
84
|
+
for (const ev of a.emits ?? []) {
|
|
85
|
+
for (const r of model.reactions) {
|
|
86
|
+
if ((r.on ?? []).includes(ev)) edges.push({ kind: 'event', from: a.name, to: r.name, via: ev })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return edges
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Escaneia um diretório → modelo agregado + wiring. */
|
|
94
|
+
export async function introspect(rootDir) {
|
|
95
|
+
const files = await walkTsFiles(rootDir)
|
|
96
|
+
const model = { actions: [], reactions: [], schedules: [] }
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
const text = await fs.readFile(file, 'utf-8')
|
|
99
|
+
if (!/define(Action|Reaction|Schedule)/.test(text)) continue
|
|
100
|
+
const s = parseStructure(file, text)
|
|
101
|
+
const rel = path.relative(rootDir, file)
|
|
102
|
+
for (const a of s.actions) model.actions.push({ ...a, file: rel })
|
|
103
|
+
for (const r of s.reactions) model.reactions.push({ ...r, file: rel })
|
|
104
|
+
for (const sc of s.schedules) model.schedules.push({ ...sc, file: rel })
|
|
105
|
+
}
|
|
106
|
+
return { ...model, wiring: deriveWiring(model) }
|
|
107
|
+
}
|