@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.
Files changed (286) hide show
  1. package/CHANGELOG.md +1616 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -0
  4. package/bin/cli.mjs +528 -0
  5. package/bin/lib/check.mjs +307 -0
  6. package/bin/lib/components.mjs +151 -0
  7. package/bin/lib/create.mjs +208 -0
  8. package/bin/lib/db-check-runner.mjs +86 -0
  9. package/bin/lib/db-migrate-runner.mjs +89 -0
  10. package/bin/lib/db-scaffold-runner.mjs +84 -0
  11. package/bin/lib/db.mjs +261 -0
  12. package/bin/lib/docs-include.mjs +48 -0
  13. package/bin/lib/gen-dicts.mjs +134 -0
  14. package/bin/lib/gen-docs.mjs +288 -0
  15. package/bin/lib/gen-manifest.mjs +102 -0
  16. package/bin/lib/gen-openapi.mjs +195 -0
  17. package/bin/lib/gen-runner.mjs +472 -0
  18. package/bin/lib/gen-stubs.mjs +463 -0
  19. package/bin/lib/gen.mjs +311 -0
  20. package/bin/lib/init.mjs +514 -0
  21. package/bin/lib/introspect.mjs +107 -0
  22. package/bin/lib/mcp.mjs +85 -0
  23. package/bin/lib/postinstall.mjs +56 -0
  24. package/docs/chat-event-protocol.md +85 -0
  25. package/docs/code-style.md +16 -0
  26. package/docs/data-layer.md +246 -0
  27. package/docs/ownership-vs-shadcn-lock.md +102 -0
  28. package/docs/protocol.md +2053 -0
  29. package/docs/releasing.md +110 -0
  30. package/docs/shellnav.md +131 -0
  31. package/package.json +338 -0
  32. package/registry/hooks/hooks.json +26 -0
  33. package/registry/hooks/link-memory-on-start.mjs +46 -0
  34. package/registry/hooks/opus-check-on-stop.mjs +114 -0
  35. package/registry/skills/create-action/SKILL.md +49 -0
  36. package/registry/skills/create-action/scaffold.mjs +122 -0
  37. package/registry/templates/app/_gitignore +3 -0
  38. package/registry/templates/app/_npmrc +1 -0
  39. package/registry/templates/app/_opus/_gitignore +5 -0
  40. package/registry/templates/app/_prettierrc.json +6 -0
  41. package/registry/templates/app/index.html +13 -0
  42. package/registry/templates/app/opus.config.ts +16 -0
  43. package/registry/templates/app/package.json +43 -0
  44. package/registry/templates/app/pnpm-workspace.yaml +11 -0
  45. package/registry/templates/app/public/favicon.svg +4 -0
  46. package/registry/templates/app/src/App.tsx +37 -0
  47. package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
  48. package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
  49. package/registry/templates/app/src/domains/tasks/index.ts +13 -0
  50. package/registry/templates/app/src/index.css +18 -0
  51. package/registry/templates/app/src/main.tsx +25 -0
  52. package/registry/templates/app/tsconfig.json +20 -0
  53. package/registry/templates/app/vite.config.ts +46 -0
  54. package/registry/templates/monorepo/_gitignore +3 -0
  55. package/registry/templates/monorepo/_npmrc +1 -0
  56. package/registry/templates/monorepo/package.json +9 -0
  57. package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
  58. package/src/ai/ask.ts +64 -0
  59. package/src/ai/drivers/anthropic.ts +309 -0
  60. package/src/ai/index.ts +17 -0
  61. package/src/audit/drivers/console.ts +117 -0
  62. package/src/audit/drivers/pg.ts +172 -0
  63. package/src/audit/index.ts +51 -0
  64. package/src/auth/drivers/better-auth.ts +103 -0
  65. package/src/auth/drivers/jwt.ts +188 -0
  66. package/src/auth/index.ts +9 -0
  67. package/src/client/drivers/fetch.ts +202 -0
  68. package/src/client/index.ts +22 -0
  69. package/src/core/actions.ts +110 -0
  70. package/src/core/audit.ts +239 -0
  71. package/src/core/contracts.ts +137 -0
  72. package/src/core/domain.ts +310 -0
  73. package/src/core/errors.ts +181 -0
  74. package/src/core/index.ts +174 -0
  75. package/src/core/logical-type.ts +31 -0
  76. package/src/core/reactions.ts +81 -0
  77. package/src/core/runtime.ts +1167 -0
  78. package/src/core/schedules.ts +41 -0
  79. package/src/core/types.ts +1356 -0
  80. package/src/data/drivers/kysely.ts +389 -0
  81. package/src/data/index.ts +10 -0
  82. package/src/data/readonly-pool.ts +160 -0
  83. package/src/dsl/eval.ts +136 -0
  84. package/src/dsl/index.ts +29 -0
  85. package/src/dsl/kysely.ts +230 -0
  86. package/src/dsl/loads.ts +123 -0
  87. package/src/dsl/parser.ts +423 -0
  88. package/src/dsl/types.ts +113 -0
  89. package/src/events/drivers/mitt.ts +70 -0
  90. package/src/events/index.ts +9 -0
  91. package/src/log/drivers/pino.ts +57 -0
  92. package/src/log/index.ts +9 -0
  93. package/src/mcp/index.ts +62 -0
  94. package/src/queue/drivers/bullmq.ts +190 -0
  95. package/src/queue/index.ts +9 -0
  96. package/src/scheduler/drivers/node-cron.ts +93 -0
  97. package/src/scheduler/every.ts +45 -0
  98. package/src/scheduler/index.ts +9 -0
  99. package/src/schema/drivers/zod.ts +765 -0
  100. package/src/schema/entity.ts +439 -0
  101. package/src/schema/format/locale.ts +144 -0
  102. package/src/schema/index.ts +65 -0
  103. package/src/schema/openapi.ts +302 -0
  104. package/src/schema/scaffold.ts +160 -0
  105. package/src/server/drivers/fastify.ts +224 -0
  106. package/src/server/drivers/node.ts +386 -0
  107. package/src/server/index.ts +142 -0
  108. package/src/storage/drivers/fs.ts +90 -0
  109. package/src/storage/drivers/s3.ts +117 -0
  110. package/src/storage/index.ts +27 -0
  111. package/src/testing/fake.ts +298 -0
  112. package/src/testing/index.ts +324 -0
  113. package/src/ui/components/patterns/action-form-card.tsx +48 -0
  114. package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
  115. package/src/ui/components/patterns/app-shell.tsx +227 -0
  116. package/src/ui/components/patterns/confirm.tsx +226 -0
  117. package/src/ui/components/patterns/data-state.tsx +75 -0
  118. package/src/ui/components/patterns/form-dialog.tsx +64 -0
  119. package/src/ui/components/patterns/form.tsx +584 -0
  120. package/src/ui/components/patterns/list.tsx +1488 -0
  121. package/src/ui/components/patterns/page.tsx +46 -0
  122. package/src/ui/components/patterns/section-shell.tsx +246 -0
  123. package/src/ui/components/patterns/shell-nav.tsx +150 -0
  124. package/src/ui/components/patterns/sidebar.tsx +89 -0
  125. package/src/ui/components/patterns/split.tsx +93 -0
  126. package/src/ui/components/patterns/trigger.tsx +196 -0
  127. package/src/ui/components/patterns/view.tsx +84 -0
  128. package/src/ui/components/primitives/accordion.tsx +64 -0
  129. package/src/ui/components/primitives/alert-dialog.tsx +190 -0
  130. package/src/ui/components/primitives/alert.tsx +116 -0
  131. package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
  132. package/src/ui/components/primitives/avatar.tsx +107 -0
  133. package/src/ui/components/primitives/badge.tsx +37 -0
  134. package/src/ui/components/primitives/breadcrumb.tsx +109 -0
  135. package/src/ui/components/primitives/button-group.tsx +83 -0
  136. package/src/ui/components/primitives/button.tsx +102 -0
  137. package/src/ui/components/primitives/calendar.tsx +218 -0
  138. package/src/ui/components/primitives/card.tsx +56 -0
  139. package/src/ui/components/primitives/carousel.tsx +239 -0
  140. package/src/ui/components/primitives/chat.tsx +407 -0
  141. package/src/ui/components/primitives/checkbox.tsx +30 -0
  142. package/src/ui/components/primitives/collapsible.tsx +31 -0
  143. package/src/ui/components/primitives/command.tsx +182 -0
  144. package/src/ui/components/primitives/composer.tsx +121 -0
  145. package/src/ui/components/primitives/copyable.tsx +50 -0
  146. package/src/ui/components/primitives/dialog.tsx +147 -0
  147. package/src/ui/components/primitives/drawer.tsx +141 -0
  148. package/src/ui/components/primitives/empty.tsx +104 -0
  149. package/src/ui/components/primitives/field.tsx +246 -0
  150. package/src/ui/components/primitives/icon-picker.tsx +180 -0
  151. package/src/ui/components/primitives/input-group.tsx +168 -0
  152. package/src/ui/components/primitives/input-otp.tsx +75 -0
  153. package/src/ui/components/primitives/input.tsx +72 -0
  154. package/src/ui/components/primitives/item.tsx +193 -0
  155. package/src/ui/components/primitives/kbd.tsx +28 -0
  156. package/src/ui/components/primitives/label.tsx +22 -0
  157. package/src/ui/components/primitives/markdown.tsx +35 -0
  158. package/src/ui/components/primitives/menu.tsx +255 -0
  159. package/src/ui/components/primitives/pagination.tsx +127 -0
  160. package/src/ui/components/primitives/popover.tsx +87 -0
  161. package/src/ui/components/primitives/progress.tsx +29 -0
  162. package/src/ui/components/primitives/radio-group.tsx +43 -0
  163. package/src/ui/components/primitives/resizable.tsx +51 -0
  164. package/src/ui/components/primitives/scroll-area.tsx +56 -0
  165. package/src/ui/components/primitives/select.tsx +479 -0
  166. package/src/ui/components/primitives/separator.tsx +26 -0
  167. package/src/ui/components/primitives/skeleton.tsx +13 -0
  168. package/src/ui/components/primitives/slider.tsx +61 -0
  169. package/src/ui/components/primitives/sonner.tsx +46 -0
  170. package/src/ui/components/primitives/spinner.tsx +29 -0
  171. package/src/ui/components/primitives/switch.tsx +33 -0
  172. package/src/ui/components/primitives/table.tsx +114 -0
  173. package/src/ui/components/primitives/tabs.tsx +104 -0
  174. package/src/ui/components/primitives/textarea.tsx +18 -0
  175. package/src/ui/components/primitives/toggle-group.tsx +81 -0
  176. package/src/ui/components/primitives/toggle.tsx +45 -0
  177. package/src/ui/components/primitives/tooltip.tsx +55 -0
  178. package/src/ui/components/primitives/truncate.tsx +49 -0
  179. package/src/ui/docs/DocBrowser.tsx +90 -0
  180. package/src/ui/docs/changelog.tsx +80 -0
  181. package/src/ui/docs/content/accordion.md +86 -0
  182. package/src/ui/docs/content/action-form-card.md +24 -0
  183. package/src/ui/docs/content/action-form-dialog.md +30 -0
  184. package/src/ui/docs/content/action-form.md +125 -0
  185. package/src/ui/docs/content/action-list-dialog.md +68 -0
  186. package/src/ui/docs/content/action-list.md +194 -0
  187. package/src/ui/docs/content/action-trigger.md +72 -0
  188. package/src/ui/docs/content/action-view.md +47 -0
  189. package/src/ui/docs/content/actions.md +138 -0
  190. package/src/ui/docs/content/ai.md +112 -0
  191. package/src/ui/docs/content/alert-dialog.md +73 -0
  192. package/src/ui/docs/content/alert.md +69 -0
  193. package/src/ui/docs/content/app-shell.md +155 -0
  194. package/src/ui/docs/content/aspect-ratio.md +66 -0
  195. package/src/ui/docs/content/audit.md +84 -0
  196. package/src/ui/docs/content/auth.md +70 -0
  197. package/src/ui/docs/content/avatar.md +94 -0
  198. package/src/ui/docs/content/badge.md +48 -0
  199. package/src/ui/docs/content/breadcrumb.md +87 -0
  200. package/src/ui/docs/content/button-group.md +71 -0
  201. package/src/ui/docs/content/button.md +60 -0
  202. package/src/ui/docs/content/calendar.md +62 -0
  203. package/src/ui/docs/content/card.md +49 -0
  204. package/src/ui/docs/content/carousel.md +85 -0
  205. package/src/ui/docs/content/chat.md +69 -0
  206. package/src/ui/docs/content/checkbox.md +75 -0
  207. package/src/ui/docs/content/cli.md +58 -0
  208. package/src/ui/docs/content/collapsible.md +64 -0
  209. package/src/ui/docs/content/command.md +56 -0
  210. package/src/ui/docs/content/composer.md +50 -0
  211. package/src/ui/docs/content/confirm.md +120 -0
  212. package/src/ui/docs/content/copyable.md +30 -0
  213. package/src/ui/docs/content/customization.md +110 -0
  214. package/src/ui/docs/content/cycle.md +34 -0
  215. package/src/ui/docs/content/data-state.md +47 -0
  216. package/src/ui/docs/content/data.md +99 -0
  217. package/src/ui/docs/content/dialog.md +60 -0
  218. package/src/ui/docs/content/drawer.md +55 -0
  219. package/src/ui/docs/content/empty.md +66 -0
  220. package/src/ui/docs/content/events.md +61 -0
  221. package/src/ui/docs/content/field.md +58 -0
  222. package/src/ui/docs/content/getting-started.md +109 -0
  223. package/src/ui/docs/content/icon-picker.md +51 -0
  224. package/src/ui/docs/content/input-group.md +78 -0
  225. package/src/ui/docs/content/input-otp.md +72 -0
  226. package/src/ui/docs/content/input.md +78 -0
  227. package/src/ui/docs/content/item.md +84 -0
  228. package/src/ui/docs/content/kbd.md +62 -0
  229. package/src/ui/docs/content/label.md +32 -0
  230. package/src/ui/docs/content/log.md +55 -0
  231. package/src/ui/docs/content/markdown.md +41 -0
  232. package/src/ui/docs/content/mcp.md +44 -0
  233. package/src/ui/docs/content/menu.md +114 -0
  234. package/src/ui/docs/content/microcopy.md +83 -0
  235. package/src/ui/docs/content/page.md +34 -0
  236. package/src/ui/docs/content/pagination.md +99 -0
  237. package/src/ui/docs/content/popover.md +49 -0
  238. package/src/ui/docs/content/progress.md +69 -0
  239. package/src/ui/docs/content/queue.md +62 -0
  240. package/src/ui/docs/content/radio-group.md +77 -0
  241. package/src/ui/docs/content/resizable.md +86 -0
  242. package/src/ui/docs/content/router.md +56 -0
  243. package/src/ui/docs/content/runtime.md +77 -0
  244. package/src/ui/docs/content/scheduler.md +66 -0
  245. package/src/ui/docs/content/scroll-area.md +89 -0
  246. package/src/ui/docs/content/section-shell.md +121 -0
  247. package/src/ui/docs/content/select.md +342 -0
  248. package/src/ui/docs/content/separator.md +33 -0
  249. package/src/ui/docs/content/sidebar.md +38 -0
  250. package/src/ui/docs/content/skeleton.md +34 -0
  251. package/src/ui/docs/content/slider.md +64 -0
  252. package/src/ui/docs/content/spinner.md +37 -0
  253. package/src/ui/docs/content/split.md +33 -0
  254. package/src/ui/docs/content/storage.md +69 -0
  255. package/src/ui/docs/content/switch.md +69 -0
  256. package/src/ui/docs/content/table.md +102 -0
  257. package/src/ui/docs/content/tabs.md +94 -0
  258. package/src/ui/docs/content/testing.md +89 -0
  259. package/src/ui/docs/content/textarea.md +30 -0
  260. package/src/ui/docs/content/toast.md +67 -0
  261. package/src/ui/docs/content/toggle-group.md +81 -0
  262. package/src/ui/docs/content/toggle.md +72 -0
  263. package/src/ui/docs/content/tokens.md +171 -0
  264. package/src/ui/docs/content/tooltip.md +50 -0
  265. package/src/ui/docs/content/truncate.md +37 -0
  266. package/src/ui/docs/content/ui.md +40 -0
  267. package/src/ui/docs/content/upgrading.md +48 -0
  268. package/src/ui/docs/doc-client.tsx +214 -0
  269. package/src/ui/docs/doc.tsx +301 -0
  270. package/src/ui/docs/folder.tsx +149 -0
  271. package/src/ui/docs/index.ts +21 -0
  272. package/src/ui/docs/markdown.tsx +130 -0
  273. package/src/ui/docs/md-raw.d.ts +4 -0
  274. package/src/ui/docs/plugin.ts +104 -0
  275. package/src/ui/docs/registry.tsx +424 -0
  276. package/src/ui/docs/standalone.tsx +107 -0
  277. package/src/ui/drivers/react.tsx +627 -0
  278. package/src/ui/index.ts +92 -0
  279. package/src/ui/lib/cn.ts +10 -0
  280. package/src/ui/lib/zod-pt-br.ts +38 -0
  281. package/src/ui/meta.ts +412 -0
  282. package/src/ui/react.tsx +235 -0
  283. package/src/ui/router.ts +96 -0
  284. package/src/ui/theme.css +234 -0
  285. package/src/vite/design.ts +652 -0
  286. package/src/vite/index.ts +8 -0
@@ -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
+ }