@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,307 @@
1
+ /**
2
+ * opus check — régua executável das convenções de action (estático, TS compiler API).
3
+ *
4
+ * Lê `defineAction({...})`, `defineContract({...})` e `bindAction(contract, {...})`
5
+ * do source (sem rodar) e aplica regras de padrão:
6
+ * action-name — name no formato <resource>.<verb> (minúsculo, ponto)
7
+ * kind — kind ∈ simple|form|list|view
8
+ * field-order — campos na ordem canônica do ActionBase (grupos)
9
+ * export — defineAction/defineContract/bindAction como `export const`
10
+ * requires-sem-authorize — `requires` é declarativo (o runtime NÃO o executa);
11
+ * action com requires precisa de authorize (no defineAction,
12
+ * no contrato ou no binding). O join contrato↔bind é por
13
+ * identificador exportado, cross-file; referência ambígua
14
+ * ou não encontrada não acusa (sem falso-positivo).
15
+ *
16
+ * A checagem cross-file de registro no runtime (graph) fica pra v2 (introspecção/MCP).
17
+ * Saída: findings + exit ≠ 0 se houver erro. Zero actions num diretório NÃO-marcado
18
+ * (sem opus.json) segue reprovando — gate que devia ter o que checar e não tem é sintoma.
19
+ * Mas num projeto MARCADO (tem opus.json) zero actions é vacuamente ok: server/lib/app
20
+ * sem domínio ainda são opus válidos, e travar aí seria falso-positivo (ver emptyScanVerdict).
21
+ * É a régua do reviewer e a métrica de padrão.
22
+ */
23
+
24
+ import { promises as fs } from 'node:fs'
25
+ import path from 'node:path'
26
+ import ts from 'typescript'
27
+
28
+ // Ordem canônica (grupos do ActionBase em src/core/types.ts). Campos kind-específicos
29
+ // (fields/paginate/filters/expand/background/emits/successStatus…) NÃO entram no mapa —
30
+ // são ignorados na checagem de ordem (não geram falso-positivo).
31
+ const FIELD_GROUP = {
32
+ name: 0, kind: 0,
33
+ label: 1, title: 1, summary: 1, description: 1, icon: 1, color: 1, messages: 1, tags: 1, examples: 1, errors: 1,
34
+ input: 2, output: 2,
35
+ public: 3, requires: 3, authorize: 3, loads: 3,
36
+ handler: 4, mockHandler: 4,
37
+ audit: 5, confirm: 5, invalidates: 5, rateLimit: 5, idempotency: 5,
38
+ ai: 6, automatable: 6, internal: 6,
39
+ }
40
+
41
+ // <resource>.<verb> com namespacing opcional: <resource>(.<sub>)*.<verb> — ≥2 segmentos,
42
+ // minúsculo+hífen. Ex.: client.list, user.set-password, ticket.sla.check.
43
+ const SEG = '[a-z][a-z0-9]*(-[a-z0-9]+)*'
44
+ const ACTION_NAME_RE = new RegExp(`^${SEG}(\\.${SEG})+$`)
45
+ const KINDS = new Set(['simple', 'form', 'list', 'view'])
46
+
47
+ /** Os três nomes que denotam action/contrato — o arquivo sem nenhum deles é pulado. */
48
+ export const ACTION_MARKERS = ['defineAction', 'defineContract', 'bindAction']
49
+
50
+ /**
51
+ * Extrai `defineAction`/`defineContract`/`bindAction` de um source. Puro/sintático —
52
+ * testável sem CLI. Cada item traz `form` (qual das três chamadas), `ident` (o nome
53
+ * da const, se houver) e, no bindAction, `contractRef` (o identificador do 1º arg).
54
+ */
55
+ export function parseActions(fileName, sourceText) {
56
+ const sf = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
57
+ const actions = []
58
+
59
+ const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1
60
+
61
+ function constIdent(callNode) {
62
+ const decl = callNode.parent
63
+ if (!decl || !ts.isVariableDeclaration(decl)) return null
64
+ return ts.isIdentifier(decl.name) ? decl.name.text : null
65
+ }
66
+
67
+ function isExported(callNode) {
68
+ // call → VariableDeclaration → VariableDeclarationList → VariableStatement (com export)
69
+ const decl = callNode.parent
70
+ if (!decl || !ts.isVariableDeclaration(decl)) return false
71
+ const list = decl.parent
72
+ const stmt = list && list.parent
73
+ if (!stmt || !ts.isVariableStatement(stmt)) return false
74
+ return (stmt.modifiers ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
75
+ }
76
+
77
+ function strProp(obj, key) {
78
+ const p = obj.properties.find(
79
+ (pr) => ts.isPropertyAssignment(pr) && pr.name && pr.name.getText(sf) === key,
80
+ )
81
+ if (!p || !ts.isStringLiteral(p.initializer)) return null
82
+ return p.initializer.text
83
+ }
84
+
85
+ function objKeys(obj) {
86
+ return obj.properties
87
+ .filter((p) => ts.isPropertyAssignment(p) || ts.isMethodDeclaration(p) || ts.isShorthandPropertyAssignment(p))
88
+ .map((p) => (p.name ? p.name.getText(sf) : ''))
89
+ .filter(Boolean)
90
+ }
91
+
92
+ function visit(node) {
93
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
94
+ const callee = node.expression.text
95
+
96
+ if (
97
+ (callee === 'defineAction' || callee === 'defineContract') &&
98
+ node.arguments.length > 0 &&
99
+ ts.isObjectLiteralExpression(node.arguments[0])
100
+ ) {
101
+ const obj = node.arguments[0]
102
+ actions.push({
103
+ form: callee,
104
+ name: strProp(obj, 'name'),
105
+ kind: strProp(obj, 'kind'),
106
+ keys: objKeys(obj),
107
+ ident: constIdent(node),
108
+ exported: isExported(node),
109
+ line: lineOf(node),
110
+ })
111
+ }
112
+
113
+ if (
114
+ callee === 'bindAction' &&
115
+ node.arguments.length >= 2 &&
116
+ ts.isObjectLiteralExpression(node.arguments[1])
117
+ ) {
118
+ const ref = node.arguments[0]
119
+ actions.push({
120
+ form: 'bindAction',
121
+ name: null,
122
+ kind: null,
123
+ keys: objKeys(node.arguments[1]),
124
+ ident: constIdent(node),
125
+ contractRef: ts.isIdentifier(ref) ? ref.text : ref.getText(sf),
126
+ exported: isExported(node),
127
+ line: lineOf(node),
128
+ })
129
+ }
130
+ }
131
+ ts.forEachChild(node, visit)
132
+ }
133
+ visit(sf)
134
+ return actions
135
+ }
136
+
137
+ /** Aplica as regras locais (por arquivo) a um item → findings. */
138
+ export function lintAction(a) {
139
+ const out = []
140
+ const label = a.name ?? (a.form === 'bindAction' ? `bindAction(${a.contractRef})` : '(sem name)')
141
+ const at = (rule, message) => out.push({ rule, level: 'error', action: label, line: a.line, message })
142
+
143
+ if (a.form === 'bindAction') {
144
+ // O binding não repete name/kind/ordem — quem carrega isso é o contrato.
145
+ if (!a.exported) at('export', 'bindAction precisa ser `export const` (senão não dá pra registrar)')
146
+ return out
147
+ }
148
+
149
+ if (!a.name) at('action-name', `${a.form} sem \`name\``)
150
+ else if (!ACTION_NAME_RE.test(a.name)) at('action-name', `name "${a.name}" não é <resource>.<verb> (minúsculo, ponto)`)
151
+
152
+ if (!a.kind) at('kind', `${a.form} sem \`kind\``)
153
+ else if (!KINDS.has(a.kind)) at('kind', `kind "${a.kind}" inválido (use simple|form|list|view)`)
154
+
155
+ // field-order: os campos conhecidos devem ter group-index não-decrescente
156
+ let lastGroup = -1
157
+ let lastKey = ''
158
+ for (const k of a.keys) {
159
+ const g = FIELD_GROUP[k]
160
+ if (g === undefined) continue // kind-específico/desconhecido → lenient
161
+ if (g < lastGroup) {
162
+ at('field-order', `campo "${k}" fora de ordem (depois de "${lastKey}") — siga identidade→docs→input/output→authorize→handler→comportamento`)
163
+ break
164
+ }
165
+ lastGroup = g
166
+ lastKey = k
167
+ }
168
+
169
+ if (!a.exported) at('export', `${a.form} precisa ser \`export const\` (senão não dá pra registrar)`)
170
+
171
+ // requires sem authorize: no defineAction o par mora no mesmo objeto — acusa direto.
172
+ // No defineContract a regra é cross-file (o bind pode trazer o authorize row-level);
173
+ // fica pro join do checkProject.
174
+ if (a.form === 'defineAction' && a.keys.includes('requires') && !a.keys.includes('authorize')) {
175
+ at(
176
+ 'requires-sem-authorize',
177
+ '`requires` é declarativo (o runtime não o executa) — sem `authorize`, a action fica ABERTA. Declare o authorize.',
178
+ )
179
+ }
180
+
181
+ return out
182
+ }
183
+
184
+ /** Checa um source isolado → { actions, findings }. Regras cross-file: checkProject. */
185
+ export function checkSource(fileName, sourceText) {
186
+ const actions = parseActions(fileName, sourceText)
187
+ const findings = actions.flatMap(lintAction)
188
+ return { actions, findings }
189
+ }
190
+
191
+ /**
192
+ * Checa um projeto (lista de sources) → { actions, contracts, findings }.
193
+ * Além das regras locais, faz o join contrato↔bind por identificador exportado:
194
+ * bind de contrato com `requires` onde NEM contrato NEM binding têm `authorize`
195
+ * é `requires-sem-authorize` (no bind — é ele que produz a action executável).
196
+ * Referência não encontrada ou identificador duplicado não acusa.
197
+ */
198
+ export function checkProject(sources) {
199
+ let actions = 0
200
+ let contracts = 0
201
+ const findings = []
202
+ const contractsByIdent = new Map() // ident → item | 'ambiguous'
203
+ const binds = [] // { item, file }
204
+
205
+ for (const { file, text } of sources) {
206
+ for (const item of parseActions(file, text)) {
207
+ if (item.form === 'bindAction') {
208
+ actions += 1
209
+ binds.push({ item, file })
210
+ } else if (item.form === 'defineContract') {
211
+ contracts += 1
212
+ if (item.ident !== null) {
213
+ contractsByIdent.set(item.ident, contractsByIdent.has(item.ident) ? 'ambiguous' : item)
214
+ }
215
+ } else {
216
+ actions += 1
217
+ }
218
+ for (const f of lintAction(item)) findings.push({ ...f, file })
219
+ }
220
+ }
221
+
222
+ for (const { item, file } of binds) {
223
+ const contract = contractsByIdent.get(item.contractRef)
224
+ if (contract === undefined || contract === 'ambiguous') continue
225
+ const protectedSomewhere = contract.keys.includes('authorize') || item.keys.includes('authorize')
226
+ if (contract.keys.includes('requires') && !protectedSomewhere) {
227
+ findings.push({
228
+ rule: 'requires-sem-authorize',
229
+ level: 'error',
230
+ action: contract.name ?? item.contractRef,
231
+ line: item.line,
232
+ file,
233
+ message:
234
+ '`requires` do contrato é declarativo (o runtime não o executa) — sem `authorize` no contrato ou no binding, a action fica ABERTA.',
235
+ })
236
+ }
237
+ }
238
+
239
+ return { actions, contracts, findings }
240
+ }
241
+
242
+ const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git', '.next', 'coverage'])
243
+
244
+ export async function walkTsFiles(dir, acc = []) {
245
+ let entries
246
+ try {
247
+ entries = await fs.readdir(dir, { withFileTypes: true })
248
+ } catch {
249
+ return acc
250
+ }
251
+ for (const e of entries) {
252
+ if (e.name.startsWith('.') && e.name !== '.') continue
253
+ const full = path.join(dir, e.name)
254
+ if (e.isDirectory()) {
255
+ if (!SKIP_DIRS.has(e.name)) await walkTsFiles(full, acc)
256
+ } else if (
257
+ (e.name.endsWith('.ts') || e.name.endsWith('.tsx')) &&
258
+ !e.name.endsWith('.d.ts') &&
259
+ !e.name.endsWith('.test.ts')
260
+ ) {
261
+ acc.push(full)
262
+ }
263
+ }
264
+ return acc
265
+ }
266
+
267
+ async function exists(p) {
268
+ try {
269
+ await fs.access(p)
270
+ return true
271
+ } catch {
272
+ return false
273
+ }
274
+ }
275
+
276
+ /** Escaneia um diretório → { files, actions, contracts, findings, hasMarker }.
277
+ * `hasMarker` = o diretório é um projeto opus (tem opus.json na raiz). */
278
+ export async function scanDir(rootDir) {
279
+ const files = await walkTsFiles(rootDir)
280
+ const sources = []
281
+ for (const file of files) {
282
+ const text = await fs.readFile(file, 'utf-8')
283
+ if (!ACTION_MARKERS.some((m) => text.includes(m))) continue
284
+ sources.push({ file: path.relative(rootDir, file), text })
285
+ }
286
+ const { actions, contracts, findings } = checkProject(sources)
287
+ const hasMarker = await exists(path.join(rootDir, 'opus.json'))
288
+ return { files: files.length, actions, contracts, findings, hasMarker }
289
+ }
290
+
291
+ /**
292
+ * Veredito de um scan SEM actions. Num projeto MARCADO (tem opus.json) zero actions é
293
+ * vacuamente OK — um server, uma lib ou um app que ainda não tem domínio são opus
294
+ * legítimos, e travar o gate aí gera falso-positivo (o Stop-hook num services/ que a
295
+ * sessão tocou). Sem marcador, zero actions segue SINTOMA: diretório errado ou padrão
296
+ * que o check não enxerga — mantém o erro com a dica.
297
+ */
298
+ export function emptyScanVerdict({ hasMarker, contracts }) {
299
+ if (hasMarker) {
300
+ return { ok: true, message: 'projeto opus sem actions — nada a checar.' }
301
+ }
302
+ const hint =
303
+ contracts > 0
304
+ ? `${contracts} contrato(s) (defineContract) sem bindAction no diretório — o check roda na raiz do app, onde os binds moram.`
305
+ : `procuro defineAction, defineContract e bindAction — confira o diretório.`
306
+ return { ok: false, message: hint }
307
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Manifesto de componentes da UI do Opus — a camada de DESCOBERTA (estático, TS compiler API).
3
+ *
4
+ * Lê o `export const meta` co-localizado em cada componente (sem rodar JSX/React) + os
5
+ * nomes exportados + as variantes do `cva` (variant/size + defaults). É o que o MCP expõe
6
+ * pro agente de design ("que componentes existem, quando usar, o que importar, que variantes
7
+ * tem") em vez de ele reinventar (ex.: o Switcher na mão) ou chutar variante.
8
+ */
9
+
10
+ import { promises as fs } from 'node:fs'
11
+ import path from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+ import ts from 'typescript'
14
+
15
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
16
+ const COMPONENTS_DIR = path.join(PACKAGE_ROOT, 'src', 'ui', 'components')
17
+
18
+ const hasExport = (node) =>
19
+ (node.modifiers ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
20
+
21
+ /** Chaves de um object literal (identifier | string | number). */
22
+ const objKeys = (sf, objNode) =>
23
+ objNode.properties
24
+ .filter((p) => ts.isPropertyAssignment(p) && p.name)
25
+ .map((p) =>
26
+ ts.isStringLiteral(p.name) || ts.isNumericLiteral(p.name) ? p.name.text : p.name.getText(sf),
27
+ )
28
+
29
+ /**
30
+ * Extrai as variantes dos `cva('base', { variants: {...}, defaultVariants: {...} })` do
31
+ * source — ex.: { variant: ['default','outline',…], size: ['default','sm',…] } + defaults.
32
+ * Estático (AST): nada de rodar o componente.
33
+ */
34
+ function extractVariants(sf) {
35
+ const variants = {}
36
+ const defaults = {}
37
+ const prop = (obj, key) =>
38
+ obj.properties.find((p) => ts.isPropertyAssignment(p) && p.name && p.name.getText(sf) === key)
39
+
40
+ function visit(node) {
41
+ if (
42
+ ts.isCallExpression(node) &&
43
+ node.expression.getText(sf) === 'cva' &&
44
+ node.arguments.length >= 2 &&
45
+ ts.isObjectLiteralExpression(node.arguments[1])
46
+ ) {
47
+ const config = node.arguments[1]
48
+ const variantsProp = prop(config, 'variants')
49
+ if (variantsProp && ts.isObjectLiteralExpression(variantsProp.initializer)) {
50
+ for (const group of variantsProp.initializer.properties) {
51
+ if (ts.isPropertyAssignment(group) && group.name && ts.isObjectLiteralExpression(group.initializer)) {
52
+ variants[group.name.getText(sf)] = objKeys(sf, group.initializer)
53
+ }
54
+ }
55
+ }
56
+ const defaultsProp = prop(config, 'defaultVariants')
57
+ if (defaultsProp && ts.isObjectLiteralExpression(defaultsProp.initializer)) {
58
+ for (const d of defaultsProp.initializer.properties) {
59
+ if (ts.isPropertyAssignment(d) && d.name && ts.isStringLiteral(d.initializer)) {
60
+ defaults[d.name.getText(sf)] = d.initializer.text
61
+ }
62
+ }
63
+ }
64
+ }
65
+ ts.forEachChild(node, visit)
66
+ }
67
+ visit(sf)
68
+ return { variants, defaults }
69
+ }
70
+
71
+ /** Extrai `{ meta, exports, variants, defaults }` de um source. Puro/sintático — testável sem CLI. */
72
+ export function parseComponent(fileName, sourceText) {
73
+ const sf = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
74
+ let meta = null
75
+ const exportsList = []
76
+
77
+ const strProp = (obj, key) => {
78
+ const p = obj.properties.find(
79
+ (pr) => ts.isPropertyAssignment(pr) && pr.name && pr.name.getText(sf) === key,
80
+ )
81
+ return p && ts.isStringLiteral(p.initializer) ? p.initializer.text : null
82
+ }
83
+
84
+ for (const stmt of sf.statements) {
85
+ if (ts.isVariableStatement(stmt) && hasExport(stmt)) {
86
+ for (const decl of stmt.declarationList.declarations) {
87
+ const nm = decl.name.getText(sf)
88
+ if (nm === 'meta' && decl.initializer && ts.isObjectLiteralExpression(decl.initializer)) {
89
+ const obj = decl.initializer
90
+ meta = {
91
+ name: strProp(obj, 'name'),
92
+ altitude: strProp(obj, 'altitude'),
93
+ ancestry: strProp(obj, 'ancestry'),
94
+ whenToUse: strProp(obj, 'whenToUse'),
95
+ }
96
+ } else {
97
+ exportsList.push(nm)
98
+ }
99
+ }
100
+ } else if (
101
+ (ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) &&
102
+ hasExport(stmt) &&
103
+ stmt.name
104
+ ) {
105
+ exportsList.push(stmt.name.getText(sf))
106
+ }
107
+ }
108
+
109
+ const { variants, defaults } = extractVariants(sf)
110
+ return { meta, exports: exportsList, variants, defaults }
111
+ }
112
+
113
+ /** Varre src/ui/components recursivamente; retorna o manifesto ordenado por nome. */
114
+ export async function listComponents() {
115
+ const out = []
116
+ async function walk(dir) {
117
+ let entries
118
+ try {
119
+ entries = await fs.readdir(dir, { withFileTypes: true })
120
+ } catch {
121
+ return
122
+ }
123
+ for (const e of entries) {
124
+ const full = path.join(dir, e.name)
125
+ if (e.isDirectory()) await walk(full)
126
+ else if (e.name.endsWith('.tsx')) {
127
+ const { meta, exports, variants, defaults } = parseComponent(
128
+ e.name,
129
+ await fs.readFile(full, 'utf-8'),
130
+ )
131
+ if (meta && meta.name) {
132
+ const entry = { ...meta, exports, importFrom: '@softize/opus/ui/react', file: path.relative(PACKAGE_ROOT, full) }
133
+ // Só inclui `variants`/`defaults` quando há cva — evita campos vazios no manifesto.
134
+ if (Object.keys(variants).length > 0) {
135
+ entry.variants = variants
136
+ if (Object.keys(defaults).length > 0) entry.defaults = defaults
137
+ }
138
+ out.push(entry)
139
+ }
140
+ }
141
+ }
142
+ }
143
+ await walk(COMPONENTS_DIR)
144
+ return out.sort((a, b) => a.name.localeCompare(b.name))
145
+ }
146
+
147
+ /** Um componente pelo nome canônico (ex.: 'dropdown-menu'), ou null. */
148
+ export async function getComponent(name) {
149
+ const all = await listComponents()
150
+ return all.find((c) => c.name === name) ?? null
151
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * opus create — scaffolda um app opus-based CANÔNICO a partir do template do registry
3
+ * (`registry/templates/app`). O esqueleto versiona com a base: sai do mesmo tarball que
4
+ * o SDK que ele configura, então nasce certo por construção — os pré-requisitos do
5
+ * protocolo (opus.config/manifest/scripts), da UI (preset+tema v4 CSS-first) e do
6
+ * Maestro (porta injetada, allowedHosts do preview) já vêm plugados.
7
+ *
8
+ * DOIS MODOS, por detecção (sobe do destino procurando `pnpm-workspace.yaml`):
9
+ * • standalone — repo próprio: o template inteiro (raiz inclusa);
10
+ * • app em MONOREPO — dentro de um workspace: gera só o que é DO APP e pula o que é
11
+ * da raiz (`.npmrc`, `.gitignore`, `pnpm-workspace.yaml` — workspace yaml aninhado
12
+ * quebra o pnpm); o que a raiz precisa ter vira AVISO (globs do workspace, escopo
13
+ * do registry, allowBuilds), sem clobber.
14
+ *
15
+ * Convenções do template:
16
+ * • segmento de caminho com `_` inicial vira `.` na cópia (`_npmrc` → `.npmrc`) —
17
+ * o npm pack descarta dotfiles do tarball, então a fonte não pode tê-los;
18
+ * • tokens `{{APP_NAME}}` e `{{OPUS_VERSION}}` são substituídos em todo arquivo.
19
+ *
20
+ * Depois da cópia roda o `initProject` (setup): opus.json, CLAUDE.md (bloco),
21
+ * `.claude/memory/` e o CI de fábrica (repo-level: acha a raiz pelo `.git`).
22
+ */
23
+
24
+ import { promises as fs } from 'node:fs'
25
+ import path from 'node:path'
26
+ import { initProject } from './init.mjs'
27
+
28
+ /** Arquivos do template que pertencem à RAIZ do repo — fora no modo app. */
29
+ const ROOT_ONLY = new Set(['_npmrc', '_gitignore', 'pnpm-workspace.yaml'])
30
+
31
+ /** Raiz do workspace pnpm que contém `dir` (sobe até achar pnpm-workspace.yaml); null se não há. */
32
+ async function workspaceRootOf(dir) {
33
+ let cur = path.resolve(dir)
34
+ for (;;) {
35
+ if (await exists(path.join(cur, 'pnpm-workspace.yaml'))) return cur
36
+ const up = path.dirname(cur)
37
+ if (up === cur) return null
38
+ cur = up
39
+ }
40
+ }
41
+
42
+ /** Glob de workspace (`apps/*`, `packages/**`, caminho exato) casa com o caminho relativo? */
43
+ function globMatches(glob, rel) {
44
+ const re = new RegExp(
45
+ `^${glob
46
+ .split('**')
47
+ .map((part) => part.split('*').map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('[^/]*'))
48
+ .join('.*')}$`,
49
+ )
50
+ return re.test(rel)
51
+ }
52
+
53
+ /** Avisos do modo app: o que a RAIZ do workspace precisa ter (sem clobber — só aviso). */
54
+ async function workspaceWarnings(wsRoot, appDir) {
55
+ const warnings = []
56
+ const rel = path.relative(wsRoot, appDir).split(path.sep).join('/')
57
+ let ws = ''
58
+ try {
59
+ ws = await fs.readFile(path.join(wsRoot, 'pnpm-workspace.yaml'), 'utf-8')
60
+ } catch {
61
+ /* Ilegível → sem como conferir. */
62
+ }
63
+ // packages: — lista simples de globs quotados ou crus.
64
+ const globs = [...ws.matchAll(/^\s*-\s*['"]?([^'"#\n]+?)['"]?\s*(?:#.*)?$/gm)].map((m) => m[1].trim())
65
+ if (!globs.some((g) => globMatches(g, rel))) {
66
+ warnings.push(`pnpm-workspace.yaml (raiz): nenhum glob de packages cobre \`${rel}\` — adicione (ex.: '${rel.split('/')[0]}/*').`)
67
+ }
68
+ if (!/allowBuilds[\s\S]*@softize\/opus/.test(ws)) {
69
+ warnings.push("pnpm-workspace.yaml (raiz): sem allowBuilds pro '@softize/opus' — o postinstall (setup) não roda no install.")
70
+ }
71
+ let npmrc = ''
72
+ try {
73
+ npmrc = await fs.readFile(path.join(wsRoot, '.npmrc'), 'utf-8')
74
+ } catch {
75
+ /* Sem .npmrc na raiz. */
76
+ }
77
+ if (!npmrc.includes('@softize:registry=')) {
78
+ warnings.push('.npmrc (raiz): sem `@softize:registry=https://registry.softize.com.br/` — o install não resolve a base.')
79
+ }
80
+ return warnings
81
+ }
82
+
83
+ async function exists(p) {
84
+ try {
85
+ await fs.access(p)
86
+ return true
87
+ } catch {
88
+ return false
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Entradas que NÃO contam como "trabalho de alguém" na recusa de dir não-vazio:
94
+ * o ponto de partida normal de repo-cliente é `git init`/clone ANTES do scaffold
95
+ * (.git, README, LICENSE). Nenhuma delas existe nos templates → sem clobber.
96
+ */
97
+ const PREEXISTING_OK = /^(\.git|README(\.[^/]+)?|LICENSE(\.[^/]+)?)$/i
98
+
99
+ /** Recusa destino com conteúdo real (nunca sobrescreve); repo recém-iniciado passa. */
100
+ async function ensureScaffoldable(targetDir) {
101
+ if (!(await exists(targetDir))) return
102
+ const blocking = (await fs.readdir(targetDir)).filter((e) => !PREEXISTING_OK.test(e))
103
+ if (blocking.length > 0) {
104
+ const shown = blocking.slice(0, 3).join(', ') + (blocking.length > 3 ? ', …' : '')
105
+ throw new Error(
106
+ `O diretório ${targetDir} já existe e não está vazio (${shown}) — escolha outro nome. (.git/README/LICENSE não contam: repo recém-iniciado é ponto de partida normal.)`,
107
+ )
108
+ }
109
+ }
110
+
111
+ async function walk(dir, base = dir) {
112
+ const out = []
113
+ for (const e of await fs.readdir(dir, { withFileTypes: true })) {
114
+ const full = path.join(dir, e.name)
115
+ if (e.isDirectory()) out.push(...(await walk(full, base)))
116
+ else out.push(path.relative(base, full))
117
+ }
118
+ return out
119
+ }
120
+
121
+ /** `_x` no início de um segmento vira `.x` (dotfiles não sobrevivem ao npm pack). */
122
+ function destRelOf(rel) {
123
+ return rel
124
+ .split(path.sep)
125
+ .map((seg) => (seg.startsWith('_') ? `.${seg.slice(1)}` : seg))
126
+ .join(path.sep)
127
+ }
128
+
129
+ /** Copia um template com os tokens substituídos; `skipTop` pula segmentos de topo. */
130
+ async function copyTemplate(template, targetDir, { appName, version }, skipTop) {
131
+ const files = []
132
+ for (const rel of (await walk(template)).sort()) {
133
+ if (skipTop !== undefined && skipTop.has(rel.split(path.sep)[0])) continue
134
+ const destRel = destRelOf(rel)
135
+ const dest = path.join(targetDir, destRel)
136
+ await fs.mkdir(path.dirname(dest), { recursive: true })
137
+ const content = await fs.readFile(path.join(template, rel), 'utf-8')
138
+ await fs.writeFile(dest, content.replaceAll('{{APP_NAME}}', appName).replaceAll('{{OPUS_VERSION}}', version))
139
+ files.push(destRel)
140
+ }
141
+ return files
142
+ }
143
+
144
+ /**
145
+ * Cria a RAIZ de um monorepo canônico (`opus create --monorepo <dir>`): workspace com
146
+ * `apps/*`/`packages/*`, allowBuilds da base, escopo do registry e gitignore. Sem app
147
+ * dentro — o primeiro vem de `opus create apps/<nome>` rodado na raiz (o modo app
148
+ * detecta o workspace; a raiz gerada passa limpa pelos avisos dele, por construção).
149
+ * @returns {Promise<{version:string, files:string[]}>}
150
+ */
151
+ export async function createMonorepo(registryDir, targetDir, name) {
152
+ const template = path.join(registryDir, 'templates', 'monorepo')
153
+ if (!(await exists(template))) {
154
+ throw new Error(`Template não encontrado em ${template} — a instalação do @softize/opus está íntegra?`)
155
+ }
156
+ await ensureScaffoldable(targetDir)
157
+ // Monorepo dentro de monorepo é quase certamente engano (workspace aninhado).
158
+ const enclosing = await workspaceRootOf(path.dirname(path.resolve(targetDir)))
159
+ if (enclosing !== null) {
160
+ throw new Error(`Já existe um workspace em ${enclosing} — pra criar um app nele, use \`opus create\` sem --monorepo.`)
161
+ }
162
+
163
+ const pkg = await (async () => {
164
+ try {
165
+ return JSON.parse(await fs.readFile(path.join(registryDir, '..', 'package.json'), 'utf-8'))
166
+ } catch {
167
+ return null
168
+ }
169
+ })()
170
+ const version = pkg?.version ?? '0.0.0'
171
+ const files = await copyTemplate(template, targetDir, { appName: name, version })
172
+ return { version, files }
173
+ }
174
+
175
+ /**
176
+ * Cria o projeto em `targetDir` a partir do template. Recusa diretório existente
177
+ * NÃO-vazio (nunca sobrescreve trabalho de alguém). Detecta workspace envolvente:
178
+ * dentro de um, gera só os arquivos DO APP e devolve os avisos do que a raiz precisa.
179
+ * @returns {Promise<{version:string, mode:'standalone'|'app', files:string[], warnings:string[], setup:{created:string[], warnings:string[]}}>}
180
+ */
181
+ export async function createProject(registryDir, targetDir, appName) {
182
+ const template = path.join(registryDir, 'templates', 'app')
183
+ if (!(await exists(template))) {
184
+ throw new Error(`Template não encontrado em ${template} — a instalação do @softize/opus está íntegra?`)
185
+ }
186
+ await ensureScaffoldable(targetDir)
187
+
188
+ const pkg = await (async () => {
189
+ try {
190
+ return JSON.parse(await fs.readFile(path.join(registryDir, '..', 'package.json'), 'utf-8'))
191
+ } catch {
192
+ return null
193
+ }
194
+ })()
195
+ const version = pkg?.version ?? '0.0.0'
196
+
197
+ const wsRoot = await workspaceRootOf(path.dirname(path.resolve(targetDir)))
198
+ const mode = wsRoot === null ? 'standalone' : 'app'
199
+
200
+ const files = await copyTemplate(template, targetDir, { appName, version }, mode === 'app' ? ROOT_ONLY : undefined)
201
+
202
+ const warnings = mode === 'app' ? await workspaceWarnings(wsRoot, path.resolve(targetDir)) : []
203
+
204
+ // Setup por cima: opus.json (pin), CLAUDE.md (bloco), .claude/memory/, CI de fábrica
205
+ // (os repo-level acham a raiz pelo .git — no monorepo caem na raiz, só-se-faltar).
206
+ const setup = await initProject(registryDir, targetDir)
207
+ return { version, mode, files, warnings, setup: { created: setup.created, warnings: setup.warnings } }
208
+ }