@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,110 @@
1
+ ---
2
+ title: Publicar o pacote
3
+ order: 6
4
+ ---
5
+
6
+ # Publicar o @softize/opus
7
+
8
+ O Opus é publicado no **registry compartilhado da Softize** (`registry.softize.com.br`),
9
+ **não** no npm público. Os clientes consomem de lá.
10
+
11
+ > O `publishConfig` do `package.json` já aponta pra lá — `pnpm publish` "só funciona",
12
+ > sem `--registry`. Não precisa decorar a URL.
13
+
14
+ ## Release pela estação (o caminho normal)
15
+
16
+ ```bash
17
+ pnpm release # patch bump + publica no registry.softize.com.br
18
+ pnpm release minor # minor
19
+ pnpm release major # major
20
+ pnpm release 2.9.0 # versão explícita
21
+ pnpm release none # publica sem bump
22
+ ```
23
+
24
+ Auth: o token do registry no `~/.npmrc` global (`//registry.softize.com.br/:_authToken=…`),
25
+ NÃO commitado.
26
+
27
+ ## Não existe caminho pela CI (29/jul)
28
+
29
+ A camada de GitHub Actions foi **removida** deste repo — `.github/` não existe mais. Saíram
30
+ o `release.yml` (publicava por tag `v*`), o `deploy.yml` (doc, já reserva) e o `check.yml`
31
+ (typecheck+test em push/PR). É a mesma decisão do monorepo (ADR 0008 do softize): um
32
+ caminho só, na estação, com os gates junto. Não há gatilho de reserva.
33
+
34
+ O secret **`OPUS_REGISTRY_TOKEN`** ficou órfão — nada mais o lê. Apague no GitHub. O que
35
+ autentica hoje é o token do registry no seu `~/.npmrc` global.
36
+
37
+ **O que o `release.sh` absorveu**, porque sem CI ficaria sem dono:
38
+
39
+ - **Procedência** — as três guardas que só o `deploy-docs.sh` tinha: precisa estar na
40
+ `main`, com árvore limpa (`--porcelain`, que pega não-rastreado) e igual a `origin/main`.
41
+ Rodam ANTES do bump, que suja a árvore de propósito. Antes isso vinha de graça: a release
42
+ nascia de um checkout limpo da tag, e a árvore do dev nunca entrava no tarball.
43
+ - **Bump na main, automático** — a guarda `tag=versão` do CI existia pra impedir registry e
44
+ main divergindo em silêncio. Como aqui o bump acontece DEPOIS (o script versiona e
45
+ publica), a proteção equivalente é o script commitar e empurrar o bump sozinho ao fim. Se
46
+ o push falhar depois de o publish ter ido, ele grita e sai 1 — a versão existe pro cliente
47
+ e não existe no repo, e isso não pode passar como aviso no meio do log.
48
+ - **Smoke do esqueleto** — já tinha descido pro script antes (ver abaixo).
49
+
50
+ `--local` (Verdaccio) não passa pelas guardas de procedência: é sandbox, e exigir main limpa
51
+ pra testar publicação seria a guarda atrapalhando quem está experimentando.
52
+
53
+ **Gate de qualidade**: antes de bumpar/publicar roda `pnpm typecheck`
54
+ + `pnpm test` e **aborta a release se qualquer um falhar**. Como o Opus **ship source** (`.ts`,
55
+ sem build), essa é a última barreira antes do tarball — sem ela, um `tsc` vermelho vaza pro
56
+ cliente. Emergência (evite): `pnpm release --skip-checks`.
57
+
58
+ **Smoke do esqueleto**: depois do bump e antes do publish, o `release.sh` gera um app com
59
+ `opus create`, instala o **tarball exato** que vai ser publicado e roda os gates dele
60
+ (typecheck · test · `opus check` · manifest · build). É o que pega o que typecheck+test não
61
+ alcançam — `files` mal recortado, export que só resolve por caminho relativo, template
62
+ pinado numa versão que ainda não existe no registry. Ele vivia só no `release.yml`; como as
63
+ últimas releases saíram pela estação (sem tag), passou tempo sem rodar, e foi assim que o
64
+ template chegou a quebrar o `tsc` de um app recém-criado. `--skip-checks` pula junto.
65
+
66
+ O script bumpa, publica **e leva o bump pra `origin/main`** — não sobra passo manual. Em
67
+ `--local` ele não commita nada (Verdaccio é sandbox), e com `pnpm release none` não há bump
68
+ pra commitar.
69
+
70
+ ## Verificar
71
+
72
+ ```bash
73
+ npm view @softize/opus version --registry https://registry.softize.com.br/
74
+ # ou
75
+ curl -s https://registry.softize.com.br/@softize%2fopus | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d)['dist-tags'].latest))"
76
+ ```
77
+
78
+ ## Testar localmente (sem tocar no compartilhado)
79
+
80
+ Para validar uma mudança de API em consumidores locais, publique uma **prerelease** no
81
+ Verdaccio e instale exatamente essa versão. Isso reproduz o pacote como será consumido —
82
+ sem `link:`/`file:`, que podem mascarar exports e peers.
83
+
84
+ ```bash
85
+ # terminal 1 — uma vez por máquina/sessão
86
+ pnpm registry:up # Verdaccio em 127.0.0.1:6873
87
+
88
+ # terminal 2 — no Opus
89
+ pnpm version 8.7.0-rc.0 --no-git-tag-version # escolha uma versão ainda não publicada
90
+ # adicione a seção "## 8.7.0-rc.0" ao CHANGELOG.md
91
+ pnpm release:local none # gates + publish SOMENTE local
92
+
93
+ # em cada consumidor
94
+ pnpm add @softize/opus@8.7.0-rc.0 --registry http://127.0.0.1:6873/
95
+ ```
96
+
97
+ Valide o app normalmente. Ao encerrar a sessão, restaure o intervalo de versão do
98
+ consumidor para o registry compartilhado e rode `pnpm install`; a prerelease local nunca
99
+ é enviada para `registry.softize.com.br`. Para publicar outra tentativa, incremente o
100
+ sucesso (`8.6.0-rc.1`, por exemplo) e repita o mesmo fluxo.
101
+
102
+ ## Auth
103
+
104
+ O publish no compartilhado exige o token no **`~/.npmrc` global** do dev:
105
+
106
+ ```
107
+ //registry.softize.com.br/:_authToken=<token>
108
+ ```
109
+
110
+ Não vai no repo. Leitura (install) é aberta — não precisa de token.
@@ -0,0 +1,131 @@
1
+ # Proposta: `ShellNav` — o menu como primitivo pivotável
2
+
3
+ > **Status: EM DISCUSSÃO.** Desenhado com o João (2026-07-22), a partir do trabalho na
4
+ > cabine do Maestro (a nav do projeto virou `SectionShell`; a sidebar ganhou Início +
5
+ > Configurações). Pendente de aprovação e de uma versão do Opus. Refina o
6
+ > [`ownership-vs-shadcn-lock`](ownership-vs-shadcn-lock.md): o item de menu é o próximo
7
+ > primitivo a sair do "cada app copia ad-hoc".
8
+
9
+ ## O problema
10
+
11
+ O `AppShell` entrega a **moldura** da sidebar (o slot `sidebarNav`) e o `SectionShell`
12
+ entrega a **nav de seção** (nav + painel, dentro do conteúdo). Mas o **item de menu** — a
13
+ linha com ícone + label + estado ativo + badge — não é primitivo de ninguém: cada app
14
+ escreve o `<button className="…">` na mão. Resultado, medido (não suposto):
15
+
16
+ | Menu | Onde | Como é feito hoje |
17
+ |---|---|---|
18
+ | Aplicação (sidebar) | `AppShell.sidebarNav` | `<button>` à mão — sem primitivo |
19
+ | Configurações | dentro do conteúdo | `SectionShell` (primitivo) ✓ |
20
+ | Relatórios | dentro do conteúdo | `<aside>`/`<nav>` à mão — **não usa o SectionShell**, mesmo sendo a mesma forma |
21
+
22
+ E isso se repete em **três produtos** (`softize/apps/maestro`, `softize/apps/main/web`,
23
+ `empresa-x/apps/main`), cada um com a própria cópia do item — e, dentro de cada um, o
24
+ markup ainda se repete por estado (ativo/inativo/recolhido) e nível (pai/filho).
25
+
26
+ Dois fatos que calibram a urgência:
27
+
28
+ - **Não é drift visível — é duplicação + risco.** Diff token a token: mesma versão do Opus
29
+ (2.37), mesmo `--radius` (0.625rem), classes de forma **byte-a-byte iguais**
30
+ (`rounded-md px-2.5 py-1.5 gap-2.5`). Só continuam iguais porque um humano copia; nada
31
+ cobra. A "sensação de divergência" (item mais retangular numa app que noutra) vem de
32
+ comparar **componentes diferentes** (nav de app × nav de seção × switcher), não de drift
33
+ do mesmo componente.
34
+ - **Já há uma inconsistência viva:** o menu de Relatórios do GB é uma nav-dentro-do-conteúdo
35
+ — a MESMA forma do Configurações — e mesmo assim hand-rola em vez de usar o `SectionShell`.
36
+
37
+ ## A proposta
38
+
39
+ Um primitivo só: **`ShellNav`** — **pivotável e controlado** (presentacional, como o
40
+ `SectionShell` já é). O app passa **dados**, não markup, e o mesmo componente serve os dois
41
+ lares sem flag de "modo app" vs "modo seção":
42
+
43
+ - Na **sidebar do AppShell** (`sidebarNav={<ShellNav …/>}`): lê o estado de recolhido do
44
+ contexto `useAppShell` e vira ícone-só sozinho.
45
+ - **Dentro do conteúdo** (o que o `SectionShell` faz): sem esse contexto, renderiza
46
+ expandido. O `SectionShell` **passa a compor** `ShellNav` (a nav) + o painel — um nav,
47
+ dois lares.
48
+
49
+ Isto é o "tanto faz onde, mesmo comportamento": o componente **se adapta ao contexto** via
50
+ `useAppShell`, sem o consumidor configurar nada.
51
+
52
+ ### Heading com espaço pra botão
53
+
54
+ Slot `heading` — título + ação opcional. É o "+" de novo relatório, o título da seção, a
55
+ marca na sidebar. Trivial e resolve o gesto que hoje mora à mão em cada nav.
56
+
57
+ ### Árvore embutida; DnD composto (a régua)
58
+
59
+ - **Aninhamento/árvore → dentro do `ShellNav`.** Não é feature nova: o accordion da sidebar
60
+ (Vendas → Leads/Propostas, no GB) já é uma árvore de um nível. Barato, já necessário.
61
+ - **Drag-drop de reordenar → COMPOSTO por fora**, estilo `dnd-kit`: o `ShellNav` expõe a
62
+ costura (um wrapper de item + um slot pro indicador de drop); um companheiro
63
+ (`useNavReorder`) pluga o arrasto só onde se opta. O peso — estado de drag, indicadores,
64
+ toque e principalmente a **a11y** (nav é *landmark*; reorder é *widget de manipulação* —
65
+ identidades diferentes) — vive fora do primitivo.
66
+
67
+ **O critério (viaja pra próximas decisões de primitivo): embute o que a maioria
68
+ compartilha; compõe o que um só precisa.** Reordenar é feature de um consumidor (só o
69
+ Relatórios); embuti-la faria os muitos pagarem a complexidade pelo um.
70
+
71
+ ### GB é a aparência de referência
72
+
73
+ Das três, a implementação do GB é a mais madura (tem rail/colapso, accordion, estados
74
+ ativos, âncora inferior de Configurações). **O `ShellNav` codifica o look do GB como
75
+ default** — nada regride: o GB migra pro primitivo e renderiza igual; a cabine (já alinhada
76
+ às classes do GB) passa a ser *literalmente* o mesmo componente, e a "sensação de
77
+ retangular" some. A **identidade por app** continua nos **tokens** (`--radius`, cores — o
78
+ Opus já permite sobrescrever no `theme.css` do app), não num fork do componente: a forma é
79
+ compartilhada; a pele é temável.
80
+
81
+ ## API (esboço)
82
+
83
+ ```tsx
84
+ interface ShellNavItem {
85
+ id: string
86
+ label: string
87
+ icon?: ReactNode
88
+ badge?: ReactNode
89
+ disabled?: boolean
90
+ children?: ShellNavItem[] // accordion (app) / pasta (relatórios)
91
+ }
92
+ interface ShellNavGroup { label?: string; items: ShellNavItem[] }
93
+
94
+ interface ShellNavProps {
95
+ groups: ShellNavGroup[]
96
+ activeId?: string
97
+ onSelect: (id: string) => void
98
+ heading?: ReactNode // título + espaço pra botão (o "+" do Relatórios)
99
+ footer?: ReactNode // âncora inferior (Configurações, no app)
100
+ collapsed?: boolean // override; default lê do useAppShell quando presente
101
+ navLabel?: string
102
+ reorder?: NavReorderSeam // opt-in; sem isto, ZERO DnD (o dnd-kit/useNavReorder pluga aqui)
103
+ }
104
+
105
+ // Conveniência do heading (padroniza título + ação):
106
+ <ShellNavHeading title="Relatórios" action={<Button size="icon" icon={Plus} …/>} />
107
+ ```
108
+
109
+ O `SectionShell` reescreve por dentro pra `<ShellNav/>` + painel; sua API pública não muda
110
+ (consumidores existentes seguem).
111
+
112
+ ## Hoje × proposto
113
+
114
+ | Menu | Hoje | Proposto |
115
+ |---|---|---|
116
+ | App (sidebar) | `<button>` à mão, N cópias por estado | `AppShell sidebarNav={<ShellNav groups footer=Configurações/>}` — colapso via contexto |
117
+ | Configurações | `SectionShell` (nav+painel à mão por dentro) | `SectionShell` compõe `ShellNav` — sem mudança pro consumidor |
118
+ | Relatórios | `<aside>` + tree + DnD, tudo à mão | `<ShellNav heading={título+"+"} …/>` + `useNavReorder` ao lado |
119
+
120
+ ## Consequências / plano
121
+
122
+ - **Escopo:** sessão dedicada no Opus (não é ajuste de app). Ciclo: desenhar a API →
123
+ publicar versão do Opus → migrar os consumidores (cada `<button>` à mão vira `groups={…}`).
124
+ - **Custo:** baixo em código, alto em coordenação (bump + migração dos 3 produtos + gates
125
+ em cada). O `SectionShell` é o precedente vivo — foi o mesmo movimento, um nível abaixo.
126
+ - **Ordem sugerida:** (1) `ShellNav` + `ShellNavHeading` no Opus com o look do GB como
127
+ default; (2) `SectionShell` passa a compor; (3) migrar a cabine do Maestro (primeiro
128
+ cliente, já alinhada); (4) migrar o back-office; (5) migrar o Relatórios do GB pra
129
+ `ShellNav` + `useNavReorder` (fecha a inconsistência viva).
130
+ - **Não-objetivo:** o `ShellNav` não vira dono de DnD, tree-editor ou roteamento — segue
131
+ presentacional/controlado; o app é dono do roteamento e do estado.
package/package.json ADDED
@@ -0,0 +1,338 @@
1
+ {
2
+ "name": "@softize/opus",
3
+ "version": "8.6.6",
4
+ "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "private": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/core/index.ts",
11
+ "default": "./src/core/index.ts"
12
+ },
13
+ "./package.json": "./package.json",
14
+ "./check": {
15
+ "default": "./bin/lib/check.mjs"
16
+ },
17
+ "./introspect": {
18
+ "default": "./bin/lib/introspect.mjs"
19
+ },
20
+ "./init": {
21
+ "default": "./bin/lib/init.mjs"
22
+ },
23
+ "./docs-include": "./bin/lib/docs-include.mjs",
24
+ "./core": {
25
+ "types": "./src/core/index.ts",
26
+ "default": "./src/core/index.ts"
27
+ },
28
+ "./audit": {
29
+ "types": "./src/audit/index.ts",
30
+ "default": "./src/audit/index.ts"
31
+ },
32
+ "./audit/console": {
33
+ "types": "./src/audit/drivers/console.ts",
34
+ "default": "./src/audit/drivers/console.ts"
35
+ },
36
+ "./audit/pg": {
37
+ "types": "./src/audit/drivers/pg.ts",
38
+ "default": "./src/audit/drivers/pg.ts"
39
+ },
40
+ "./auth": {
41
+ "types": "./src/auth/index.ts",
42
+ "default": "./src/auth/index.ts"
43
+ },
44
+ "./auth/jwt": {
45
+ "types": "./src/auth/drivers/jwt.ts",
46
+ "default": "./src/auth/drivers/jwt.ts"
47
+ },
48
+ "./auth/better-auth": {
49
+ "types": "./src/auth/drivers/better-auth.ts",
50
+ "default": "./src/auth/drivers/better-auth.ts"
51
+ },
52
+ "./client": {
53
+ "types": "./src/client/index.ts",
54
+ "default": "./src/client/index.ts"
55
+ },
56
+ "./client/fetch": {
57
+ "types": "./src/client/drivers/fetch.ts",
58
+ "default": "./src/client/drivers/fetch.ts"
59
+ },
60
+ "./data": {
61
+ "types": "./src/data/index.ts",
62
+ "default": "./src/data/index.ts"
63
+ },
64
+ "./data/kysely": {
65
+ "types": "./src/data/drivers/kysely.ts",
66
+ "default": "./src/data/drivers/kysely.ts"
67
+ },
68
+ "./data/readonly-pool": {
69
+ "types": "./src/data/readonly-pool.ts",
70
+ "default": "./src/data/readonly-pool.ts"
71
+ },
72
+ "./dsl": {
73
+ "types": "./src/dsl/index.ts",
74
+ "default": "./src/dsl/index.ts"
75
+ },
76
+ "./events": {
77
+ "types": "./src/events/index.ts",
78
+ "default": "./src/events/index.ts"
79
+ },
80
+ "./events/mitt": {
81
+ "types": "./src/events/drivers/mitt.ts",
82
+ "default": "./src/events/drivers/mitt.ts"
83
+ },
84
+ "./log": {
85
+ "types": "./src/log/index.ts",
86
+ "default": "./src/log/index.ts"
87
+ },
88
+ "./log/pino": {
89
+ "types": "./src/log/drivers/pino.ts",
90
+ "default": "./src/log/drivers/pino.ts"
91
+ },
92
+ "./queue": {
93
+ "types": "./src/queue/index.ts",
94
+ "default": "./src/queue/index.ts"
95
+ },
96
+ "./queue/bullmq": {
97
+ "types": "./src/queue/drivers/bullmq.ts",
98
+ "default": "./src/queue/drivers/bullmq.ts"
99
+ },
100
+ "./scheduler": {
101
+ "types": "./src/scheduler/index.ts",
102
+ "default": "./src/scheduler/index.ts"
103
+ },
104
+ "./scheduler/node-cron": {
105
+ "types": "./src/scheduler/drivers/node-cron.ts",
106
+ "default": "./src/scheduler/drivers/node-cron.ts"
107
+ },
108
+ "./storage": "./src/storage/index.ts",
109
+ "./storage/fs": "./src/storage/drivers/fs.ts",
110
+ "./storage/s3": "./src/storage/drivers/s3.ts",
111
+ "./ai": "./src/ai/index.ts",
112
+ "./ai/anthropic": "./src/ai/drivers/anthropic.ts",
113
+ "./mcp": "./src/mcp/index.ts",
114
+ "./testing": {
115
+ "types": "./src/testing/index.ts",
116
+ "default": "./src/testing/index.ts"
117
+ },
118
+ "./schema": {
119
+ "types": "./src/schema/index.ts",
120
+ "default": "./src/schema/index.ts"
121
+ },
122
+ "./schema/zod": {
123
+ "types": "./src/schema/drivers/zod.ts",
124
+ "default": "./src/schema/drivers/zod.ts"
125
+ },
126
+ "./schema/openapi": {
127
+ "types": "./src/schema/openapi.ts",
128
+ "default": "./src/schema/openapi.ts"
129
+ },
130
+ "./server": {
131
+ "types": "./src/server/index.ts",
132
+ "default": "./src/server/index.ts"
133
+ },
134
+ "./server/fastify": {
135
+ "types": "./src/server/drivers/fastify.ts",
136
+ "default": "./src/server/drivers/fastify.ts"
137
+ },
138
+ "./server/node": {
139
+ "types": "./src/server/drivers/node.ts",
140
+ "default": "./src/server/drivers/node.ts"
141
+ },
142
+ "./ui": {
143
+ "types": "./src/ui/index.ts",
144
+ "default": "./src/ui/index.ts"
145
+ },
146
+ "./ui/react": {
147
+ "types": "./src/ui/react.tsx",
148
+ "default": "./src/ui/react.tsx"
149
+ },
150
+ "./ui/meta": {
151
+ "types": "./src/ui/meta.ts",
152
+ "default": "./src/ui/meta.ts"
153
+ },
154
+ "./ui/docs": {
155
+ "types": "./src/ui/docs/index.ts",
156
+ "default": "./src/ui/docs/index.ts"
157
+ },
158
+ "./ui/docs/standalone": {
159
+ "types": "./src/ui/docs/standalone.tsx",
160
+ "default": "./src/ui/docs/standalone.tsx"
161
+ },
162
+ "./ui/docs/plugin": {
163
+ "types": "./src/ui/docs/plugin.ts",
164
+ "default": "./src/ui/docs/plugin.ts"
165
+ },
166
+ "./vite": {
167
+ "types": "./src/vite/index.ts",
168
+ "default": "./src/vite/index.ts"
169
+ },
170
+ "./ui/theme.css": "./src/ui/theme.css"
171
+ },
172
+ "files": [
173
+ "src",
174
+ "registry",
175
+ "bin",
176
+ "docs",
177
+ "README.md",
178
+ "CHANGELOG.md",
179
+ "LICENSE"
180
+ ],
181
+ "publishConfig": {
182
+ "registry": "https://registry.npmjs.org/",
183
+ "access": "public"
184
+ },
185
+ "bin": {
186
+ "opus": "./bin/cli.mjs"
187
+ },
188
+ "dependencies": {
189
+ "@modelcontextprotocol/sdk": "^1.29.0",
190
+ "@radix-ui/react-checkbox": "^1.1.3",
191
+ "@radix-ui/react-dialog": "^1.1.4",
192
+ "@radix-ui/react-dropdown-menu": "^2.1.4",
193
+ "@radix-ui/react-label": "^2.1.1",
194
+ "@radix-ui/react-popover": "^1.1.4",
195
+ "@radix-ui/react-separator": "^1.1.2",
196
+ "@radix-ui/react-slot": "^1.1.1",
197
+ "@radix-ui/react-tabs": "^1.1.2",
198
+ "@radix-ui/react-tooltip": "^1.1.6",
199
+ "@tailwindcss/typography": "^0.5.20",
200
+ "@types/markdown-it": "^14.1.2",
201
+ "class-variance-authority": "^0.7.1",
202
+ "clsx": "^2.1.1",
203
+ "cmdk": "^1.1.1",
204
+ "date-fns": "^4.4.0",
205
+ "embla-carousel-react": "^8.6.0",
206
+ "input-otp": "^1.4.2",
207
+ "lucide-react": "^0.460.0",
208
+ "markdown-it": "^14.2.0",
209
+ "mitt": "^3.0.1",
210
+ "prism-react-renderer": "^2.4.1",
211
+ "radix-ui": "^1.6.0",
212
+ "react-day-picker": "^10.0.1",
213
+ "react-resizable-panels": "^4.11.2",
214
+ "sonner": "^1.7.1",
215
+ "sucrase": "^3.35.1",
216
+ "tailwind-merge": "^3.3.1",
217
+ "tw-animate-css": "^1.2.5",
218
+ "typescript": "^5.6.0",
219
+ "zod-to-json-schema": "^3.23.0"
220
+ },
221
+ "peerDependencies": {
222
+ "@anthropic-ai/sdk": ">=0.35.0",
223
+ "@aws-sdk/client-s3": "^3.0.0",
224
+ "@aws-sdk/s3-request-presigner": "^3.0.0",
225
+ "@hookform/resolvers": "^3.0.0",
226
+ "@tanstack/react-query": "^5.0.0",
227
+ "bullmq": "^5.0.0",
228
+ "fastify": "^5.0.0",
229
+ "jsonwebtoken": "^9.0.0",
230
+ "kysely": "^0.27.0",
231
+ "node-cron": "^3.0.0",
232
+ "pg": "^8.0.0",
233
+ "pino": "^9.0.0",
234
+ "react": "^18.0.0 || ^19.0.0",
235
+ "react-hook-form": "^7.0.0",
236
+ "vite": "^5.0.0 || ^6.0.0",
237
+ "zod": "^3.24.0"
238
+ },
239
+ "peerDependenciesMeta": {
240
+ "@hookform/resolvers": {
241
+ "optional": true
242
+ },
243
+ "@tanstack/react-query": {
244
+ "optional": true
245
+ },
246
+ "bullmq": {
247
+ "optional": true
248
+ },
249
+ "fastify": {
250
+ "optional": true
251
+ },
252
+ "jsonwebtoken": {
253
+ "optional": true
254
+ },
255
+ "kysely": {
256
+ "optional": true
257
+ },
258
+ "node-cron": {
259
+ "optional": true
260
+ },
261
+ "pg": {
262
+ "optional": true
263
+ },
264
+ "react-hook-form": {
265
+ "optional": true
266
+ },
267
+ "pino": {
268
+ "optional": true
269
+ },
270
+ "react": {
271
+ "optional": true
272
+ },
273
+ "vite": {
274
+ "optional": true
275
+ },
276
+ "zod": {
277
+ "optional": true
278
+ },
279
+ "@aws-sdk/client-s3": {
280
+ "optional": true
281
+ },
282
+ "@aws-sdk/s3-request-presigner": {
283
+ "optional": true
284
+ },
285
+ "@anthropic-ai/sdk": {
286
+ "optional": true
287
+ }
288
+ },
289
+ "devDependencies": {
290
+ "@anthropic-ai/sdk": ">=0.35.0",
291
+ "@aws-sdk/client-s3": "^3.0.0",
292
+ "@aws-sdk/s3-request-presigner": "^3.0.0",
293
+ "@hookform/resolvers": "^3.10.0",
294
+ "@tanstack/react-query": "^5.62.0",
295
+ "@testing-library/dom": "^10.4.0",
296
+ "@testing-library/react": "^16.1.0",
297
+ "@types/better-sqlite3": "^7.6.0",
298
+ "@types/jsonwebtoken": "^9.0.0",
299
+ "@types/node": "^22.0.0",
300
+ "@types/node-cron": "^3.0.0",
301
+ "@types/pg": "^8.11.0",
302
+ "@types/react": "^19.0.0",
303
+ "@types/react-dom": "^19.0.0",
304
+ "@vitest/coverage-v8": "^2.1.0",
305
+ "better-sqlite3": "^11.0.0",
306
+ "bullmq": "^5.0.0",
307
+ "fastify": "^5.0.0",
308
+ "happy-dom": "^15.11.0",
309
+ "jsonwebtoken": "^9.0.2",
310
+ "kysely": "^0.27.0",
311
+ "node-cron": "^3.0.3",
312
+ "pg": "^8.13.0",
313
+ "pino": "^9.5.0",
314
+ "react": "^19.0.0",
315
+ "react-dom": "^19.0.0",
316
+ "react-hook-form": "^7.54.0",
317
+ "tsx": "^4.19.0",
318
+ "vite": "^6.0.0",
319
+ "vitest": "^2.1.0",
320
+ "zod": "^3.24.0"
321
+ },
322
+ "repository": {
323
+ "type": "git",
324
+ "url": "git+https://github.com/softize-dev/opus.git",
325
+ "directory": "packages/opus"
326
+ },
327
+ "homepage": "https://opus.softize.com.br",
328
+ "scripts": {
329
+ "postinstall": "node ./bin/lib/postinstall.mjs",
330
+ "typecheck": "tsc --noEmit",
331
+ "test": "vitest run",
332
+ "test:watch": "vitest",
333
+ "test:cov": "vitest run --coverage",
334
+ "registry:up": "npx -y verdaccio --config ~/.config/verdaccio/config.yaml",
335
+ "release": "bash ./scripts/release.sh",
336
+ "release:local": "bash ./scripts/release.sh --local"
337
+ }
338
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/link-memory-on-start.mjs\"",
9
+ "timeout": 10
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "Stop": [
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/opus-check-on-stop.mjs\"",
20
+ "timeout": 120
21
+ }
22
+ ]
23
+ }
24
+ ]
25
+ }
26
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Hook SessionStart da base Opus: liga a memória versionada do repo
3
+ * (`.claude/memory`, política da skill `memory`) ao diretório que o Claude Code
4
+ * lê (`~/.claude/projects/<hash>/memory`). Sem isso, sessão em worktree/máquina
5
+ * nova roda AMNÉSICA — recall vazio e memória gravada num diretório órfão que
6
+ * morre com o worktree. É o `link-memory.sh` automatizado: idempotente, com
7
+ * backup de memória local pré-existente, e fail-open (repo sem `.claude/memory`
8
+ * versionado → no-op; qualquer erro → segue sem travar a sessão).
9
+ */
10
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, renameSync, rmdirSync, symlinkSync, unlinkSync } from 'node:fs'
11
+ import { homedir } from 'node:os'
12
+ import { dirname, join } from 'node:path'
13
+
14
+ try {
15
+ const root = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
16
+ const memRepo = join(root, '.claude', 'memory')
17
+ // Sem memória versionada → o repo não adota a política; nada a ligar.
18
+ if (!existsSync(memRepo)) process.exit(0)
19
+
20
+ // O hash do projeto é o caminho absoluto com separadores (e pontos) virando '-',
21
+ // como o Claude Code nomeia ~/.claude/projects/. Espelha o link-memory.sh.
22
+ const hash = root.replace(/[/.]/g, '-')
23
+ const dest = join(homedir(), '.claude', 'projects', hash, 'memory')
24
+
25
+ let st = null
26
+ try {
27
+ st = lstatSync(dest)
28
+ } catch {
29
+ /* Não existe → segue pro link. */
30
+ }
31
+ if (st?.isSymbolicLink()) {
32
+ if (readlinkSync(dest) === memRepo) process.exit(0) // Já ligado.
33
+ unlinkSync(dest) // Aponta pra outro lugar → religa.
34
+ } else if (st?.isDirectory()) {
35
+ // Memória local pré-existente: com conteúdo → backup (nunca descarta); vazia → sai.
36
+ if (readdirSync(dest).length > 0) renameSync(dest, `${dest}.local-${Date.now()}`)
37
+ else rmdirSync(dest)
38
+ } else if (st !== null) {
39
+ unlinkSync(dest) // Arquivo solto no lugar do diretório — afasta.
40
+ }
41
+ mkdirSync(dirname(dest), { recursive: true })
42
+ symlinkSync(memRepo, dest)
43
+ } catch {
44
+ /* Fail-open: memória desligada é degradação, não motivo pra travar a sessão. */
45
+ }
46
+ process.exit(0)