@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,117 @@
1
+ /**
2
+ * @softize/opus/storage/s3 — driver S3-compatível (EXPERIMENTAL).
3
+ *
4
+ * Funciona com AWS S3 e compatíveis (MinIO, R2, Backblaze…): o consumidor
5
+ * constrói o `S3Client` (região/endpoint/credenciais são dele) e o driver só
6
+ * opera bucket+key. Peer OPCIONAL: `@aws-sdk/client-s3` (+ `s3-request-presigner`
7
+ * pra `url()`) — importados sob demanda, quem não usa o driver não os instala.
8
+ */
9
+
10
+ import type { StorageAdapter, StorageObject, StoragePutOptions } from '../../core/index.ts'
11
+ import { normalizeKey } from '../index.ts'
12
+
13
+ /** O subset do S3Client que o driver usa (injetável em teste). */
14
+ export interface S3ClientLike {
15
+ send(command: unknown): Promise<unknown>
16
+ }
17
+
18
+ export interface S3StorageOptions {
19
+ /** Cliente S3 (AWS SDK v3) — o consumidor o constrói e é dono da config/credencial. */
20
+ client: S3ClientLike
21
+ bucket: string
22
+ /** Prefixo lógico dentro do bucket (ex.: `uploads`). */
23
+ prefix?: string
24
+ /** Validade default da URL assinada (default 900s = 15 min). */
25
+ urlExpiresInSeconds?: number
26
+ /** Nome do adapter (default 's3'). */
27
+ name?: string
28
+ /**
29
+ * Assinador de URL (injetável em teste). Default: `getSignedUrl` do
30
+ * `@aws-sdk/s3-request-presigner`, importado sob demanda.
31
+ */
32
+ presign?: (client: S3ClientLike, command: unknown, opts: { expiresIn: number }) => Promise<string>
33
+ }
34
+
35
+ // Import sob demanda do SDK (peer opcional): cacheado após o primeiro uso.
36
+ let sdkPromise: Promise<typeof import('@aws-sdk/client-s3')> | null = null
37
+ function sdk(): Promise<typeof import('@aws-sdk/client-s3')> {
38
+ sdkPromise ??= import('@aws-sdk/client-s3').catch(() => {
39
+ sdkPromise = null
40
+ throw new Error('storage/s3 precisa do peer @aws-sdk/client-s3 — `pnpm add @aws-sdk/client-s3`.')
41
+ })
42
+ return sdkPromise
43
+ }
44
+
45
+ async function defaultPresign(
46
+ client: S3ClientLike,
47
+ command: unknown,
48
+ opts: { expiresIn: number },
49
+ ): Promise<string> {
50
+ let presigner: typeof import('@aws-sdk/s3-request-presigner')
51
+ try {
52
+ presigner = await import('@aws-sdk/s3-request-presigner')
53
+ } catch {
54
+ throw new Error(
55
+ 'storage/s3 url() precisa do peer @aws-sdk/s3-request-presigner — `pnpm add @aws-sdk/s3-request-presigner`.',
56
+ )
57
+ }
58
+ // O tipo real exige o S3Client concreto; o subset injetável cobre os testes.
59
+ return presigner.getSignedUrl(client as never, command as never, opts)
60
+ }
61
+
62
+ export function s3Storage(options: S3StorageOptions): StorageAdapter {
63
+ const prefix = options.prefix?.replace(/^\/+|\/+$/g, '')
64
+ const presign = options.presign ?? defaultPresign
65
+
66
+ function keyOf(key: string): string {
67
+ const k = normalizeKey(key)
68
+ return prefix !== undefined && prefix.length > 0 ? `${prefix}/${k}` : k
69
+ }
70
+
71
+ return {
72
+ name: options.name ?? 's3',
73
+ kind: 'storage',
74
+
75
+ async put(key: string, data: Uint8Array, opts?: StoragePutOptions): Promise<void> {
76
+ const { PutObjectCommand } = await sdk()
77
+ await options.client.send(
78
+ new PutObjectCommand({
79
+ Bucket: options.bucket,
80
+ Key: keyOf(key),
81
+ Body: data,
82
+ ...(opts?.contentType !== undefined ? { ContentType: opts.contentType } : {}),
83
+ }),
84
+ )
85
+ },
86
+
87
+ async get(key: string): Promise<StorageObject | null> {
88
+ const { GetObjectCommand } = await sdk()
89
+ let response: { Body?: { transformToByteArray(): Promise<Uint8Array> }; ContentType?: string }
90
+ try {
91
+ response = (await options.client.send(new GetObjectCommand({ Bucket: options.bucket, Key: keyOf(key) }))) as never
92
+ } catch (err) {
93
+ // NoSuchKey/NotFound = não existe (contrato do get); o resto propaga.
94
+ const name = (err as { name?: string }).name
95
+ if (name === 'NoSuchKey' || name === 'NotFound') return null
96
+ throw err
97
+ }
98
+ if (response.Body === undefined) return null
99
+ const data = await response.Body.transformToByteArray()
100
+ return { data, ...(response.ContentType !== undefined ? { contentType: response.ContentType } : {}) }
101
+ },
102
+
103
+ async delete(key: string): Promise<void> {
104
+ const { DeleteObjectCommand } = await sdk()
105
+ // DeleteObject no S3 já é idempotente (204 mesmo sem o objeto).
106
+ await options.client.send(new DeleteObjectCommand({ Bucket: options.bucket, Key: keyOf(key) }))
107
+ },
108
+
109
+ async url(key: string, opts?: { expiresInSeconds?: number }): Promise<string> {
110
+ const { GetObjectCommand } = await sdk()
111
+ const command = new GetObjectCommand({ Bucket: options.bucket, Key: keyOf(key) })
112
+ return presign(options.client, command, {
113
+ expiresIn: opts?.expiresInSeconds ?? options.urlExpiresInSeconds ?? 900,
114
+ })
115
+ },
116
+ }
117
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @softize/opus/storage — helpers compartilhados dos drivers de storage.
3
+ *
4
+ * EXPERIMENTAL. O contrato (`StorageAdapter`) vive no core; drivers em
5
+ * `storage/fs` (disco local) e `storage/s3` (S3-compatível). A superfície é a
6
+ * mínima de backoffice (put/get/delete/url) e cresce por reincidência — não por
7
+ * especulação (streams, list e metadata ricos entram quando um caso real cobrar).
8
+ */
9
+
10
+ export type { StorageAdapter, StoragePutOptions, StorageObject } from '../core/index.ts'
11
+
12
+ /**
13
+ * Normaliza e valida uma key de storage: caminho lógico relativo, sem traversal.
14
+ * Rejeita absoluto, `..`, backslash e segmento vazio — a MESMA régua em todo
15
+ * driver (o fs depende disso pra não escapar do root; o s3 herda a disciplina).
16
+ */
17
+ export function normalizeKey(key: string): string {
18
+ const k = key.trim()
19
+ if (k.length === 0) throw new Error('Key de storage vazia.')
20
+ if (k.includes('\\')) throw new Error(`Key de storage com backslash: "${key}" (use "/").`)
21
+ if (k.startsWith('/')) throw new Error(`Key de storage absoluta: "${key}" (use caminho relativo).`)
22
+ const parts = k.split('/')
23
+ if (parts.some((p) => p === '' || p === '.' || p === '..')) {
24
+ throw new Error(`Key de storage inválida: "${key}" (sem "..", "." nem segmento vazio).`)
25
+ }
26
+ return parts.join('/')
27
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * `fake` — dado sintético DETERMINÍSTICO a partir de um schema zod. É a matéria-prima do
3
+ * modo design: um `mockHandler` devolve `fake(entity)`/`fakeMany(entity, n)` e o preview mostra
4
+ * a UI com dado realista, sem backend nem banco (o design-mode do runtime nem toca `ctx.db`).
5
+ *
6
+ * DETERMINISMO: PRNG próprio semeado (mulberry32) — mesma `seed` → mesma saída, sempre. Nunca
7
+ * `Math.random` (o preview tem que ser reproduzível entre execuções). O default é semeado por
8
+ * um valor fixo; `fakeMany` varia a seed por índice (itens diferentes, estáveis).
9
+ *
10
+ * VALIDADE: a saída satisfaz o schema (`safeParse` passa) pros checks comuns — min/max/int/
11
+ * length, enum/literal, e formatos por CHECK (email é length-aware; uuid/url/datetime têm
12
+ * tamanho próprio, então um length CONFLITANTE com esses é schema contraditório, sem valor
13
+ * válido — aceito como tal). Formato por NOME de campo (telefone, cpf, data…) é só PALPITE:
14
+ * havendo length/min/max, a restrição vence o palpite. Refinamentos (`.refine`, `.regex`,
15
+ * `.multipleOf`) são caixa-preta — sem garantia; use schemas de contrato/entidade normais.
16
+ *
17
+ * REALISMO: heurística por check do zod primeiro, depois pelo NOME do campo (email, nome, id,
18
+ * data, telefone, cpf/cnpj, descrição…) — com sabor pt-BR, que é quem consome (GB).
19
+ */
20
+ import { z, type ZodTypeAny } from 'zod'
21
+
22
+ export interface FakeOptions {
23
+ /** Semente do PRNG. Mesma seed → mesma saída. Default: 1. Aceita string (hasheada). */
24
+ seed?: number | string
25
+ }
26
+
27
+ // ── PRNG determinístico ─────────────────────────────────────────────────────────
28
+
29
+ function mulberry32(seed: number): () => number {
30
+ let a = seed >>> 0
31
+ return () => {
32
+ a = (a + 0x6d2b79f5) | 0
33
+ let t = Math.imul(a ^ (a >>> 15), 1 | a)
34
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
35
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
36
+ }
37
+ }
38
+
39
+ function hashStr(s: string): number {
40
+ let h = 2166136261
41
+ for (let i = 0; i < s.length; i++) {
42
+ h ^= s.charCodeAt(i)
43
+ h = Math.imul(h, 16777619)
44
+ }
45
+ return h >>> 0
46
+ }
47
+
48
+ interface Rng {
49
+ next: () => number
50
+ }
51
+ const rint = (r: Rng, min: number, max: number): number => min + Math.floor(r.next() * (max - min + 1))
52
+ const rpick = <T>(r: Rng, arr: readonly T[]): T => arr[Math.floor(r.next() * arr.length)] ?? arr[0]!
53
+
54
+ // ── Pools pt-BR ─────────────────────────────────────────────────────────────────
55
+
56
+ const FIRST = ['Ana', 'Bruno', 'Carla', 'Diego', 'Elena', 'Felipe', 'Gabriela', 'Hugo', 'Inês', 'João', 'Larissa', 'Marcos', 'Natália', 'Otávio', 'Paula', 'Rafael', 'Sofia', 'Tiago', 'Vitória', 'Yasmin']
57
+ const LAST = ['Silva', 'Souza', 'Oliveira', 'Santos', 'Pereira', 'Costa', 'Almeida', 'Lima', 'Gomes', 'Ribeiro', 'Carvalho', 'Fernandes', 'Araújo', 'Melo']
58
+ const WORDS = ['relatório', 'unidade', 'evento', 'proposta', 'cliente', 'veículo', 'agenda', 'pedido', 'campanha', 'lead', 'serviço', 'peça', 'garantia', 'departamento']
59
+
60
+ function fullName(r: Rng): string {
61
+ return `${rpick(r, FIRST)} ${rpick(r, LAST)}`
62
+ }
63
+ function slug(s: string): string {
64
+ return s
65
+ .normalize('NFD')
66
+ .replace(/[̀-ͯ]/g, '')
67
+ .toLowerCase()
68
+ .replace(/[^a-z0-9]+/g, '.')
69
+ .replace(/^\.|\.$/g, '')
70
+ }
71
+ function sentence(r: Rng): string {
72
+ const n = rint(r, 4, 9)
73
+ const ws = Array.from({ length: n }, () => rpick(r, WORDS))
74
+ const s = ws.join(' ')
75
+ return `${s[0]!.toUpperCase()}${s.slice(1)}.`
76
+ }
77
+ function uuid(r: Rng): string {
78
+ const hex = '0123456789abcdef'
79
+ let s = ''
80
+ for (let i = 0; i < 32; i++) {
81
+ if (i === 12) s += '4'
82
+ else if (i === 16) s += hex[8 + Math.floor(r.next() * 4)]
83
+ else s += hex[Math.floor(r.next() * 16)]
84
+ if (i === 7 || i === 11 || i === 15 || i === 19) s += '-'
85
+ }
86
+ return s
87
+ }
88
+ function isoDate(r: Rng): string {
89
+ // Janela ±180 dias em torno de um epoch FIXO (determinístico; sem Date.now).
90
+ const base = 1_750_000_000_000 // ~jun/2025, fixo
91
+ const ms = base + rint(r, -180, 180) * 86_400_000 + rint(r, 0, 86_399) * 1000
92
+ return new Date(ms).toISOString()
93
+ }
94
+ function digits(r: Rng, n: number): string {
95
+ let s = ''
96
+ for (let i = 0; i < n; i++) s += String(rint(r, 0, 9))
97
+ return s
98
+ }
99
+ function cpf(r: Rng): string {
100
+ const d = digits(r, 11)
101
+ return `${d.slice(0, 3)}.${d.slice(3, 6)}.${d.slice(6, 9)}-${d.slice(9)}`
102
+ }
103
+ function cnpj(r: Rng): string {
104
+ const d = digits(r, 14)
105
+ return `${d.slice(0, 2)}.${d.slice(2, 5)}.${d.slice(5, 8)}/${d.slice(8, 12)}-${d.slice(12)}`
106
+ }
107
+ function phone(r: Rng): string {
108
+ return `(${digits(r, 2)}) 9${digits(r, 4)}-${digits(r, 4)}`
109
+ }
110
+ const capitalize = (s: string): string => (s.length > 0 ? s[0]!.toUpperCase() + s.slice(1) : s)
111
+ const FILL = ['a', 'e', 'i', 'o', 'n', 's', 'r', 't'] as const
112
+
113
+ /** Ajusta uma string SEM formato ao length/min/max — só preenche/trunca (texto puro). */
114
+ function fitPlain(s: string, r: Rng, minC?: number, maxC?: number, lenC?: number): string {
115
+ let out = s
116
+ const target = lenC ?? minC
117
+ if (target !== undefined) while (out.length < target) out += rpick(r, FILL)
118
+ const cap = lenC ?? maxC
119
+ if (cap !== undefined && out.length > cap) out = out.slice(0, cap)
120
+ return out
121
+ }
122
+
123
+ /** Email VÁLIDO respeitando max/min — encurta ('aaaa@b.co') quando o max aperta, ou estende o
124
+ * local quando o min cobra. Um `max` < 6 é schema contraditório (não há email tão curto). */
125
+ function fakeEmail(r: Rng, minC?: number, maxC?: number): string {
126
+ let e = `${slug(fullName(r))}@exemplo.com.br`
127
+ if (maxC !== undefined && e.length > maxC) {
128
+ e = `${'a'.repeat(Math.max(1, maxC - 5))}@b.co` // reserva '@b.co' (5 chars)
129
+ }
130
+ if (minC !== undefined && e.length < minC) {
131
+ const at = e.indexOf('@')
132
+ e = e.slice(0, at) + 'x'.repeat(minC - e.length) + e.slice(at)
133
+ }
134
+ return e
135
+ }
136
+
137
+ // ── Introspecção zod 3 ──────────────────────────────────────────────────────────
138
+
139
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
140
+ type Def = any
141
+ function def(schema: ZodTypeAny): Def {
142
+ return (schema as unknown as { _def: Def })._def
143
+ }
144
+ function findCheck(d: Def, kind: string): Def | undefined {
145
+ return (d.checks ?? []).find((c: Def) => c.kind === kind)
146
+ }
147
+
148
+ function fakeString(schema: ZodTypeAny, r: Rng, hint: string): string {
149
+ const d = def(schema)
150
+ const minC = findCheck(d, 'min')?.value as number | undefined
151
+ const maxC = findCheck(d, 'max')?.value as number | undefined
152
+ const lenC = findCheck(d, 'length')?.value as number | undefined
153
+ const has = (k: string): boolean => findCheck(d, k) !== undefined
154
+ const bounded = minC !== undefined || maxC !== undefined || lenC !== undefined
155
+ const h = hint.toLowerCase()
156
+
157
+ // 1) Formato por CHECK do zod — a validade EXIGE o formato. Email é length-aware; uuid/url/
158
+ // datetime têm tamanho próprio, então um length conflitante COM esses é schema contraditório.
159
+ if (has('email')) return fakeEmail(r, minC, maxC)
160
+ if (has('uuid')) return uuid(r)
161
+ if (has('datetime')) return isoDate(r)
162
+ if (has('url')) return `https://exemplo.com.br/${slug(rpick(r, WORDS))}`
163
+ if (has('cuid') || has('cuid2')) return `c${digits(r, 24)}`
164
+
165
+ // 2) Formato por NOME de campo — só um PALPITE. Se houver length/min/max, a restrição VENCE
166
+ // (é autoritativa) e caímos no texto puro; sem restrição, o palpite formata.
167
+ if (!bounded) {
168
+ if (/mail/.test(h)) return fakeEmail(r)
169
+ if (/cnpj/.test(h)) return cnpj(r)
170
+ if (/cpf|documento|doc$/.test(h)) return cpf(r)
171
+ if (/(phone|fone|tel|celular|whats)/.test(h)) return phone(r)
172
+ if (/(^id$|_id$|id$|uuid)/.test(h)) return uuid(r)
173
+ if (/(url|link|site|href)/.test(h)) return `https://exemplo.com.br/${slug(rpick(r, WORDS))}`
174
+ if (/(at$|_at|date|data|prazo|vencimento)/.test(h)) return isoDate(r)
175
+ }
176
+
177
+ // 3) Texto puro — conteúdo por heurística leve, tamanho ajustado por fitPlain.
178
+ let s: string
179
+ if (/(desc|descric|observ|obs$|nota|resumo|body|conteudo)/.test(h)) s = sentence(r)
180
+ else if (/(nome|name|responsavel|contato)/.test(h)) s = fullName(r)
181
+ else if (/(titulo|title|label|assunto|slug)/.test(h)) s = capitalize(rpick(r, WORDS))
182
+ else s = rpick(r, WORDS)
183
+ return fitPlain(s, r, minC, maxC, lenC)
184
+ }
185
+
186
+ function fakeNumber(schema: ZodTypeAny, r: Rng): number {
187
+ const d = def(schema)
188
+ const isInt = Boolean(findCheck(d, 'int'))
189
+ const minC = findCheck(d, 'min')
190
+ const maxC = findCheck(d, 'max')
191
+ const eps = isInt ? 1 : 0.01 // recuo dos exclusivos; casa com o round(2) do float
192
+ let min = minC?.value as number | undefined
193
+ let max = maxC?.value as number | undefined
194
+ // `.gt`/`.lt` (inclusive === false) recuam pro primeiro válido.
195
+ if (min !== undefined && minC?.inclusive === false) min += eps
196
+ if (max !== undefined && maxC?.inclusive === false) max -= eps
197
+ // Defaults derivados: só-max → min = max-100 (cobre max negativo); só-min → max = min+100.
198
+ if (min === undefined) min = max !== undefined ? max - 100 : 0
199
+ if (max === undefined) max = min + 100
200
+ if (max < min) max = min
201
+ const raw = min + r.next() * (max - min)
202
+ const v = isInt ? Math.round(raw) : Math.round(raw * 100) / 100
203
+ return Math.max(min, Math.min(max, v)) // o round pode empurrar 1 casa pra fora dos limites
204
+ }
205
+
206
+ function gen(schema: ZodTypeAny, r: Rng, hint: string): unknown {
207
+ const d = def(schema)
208
+ switch (d.typeName as string) {
209
+ case 'ZodString':
210
+ return fakeString(schema, r, hint)
211
+ case 'ZodNumber':
212
+ return fakeNumber(schema, r)
213
+ case 'ZodBigInt':
214
+ return BigInt(rint(r, 0, 1_000_000))
215
+ case 'ZodBoolean':
216
+ return r.next() < 0.5
217
+ case 'ZodDate':
218
+ return new Date(isoDate(r))
219
+ case 'ZodLiteral':
220
+ return d.value
221
+ case 'ZodEnum':
222
+ return rpick(r, d.values as string[])
223
+ case 'ZodNativeEnum': {
224
+ // Enum numérico do TS tem reverse-mapping (`{0:'Red', Red:0}`); os VÁLIDOS são os valores
225
+ // das chaves cujo valor NÃO aponta pra um número (igual `getValidEnumValues` do zod).
226
+ const obj = d.values as Record<string, unknown>
227
+ const valid = Object.keys(obj)
228
+ .filter((k) => typeof obj[obj[k] as string] !== 'number')
229
+ .map((k) => obj[k])
230
+ return rpick(r, valid)
231
+ }
232
+ case 'ZodObject': {
233
+ const shape = typeof d.shape === 'function' ? d.shape() : d.shape
234
+ const out: Record<string, unknown> = {}
235
+ for (const key of Object.keys(shape)) out[key] = gen(shape[key] as ZodTypeAny, r, key)
236
+ return out
237
+ }
238
+ case 'ZodArray': {
239
+ const n = rint(r, 2, 4)
240
+ return Array.from({ length: n }, () => gen(d.type as ZodTypeAny, r, hint))
241
+ }
242
+ case 'ZodTuple':
243
+ return (d.items as ZodTypeAny[]).map((it) => gen(it, r, hint))
244
+ case 'ZodOptional':
245
+ case 'ZodNullable':
246
+ case 'ZodDefault':
247
+ case 'ZodCatch':
248
+ case 'ZodReadonly':
249
+ case 'ZodBranded':
250
+ // Populamos o interior (fixture rico); optional/nullable NÃO viram undefined/null.
251
+ return gen((d.innerType ?? d.schema) as ZodTypeAny, r, hint)
252
+ case 'ZodEffects':
253
+ return gen(d.schema as ZodTypeAny, r, hint)
254
+ case 'ZodPipeline':
255
+ return gen(d.in as ZodTypeAny, r, hint)
256
+ case 'ZodUnion':
257
+ return gen(rpick(r, d.options as ZodTypeAny[]), r, hint)
258
+ case 'ZodDiscriminatedUnion': {
259
+ const opts = [...(d.options as Map<unknown, ZodTypeAny> | ZodTypeAny[]).values?.() ?? (d.options as ZodTypeAny[])]
260
+ return gen(rpick(r, opts as ZodTypeAny[]), r, hint)
261
+ }
262
+ case 'ZodRecord': {
263
+ const out: Record<string, unknown> = {}
264
+ for (let i = 0; i < rint(r, 1, 3); i++) out[slug(rpick(r, WORDS))] = gen(d.valueType as ZodTypeAny, r, hint)
265
+ return out
266
+ }
267
+ case 'ZodNull':
268
+ return null
269
+ case 'ZodAny':
270
+ case 'ZodUnknown':
271
+ case 'ZodUndefined':
272
+ case 'ZodVoid':
273
+ return undefined
274
+ default:
275
+ return undefined
276
+ }
277
+ }
278
+
279
+ // ── API pública ─────────────────────────────────────────────────────────────────
280
+
281
+ function seedOf(opts: FakeOptions | undefined, salt: number): number {
282
+ const s = opts?.seed ?? 1
283
+ return (typeof s === 'string' ? hashStr(s) : s >>> 0) ^ (salt * 0x9e3779b1)
284
+ }
285
+
286
+ /** Um valor sintético que satisfaz o schema. Determinístico pela `seed`. */
287
+ export function fake<T>(schema: z.ZodType<T>, opts?: FakeOptions): T {
288
+ const r: Rng = { next: mulberry32(seedOf(opts, 0)) }
289
+ return gen(schema as ZodTypeAny, r, '') as T
290
+ }
291
+
292
+ /** `count` valores sintéticos distintos (seed variada por índice), estáveis entre execuções. */
293
+ export function fakeMany<T>(schema: z.ZodType<T>, count: number, opts?: FakeOptions): T[] {
294
+ return Array.from({ length: Math.max(0, count) }, (_, i) => {
295
+ const r: Rng = { next: mulberry32(seedOf(opts, i + 1)) }
296
+ return gen(schema as ZodTypeAny, r, '') as T
297
+ })
298
+ }