@engenty/wizards-runtime 0.7.11 → 0.9.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 (257) hide show
  1. package/README.md +5 -1
  2. package/apps/runtime/dist/agents/architect.js +2 -2
  3. package/apps/runtime/dist/agents/studio-assistant.js +1 -1
  4. package/apps/runtime/dist/agents/website.js +1 -1
  5. package/apps/runtime/dist/app.js +1 -1
  6. package/apps/runtime/dist/assistant/browser.js +1 -1
  7. package/apps/runtime/dist/authoring/guide.js +16 -4
  8. package/apps/runtime/dist/authoring/ops.js +1 -1
  9. package/apps/runtime/dist/cli/exec.js +4 -1
  10. package/apps/runtime/dist/cli/features.js +1 -1
  11. package/apps/runtime/dist/cli/validate.js +2 -2
  12. package/apps/runtime/dist/connectors/builtin.js +4 -2
  13. package/apps/runtime/dist/connectors/external.js +1 -1
  14. package/apps/runtime/dist/connectors/index.js +37 -6
  15. package/apps/runtime/dist/connectors/smtp.js +221 -0
  16. package/apps/runtime/dist/connectors/via-account.js +1 -1
  17. package/apps/runtime/dist/credits/estimate.js +1 -1
  18. package/apps/runtime/dist/db/migrations/tenant/0018_share_job.sql +18 -0
  19. package/apps/runtime/dist/db/migrations/tenant/meta/0018_snapshot.json +3460 -0
  20. package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
  21. package/apps/runtime/dist/db/schema.js +19 -0
  22. package/apps/runtime/dist/docs/dev/index.md +25 -13
  23. package/apps/runtime/dist/docs/dev/integration/index.md +1 -1
  24. package/apps/runtime/dist/docs/dev/integration/openui.md +1 -1
  25. package/apps/runtime/dist/docs/dev/integration/own-ui.md +2 -2
  26. package/apps/runtime/dist/docs/dev/meta.json +8 -2
  27. package/apps/runtime/dist/docs/dev/plugins/examples.md +2 -3
  28. package/apps/runtime/dist/docs/dev/plugins/studio.md +5 -6
  29. package/apps/runtime/dist/docs/dev/plugins/styling.md +10 -7
  30. package/apps/runtime/dist/docs/dev/wizard-format/agent-steps.md +112 -0
  31. package/apps/runtime/dist/docs/dev/wizard-format/branches.md +63 -0
  32. package/apps/runtime/dist/docs/dev/wizard-format/film-steps.md +47 -0
  33. package/apps/runtime/dist/docs/dev/wizard-format/generate-steps.md +57 -0
  34. package/apps/runtime/dist/docs/dev/wizard-format/index.md +188 -0
  35. package/apps/runtime/dist/docs/dev/wizard-format/lists-and-connections.md +65 -0
  36. package/apps/runtime/dist/docs/dev/wizard-format/meta.json +19 -0
  37. package/apps/runtime/dist/docs/dev/wizard-format/pages-and-fields.md +102 -0
  38. package/apps/runtime/dist/docs/dev/wizard-format/review-and-result.md +64 -0
  39. package/apps/runtime/dist/docs/dev/wizard-format/templates.md +57 -0
  40. package/apps/runtime/dist/docs/dev/wizard-format/validate.md +63 -0
  41. package/apps/runtime/dist/docs/dev/wizard-format/views-and-widgets.md +103 -0
  42. package/apps/runtime/dist/docs/dev/wizard-format/wizard-file.md +147 -0
  43. package/apps/runtime/dist/docs/user/build-a-wizard.md +2 -2
  44. package/apps/runtime/dist/docs/user/run.md +21 -4
  45. package/apps/runtime/dist/docs/user/space.md +14 -10
  46. package/apps/runtime/dist/docs/user/steps.md +26 -1
  47. package/apps/runtime/dist/docs/user/test-and-share.md +3 -2
  48. package/apps/runtime/dist/documents/parse.js +1 -1
  49. package/apps/runtime/dist/engine/decide.js +1 -1
  50. package/apps/runtime/dist/engine/events.js +1 -1
  51. package/apps/runtime/dist/engine/input.js +3 -3
  52. package/apps/runtime/dist/engine/prepare.js +1 -1
  53. package/apps/runtime/dist/engine/requirements.js +25 -10
  54. package/apps/runtime/dist/engine/runner.js +23 -14
  55. package/apps/runtime/dist/engine/steps.js +6 -6
  56. package/apps/runtime/dist/engine/surface.js +3 -3
  57. package/apps/runtime/dist/engine/template.js +4 -4
  58. package/apps/runtime/dist/env.js +5 -0
  59. package/apps/runtime/dist/features.js +1 -1
  60. package/apps/runtime/dist/film/project.js +2 -2
  61. package/apps/runtime/dist/film/step.js +2 -2
  62. package/apps/runtime/dist/harness/antigravity.js +2 -10
  63. package/apps/runtime/dist/harness/apple.js +1 -1
  64. package/apps/runtime/dist/harness/claude.js +3 -11
  65. package/apps/runtime/dist/harness/codex.js +6 -12
  66. package/apps/runtime/dist/harness/cursor.js +2 -10
  67. package/apps/runtime/dist/harness/env.js +23 -0
  68. package/apps/runtime/dist/harness/index.js +6 -3
  69. package/apps/runtime/dist/harness/model.js +1 -1
  70. package/apps/runtime/dist/harness/terminal.js +1 -1
  71. package/apps/runtime/dist/mcp/flow-app.js +1 -1
  72. package/apps/runtime/dist/media/conversation.js +1 -1
  73. package/apps/runtime/dist/media/transcribe.js +1 -1
  74. package/apps/runtime/dist/model-errors.js +1 -1
  75. package/apps/runtime/dist/models.js +3 -3
  76. package/apps/runtime/dist/plugin.js +1 -1
  77. package/apps/runtime/dist/plugins/discovery.js +1 -1
  78. package/apps/runtime/dist/plugins/loader.js +2 -2
  79. package/apps/runtime/dist/plugins/runs.js +2 -2
  80. package/apps/runtime/dist/render/downloads.js +2 -2
  81. package/apps/runtime/dist/render/shaped.js +1 -1
  82. package/apps/runtime/dist/routes/api.js +2 -2
  83. package/apps/runtime/dist/routes/lang.js +1 -1
  84. package/apps/runtime/dist/routes/marketplace.js +1 -1
  85. package/apps/runtime/dist/routes/runs.js +110 -10
  86. package/apps/runtime/dist/routes/studio.js +20 -8
  87. package/apps/runtime/dist/services/access.js +1 -1
  88. package/apps/runtime/dist/services/api.js +4 -4
  89. package/apps/runtime/dist/services/assistant-chats.js +2 -2
  90. package/apps/runtime/dist/services/brand.js +2 -2
  91. package/apps/runtime/dist/services/cloud.js +1 -1
  92. package/apps/runtime/dist/services/errors.js +1 -1
  93. package/apps/runtime/dist/services/files.js +1 -1
  94. package/apps/runtime/dist/services/knowledge-model.js +3 -3
  95. package/apps/runtime/dist/services/knowledge.js +2 -2
  96. package/apps/runtime/dist/services/marketplace.js +5 -5
  97. package/apps/runtime/dist/services/package-file.js +3 -3
  98. package/apps/runtime/dist/services/package.js +2 -2
  99. package/apps/runtime/dist/services/profile.js +1 -1
  100. package/apps/runtime/dist/services/project-files.js +2 -2
  101. package/apps/runtime/dist/services/project-index.js +1 -1
  102. package/apps/runtime/dist/services/projects.js +2 -2
  103. package/apps/runtime/dist/services/runners.js +3 -3
  104. package/apps/runtime/dist/services/runs.js +2 -2
  105. package/apps/runtime/dist/services/shares.js +1 -1
  106. package/apps/runtime/dist/services/site.js +2 -2
  107. package/apps/runtime/dist/services/space-categories.js +3 -3
  108. package/apps/runtime/dist/services/space-data.js +4 -4
  109. package/apps/runtime/dist/services/spaces.js +6 -6
  110. package/apps/runtime/dist/services/translations.js +5 -5
  111. package/apps/runtime/dist/services/widgets.js +1 -1
  112. package/apps/runtime/dist/services/wizards.js +3 -3
  113. package/apps/runtime/dist/sharing/agent.js +65 -0
  114. package/apps/runtime/dist/sharing/files.js +121 -0
  115. package/apps/runtime/dist/sharing/index.js +298 -0
  116. package/apps/runtime/dist/sharing/jobs.js +186 -0
  117. package/apps/runtime/dist/sharing/state.js +116 -0
  118. package/apps/runtime/dist/sharing/targets/browser.js +131 -0
  119. package/apps/runtime/dist/sharing/targets/connector.js +63 -0
  120. package/apps/runtime/dist/sharing/targets/email.js +98 -0
  121. package/apps/runtime/dist/sharing/targets/slack.js +54 -0
  122. package/apps/runtime/dist/sharing/targets/storage.js +164 -0
  123. package/apps/runtime/dist/sharing/types.js +13 -0
  124. package/apps/runtime/dist/store/index.js +3 -3
  125. package/apps/runtime/dist/store/shared-lists.js +1 -1
  126. package/apps/runtime/dist/texts.js +109 -1
  127. package/apps/runtime/dist/tools/browser.js +48 -0
  128. package/apps/runtime/dist/tools/connector.js +3 -2
  129. package/apps/runtime/dist/tools/index.js +1 -1
  130. package/apps/runtime/dist/tools/plugin.js +2 -2
  131. package/apps/runtime/dist/tools/project.js +1 -1
  132. package/apps/runtime/dist/tools/store.js +20 -3
  133. package/apps/runtime/dist/widgets/bundle.js +1 -1
  134. package/apps/runtime/dist/widgets/step.js +3 -3
  135. package/apps/web/dist/assets/AboutDialog-CB0EwGxP.js +1 -0
  136. package/apps/web/dist/assets/{ChangelogDialog-CZrd-MgC.js → ChangelogDialog-BQRBkiVD.js} +1 -1
  137. package/apps/web/dist/assets/ChatView-BLZkLlHi.js +10 -0
  138. package/apps/web/dist/assets/EditorPage-YawfYRAL.js +4 -0
  139. package/apps/web/dist/assets/{FlowPreview-oX_Emm7u.js → FlowPreview-B0xF8e3r.js} +1 -1
  140. package/apps/web/dist/assets/{Harness-DiB8CBdY.js → Harness-snRbALGd.js} +2 -2
  141. package/apps/web/dist/assets/NewWizardChannels-BGzp9z0m.js +1 -0
  142. package/apps/web/dist/assets/{NewWizardPage-C2Oh2sMM.js → NewWizardPage-cOHfwcnA.js} +2 -2
  143. package/apps/web/dist/assets/OnboardingPage-jB3ChKi3.js +1 -0
  144. package/apps/web/dist/assets/ProjectPage-hRCBquNb.js +1 -0
  145. package/apps/web/dist/assets/PublicRunner-D_ZkOyRV.js +1 -0
  146. package/apps/web/dist/assets/{ReadOnly-CD5YWvrW.js → ReadOnly-Db_odT6Y.js} +1 -1
  147. package/apps/web/dist/assets/RunDrawer-B6AQw90D.js +4 -0
  148. package/apps/web/dist/assets/RunnerView-DHUkvi_G.js +1 -0
  149. package/apps/web/dist/assets/SettingsPage-DleMaLz8.js +2 -0
  150. package/apps/web/dist/assets/SetupPage-DTvVJR1U.js +1 -0
  151. package/apps/web/dist/assets/{SharePage-B6u9kS12.js → SharePage-Bl-rfCji.js} +1 -1
  152. package/apps/web/dist/assets/changelog-BcW79fHm.js +1 -0
  153. package/apps/web/dist/assets/esm-Bji0ThaK.js +7 -0
  154. package/apps/web/dist/assets/index-CQKA-z2z.js +14 -0
  155. package/apps/web/dist/assets/index-Cnziz-H-.css +1 -0
  156. package/apps/web/dist/assets/oss-credits-BjwxLtU4.js +1 -0
  157. package/apps/web/dist/assets/{phone-BjmhIKuT.js → phone-DwAf8S3l.js} +1 -1
  158. package/apps/web/dist/assets/settings-menu-B1ND1GF3.js +1 -0
  159. package/apps/web/dist/assets/ui-Cdv3QUOd.js +137 -0
  160. package/apps/web/dist/assets/{upload-JX9BcMTq.js → upload-DCMZI6pp.js} +1 -1
  161. package/apps/web/dist/assets/{wrench-BSxnPs2_.js → wrench-BRYtnxti.js} +1 -1
  162. package/apps/web/dist/index.html +3 -3
  163. package/apps/web/dist/mcp/flow.html +53 -53
  164. package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/definition.d.ts +22 -0
  165. package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/definition.js +26 -0
  166. package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/run.d.ts +6 -0
  167. package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/run.js +2 -0
  168. package/node_modules/@engenty/wizards-shared/dist/sharing.d.ts +135 -0
  169. package/node_modules/@engenty/wizards-shared/dist/sharing.js +11 -0
  170. package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/store.d.ts +8 -0
  171. package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/package.json +1 -1
  172. package/package.json +4 -3
  173. package/plugin/.claude-plugin/plugin.json +1 -1
  174. package/apps/runtime/dist/docs/dev/wizard-format.md +0 -125
  175. package/apps/web/dist/assets/AboutDialog-CPRAlD2H.js +0 -1
  176. package/apps/web/dist/assets/ChatView-DVeERQdi.js +0 -10
  177. package/apps/web/dist/assets/EditorPage-DhUoKcss.js +0 -4
  178. package/apps/web/dist/assets/NewWizardChannels-Ba3XGpro.js +0 -1
  179. package/apps/web/dist/assets/OnboardingPage-D_4ShWrL.js +0 -1
  180. package/apps/web/dist/assets/ProjectPage-C58rq50b.js +0 -1
  181. package/apps/web/dist/assets/PublicRunner-CE0ohzyd.js +0 -1
  182. package/apps/web/dist/assets/RunDrawer-Cd8-qHV7.js +0 -4
  183. package/apps/web/dist/assets/RunnerView-BW1h4x63.js +0 -1
  184. package/apps/web/dist/assets/SettingsPage-ChYLv-MG.js +0 -1
  185. package/apps/web/dist/assets/SetupPage-hX7deF0g.js +0 -1
  186. package/apps/web/dist/assets/changelog-Dn9LT4MS.js +0 -1
  187. package/apps/web/dist/assets/esm-C-ZWeuhU.js +0 -7
  188. package/apps/web/dist/assets/index-4jH2mkp7.js +0 -14
  189. package/apps/web/dist/assets/index-BwW652E-.css +0 -1
  190. package/apps/web/dist/assets/oss-credits-Bc05Qv5H.js +0 -1
  191. package/apps/web/dist/assets/settings-menu-Dpkp3f56.js +0 -1
  192. package/apps/web/dist/assets/ui-DMmT-cSO.js +0 -137
  193. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/assistant.d.ts +0 -0
  194. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/assistant.js +0 -0
  195. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/changes.d.ts +0 -0
  196. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/changes.js +0 -0
  197. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/claude-plugin.d.ts +0 -0
  198. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/claude-plugin.js +0 -0
  199. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/connectors.d.ts +0 -0
  200. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/connectors.js +0 -0
  201. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/decision.d.ts +0 -0
  202. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/decision.js +0 -0
  203. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/engenty/data-tables/columns-format.d.ts +0 -0
  204. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/engenty/data-tables/columns-format.js +0 -0
  205. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/engenty/data-tables/columns-value.d.ts +0 -0
  206. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/engenty/data-tables/columns-value.js +0 -0
  207. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/engenty/data-tables/columns.d.ts +0 -0
  208. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/engenty/data-tables/columns.js +0 -0
  209. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/engenty/data-tables/index.d.ts +0 -0
  210. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/engenty/data-tables/index.js +0 -0
  211. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/engenty/search-index/catalog-lexical.d.ts +0 -0
  212. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/engenty/search-index/catalog-lexical.js +0 -0
  213. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/engenty/search-index/catalog-rank.d.ts +0 -0
  214. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/engenty/search-index/catalog-rank.js +0 -0
  215. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/feature-flags.d.ts +0 -0
  216. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/feature-flags.js +0 -0
  217. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/features.d.ts +0 -0
  218. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/features.js +0 -0
  219. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/fit.d.ts +0 -0
  220. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/fit.js +0 -0
  221. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/json-schema.d.ts +0 -0
  222. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/json-schema.js +0 -0
  223. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/knowledge.d.ts +0 -0
  224. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/knowledge.js +0 -0
  225. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/marketplace-entry.d.ts +0 -0
  226. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/marketplace-entry.js +0 -0
  227. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/marketplace-search.d.ts +0 -0
  228. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/marketplace-search.js +0 -0
  229. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/marketplace.d.ts +0 -0
  230. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/marketplace.js +0 -0
  231. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/merge.d.ts +0 -0
  232. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/merge.js +0 -0
  233. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/numbers.d.ts +0 -0
  234. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/numbers.js +0 -0
  235. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/projects.d.ts +0 -0
  236. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/projects.js +0 -0
  237. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/providers.d.ts +0 -0
  238. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/providers.js +0 -0
  239. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/runner-texts.d.ts +0 -0
  240. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/runner-texts.js +0 -0
  241. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/runners.d.ts +0 -0
  242. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/runners.js +0 -0
  243. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/shape.d.ts +0 -0
  244. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/shape.js +0 -0
  245. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/space-data.d.ts +0 -0
  246. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/space-data.js +0 -0
  247. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/store.js +0 -0
  248. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/surface.d.ts +0 -0
  249. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/surface.js +0 -0
  250. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/texts.d.ts +0 -0
  251. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/texts.js +0 -0
  252. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/translation.d.ts +0 -0
  253. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/translation.js +0 -0
  254. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/wizard-package.d.ts +0 -0
  255. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/wizard-package.js +0 -0
  256. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/workspace.d.ts +0 -0
  257. /package/node_modules/{@engenty-wizards/shared → @engenty/wizards-shared}/dist/workspace.js +0 -0
@@ -0,0 +1,298 @@
1
+ import { and, desc, eq } from "drizzle-orm";
2
+ import { listConnectors, resolveConnector } from "../connectors/external.js";
3
+ import { connectionViews, connectorOptions, registerConnectionSource, } from "../connectors/index.js";
4
+ import { db, schema } from "../db/client.js";
5
+ import { renderTemplate } from "../engine/template.js";
6
+ import { scopeOf } from "../store/index.js";
7
+ import { currentTenant } from "../tenants/tenant.js";
8
+ import { resultStepOf, resultText } from "./files.js";
9
+ import { shareJobs } from "./state.js";
10
+ import { browserTarget } from "./targets/browser.js";
11
+ import { canPublish, connectorTarget } from "./targets/connector.js";
12
+ import { emailTarget } from "./targets/email.js";
13
+ import { slackTarget } from "./targets/slack.js";
14
+ import { storageConnectors, storageTarget } from "./targets/storage.js";
15
+ import { sayIn } from "./types.js";
16
+ /**
17
+ * The share targets of this runtime. The fixed ones are listed here; every connector that
18
+ * stores files adds `storage:<connector>`, every connector that can write `connector:<connector>`.
19
+ * To add a target, write it in `./targets` and name it here.
20
+ */
21
+ const FIXED = [emailTarget, slackTarget, browserTarget];
22
+ /** A share target by its id; null when this project has none of that name. */
23
+ export async function shareTarget(projectId, id) {
24
+ const fixed = FIXED.find((t) => t.id === id);
25
+ if (fixed) {
26
+ return fixed;
27
+ }
28
+ const cut = id.indexOf(":");
29
+ const [kind, connectorId] = [id.slice(0, cut), id.slice(cut + 1)];
30
+ if (kind === "storage") {
31
+ const connector = storageConnectors().find((c) => c.id === connectorId);
32
+ return connector ? storageTarget(connector) : null;
33
+ }
34
+ if (kind === "connector") {
35
+ const found = await resolveConnector(projectId, connectorId);
36
+ return found && canPublish(found.connector)
37
+ ? connectorTarget(found.connector, found.imported)
38
+ : null;
39
+ }
40
+ return null;
41
+ }
42
+ /** Every target of a project, in the order a result shows them. */
43
+ async function projectTargets(projectId) {
44
+ const imported = (await listConnectors(projectId)).filter((c) => c.sourceKind !== "builtin");
45
+ const through = await Promise.all(imported.map((c) => shareTarget(projectId, `connector:${c.id}`)));
46
+ return [
47
+ emailTarget,
48
+ ...storageConnectors().flatMap((c) => storageTarget(c) ?? []),
49
+ slackTarget,
50
+ ...through.filter((t) => Boolean(t)),
51
+ browserTarget,
52
+ ];
53
+ }
54
+ /**
55
+ * Every target a wizard of the project can name on its result, for the editor and for whoever
56
+ * authors a wizard: the default ones, and one for each built-in connector that can write.
57
+ */
58
+ export async function projectShareTargets(projectId, lang) {
59
+ const targets = await projectTargets(projectId);
60
+ const builtin = (await listConnectors(projectId)).filter((c) => c.sourceKind === "builtin");
61
+ for (const connector of builtin) {
62
+ const target = await shareTarget(projectId, `connector:${connector.id}`);
63
+ if (target) {
64
+ targets.push(target);
65
+ }
66
+ }
67
+ return Promise.all(targets.map(async (target) => ({
68
+ id: target.id,
69
+ name: sayIn(lang, target.name),
70
+ description: sayIn(lang, target.description),
71
+ icon: target.icon,
72
+ agent: Boolean(target.agent),
73
+ usable: await offerable(target, projectId),
74
+ byDefault: target.byDefault !== false,
75
+ fields: target.fields.map((f) => ({ id: f.id, label: sayIn(lang, f.label), kind: f.kind })),
76
+ })));
77
+ }
78
+ /** Whether a target can work here right now: what it drives is there, its account can be connected. */
79
+ async function offerable(target, projectId) {
80
+ if (target.available && !(await target.available())) {
81
+ return false;
82
+ }
83
+ const connection = target.connection?.();
84
+ return !connection || (await connectorOptions(connection, projectId)).length > 0;
85
+ }
86
+ /**
87
+ * The targets a result names: its own list, else every target of the project a result offers
88
+ * by default. `missing` names listed targets this project does not have.
89
+ */
90
+ async function namedTargets(run, projectId) {
91
+ const result = resultStepOf(run);
92
+ if (!result) {
93
+ return { entries: [], listed: true, missing: [] };
94
+ }
95
+ if (!result.share) {
96
+ const entries = (await projectTargets(projectId))
97
+ .filter((target) => target.byDefault !== false)
98
+ .map((target) => ({ key: target.id, target, values: {} }));
99
+ return { entries, listed: false, missing: [] };
100
+ }
101
+ const entries = [];
102
+ const missing = [];
103
+ for (const [i, option] of result.share.entries()) {
104
+ const target = await shareTarget(projectId, option.target);
105
+ if (target) {
106
+ entries.push({
107
+ key: `${i}:${option.target}`,
108
+ target,
109
+ label: option.label,
110
+ values: option.values ?? {},
111
+ });
112
+ }
113
+ else {
114
+ missing.push(option.label ?? option.target);
115
+ }
116
+ }
117
+ return { entries, listed: true, missing };
118
+ }
119
+ /**
120
+ * The targets a finished run offers. A wizard that lists them on its result decides; else every
121
+ * target that needs no model is offered, and the ones an agent drives only to the wizard's owner
122
+ * — they run on the wizard's credits.
123
+ */
124
+ export async function offeredTargets(run, projectId, member) {
125
+ const { entries, listed } = await namedTargets(run, projectId);
126
+ const asked = entries.filter((e) => listed || !e.target.agent || member);
127
+ const usable = await Promise.all(asked.map((e) => offerable(e.target, projectId)));
128
+ return asked.filter((_, i) => usable[i]);
129
+ }
130
+ export function targetScope(run, projectId, lang) {
131
+ const result = resultStepOf(run);
132
+ return {
133
+ run,
134
+ projectId,
135
+ lang,
136
+ title: [result?.title, run.definition.title].filter(Boolean).join(" · "),
137
+ message: result?.message ?? null,
138
+ text: resultText(run),
139
+ };
140
+ }
141
+ async function templateScope(run, projectId) {
142
+ const project = await db.query.project.findFirst({ where: eq(schema.project.id, projectId) });
143
+ return {
144
+ def: run.definition,
145
+ state: run.state,
146
+ brand: project?.brand ?? {},
147
+ };
148
+ }
149
+ async function targetView(entry, scope, templates) {
150
+ const { target } = entry;
151
+ const { lang } = scope;
152
+ const connection = target.connection?.() ?? null;
153
+ const [account] = connection
154
+ ? await connectionViews([connection], scopeOf(scope.run), scope.projectId, lang)
155
+ : [null];
156
+ const defaults = (await target.defaults?.(scope)) ?? {};
157
+ return {
158
+ key: entry.key,
159
+ target: target.id,
160
+ name: entry.label ?? sayIn(lang, target.name),
161
+ description: sayIn(lang, target.description),
162
+ icon: target.icon,
163
+ connection: account,
164
+ fields: target.fields.map((f) => ({
165
+ ...f,
166
+ label: sayIn(lang, f.label),
167
+ placeholder: f.placeholder && sayIn(lang, f.placeholder),
168
+ help: f.help && sayIn(lang, f.help),
169
+ value: entry.values[f.id] !== undefined
170
+ ? renderTemplate(entry.values[f.id], templates)
171
+ : (defaults[f.id] ?? ""),
172
+ })),
173
+ takes: target.takes,
174
+ agent: Boolean(target.agent),
175
+ presets: (target.presets ?? []).map((p) => ({
176
+ label: p.label,
177
+ values: Object.fromEntries(Object.entries(p.values).map(([id, v]) => [id, sayIn(lang, v)])),
178
+ })),
179
+ };
180
+ }
181
+ /** What the share sheet shows: the targets of this result and what was sent before. */
182
+ export async function shareOverview(run, projectId, member, lang) {
183
+ const scope = targetScope(run, projectId, lang);
184
+ const templates = await templateScope(run, projectId);
185
+ const offered = await offeredTargets(run, projectId, member);
186
+ const jobs = await shareJobs(run.id);
187
+ return {
188
+ targets: await Promise.all(offered.map((entry) => targetView(entry, scope, templates))),
189
+ jobs,
190
+ };
191
+ }
192
+ const PLACEHOLDER = /\{\{\s*([^}]+?)\s*\}\}/g;
193
+ /** The placeholders of prefilled values that come out empty. */
194
+ function emptyPlaceholders(values, templates) {
195
+ const empty = new Set();
196
+ for (const value of Object.values(values)) {
197
+ for (const [whole, ref] of value.matchAll(PLACEHOLDER)) {
198
+ if (!renderTemplate(whole, templates).trim()) {
199
+ empty.add(ref.trim());
200
+ }
201
+ }
202
+ }
203
+ return [...empty];
204
+ }
205
+ /** How much of a written deliverable the preview shows. */
206
+ const EXCERPT = 1500;
207
+ /** A run that never happened, for a wizard without a finished test run: the draft, no answers. */
208
+ function standIn(wizard, userId) {
209
+ const now = new Date();
210
+ return {
211
+ id: `preview-${wizard.id}`,
212
+ wizardId: wizard.id,
213
+ tenantId: wizard.tenantId,
214
+ definition: wizard.draft,
215
+ files: [],
216
+ version: null,
217
+ mode: "test",
218
+ runner: null,
219
+ lang: null,
220
+ visitorId: null,
221
+ userId,
222
+ ipHash: null,
223
+ origin: null,
224
+ client: null,
225
+ status: "done",
226
+ cursor: null,
227
+ state: { values: {}, outputs: {}, history: [], notes: {} },
228
+ error: null,
229
+ ask: null,
230
+ answer: null,
231
+ unattended: false,
232
+ deviceOffer: null,
233
+ costMicros: 0,
234
+ shareToken: null,
235
+ sharedAt: null,
236
+ expiresAt: null,
237
+ createdAt: now,
238
+ updatedAt: now,
239
+ };
240
+ }
241
+ /**
242
+ * The draft's result page as a visitor (`member` false) or its owner gets it: its share targets,
243
+ * filled from the wizard's last finished test run, and what that run wrote. `resultId` picks the
244
+ * result step where the draft has several.
245
+ */
246
+ export async function sharePreview(wizard, userId, resultId, member, lang) {
247
+ const last = await db.query.run.findFirst({
248
+ where: and(eq(schema.run.tenantId, currentTenant()), eq(schema.run.wizardId, wizard.id), eq(schema.run.mode, "test"), eq(schema.run.status, "done")),
249
+ orderBy: [desc(schema.run.createdAt)],
250
+ });
251
+ // The draft as it is now, on what the last test run answered and made.
252
+ const run = {
253
+ ...(last ?? standIn(wizard, userId)),
254
+ definition: wizard.draft,
255
+ cursor: resultId,
256
+ };
257
+ const scope = targetScope(run, wizard.projectId, lang);
258
+ const templates = await templateScope(run, wizard.projectId);
259
+ const { entries, listed, missing } = await namedTargets(run, wizard.projectId);
260
+ const targets = [];
261
+ const left = missing.map((name) => ({ name, why: "setup" }));
262
+ for (const entry of entries) {
263
+ const name = entry.label ?? sayIn(lang, entry.target.name);
264
+ if (!listed && entry.target.agent && !member) {
265
+ left.push({ name, why: "agent" });
266
+ }
267
+ else if (!(await offerable(entry.target, wizard.projectId))) {
268
+ left.push({ name, why: "setup" });
269
+ }
270
+ else {
271
+ targets.push({
272
+ ...(await targetView(entry, scope, templates)),
273
+ empty: emptyPlaceholders(entry.values, templates),
274
+ });
275
+ }
276
+ }
277
+ const outputs = {};
278
+ for (const d of resultStepOf(run)?.deliverables ?? []) {
279
+ const text = run.state.outputs[d.from]?.text?.trim();
280
+ if (text) {
281
+ outputs[d.from] = text.slice(0, EXCERPT);
282
+ }
283
+ }
284
+ return {
285
+ run: last ? { id: last.id, at: last.updatedAt.toISOString() } : null,
286
+ targets,
287
+ left,
288
+ outputs,
289
+ };
290
+ }
291
+ // The account a target sends with is a connection of its own, kept like the wizard's connections.
292
+ registerConnectionSource(async (_run, connectionId, projectId) => {
293
+ if (!connectionId.startsWith("share:")) {
294
+ return null;
295
+ }
296
+ const target = await shareTarget(projectId, connectionId.slice("share:".length));
297
+ return target?.connection?.() ?? null;
298
+ });
@@ -0,0 +1,186 @@
1
+ import { noteText } from "@engenty/wizards-shared/run";
2
+ import { connectionContext } from "../connectors/index.js";
3
+ import { askPerson } from "../engine/asks.js";
4
+ import { signalChanged } from "../engine/events.js";
5
+ import { releaseResources } from "../engine/resources.js";
6
+ import { loadProject, makeContext, projectIdOf } from "../engine/runner.js";
7
+ import { env } from "../env.js";
8
+ import { errorText } from "../model-errors.js";
9
+ import { ServiceError } from "../services/errors.js";
10
+ import { shareRun } from "../services/shares.js";
11
+ import { scopeOf } from "../store/index.js";
12
+ import { serverText } from "../texts.js";
13
+ import { SHARE_STEP } from "./agent.js";
14
+ import { shareFiles } from "./files.js";
15
+ import { offeredTargets, targetScope } from "./index.js";
16
+ import { addJobEvent, createJob, finishJob, jobCount, sentCount, setWorking, workingJob, } from "./state.js";
17
+ import { parseRecipients } from "./targets/email.js";
18
+ import { ShareError, sayIn } from "./types.js";
19
+ /** What the person filled in does not do: per field, what is wrong. `_files` and `_account` name the other steps. */
20
+ export class ShareInputError extends Error {
21
+ fields;
22
+ constructor(fields, said) {
23
+ super(Object.values(fields)[0] ?? said);
24
+ this.fields = fields;
25
+ }
26
+ }
27
+ /** Jobs one run of a person without an account may start. */
28
+ const VISITOR_JOBS = 20;
29
+ /** A link people elsewhere can open: not one that points at this machine. */
30
+ function publicOrigin() {
31
+ const host = new URL(env.appUrl).hostname;
32
+ return !(host === "localhost" ||
33
+ host.endsWith(".localhost") ||
34
+ host.endsWith(".local") ||
35
+ host === "127.0.0.1" ||
36
+ host === "[::1]");
37
+ }
38
+ function contextOf(run, projectId, entry, who, job) {
39
+ const connection = entry.target.connection?.() ?? null;
40
+ const signal = job?.signal ?? new AbortController().signal;
41
+ const emit = (message) => job ? addJobEvent(run.id, job.id, sayIn(who.lang, message)) : Promise.resolve();
42
+ const ask = (question) => !job || run.unattended
43
+ ? Promise.resolve(null)
44
+ : askPerson(run.id, { ...question, stepId: SHARE_STEP }, signal);
45
+ return {
46
+ ...targetScope(run, projectId, who.lang),
47
+ signal,
48
+ member: who.member,
49
+ sentBefore: job?.sentBefore ?? 0,
50
+ emit,
51
+ ask,
52
+ account: () => connection ? connectionContext(scopeOf(run), connection, projectId) : Promise.resolve(null),
53
+ link: async () => (publicOrigin() ? (await shareRun(run)).url : null),
54
+ step: async () => makeContext(run, await loadProject(run), structuredClone(run.state), signal, {
55
+ stepId: SHARE_STEP,
56
+ emit: (_type, message) => emit(typeof message === "string" ? message : noteText(message, who.lang)),
57
+ ask,
58
+ }),
59
+ };
60
+ }
61
+ async function entryOf(run, projectId, key, member) {
62
+ const entry = (await offeredTargets(run, projectId, member)).find((e) => e.key === key);
63
+ if (!entry) {
64
+ throw new ServiceError("not_found", { server: "shareTargetUnknown" });
65
+ }
66
+ return entry;
67
+ }
68
+ /**
69
+ * Sends a finished result to one target. Answers as soon as the job stands; the work goes on in
70
+ * the background and shows in the run's view. One job per run at a time.
71
+ */
72
+ export async function startShare(run, request, who) {
73
+ if (run.status !== "done") {
74
+ throw new ServiceError("refused", { server: "shareOnlyDone" });
75
+ }
76
+ if (workingJob(run.id)) {
77
+ throw new ServiceError("refused", { server: "shareBusy" });
78
+ }
79
+ // A run of someone without an account sends a limited number of times: it is no mail relay.
80
+ if (!who.member && (await jobCount(run.id)) >= VISITOR_JOBS) {
81
+ throw new ServiceError("refused", { server: "shareTooOften" });
82
+ }
83
+ const lang = request.lang ?? "en";
84
+ const projectId = await projectIdOf(run);
85
+ const entry = await entryOf(run, projectId, request.target, who.member);
86
+ const { target } = entry;
87
+ const values = {};
88
+ const errors = {};
89
+ for (const field of target.fields) {
90
+ const value = String(request.values[field.id] ?? "")
91
+ .trim()
92
+ .slice(0, field.kind === "textarea" ? 20_000 : 2000);
93
+ values[field.id] = value;
94
+ if (!value) {
95
+ if (field.required) {
96
+ errors[field.id] = serverText(lang, "shareRequired");
97
+ }
98
+ continue;
99
+ }
100
+ if (field.kind === "url" && !/^https?:\/\/[^\s/]+\.[^\s]+$/i.test(value)) {
101
+ errors[field.id] = serverText(lang, "shareNeedsHttps");
102
+ }
103
+ if (field.kind === "emails") {
104
+ try {
105
+ parseRecipients(value);
106
+ }
107
+ catch (err) {
108
+ errors[field.id] = errorText(err, lang);
109
+ }
110
+ }
111
+ }
112
+ const items = target.takes === "files" ? request.items.slice(0, 20) : [];
113
+ if (target.needsFiles && !items.length) {
114
+ errors._files = serverText(lang, "sharePickFile");
115
+ }
116
+ const connection = target.connection?.() ?? null;
117
+ if (connection && !(await connectionContext(scopeOf(run), connection, projectId))) {
118
+ errors._account = serverText(lang, "shareConnectAccount");
119
+ }
120
+ if (Object.keys(errors).length) {
121
+ throw new ShareInputError(errors, serverText(lang, "shareCheckInput"));
122
+ }
123
+ const job = await createJob({
124
+ runId: run.id,
125
+ target: target.id,
126
+ name: entry.label ?? sayIn(lang, target.name),
127
+ icon: target.icon,
128
+ request: { target: request.target, items, values, lang },
129
+ });
130
+ const abort = new AbortController();
131
+ setWorking(run.id, { jobId: job.id, abort });
132
+ const ctx = contextOf(run, projectId, entry, { member: who.member, lang }, { id: job.id, signal: abort.signal, sentBefore: await sentCount(run.id, target.id) });
133
+ void (async () => {
134
+ try {
135
+ if (items.length) {
136
+ await ctx.emit({ server: "sharePreparing" });
137
+ }
138
+ const files = await shareFiles(run, items);
139
+ const outcome = await target.send({ values, files }, ctx);
140
+ await finishJob(run.id, job.id, abort.signal.aborted
141
+ ? { status: "cancelled" }
142
+ : {
143
+ status: "done",
144
+ result: { summary: sayIn(lang, outcome.summary), url: outcome.url ?? null },
145
+ });
146
+ }
147
+ catch (err) {
148
+ if (!abort.signal.aborted && !(err instanceof ShareError)) {
149
+ console.error(`[share ${run.id} → ${target.id}]`, err);
150
+ }
151
+ await finishJob(run.id, job.id, abort.signal.aborted
152
+ ? { status: "cancelled" }
153
+ : {
154
+ status: "failed",
155
+ error: err instanceof ShareError
156
+ ? errorText(err, lang)
157
+ : `${serverText(lang, "shareFailed")}: ${String(err?.message ?? err).slice(0, 240)}`,
158
+ }).catch(() => undefined);
159
+ }
160
+ finally {
161
+ setWorking(run.id, null);
162
+ await releaseResources(run.id).catch(() => undefined);
163
+ signalChanged(run.id);
164
+ }
165
+ })();
166
+ return job;
167
+ }
168
+ /** Stops the job a run is working on; what it already sent stays sent. */
169
+ export async function cancelShare(runId, jobId) {
170
+ const job = workingJob(runId);
171
+ if (!job || (jobId && job.jobId !== jobId)) {
172
+ return false;
173
+ }
174
+ job.abort.abort();
175
+ await finishJob(runId, job.jobId, { status: "cancelled" });
176
+ return true;
177
+ }
178
+ /** The choices of a form field that come from the connected account (Slack's channels). */
179
+ export async function shareChoices(run, key, field, who) {
180
+ const projectId = await projectIdOf(run);
181
+ const entry = await entryOf(run, projectId, key, who.member);
182
+ if (!entry.target.choices) {
183
+ return [];
184
+ }
185
+ return entry.target.choices(field, contextOf(run, projectId, entry, who, null));
186
+ }
@@ -0,0 +1,116 @@
1
+ import { and, desc, eq } from "drizzle-orm";
2
+ import { nanoid } from "nanoid";
3
+ import { db, schema } from "../db/client.js";
4
+ import { pendingAsk } from "../engine/asks.js";
5
+ import { signalChanged } from "../engine/events.js";
6
+ import { currentTenant } from "../tenants/tenant.js";
7
+ import { serverText } from "../texts.js";
8
+ /** Jobs this process is working on, by run. One at a time per run: they share its browser. */
9
+ const working = new Map();
10
+ export function workingJob(runId) {
11
+ return working.get(runId) ?? null;
12
+ }
13
+ export function setWorking(runId, job) {
14
+ if (job) {
15
+ working.set(runId, job);
16
+ }
17
+ else {
18
+ working.delete(runId);
19
+ }
20
+ }
21
+ async function view(row) {
22
+ return {
23
+ id: row.id,
24
+ target: row.target,
25
+ name: row.name,
26
+ icon: row.icon,
27
+ status: row.status,
28
+ events: row.events,
29
+ ask: row.status === "running" ? await pendingAsk(row.runId) : null,
30
+ result: row.result ?? null,
31
+ error: row.error,
32
+ createdAt: row.createdAt.toISOString(),
33
+ };
34
+ }
35
+ /** A job left "running" by a server that stopped: nobody works on it any more. */
36
+ async function settled(row) {
37
+ if (row.status !== "running" || working.get(row.runId)?.jobId === row.id) {
38
+ return row;
39
+ }
40
+ const error = serverText(row.request.lang ?? "en", "shareInterrupted");
41
+ await db
42
+ .update(schema.shareJob)
43
+ .set({ status: "failed", error, updatedAt: new Date() })
44
+ .where(eq(schema.shareJob.id, row.id));
45
+ return { ...row, status: "failed", error };
46
+ }
47
+ export async function createJob(input) {
48
+ const [row] = await db
49
+ .insert(schema.shareJob)
50
+ // The column's default counts whole seconds; jobs of one second must still keep their order.
51
+ .values({
52
+ id: nanoid(14),
53
+ tenantId: currentTenant(),
54
+ status: "running",
55
+ createdAt: new Date(),
56
+ ...input,
57
+ })
58
+ .returning();
59
+ signalChanged(input.runId);
60
+ return view(row);
61
+ }
62
+ export async function addJobEvent(runId, jobId, message) {
63
+ const row = await db.query.shareJob.findFirst({ where: eq(schema.shareJob.id, jobId) });
64
+ if (!row) {
65
+ return;
66
+ }
67
+ const events = [
68
+ ...row.events,
69
+ { at: new Date().toISOString(), message: message.slice(0, 300) },
70
+ ].slice(-40);
71
+ await db
72
+ .update(schema.shareJob)
73
+ .set({ events, updatedAt: new Date() })
74
+ .where(eq(schema.shareJob.id, jobId));
75
+ signalChanged(runId);
76
+ }
77
+ export async function finishJob(runId, jobId, end) {
78
+ await db
79
+ .update(schema.shareJob)
80
+ .set({
81
+ status: end.status,
82
+ result: end.status === "done" ? end.result : null,
83
+ error: end.status === "failed" ? end.error : null,
84
+ updatedAt: new Date(),
85
+ })
86
+ .where(and(eq(schema.shareJob.id, jobId), eq(schema.shareJob.status, "running")));
87
+ signalChanged(runId);
88
+ }
89
+ export async function shareJobs(runId, limit = 12) {
90
+ const rows = await db.query.shareJob.findMany({
91
+ where: eq(schema.shareJob.runId, runId),
92
+ orderBy: [desc(schema.shareJob.createdAt), desc(schema.shareJob.id)],
93
+ limit,
94
+ });
95
+ return Promise.all(rows.map(async (row) => view(await settled(row))));
96
+ }
97
+ /** The run's newest job: the one on its way, or the last one that arrived or failed. */
98
+ export async function latestShareJob(runId) {
99
+ return (await shareJobs(runId, 1))[0] ?? null;
100
+ }
101
+ /** How many jobs a run has started, whatever became of them. */
102
+ export async function jobCount(runId) {
103
+ const rows = await db.query.shareJob.findMany({
104
+ where: eq(schema.shareJob.runId, runId),
105
+ columns: { id: true },
106
+ });
107
+ return rows.length;
108
+ }
109
+ /** How often a target has sent for a run. */
110
+ export async function sentCount(runId, target) {
111
+ const rows = await db.query.shareJob.findMany({
112
+ where: and(eq(schema.shareJob.runId, runId), eq(schema.shareJob.target, target), eq(schema.shareJob.status, "done")),
113
+ columns: { id: true },
114
+ });
115
+ return rows.length;
116
+ }