@desplega.ai/agent-swarm 1.149.1 → 1.151.0

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 (538) hide show
  1. package/README.md +18 -3
  2. package/dist/{acp-adapter-vhkzfpjg.js → acp-adapter-ex8hzjfc.js} +5 -5
  3. package/dist/{actions-9d2694yk.js → actions-2bgpyhww.js} +39 -11
  4. package/dist/{app-fq2ggynt.js → app-1706zhr1.js} +7 -7
  5. package/dist/{artifact-gj9arwrr.js → artifact-td4q4dch.js} +1 -1
  6. package/dist/{assistant-edb6n9nn.js → assistant-bavke5p7.js} +44 -20
  7. package/dist/{azure-openai-responses-cqar64rc.js → azure-openai-responses-f38x2avq.js} +13 -13
  8. package/dist/{boot-reembed-msweabbp.js → boot-reembed-3cmrrmpa.js} +9 -9
  9. package/dist/{boot-reembed-q5wd3tha.js → boot-reembed-nafcmmmc.js} +14 -16
  10. package/dist/{boot-scrub-logs-3g1az933.js → boot-scrub-logs-cd6a2sq0.js} +5 -5
  11. package/dist/{claude-adapter-v3rnzsdc.js → claude-adapter-wfp2gzvz.js} +15 -16
  12. package/dist/{claude-managed-adapter-qvsmh6mv.js → claude-managed-adapter-dsmkcrjy.js} +3 -3
  13. package/dist/{claude-sdk-session-qxrpsna1.js → claude-sdk-session-bht9jjak.js} +15 -16
  14. package/dist/{cli-5m0qmvdp.js → cli-0qb1z6s2.js} +1 -1
  15. package/dist/{cli-9d8ntetj.js → cli-2658f6at.js} +1 -1
  16. package/dist/{cli-7evafqvy.js → cli-2tbvf1z1.js} +85 -69
  17. package/dist/{cli-k351hv5k.js → cli-3v9xhjcn.js} +24 -12
  18. package/dist/{cli-1ebpar1q.js → cli-4wppfkcm.js} +350 -20
  19. package/dist/{cli-4260r1tg.js → cli-5jd2z7av.js} +97 -84
  20. package/dist/{cli-cv7s5zdq.js → cli-5tpgq60f.js} +9865 -715
  21. package/dist/{cli-6skz3rk8.js → cli-7tgcstn2.js} +9 -4
  22. package/dist/{cli-01g3x1ca.js → cli-9004vfdf.js} +108 -56
  23. package/dist/{cli-1c94wvcw.js → cli-a1z3h107.js} +124 -99
  24. package/dist/{cli-9br8ypfv.js → cli-czjtgfys.js} +2 -2
  25. package/dist/{cli-5my3bjsd.js → cli-d3p82erb.js} +7 -7
  26. package/dist/{cli-e29gmdmw.js → cli-dv6cdjs2.js} +2 -1
  27. package/dist/{cli-wby1xnnb.js → cli-f1wky6zp.js} +1 -1
  28. package/dist/{cli-zvz62chv.js → cli-fm4ytsdk.js} +4 -4
  29. package/dist/{cli-tg8qj8ee.js → cli-frcta2w4.js} +6 -6
  30. package/dist/{cli-7xx10h0a.js → cli-g69fdc9g.js} +2 -2
  31. package/dist/{cli-cmp8g3sw.js → cli-gbfwcd3k.js} +44 -9
  32. package/dist/{handlers-nsg0bcye.js → cli-gjyvhmmh.js} +97 -45
  33. package/dist/{cli-08b07b4r.js → cli-ht5rs63n.js} +1 -1
  34. package/dist/{cli-jecc01cb.js → cli-j4nv2wqy.js} +2 -4
  35. package/dist/{cli-cd60fv6j.js → cli-j9xczhaq.js} +1 -1
  36. package/dist/cli-pf9q3696.js +172 -0
  37. package/dist/{cli-xm87hzbw.js → cli-pgj4a5fb.js} +1 -1
  38. package/dist/{cli-jezx9b9c.js → cli-ppm4975n.js} +9 -3
  39. package/dist/{cli-3z634pqe.js → cli-q88st9aj.js} +2 -2
  40. package/dist/{cli-3sysyjve.js → cli-r2q6pevn.js} +142 -49
  41. package/dist/{cli-07jd68a1.js → cli-re8z5p8p.js} +1 -1
  42. package/dist/{cli-qgd4sqz5.js → cli-s14agm5s.js} +121 -14
  43. package/dist/{cli-svbrpf5q.js → cli-sjaae6ff.js} +9872 -424
  44. package/dist/{cli-73gq0zxn.js → cli-vwm416m6.js} +13 -13
  45. package/dist/{cli-4wtq5jjv.js → cli-wbyk465z.js} +1 -1
  46. package/dist/{cli-3apbzgyk.js → cli-z61ykvam.js} +19 -14
  47. package/dist/{cli-e06a8a7y.js → cli-zkxymas5.js} +4 -3
  48. package/dist/cli.js +26 -25
  49. package/dist/{codex-adapter-jyw7a91z.js → codex-adapter-jjjk8w4m.js} +8 -9
  50. package/dist/{codex-hook-hfrt9shr.js → codex-hook-8jexc34x.js} +3 -3
  51. package/dist/{codex-session-runner-bmngj0s5.js → codex-session-runner-nd6ayxkx.js} +8 -9
  52. package/dist/{commands-n755g391.js → commands-9v4psyq1.js} +5 -5
  53. package/dist/{db-s1cgh5gv.js → db-wnt58tpe.js} +5 -5
  54. package/dist/dispatcher-05jh1aqk.js +74 -0
  55. package/dist/{e2b-p04vmtx0.js → e2b-413b8t4v.js} +1 -1
  56. package/dist/{google-generative-ai-tsskx7te.js → google-generative-ai-ndjmj5fa.js} +7 -7
  57. package/dist/{google-vertex-ppw8ydpa.js → google-vertex-fr665tqs.js} +10 -10
  58. package/dist/handlers-tw90pkh5.js +65 -0
  59. package/dist/{hook-jf0xe8zh.js → hook-123e2scb.js} +11 -12
  60. package/dist/{hook-h9cqrm8q.js → hook-sxe3r349.js} +11 -12
  61. package/dist/{http-nc6pydw5.js → http-ecsar8ez.js} +1442 -581
  62. package/dist/{index-ezxkds2n.js → index-16js521z.js} +335 -107
  63. package/dist/{index-769nc012.js → index-hw26vj40.js} +25 -19
  64. package/dist/{index-0wdy6gwc.js → index-nnrbpcjp.js} +25 -17
  65. package/dist/index-ymn7em0p.js +25 -0
  66. package/dist/{index-x1zcj09z.js → index-zj518gf9.js} +26 -20
  67. package/dist/{keepalive-nesm3xb6.js → keepalive-asqtcm4y.js} +7 -7
  68. package/dist/{lead-bwy6t2za.js → lead-xbfr7fvt.js} +40 -48
  69. package/dist/{maintenance-8w393zf0.js → maintenance-qma55qvw.js} +9 -9
  70. package/dist/{oauth-refresh-sweep-sh8xjy08.js → oauth-refresh-sweep-3s04c6qw.js} +6 -6
  71. package/dist/{onboard-25ps29h1.js → onboard-drskd386.js} +13 -4
  72. package/dist/{openai-codex-responses-g7w48y30.js → openai-codex-responses-wy6a43wd.js} +13 -13
  73. package/dist/{openai-responses-nmeyptke.js → openai-responses-6xwf7bzg.js} +19 -19
  74. package/dist/{opencode-adapter-dce4wskd.js → opencode-adapter-28axqa09.js} +4 -4
  75. package/dist/{otel-impl-b93gxnz5.js → otel-impl-943c96er.js} +1 -1
  76. package/dist/{pi-mono-adapter-zvshk5cj.js → pi-mono-adapter-jcqt71cg.js} +17 -19
  77. package/dist/{pricing-refresh-5v56x7gb.js → pricing-refresh-mx51az05.js} +7 -7
  78. package/dist/{rbac-roles-x55cnezn.js → rbac-roles-78baecqd.js} +13 -9
  79. package/dist/{rbac-roles-0p0s6pbj.js → rbac-roles-wcmrg6sv.js} +7 -7
  80. package/dist/{render-v2-hyd45w8b.js → render-v2-8zjntgf5.js} +7 -7
  81. package/dist/{seed-pricing-49gbgn96.js → seed-pricing-jqjs6cws.js} +6 -6
  82. package/dist/{setup-qvq64vzq.js → setup-4pks0vfk.js} +2 -2
  83. package/dist/work-objects-wangt792.js +165 -0
  84. package/dist/{worker-scj60cxg.js → worker-p2mr422x.js} +40 -48
  85. package/openapi.json +1365 -268
  86. package/package.json +4 -3
  87. package/src/agentmail/handlers.ts +95 -66
  88. package/src/be/budget-refusal-notify.ts +1 -0
  89. package/src/be/db/tasks/read.ts +3 -0
  90. package/src/be/db/tasks/write.ts +13 -1
  91. package/src/be/db-client.ts +5 -0
  92. package/src/be/db.ts +15 -5
  93. package/src/be/extensions/db.ts +461 -0
  94. package/src/be/extensions/validate.ts +107 -0
  95. package/src/be/memory/boot-reembed.ts +17 -12
  96. package/src/be/memory/index.ts +12 -0
  97. package/src/be/memory/providers/openai-embedding.ts +4 -0
  98. package/src/be/memory/types.ts +2 -0
  99. package/src/be/migrations/153_deferred_task_wait_members.sql +25 -0
  100. package/src/be/migrations/154_routing_source.sql +3 -0
  101. package/src/be/migrations/155_extensions.sql +66 -0
  102. package/src/be/migrations/156_inbox_item_state_read_at.sql +7 -0
  103. package/src/be/rbac-roles.ts +3 -2
  104. package/src/be/scripts/typecheck.ts +59 -41
  105. package/src/be/seed/schedules-seeder.ts +25 -2
  106. package/src/be/seed/workflows-seeder.ts +17 -0
  107. package/src/be/seed-scripts/catalog/delegate.ts +3 -1
  108. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  109. package/src/be/seed-skills/index.ts +7 -0
  110. package/src/be/steering.ts +1 -0
  111. package/src/be/swarm-config-guard.ts +3 -1
  112. package/src/commands/credential-refresh.ts +70 -0
  113. package/src/commands/onboard/compose-generator.ts +14 -2
  114. package/src/commands/onboard/env-generator.ts +7 -0
  115. package/src/commands/provider-credentials.ts +2 -25
  116. package/src/commands/runner.ts +24 -57
  117. package/src/extensions/apply-task-create.ts +135 -0
  118. package/src/extensions/bundle-path.ts +10 -0
  119. package/src/extensions/contract-runtime.ts +7 -0
  120. package/src/extensions/contract-types.generated.ts +4 -0
  121. package/src/extensions/contract.ts +270 -0
  122. package/src/extensions/ctx.ts +186 -0
  123. package/src/extensions/dispatcher.ts +479 -0
  124. package/src/extensions/identity.ts +44 -0
  125. package/src/extensions/lifecycle.ts +281 -0
  126. package/src/extensions/loader.ts +207 -0
  127. package/src/extensions/post-bridge.ts +161 -0
  128. package/src/github/handlers.ts +185 -140
  129. package/src/gitlab/handlers.ts +97 -77
  130. package/src/heartbeat/heartbeat.ts +155 -83
  131. package/src/http/agents.ts +1 -1
  132. package/src/http/all-routes.ts +1 -0
  133. package/src/http/approval-requests.ts +1 -0
  134. package/src/http/apps.ts +14 -9
  135. package/src/http/config.ts +4 -1
  136. package/src/http/core.ts +38 -2
  137. package/src/http/extensions.ts +547 -0
  138. package/src/http/inbox-state.ts +2 -0
  139. package/src/http/index.ts +41 -3
  140. package/src/http/mcp-bridge.ts +96 -62
  141. package/src/http/mcp.ts +1 -1
  142. package/src/http/script-runs.ts +5 -0
  143. package/src/http/status.ts +6 -4
  144. package/src/http/tasks.ts +51 -30
  145. package/src/integrations/kapso/inbound.ts +15 -10
  146. package/src/jira/sync.ts +26 -16
  147. package/src/linear/sync.ts +26 -16
  148. package/src/prompts/session-templates.ts +18 -13
  149. package/src/providers/claude-adapter.ts +21 -8
  150. package/src/rbac/admission.ts +1 -1
  151. package/src/rbac/elevated-agents.ts +24 -0
  152. package/src/rbac/legacy-policy.ts +35 -6
  153. package/src/rbac/permissions.ts +8 -0
  154. package/src/scheduler/deferred-task-waits.ts +138 -27
  155. package/src/scheduler/schedule-task.ts +54 -24
  156. package/src/scheduler/scheduler.ts +108 -125
  157. package/src/scripts-runtime/executors/native.ts +5 -1
  158. package/src/scripts-runtime/executors/types.ts +5 -0
  159. package/src/scripts-runtime/import-allowlist.ts +79 -19
  160. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  161. package/src/scripts-runtime/swarm-config.ts +8 -0
  162. package/src/scripts-runtime/swarm-sdk.ts +3 -0
  163. package/src/scripts-runtime/types/stdlib.d.ts +16 -3
  164. package/src/scripts-runtime/types/swarm-extension.d.ts +562 -0
  165. package/src/scripts-runtime/types/swarm-sdk.d.ts +16 -3
  166. package/src/server.ts +6 -0
  167. package/src/slack/HEURISTICS.md +3 -1
  168. package/src/slack/actions.ts +21 -16
  169. package/src/slack/app.ts +2 -0
  170. package/src/slack/assistant.ts +4 -12
  171. package/src/slack/blocks.ts +1 -1
  172. package/src/slack/handlers.ts +107 -19
  173. package/src/slack/inbound-files.ts +9 -5
  174. package/src/slack/render-v2.ts +188 -38
  175. package/src/slack/router.ts +13 -3
  176. package/src/slack/steering.ts +3 -3
  177. package/src/slack/thread-buffer.ts +66 -23
  178. package/src/slack/work-objects.test.ts +252 -0
  179. package/src/slack/work-objects.ts +118 -0
  180. package/src/tasks/errors.ts +9 -0
  181. package/src/tasks/sibling-awareness.ts +35 -1
  182. package/src/tasks/worker-follow-up.ts +67 -11
  183. package/src/tests/acp-adapter.test.ts +10 -10
  184. package/src/tests/agent-fs-live-url.test.ts +40 -0
  185. package/src/tests/agent-fs-provision-seeder.test.ts +12 -12
  186. package/src/tests/api-key-tracking.test.ts +18 -18
  187. package/src/tests/api-key.test.ts +4 -2
  188. package/src/tests/apps-rbac.test.ts +1 -1
  189. package/src/tests/apps-spike5.test.ts +1 -1
  190. package/src/tests/apps-sync-engine.test.ts +8 -8
  191. package/src/tests/apps-user-config.test.ts +1 -1
  192. package/src/tests/artifact-sdk.test.ts +8 -8
  193. package/src/tests/asset-key-mcp.test.ts +2 -0
  194. package/src/tests/automation-preflight-alert.test.ts +1 -1
  195. package/src/tests/base-prompt.test.ts +7 -6
  196. package/src/tests/budget-user-scope.test.ts +1 -1
  197. package/src/tests/budgets-routes.test.ts +1 -1
  198. package/src/tests/claude-adapter-binary.test.ts +22 -40
  199. package/src/tests/claude-adapter-otel.test.ts +13 -7
  200. package/src/tests/claude-adapter.test.ts +8 -6
  201. package/src/tests/claude-hook-credentials.test.ts +3 -3
  202. package/src/tests/claude-managed-adapter.test.ts +4 -4
  203. package/src/tests/claude-managed-setup.test.ts +3 -3
  204. package/src/tests/claude-sdk-lifecycle.test.ts +1 -1
  205. package/src/tests/claude-sdk-transport.test.ts +6 -6
  206. package/src/tests/claude-stop-hook.test.ts +8 -8
  207. package/src/tests/claude-worker-parity.test.ts +3 -3
  208. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +5 -5
  209. package/src/tests/codex-adapter.test.ts +7 -5
  210. package/src/tests/codex-app-server.test.ts +2 -2
  211. package/src/tests/codex-login.test.ts +15 -15
  212. package/src/tests/codex-oauth-credential-info.test.ts +4 -4
  213. package/src/tests/codex-oauth-keep-warm.test.ts +4 -4
  214. package/src/tests/codex-oauth-refresh-lock.test.ts +1 -1
  215. package/src/tests/codex-oauth-storage.test.ts +7 -7
  216. package/src/tests/codex-pool.test.ts +1 -1
  217. package/src/tests/codex-steering-hook.test.ts +1 -1
  218. package/src/tests/codex-swarm-events.test.ts +1 -1
  219. package/src/tests/config-api-only-keys.test.ts +1 -1
  220. package/src/tests/config-session-auth.test.ts +2 -2
  221. package/src/tests/connection-embedded-auth.test.ts +20 -18
  222. package/src/tests/core-auth.test.ts +1 -1
  223. package/src/tests/create-page-tool.test.ts +3 -3
  224. package/src/tests/credential-refresh.test.ts +115 -0
  225. package/src/tests/credential-wait.test.ts +2 -2
  226. package/src/tests/credentials.test.ts +6 -4
  227. package/src/tests/db-queries-oauth.test.ts +40 -40
  228. package/src/tests/defer-task.test.ts +73 -8
  229. package/src/tests/deferred-task-wait-migration.test.ts +64 -0
  230. package/src/tests/deferred-task-wake.test.ts +349 -8
  231. package/src/tests/devin-adapter.test.ts +1 -1
  232. package/src/tests/devin-api.test.ts +1 -1
  233. package/src/tests/docs-index-footprint.test.js +3 -3
  234. package/src/tests/e2b-dispatch.test.ts +21 -19
  235. package/src/tests/e2e-claude-transport.test.ts +3 -3
  236. package/src/tests/e2e-redact.test.ts +6 -6
  237. package/src/tests/ensure-token.test.ts +68 -68
  238. package/src/tests/entrypoint-api-readiness.test.ts +1 -1
  239. package/src/tests/entrypoint-codex-oauth-seed.test.ts +13 -13
  240. package/src/tests/env-flag.test.ts +1 -0
  241. package/src/tests/events-http.test.ts +1 -1
  242. package/src/tests/extensions-db.test.ts +216 -0
  243. package/src/tests/extensions-dispatcher.test.ts +275 -0
  244. package/src/tests/extensions-example-heartbeat-remediate.test.ts +69 -0
  245. package/src/tests/extensions-example-mcp-install.test.ts +63 -0
  246. package/src/tests/extensions-example-no-exclamation-marks.test.ts +72 -0
  247. package/src/tests/extensions-example-rewrite-and-suppress.test.ts +70 -0
  248. package/src/tests/extensions-example-route-slack-channel.test.ts +217 -0
  249. package/src/tests/extensions-heartbeat.test.ts +291 -0
  250. package/src/tests/extensions-http.test.ts +431 -0
  251. package/src/tests/extensions-lead-equivalence.test.ts +49 -0
  252. package/src/tests/extensions-lifecycle.test.ts +209 -0
  253. package/src/tests/extensions-loader.test.ts +95 -0
  254. package/src/tests/extensions-mcp-tools.test.ts +245 -0
  255. package/src/tests/extensions-pre-task.test.ts +487 -0
  256. package/src/tests/extensions-run-context.test.ts +41 -0
  257. package/src/tests/extensions-tool-call.test.ts +288 -0
  258. package/src/tests/extensions-validate.test.ts +196 -0
  259. package/src/tests/fixtures/extensions/bad-import/hooks.ts +8 -0
  260. package/src/tests/fixtures/extensions/bad-import/manifest.json +9 -0
  261. package/src/tests/fixtures/extensions/bad-return-shape/hooks.ts +10 -0
  262. package/src/tests/fixtures/extensions/bad-return-shape/manifest.json +9 -0
  263. package/src/tests/fixtures/extensions/block-tasks-from-source/hooks.ts +22 -0
  264. package/src/tests/fixtures/extensions/block-tasks-from-source/manifest.json +9 -0
  265. package/src/tests/fixtures/extensions/ignore-channel/hooks.ts +22 -0
  266. package/src/tests/fixtures/extensions/ignore-channel/manifest.json +9 -0
  267. package/src/tests/fixtures/extensions/load.ts +10 -0
  268. package/src/tests/fixtures/extensions/minimal/hooks.ts +7 -0
  269. package/src/tests/fixtures/extensions/minimal/manifest.json +9 -0
  270. package/src/tests/fixtures/extensions/never-fail-long-tasks/hooks.ts +11 -0
  271. package/src/tests/fixtures/extensions/never-fail-long-tasks/manifest.json +9 -0
  272. package/src/tests/fixtures/extensions/no-exclamation-marks/hooks.ts +23 -0
  273. package/src/tests/fixtures/extensions/no-exclamation-marks/manifest.json +9 -0
  274. package/src/tests/fixtures/extensions/post-logger/hooks.ts +9 -0
  275. package/src/tests/fixtures/extensions/post-logger/manifest.json +9 -0
  276. package/src/tests/fixtures/extensions/priority-a/hooks.ts +7 -0
  277. package/src/tests/fixtures/extensions/priority-a/manifest.json +9 -0
  278. package/src/tests/fixtures/extensions/priority-b/hooks.ts +7 -0
  279. package/src/tests/fixtures/extensions/priority-b/manifest.json +9 -0
  280. package/src/tests/fixtures/extensions/record-only-on-tag/hooks.ts +16 -0
  281. package/src/tests/fixtures/extensions/record-only-on-tag/manifest.json +9 -0
  282. package/src/tests/fixtures/extensions/reserved-assets/hooks.ts +5 -0
  283. package/src/tests/fixtures/extensions/reserved-assets/manifest.json +10 -0
  284. package/src/tests/fixtures/extensions/rewrite-progress-prefix/hooks.ts +22 -0
  285. package/src/tests/fixtures/extensions/rewrite-progress-prefix/manifest.json +9 -0
  286. package/src/tests/fixtures/extensions/rewrite-task-priority/hooks.ts +7 -0
  287. package/src/tests/fixtures/extensions/rewrite-task-priority/manifest.json +9 -0
  288. package/src/tests/fixtures/extensions/route-channel-to-agent/hooks.ts +26 -0
  289. package/src/tests/fixtures/extensions/route-channel-to-agent/manifest.json +9 -0
  290. package/src/tests/fixtures/extensions/slow/hooks.ts +10 -0
  291. package/src/tests/fixtures/extensions/slow/manifest.json +9 -0
  292. package/src/tests/fixtures/extensions/suppress-lead-follow-up/hooks.ts +12 -0
  293. package/src/tests/fixtures/extensions/suppress-lead-follow-up/manifest.json +9 -0
  294. package/src/tests/fixtures/extensions/throws/hooks.ts +12 -0
  295. package/src/tests/fixtures/extensions/throws/manifest.json +9 -0
  296. package/src/tests/fixtures/extensions/worker-runtime/hooks.ts +5 -0
  297. package/src/tests/fixtures/extensions/worker-runtime/manifest.json +9 -0
  298. package/src/tests/fs-routes.test.ts +1 -1
  299. package/src/tests/gitlab-auth.test.ts +10 -10
  300. package/src/tests/harness-provider-resolution.test.ts +8 -5
  301. package/src/tests/heartbeat-reroute-decision.test.ts +1 -0
  302. package/src/tests/helper-credentials.test.ts +137 -0
  303. package/src/tests/http/context-routes.test.ts +2 -2
  304. package/src/tests/http-api-integration.test.ts +5 -2
  305. package/src/tests/http-users.test.ts +1 -1
  306. package/src/tests/identity-refresh.test.ts +2 -2
  307. package/src/tests/internal-ai/complete-structured.test.ts +2 -2
  308. package/src/tests/internal-ai/credentials.test.ts +19 -17
  309. package/src/tests/jira-metadata.test.ts +1 -1
  310. package/src/tests/jira-oauth.test.ts +5 -5
  311. package/src/tests/jira-sync.test.ts +1 -1
  312. package/src/tests/jira-webhook-lifecycle.test.ts +3 -3
  313. package/src/tests/jira-webhook.test.ts +2 -2
  314. package/src/tests/kapso-inbound.test.ts +2 -2
  315. package/src/tests/kv-http.test.ts +1 -1
  316. package/src/tests/kv-namespace-resolution.test.ts +1 -1
  317. package/src/tests/kv-page-proxy.test.ts +2 -2
  318. package/src/tests/launch-password-rejection.test.ts +2 -2
  319. package/src/tests/linear-webhook.test.ts +1 -1
  320. package/src/tests/mcp-oauth-dcr-reuse.test.ts +6 -6
  321. package/src/tests/mcp-oauth-ensure-token.test.ts +28 -22
  322. package/src/tests/mcp-oauth-manual-client.test.ts +16 -12
  323. package/src/tests/mcp-oauth-queries.test.ts +35 -32
  324. package/src/tests/mcp-oauth-resolve-secrets.test.ts +8 -8
  325. package/src/tests/mcp-oauth-wrapper.test.ts +20 -20
  326. package/src/tests/mcp-user-route.test.ts +1 -1
  327. package/src/tests/memory-edges.test.ts +1 -1
  328. package/src/tests/memory-edit-authorization.test.ts +32 -0
  329. package/src/tests/memory-rate-endpoint.test.ts +1 -1
  330. package/src/tests/memory-rate-tool.test.ts +2 -2
  331. package/src/tests/memory-rater-e2e.test.ts +1 -1
  332. package/src/tests/memory-rater-llm-summarizer.test.ts +3 -3
  333. package/src/tests/memory-rater-llm.test.ts +1 -1
  334. package/src/tests/model-control.test.ts +3 -0
  335. package/src/tests/multi-runtime-enabled-stats.test.ts +2 -2
  336. package/src/tests/multi-runtime-registration.test.ts +111 -17
  337. package/src/tests/oauth-access-token-tool.test.ts +18 -18
  338. package/src/tests/oauth-callback-flow.test.ts +8 -8
  339. package/src/tests/oauth-credential-bindings.test.ts +29 -27
  340. package/src/tests/oauth-keepalive.test.ts +10 -10
  341. package/src/tests/oauth-presets.test.ts +13 -5
  342. package/src/tests/oauth-refresh-failure.test.ts +9 -9
  343. package/src/tests/oauth-refresh-sweep.test.ts +20 -13
  344. package/src/tests/oauth-wrapper.test.ts +1 -1
  345. package/src/tests/onboard-compose.test.ts +47 -3
  346. package/src/tests/onboard-env.test.ts +26 -26
  347. package/src/tests/onboard-non-interactive.test.ts +4 -4
  348. package/src/tests/opencode-adapter.test.ts +1 -1
  349. package/src/tests/opencode-plugin.test.ts +2 -2
  350. package/src/tests/otel-session-cost-metrics.test.ts +1 -1
  351. package/src/tests/page-proxy-authed.test.ts +2 -2
  352. package/src/tests/page-proxy.test.ts +2 -2
  353. package/src/tests/page-session.test.ts +7 -7
  354. package/src/tests/page-viewer-auth.test.ts +2 -2
  355. package/src/tests/pages-authed-mode.test.ts +1 -1
  356. package/src/tests/pages-http.test.ts +1 -1
  357. package/src/tests/pages-password-mode.test.ts +1 -1
  358. package/src/tests/pages-public-authed-401.test.ts +1 -1
  359. package/src/tests/pages-public-html.test.ts +2 -2
  360. package/src/tests/pages-view-count.test.ts +1 -1
  361. package/src/tests/pi-mono-adapter.test.ts +10 -10
  362. package/src/tests/pi-mono-extension.test.ts +2 -2
  363. package/src/tests/preload.ts +5 -0
  364. package/src/tests/pricing-routes.test.ts +1 -1
  365. package/src/tests/profile-sync-warning.test.ts +8 -8
  366. package/src/tests/profile-sync.test.ts +8 -8
  367. package/src/tests/prompt-template-session.test.ts +36 -5
  368. package/src/tests/provider-process-group.test.ts +95 -4
  369. package/src/tests/providers/pi-cost.test.ts +1 -1
  370. package/src/tests/queue-stall-alarm.test.ts +1 -1
  371. package/src/tests/rbac-admission.test.ts +19 -4
  372. package/src/tests/rbac-charact-http.test.ts +1 -1
  373. package/src/tests/rbac-e2e-helpers.ts +1 -1
  374. package/src/tests/rbac-engine.test.ts +39 -0
  375. package/src/tests/realtime-room-auth.test.ts +1 -1
  376. package/src/tests/realtime-room-surfaces.test.ts +1 -1
  377. package/src/tests/reload-config.test.ts +45 -1
  378. package/src/tests/routing-affinity-inventory.test.ts +1 -1
  379. package/src/tests/routing-reason-contract.test.ts +76 -3
  380. package/src/tests/routing-reason-inventory.test.ts +11 -6
  381. package/src/tests/routing-source.test.ts +53 -0
  382. package/src/tests/runner-agent-fs-provision-retry.test.ts +1 -1
  383. package/src/tests/runner-context-preamble.test.ts +1 -1
  384. package/src/tests/runner-deployment-resume.test.ts +1 -1
  385. package/src/tests/runner-fallback-output.test.ts +1 -1
  386. package/src/tests/runner-identity-refresh.test.ts +1 -1
  387. package/src/tests/runner-skills-refresh.test.ts +1 -1
  388. package/src/tests/schedule-target-type.test.ts +3 -3
  389. package/src/tests/scheduled-tasks.test.ts +14 -5
  390. package/src/tests/scheduler-occurrence.test.ts +347 -0
  391. package/src/tests/script-apis-mcp.test.ts +1 -1
  392. package/src/tests/script-connections-http.test.ts +61 -58
  393. package/src/tests/script-connections-mcp.test.ts +1 -1
  394. package/src/tests/script-connections.test.ts +3 -1
  395. package/src/tests/script-runs-http.test.ts +15 -7
  396. package/src/tests/script-workflows-runtime-e2e.test.ts +1 -1
  397. package/src/tests/scripts-boot-reembed.test.ts +4 -0
  398. package/src/tests/scripts-embeddings.test.ts +4 -0
  399. package/src/tests/scripts-external-api.test.ts +1 -1
  400. package/src/tests/scripts-http.test.ts +1 -1
  401. package/src/tests/scripts-import-allowlist.test.ts +44 -1
  402. package/src/tests/scripts-mcp-e2e.test.ts +81 -1
  403. package/src/tests/scripts-only-gating.test.ts +4 -2
  404. package/src/tests/scripts-retention.test.ts +1 -1
  405. package/src/tests/scripts-runtime-identity.test.ts +2 -2
  406. package/src/tests/scripts-runtime-secret-egress.test.ts +1 -1
  407. package/src/tests/scripts-runtime.test.ts +6 -4
  408. package/src/tests/scripts-typecheck.test.ts +21 -0
  409. package/src/tests/sdk-allowlist.test.ts +1 -1
  410. package/src/tests/secret-scrubber.test.ts +38 -31
  411. package/src/tests/seed-registry.test.ts +46 -8
  412. package/src/tests/seed-schedules.test.ts +44 -9
  413. package/src/tests/seed-workflows.test.ts +21 -9
  414. package/src/tests/send-task-output-schema.test.ts +9 -1
  415. package/src/tests/send-task-requested-by.test.ts +1 -0
  416. package/src/tests/send-task-slack-routing-guard.test.ts +2 -0
  417. package/src/tests/session-costs-codex-recompute.test.ts +1 -1
  418. package/src/tests/session-costs-golden.test.ts +2 -2
  419. package/src/tests/session-costs-model-key-normalize.test.ts +2 -2
  420. package/src/tests/session-costs-recompute-all-providers.test.ts +1 -1
  421. package/src/tests/session-tokens.test.ts +1 -1
  422. package/src/tests/shutdown-error-scrubbing.test.ts +1 -1
  423. package/src/tests/slack-blocks.test.ts +12 -1
  424. package/src/tests/slack-file-tools-attachments.test.ts +1 -1
  425. package/src/tests/slack-inbound-files.test.ts +222 -86
  426. package/src/tests/slack-reaction-config.test.ts +5 -4
  427. package/src/tests/slack-read-boundaries.test.ts +1 -1
  428. package/src/tests/slack-render-v2.test.ts +196 -4
  429. package/src/tests/slack-router.test.ts +17 -2
  430. package/src/tests/slack-steering.test.ts +19 -0
  431. package/src/tests/status.test.ts +19 -7
  432. package/src/tests/steering-core.test.ts +1 -1
  433. package/src/tests/steering-enabled.test.ts +6 -8
  434. package/src/tests/steering-transport.test.ts +4 -4
  435. package/src/tests/store-progress-output-guidance.test.ts +34 -0
  436. package/src/tests/swarm-tool-result-gate.test.ts +11 -11
  437. package/src/tests/swarm-x-tool.test.ts +7 -5
  438. package/src/tests/sync-ai-toolbox-skills.test.ts +19 -0
  439. package/src/tests/task-supersede-resume.test.ts +1 -1
  440. package/src/tests/task-tool-preload-mcp.test.ts +4 -1
  441. package/src/tests/task-tools-ctx.test.ts +2 -0
  442. package/src/tests/tasks-requested-by-filter.test.ts +2 -2
  443. package/src/tests/telemetry-init.test.ts +11 -3
  444. package/src/tests/tool-annotations.test.ts +1 -1
  445. package/src/tests/tool-output-agent-id.test.ts +1 -0
  446. package/src/tests/tool-registrar-no-input.test.ts +42 -1
  447. package/src/tests/tracker-fold.test.ts +7 -7
  448. package/src/tests/tracker-tools.test.ts +3 -3
  449. package/src/tests/ui-followup-lead-delegation.test.ts +1 -0
  450. package/src/tests/unified-oauth-migration.test.ts +5 -5
  451. package/src/tests/user-token-rest-auth.test.ts +1 -1
  452. package/src/tests/user-token-routes.test.ts +1 -1
  453. package/src/tests/whoami-route.test.ts +1 -1
  454. package/src/tests/workflow-agent-task.test.ts +15 -13
  455. package/src/tests/workflow-ctx.test.ts +1 -1
  456. package/src/tests/workflow-e2e.test.ts +1 -1
  457. package/src/tests/workflow-engine-v2.test.ts +3 -1
  458. package/src/tests/workflow-executors.test.ts +2 -2
  459. package/src/tests/workflow-http-v2.test.ts +8 -6
  460. package/src/tests/workflow-input-redaction.test.ts +7 -5
  461. package/src/tests/workflow-integration-io.test.ts +2 -2
  462. package/src/tests/workflow-llm.test.ts +5 -5
  463. package/src/tests/workflow-registry.test.ts +1 -1
  464. package/src/tests/workflow-swarm-script.test.ts +1 -1
  465. package/src/tests/workflow-triggers-v2.test.ts +23 -21
  466. package/src/tests/x-composio.test.ts +9 -7
  467. package/src/tests/x402-client.test.ts +1 -1
  468. package/src/tests/x402-config.test.ts +28 -41
  469. package/src/tools/defer-task.ts +156 -24
  470. package/src/tools/extension-common.ts +84 -0
  471. package/src/tools/extension-install.ts +65 -0
  472. package/src/tools/extension-list.ts +66 -0
  473. package/src/tools/script-common.ts +43 -18
  474. package/src/tools/send-task.ts +87 -105
  475. package/src/tools/skills/skill-publish.ts +1 -0
  476. package/src/tools/store-progress.ts +1 -1
  477. package/src/tools/task-action.ts +43 -22
  478. package/src/tools/task-tool-ctx.ts +2 -0
  479. package/src/tools/templates.ts +2 -1
  480. package/src/tools/tool-config.ts +3 -1
  481. package/src/tools/utils.ts +140 -16
  482. package/src/types.ts +144 -0
  483. package/src/utils/constants.ts +7 -1
  484. package/src/utils/credentials.test.ts +16 -16
  485. package/src/utils/multi-runtime.ts +2 -2
  486. package/src/utils/steering-enabled.ts +4 -4
  487. package/src/workflows/executors/agent-task.ts +18 -3
  488. package/templates/ai-toolbox.manifest.json +7 -1
  489. package/templates/extensions/README.md +17 -0
  490. package/templates/extensions/notify-on-complete/README.md +11 -0
  491. package/templates/extensions/notify-on-complete/hooks.ts +61 -0
  492. package/templates/extensions/notify-on-complete/manifest.json +7 -0
  493. package/templates/extensions/require-ticket-ref/README.md +11 -0
  494. package/templates/extensions/require-ticket-ref/hooks.ts +30 -0
  495. package/templates/extensions/require-ticket-ref/manifest.json +7 -0
  496. package/templates/extensions/require-verification-note/README.md +11 -0
  497. package/templates/extensions/require-verification-note/hooks.ts +32 -0
  498. package/templates/extensions/require-verification-note/manifest.json +7 -0
  499. package/templates/schedules/daily-hn-briefing/config.json +1 -1
  500. package/templates/schedules/gtm-weekly-review/config.json +1 -1
  501. package/templates/schedules/weekly-code-health-reports/config.json +1 -1
  502. package/templates/schedules/weekly-dependabot-triage/config.json +1 -1
  503. package/templates/schedules/weekly-dora-metrics/config.json +1 -1
  504. package/templates/schedules/weekly-harness-upgrade-check/config.json +1 -1
  505. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +1 -1
  506. package/templates/skills/scheduling/content.md +2 -0
  507. package/templates/skills/serply-search/SKILL.md +95 -0
  508. package/templates/skills/serply-search/config.json +13 -0
  509. package/templates/skills/serply-search/content.md +90 -0
  510. package/templates/skills/swarm-extensions/config.json +14 -0
  511. package/templates/skills/swarm-extensions/content.md +120 -0
  512. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  513. package/templates/skills/swarm-scripts/content.md +2 -2
  514. package/templates/workflows/claude-code-changelog-watch/config.json +1 -1
  515. package/templates/workflows/gsc-topic-miner/config.json +1 -1
  516. package/templates/workflows/linear-drain-loop/config.json +1 -1
  517. package/templates/workflows/llm-safe-release-context/config.json +1 -1
  518. package/templates/workflows/ralph-loop/config.json +1 -1
  519. package/dist/cli-7fehefdf.js +0 -562
  520. package/dist/cli-ayvy5kde.js +0 -164
  521. package/dist/cli-ddfw1mc4.js +0 -4322
  522. package/dist/cli-hzqmgcm2.js +0 -3066
  523. package/dist/cli-j4d4f6hv.js +0 -338
  524. package/dist/cli-qgm2pfpq.js +0 -6377
  525. package/dist/cli-sdzq01t9.js +0 -823
  526. package/dist/cli-xvkmedgn.js +0 -332
  527. package/dist/cli-y3pq4v9g.js +0 -664
  528. package/dist/{anthropic-messages-vs2rabzp.js → anthropic-messages-2y8nktar.js} +19 -19
  529. package/dist/{claude-managed-setup-f5pmsyrp.js → claude-managed-setup-48dwg9bv.js} +3 -3
  530. package/dist/{cli-81r683bv.js → cli-ch38q5z4.js} +0 -0
  531. package/dist/{cli-6664w7y4.js → cli-kj76p1ez.js} +8 -8
  532. package/dist/{cli-c71mg51n.js → cli-r2fpe7cx.js} +5 -5
  533. package/dist/{cli-4xz3pmrq.js → cli-y6x0gqtg.js} +16 -16
  534. package/dist/{index-aa3sa6s8.js → index-2wsdsnmm.js} +1 -1
  535. package/dist/{mistral-conversations-4ryk7h83.js → mistral-conversations-qhr84ph3.js} +9 -9
  536. package/dist/{openai-completions-0bqmsajw.js → openai-completions-32rdwmze.js} +19 -19
  537. package/dist/{openrouter-images-qx18fyp8.js → openrouter-images-9f9ek4rw.js} +3 -3
  538. package/dist/{pi-messages-79jvzz1d.js → pi-messages-b1mbr6gp.js} +4 -4
@@ -1,3066 +0,0 @@
1
- import {
2
- buildScriptCredentialBindingsWithFailures,
3
- getScriptApiConnectionDescriptors,
4
- getScriptById,
5
- getScriptMcpConnectionDescriptors,
6
- listScriptConnections,
7
- restoreScratchScriptLastUsedIfUnchanged,
8
- runScript,
9
- touchScratchScriptLastUsed
10
- } from "./cli-svbrpf5q.js";
11
- import {
12
- defaultAssetKey,
13
- getDbClient,
14
- getLeadAgent,
15
- init_db,
16
- init_key
17
- } from "./cli-qgd4sqz5.js";
18
- import {
19
- init_zod
20
- } from "./cli-sd0mxa3c.js";
21
- import {
22
- _enum,
23
- array,
24
- boolean,
25
- discriminatedUnion,
26
- literal,
27
- never,
28
- number,
29
- object,
30
- record,
31
- string,
32
- union,
33
- unknown
34
- } from "./cli-w5e1d1vv.js";
35
-
36
- // src/be/scripts/run-saved.ts
37
- function getSavedScriptOwnerAgentId(script) {
38
- return script.scopeId ?? script.createdByAgentId;
39
- }
40
- async function runSavedScriptAsAgent(args) {
41
- const runStartTouch = args.script.isScratch ? await touchScratchScriptLastUsed(args.script.id) : null;
42
- const credentials = await buildScriptCredentialBindingsWithFailures({
43
- agentId: args.agentId
44
- });
45
- const output = await runScript({
46
- source: args.script.source,
47
- args: args.input,
48
- fsMode: args.script.fsMode,
49
- agentId: args.agentId,
50
- egressSecrets: credentials.egressSecrets,
51
- failedBindings: credentials.failedBindings,
52
- apiConnections: getScriptApiConnectionDescriptors({ agentId: args.agentId }),
53
- mcpConnections: getScriptMcpConnectionDescriptors({ agentId: args.agentId })
54
- });
55
- if (output.exitCode === 0 && !output.error && !output.runtimeError) {
56
- await touchScratchScriptLastUsed(args.script.id);
57
- } else if (runStartTouch) {
58
- await restoreScratchScriptLastUsedIfUnchanged(args.script.id, args.script.updatedAt, runStartTouch);
59
- }
60
- return output;
61
- }
62
-
63
- // src/apps/definition.ts
64
- init_zod();
65
- // src/apps/catalog.generated.json
66
- var catalog_generated_default = {
67
- $comment: "GENERATED by apps/ui/scripts/generate-catalog-schema.ts — do not hand-edit. Regenerate with `cd apps/ui && bun run generate:catalog-schema`.",
68
- componentTypes: [
69
- "Stack",
70
- "Grid",
71
- "Split",
72
- "Divider",
73
- "Tabs",
74
- "Container",
75
- "Card",
76
- "Heading",
77
- "Text",
78
- "Markdown",
79
- "SearchInput",
80
- "Select",
81
- "Button",
82
- "Metric",
83
- "Alert",
84
- "Badge",
85
- "Table",
86
- "Form",
87
- "Drawer",
88
- "DetailList",
89
- "ElementRef",
90
- "ElementSlot"
91
- ],
92
- actionTypes: [
93
- "swarm.sdk",
94
- "swarm.call",
95
- "app.mutate",
96
- "app.refresh",
97
- "app.action",
98
- "app.navigate"
99
- ],
100
- components: {
101
- Stack: {
102
- description: "THE primary layout primitive — a flex column (default) or row of its children. `gap`/`padding` use the shared spacing scale (none|xs|sm|md|lg|xl, default gap md); `align` is the cross axis, `justify` the main axis, `wrap` lets a row reflow. For rows, set `collapseBelow` (sm|md|lg) so the row stacks vertically on narrow viewports — filter bars should always set it. Use this as the page root and for every section; `Container` is the legacy 2-prop alias kept for older pages.",
103
- slots: ["default"],
104
- props: {
105
- $schema: "http://json-schema.org/draft-07/schema#",
106
- type: "object",
107
- properties: {
108
- direction: {
109
- type: "string",
110
- enum: ["column", "row"]
111
- },
112
- gap: {
113
- type: "string",
114
- enum: ["none", "xs", "sm", "md", "lg", "xl"]
115
- },
116
- align: {
117
- type: "string",
118
- enum: ["start", "center", "end", "stretch"]
119
- },
120
- justify: {
121
- type: "string",
122
- enum: ["start", "center", "end", "between"]
123
- },
124
- wrap: {
125
- type: "boolean"
126
- },
127
- padding: {
128
- type: "string",
129
- enum: ["none", "xs", "sm", "md", "lg", "xl"]
130
- },
131
- collapseBelow: {
132
- type: "string",
133
- enum: ["sm", "md", "lg"]
134
- }
135
- },
136
- additionalProperties: false
137
- }
138
- },
139
- Grid: {
140
- description: "Responsive grid of equal-width cells, one per child. `columns` is either a single count (1-6) — responsive shorthand: 1 column on phones, 2 from `sm`, the count from `md` up — or a per-breakpoint object `{ base, sm, md, lg }` (default `{ base: 1, md: 2, lg: 3 }`). Prefer this over a wrapping Stack for card strips and metric tiles.",
141
- slots: ["default"],
142
- props: {
143
- $schema: "http://json-schema.org/draft-07/schema#",
144
- type: "object",
145
- properties: {
146
- columns: {
147
- anyOf: [
148
- {
149
- type: "integer",
150
- minimum: 1,
151
- maximum: 6
152
- },
153
- {
154
- type: "object",
155
- properties: {
156
- base: {
157
- type: "integer",
158
- minimum: 1,
159
- maximum: 6
160
- },
161
- sm: {
162
- type: "integer",
163
- minimum: 1,
164
- maximum: 6
165
- },
166
- md: {
167
- type: "integer",
168
- minimum: 1,
169
- maximum: 6
170
- },
171
- lg: {
172
- type: "integer",
173
- minimum: 1,
174
- maximum: 6
175
- }
176
- },
177
- additionalProperties: false
178
- }
179
- ]
180
- },
181
- gap: {
182
- type: "string",
183
- enum: ["none", "xs", "sm", "md", "lg", "xl"]
184
- },
185
- padding: {
186
- type: "string",
187
- enum: ["none", "xs", "sm", "md", "lg", "xl"]
188
- }
189
- },
190
- additionalProperties: false
191
- }
192
- },
193
- Split: {
194
- description: "Two-pane layout. POSITIONAL children: the FIRST child is the left pane, the SECOND is the right pane, and any further children stack inside the right pane. `ratio` sizes them (1-1|1-2|2-1|1-3|3-1, default 2-1 = wide left). Below `collapseBelow` (sm|md|lg, default md) the panes stack vertically; `reverse` only flips that stacked order (right pane first on narrow screens).",
195
- slots: ["default"],
196
- props: {
197
- $schema: "http://json-schema.org/draft-07/schema#",
198
- type: "object",
199
- properties: {
200
- ratio: {
201
- type: "string",
202
- enum: ["1-1", "1-2", "2-1", "1-3", "3-1"]
203
- },
204
- gap: {
205
- type: "string",
206
- enum: ["none", "xs", "sm", "md", "lg", "xl"]
207
- },
208
- collapseBelow: {
209
- type: "string",
210
- enum: ["sm", "md", "lg"]
211
- },
212
- reverse: {
213
- type: "boolean"
214
- },
215
- padding: {
216
- type: "string",
217
- enum: ["none", "xs", "sm", "md", "lg", "xl"]
218
- }
219
- },
220
- additionalProperties: false
221
- }
222
- },
223
- Divider: {
224
- description: 'Horizontal rule between sections, optionally with a centered `label` (e.g. `"Filters"`).',
225
- props: {
226
- $schema: "http://json-schema.org/draft-07/schema#",
227
- type: "object",
228
- properties: {
229
- label: {
230
- type: "string"
231
- }
232
- },
233
- additionalProperties: false
234
- }
235
- },
236
- Tabs: {
237
- description: 'Tabbed sections. POSITIONAL children: children[i] is the body of tabs[i], so declare exactly as many children as tabs, in the same order. Only the active tab is visible but every child stays mounted, so polled Tables in background tabs keep their data warm. The active key is mirrored into state at `/ui/<id>/tab`, so other components can bind `{ "$state": "/ui/<id>/tab" }`. `defaultTab` picks the initial key (defaults to the first tab).',
238
- slots: ["default"],
239
- props: {
240
- $schema: "http://json-schema.org/draft-07/schema#",
241
- type: "object",
242
- properties: {
243
- id: {
244
- type: "string"
245
- },
246
- tabs: {
247
- minItems: 1,
248
- type: "array",
249
- items: {
250
- type: "object",
251
- properties: {
252
- key: {
253
- type: "string"
254
- },
255
- label: {
256
- type: "string"
257
- }
258
- },
259
- required: ["key"],
260
- additionalProperties: false
261
- }
262
- },
263
- defaultTab: {
264
- type: "string"
265
- }
266
- },
267
- required: ["id", "tabs"],
268
- additionalProperties: false
269
- }
270
- },
271
- Container: {
272
- description: "Legacy flex container (row/column + gap). Prefer `Stack`, which supports the full spacing scale plus alignment, wrapping and padding.",
273
- slots: ["default"],
274
- props: {
275
- $schema: "http://json-schema.org/draft-07/schema#",
276
- type: "object",
277
- properties: {
278
- direction: {
279
- type: "string",
280
- enum: ["row", "column"]
281
- },
282
- gap: {
283
- type: "string",
284
- enum: ["none", "sm", "md", "lg"]
285
- }
286
- },
287
- additionalProperties: false
288
- }
289
- },
290
- Card: {
291
- description: "Bordered card with optional title and description.",
292
- slots: ["default"],
293
- props: {
294
- $schema: "http://json-schema.org/draft-07/schema#",
295
- type: "object",
296
- properties: {
297
- title: {
298
- type: "string"
299
- },
300
- description: {
301
- type: "string"
302
- }
303
- },
304
- additionalProperties: false
305
- }
306
- },
307
- Heading: {
308
- description: "Text heading (h1/h2/h3).",
309
- props: {
310
- $schema: "http://json-schema.org/draft-07/schema#",
311
- type: "object",
312
- properties: {
313
- text: {
314
- type: "string"
315
- },
316
- level: {
317
- type: "string",
318
- enum: ["h1", "h2", "h3"]
319
- }
320
- },
321
- required: ["text"],
322
- additionalProperties: false
323
- }
324
- },
325
- Text: {
326
- description: "Paragraph text.",
327
- props: {
328
- $schema: "http://json-schema.org/draft-07/schema#",
329
- type: "object",
330
- properties: {
331
- content: {
332
- type: "string"
333
- },
334
- tone: {
335
- type: "string",
336
- enum: ["default", "muted"]
337
- }
338
- },
339
- required: ["content"],
340
- additionalProperties: false
341
- }
342
- },
343
- Markdown: {
344
- description: "Rendered markdown block — headings, lists, links, tables, fenced code. Use it for prose (about/help sections, instructions); use `Text` for a single plain paragraph.",
345
- props: {
346
- $schema: "http://json-schema.org/draft-07/schema#",
347
- type: "object",
348
- properties: {
349
- content: {
350
- type: "string"
351
- }
352
- },
353
- required: ["content"],
354
- additionalProperties: false
355
- }
356
- },
357
- SearchInput: {
358
- description: 'Free-text search box. Writes the debounced (~200ms) query into state at `/ui/<id>/value`; bind it into a Table with `"search": { "$state": "/ui/<id>/value" }`. Purely client-side — it filters rows the page already polled, it does not re-run the query.',
359
- props: {
360
- $schema: "http://json-schema.org/draft-07/schema#",
361
- type: "object",
362
- properties: {
363
- id: {
364
- type: "string"
365
- },
366
- placeholder: {
367
- type: "string"
368
- },
369
- label: {
370
- type: "string"
371
- }
372
- },
373
- required: ["id"],
374
- additionalProperties: false
375
- }
376
- },
377
- Select: {
378
- description: 'Dropdown filter/picker. `options` are plain strings or `{ value, label }` objects. Writes the chosen value into state at `/ui/<id>/value` (clearing writes `null`); bind it into a Table with `"filters": { "<column>": { "$state": "/ui/<id>/value" } }`. `clearable` (default true) shows a clear button.',
379
- props: {
380
- $schema: "http://json-schema.org/draft-07/schema#",
381
- type: "object",
382
- properties: {
383
- id: {
384
- type: "string"
385
- },
386
- options: {
387
- minItems: 1,
388
- type: "array",
389
- items: {
390
- anyOf: [
391
- {
392
- type: "string"
393
- },
394
- {
395
- type: "object",
396
- properties: {
397
- value: {
398
- type: "string"
399
- },
400
- label: {
401
- type: "string"
402
- }
403
- },
404
- required: ["value"],
405
- additionalProperties: false
406
- }
407
- ]
408
- }
409
- },
410
- placeholder: {
411
- type: "string"
412
- },
413
- label: {
414
- type: "string"
415
- },
416
- clearable: {
417
- type: "boolean"
418
- }
419
- },
420
- required: ["id", "options"],
421
- additionalProperties: false
422
- }
423
- },
424
- Button: {
425
- description: 'Interactive button. Wire to actions via `on.press`. `variant: "destructive-outline"` is the canonical red-outlined destructive look (standalone deletes should use it, not solid red). `disabled` accepts a constant or a bound boolean; `busyWith: "<actionName>"` disables the button and shows a spinner while that custom action is running.',
426
- props: {
427
- $schema: "http://json-schema.org/draft-07/schema#",
428
- type: "object",
429
- properties: {
430
- label: {
431
- type: "string"
432
- },
433
- variant: {
434
- type: "string",
435
- enum: [
436
- "default",
437
- "secondary",
438
- "outline",
439
- "ghost",
440
- "destructive",
441
- "destructive-outline"
442
- ]
443
- },
444
- disabled: {
445
- type: "boolean"
446
- },
447
- busyWith: {
448
- type: "string"
449
- }
450
- },
451
- required: ["label"],
452
- additionalProperties: false
453
- }
454
- },
455
- Metric: {
456
- description: 'Single label/value tile for status pages. Bind `loading` to the backing query (`{ "$state": "/queries/<name>/loading" }`) to show a skeleton instead of a blank value while it loads.',
457
- props: {
458
- $schema: "http://json-schema.org/draft-07/schema#",
459
- type: "object",
460
- properties: {
461
- label: {
462
- type: "string"
463
- },
464
- value: {
465
- anyOf: [
466
- {
467
- type: "string"
468
- },
469
- {
470
- type: "number"
471
- }
472
- ]
473
- },
474
- loading: {
475
- type: "boolean"
476
- }
477
- },
478
- required: ["label", "value"],
479
- additionalProperties: false
480
- }
481
- },
482
- Alert: {
483
- description: "Status-toned inline alert.",
484
- props: {
485
- $schema: "http://json-schema.org/draft-07/schema#",
486
- type: "object",
487
- properties: {
488
- message: {
489
- type: "string"
490
- },
491
- tone: {
492
- type: "string",
493
- enum: ["info", "success", "warning", "error"]
494
- },
495
- title: {
496
- type: "string"
497
- }
498
- },
499
- required: ["message"],
500
- additionalProperties: false
501
- }
502
- },
503
- Badge: {
504
- description: "Status pill (uppercase tag chip) with a semantic tone.",
505
- props: {
506
- $schema: "http://json-schema.org/draft-07/schema#",
507
- type: "object",
508
- properties: {
509
- text: {
510
- anyOf: [
511
- {
512
- type: "string"
513
- },
514
- {
515
- type: "number"
516
- }
517
- ]
518
- },
519
- tone: {
520
- type: "string",
521
- enum: [
522
- "neutral",
523
- "success",
524
- "active",
525
- "error",
526
- "info",
527
- "pending",
528
- "warning",
529
- "paused"
530
- ]
531
- }
532
- },
533
- required: ["text"],
534
- additionalProperties: false
535
- }
536
- },
537
- Table: {
538
- description: 'Data table. Bind `data`/`loading`/`error` to a named query (`/queries/<name>/...`). `rowActions` chains receive `{ "$row": "<col>" }` (or `{ "$row": "" }` for the whole row) and `{ "$rowIndex": true }`. Destructive row actions always confirm via a dialog; override the copy with `confirm: { title, description, confirmLabel }`. `search` (case-insensitive substring across every listed column) and `filters` (per-column equality, `null`/`""` disables one) narrow the polled rows client-side — bind them to a `SearchInput` / `Select` via `/ui/<id>/value`, or pin a constant (e.g. `"filters": { "pinned": true }`). Columns take `pinned: "left"|"right"` to survive horizontal scroll; `pagination` pages large row sets (auto past 200 rows); `density: "compact"` tightens row height.',
539
- props: {
540
- $schema: "http://json-schema.org/draft-07/schema#",
541
- type: "object",
542
- properties: {
543
- data: {
544
- type: "array",
545
- items: {
546
- type: "object",
547
- propertyNames: {
548
- type: "string"
549
- },
550
- additionalProperties: {}
551
- }
552
- },
553
- columns: {
554
- type: "array",
555
- items: {
556
- type: "object",
557
- properties: {
558
- key: {
559
- type: "string"
560
- },
561
- label: {
562
- type: "string"
563
- },
564
- kind: {
565
- type: "string",
566
- enum: ["text", "string", "number", "boolean", "date", "badge", "enum"]
567
- },
568
- tones: {
569
- type: "object",
570
- propertyNames: {
571
- type: "string"
572
- },
573
- additionalProperties: {
574
- type: "string",
575
- enum: [
576
- "neutral",
577
- "success",
578
- "active",
579
- "error",
580
- "info",
581
- "pending",
582
- "warning",
583
- "paused"
584
- ]
585
- }
586
- },
587
- width: {
588
- type: "number"
589
- },
590
- pinned: {
591
- type: "string",
592
- enum: ["left", "right"]
593
- }
594
- },
595
- required: ["key"],
596
- additionalProperties: false
597
- }
598
- },
599
- rowActions: {
600
- type: "array",
601
- items: {
602
- type: "object",
603
- properties: {
604
- label: {
605
- type: "string"
606
- },
607
- variant: {
608
- type: "string",
609
- enum: [
610
- "default",
611
- "secondary",
612
- "outline",
613
- "ghost",
614
- "destructive",
615
- "destructive-outline"
616
- ]
617
- },
618
- confirm: {
619
- anyOf: [
620
- {
621
- type: "boolean"
622
- },
623
- {
624
- type: "object",
625
- properties: {
626
- title: {
627
- type: "string"
628
- },
629
- description: {
630
- type: "string"
631
- },
632
- confirmLabel: {
633
- type: "string"
634
- }
635
- },
636
- additionalProperties: false
637
- },
638
- {
639
- type: "string"
640
- }
641
- ]
642
- },
643
- actions: {
644
- type: "array",
645
- items: {
646
- type: "object",
647
- properties: {
648
- action: {
649
- type: "string"
650
- },
651
- params: {
652
- type: "object",
653
- propertyNames: {
654
- type: "string"
655
- },
656
- additionalProperties: {}
657
- }
658
- },
659
- required: ["action"],
660
- additionalProperties: false
661
- }
662
- }
663
- },
664
- required: ["label", "actions"],
665
- additionalProperties: false
666
- }
667
- },
668
- loading: {
669
- type: "boolean"
670
- },
671
- error: {
672
- anyOf: [
673
- {
674
- type: "string"
675
- },
676
- {
677
- type: "null"
678
- }
679
- ]
680
- },
681
- emptyMessage: {
682
- type: "string"
683
- },
684
- search: {
685
- type: "string"
686
- },
687
- filters: {
688
- type: "object",
689
- propertyNames: {
690
- type: "string"
691
- },
692
- additionalProperties: {
693
- anyOf: [
694
- {
695
- anyOf: [
696
- {
697
- type: "string"
698
- },
699
- {
700
- type: "number"
701
- },
702
- {
703
- type: "boolean"
704
- }
705
- ]
706
- },
707
- {
708
- type: "null"
709
- }
710
- ]
711
- }
712
- },
713
- pagination: {
714
- type: "boolean"
715
- },
716
- density: {
717
- type: "string",
718
- enum: ["comfortable", "compact"]
719
- }
720
- },
721
- required: ["columns"],
722
- additionalProperties: false
723
- }
724
- },
725
- Form: {
726
- description: 'Field form. Values live in state under `/forms/<id>/<field>`; `onSubmit` chains receive `$form` (all collected values) and `$form: "<field>"` for one. `app.mutate` failures in the chain surface inline under the fields (not the page banner), and the submit button shows a pending spinner while the chain runs.',
727
- props: {
728
- $schema: "http://json-schema.org/draft-07/schema#",
729
- type: "object",
730
- properties: {
731
- id: {
732
- type: "string"
733
- },
734
- title: {
735
- type: "string"
736
- },
737
- fields: {
738
- type: "array",
739
- items: {
740
- type: "object",
741
- properties: {
742
- name: {
743
- type: "string",
744
- pattern: "^[^$]"
745
- },
746
- label: {
747
- type: "string"
748
- },
749
- kind: {
750
- type: "string",
751
- enum: ["string", "text", "number", "boolean", "date", "enum"]
752
- },
753
- options: {
754
- type: "array",
755
- items: {
756
- type: "string"
757
- }
758
- },
759
- placeholder: {
760
- type: "string"
761
- },
762
- required: {
763
- type: "boolean"
764
- }
765
- },
766
- required: ["name"],
767
- additionalProperties: false
768
- }
769
- },
770
- submitLabel: {
771
- type: "string"
772
- },
773
- onSubmit: {
774
- type: "array",
775
- items: {
776
- type: "object",
777
- properties: {
778
- action: {
779
- type: "string"
780
- },
781
- params: {
782
- type: "object",
783
- propertyNames: {
784
- type: "string"
785
- },
786
- additionalProperties: {}
787
- }
788
- },
789
- required: ["action"],
790
- additionalProperties: false
791
- }
792
- }
793
- },
794
- required: ["id", "fields", "onSubmit"],
795
- additionalProperties: false
796
- }
797
- },
798
- Drawer: {
799
- description: 'Route-driven slide-in side panel (sheet). Open exactly when the route param named by `param` is set — open it with `app.navigate` (e.g. a rowAction setting `{ "panel": { "$row": "id" } }`), and the URL stays shareable: a deep link with the param renders the drawer open, and closing it clears the param. Children mount only while open (a Table inside does NOT stay warm when closed — unlike Tabs). `side` right (default) or left; `size` sm|md|lg|xl width.',
800
- slots: ["default"],
801
- props: {
802
- $schema: "http://json-schema.org/draft-07/schema#",
803
- type: "object",
804
- properties: {
805
- param: {
806
- type: "string"
807
- },
808
- title: {
809
- type: "string"
810
- },
811
- description: {
812
- type: "string"
813
- },
814
- side: {
815
- type: "string",
816
- enum: ["right", "left"]
817
- },
818
- size: {
819
- type: "string",
820
- enum: ["sm", "md", "lg", "xl"]
821
- }
822
- },
823
- required: ["param"],
824
- additionalProperties: false
825
- }
826
- },
827
- DetailList: {
828
- description: 'Read-only label/value detail view for ONE record. Bind `data` to a single row — usually `{ "$state": "/queries/<name>/data/0" }` with a `$param`-filtered query. `fields` pick and format properties with the same kinds as Table columns (badge tones, relative dates) plus `code` for raw/JSON values. Bind `loading` to the query (`/queries/<name>/loading`) for skeleton fields while it loads; `emptyMessage` shows when the record is genuinely missing.',
829
- props: {
830
- $schema: "http://json-schema.org/draft-07/schema#",
831
- type: "object",
832
- properties: {
833
- data: {
834
- anyOf: [
835
- {
836
- type: "object",
837
- propertyNames: {
838
- type: "string"
839
- },
840
- additionalProperties: {}
841
- },
842
- {
843
- type: "null"
844
- }
845
- ]
846
- },
847
- fields: {
848
- minItems: 1,
849
- type: "array",
850
- items: {
851
- type: "object",
852
- properties: {
853
- key: {
854
- type: "string"
855
- },
856
- label: {
857
- type: "string"
858
- },
859
- kind: {
860
- type: "string",
861
- enum: ["text", "string", "number", "boolean", "date", "badge", "enum", "code"]
862
- },
863
- tones: {
864
- type: "object",
865
- propertyNames: {
866
- type: "string"
867
- },
868
- additionalProperties: {
869
- type: "string",
870
- enum: [
871
- "neutral",
872
- "success",
873
- "active",
874
- "error",
875
- "info",
876
- "pending",
877
- "warning",
878
- "paused"
879
- ]
880
- }
881
- }
882
- },
883
- required: ["key"],
884
- additionalProperties: false
885
- }
886
- },
887
- emptyMessage: {
888
- type: "string"
889
- },
890
- columns: {
891
- anyOf: [
892
- {
893
- type: "number",
894
- const: 1
895
- },
896
- {
897
- type: "number",
898
- const: 2
899
- }
900
- ]
901
- },
902
- loading: {
903
- type: "boolean"
904
- }
905
- },
906
- required: ["fields"],
907
- additionalProperties: false
908
- }
909
- },
910
- ElementRef: {
911
- description: "Reference a reusable element from this app or an exported element from another app. Consumer children are inserted at the target's ElementSlot.",
912
- slots: ["default"],
913
- props: {
914
- $schema: "http://json-schema.org/draft-07/schema#",
915
- type: "object",
916
- properties: {
917
- app: {
918
- type: "string"
919
- },
920
- element: {
921
- type: "string"
922
- },
923
- props: {
924
- type: "object",
925
- propertyNames: {
926
- type: "string"
927
- },
928
- additionalProperties: {}
929
- },
930
- instanceKey: {
931
- type: "string"
932
- }
933
- },
934
- required: ["element"],
935
- additionalProperties: false
936
- }
937
- },
938
- ElementSlot: {
939
- description: "Leaf insertion point for children supplied by an ElementRef consumer.",
940
- props: {
941
- $schema: "http://json-schema.org/draft-07/schema#",
942
- type: "object",
943
- properties: {},
944
- additionalProperties: false
945
- }
946
- }
947
- },
948
- actions: {
949
- "swarm.sdk": {
950
- description: "Invoke a method on the in-SPA Swarm SDK with the viewer's bearer.",
951
- params: {
952
- $schema: "http://json-schema.org/draft-07/schema#",
953
- type: "object",
954
- properties: {
955
- sdk: {
956
- type: "string",
957
- enum: [
958
- "tasks.create",
959
- "tasks.list",
960
- "tasks.get",
961
- "tasks.storeProgress",
962
- "agents.list",
963
- "agents.get",
964
- "events.create",
965
- "events.list",
966
- "events.batch",
967
- "events.counts",
968
- "memory.search",
969
- "memory.list",
970
- "memory.get",
971
- "memory.rate",
972
- "repos.list",
973
- "repos.get",
974
- "repos.create",
975
- "repos.update",
976
- "repos.delete",
977
- "schedules.list",
978
- "schedules.get",
979
- "schedules.create",
980
- "schedules.update",
981
- "schedules.delete",
982
- "schedules.run",
983
- "approvalRequests.list",
984
- "approvalRequests.get",
985
- "approvalRequests.create",
986
- "approvalRequests.respond",
987
- "assets.list",
988
- "assets.audit",
989
- "assets.registerMapping",
990
- "assets.move"
991
- ]
992
- },
993
- args: {
994
- type: "object",
995
- propertyNames: {
996
- type: "string"
997
- },
998
- additionalProperties: {}
999
- }
1000
- },
1001
- required: ["sdk"],
1002
- additionalProperties: false
1003
- }
1004
- },
1005
- "swarm.call": {
1006
- description: "Raw HTTP call to a swarm `/api/*` endpoint with the viewer's bearer.",
1007
- params: {
1008
- $schema: "http://json-schema.org/draft-07/schema#",
1009
- type: "object",
1010
- properties: {
1011
- method: {
1012
- type: "string",
1013
- enum: ["GET", "POST", "PUT", "DELETE", "PATCH"]
1014
- },
1015
- endpoint: {
1016
- type: "string"
1017
- },
1018
- body: {
1019
- type: "object",
1020
- propertyNames: {
1021
- type: "string"
1022
- },
1023
- additionalProperties: {}
1024
- }
1025
- },
1026
- required: ["method", "endpoint"],
1027
- additionalProperties: false
1028
- }
1029
- },
1030
- "app.mutate": {
1031
- description: "Create / update / delete a swarm-app row, then refetch every named query on the same model.",
1032
- params: {
1033
- $schema: "http://json-schema.org/draft-07/schema#",
1034
- type: "object",
1035
- properties: {
1036
- model: {
1037
- type: "string"
1038
- },
1039
- op: {
1040
- type: "string",
1041
- enum: ["create", "update", "delete"]
1042
- },
1043
- rowId: {
1044
- type: "string"
1045
- },
1046
- values: {
1047
- type: "object",
1048
- propertyNames: {
1049
- type: "string"
1050
- },
1051
- additionalProperties: {}
1052
- },
1053
- formId: {
1054
- type: "string"
1055
- }
1056
- },
1057
- required: ["model", "op"],
1058
- additionalProperties: false
1059
- }
1060
- },
1061
- "app.refresh": {
1062
- description: "Refetch one named swarm-app query, or all of them when `query` is omitted.",
1063
- params: {
1064
- $schema: "http://json-schema.org/draft-07/schema#",
1065
- type: "object",
1066
- properties: {
1067
- query: {
1068
- type: "string"
1069
- }
1070
- },
1071
- additionalProperties: false
1072
- }
1073
- },
1074
- "app.action": {
1075
- description: "Invoke a named custom action from the app definition's `actions` map (script- or task-backed). Result lands in state at `/actions/<name>`.",
1076
- params: {
1077
- $schema: "http://json-schema.org/draft-07/schema#",
1078
- type: "object",
1079
- properties: {
1080
- name: {
1081
- type: "string"
1082
- },
1083
- input: {
1084
- type: "object",
1085
- propertyNames: {
1086
- type: "string"
1087
- },
1088
- additionalProperties: {}
1089
- }
1090
- },
1091
- required: ["name"],
1092
- additionalProperties: false
1093
- }
1094
- },
1095
- "app.navigate": {
1096
- description: "Navigate to another page of this app (`/apps/:id/p/<page>?<params>`). `params` replace the current route params wholesale and may use `$row` / `$form` sentinels. Pushes a history entry (browser Back returns). The current route is mirrored into state at `/route` as `{ page, params }`.",
1097
- params: {
1098
- $schema: "http://json-schema.org/draft-07/schema#",
1099
- type: "object",
1100
- properties: {
1101
- page: {
1102
- type: "string"
1103
- },
1104
- params: {
1105
- type: "object",
1106
- propertyNames: {
1107
- type: "string"
1108
- },
1109
- additionalProperties: {
1110
- anyOf: [
1111
- {
1112
- type: "string"
1113
- },
1114
- {
1115
- type: "number"
1116
- },
1117
- {
1118
- type: "boolean"
1119
- }
1120
- ]
1121
- }
1122
- }
1123
- },
1124
- required: ["page"],
1125
- additionalProperties: false
1126
- }
1127
- }
1128
- }
1129
- };
1130
-
1131
- // src/apps/page-validator.ts
1132
- var ELEMENT_KEYS = new Set([
1133
- "type",
1134
- "props",
1135
- "children",
1136
- "on",
1137
- "visible",
1138
- "repeat",
1139
- "watch"
1140
- ]);
1141
- var UI_STATE_COMPONENTS = new Set(["SearchInput", "Select", "Tabs"]);
1142
- var CONDITION_KEYS = new Set(["eq", "neq", "gt", "gte", "lt", "lte", "not"]);
1143
- function isPlainObject(value) {
1144
- if (typeof value !== "object" || value === null || Array.isArray(value))
1145
- return false;
1146
- const prototype = Object.getPrototypeOf(value);
1147
- return prototype === Object.prototype || prototype === null;
1148
- }
1149
- function isStateBinding(value) {
1150
- return isPlainObject(value) && Object.keys(value).length === 1 && typeof value.$state === "string";
1151
- }
1152
- function isStateCondition(value) {
1153
- return isPlainObject(value) && typeof value.$state === "string" && Object.keys(value).some((key) => CONDITION_KEYS.has(key));
1154
- }
1155
- function isParamBinding(value) {
1156
- return isPlainObject(value) && Object.keys(value).length === 1 && typeof value.$param === "string";
1157
- }
1158
- function isRepeatBinding(value) {
1159
- if (!isPlainObject(value))
1160
- return false;
1161
- const keys = Object.keys(value);
1162
- if (keys.length !== 1)
1163
- return false;
1164
- if (keys[0] === "$item")
1165
- return typeof value.$item === "string";
1166
- return keys[0] === "$index" && value.$index === true;
1167
- }
1168
- function collectRepeatBindingPaths(value, path, paths) {
1169
- if (isRepeatBinding(value)) {
1170
- paths.push(path);
1171
- return;
1172
- }
1173
- if (Array.isArray(value)) {
1174
- for (const [index, child] of value.entries()) {
1175
- collectRepeatBindingPaths(child, appendPath(path, index), paths);
1176
- }
1177
- return;
1178
- }
1179
- if (!isPlainObject(value))
1180
- return;
1181
- for (const [key, child] of Object.entries(value)) {
1182
- collectRepeatBindingPaths(child, appendPath(path, key), paths);
1183
- }
1184
- }
1185
- function isActionSentinel(value) {
1186
- if (!isPlainObject(value) || Object.keys(value).length !== 1)
1187
- return false;
1188
- if (typeof value.$row === "string")
1189
- return true;
1190
- if (value.$rowIndex === true)
1191
- return true;
1192
- return typeof value.$form === "string";
1193
- }
1194
- function appendPath(path, part) {
1195
- return path ? `${path}.${part}` : String(part);
1196
- }
1197
- function issue(path, message) {
1198
- return { path, message };
1199
- }
1200
- function dedupeIssues(issues) {
1201
- const seen = new Set;
1202
- return issues.filter(({ path, message }) => {
1203
- const key = `${path}\x00${message}`;
1204
- if (seen.has(key))
1205
- return false;
1206
- seen.add(key);
1207
- return true;
1208
- });
1209
- }
1210
- function typeMatches(value, type) {
1211
- switch (type) {
1212
- case "null":
1213
- return value === null;
1214
- case "object":
1215
- return isPlainObject(value);
1216
- case "array":
1217
- return Array.isArray(value);
1218
- case "string":
1219
- return typeof value === "string";
1220
- case "number":
1221
- return typeof value === "number" && Number.isFinite(value);
1222
- case "integer":
1223
- return typeof value === "number" && Number.isInteger(value);
1224
- case "boolean":
1225
- return typeof value === "boolean";
1226
- default:
1227
- return true;
1228
- }
1229
- }
1230
- function equalJsonValue(left, right) {
1231
- if (Object.is(left, right))
1232
- return true;
1233
- if (Array.isArray(left) && Array.isArray(right) || isPlainObject(left) && isPlainObject(right)) {
1234
- return JSON.stringify(left) === JSON.stringify(right);
1235
- }
1236
- return false;
1237
- }
1238
- function validateSchema(value, schema, path, allowActionSentinels, skipPath) {
1239
- if (isStateBinding(value)) {
1240
- return { issues: [], stateRefs: [{ path, value: value.$state }] };
1241
- }
1242
- if (isRepeatBinding(value))
1243
- return { issues: [], stateRefs: [] };
1244
- if (isActionSentinel(value)) {
1245
- return allowActionSentinels ? { issues: [], stateRefs: [] } : {
1246
- issues: [
1247
- issue(path, "$row, $rowIndex, and $form bindings are only allowed in action params")
1248
- ],
1249
- stateRefs: []
1250
- };
1251
- }
1252
- if (schema.anyOf) {
1253
- for (const candidate of schema.anyOf) {
1254
- const result2 = validateSchema(value, candidate, path, allowActionSentinels);
1255
- if (result2.issues.length === 0)
1256
- return result2;
1257
- }
1258
- return { issues: [issue(path, "must match one of the allowed schemas")], stateRefs: [] };
1259
- }
1260
- if (schema.const !== undefined && !equalJsonValue(value, schema.const)) {
1261
- return { issues: [issue(path, `must equal ${JSON.stringify(schema.const)}`)], stateRefs: [] };
1262
- }
1263
- if (schema.enum && !schema.enum.some((candidate) => equalJsonValue(value, candidate))) {
1264
- return {
1265
- issues: [issue(path, `must be one of ${schema.enum.map(String).join(", ")}`)],
1266
- stateRefs: []
1267
- };
1268
- }
1269
- const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
1270
- if (types.length > 0 && !types.some((type) => typeMatches(value, type))) {
1271
- return { issues: [issue(path, `must be ${types.join(" or ")}`)], stateRefs: [] };
1272
- }
1273
- const result = { issues: [], stateRefs: [] };
1274
- if (isPlainObject(value)) {
1275
- const properties = schema.properties ?? {};
1276
- for (const required of schema.required ?? []) {
1277
- if (!Object.hasOwn(value, required)) {
1278
- result.issues.push(issue(appendPath(path, required), "is required"));
1279
- }
1280
- }
1281
- for (const [key, child] of Object.entries(value)) {
1282
- const childPath = appendPath(path, key);
1283
- if (skipPath?.(childPath))
1284
- continue;
1285
- const propertySchema = properties[key];
1286
- if (propertySchema) {
1287
- const childAllowsActionSentinels = allowActionSentinels || key === "params" && Object.hasOwn(properties, "action") && (schema.required ?? []).includes("action");
1288
- const childResult = validateSchema(child, propertySchema, childPath, childAllowsActionSentinels, skipPath);
1289
- result.issues.push(...childResult.issues);
1290
- result.stateRefs.push(...childResult.stateRefs);
1291
- } else if (schema.additionalProperties === false) {
1292
- result.issues.push(issue(childPath, "unknown property"));
1293
- } else if (isPlainObject(schema.additionalProperties)) {
1294
- const childResult = validateSchema(child, schema.additionalProperties, childPath, allowActionSentinels, skipPath);
1295
- result.issues.push(...childResult.issues);
1296
- result.stateRefs.push(...childResult.stateRefs);
1297
- } else {
1298
- const childResult = validateSchema(child, {}, childPath, allowActionSentinels, skipPath);
1299
- result.issues.push(...childResult.issues);
1300
- result.stateRefs.push(...childResult.stateRefs);
1301
- }
1302
- }
1303
- } else if (Array.isArray(value)) {
1304
- for (const [index, child] of value.entries()) {
1305
- const childResult = validateSchema(child, schema.items ?? {}, appendPath(path, index), allowActionSentinels, skipPath);
1306
- result.issues.push(...childResult.issues);
1307
- result.stateRefs.push(...childResult.stateRefs);
1308
- }
1309
- }
1310
- return result;
1311
- }
1312
- function actionParams(step, path, definition, catalog, validateNavigation = false) {
1313
- const result = { issues: [], stateRefs: [] };
1314
- if (typeof step.action !== "string") {
1315
- result.issues.push(issue(appendPath(path, "action"), "must be a string"));
1316
- return result;
1317
- }
1318
- if (validateNavigation) {
1319
- if (step.action !== "app.navigate")
1320
- return result;
1321
- const paramsPath2 = appendPath(path, "params");
1322
- const params2 = step.params ?? {};
1323
- if (!isPlainObject(params2))
1324
- return result;
1325
- const targetPage = params2.page;
1326
- if (typeof targetPage !== "string") {
1327
- if (isActionSentinel(targetPage) || isStateBinding(targetPage)) {
1328
- result.issues.push(issue(appendPath(paramsPath2, "page"), "must be a literal page name"));
1329
- }
1330
- return result;
1331
- }
1332
- const target = Object.hasOwn(definition.pages, targetPage) ? definition.pages[targetPage] : undefined;
1333
- if (!target) {
1334
- result.issues.push(issue(appendPath(paramsPath2, "page"), `unknown page "${targetPage}"`));
1335
- return result;
1336
- }
1337
- const suppliedParams = isPlainObject(params2.params) && !isActionSentinel(params2.params) && !isStateBinding(params2.params) ? params2.params : {};
1338
- const declaredParams = target.params ?? {};
1339
- for (const name of Object.keys(suppliedParams)) {
1340
- if (!Object.hasOwn(declaredParams, name)) {
1341
- result.issues.push(issue(appendPath(appendPath(paramsPath2, "params"), name), `param "${name}" is not declared by target page "${targetPage}"`));
1342
- }
1343
- }
1344
- for (const [name, parameter] of Object.entries(declaredParams)) {
1345
- if (parameter.required === true && !Object.hasOwn(suppliedParams, name)) {
1346
- result.issues.push(issue(appendPath(appendPath(paramsPath2, "params"), name), `required param "${name}" is missing for target page "${targetPage}"`));
1347
- }
1348
- }
1349
- return result;
1350
- }
1351
- const actionDefinition = catalog.actions[step.action];
1352
- if (!catalog.actionTypes.includes(step.action) || !actionDefinition) {
1353
- result.issues.push(issue(appendPath(path, "action"), `unknown action type "${step.action}"`));
1354
- return result;
1355
- }
1356
- const paramsPath = appendPath(path, "params");
1357
- const params = step.params ?? {};
1358
- const schemaResult = validateSchema(params, actionDefinition.params, paramsPath, true);
1359
- result.issues.push(...schemaResult.issues);
1360
- result.stateRefs.push(...schemaResult.stateRefs);
1361
- if (!isPlainObject(params))
1362
- return result;
1363
- if (step.action === "app.mutate") {
1364
- const model = params.model;
1365
- if (typeof model !== "string" || !Object.hasOwn(definition.models, model)) {
1366
- result.issues.push(issue(appendPath(paramsPath, "model"), `unknown model "${String(model)}"`));
1367
- return result;
1368
- }
1369
- const op = params.op;
1370
- if (op !== "create" && op !== "update" && op !== "delete") {
1371
- result.issues.push(issue(appendPath(paramsPath, "op"), "must be create, update, or delete"));
1372
- }
1373
- if (op === "update" || op === "delete") {
1374
- const rowId = params.rowId;
1375
- const validRowId = typeof rowId === "string" || isPlainObject(rowId) && typeof rowId.$row === "string" && Object.keys(rowId).length === 1;
1376
- if (!validRowId) {
1377
- result.issues.push(issue(appendPath(paramsPath, "rowId"), "is required for update and delete and must be a string or $row binding"));
1378
- }
1379
- }
1380
- if (isPlainObject(params.values) && !isActionSentinel(params.values) && !isStateBinding(params.values)) {
1381
- for (const column of Object.keys(params.values)) {
1382
- const columnDefinition = definition.models[model].columns[column];
1383
- if (!columnDefinition || columnDefinition.hidden === true) {
1384
- result.issues.push(issue(appendPath(appendPath(paramsPath, "values"), column), `unknown or hidden column "${column}"`));
1385
- }
1386
- }
1387
- }
1388
- } else if (step.action === "app.refresh") {
1389
- if (typeof params.query === "string" && !Object.hasOwn(definition.queries ?? {}, params.query)) {
1390
- result.issues.push(issue(appendPath(paramsPath, "query"), `unknown query "${params.query}"`));
1391
- }
1392
- } else if (step.action === "app.action") {
1393
- if (typeof params.name !== "string" || !Object.hasOwn(definition.actions ?? {}, params.name)) {
1394
- result.issues.push(issue(appendPath(paramsPath, "name"), `unknown app action "${String(params.name)}"`));
1395
- }
1396
- }
1397
- return result;
1398
- }
1399
- function validateActionChain(chain, path, definition, catalog) {
1400
- const result = { issues: [], stateRefs: [] };
1401
- if (!Array.isArray(chain)) {
1402
- result.issues.push(issue(path, "must be an action array"));
1403
- return result;
1404
- }
1405
- for (const [index, step] of chain.entries()) {
1406
- const stepPath = appendPath(path, index);
1407
- if (!isPlainObject(step)) {
1408
- result.issues.push(issue(stepPath, "must be an action object"));
1409
- continue;
1410
- }
1411
- const stepResult = actionParams(step, stepPath, definition, catalog);
1412
- result.issues.push(...stepResult.issues);
1413
- result.stateRefs.push(...stepResult.stateRefs);
1414
- }
1415
- return result;
1416
- }
1417
- function validateActionMap(value, path, definition, catalog, allowSingle) {
1418
- const result = { issues: [], stateRefs: [] };
1419
- if (!isPlainObject(value)) {
1420
- result.issues.push(issue(path, "must be an object of action chains"));
1421
- return result;
1422
- }
1423
- for (const [event, chain] of Object.entries(value)) {
1424
- const eventPath = appendPath(path, event);
1425
- const normalized = allowSingle && isPlainObject(chain) ? [chain] : chain;
1426
- const chainResult = validateActionChain(normalized, eventPath, definition, catalog);
1427
- result.issues.push(...chainResult.issues);
1428
- result.stateRefs.push(...chainResult.stateRefs);
1429
- }
1430
- return result;
1431
- }
1432
- function validateStateRef(ref, definition, formIds, uiIds, pageParams) {
1433
- if (ref.value === "/route/page")
1434
- return null;
1435
- const routeParam = /^\/route\/params\/([^/]+)$/.exec(ref.value);
1436
- if (routeParam) {
1437
- const name2 = routeParam[1];
1438
- return Object.hasOwn(pageParams, name2) ? null : issue(ref.path, `state reference targets unknown route param "${name2}"`);
1439
- }
1440
- const userConfigMatch = /^\/user\/([^/]+)(\/.*)?$/.exec(ref.value);
1441
- if (userConfigMatch) {
1442
- const [, name2, suffix] = userConfigMatch;
1443
- if (suffix) {
1444
- return issue(ref.path, `userConfig state reference "${ref.value}" must target exactly /user/<field>; nested paths are not supported`);
1445
- }
1446
- if (Object.hasOwn(definition.userConfig ?? {}, name2))
1447
- return null;
1448
- const declared = Object.keys(definition.userConfig ?? {}).sort();
1449
- return issue(ref.path, `state reference targets unknown userConfig field "${name2}"; declared fields: ${declared.length > 0 ? declared.join(", ") : "none"}`);
1450
- }
1451
- const match = /^\/(queries|forms|actions|ui)\/([^/]+)(?:\/.*)?$/.exec(ref.value);
1452
- if (!match)
1453
- return issue(ref.path, `invalid state reference "${ref.value}"`);
1454
- const [, namespace, name] = match;
1455
- const exists = namespace === "queries" && Object.hasOwn(definition.queries ?? {}, name) || namespace === "forms" && formIds.has(name) || namespace === "actions" && Object.hasOwn(definition.actions ?? {}, name) || namespace === "ui" && uiIds.has(name);
1456
- const targetKind = namespace === "queries" ? "query" : namespace === "forms" ? "form" : namespace === "actions" ? "action" : "UI control";
1457
- if (!exists)
1458
- return issue(ref.path, `state reference targets unknown ${targetKind} "${name}"`);
1459
- if (namespace === "queries") {
1460
- const dataMatch = /^\/queries\/[^/]+\/data(?:\/(.*))?$/.exec(ref.value);
1461
- const segments = dataMatch?.[1]?.split("/").filter(Boolean) ?? [];
1462
- if (/^\d+$/.test(segments[0] ?? ""))
1463
- segments.shift();
1464
- const columnName = segments[0];
1465
- const query = definition.queries?.[name];
1466
- if (columnName && query && Object.hasOwn(definition.models, query.model)) {
1467
- return fieldBindingIssue(query.model, definition.models[query.model], columnName, ref.path);
1468
- }
1469
- }
1470
- return null;
1471
- }
1472
- var COMPARISON_KEYS = ["eq", "neq", "gt", "gte", "lt", "lte"];
1473
- function visibleConditionIssues(value, path) {
1474
- if (typeof value === "boolean")
1475
- return [];
1476
- if (!isPlainObject(value)) {
1477
- return [issue(path, "visible must be a boolean, a { $state } binding, or a condition object")];
1478
- }
1479
- for (const logical of ["$and", "$or"]) {
1480
- if (!Object.hasOwn(value, logical))
1481
- continue;
1482
- const children = value[logical];
1483
- if (!Array.isArray(children)) {
1484
- return [issue(appendPath(path, logical), "must be an array of conditions")];
1485
- }
1486
- return children.flatMap((child, index) => visibleConditionIssues(child, appendPath(appendPath(path, logical), index)));
1487
- }
1488
- if (typeof value.$state !== "string") {
1489
- return [
1490
- issue(path, 'a visible condition must bind "$state" (or combine conditions with "$and"/"$or") — a wrapper like {"not": {…}} is not evaluated by the renderer')
1491
- ];
1492
- }
1493
- const found = [];
1494
- const comparisons = COMPARISON_KEYS.filter((key) => value[key] !== undefined);
1495
- if (comparisons.length > 1) {
1496
- found.push(issue(path, `use exactly one comparison key per condition (found ${comparisons.join(", ")}); combine conditions with "$and"/"$or"`));
1497
- }
1498
- if (Object.hasOwn(value, "not") && value.not !== true) {
1499
- found.push(issue(appendPath(path, "not"), '"not" is a negation flag and must be exactly true (e.g. { "$state": "/queries/q/data/0/id", "not": true } shows the element when the value is absent/falsy)'));
1500
- }
1501
- return found;
1502
- }
1503
- function collectStateRefs(value, path, refs) {
1504
- if (isStateBinding(value) || isStateCondition(value)) {
1505
- refs.push({ path, value: value.$state });
1506
- }
1507
- if (Array.isArray(value)) {
1508
- for (const [index, child] of value.entries()) {
1509
- collectStateRefs(child, appendPath(path, index), refs);
1510
- }
1511
- return;
1512
- }
1513
- if (!isPlainObject(value))
1514
- return;
1515
- for (const [key, child] of Object.entries(value)) {
1516
- if (key !== "$state")
1517
- collectStateRefs(child, appendPath(path, key), refs);
1518
- }
1519
- }
1520
- function queryModelFromDataBinding(definition, value) {
1521
- if (!isStateBinding(value))
1522
- return null;
1523
- const match = /^\/queries\/([^/]+)\/data(?:\/\d+)?$/.exec(value.$state);
1524
- if (!match)
1525
- return null;
1526
- const queryName = match[1];
1527
- const query = Object.hasOwn(definition.queries ?? {}, queryName) ? definition.queries?.[queryName] : undefined;
1528
- if (!query || !Object.hasOwn(definition.models, query.model))
1529
- return null;
1530
- return { modelName: query.model, model: definition.models[query.model] };
1531
- }
1532
- function fieldBindingIssue(modelName, model, columnName, path, allowSystemField = true) {
1533
- if (columnName === "")
1534
- return null;
1535
- const column = Object.hasOwn(model.columns, columnName) ? model.columns[columnName] : undefined;
1536
- if (column && column.hidden !== true)
1537
- return null;
1538
- if (allowSystemField && Object.hasOwn(SYSTEM_COLUMN_KINDS, columnName))
1539
- return null;
1540
- return issue(path, `unknown or hidden column "${columnName}" on model "${modelName}"`);
1541
- }
1542
- function collectRowFieldIssues(value, path, modelName, model, issues) {
1543
- if (isPlainObject(value) && typeof value.$row === "string") {
1544
- const found = fieldBindingIssue(modelName, model, value.$row, appendPath(path, "$row"));
1545
- if (found)
1546
- issues.push(found);
1547
- return;
1548
- }
1549
- if (Array.isArray(value)) {
1550
- for (const [index, child] of value.entries()) {
1551
- collectRowFieldIssues(child, appendPath(path, index), modelName, model, issues);
1552
- }
1553
- return;
1554
- }
1555
- if (!isPlainObject(value))
1556
- return;
1557
- for (const [key, child] of Object.entries(value)) {
1558
- collectRowFieldIssues(child, appendPath(path, key), modelName, model, issues);
1559
- }
1560
- }
1561
- function literalMutateModel(definition, chain) {
1562
- if (!Array.isArray(chain))
1563
- return null;
1564
- const names = new Set;
1565
- for (const step of chain) {
1566
- if (isPlainObject(step) && step.action === "app.mutate" && isPlainObject(step.params) && typeof step.params.model === "string") {
1567
- names.add(step.params.model);
1568
- }
1569
- }
1570
- if (names.size !== 1)
1571
- return null;
1572
- const modelName = [...names][0];
1573
- return Object.hasOwn(definition.models, modelName) ? { modelName, model: definition.models[modelName] } : null;
1574
- }
1575
- function visitActionChain(chain, path, visit, allowSingle) {
1576
- const normalized = allowSingle && isPlainObject(chain) ? [chain] : chain;
1577
- if (!Array.isArray(normalized))
1578
- return;
1579
- for (const [index, step] of normalized.entries()) {
1580
- if (isPlainObject(step))
1581
- visit(step, appendPath(path, index));
1582
- }
1583
- }
1584
- function visitPageActionSteps(page, pagePath, visit) {
1585
- if (!isPlainObject(page.elements))
1586
- return;
1587
- for (const [elementId, rawElement] of Object.entries(page.elements)) {
1588
- if (!isPlainObject(rawElement))
1589
- continue;
1590
- const elementPath = `${pagePath}.elements.${elementId}`;
1591
- for (const key of ["on", "watch"]) {
1592
- const actionMap = rawElement[key];
1593
- if (!isPlainObject(actionMap))
1594
- continue;
1595
- for (const [event, chain] of Object.entries(actionMap)) {
1596
- visitActionChain(chain, `${elementPath}.${key}.${event}`, visit, true);
1597
- }
1598
- }
1599
- if (!isPlainObject(rawElement.props))
1600
- continue;
1601
- if (rawElement.type === "Table" && Array.isArray(rawElement.props.rowActions)) {
1602
- for (const [index, rowAction] of rawElement.props.rowActions.entries()) {
1603
- if (!isPlainObject(rowAction))
1604
- continue;
1605
- visitActionChain(rowAction.actions, `${elementPath}.props.rowActions.${index}.actions`, visit, false);
1606
- }
1607
- }
1608
- if (rawElement.type === "Form") {
1609
- visitActionChain(rawElement.props.onSubmit, `${elementPath}.props.onSubmit`, visit, false);
1610
- }
1611
- }
1612
- }
1613
- function crossPageDefinitionIssues(definition, catalog) {
1614
- const issues = [];
1615
- if (!Object.hasOwn(definition.pages, definition.defaultPage)) {
1616
- issues.push(issue("defaultPage", `default page "${definition.defaultPage}" not found`));
1617
- }
1618
- for (const [pageName, page] of Object.entries(definition.pages)) {
1619
- const pagePath = appendPath("pages", pageName);
1620
- const declaredParams = page.params ?? {};
1621
- visitPageActionSteps(page, pagePath, (step, path) => {
1622
- const result = actionParams(step, path, definition, catalog, true);
1623
- issues.push(...result.issues);
1624
- });
1625
- if (!isPlainObject(page.elements))
1626
- continue;
1627
- for (const [elementId, rawElement] of Object.entries(page.elements)) {
1628
- if (!isPlainObject(rawElement))
1629
- continue;
1630
- const elementPath = `${pagePath}.elements.${elementId}`;
1631
- if (rawElement.type === "Drawer" && isPlainObject(rawElement.props)) {
1632
- const paramPath = `${elementPath}.props.param`;
1633
- if (Object.hasOwn(rawElement.props, "param") && typeof rawElement.props.param !== "string") {
1634
- issues.push(issue(paramPath, "Drawer param must be a literal route param name (not a binding)"));
1635
- } else if (typeof rawElement.props.param === "string" && !Object.hasOwn(declaredParams, rawElement.props.param)) {
1636
- issues.push(issue(paramPath, `param "${rawElement.props.param}" is not declared on page "${pageName}"`));
1637
- }
1638
- }
1639
- }
1640
- const refs = [];
1641
- collectStateRefs(page, pagePath, refs);
1642
- for (const ref of refs) {
1643
- const queryMatch = /^\/queries\/([^/]+)(?:\/.*)?$/.exec(ref.value);
1644
- if (!queryMatch)
1645
- continue;
1646
- const queryName = queryMatch[1];
1647
- const query = definition.queries?.[queryName];
1648
- if (!query)
1649
- continue;
1650
- for (const value of Object.values(query.filter ?? {})) {
1651
- if (!isParamBinding(value) || Object.hasOwn(declaredParams, value.$param))
1652
- continue;
1653
- issues.push(issue(ref.path, `query "${queryName}" requires undeclared page param "${value.$param}"`));
1654
- }
1655
- }
1656
- }
1657
- return dedupeIssues(issues);
1658
- }
1659
- var MAX_ELEMENT_REF_DEPTH = 5;
1660
- var MAX_ELEMENT_REF_EXPANSION_WORK = 100;
1661
- var MAX_ELEMENT_REF_EXPANSION_ISSUES = 100;
1662
- function collectElementRefs(elements, elementsPath) {
1663
- const refs = [];
1664
- for (const [elementId, rawElement] of Object.entries(elements)) {
1665
- if (!isPlainObject(rawElement) || rawElement.type !== "ElementRef")
1666
- continue;
1667
- refs.push({ path: appendPath(elementsPath, elementId), node: rawElement });
1668
- }
1669
- return refs;
1670
- }
1671
- function reusableElementHasSlot(element) {
1672
- return Object.values(element.elements).some((node) => isPlainObject(node) && node.type === "ElementSlot");
1673
- }
1674
- function isElementPropBinding(value) {
1675
- return isStateBinding(value) || isRepeatBinding(value);
1676
- }
1677
- function elementPropAccepts(prop, value) {
1678
- if (isElementPropBinding(value))
1679
- return true;
1680
- if (prop.kind === "string")
1681
- return typeof value === "string";
1682
- if (prop.kind === "enum") {
1683
- return typeof value === "string" && Boolean(prop.enum?.includes(value));
1684
- }
1685
- if (prop.kind === "number")
1686
- return typeof value === "number" && Number.isFinite(value);
1687
- if (prop.kind === "boolean")
1688
- return typeof value === "boolean";
1689
- return typeof value === "string" && isIso8601Date(value);
1690
- }
1691
- async function elementReferenceIssues(definition, context) {
1692
- const issues = [];
1693
- const rootAppId = context.currentAppId ?? "$self";
1694
- const cleanTargetDepths = new Map;
1695
- let expansionWork = 0;
1696
- let expansionIssueCount = 0;
1697
- let expansionStopped = false;
1698
- const stopExpansion = (path) => {
1699
- if (expansionStopped)
1700
- return;
1701
- expansionStopped = true;
1702
- issues.push(issue(path, "element reference expansion exceeded budget — simplify the reference graph"));
1703
- };
1704
- const pushExpansionIssue = (found) => {
1705
- if (expansionStopped)
1706
- return;
1707
- if (expansionIssueCount >= MAX_ELEMENT_REF_EXPANSION_ISSUES) {
1708
- stopExpansion(found.path);
1709
- return;
1710
- }
1711
- expansionIssueCount += 1;
1712
- issues.push(found);
1713
- };
1714
- const consumeExpansionWork = (path) => {
1715
- if (expansionStopped)
1716
- return false;
1717
- if (expansionWork >= MAX_ELEMENT_REF_EXPANSION_WORK) {
1718
- stopExpansion(path);
1719
- return false;
1720
- }
1721
- expansionWork += 1;
1722
- return true;
1723
- };
1724
- const resolvedApps = new Map;
1725
- const resolveTarget = async (appId) => {
1726
- if (appId === rootAppId)
1727
- return { appId, definition };
1728
- if (resolvedApps.has(appId))
1729
- return resolvedApps.get(appId);
1730
- const resolved = await context.resolveApp?.(appId);
1731
- const target = resolved ? {
1732
- appId,
1733
- name: resolved.name,
1734
- definition: resolved.definition,
1735
- unparseable: resolved.definitionError !== undefined
1736
- } : null;
1737
- resolvedApps.set(appId, target);
1738
- return target;
1739
- };
1740
- const describeTargetApp = (target) => target.appId === rootAppId && !target.name ? "this app" : `app "${target.name ?? target.appId}"`;
1741
- const validateReference = async (ref, ownerDefinition, ownerAppId, depth, stack) => {
1742
- if (expansionStopped)
1743
- return false;
1744
- if (!isPlainObject(ref.node.props))
1745
- return false;
1746
- let referenceClean = true;
1747
- const elementName = ref.node.props.element;
1748
- if (typeof elementName !== "string") {
1749
- pushExpansionIssue(issue(appendPath(ref.path, "props.element"), "element and app must be literal strings — dynamic references are not supported"));
1750
- return false;
1751
- }
1752
- const explicitAppId = ref.node.props.app;
1753
- if (explicitAppId !== undefined && typeof explicitAppId !== "string") {
1754
- pushExpansionIssue(issue(appendPath(ref.path, "props.app"), "element and app must be literal strings — dynamic references are not supported"));
1755
- return false;
1756
- }
1757
- const targetAppId = explicitAppId ?? ownerAppId;
1758
- if (depth > MAX_ELEMENT_REF_DEPTH) {
1759
- pushExpansionIssue(issue(appendPath(ref.path, "props.element"), `element reference expansion exceeds the maximum depth of ${MAX_ELEMENT_REF_DEPTH}`));
1760
- return false;
1761
- }
1762
- const targetApp = targetAppId === ownerAppId ? { appId: ownerAppId, definition: ownerDefinition } : await resolveTarget(targetAppId);
1763
- if (!targetApp) {
1764
- if (context.skipExternalTargetResolution && targetAppId !== ownerAppId)
1765
- return true;
1766
- pushExpansionIssue(issue(appendPath(ref.path, "props.app"), `referenced app "${targetAppId}" not found`));
1767
- return false;
1768
- }
1769
- if (targetApp.unparseable) {
1770
- pushExpansionIssue(issue(appendPath(ref.path, "props.app"), `referenced app "${targetApp.name ?? targetAppId}" has an invalid definition and cannot supply elements`));
1771
- return false;
1772
- }
1773
- const target = targetApp.definition.elements?.[elementName];
1774
- if (!target) {
1775
- const targetLocation = targetAppId === rootAppId ? "" : ` in ${describeTargetApp(targetApp)}`;
1776
- pushExpansionIssue(issue(appendPath(ref.path, "props.element"), `element "${elementName}" not found${targetLocation}`));
1777
- return false;
1778
- }
1779
- if (targetAppId !== ownerAppId && target.export !== true) {
1780
- referenceClean = false;
1781
- pushExpansionIssue(issue(appendPath(ref.path, "props.element"), `element "${elementName}" in ${describeTargetApp(targetApp)} is private; set export: true before referencing it from another app`));
1782
- }
1783
- const suppliedProps = isPlainObject(ref.node.props.props) ? ref.node.props.props : {};
1784
- const declaredProps = target.props ?? {};
1785
- for (const propName of Object.keys(suppliedProps)) {
1786
- const declared = declaredProps[propName];
1787
- if (!declared) {
1788
- referenceClean = false;
1789
- pushExpansionIssue(issue(`${ref.path}.props.props.${propName}`, `prop "${propName}" is not declared by element "${elementName}"`));
1790
- } else if (!elementPropAccepts(declared, suppliedProps[propName])) {
1791
- referenceClean = false;
1792
- pushExpansionIssue(issue(`${ref.path}.props.props.${propName}`, `prop "${propName}" must be a ${declared.kind} value or a binding`));
1793
- }
1794
- }
1795
- for (const [propName, declared] of Object.entries(declaredProps)) {
1796
- if (declared.required === true && declared.default === undefined && !Object.hasOwn(suppliedProps, propName)) {
1797
- referenceClean = false;
1798
- pushExpansionIssue(issue(`${ref.path}.props.props.${propName}`, `required prop "${propName}" is missing for element "${elementName}"`));
1799
- }
1800
- }
1801
- if (Array.isArray(ref.node.children) && ref.node.children.length > 0 && !reusableElementHasSlot(target)) {
1802
- referenceClean = false;
1803
- pushExpansionIssue(issue(appendPath(ref.path, "children"), `element "${elementName}" has no ElementSlot and cannot accept children`));
1804
- }
1805
- const targetKey = `${targetAppId}\x00${elementName}`;
1806
- if (stack.has(targetKey)) {
1807
- const targetLocation = targetAppId === rootAppId ? "" : ` in ${describeTargetApp(targetApp)}`;
1808
- pushExpansionIssue(issue(appendPath(ref.path, "props.element"), `recursive element reference cycle reaches "${elementName}"${targetLocation}`));
1809
- return false;
1810
- }
1811
- const cleanAtDepth = cleanTargetDepths.get(targetKey);
1812
- if (cleanAtDepth !== undefined && depth <= cleanAtDepth)
1813
- return referenceClean;
1814
- if (!consumeExpansionWork(appendPath(ref.path, "props.element")))
1815
- return false;
1816
- const nextStack = new Set(stack).add(targetKey);
1817
- let targetClean = true;
1818
- for (const nested of collectElementRefs(target.elements, `${ref.path}.target.${elementName}.elements`)) {
1819
- if (!await validateReference(nested, targetApp.definition, targetAppId, depth + 1, nextStack)) {
1820
- targetClean = false;
1821
- }
1822
- if (expansionStopped)
1823
- break;
1824
- }
1825
- if (targetClean && !expansionStopped) {
1826
- cleanTargetDepths.set(targetKey, Math.max(cleanAtDepth ?? 0, depth));
1827
- }
1828
- return referenceClean && targetClean;
1829
- };
1830
- for (const [pageName, page] of Object.entries(definition.pages)) {
1831
- if (expansionStopped)
1832
- break;
1833
- for (const ref of collectElementRefs(page.elements, `pages.${pageName}.elements`)) {
1834
- await validateReference(ref, definition, rootAppId, 1, new Set);
1835
- if (expansionStopped)
1836
- break;
1837
- }
1838
- }
1839
- for (const [elementName, element] of Object.entries(definition.elements ?? {})) {
1840
- if (expansionStopped)
1841
- break;
1842
- const stack = new Set([`${rootAppId}\x00${elementName}`]);
1843
- for (const ref of collectElementRefs(element.elements, `elements.${elementName}.elements`)) {
1844
- await validateReference(ref, definition, rootAppId, 1, stack);
1845
- if (expansionStopped)
1846
- break;
1847
- }
1848
- }
1849
- return issues;
1850
- }
1851
- async function elementDefinitionIssues(definition, catalog, context = {}) {
1852
- const issues = [];
1853
- for (const [elementName, element] of Object.entries(definition.elements ?? {})) {
1854
- const elementPath = `elements.${elementName}`;
1855
- issues.push(...validatePage(definition, catalog, {
1856
- tree: element,
1857
- path: elementPath,
1858
- elementMode: element.mode,
1859
- elementProps: element.props
1860
- }));
1861
- const slots = Object.entries(element.elements).filter(([, node]) => isPlainObject(node) && node.type === "ElementSlot");
1862
- if (element.mode === "pure" && slots.length > 1) {
1863
- for (const [slotId] of slots.slice(1)) {
1864
- issues.push(issue(`${elementPath}.elements.${slotId}`, "pure elements may contain at most one ElementSlot"));
1865
- }
1866
- }
1867
- visitPageActionSteps(element, elementPath, (step, path) => {
1868
- const navigation = actionParams(step, path, definition, catalog, true);
1869
- issues.push(...navigation.issues);
1870
- if (element.mode === "pure") {
1871
- issues.push(issue(appendPath(path, "action"), "pure elements cannot invoke actions — use a bound element"));
1872
- } else if (element.export === true && step.action === "app.navigate") {
1873
- issues.push(issue(appendPath(path, "action"), "exported bound elements cannot use app.navigate; keep navigation in the consuming app or make the element private"));
1874
- }
1875
- });
1876
- }
1877
- for (const referenceIssue of await elementReferenceIssues(definition, context)) {
1878
- issues.push(referenceIssue);
1879
- }
1880
- return dedupeIssues(issues);
1881
- }
1882
- function validatePage(definition, catalog, target) {
1883
- const issues = [];
1884
- const stateRefs = [];
1885
- const formIds = new Set;
1886
- const uiIds = new Set;
1887
- const pageName = typeof target === "string" ? target : undefined;
1888
- const options = typeof target === "string" ? undefined : target;
1889
- const page = options?.tree ?? definition.pages[pageName];
1890
- const pagePath = options?.path ?? appendPath("pages", pageName);
1891
- const pageParams = pageName ? definition.pages[pageName]?.params ?? {} : {};
1892
- const elementsPath = appendPath(pagePath, "elements");
1893
- const root = page.root;
1894
- const elements = page.elements;
1895
- if (typeof root !== "string")
1896
- issues.push(issue(appendPath(pagePath, "root"), "must be a string"));
1897
- if (!isPlainObject(elements)) {
1898
- issues.push(issue(elementsPath, "must be a non-empty object"));
1899
- return issues;
1900
- }
1901
- const elementEntries = Object.entries(elements);
1902
- if (elementEntries.length === 0) {
1903
- issues.push(issue(elementsPath, "must be a non-empty object"));
1904
- return issues;
1905
- }
1906
- if (typeof root === "string" && !Object.hasOwn(elements, root)) {
1907
- issues.push(issue(appendPath(pagePath, "root"), `root element "${root}" not found`));
1908
- }
1909
- for (const [elementId, rawElement] of elementEntries) {
1910
- const elementPath = appendPath(elementsPath, elementId);
1911
- if (!isPlainObject(rawElement)) {
1912
- issues.push(issue(elementPath, "must be an element object"));
1913
- continue;
1914
- }
1915
- for (const key of Object.keys(rawElement)) {
1916
- if (!ELEMENT_KEYS.has(key))
1917
- issues.push(issue(appendPath(elementPath, key), "unknown element key"));
1918
- }
1919
- const type = rawElement.type;
1920
- const component = typeof type === "string" ? catalog.components[type] : undefined;
1921
- if (typeof type !== "string") {
1922
- issues.push(issue(appendPath(elementPath, "type"), "must be a string"));
1923
- } else if (!catalog.componentTypes.includes(type) || !component) {
1924
- issues.push(issue(appendPath(elementPath, "type"), `unknown component type "${type}"`));
1925
- }
1926
- if (type === "ElementSlot") {
1927
- if (options?.elementMode !== "pure") {
1928
- issues.push(issue(appendPath(elementPath, "type"), "ElementSlot is only allowed inside a pure reusable element"));
1929
- }
1930
- if (Object.hasOwn(rawElement, "children") && (!Array.isArray(rawElement.children) || rawElement.children.length > 0)) {
1931
- issues.push(issue(appendPath(elementPath, "children"), "ElementSlot must be a leaf"));
1932
- }
1933
- }
1934
- if (component) {
1935
- const propsPath = appendPath(elementPath, "props");
1936
- const actionChainPath = (path) => type === "Form" && path === `${propsPath}.onSubmit` || type === "Table" && /^.+\.rowActions\.\d+\.actions$/.test(path);
1937
- const propsResult = validateSchema(Object.hasOwn(rawElement, "props") ? rawElement.props : {}, component.props, propsPath, false, actionChainPath);
1938
- issues.push(...propsResult.issues);
1939
- stateRefs.push(...propsResult.stateRefs);
1940
- if (type === "Form" && isPlainObject(rawElement.props) && typeof rawElement.props.id === "string") {
1941
- formIds.add(rawElement.props.id);
1942
- }
1943
- if (typeof type === "string" && UI_STATE_COMPONENTS.has(type) && isPlainObject(rawElement.props) && typeof rawElement.props.id === "string") {
1944
- uiIds.add(rawElement.props.id);
1945
- }
1946
- } else if (!Object.hasOwn(rawElement, "props")) {
1947
- issues.push(issue(appendPath(elementPath, "props"), "is required"));
1948
- }
1949
- if (isPlainObject(rawElement.props)) {
1950
- if (typeof rawElement.props.busyWith === "string" && !Object.hasOwn(definition.actions ?? {}, rawElement.props.busyWith)) {
1951
- issues.push(issue(`${elementPath}.props.busyWith`, `unknown app action "${rawElement.props.busyWith}"`));
1952
- }
1953
- if (type === "Table") {
1954
- const queryModel = queryModelFromDataBinding(definition, rawElement.props.data);
1955
- if (queryModel) {
1956
- if (Array.isArray(rawElement.props.columns)) {
1957
- for (const [index, field] of rawElement.props.columns.entries()) {
1958
- if (!isPlainObject(field) || typeof field.key !== "string")
1959
- continue;
1960
- const found = fieldBindingIssue(queryModel.modelName, queryModel.model, field.key, `${elementPath}.props.columns.${index}.key`);
1961
- if (found)
1962
- issues.push(found);
1963
- }
1964
- }
1965
- if (isPlainObject(rawElement.props.filters) && !isStateBinding(rawElement.props.filters)) {
1966
- for (const columnName of Object.keys(rawElement.props.filters)) {
1967
- const found = fieldBindingIssue(queryModel.modelName, queryModel.model, columnName, `${elementPath}.props.filters.${columnName}`);
1968
- if (found)
1969
- issues.push(found);
1970
- }
1971
- }
1972
- collectRowFieldIssues(rawElement.props.rowActions, `${elementPath}.props.rowActions`, queryModel.modelName, queryModel.model, issues);
1973
- }
1974
- } else if (type === "DetailList") {
1975
- const queryModel = queryModelFromDataBinding(definition, rawElement.props.data);
1976
- if (queryModel && Array.isArray(rawElement.props.fields)) {
1977
- for (const [index, field] of rawElement.props.fields.entries()) {
1978
- if (!isPlainObject(field) || typeof field.key !== "string")
1979
- continue;
1980
- const found = fieldBindingIssue(queryModel.modelName, queryModel.model, field.key, `${elementPath}.props.fields.${index}.key`);
1981
- if (found)
1982
- issues.push(found);
1983
- }
1984
- }
1985
- } else if (type === "Form") {
1986
- if (Array.isArray(rawElement.props.fields)) {
1987
- for (const [index, field] of rawElement.props.fields.entries()) {
1988
- if (!isPlainObject(field) || typeof field.name !== "string")
1989
- continue;
1990
- if (field.name.startsWith("$")) {
1991
- issues.push(issue(`${elementPath}.props.fields.${index}.name`, "field names must not start with '$' (reserved form slots)"));
1992
- }
1993
- }
1994
- }
1995
- const mutateModel = literalMutateModel(definition, rawElement.props.onSubmit);
1996
- if (mutateModel && Array.isArray(rawElement.props.fields)) {
1997
- for (const [index, field] of rawElement.props.fields.entries()) {
1998
- if (!isPlainObject(field) || typeof field.name !== "string")
1999
- continue;
2000
- const found = fieldBindingIssue(mutateModel.modelName, mutateModel.model, field.name, `${elementPath}.props.fields.${index}.name`, false);
2001
- if (found)
2002
- issues.push(found);
2003
- }
2004
- }
2005
- }
2006
- }
2007
- if (Object.hasOwn(rawElement, "children") && type !== "ElementSlot") {
2008
- if (!Array.isArray(rawElement.children)) {
2009
- issues.push(issue(appendPath(elementPath, "children"), "must be an array of element ids"));
2010
- } else {
2011
- for (const [index, child] of rawElement.children.entries()) {
2012
- if (typeof child !== "string") {
2013
- issues.push(issue(appendPath(appendPath(elementPath, "children"), index), "must be a string"));
2014
- }
2015
- }
2016
- }
2017
- if (component && !component.slots) {
2018
- issues.push(issue(appendPath(elementPath, "children"), `component "${type}" does not accept children`));
2019
- }
2020
- }
2021
- if (Object.hasOwn(rawElement, "on")) {
2022
- const onResult = validateActionMap(rawElement.on, appendPath(elementPath, "on"), definition, catalog, true);
2023
- issues.push(...onResult.issues);
2024
- stateRefs.push(...onResult.stateRefs);
2025
- }
2026
- if (Object.hasOwn(rawElement, "watch")) {
2027
- const watchResult = validateActionMap(rawElement.watch, appendPath(elementPath, "watch"), definition, catalog, true);
2028
- issues.push(...watchResult.issues);
2029
- stateRefs.push(...watchResult.stateRefs);
2030
- }
2031
- for (const key of ["visible", "repeat"]) {
2032
- if (!Object.hasOwn(rawElement, key))
2033
- continue;
2034
- const bindingPath = appendPath(elementPath, key);
2035
- const bindingResult = validateSchema(rawElement[key], {}, bindingPath, false);
2036
- issues.push(...bindingResult.issues);
2037
- stateRefs.push(...bindingResult.stateRefs);
2038
- if (key === "visible") {
2039
- collectStateRefs(rawElement[key], bindingPath, stateRefs);
2040
- issues.push(...visibleConditionIssues(rawElement[key], bindingPath));
2041
- }
2042
- }
2043
- if (type === "Table" && isPlainObject(rawElement.props) && Array.isArray(rawElement.props.rowActions)) {
2044
- for (const [rowActionIndex, rowAction] of rawElement.props.rowActions.entries()) {
2045
- if (!isPlainObject(rowAction) || !Object.hasOwn(rowAction, "actions"))
2046
- continue;
2047
- const chainResult = validateActionChain(rowAction.actions, `${elementPath}.props.rowActions.${rowActionIndex}.actions`, definition, catalog);
2048
- issues.push(...chainResult.issues);
2049
- stateRefs.push(...chainResult.stateRefs);
2050
- }
2051
- }
2052
- if (type === "Form" && isPlainObject(rawElement.props) && Object.hasOwn(rawElement.props, "onSubmit")) {
2053
- const chainResult = validateActionChain(rawElement.props.onSubmit, `${elementPath}.props.onSubmit`, definition, catalog);
2054
- issues.push(...chainResult.issues);
2055
- stateRefs.push(...chainResult.stateRefs);
2056
- }
2057
- }
2058
- const parentByChild = new Map;
2059
- for (const [elementId, rawElement] of elementEntries) {
2060
- if (!isPlainObject(rawElement) || !Array.isArray(rawElement.children))
2061
- continue;
2062
- for (const [index, child] of rawElement.children.entries()) {
2063
- if (typeof child !== "string")
2064
- continue;
2065
- const childPath = `${elementsPath}.${elementId}.children.${index}`;
2066
- if (!Object.hasOwn(elements, child)) {
2067
- issues.push(issue(childPath, `child element "${child}" not found`));
2068
- continue;
2069
- }
2070
- const previousParent = parentByChild.get(child);
2071
- if (previousParent && previousParent !== elementId) {
2072
- issues.push(issue(childPath, `element "${child}" is already a child of "${previousParent}"`));
2073
- } else {
2074
- parentByChild.set(child, elementId);
2075
- }
2076
- }
2077
- }
2078
- if (options?.elementMode) {
2079
- for (const [elementId, rawElement] of elementEntries) {
2080
- if (!isPlainObject(rawElement))
2081
- continue;
2082
- const repeatBindingPaths = [];
2083
- collectRepeatBindingPaths(rawElement, `${elementsPath}.${elementId}`, repeatBindingPaths);
2084
- if (repeatBindingPaths.length === 0)
2085
- continue;
2086
- let repeatedScope = Object.hasOwn(rawElement, "repeat");
2087
- let parentId = parentByChild.get(elementId);
2088
- const visitedParents = new Set;
2089
- while (!repeatedScope && parentId && !visitedParents.has(parentId)) {
2090
- visitedParents.add(parentId);
2091
- const parent = elements[parentId];
2092
- repeatedScope = isPlainObject(parent) && Object.hasOwn(parent, "repeat");
2093
- parentId = parentByChild.get(parentId);
2094
- }
2095
- if (!repeatedScope) {
2096
- for (const bindingPath of repeatBindingPaths) {
2097
- issues.push(issue(bindingPath, "$item and $index bindings are only allowed inside a repeated element"));
2098
- }
2099
- }
2100
- }
2101
- }
2102
- const visited = new Set;
2103
- const visiting = new Set;
2104
- const visit = (elementId) => {
2105
- if (visited.has(elementId))
2106
- return;
2107
- visiting.add(elementId);
2108
- const rawElement = elements[elementId];
2109
- if (isPlainObject(rawElement) && Array.isArray(rawElement.children)) {
2110
- for (const [index, child] of rawElement.children.entries()) {
2111
- if (typeof child !== "string" || !Object.hasOwn(elements, child))
2112
- continue;
2113
- if (visiting.has(child)) {
2114
- issues.push(issue(`${elementsPath}.${elementId}.children.${index}`, `cycle references element "${child}"`));
2115
- continue;
2116
- }
2117
- visit(child);
2118
- }
2119
- }
2120
- visiting.delete(elementId);
2121
- visited.add(elementId);
2122
- };
2123
- for (const elementId of Object.keys(elements))
2124
- visit(elementId);
2125
- const reachable = new Set;
2126
- const markReachable = (elementId) => {
2127
- if (reachable.has(elementId) || !Object.hasOwn(elements, elementId))
2128
- return;
2129
- reachable.add(elementId);
2130
- const rawElement = elements[elementId];
2131
- if (!isPlainObject(rawElement) || !Array.isArray(rawElement.children))
2132
- return;
2133
- for (const child of rawElement.children) {
2134
- if (typeof child === "string")
2135
- markReachable(child);
2136
- }
2137
- };
2138
- if (typeof root === "string")
2139
- markReachable(root);
2140
- for (const elementId of Object.keys(elements)) {
2141
- if (!reachable.has(elementId)) {
2142
- issues.push(issue(`${elementsPath}.${elementId}`, "element is not reachable from root"));
2143
- }
2144
- }
2145
- for (const ref of stateRefs) {
2146
- const propMatch = /^\/props\/([^/]+)(?:\/.*)?$/.exec(ref.value);
2147
- const userConfigRef = ref.value === "/user" || ref.value.startsWith("/user/");
2148
- let stateIssue;
2149
- if (options?.elementMode && userConfigRef) {
2150
- stateIssue = issue(ref.path, `${options.elementMode} elements cannot read /user state; read userConfig via a prop or bind it at page level`);
2151
- } else if (options?.elementMode && propMatch) {
2152
- stateIssue = Object.hasOwn(options.elementProps ?? {}, propMatch[1]) ? null : issue(ref.path, `state reference targets unknown element prop "${propMatch[1]}"`);
2153
- } else if (options?.elementMode === "pure") {
2154
- stateIssue = issue(ref.path, `pure element state reference "${ref.value}" escapes /props/<declared>; use a declared prop or switch the element to bound mode`);
2155
- } else {
2156
- stateIssue = validateStateRef(ref, definition, formIds, uiIds, pageParams);
2157
- }
2158
- if (stateIssue)
2159
- issues.push(stateIssue);
2160
- }
2161
- return dedupeIssues(issues);
2162
- }
2163
-
2164
- // src/apps/sync-run-as.ts
2165
- init_db();
2166
- async function resolveSyncRunAs(script) {
2167
- return getSavedScriptOwnerAgentId(script) ?? (await getLeadAgent())?.id ?? "app-sync";
2168
- }
2169
-
2170
- // src/apps/definition.ts
2171
- var AppNameSchema = string().regex(/^[a-z][a-zA-Z0-9_]{0,39}$/, {
2172
- message: "must start with a lowercase letter and contain only letters, numbers, or underscores"
2173
- });
2174
- var ColumnKindSchema = _enum(["string", "number", "boolean", "date", "enum"]);
2175
- var ISO_8601_PREFIX = /^\d{4}-\d{2}-\d{2}(?:T.*)?$/;
2176
- function isIso8601Date(value) {
2177
- return ISO_8601_PREFIX.test(value) && !Number.isNaN(Date.parse(value));
2178
- }
2179
- var SourceTransformSchema = _enum(["slug", "lower", "upper", "cents", "date-parse"]);
2180
- var TRANSFORM_COLUMN_KIND = {
2181
- slug: "string",
2182
- lower: "string",
2183
- upper: "string",
2184
- cents: "number",
2185
- "date-parse": "date"
2186
- };
2187
- var ColumnSourceBindingSchema = object({
2188
- of: AppNameSchema,
2189
- field: string().min(1),
2190
- transform: SourceTransformSchema.optional()
2191
- }).strict();
2192
- var ColumnDefSchema = object({
2193
- kind: ColumnKindSchema,
2194
- required: boolean().optional(),
2195
- enum: array(string()).optional(),
2196
- index: boolean().optional(),
2197
- default: union([string(), number(), boolean()]).optional(),
2198
- hidden: boolean().optional(),
2199
- source: ColumnSourceBindingSchema.optional()
2200
- }).superRefine((column, ctx) => {
2201
- if (column.kind === "enum") {
2202
- if (!column.enum || column.enum.length === 0) {
2203
- ctx.addIssue({ code: "custom", path: ["enum"], message: "enum values are required" });
2204
- } else if (column.enum.some((value) => value.length === 0)) {
2205
- ctx.addIssue({ code: "custom", path: ["enum"], message: "enum values must be non-empty" });
2206
- } else if (new Set(column.enum).size !== column.enum.length) {
2207
- ctx.addIssue({ code: "custom", path: ["enum"], message: "enum values must be unique" });
2208
- }
2209
- } else if (column.enum !== undefined) {
2210
- ctx.addIssue({
2211
- code: "custom",
2212
- path: ["enum"],
2213
- message: "enum values are only allowed for enum columns"
2214
- });
2215
- }
2216
- if (column.default === undefined)
2217
- return;
2218
- const valid = column.kind === "string" && typeof column.default === "string" || column.kind === "number" && typeof column.default === "number" && Number.isFinite(column.default) || column.kind === "boolean" && typeof column.default === "boolean" || column.kind === "date" && typeof column.default === "string" && isIso8601Date(column.default) || column.kind === "enum" && typeof column.default === "string" && Boolean(column.enum?.includes(column.default));
2219
- if (!valid) {
2220
- ctx.addIssue({
2221
- code: "custom",
2222
- path: ["default"],
2223
- message: `default must be a valid ${column.kind} value`
2224
- });
2225
- }
2226
- });
2227
- var UserConfigFieldSchema = object({
2228
- kind: ColumnKindSchema,
2229
- default: union([string(), number(), boolean()]).optional(),
2230
- enum: array(string()).optional(),
2231
- label: string().optional(),
2232
- required: never().optional()
2233
- }).strict().superRefine((field, ctx) => {
2234
- if (field.kind === "enum") {
2235
- if (!field.enum || field.enum.length === 0) {
2236
- ctx.addIssue({ code: "custom", path: ["enum"], message: "enum values are required" });
2237
- } else if (field.enum.some((value) => value.length === 0)) {
2238
- ctx.addIssue({ code: "custom", path: ["enum"], message: "enum values must be non-empty" });
2239
- } else if (new Set(field.enum).size !== field.enum.length) {
2240
- ctx.addIssue({ code: "custom", path: ["enum"], message: "enum values must be unique" });
2241
- }
2242
- } else if (field.enum !== undefined) {
2243
- ctx.addIssue({
2244
- code: "custom",
2245
- path: ["enum"],
2246
- message: "enum values are only allowed for enum fields"
2247
- });
2248
- }
2249
- if (field.default === undefined)
2250
- return;
2251
- const valid = field.kind === "string" && typeof field.default === "string" || field.kind === "number" && typeof field.default === "number" && Number.isFinite(field.default) || field.kind === "boolean" && typeof field.default === "boolean" || field.kind === "date" && typeof field.default === "string" && isIso8601Date(field.default) || field.kind === "enum" && typeof field.default === "string" && Boolean(field.enum?.includes(field.default));
2252
- if (!valid) {
2253
- ctx.addIssue({
2254
- code: "custom",
2255
- path: ["default"],
2256
- message: `default must be a valid ${field.kind} value`
2257
- });
2258
- }
2259
- });
2260
- var UserConfigSchema = record(AppNameSchema, UserConfigFieldSchema);
2261
- var SourceDefSchema = discriminatedUnion("connector", [
2262
- object({
2263
- connector: literal("script"),
2264
- scriptId: string().uuid(),
2265
- joinKey: AppNameSchema,
2266
- args: record(string(), unknown()).optional(),
2267
- connection: string().min(1).optional()
2268
- }).strict(),
2269
- object({
2270
- connector: literal("swarm-tasks"),
2271
- joinKey: AppNameSchema,
2272
- config: record(string(), union([string(), number(), boolean()])).optional()
2273
- }).strict()
2274
- ]);
2275
- var ModelDefSchema = object({
2276
- columns: record(AppNameSchema, ColumnDefSchema),
2277
- sources: record(AppNameSchema, SourceDefSchema).optional()
2278
- }).superRefine((model, ctx) => {
2279
- const count = Object.keys(model.columns).length;
2280
- if (count < 1 || count > 40) {
2281
- ctx.addIssue({ code: "custom", path: ["columns"], message: "must define 1 to 40 columns" });
2282
- }
2283
- for (const name of Object.keys(model.columns)) {
2284
- if (Object.hasOwn(SYSTEM_COLUMN_KINDS, name)) {
2285
- ctx.addIssue({
2286
- code: "custom",
2287
- path: ["columns", name],
2288
- message: "reserved column name"
2289
- });
2290
- }
2291
- }
2292
- if (Object.keys(model.sources ?? {}).length > 4) {
2293
- ctx.addIssue({ code: "custom", path: ["sources"], message: "must define at most 4 sources" });
2294
- }
2295
- });
2296
- var AppQueryParamRefSchema = object({
2297
- $param: AppNameSchema
2298
- }).strict();
2299
- var SYSTEM_COLUMN_KINDS = {
2300
- id: "string",
2301
- createdAt: "date",
2302
- updatedAt: "date",
2303
- createdBy: "string",
2304
- updatedBy: "string",
2305
- source: "string",
2306
- syncedAt: "date",
2307
- stale: "boolean"
2308
- };
2309
- var SORTABLE_SYSTEM_DATE_COLUMNS = new Set(["createdAt", "updatedAt", "syncedAt"]);
2310
- var AppQueryDefSchema = object({
2311
- model: AppNameSchema,
2312
- filter: record(string(), union([string(), number(), boolean(), AppQueryParamRefSchema])).optional(),
2313
- sort: object({
2314
- column: string(),
2315
- dir: _enum(["asc", "desc"])
2316
- }).optional(),
2317
- limit: number().int().positive().max(1000).optional()
2318
- });
2319
- var AppActionDefSchema = discriminatedUnion("kind", [
2320
- object({
2321
- kind: literal("script"),
2322
- scriptId: string().uuid(),
2323
- args: record(string(), unknown()).optional()
2324
- }),
2325
- object({
2326
- kind: literal("task"),
2327
- prompt: string().min(1),
2328
- agentId: string().min(1).optional()
2329
- }),
2330
- object({
2331
- kind: literal("sync"),
2332
- model: AppNameSchema.optional(),
2333
- source: AppNameSchema.optional()
2334
- })
2335
- ]);
2336
- var AppPageParamSchema = object({
2337
- kind: _enum(["string", "number", "boolean"]).optional(),
2338
- required: boolean().optional()
2339
- }).strict();
2340
- var RESERVED_PAGE_PARAM_NAMES = new Set(["mode", "apiUrl", "apiKey", "email", "name"]);
2341
- var AppPageSchema = object({
2342
- root: string(),
2343
- elements: record(string(), unknown()),
2344
- title: string().optional(),
2345
- params: record(AppNameSchema, AppPageParamSchema).optional()
2346
- }).strict();
2347
- var ElementPropDefSchema = object({
2348
- kind: ColumnKindSchema,
2349
- required: boolean().optional(),
2350
- enum: array(string()).min(1).optional(),
2351
- default: union([string(), number(), boolean()]).optional()
2352
- }).strict().superRefine((prop, ctx) => {
2353
- if (prop.kind === "enum") {
2354
- if (!prop.enum) {
2355
- ctx.addIssue({ code: "custom", path: ["enum"], message: "enum values are required" });
2356
- }
2357
- } else if (prop.enum !== undefined) {
2358
- ctx.addIssue({
2359
- code: "custom",
2360
- path: ["enum"],
2361
- message: "enum values are only allowed for enum props"
2362
- });
2363
- }
2364
- if (prop.default === undefined)
2365
- return;
2366
- const valid = prop.kind === "string" && typeof prop.default === "string" || prop.kind === "number" && typeof prop.default === "number" && Number.isFinite(prop.default) || prop.kind === "boolean" && typeof prop.default === "boolean" || prop.kind === "date" && typeof prop.default === "string" && isIso8601Date(prop.default) || prop.kind === "enum" && typeof prop.default === "string" && Boolean(prop.enum?.includes(prop.default));
2367
- if (!valid) {
2368
- ctx.addIssue({
2369
- code: "custom",
2370
- path: ["default"],
2371
- message: `default must be a valid ${prop.kind} value`
2372
- });
2373
- }
2374
- });
2375
- var AppElementSchema = object({
2376
- mode: _enum(["pure", "bound"]),
2377
- export: boolean().optional(),
2378
- props: record(AppNameSchema, ElementPropDefSchema).optional(),
2379
- root: string(),
2380
- elements: record(string(), unknown())
2381
- }).strict().superRefine((element, ctx) => {
2382
- if (Object.keys(element.elements).length > 150) {
2383
- ctx.addIssue({
2384
- code: "custom",
2385
- path: ["elements"],
2386
- message: "must contain at most 150 nodes"
2387
- });
2388
- }
2389
- });
2390
- var AppElementsSchema = record(AppNameSchema, AppElementSchema);
2391
- var AppThemeIdSchema = string().regex(/^[a-z][a-z0-9-]{0,39}$/, {
2392
- message: "must be a lowercase slug (letters, digits, dashes)"
2393
- });
2394
- var AppDefinitionSchema = object({
2395
- models: record(AppNameSchema, ModelDefSchema),
2396
- queries: record(AppNameSchema, AppQueryDefSchema).optional(),
2397
- actions: record(AppNameSchema, AppActionDefSchema).optional(),
2398
- elements: AppElementsSchema.optional(),
2399
- userConfig: UserConfigSchema.optional(),
2400
- pages: record(AppNameSchema, AppPageSchema),
2401
- defaultPage: AppNameSchema,
2402
- theme: AppThemeIdSchema.optional()
2403
- }).superRefine((definition, ctx) => {
2404
- if (!Object.hasOwn(definition.pages, definition.defaultPage)) {
2405
- ctx.addIssue({
2406
- code: "custom",
2407
- path: ["defaultPage"],
2408
- message: `unknown page "${definition.defaultPage}"`
2409
- });
2410
- }
2411
- for (const [pageName, page] of Object.entries(definition.pages)) {
2412
- for (const paramName of Object.keys(page.params ?? {})) {
2413
- if (RESERVED_PAGE_PARAM_NAMES.has(paramName)) {
2414
- ctx.addIssue({
2415
- code: "custom",
2416
- path: ["pages", pageName, "params", paramName],
2417
- message: "reserved param name"
2418
- });
2419
- }
2420
- }
2421
- }
2422
- const modelCount = Object.keys(definition.models).length;
2423
- if (modelCount > 10) {
2424
- ctx.addIssue({ code: "custom", path: ["models"], message: "must define at most 10 models" });
2425
- }
2426
- const actionCount = Object.keys(definition.actions ?? {}).length;
2427
- if (actionCount > 20) {
2428
- ctx.addIssue({
2429
- code: "custom",
2430
- path: ["actions"],
2431
- message: "must define at most 20 actions"
2432
- });
2433
- }
2434
- const userConfigCount = Object.keys(definition.userConfig ?? {}).length;
2435
- if (userConfigCount > 20) {
2436
- ctx.addIssue({
2437
- code: "custom",
2438
- path: ["userConfig"],
2439
- message: "must define at most 20 fields"
2440
- });
2441
- }
2442
- const elementCount = Object.keys(definition.elements ?? {}).length;
2443
- if (elementCount > 20) {
2444
- ctx.addIssue({
2445
- code: "custom",
2446
- path: ["elements"],
2447
- message: "must define at most 20 reusable elements"
2448
- });
2449
- }
2450
- for (const [queryName, query] of Object.entries(definition.queries ?? {})) {
2451
- if (!Object.hasOwn(definition.models, query.model)) {
2452
- ctx.addIssue({
2453
- code: "custom",
2454
- path: ["queries", queryName, "model"],
2455
- message: `unknown model "${query.model}"`
2456
- });
2457
- continue;
2458
- }
2459
- const model = definition.models[query.model];
2460
- for (const [column, value] of Object.entries(query.filter ?? {})) {
2461
- const columnDefinition = Object.hasOwn(model.columns, column) ? model.columns[column] : Object.hasOwn(SYSTEM_COLUMN_KINDS, column) ? { kind: SYSTEM_COLUMN_KINDS[column] } : undefined;
2462
- if (!columnDefinition || "hidden" in columnDefinition && columnDefinition.hidden === true) {
2463
- ctx.addIssue({
2464
- code: "custom",
2465
- path: ["queries", queryName, "filter", column],
2466
- message: `unknown or hidden column "${column}"`
2467
- });
2468
- continue;
2469
- }
2470
- if (typeof value === "object")
2471
- continue;
2472
- const valid = columnDefinition.kind === "string" && typeof value === "string" || columnDefinition.kind === "number" && typeof value === "number" && Number.isFinite(value) || columnDefinition.kind === "boolean" && typeof value === "boolean" || columnDefinition.kind === "date" && typeof value === "string" && isIso8601Date(value) || columnDefinition.kind === "enum" && typeof value === "string" && Boolean(columnDefinition.enum?.includes(value));
2473
- if (!valid) {
2474
- ctx.addIssue({
2475
- code: "custom",
2476
- path: ["queries", queryName, "filter", column],
2477
- message: `filter must be a valid ${columnDefinition.kind} value`
2478
- });
2479
- }
2480
- }
2481
- const sortColumn = query.sort?.column;
2482
- if (sortColumn && !SORTABLE_SYSTEM_DATE_COLUMNS.has(sortColumn) && (!Object.hasOwn(model.columns, sortColumn) || model.columns[sortColumn].hidden === true)) {
2483
- ctx.addIssue({
2484
- code: "custom",
2485
- path: ["queries", queryName, "sort", "column"],
2486
- message: `unknown or hidden column "${sortColumn}"`
2487
- });
2488
- }
2489
- }
2490
- });
2491
- var APP_DEFINITION_TOP_LEVEL_KEYS = new Set([
2492
- "models",
2493
- "queries",
2494
- "actions",
2495
- "elements",
2496
- "userConfig",
2497
- "pages",
2498
- "defaultPage",
2499
- "schemaVersion",
2500
- "theme"
2501
- ]);
2502
- var TOP_LEVEL_KEY_SUGGESTIONS = {
2503
- element: "elements",
2504
- userconfig: "userConfig",
2505
- themes: "theme",
2506
- appearance: "theme",
2507
- styling: "theme"
2508
- };
2509
- function flattenIssue(issue2, prefix = []) {
2510
- const path = [...prefix, ...issue2.path];
2511
- if (issue2.code === "invalid_key" && issue2.issues.length > 0) {
2512
- return issue2.issues.flatMap((nestedIssue) => flattenIssue(nestedIssue, path));
2513
- }
2514
- return [{ path: path.join("."), message: issue2.message }];
2515
- }
2516
- function appDefinitionIssues(error) {
2517
- return error.issues.flatMap((issue2) => flattenIssue(issue2));
2518
- }
2519
- function collectScriptReferences(definition) {
2520
- const references = new Map;
2521
- const add = (scriptId, path) => {
2522
- if (typeof scriptId !== "string" || scriptId.length === 0)
2523
- return;
2524
- const paths = references.get(scriptId);
2525
- if (paths)
2526
- paths.push(path);
2527
- else
2528
- references.set(scriptId, [path]);
2529
- };
2530
- if (!isMergePatchObject(definition))
2531
- return references;
2532
- const actions = definition.actions;
2533
- if (isMergePatchObject(actions)) {
2534
- for (const [actionName, action] of Object.entries(actions)) {
2535
- if (isMergePatchObject(action))
2536
- add(action.scriptId, `actions.${actionName}`);
2537
- }
2538
- }
2539
- const models = definition.models;
2540
- if (isMergePatchObject(models)) {
2541
- for (const [modelName, model] of Object.entries(models)) {
2542
- if (!isMergePatchObject(model) || !isMergePatchObject(model.sources))
2543
- continue;
2544
- for (const [sourceName, source] of Object.entries(model.sources)) {
2545
- if (isMergePatchObject(source)) {
2546
- add(source.scriptId, `models.${modelName}.sources.${sourceName}`);
2547
- }
2548
- }
2549
- }
2550
- }
2551
- return references;
2552
- }
2553
- function foreignScriptForWriter(context, script) {
2554
- if (script.scope !== "agent")
2555
- return false;
2556
- if (context.writerIsUser === true)
2557
- return true;
2558
- if (!context.writerAgentId)
2559
- return false;
2560
- return getSavedScriptOwnerAgentId(script) !== context.writerAgentId;
2561
- }
2562
- function scriptRefKey(scriptId, args, connection) {
2563
- if (typeof scriptId !== "string" || scriptId.length === 0)
2564
- return null;
2565
- return JSON.stringify({ scriptId, args: args ?? null, connection: connection ?? null });
2566
- }
2567
- function collectScriptReferencePathMap(definition) {
2568
- const byPath = new Map;
2569
- if (!isMergePatchObject(definition))
2570
- return byPath;
2571
- const actions = definition.actions;
2572
- if (isMergePatchObject(actions)) {
2573
- for (const [actionName, action] of Object.entries(actions)) {
2574
- if (!isMergePatchObject(action))
2575
- continue;
2576
- const key = scriptRefKey(action.scriptId, action.args, undefined);
2577
- if (key)
2578
- byPath.set(`actions.${actionName}`, key);
2579
- }
2580
- }
2581
- const models = definition.models;
2582
- if (isMergePatchObject(models)) {
2583
- for (const [modelName, model] of Object.entries(models)) {
2584
- if (!isMergePatchObject(model) || !isMergePatchObject(model.sources))
2585
- continue;
2586
- for (const [sourceName, source] of Object.entries(model.sources)) {
2587
- if (!isMergePatchObject(source))
2588
- continue;
2589
- const key = scriptRefKey(source.scriptId, source.args, source.connection);
2590
- if (key)
2591
- byPath.set(`models.${modelName}.sources.${sourceName}`, key);
2592
- }
2593
- }
2594
- }
2595
- return byPath;
2596
- }
2597
- async function modelSourceIssues(modelName, model, context, grandfatheredScriptRefs) {
2598
- const issues = [];
2599
- const sources = model.sources ?? {};
2600
- const sourceNames = new Set(Object.keys(sources));
2601
- for (const [sourceName, source] of Object.entries(sources)) {
2602
- const path = `models.${modelName}.sources.${sourceName}`;
2603
- const joinColumn = Object.hasOwn(model.columns, source.joinKey) ? model.columns[source.joinKey] : undefined;
2604
- if (!joinColumn || joinColumn.hidden === true) {
2605
- issues.push({
2606
- path: `${path}.joinKey`,
2607
- message: `unknown or hidden column "${source.joinKey}"`
2608
- });
2609
- } else if (joinColumn.kind !== "string") {
2610
- issues.push({
2611
- path: `${path}.joinKey`,
2612
- message: `join key column "${source.joinKey}" must be a string column`
2613
- });
2614
- } else {
2615
- if (joinColumn.source) {
2616
- issues.push({
2617
- path: `${path}.joinKey`,
2618
- message: `join key column "${source.joinKey}" must not be bound to a source`
2619
- });
2620
- }
2621
- if (joinColumn.required === true) {
2622
- issues.push({
2623
- path: `${path}.joinKey`,
2624
- message: `join key column "${source.joinKey}" must not be required`
2625
- });
2626
- }
2627
- if (joinColumn.default !== undefined) {
2628
- issues.push({
2629
- path: `${path}.joinKey`,
2630
- message: `join key column "${source.joinKey}" must not declare a default`
2631
- });
2632
- }
2633
- }
2634
- if (source.connector !== "script")
2635
- continue;
2636
- const script = await getScriptById(source.scriptId);
2637
- if (!script) {
2638
- issues.push({ path: `${path}.scriptId`, message: `script "${source.scriptId}" not found` });
2639
- continue;
2640
- }
2641
- const runAs = await resolveSyncRunAs(script);
2642
- const grandfatheredRef = grandfatheredScriptRefs.get(path) === scriptRefKey(source.scriptId, source.args, source.connection);
2643
- if (foreignScriptForWriter(context, script) && !grandfatheredRef) {
2644
- issues.push({
2645
- path: `${path}.scriptId`,
2646
- message: `script "${source.scriptId}" is agent-scoped to another agent — reference a script you own or a global script`
2647
- });
2648
- } else if (script.scope !== "agent" && (context.writerIsUser === true || typeof context.writerAgentId === "string") && context.writerAgentId !== runAs && !grandfatheredRef) {
2649
- issues.push({
2650
- path: `${path}.scriptId`,
2651
- message: `script "${source.scriptId}" syncs with agent "${runAs}"'s credentials — only that agent or the operator may wire or alter this source`
2652
- });
2653
- }
2654
- if (source.connection !== undefined) {
2655
- const reachable = listScriptConnections({ agentId: runAs });
2656
- if (!reachable.some((connection) => connection.slug === source.connection)) {
2657
- issues.push({
2658
- path: `${path}.connection`,
2659
- message: `connection "${source.connection}" not found or disabled for the sync run-as identity`
2660
- });
2661
- }
2662
- }
2663
- }
2664
- for (const [columnName, column] of Object.entries(model.columns)) {
2665
- const path = `models.${modelName}.columns.${columnName}`;
2666
- if (!column.source) {
2667
- if (sourceNames.size > 0 && column.required === true && column.hidden !== true && column.default === undefined) {
2668
- issues.push({
2669
- path,
2670
- message: "required column on a model with sources must declare a default — sync-created rows cannot supply it"
2671
- });
2672
- }
2673
- continue;
2674
- }
2675
- if (!sourceNames.has(column.source.of)) {
2676
- issues.push({ path: `${path}.source.of`, message: `unknown source "${column.source.of}"` });
2677
- }
2678
- const transform = column.source.transform;
2679
- if (transform && TRANSFORM_COLUMN_KIND[transform] !== column.kind) {
2680
- issues.push({
2681
- path: `${path}.source.transform`,
2682
- message: `transform "${transform}" requires a ${TRANSFORM_COLUMN_KIND[transform]} column`
2683
- });
2684
- }
2685
- if (column.required === true) {
2686
- issues.push({
2687
- path: `${path}.required`,
2688
- message: "source-bound column must not be required"
2689
- });
2690
- }
2691
- if (column.default !== undefined) {
2692
- issues.push({
2693
- path: `${path}.default`,
2694
- message: "source-bound column must not declare a default"
2695
- });
2696
- }
2697
- }
2698
- return issues;
2699
- }
2700
- function syncActionIssues(name, action, models) {
2701
- if (action.model !== undefined && !Object.hasOwn(models, action.model)) {
2702
- return [{ path: `actions.${name}.model`, message: `unknown model "${action.model}"` }];
2703
- }
2704
- const candidates = action.model !== undefined ? [action.model] : Object.keys(models);
2705
- const pairs = candidates.flatMap((modelName) => Object.keys(models[modelName]?.sources ?? {}).filter((sourceName) => action.source === undefined || sourceName === action.source));
2706
- if (pairs.length > 0)
2707
- return [];
2708
- if (action.source !== undefined) {
2709
- return [
2710
- {
2711
- path: `actions.${name}.source`,
2712
- message: action.model !== undefined ? `unknown source "${action.source}" on model "${action.model}"` : `unknown source "${action.source}" — no model declares it`
2713
- }
2714
- ];
2715
- }
2716
- if (action.model !== undefined) {
2717
- return [
2718
- { path: `actions.${name}.model`, message: `model "${action.model}" declares no sources` }
2719
- ];
2720
- }
2721
- return [{ path: `actions.${name}`, message: "no model declares a source to sync" }];
2722
- }
2723
- async function parseAppDefinition(input, elementContext = {}) {
2724
- if (isMergePatchObject(input) && Object.hasOwn(input, "page")) {
2725
- return {
2726
- success: false,
2727
- issues: [
2728
- {
2729
- path: "page",
2730
- message: "legacy singular page is no longer supported — define pages plus defaultPage"
2731
- }
2732
- ]
2733
- };
2734
- }
2735
- if (isMergePatchObject(input)) {
2736
- const unknownKeys = Object.keys(input).filter((key) => !APP_DEFINITION_TOP_LEVEL_KEYS.has(key));
2737
- if (unknownKeys.length > 0) {
2738
- return {
2739
- success: false,
2740
- issues: unknownKeys.map((key) => ({
2741
- path: key,
2742
- message: `unknown top-level key "${key}"${TOP_LEVEL_KEY_SUGGESTIONS[key] ? ` — did you mean "${TOP_LEVEL_KEY_SUGGESTIONS[key]}"?` : ""}`
2743
- }))
2744
- };
2745
- }
2746
- }
2747
- const parsedInput = isMergePatchObject(input) ? { ...input } : input;
2748
- if (isMergePatchObject(parsedInput))
2749
- delete parsedInput.schemaVersion;
2750
- const parsed = AppDefinitionSchema.safeParse(parsedInput);
2751
- if (!parsed.success)
2752
- return { success: false, issues: appDefinitionIssues(parsed.error) };
2753
- const issues = [
2754
- ...Object.keys(parsed.data.pages).flatMap((pageName) => validatePage(parsed.data, catalog_generated_default, pageName)),
2755
- ...crossPageDefinitionIssues(parsed.data, catalog_generated_default),
2756
- ...await elementDefinitionIssues(parsed.data, catalog_generated_default, elementContext)
2757
- ];
2758
- const grandfatheredScriptRefs = collectScriptReferencePathMap(elementContext.existingDefinition);
2759
- for (const [modelName, model] of Object.entries(parsed.data.models)) {
2760
- issues.push(...await modelSourceIssues(modelName, model, elementContext, grandfatheredScriptRefs));
2761
- }
2762
- for (const [name, action] of Object.entries(parsed.data.actions ?? {})) {
2763
- if (action.kind === "sync") {
2764
- issues.push(...syncActionIssues(name, action, parsed.data.models));
2765
- continue;
2766
- }
2767
- if (action.kind !== "script")
2768
- continue;
2769
- const script = await getScriptById(action.scriptId);
2770
- if (!script) {
2771
- issues.push({
2772
- path: `actions.${name}.scriptId`,
2773
- message: `script "${action.scriptId}" not found`
2774
- });
2775
- continue;
2776
- }
2777
- if (foreignScriptForWriter(elementContext, script) && grandfatheredScriptRefs.get(`actions.${name}`) !== scriptRefKey(action.scriptId, action.args, undefined)) {
2778
- issues.push({
2779
- path: `actions.${name}.scriptId`,
2780
- message: `script "${action.scriptId}" is agent-scoped to another agent — reference a script you own or a global script`
2781
- });
2782
- }
2783
- }
2784
- if (issues.length > 0)
2785
- return { success: false, issues };
2786
- return { success: true, definition: parsed.data };
2787
- }
2788
- function isMergePatchObject(value) {
2789
- return typeof value === "object" && value !== null && !Array.isArray(value);
2790
- }
2791
- function defineMergePatchValue(target, key, value) {
2792
- Object.defineProperty(target, key, {
2793
- value,
2794
- configurable: true,
2795
- enumerable: true,
2796
- writable: true
2797
- });
2798
- }
2799
- var DANGEROUS_PATCH_KEYS = new Set(["__proto__", "constructor", "prototype"]);
2800
- function dangerousPatchKeyIssues(value, path = []) {
2801
- if (!isMergePatchObject(value))
2802
- return [];
2803
- const issues = [];
2804
- for (const [key, child] of Object.entries(value)) {
2805
- const childPath = [...path, key];
2806
- if (DANGEROUS_PATCH_KEYS.has(key)) {
2807
- issues.push({
2808
- path: childPath.join("."),
2809
- message: `unsafe merge patch key "${key}" is not allowed`
2810
- });
2811
- continue;
2812
- }
2813
- issues.push(...dangerousPatchKeyIssues(child, childPath));
2814
- }
2815
- return issues;
2816
- }
2817
- function definitionPatchIssues(stored, patch) {
2818
- if (!isMergePatchObject(patch))
2819
- return [];
2820
- const issues = dangerousPatchKeyIssues(patch);
2821
- if (Object.hasOwn(patch, "page")) {
2822
- issues.push({
2823
- path: "page",
2824
- message: "definitions are normalized to the pages map — patch pages.<name> instead"
2825
- });
2826
- }
2827
- if (isMergePatchObject(patch.pages)) {
2828
- const effectiveDefaultPage = typeof patch.defaultPage === "string" ? patch.defaultPage : stored.defaultPage;
2829
- if (patch.pages[effectiveDefaultPage] === null) {
2830
- issues.push({
2831
- path: `pages.${effectiveDefaultPage}`,
2832
- message: "cannot delete the default page"
2833
- });
2834
- }
2835
- }
2836
- if (isMergePatchObject(patch.elements)) {
2837
- for (const [elementName, elementPatch] of Object.entries(patch.elements)) {
2838
- if (!isMergePatchObject(elementPatch))
2839
- continue;
2840
- const replacesWholeElement = Object.keys(elementPatch).some((key) => key !== "elements");
2841
- if (!replacesWholeElement || !isMergePatchObject(elementPatch.elements))
2842
- continue;
2843
- for (const [nodeId, nodePatch] of Object.entries(elementPatch.elements)) {
2844
- if (nodePatch !== null)
2845
- continue;
2846
- issues.push({
2847
- path: `elements.${elementName}.elements.${nodeId}`,
2848
- message: "null node in a full element replace — to delete a node use elements.<name>.elements.<id> = null"
2849
- });
2850
- }
2851
- }
2852
- }
2853
- return issues;
2854
- }
2855
- function withoutSchemaVersion(patch) {
2856
- if (!isMergePatchObject(patch))
2857
- return patch;
2858
- const normalized = { ...patch };
2859
- delete normalized.schemaVersion;
2860
- return normalized;
2861
- }
2862
- function applyMergePatch(target, patch, path) {
2863
- if (!isMergePatchObject(patch))
2864
- return patch;
2865
- const result = isMergePatchObject(target) ? { ...target } : {};
2866
- const entriesAreAtomic = path.length === 1 && path[0] === "actions" || path.length === 1 && path[0] === "elements" || path.length === 1 && path[0] === "userConfig" || path.length === 3 && path[0] === "models" && path[2] === "columns" || path.length === 3 && path[0] === "models" && path[2] === "sources" || path.length === 3 && path[0] === "elements" && path[2] === "elements" || path.length === 3 && path[0] === "pages" && (path[2] === "elements" || path[2] === "params");
2867
- for (const [key, value] of Object.entries(patch)) {
2868
- if (value === null) {
2869
- delete result[key];
2870
- continue;
2871
- }
2872
- defineMergePatchValue(result, key, entriesAreAtomic && !(path.length === 1 && path[0] === "elements" && isMergePatchObject(value) && Object.keys(value).length === 1 && isMergePatchObject(value.elements)) ? value : applyMergePatch(result[key], value, [...path, key]));
2873
- }
2874
- return result;
2875
- }
2876
- function applyAppDefinitionPatch(stored, patch) {
2877
- const normalizedPatch = withoutSchemaVersion(patch);
2878
- const issues = definitionPatchIssues(stored, normalizedPatch);
2879
- if (issues.length > 0)
2880
- return { success: false, issues };
2881
- const merged = applyMergePatch(structuredClone(stored), normalizedPatch, []);
2882
- return { success: true, definition: structuredClone(merged) };
2883
- }
2884
-
2885
- // src/apps/store.ts
2886
- init_key();
2887
- init_db();
2888
-
2889
- // src/apps/format-upgrades.ts
2890
- var CURRENT_APP_SCHEMA_VERSION = 1;
2891
- function isRecord(value) {
2892
- return typeof value === "object" && value !== null && !Array.isArray(value);
2893
- }
2894
- function upgradeFrom0(raw) {
2895
- const upgraded = { ...raw };
2896
- if (isRecord(upgraded.page) && !Object.hasOwn(upgraded, "pages")) {
2897
- upgraded.pages = { main: upgraded.page };
2898
- upgraded.defaultPage = "main";
2899
- }
2900
- delete upgraded.page;
2901
- if (isRecord(upgraded.models)) {
2902
- upgraded.models = Object.fromEntries(Object.entries(upgraded.models).map(([modelName, model]) => {
2903
- if (!isRecord(model))
2904
- return [modelName, model];
2905
- const upgradedModel = { ...model };
2906
- delete upgradedModel.sources;
2907
- if (isRecord(upgradedModel.columns)) {
2908
- upgradedModel.columns = Object.fromEntries(Object.entries(upgradedModel.columns).map(([columnName, column]) => {
2909
- if (!isRecord(column))
2910
- return [columnName, column];
2911
- const upgradedColumn = { ...column };
2912
- delete upgradedColumn.source;
2913
- return [columnName, upgradedColumn];
2914
- }));
2915
- }
2916
- return [modelName, upgradedModel];
2917
- }));
2918
- }
2919
- upgraded.schemaVersion = 1;
2920
- return upgraded;
2921
- }
2922
- var APP_FORMAT_UPGRADES = [{ from: 0, to: 1, upgrade: upgradeFrom0 }];
2923
- function schemaVersionOf(raw) {
2924
- if (!isRecord(raw) || typeof raw.schemaVersion !== "number" || !Number.isInteger(raw.schemaVersion)) {
2925
- return 0;
2926
- }
2927
- return raw.schemaVersion;
2928
- }
2929
- function upgradeAppDefinition(raw) {
2930
- if (!isRecord(raw))
2931
- return raw;
2932
- let upgraded = raw;
2933
- let version = schemaVersionOf(upgraded);
2934
- while (version < CURRENT_APP_SCHEMA_VERSION) {
2935
- const next = APP_FORMAT_UPGRADES.find((upgrade) => upgrade.from === version);
2936
- if (!next)
2937
- break;
2938
- upgraded = next.upgrade(upgraded);
2939
- version = next.to;
2940
- }
2941
- return upgraded;
2942
- }
2943
- function stampAppDefinition(definition) {
2944
- return { ...definition, schemaVersion: CURRENT_APP_SCHEMA_VERSION };
2945
- }
2946
-
2947
- // src/apps/store.ts
2948
- function invalidJsonIssue(error) {
2949
- return {
2950
- path: "definition",
2951
- message: `invalid stored JSON${error instanceof Error ? `: ${error.message}` : ""}`
2952
- };
2953
- }
2954
- function decodeAppDefinition(raw) {
2955
- const upgraded = upgradeAppDefinition(raw);
2956
- const parsed = AppDefinitionSchema.safeParse(upgraded);
2957
- if (!parsed.success) {
2958
- return {
2959
- definition: raw,
2960
- definitionError: appDefinitionIssues(parsed.error)
2961
- };
2962
- }
2963
- return {
2964
- definition: {
2965
- ...parsed.data,
2966
- schemaVersion: CURRENT_APP_SCHEMA_VERSION
2967
- }
2968
- };
2969
- }
2970
- function decodeApp(row) {
2971
- let rawDefinition;
2972
- try {
2973
- rawDefinition = JSON.parse(row.definition);
2974
- } catch (error) {
2975
- return {
2976
- id: row.id,
2977
- name: row.name,
2978
- ...row.description === null ? {} : { description: row.description },
2979
- definition: row.definition,
2980
- definitionError: [invalidJsonIssue(error)],
2981
- createdAt: row.created_at,
2982
- updatedAt: row.updated_at
2983
- };
2984
- }
2985
- const decoded = decodeAppDefinition(rawDefinition);
2986
- return {
2987
- id: row.id,
2988
- name: row.name,
2989
- ...row.description === null ? {} : { description: row.description },
2990
- definition: decoded.definition,
2991
- ...decoded.definitionError ? { definitionError: decoded.definitionError } : {},
2992
- createdAt: row.created_at,
2993
- updatedAt: row.updated_at
2994
- };
2995
- }
2996
- function appDefinitionNeedsRepair(app) {
2997
- return app.definitionError !== undefined;
2998
- }
2999
- function encodeDefinition(definition) {
3000
- return JSON.stringify(stampAppDefinition(definition));
3001
- }
3002
- function nextTimestamp(previous) {
3003
- const now = Date.now();
3004
- const previousMs = previous ? Date.parse(previous) : Number.NEGATIVE_INFINITY;
3005
- return new Date(Math.max(now, previousMs + 1)).toISOString();
3006
- }
3007
- async function createApp(input) {
3008
- const id = input.id ?? crypto.randomUUID();
3009
- const now = nextTimestamp();
3010
- const row = await getDbClient().get(`INSERT INTO apps (id, name, description, definition, created_at, updated_at, "key")
3011
- VALUES (?, ?, ?, ?, ?, ?, ?)
3012
- RETURNING id, name, description, definition, created_at, updated_at`, [
3013
- id,
3014
- input.name,
3015
- input.description ?? null,
3016
- encodeDefinition(input.definition),
3017
- now,
3018
- now,
3019
- defaultAssetKey("app", id)
3020
- ]);
3021
- if (!row)
3022
- throw new Error("Failed to create app");
3023
- return decodeApp(row);
3024
- }
3025
- async function getApp(id) {
3026
- const row = await getDbClient().get(`SELECT id, name, description, definition, created_at, updated_at FROM apps WHERE id = ?`, [id]);
3027
- return row ? decodeApp(row) : null;
3028
- }
3029
- async function listApps() {
3030
- const rows = await getDbClient().query(`SELECT id, name, description, definition, created_at, updated_at
3031
- FROM apps ORDER BY created_at DESC, id`);
3032
- return rows.map((row) => ({
3033
- id: row.id,
3034
- name: row.name,
3035
- ...row.description === null ? {} : { description: row.description },
3036
- createdAt: row.created_at,
3037
- updatedAt: row.updated_at
3038
- }));
3039
- }
3040
- async function listAppRecords() {
3041
- const rows = await getDbClient().query(`SELECT id, name, description, definition, created_at, updated_at
3042
- FROM apps ORDER BY created_at ASC, id ASC`);
3043
- return rows.map(decodeApp);
3044
- }
3045
- async function updateApp(id, patch) {
3046
- const existing = await getApp(id);
3047
- if (!existing)
3048
- return null;
3049
- const updatedAt = nextTimestamp(existing.updatedAt);
3050
- const row = await getDbClient().get(`UPDATE apps
3051
- SET name = ?, description = ?, definition = ?, updated_at = ?
3052
- WHERE id = ?
3053
- RETURNING id, name, description, definition, created_at, updated_at`, [
3054
- patch.name ?? existing.name,
3055
- patch.description === undefined ? existing.description ?? null : patch.description,
3056
- encodeDefinition(patch.definition ?? existing.definition),
3057
- updatedAt,
3058
- id
3059
- ]);
3060
- return row ? decodeApp(row) : null;
3061
- }
3062
- async function deleteApp(id) {
3063
- return (await getDbClient().run("DELETE FROM apps WHERE id = ?", [id])).changes > 0;
3064
- }
3065
-
3066
- export { getSavedScriptOwnerAgentId, runSavedScriptAsAgent, resolveSyncRunAs, AppNameSchema, ColumnKindSchema, isIso8601Date, SYSTEM_COLUMN_KINDS, SORTABLE_SYSTEM_DATE_COLUMNS, AppElementsSchema, AppDefinitionSchema, collectScriptReferences, parseAppDefinition, applyAppDefinitionPatch, upgradeAppDefinition, decodeAppDefinition, appDefinitionNeedsRepair, createApp, getApp, listApps, listAppRecords, updateApp, deleteApp };