@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,62 @@
1
+ /**
2
+ * @softize/opus/mcp — expõe as actions `ai:enabled` de um runtime como **tools MCP**, pra
3
+ * uma IA de FORA (Claude Desktop, o agente de um parceiro, um hub próprio) alcançar o app
4
+ * pelo protocolo. É o MESMO bridge do agente co-locado (`runtime.aiTools`) e a MESMA execução
5
+ * (`runtime.execute`, com o contexto resolvido) — o MCP é só o transporte.
6
+ *
7
+ * Você monta o transporte (stdio pra local, HTTP/SSE pra remoto) e conecta:
8
+ * `await createOpusMcpServer(runtime, { resolveContext }).connect(transport)`.
9
+ *
10
+ * `@modelcontextprotocol/sdk` já é dep do pacote.
11
+ */
12
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
13
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
14
+ import { zodToJsonSchema } from 'zod-to-json-schema'
15
+ import type { ContextBase, Runtime } from '../core/index.ts'
16
+
17
+ export interface OpusMcpOptions {
18
+ name?: string
19
+ version?: string
20
+ /** Resolve a identidade da requisição MCP → contexto do runtime (auth). O `extra` traz o
21
+ * que o transporte capturou (headers/token). Sem isso, executa ANÔNIMO — o `can` nega, então
22
+ * só actions públicas rodam. Pra "como o usuário", valide o token (IdP) aqui e devolva o base. */
23
+ resolveContext?: (extra: unknown) => Promise<ContextBase> | ContextBase
24
+ /** Conversor Schema → JSON Schema (injetável). Default: `zod-to-json-schema` inline. */
25
+ toJsonSchema?: (schema: unknown) => unknown
26
+ }
27
+
28
+ const ANON: ContextBase = { user: null, tenantId: null, can: () => false, meta: {} }
29
+
30
+ /**
31
+ * Monta um `Server` MCP que expõe as actions `ai:enabled` do runtime como tools (ListTools) e
32
+ * as executa como o contexto resolvido (CallTool). Conecte num transporte com `.connect()`.
33
+ */
34
+ export function createOpusMcpServer(runtime: Runtime, opts: OpusMcpOptions = {}): Server {
35
+ const toJsonSchema =
36
+ opts.toJsonSchema ?? ((schema: unknown) => zodToJsonSchema(schema as never, { $refStrategy: 'none' }))
37
+
38
+ const server = new Server(
39
+ { name: opts.name ?? 'opus', version: opts.version ?? '0.0.0' },
40
+ { capabilities: { tools: {} } },
41
+ )
42
+
43
+ server.setRequestHandler(ListToolsRequestSchema, () => ({
44
+ tools: runtime.aiTools().map((t) => ({
45
+ name: t.name,
46
+ description: t.description,
47
+ inputSchema: toJsonSchema(t.inputSchema) as { type: 'object' },
48
+ })),
49
+ }))
50
+
51
+ server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
52
+ const base = opts.resolveContext ? await opts.resolveContext(extra) : ANON
53
+ const result = await runtime.execute(req.params.name, req.params.arguments ?? {}, base)
54
+ const payload = result.ok ? result.data : { error: result.error }
55
+ return {
56
+ content: [{ type: 'text', text: JSON.stringify(payload ?? null) }],
57
+ isError: !result.ok,
58
+ }
59
+ })
60
+
61
+ return server
62
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @softize/opus/queue/bullmq — BullMQ queue driver
3
+ *
4
+ * Implementa `QueueAdapter` wrappando uma instância `Queue` do BullMQ.
5
+ * Faz mapping `JobSpec` ↔ BullMQ job e `BullMQ state` → `JobStatus`.
6
+ *
7
+ * Worker (consumer-side) é responsabilidade do consumer — esse adapter
8
+ * só **enfileira** e **consulta**. Pra processar, o consumer registra
9
+ * um `new Worker(queueName, processor, { connection })` que chama
10
+ * `runtime.execute()` no callback.
11
+ *
12
+ * Uso:
13
+ * import { Queue } from 'bullmq'
14
+ * import { bullmqQueue } from '@softize/opus/queue/bullmq'
15
+ *
16
+ * const queue = new Queue('tbdlib-jobs', { connection: redisOpts })
17
+ * createRuntime({ queue: bullmqQueue({ queue }) })
18
+ */
19
+
20
+ import type {
21
+ JobHandle,
22
+ JobSpec,
23
+ JobStatus,
24
+ QueueAdapter,
25
+ } from '../../core/index.ts'
26
+
27
+ // =============================================================================
28
+ // BullMQ-compatible interfaces (zero direct dep no tipo público)
29
+ // =============================================================================
30
+
31
+ /** Estado nativo do BullMQ retornado por `job.getState()`. */
32
+ export type BullMQState =
33
+ | 'waiting'
34
+ | 'waiting-children'
35
+ | 'active'
36
+ | 'completed'
37
+ | 'failed'
38
+ | 'delayed'
39
+ | 'paused'
40
+ | 'prioritized'
41
+ | 'unknown'
42
+
43
+ export interface BullMQJob {
44
+ id?: string | undefined
45
+ data: unknown
46
+ returnvalue?: unknown
47
+ failedReason?: string | undefined
48
+ attemptsMade: number
49
+ timestamp: number
50
+ processedOn?: number | undefined
51
+ finishedOn?: number | undefined
52
+ progress: number | object
53
+ getState(): Promise<BullMQState>
54
+ remove(): Promise<void>
55
+ }
56
+
57
+ export interface BullMQQueue {
58
+ add(
59
+ name: string,
60
+ data: unknown,
61
+ opts?: { jobId?: string },
62
+ ): Promise<BullMQJob>
63
+ getJob(jobId: string): Promise<BullMQJob | undefined | null>
64
+ }
65
+
66
+ // =============================================================================
67
+ // Adapter factory
68
+ // =============================================================================
69
+
70
+ export interface BullmqQueueOptions {
71
+ queue: BullMQQueue
72
+ name?: string
73
+ }
74
+
75
+ export function bullmqQueue(options: BullmqQueueOptions): QueueAdapter {
76
+ const { queue, name = 'bullmq' } = options
77
+
78
+ return {
79
+ name,
80
+ kind: 'queue',
81
+
82
+ async enqueue(spec: JobSpec): Promise<JobHandle> {
83
+ const job = await queue.add(spec.action, spec, { jobId: spec.jobId })
84
+ return jobToHandle(spec.action, job, 'queued')
85
+ },
86
+
87
+ async status(jobId: string): Promise<JobHandle | null> {
88
+ const job = await queue.getJob(jobId)
89
+ if (job === undefined || job === null) return null
90
+ const state = await job.getState()
91
+ const action = readAction(job)
92
+ return jobToHandle(action, job, mapState(state))
93
+ },
94
+
95
+ async cancel(jobId: string): Promise<boolean> {
96
+ const job = await queue.getJob(jobId)
97
+ if (job === undefined || job === null) return false
98
+ await job.remove()
99
+ return true
100
+ },
101
+ }
102
+ }
103
+
104
+ // =============================================================================
105
+ // Mapping
106
+ // =============================================================================
107
+
108
+ /**
109
+ * BullMQ state → tbdlib JobStatus.
110
+ *
111
+ * tbdlib não tem 'paused' nem 'delayed' separados — todos viram 'queued'.
112
+ * Distinção fina fica no `details.meta.bullmqState` se consumer precisar.
113
+ */
114
+ export function mapState(state: BullMQState): JobStatus {
115
+ switch (state) {
116
+ case 'completed':
117
+ return 'done'
118
+ case 'failed':
119
+ return 'failed'
120
+ case 'active':
121
+ return 'running'
122
+ case 'waiting':
123
+ case 'waiting-children':
124
+ case 'delayed':
125
+ case 'paused':
126
+ case 'prioritized':
127
+ return 'queued'
128
+ /* v8 ignore next 3 — 'unknown' é defensivo; BullMQ documenta os estados acima */
129
+ case 'unknown':
130
+ default:
131
+ return 'queued'
132
+ }
133
+ }
134
+
135
+ function jobToHandle(
136
+ action: string,
137
+ job: BullMQJob,
138
+ status: JobStatus,
139
+ ): JobHandle {
140
+ const handle: JobHandle = {
141
+ jobId: job.id ?? '',
142
+ action,
143
+ status,
144
+ enqueuedAt: toIso(job.timestamp),
145
+ attempts: job.attemptsMade,
146
+ }
147
+ if (job.processedOn !== undefined) handle.startedAt = toIso(job.processedOn)
148
+ if (job.finishedOn !== undefined) handle.finishedAt = toIso(job.finishedOn)
149
+ if (status === 'done' && job.returnvalue !== undefined) handle.data = job.returnvalue
150
+ if (status === 'failed' && job.failedReason !== undefined) {
151
+ handle.error = {
152
+ code: 'queue.job_failed',
153
+ category: 'internal',
154
+ message: job.failedReason,
155
+ severity: 'error',
156
+ retriable: false,
157
+ }
158
+ }
159
+ const progress = readProgress(job)
160
+ if (progress !== undefined) handle.progress = progress
161
+ return handle
162
+ }
163
+
164
+ function readAction(job: BullMQJob): string {
165
+ if (
166
+ typeof job.data === 'object' &&
167
+ job.data !== null &&
168
+ 'action' in job.data &&
169
+ typeof (job.data as { action: unknown }).action === 'string'
170
+ ) {
171
+ return (job.data as { action: string }).action
172
+ }
173
+ return ''
174
+ }
175
+
176
+ function readProgress(job: BullMQJob): JobHandle['progress'] | undefined {
177
+ const p = job.progress
178
+ if (typeof p === 'number') {
179
+ if (p === 0) return undefined
180
+ return { percent: p }
181
+ }
182
+ if (typeof p === 'object' && p !== null && Object.keys(p).length > 0) {
183
+ return p as JobHandle['progress']
184
+ }
185
+ return undefined
186
+ }
187
+
188
+ function toIso(ms: number): string {
189
+ return new Date(ms).toISOString()
190
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @softize/opus/queue — shared helpers/types for queue drivers.
3
+ *
4
+ * Drivers (bullmq, futuro: inngest, trigger-dev) implementam QueueAdapter
5
+ * do core. Espaço reservado pra helpers comuns (job status mapping,
6
+ * retry calculator universal).
7
+ */
8
+
9
+ export {}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @softize/opus/scheduler/node-cron — in-process cron scheduler driver.
3
+ *
4
+ * Wrappa `node-cron` implementando `SchedulerAdapter`. Roda na mesma
5
+ * process; suficiente pra monolitos. Pra ambientes distribuídos (múltiplos
6
+ * pods rodando o mesmo schedule), use o driver `/bullmq` (a vir) ou
7
+ * `/temporal` que coordenam via Redis/storage.
8
+ *
9
+ * Uso:
10
+ * import { nodeCronScheduler } from '@softize/opus/scheduler/node-cron'
11
+ *
12
+ * createRuntime({
13
+ * scheduler: nodeCronScheduler(),
14
+ * // ...
15
+ * })
16
+ */
17
+
18
+ import { schedule as cronSchedule, type ScheduledTask } from 'node-cron'
19
+ import type {
20
+ ScheduleDef,
21
+ SchedulerAdapter,
22
+ Unsubscribe,
23
+ } from '../../core/index.ts'
24
+ import { everyToCron } from '../every.ts'
25
+
26
+ export interface NodeCronOptions {
27
+ /** Nome do adapter. Default 'node-cron'. */
28
+ name?: string
29
+
30
+ /** Override do logger de erros. Default console.error. */
31
+ onError?: (err: unknown, schedule: ScheduleDef) => void
32
+ }
33
+
34
+ export function nodeCronScheduler(
35
+ options: NodeCronOptions = {},
36
+ ): SchedulerAdapter {
37
+ const { name = 'node-cron', onError = defaultOnError } = options
38
+ const tasks: ScheduledTask[] = []
39
+
40
+ return {
41
+ name,
42
+ kind: 'scheduler',
43
+
44
+ register(spec, fire): Unsubscribe {
45
+ const cron = resolveCron(spec)
46
+ const cronOptions: { scheduled: true; timezone?: string } = {
47
+ scheduled: true,
48
+ }
49
+ if (spec.timezone !== undefined) cronOptions.timezone = spec.timezone
50
+ const task = cronSchedule(
51
+ cron,
52
+ () => {
53
+ // Wrap sync throws + async rejects no mesmo handler
54
+ void (async () => {
55
+ try {
56
+ await fire()
57
+ } catch (err) {
58
+ onError(err, spec)
59
+ }
60
+ })()
61
+ },
62
+ cronOptions,
63
+ )
64
+ tasks.push(task)
65
+ return () => {
66
+ task.stop()
67
+ const idx = tasks.indexOf(task)
68
+ if (idx >= 0) tasks.splice(idx, 1)
69
+ }
70
+ },
71
+
72
+ async dispose() {
73
+ for (const task of tasks.splice(0)) task.stop()
74
+ },
75
+
76
+ async healthCheck() {
77
+ return { ok: true, details: { tasks: tasks.length } }
78
+ },
79
+ }
80
+ }
81
+
82
+ function resolveCron(spec: ScheduleDef): string {
83
+ if (spec.cron !== undefined) return spec.cron
84
+ if (spec.every !== undefined) return everyToCron(spec.every)
85
+ throw new Error(
86
+ `Schedule "${spec.name}" must declare either 'cron' or 'every'.`,
87
+ )
88
+ }
89
+
90
+ function defaultOnError(err: unknown, schedule: ScheduleDef): void {
91
+ // eslint-disable-next-line no-console
92
+ console.error(`[tbdlib/scheduler] schedule "${schedule.name}" fire failed:`, err)
93
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Conversão de `every` shorthand → expressão cron de 5 campos.
3
+ *
4
+ * Suporta unidades: 's' (segundo), 'm' (minuto), 'h' (hora).
5
+ * - '15s' → '*\/15 * * * * *' (6-field cron com segundos)
6
+ * - '5m' → '*\/5 * * * *'
7
+ * - '1h' → '0 * * * *' (toda hora cheia)
8
+ * - '6h' → '0 *\/6 * * *'
9
+ * - '1d' → '0 0 * * *' (meia-noite)
10
+ *
11
+ * Não há "every 3 days" — limita ao que faz sentido em cron.
12
+ */
13
+
14
+ export function everyToCron(every: string): string {
15
+ const match = /^(\d+)(s|m|h|d)$/.exec(every.trim())
16
+ if (match === null) {
17
+ throw new Error(
18
+ `Invalid 'every' shorthand: "${every}". ` +
19
+ `Expected formato "<n><s|m|h|d>" (ex: "15s", "5m", "1h", "1d").`,
20
+ )
21
+ }
22
+ const n = Number.parseInt(match[1] as string, 10)
23
+ const unit = match[2] as 's' | 'm' | 'h' | 'd'
24
+
25
+ if (n <= 0) {
26
+ throw new Error(`'every' value must be > 0, got "${every}".`)
27
+ }
28
+
29
+ switch (unit) {
30
+ case 's':
31
+ if (n > 59) throw new Error(`'every' seconds must be <= 59, got ${n}.`)
32
+ return `*/${n} * * * * *`
33
+ case 'm':
34
+ if (n > 59) throw new Error(`'every' minutes must be <= 59, got ${n}.`)
35
+ return `*/${n} * * * *`
36
+ case 'h':
37
+ if (n > 23) throw new Error(`'every' hours must be <= 23, got ${n}.`)
38
+ return n === 1 ? '0 * * * *' : `0 */${n} * * *`
39
+ case 'd':
40
+ if (n !== 1) {
41
+ throw new Error(`'every' days only supports 1d. For longer intervals use cron.`)
42
+ }
43
+ return '0 0 * * *'
44
+ }
45
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @softize/opus/scheduler — shared helpers/types for scheduler drivers.
3
+ *
4
+ * Drivers (node-cron, futuro: bullmq-repeatable, temporal) implementam
5
+ * SchedulerAdapter do core. Conversões cron ↔ shorthand `every` moram
6
+ * aqui pra serem reusadas entre drivers.
7
+ */
8
+
9
+ export { everyToCron } from './every.ts'