@openhands/agent-canvas 1.18.0 → 1.20.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.
- package/README.md +1 -1
- package/README.windows.md +2 -2
- package/build/assets/acp-providers-DOKcGeaV.js +1 -0
- package/build/assets/active-backend-context-BETEdaJl.js +2 -0
- package/build/assets/{agent-context-settings-D3peZkjX.js → agent-context-settings-Dir0loAn.js} +1 -1
- package/build/assets/{agent-profiles-service.api-DkeZaqIl.js → agent-profiles-service.api-CiMVQAIT.js} +1 -1
- package/build/assets/{agent-profiles-settings-7yMWYjGF.js → agent-profiles-settings-CT5owzRT.js} +1 -1
- package/build/assets/agent-server-conversation-service.api-BtOlitCD.js +88 -0
- package/build/assets/agent-settings-BnzyTgCb.js +2 -0
- package/build/assets/agent-settings-CeXj3xK-.js +1 -0
- package/build/assets/{alert-banner-eYyqvHL3.js → alert-banner-Ccx1Du6n.js} +1 -1
- package/build/assets/{api-key-entry-screen-CCBvBD8u.js → api-key-entry-screen-DXIuzBjY.js} +1 -1
- package/build/assets/{app-settings-BtYDs-Be.js → app-settings-DkIJVTKL.js} +1 -1
- package/build/assets/automation-detail-CF2SweVs.js +3 -0
- package/build/assets/automation-disabled-reason-QFtMrdOr.js +1 -0
- package/build/assets/{automation-export-BJsfi7n2.js → automation-export-CkvehhPF.js} +1 -1
- package/build/assets/{automation-git-sync-B7o_Nbl-.js → automation-git-sync-di4tBAyZ.js} +1 -1
- package/build/assets/automation-setup-route-FERj7WEe.js +3 -0
- package/build/assets/{automation-templates-Bb98r-kx.js → automation-templates-DlaCRvD-.js} +1 -1
- package/build/assets/automations-list-yK7ETmYP.js +1 -0
- package/build/assets/{back-nav-button-B2yBiVMB.js → back-nav-button-oDvIxpQ5.js} +1 -1
- package/build/assets/backend-form-modal-DzYf_arO.js +1 -0
- package/build/assets/{backend-synced-settings-badge-DLEp6cq7.js → backend-synced-settings-badge-DXDDHWX4.js} +1 -1
- package/build/assets/{badge-ehL-kgeO.js → badge-CDHZEVeN.js} +1 -1
- package/build/assets/{base-modal-C1a0TsSx.js → base-modal-CZq4kB_H.js} +1 -1
- package/build/assets/{brand-button-CUMfjb99.js → brand-button-CGUwj6zK.js} +1 -1
- package/build/assets/{browser-SE24Tk8g.js → browser-kY6jN61T.js} +2 -2
- package/build/assets/{browser-tab-HjJxHA0V.js → browser-tab-DFyZXsnB.js} +1 -1
- package/build/assets/{canvas-extension-page-BsUGBb1T.js → canvas-extension-page-DsGq1DtT.js} +1 -1
- package/build/assets/{canvas-extensions-C9Tm6s_E.js → canvas-extensions-Cb2Z0Hdc.js} +1 -1
- package/build/assets/{canvas-extensions-runtime-B8kxM4rO.js → canvas-extensions-runtime-DS6eT4nK.js} +1 -1
- package/build/assets/{chat-send-button-CGvc2wpE.js → chat-send-button-B6aVaD9V.js} +1 -1
- package/build/assets/{circle-plus-check-toggle-BDX_ctNs.js → circle-plus-check-toggle-ZOXpJ3I2.js} +1 -1
- package/build/assets/{cloud-funnel-analytics-D5uJ_4JE.js → cloud-funnel-analytics-BH_zxjiO.js} +1 -1
- package/build/assets/{cog-CAEugqtV.js → cog-DVZT1f0s.js} +1 -1
- package/build/assets/{command-menu-CQxs4mtX.js → command-menu-ImeHQ6c2.js} +1 -1
- package/build/assets/{commits-tab-D0xgjipP.js → commits-tab-B5SUF66c.js} +1 -1
- package/build/assets/{condenser-settings-D63NJRBT.js → condenser-settings-BlGOXEXM.js} +1 -1
- package/build/assets/{confirmation-modal-78sd9Vny.js → confirmation-modal-oN1EId79.js} +1 -1
- package/build/assets/{context-menu-list-item-Cm7-7Y-7.js → context-menu-list-item-DIm5xQg6.js} +1 -1
- package/build/assets/{conversation-B_nweu5p.js → conversation-9a53tdbO.js} +4 -4
- package/build/assets/conversation-XRsI28ZM.js +1 -0
- package/build/assets/conversation-panel-XRsI28ZM.js +1 -0
- package/build/assets/{conversation-state-store-9D_3P7q4.js → conversation-state-store-Br-eE-gg.js} +1 -1
- package/build/assets/{conversation-tab-empty-state-B3dpJXX0.js → conversation-tab-empty-state-BT1GrnKP.js} +1 -1
- package/build/assets/{copy-to-clipboard-button-B1ljgNxO.js → copy-to-clipboard-button-CvTHqY13.js} +1 -1
- package/build/assets/{device-verify-D5QRIS_x.js → device-verify-DX0xdNjo.js} +1 -1
- package/build/assets/{dropdown-classes-BGvOvW2_.js → dropdown-classes-DyyMaaXl.js} +1 -1
- package/build/assets/edit-automation-modal-bfD-5NIc.js +1 -0
- package/build/assets/empty-state-B5advKVW.js +1 -0
- package/build/assets/{entry.client-BMafTpjO.js → entry.client-D_2gnSyB.js} +2 -2
- package/build/assets/{enum-filter-dropdown-UNaom12H.js → enum-filter-dropdown-VAJp5mqh.js} +1 -1
- package/build/assets/{environment-switch-overlay-COibRUA-.js → environment-switch-overlay-C7x_yhEs.js} +1 -1
- package/build/assets/{error-state-Cf8LRuuY.js → error-state-BgYl7YeB.js} +1 -1
- package/build/assets/{extensions-hub-C_6ahIua.js → extensions-hub-DjhOed1I.js} +1 -1
- package/build/assets/{extensions-navigation-C5GfpzFe.js → extensions-navigation-Di-J2KUR.js} +1 -1
- package/build/assets/{files-tab-D5PPIWI1.js → files-tab-BNDp3e_x.js} +1 -1
- package/build/assets/{form-control-classes-Dbq-7YP_.js → form-control-classes-BVI_szaj.js} +1 -1
- package/build/assets/{get-git-path-jU2zjqIB.js → get-git-path-DOe6PM_a.js} +1 -1
- package/build/assets/git-repo-dropdown-1fZGYtYm.js +1 -0
- package/build/assets/{git-repo-dropdown-uxlFw2gn.js → git-repo-dropdown-DNlinQ7r.js} +1 -1
- package/build/assets/{highlighted-source-view-C7pAXrT0.js → highlighted-source-view-DPerL6tF.js} +1 -1
- package/build/assets/home-4lyLzYjG.js +1 -0
- package/build/assets/home-BGaB0EeK.js +1 -0
- package/build/assets/home-automation-run-tooltip-v5n7zXR0.js +1 -0
- package/build/assets/{home-store-DCbei3DX.js → home-store-roz1x6D5.js} +1 -1
- package/build/assets/{hooks-service-Dnmhqc0Z.js → hooks-service-tzRqtAlm.js} +1 -1
- package/build/assets/index-home-6Y_Y7kOo.js +1 -0
- package/build/assets/{install-server-modal-B81I-5OE.js → install-server-modal-DkMCfj0J.js} +1 -1
- package/build/assets/{key-O84CMFHb.js → key-DpfBWQbk.js} +1 -1
- package/build/assets/{launch-ZLxI4opd.js → launch-TrnYlJe6.js} +1 -1
- package/build/assets/link-external-B4gx3EeM.js +1 -0
- package/build/assets/llm-not-configured-banner-BdjItKQR.js +27 -0
- package/build/assets/{llm-settings-CODhjQzy.js → llm-settings-CUgt39uu.js} +1 -1
- package/build/assets/llm-settings-DEfw-fUJ.js +1 -0
- package/build/assets/{llm-subscription-service-CgIDc38n.js → llm-subscription-service-LGYVzyAc.js} +1 -1
- package/build/assets/{loading-spinner-BeLV0IXf.js → loading-spinner-BJc2J0Dw.js} +1 -1
- package/build/assets/{manage-backends-modal-cMXqCb-B.js → manage-backends-modal-CIjk0B2H.js} +1 -1
- package/build/assets/manifest-23d79467.js +1 -0
- package/build/assets/{manifest-subpage-layout-CVZgVKoY.js → manifest-subpage-layout-DGYRoP_8.js} +1 -1
- package/build/assets/{markdown-renderer-DyFl9-mL.js → markdown-renderer-DRpwRlNm.js} +1 -1
- package/build/assets/mcp-CoUOIGBF.js +1 -0
- package/build/assets/mcp-installed-servers-BW51n6PC.js +1 -0
- package/build/assets/mcp-marketplace-utils-BwEnDYqm.js +1 -0
- package/build/assets/mcp-page-hZ4aJjj_.js +9 -0
- package/build/assets/{messages-Bk1xhbU2.js → messages-WUb9Yh-p.js} +4 -4
- package/build/assets/{modal-body-C24WsdFb.js → modal-body-B_95ci1c.js} +1 -1
- package/build/assets/{modal-classes-B0b9eiL8.js → modal-classes-BPcBCWVj.js} +1 -1
- package/build/assets/{modal-close-button-DfqfB2Nj.js → modal-close-button-Cq3dma5u.js} +1 -1
- package/build/assets/onboarding-Cc4Av7bi.js +1 -0
- package/build/assets/{onboarding-modal-BT3Gx_sZ.js → onboarding-modal-DCFD4Dsa.js} +1 -1
- package/build/assets/{organization-service.api-B5CvmnYQ.js → organization-service.api-zhHpQU0t.js} +1 -1
- package/build/assets/parse-git-remote-url-BG8pcKL8.js +1 -0
- package/build/assets/{planner-tab-CstoOlAB.js → planner-tab-yISBVUKu.js} +1 -1
- package/build/assets/{plugins-management-service-5rqVPEGN.js → plugins-management-service-2oP6_tWr.js} +1 -1
- package/build/assets/{profiles-service.api-D1XKMgr0.js → profiles-service.api-B5thiTI3.js} +1 -1
- package/build/assets/{providers-XmZ9HM4Q.js → providers-Co9PsD0s.js} +1 -1
- package/build/assets/{proxy-XP6U1Dhv.js → proxy-BCCQUna8.js} +1 -1
- package/build/assets/recommended-automations-launcher-D1Do753p.js +1 -0
- package/build/assets/root-D1y82Lhg.js +2 -0
- package/build/assets/{root-layout-DUquNZTU.js → root-layout-cQTYEG2w.js} +2 -2
- package/build/assets/root-sEWn3ha1.css +1 -0
- package/build/assets/{schema-field-Y3F8JvXd.js → schema-field-DSi0d7a8.js} +1 -1
- package/build/assets/{sdk-section-page-CNZTLwH9.js → sdk-section-page-DCoI2dtd.js} +1 -1
- package/build/assets/{secret-form-9-BoGA40.js → secret-form-BDvzojzN.js} +1 -1
- package/build/assets/{secrets-service-BOIsfD9j.js → secrets-service-C3CNNEpY.js} +1 -1
- package/build/assets/{secrets-settings-CBJgFr42.js → secrets-settings-BKqfYQt5.js} +1 -1
- package/build/assets/{settings-D4hdToD4.js → settings-C8ZLONyx.js} +1 -1
- package/build/assets/{settings-dropdown-input-DBvsZ7-Q.js → settings-dropdown-input-Df_HXRk4.js} +1 -1
- package/build/assets/{settings-index-DeQphyBK.js → settings-index-BWvlGHg3.js} +1 -1
- package/build/assets/{settings-input-dVuzapBA.js → settings-input-B3i5U9GL.js} +1 -1
- package/build/assets/{settings-list-classes-DhAPuXm6.js → settings-list-classes-DbqcSiVW.js} +1 -1
- package/build/assets/{settings-modal-tHiJ8iYV.js → settings-modal-DHKJSBPL.js} +1 -1
- package/build/assets/{settings-nav-GAQjW1a-.js → settings-nav-BThjwLWD.js} +1 -1
- package/build/assets/{settings-service.api-C1vZH9Vh.js → settings-service.api-CYBTLGOd.js} +1 -1
- package/build/assets/{settings-switch-DDd1cQ8t.js → settings-switch-CI93Iozg.js} +1 -1
- package/build/assets/{shared-conversation-Cyg7m7ns.js → shared-conversation-C7OILS-U.js} +1 -1
- package/build/assets/{sidebar-layout-DH0E-5KW.js → sidebar-layout-Csb6Geq0.js} +1 -1
- package/build/assets/{sidebar-mobile-menu-toggle-CQ4iky__.js → sidebar-mobile-menu-toggle-B2xwXm4s.js} +1 -1
- package/build/assets/{sidebar-nav-link-oLQiKUBu.js → sidebar-nav-link-Xv7QEMVS.js} +1 -1
- package/build/assets/{sidebar-onboarding-checklist-storage-TfvNNnnr.js → sidebar-onboarding-checklist-storage-_zxMqHS0.js} +1 -1
- package/build/assets/{skill-card-pill-row-DfjIfj0K.js → skill-card-pill-row-CxOFySng.js} +1 -1
- package/build/assets/{skill-enablement-DXeaImBz.js → skill-enablement-BBKya60C.js} +1 -1
- package/build/assets/{skill-filter-DsNLmn76.js → skill-filter-BvtbD--b.js} +1 -1
- package/build/assets/{skill-icon-badge-0MK_68Nc.js → skill-icon-badge-CVxE42yg.js} +1 -1
- package/build/assets/{skill-scope-BE-H9TSZ.js → skill-scope-C0NxJnpT.js} +1 -1
- package/build/assets/{skills-plugins-vzSdxSk_.js → skills-plugins-BGjX4jve.js} +1 -1
- package/build/assets/{skills-settings-BiPOH9H9.js → skills-settings-d3ZwIjSP.js} +1 -1
- package/build/assets/{styled-tooltip-CejG0rqv.js → styled-tooltip-CknPQH4Y.js} +1 -1
- package/build/assets/{task-list-tab-OkZSqnTp.js → task-list-tab-CQisRcPh.js} +1 -1
- package/build/assets/{terminal-Dph60VFZ.js → terminal-Bs50gJPC.js} +1 -1
- package/build/assets/{toggle-switch-B2uXaAUS.js → toggle-switch-D3TBxlsc.js} +1 -1
- package/build/assets/trash-D1QMHInX.js +1 -0
- package/build/assets/{typography-DgcLGdHk.js → typography-BdAApXGl.js} +1 -1
- package/build/assets/{usage-tab-MCmNRAnR.js → usage-tab-ClVHAFJy.js} +1 -1
- package/build/assets/{use-acp-credential-form-ychoGqjC.js → use-acp-credential-form-GfqcTJl5.js} +1 -1
- package/build/assets/{use-activate-agent-profile-8L8Xdghy.js → use-activate-agent-profile-DO6SNPkK.js} +1 -1
- package/build/assets/{use-agent-profiles-Bi8hSkze.js → use-agent-profiles-YsBlt3tT.js} +1 -1
- package/build/assets/{use-automation-health-U99vvHxx.js → use-automation-health-CbQ9n-k7.js} +1 -1
- package/build/assets/{use-automation-permissions-Buus0R6h.js → use-automation-permissions-gFy-2VD0.js} +1 -1
- package/build/assets/{use-automations-Bq5hVuv0.js → use-automations-7_XBXHQK.js} +1 -1
- package/build/assets/{use-can-manage-org-profiles-MzIeEV8F.js → use-can-manage-org-profiles-Dt4YkPO7.js} +1 -1
- package/build/assets/{use-canvas-extensions-Ca9KoVEm.js → use-canvas-extensions-BhYQrtMK.js} +1 -1
- package/build/assets/{use-cloud-current-user-id-B4ZLUZSa.js → use-cloud-current-user-id-Dqw6qQUg.js} +1 -1
- package/build/assets/{use-config-l5tkDHyD.js → use-config-BJ_FJCjV.js} +1 -1
- package/build/assets/use-create-conversation-D15Px574.js +1 -0
- package/build/assets/use-create-secret-D_qPU88t.js +1 -0
- package/build/assets/{use-free-models-gCsJ5BV0.js → use-free-models-FAk2nngq.js} +1 -1
- package/build/assets/{use-get-secrets-CMTuWEp_.js → use-get-secrets-Pbo9jh2A.js} +1 -1
- package/build/assets/{use-llm-configured-Dl4zg8ht.js → use-llm-configured-DF9BYmIe.js} +1 -1
- package/build/assets/{use-llm-profiles-SWsuFmFu.js → use-llm-profiles-U4u1gzuk.js} +1 -1
- package/build/assets/use-manifest-capabilities-D81_YdAF.js +1 -0
- package/build/assets/{use-onboarding-completion-Xl63ZvzQ.js → use-onboarding-completion-C4W-m2Jk.js} +1 -1
- package/build/assets/{use-pinned-home-route-BXDIUaxQ.js → use-pinned-home-route-DByWTojw.js} +1 -1
- package/build/assets/{use-plugins-marketplace-BIIaPiaB.js → use-plugins-marketplace-CTRoukH0.js} +1 -1
- package/build/assets/{use-save-agent-profile-D-ZHHE94.js → use-save-agent-profile-xpHBxMZM.js} +1 -1
- package/build/assets/{use-save-settings-Dapkf4cC.js → use-save-settings-CmX41wlf.js} +1 -1
- package/build/assets/{use-settings-C2KKQKVC.js → use-settings-D69cw06D.js} +1 -1
- package/build/assets/{use-settings-nav-items-TOc0Rsfd.js → use-settings-nav-items-CVz0SnDr.js} +1 -1
- package/build/assets/use-tracking-BAFpgpgq.js +3 -0
- package/build/assets/{use-user-conversation-5UFqMoO4.js → use-user-conversation-BBDXUbOc.js} +1 -1
- package/build/assets/utils-CbjwNcR-.js +1 -0
- package/build/assets/{vendor~browser-hbU4u2Ag.js → vendor~browser-BZ-zIfb6.js} +1 -1
- package/build/assets/vendor~entry.client~root~root-layout~index-home~home~conversation-panel~conversation~launch~b0x1wrp2-DvZtOcoT.js +2 -0
- package/build/assets/{vendor~root-layout~index-home~home~conversation-panel~conversation~mcp~automations-list~aut~ibfbmr9f-CxhwKuAd.js → vendor~root-layout~index-home~home~conversation-panel~conversation~mcp~automations-list~aut~ibfbmr9f-BIjRfQ4u.js} +1 -1
- package/build/assets/vendor~root~root-layout~index-home~home~conversation-panel~conversation~launch~extensions-h~inibp4qe-BBfdJIV9.js +48 -0
- package/build/assets/{vendor~root~root-layout~index-home~home~conversation-panel~conversation~launch~skills-setti~eo2nf80n-3eJ4ExtV.js → vendor~root~root-layout~index-home~home~conversation-panel~conversation~launch~skills-setti~eo2nf80n-fgzdgvrL.js} +3579 -165
- package/build/assets/{vendor~root~root-layout~index-home~home~conversation-panel~conversation~launch~skills-setti~jz4f6vo2-C7HiY4Ae.js → vendor~root~root-layout~index-home~home~conversation-panel~conversation~launch~skills-setti~jz4f6vo2-BqdU447s.js} +794 -227
- package/build/assets/{verification-settings-DdBpaVPz.js → verification-settings-BXspn27q.js} +1 -1
- package/build/index.html +4 -4
- package/build/locales/ar/openhands.json +18 -1
- package/build/locales/ca/openhands.json +18 -1
- package/build/locales/de/openhands.json +18 -1
- package/build/locales/en/openhands.json +18 -1
- package/build/locales/es/openhands.json +18 -1
- package/build/locales/fr/openhands.json +18 -1
- package/build/locales/it/openhands.json +18 -1
- package/build/locales/ja/openhands.json +18 -1
- package/build/locales/ko-KR/openhands.json +18 -1
- package/build/locales/no/openhands.json +18 -1
- package/build/locales/pt/openhands.json +18 -1
- package/build/locales/tr/openhands.json +18 -1
- package/build/locales/uk/openhands.json +18 -1
- package/build/locales/zh-CN/openhands.json +18 -1
- package/build/locales/zh-TW/openhands.json +18 -1
- package/config/defaults.json +3 -3
- package/dist/api/agent-profiles-service/profile-field-support.d.ts +2 -0
- package/dist/components/conversation-events/chat/event-content-helpers/get-skill-ready-content.cjs +3 -3
- package/dist/components/conversation-events/chat/event-content-helpers/get-skill-ready-content.cjs.map +1 -1
- package/dist/components/conversation-events/chat/event-content-helpers/get-skill-ready-content.js +12 -16
- package/dist/components/conversation-events/chat/event-content-helpers/get-skill-ready-content.js.map +1 -1
- package/dist/components/features/automations/agent-profile-selector.d.ts +7 -0
- package/dist/components/features/automations/detail/disabled-reason-banner.d.ts +16 -0
- package/dist/components/features/home/featured-automations/home-automation-run-tooltip.cjs +1 -1
- package/dist/components/features/home/featured-automations/home-automation-run-tooltip.cjs.map +1 -1
- package/dist/components/features/home/featured-automations/home-automation-run-tooltip.js +38 -29
- package/dist/components/features/home/featured-automations/home-automation-run-tooltip.js.map +1 -1
- package/dist/components/features/settings/agent-profiles/profile-scope-list.d.ts +20 -0
- package/dist/config/defaults.cjs +1 -1
- package/dist/config/defaults.cjs.map +1 -1
- package/dist/config/defaults.js +3 -3
- package/dist/config/defaults.js.map +1 -1
- package/dist/constants/acp-providers.cjs +1 -1
- package/dist/constants/acp-providers.cjs.map +1 -1
- package/dist/constants/acp-providers.d.ts +0 -5
- package/dist/constants/acp-providers.js +20 -16
- package/dist/constants/acp-providers.js.map +1 -1
- package/dist/constants/profile-scope.d.ts +21 -0
- package/dist/hooks/mutation/use-create-conversation.cjs +1 -1
- package/dist/hooks/mutation/use-create-conversation.cjs.map +1 -1
- package/dist/hooks/mutation/use-create-conversation.js +30 -19
- package/dist/hooks/mutation/use-create-conversation.js.map +1 -1
- package/dist/hooks/mutation/use-switch-acp-model.cjs +1 -1
- package/dist/hooks/mutation/use-switch-acp-model.cjs.map +1 -1
- package/dist/hooks/mutation/use-switch-acp-model.js +11 -12
- package/dist/hooks/mutation/use-switch-acp-model.js.map +1 -1
- package/dist/hooks/query/query-keys.cjs +1 -1
- package/dist/hooks/query/query-keys.cjs.map +1 -1
- package/dist/hooks/query/query-keys.d.ts +1 -0
- package/dist/hooks/query/query-keys.js +10 -1
- package/dist/hooks/query/query-keys.js.map +1 -1
- package/dist/hooks/query/use-active-acp-profile-detail.cjs +1 -1
- package/dist/hooks/query/use-active-acp-profile-detail.cjs.map +1 -1
- package/dist/hooks/query/use-active-acp-profile-detail.d.ts +0 -9
- package/dist/hooks/query/use-active-acp-profile-detail.js +4 -13
- package/dist/hooks/query/use-active-acp-profile-detail.js.map +1 -1
- package/dist/hooks/query/use-automation-sdk-version.cjs +1 -1
- package/dist/hooks/query/use-automation-sdk-version.cjs.map +1 -1
- package/dist/hooks/query/use-automation-sdk-version.js +3 -1
- package/dist/hooks/query/use-automation-sdk-version.js.map +1 -1
- package/dist/hooks/use-drag-resize.cjs +1 -1
- package/dist/hooks/use-drag-resize.cjs.map +1 -1
- package/dist/hooks/use-drag-resize.js +8 -9
- package/dist/hooks/use-drag-resize.js.map +1 -1
- package/dist/i18n/declaration.cjs +1 -1
- package/dist/i18n/declaration.cjs.map +1 -1
- package/dist/i18n/declaration.d.ts +18 -1
- package/dist/i18n/declaration.js +1 -1
- package/dist/i18n/declaration.js.map +1 -1
- package/dist/i18n/translation.cjs +3 -3
- package/dist/i18n/translation.cjs.map +1 -1
- package/dist/i18n/translation.js +289 -0
- package/dist/i18n/translation.js.map +1 -1
- package/dist/locales/ar/openhands.json +18 -1
- package/dist/locales/ca/openhands.json +18 -1
- package/dist/locales/de/openhands.json +18 -1
- package/dist/locales/en/openhands.json +18 -1
- package/dist/locales/es/openhands.json +18 -1
- package/dist/locales/fr/openhands.json +18 -1
- package/dist/locales/it/openhands.json +18 -1
- package/dist/locales/ja/openhands.json +18 -1
- package/dist/locales/ko-KR/openhands.json +18 -1
- package/dist/locales/no/openhands.json +18 -1
- package/dist/locales/pt/openhands.json +18 -1
- package/dist/locales/tr/openhands.json +18 -1
- package/dist/locales/uk/openhands.json +18 -1
- package/dist/locales/zh-CN/openhands.json +18 -1
- package/dist/locales/zh-TW/openhands.json +18 -1
- package/dist/manifests/automation-setup.cjs.map +1 -1
- package/dist/manifests/automation-setup.js.map +1 -1
- package/dist/manifests/manifest-local-validation.cjs.map +1 -1
- package/dist/manifests/manifest-local-validation.js.map +1 -1
- package/dist/manifests/manifest-validation.cjs +1 -1
- package/dist/manifests/manifest-validation.cjs.map +1 -1
- package/dist/manifests/manifest-validation.js +2 -2
- package/dist/manifests/manifest-validation.js.map +1 -1
- package/dist/node_modules/@openhands/extensions/automations/bundle-index.cjs +3670 -247
- package/dist/node_modules/@openhands/extensions/automations/bundle-index.cjs.map +1 -1
- package/dist/node_modules/@openhands/extensions/automations/bundle-index.js +26 -3
- package/dist/node_modules/@openhands/extensions/automations/bundle-index.js.map +1 -1
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-agents-md-maintainer/manifest.cjs +1 -1
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-agents-md-maintainer/manifest.cjs.map +1 -1
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-agents-md-maintainer/manifest.js +4 -1
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-agents-md-maintainer/manifest.js.map +1 -1
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-delivery-watchdog/manifest.cjs +2 -0
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-delivery-watchdog/manifest.cjs.map +1 -0
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-delivery-watchdog/manifest.js +79 -0
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-delivery-watchdog/manifest.js.map +1 -0
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-to-pr/manifest.cjs +1 -11
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-to-pr/manifest.cjs.map +1 -1
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-to-pr/manifest.js +25 -11
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-to-pr/manifest.js.map +1 -1
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-triage/manifest.cjs +2 -0
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-triage/manifest.cjs.map +1 -0
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-triage/manifest.js +72 -0
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-triage/manifest.js.map +1 -0
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-pr-reviewer/manifest.cjs +1 -10
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-pr-reviewer/manifest.cjs.map +1 -1
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-pr-reviewer/manifest.js +24 -11
- package/dist/node_modules/@openhands/extensions/automations/catalog/github-pr-reviewer/manifest.js.map +1 -1
- package/dist/node_modules/@openhands/extensions/automations/catalog/gitlab-issue-to-mr/manifest.cjs +12 -0
- package/dist/node_modules/@openhands/extensions/automations/catalog/gitlab-issue-to-mr/manifest.cjs.map +1 -0
- package/dist/node_modules/@openhands/extensions/automations/catalog/gitlab-issue-to-mr/manifest.js +115 -0
- package/dist/node_modules/@openhands/extensions/automations/catalog/gitlab-issue-to-mr/manifest.js.map +1 -0
- package/dist/node_modules/@openhands/extensions/automations/catalog-index.cjs +1 -1
- package/dist/node_modules/@openhands/extensions/automations/catalog-index.cjs.map +1 -1
- package/dist/node_modules/@openhands/extensions/automations/catalog-index.js +20 -14
- package/dist/node_modules/@openhands/extensions/automations/catalog-index.js.map +1 -1
- package/dist/node_modules/@openhands/extensions/integrations/catalog/reportportal.cjs +2 -0
- package/dist/node_modules/@openhands/extensions/integrations/catalog/reportportal.cjs.map +1 -0
- package/dist/node_modules/@openhands/extensions/integrations/catalog/reportportal.js +73 -0
- package/dist/node_modules/@openhands/extensions/integrations/catalog/reportportal.js.map +1 -0
- package/dist/node_modules/@openhands/extensions/integrations/catalog-index.cjs +1 -1
- package/dist/node_modules/@openhands/extensions/integrations/catalog-index.cjs.map +1 -1
- package/dist/node_modules/@openhands/extensions/integrations/catalog-index.js +9 -7
- package/dist/node_modules/@openhands/extensions/integrations/catalog-index.js.map +1 -1
- package/dist/node_modules/@openhands/extensions/skills/index.cjs +794 -227
- package/dist/node_modules/@openhands/extensions/skills/index.cjs.map +1 -1
- package/dist/node_modules/@openhands/extensions/skills/index.js +46 -6
- package/dist/node_modules/@openhands/extensions/skills/index.js.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/agent-server-compatibility.cjs +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/agent-server-compatibility.cjs.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/agent-server-compatibility.js +15 -8
- package/dist/node_modules/@openhands/typescript-client/dist/client/agent-server-compatibility.js.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/bash-client.cjs +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/bash-client.cjs.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/bash-client.js +11 -13
- package/dist/node_modules/@openhands/typescript-client/dist/client/bash-client.js.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/conversation-client.cjs +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/conversation-client.cjs.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/conversation-client.js +6 -0
- package/dist/node_modules/@openhands/typescript-client/dist/client/conversation-client.js.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/file-client.cjs +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/file-client.cjs.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/file-client.js +9 -12
- package/dist/node_modules/@openhands/typescript-client/dist/client/file-client.js.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/openhands-client.cjs +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/openhands-client.cjs.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/openhands-client.js +9 -10
- package/dist/node_modules/@openhands/typescript-client/dist/client/openhands-client.js.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/runtime-transport.cjs +2 -0
- package/dist/node_modules/@openhands/typescript-client/dist/client/runtime-transport.cjs.map +1 -0
- package/dist/node_modules/@openhands/typescript-client/dist/client/runtime-transport.js +41 -0
- package/dist/node_modules/@openhands/typescript-client/dist/client/runtime-transport.js.map +1 -0
- package/dist/node_modules/@openhands/typescript-client/dist/client/settings-client.cjs +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/settings-client.cjs.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/settings-client.js +2 -2
- package/dist/node_modules/@openhands/typescript-client/dist/client/settings-client.js.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/vscode-client.cjs +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/vscode-client.cjs.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/vscode-client.js +3 -6
- package/dist/node_modules/@openhands/typescript-client/dist/client/vscode-client.js.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/clients.cjs +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/clients.js +1 -2
- package/dist/node_modules/@openhands/typescript-client/dist/conversation/conversation-manager.cjs +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/conversation/conversation-manager.js +0 -1
- package/dist/node_modules/@openhands/typescript-client/dist/models/acp-providers.cjs +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/models/acp-providers.cjs.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/models/acp-providers.js +199 -61
- package/dist/node_modules/@openhands/typescript-client/dist/models/acp-providers.js.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/workspace/remote-workspace.cjs +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/workspace/remote-workspace.cjs.map +1 -1
- package/dist/node_modules/@openhands/typescript-client/dist/workspace/remote-workspace.js +9 -8
- package/dist/node_modules/@openhands/typescript-client/dist/workspace/remote-workspace.js.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.cjs.map +1 -1
- package/dist/package.js +3 -3
- package/dist/package.js.map +1 -1
- package/dist/routes/agent-settings.d.ts +11 -0
- package/dist/types/automation.cjs.map +1 -1
- package/dist/types/automation.d.ts +26 -2
- package/dist/types/automation.js.map +1 -1
- package/dist/utils/automation-disabled-reason.cjs +2 -0
- package/dist/utils/automation-disabled-reason.cjs.map +1 -0
- package/dist/utils/automation-disabled-reason.d.ts +32 -0
- package/dist/utils/automation-disabled-reason.js +19 -0
- package/dist/utils/automation-disabled-reason.js.map +1 -0
- package/dist/utils/parse-git-remote-url.cjs +1 -1
- package/dist/utils/parse-git-remote-url.cjs.map +1 -1
- package/dist/utils/parse-git-remote-url.d.ts +5 -1
- package/dist/utils/parse-git-remote-url.js +20 -13
- package/dist/utils/parse-git-remote-url.js.map +1 -1
- package/package.json +3 -3
- package/scripts/check-sdk-version-sync.mjs +79 -8
- package/scripts/dev-safe.mjs +14 -1
- package/scripts/docker-build.mjs +3 -0
- package/scripts/static-server.mjs +24 -7
- package/build/assets/acp-providers-DcY_mKYa.js +0 -1
- package/build/assets/active-backend-context-BnH4-L8h.js +0 -2
- package/build/assets/agent-server-conversation-service.api-jiCopc2E.js +0 -88
- package/build/assets/agent-settings-BF8hM6wb.js +0 -2
- package/build/assets/agent-settings-YPutBAeR.js +0 -1
- package/build/assets/automation-detail-BxXhb_m2.js +0 -3
- package/build/assets/automation-setup-route-qaJ6lLCf.js +0 -3
- package/build/assets/automations-list-DEPVNud3.js +0 -1
- package/build/assets/backend-form-modal-DxisQV_6.js +0 -1
- package/build/assets/conversation-edjzrv5w.js +0 -1
- package/build/assets/conversation-panel-edjzrv5w.js +0 -1
- package/build/assets/edit-automation-modal-D_JWSmxT.js +0 -1
- package/build/assets/empty-state-FK_s1B60.js +0 -1
- package/build/assets/git-repo-dropdown-BoDDWXfX.js +0 -1
- package/build/assets/home-BM5_Wz_R.js +0 -1
- package/build/assets/home-Ivfm7KC4.js +0 -1
- package/build/assets/home-automation-run-tooltip-B1N2uO9p.js +0 -1
- package/build/assets/index-home-td9BN2T3.js +0 -1
- package/build/assets/kebab-menu-item-content-SCJBwZnp.js +0 -1
- package/build/assets/link-external-BNZzCM1I.js +0 -1
- package/build/assets/llm-not-configured-banner-Dh_ajcM8.js +0 -27
- package/build/assets/llm-settings-CZaXYgtw.js +0 -1
- package/build/assets/manifest-bfeccb98.js +0 -1
- package/build/assets/mcp-CraxoXQv.js +0 -1
- package/build/assets/mcp-marketplace-utils-C7PYr293.js +0 -1
- package/build/assets/mcp-page-CteH_K0N.js +0 -9
- package/build/assets/onboarding-DXFBRqEy.js +0 -1
- package/build/assets/parse-git-remote-url-DpDM9bK6.js +0 -1
- package/build/assets/recommended-automations-launcher-DEHRKIAg.js +0 -1
- package/build/assets/root-CM6IZHlg.css +0 -1
- package/build/assets/root-DdFkodKW.js +0 -2
- package/build/assets/trash-Ckh7yCaB.js +0 -1
- package/build/assets/use-create-conversation-i_PdFdIs.js +0 -1
- package/build/assets/use-create-secret-CjWq0J4O.js +0 -1
- package/build/assets/use-manifest-capabilities-CdqYfUAC.js +0 -1
- package/build/assets/use-tracking-j4uo9VEW.js +0 -3
- package/build/assets/utils-BgLSZdyy.js +0 -1
- package/build/assets/vendor~entry.client~root~root-layout~index-home~home~conversation-panel~conversation~launch~b0x1wrp2-Z0lly3FE.js +0 -2
- package/build/assets/vendor~root~root-layout~index-home~home~conversation-panel~conversation~launch~extensions-h~inibp4qe-CnsDDzeE.js +0 -48
- package/dist/node_modules/@openhands/typescript-client/dist/client/desktop-client.cjs +0 -2
- package/dist/node_modules/@openhands/typescript-client/dist/client/desktop-client.cjs.map +0 -1
- package/dist/node_modules/@openhands/typescript-client/dist/client/desktop-client.js +0 -21
- package/dist/node_modules/@openhands/typescript-client/dist/client/desktop-client.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "無法使用的設定檔 ({{id}})",
|
|
2
3
|
"COMMON$PATTERN": "模式:",
|
|
3
4
|
"COMMON$INCLUDE": "包含:",
|
|
4
5
|
"COMMON$RESULTS": "結果 ({{count}})",
|
|
@@ -321,6 +322,13 @@
|
|
|
321
322
|
"SETTINGS$AGENT_PAGE_DESCRIPTION": "在內建的 OpenHands 代理和外部 ACP(Agent Client Protocol)子處理程序(如 Claude Code、Codex 或 Gemini CLI)之間進行選擇。",
|
|
322
323
|
"SETTINGS$AGENT_TYPE_OPENHANDS": "OpenHands",
|
|
323
324
|
"SETTINGS$AGENT_TYPE_ACP": "ACP(外部子處理程序)",
|
|
325
|
+
"SETTINGS$AGENT_PROFILE_MCP": "MCP 伺服器",
|
|
326
|
+
"SETTINGS$AGENT_PROFILE_MCP_ALL": "全部伺服器",
|
|
327
|
+
"SETTINGS$AGENT_PROFILE_MCP_CHOOSE": "自選伺服器",
|
|
328
|
+
"SETTINGS$AGENT_PROFILE_MCP_ALL_HINT": "該代理可以使用你設定的所有 MCP 伺服器,其中也包括能夠進行修改的伺服器。",
|
|
329
|
+
"SETTINGS$AGENT_PROFILE_MCP_CHOOSE_HINT": "該代理只能使用你選擇的伺服器。其他 MCP 伺服器對它不可見。",
|
|
330
|
+
"SETTINGS$AGENT_PROFILE_MCP_NONE": "你還沒有設定任何 MCP 伺服器。",
|
|
331
|
+
"SETTINGS$AGENT_PROFILE_MCP_DANGLING": "{{names}} 已不再設定。保持選取並儲存會導致該代理無法啟動——請取消選取或重新新增該伺服器。",
|
|
324
332
|
"SETTINGS$AGENT_PRESET": "預設集",
|
|
325
333
|
"SETTINGS$AGENT_PRESET_CUSTOM": "自訂",
|
|
326
334
|
"SETTINGS$AGENT_COMMAND": "命令",
|
|
@@ -840,6 +848,9 @@
|
|
|
840
848
|
"AUTOMATIONS$DETAIL$PHASE_PREPARING": "正在準備",
|
|
841
849
|
"AUTOMATIONS$DETAIL$PHASE_RUNNING_AGENT": "代理執行中",
|
|
842
850
|
"AUTOMATIONS$DETAIL$INACTIVE": "非作用中",
|
|
851
|
+
"AUTOMATIONS$DETAIL$DISABLED_MANUAL": "已手動關閉",
|
|
852
|
+
"AUTOMATIONS$DETAIL$DISABLED_REASON_HEADING": "暫停原因",
|
|
853
|
+
"AUTOMATIONS$DETAIL$DISABLED_AT": "{{time}} 暫停",
|
|
843
854
|
"AUTOMATIONS$DETAIL$LAST_RUN": "上次執行",
|
|
844
855
|
"AUTOMATIONS$DETAIL$LOAD_MORE_RUNS": "載入更多",
|
|
845
856
|
"AUTOMATIONS$DETAIL$EXPORT_JSON": "匯出 JSON",
|
|
@@ -2455,5 +2466,11 @@
|
|
|
2455
2466
|
"SETTINGS$PROVIDER_CONNECTION_UPDATED": "已更新連線「{{name}}」",
|
|
2456
2467
|
"BUTTON$VIEW_FULL_SIZE_IMAGE": "檢視原始尺寸圖片",
|
|
2457
2468
|
"BUTTON$REMOVE_IMAGE": "移除圖片",
|
|
2458
|
-
"IMAGE$FULL_SIZE_PREVIEW": "原始尺寸圖片預覽"
|
|
2469
|
+
"IMAGE$FULL_SIZE_PREVIEW": "原始尺寸圖片預覽",
|
|
2470
|
+
"SETTINGS$AGENT_PROFILE_SECRETS": "機密",
|
|
2471
|
+
"SETTINGS$AGENT_PROFILE_SECRETS_ALL": "不由設定檔限制",
|
|
2472
|
+
"SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "自選機密",
|
|
2473
|
+
"SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "允許使用代理啟動時提供的機密。用於自動化時,請在此設定檔中明確選擇機密。",
|
|
2474
|
+
"SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "該代理只能使用你選擇的機密,請確認它需要的都已包含。外部代理需要自身的供應商憑證才能登入。",
|
|
2475
|
+
"SETTINGS$AGENT_PROFILE_SECRETS_NONE": "你還沒有儲存任何機密。"
|
|
2459
2476
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"automation-setup.cjs","names":[],"sources":["../../src/manifests/automation-setup.ts"],"sourcesContent":["/**\n * The only module that knows a setup entry configures an *automation*.\n *\n * The published contract states just what varies between entries; everything\n * derivable is the host's to generate. That derivation is here, and nowhere\n * else, so the rest of `src/manifests/` stays about forms rather than about\n * automations.\n *\n * The algorithm mirrors `tests/test_automation_setup.py::_render_payload` in\n * `OpenHands/extensions`, which is the authoritative reference: it produces the\n * request bodies published in that repository's contract fixtures, and those\n * were verified against the live service. The create model is `extra=\"forbid\"`,\n * so any divergence is a hard 422 rather than a dropped field.\n */\n\nimport i18n from \"#/i18n\";\nimport { I18nKey } from \"#/i18n/declaration\";\nimport { findAutomationCommand } from \"#/utils/automation-catalog\";\nimport { getAutomationEndpoint } from \"./automation-interface\";\nimport {\n actionKinds,\n collectFields,\n fieldText,\n fieldValues,\n} from \"./manifest-local-validation\";\nimport { interpolateText, interpolateValue } from \"./manifest-template\";\nimport type {\n SetupActionKind,\n SetupBlock,\n SetupBundleConfigValue,\n SetupEntry,\n SetupFormValues,\n SetupRequestBody,\n SetupTriggerKind,\n} from \"./types\";\n\n/**\n * The creation endpoint a derived draft would be posted to. Resolved on call\n * rather than at import, because the endpoint is the interface manifest's and\n * this module loads whether or not one was admitted.\n *\n * A bundle entry is created through the raw endpoint, because what it sends is\n * a tarball it uploaded rather than arguments to a preset. Called without an\n * entry - as the import path does - it answers for a prompt.\n */\nexport function automationCreateEndpoint(\n entry?: SetupEntry,\n selectedAction?: string | null,\n): string {\n const kind = selectedActionKind(entry, selectedAction);\n if (kind === \"plugin\") return getAutomationEndpoint(\"createPlugin\");\n if (kind === \"upload\" || kind === \"bundle\") {\n return requireBundleEndpoint(\"createBundle\");\n }\n return getAutomationEndpoint(\"createPrompt\");\n}\n\n/** Where a bundle's tarball is uploaded, before the create call. */\nexport function automationUploadEndpoint(): string {\n return requireBundleEndpoint(\"uploads\");\n}\n\n/** The endpoints a bundle entry cannot be created without. */\nconst BUNDLE_ENDPOINTS = [\"createBundle\", \"uploads\"] as const;\n\n/**\n * An endpoint only a bundle needs. The interface manifest may predate bundles,\n * and the host holds no path of its own to fall back to, so this is where that\n * runs out rather than somewhere deep in a request.\n */\nfunction requireBundleEndpoint(\n name: (typeof BUNDLE_ENDPOINTS)[number],\n): string {\n const path = getAutomationEndpoint(name);\n if (!path) {\n throw new Error(\n `The published automation interface declares no '${name}' endpoint, ` +\n \"so this deployment cannot create an automation from a script bundle.\",\n );\n }\n return path;\n}\n\n/**\n * The endpoints this entry needs that the published interface does not declare.\n *\n * Asked before the form renders rather than discovered at the moment of\n * creating: a pinned package that predates bundles can never answer, and the\n * dialog can say so while nothing has been filled in yet. Empty for every\n * entry that is not a bundle, which needs nothing beyond what the block has\n * always declared.\n */\nfunction missingEndpointsForAction(kind: SetupActionKind | \"bundle\"): string[] {\n return kind === \"upload\" || kind === \"bundle\"\n ? BUNDLE_ENDPOINTS.filter((name) => !getAutomationEndpoint(name))\n : [];\n}\n\nexport function missingCreateEndpoints(\n entry: SetupEntry,\n selectedAction?: string | null,\n): string[] {\n if (isBundleEntry(entry)) return missingEndpointsForAction(\"bundle\");\n if (!entry.setup.actions) return [];\n\n if (selectedAction && selectedAction in entry.setup.actions) {\n return missingEndpointsForAction(selectedAction as SetupActionKind);\n }\n\n const missingByAction = actionKinds(entry.setup).map(\n missingEndpointsForAction,\n );\n if (missingByAction.some((missing) => missing.length === 0)) return [];\n return Array.from(new Set(missingByAction.flat()));\n}\n\n/** Whether this entry ships a script tarball instead of a prompt. */\nexport function isBundleEntry(entry: SetupEntry): boolean {\n return entry.setup.mode === \"direct\" && entry.setup.bundle !== undefined;\n}\n\nexport function isUploadAction(\n entry: SetupEntry,\n selectedAction?: string | null,\n): boolean {\n return selectedActionKind(entry, selectedAction) === \"upload\";\n}\n\nexport function selectedActionKind(\n entry: SetupEntry | undefined,\n selectedAction?: string | null,\n): SetupActionKind | \"bundle\" | \"prompt\" {\n if (!entry) return \"prompt\";\n if (isBundleEntry(entry)) return \"bundle\";\n const actions = entry.setup.actions ?? {};\n if (selectedAction && selectedAction in actions) {\n return selectedAction as SetupActionKind;\n }\n const keys = actionKinds(entry.setup);\n if (keys.length === 1) return keys[0];\n return \"prompt\";\n}\n\n/**\n * The `tarball_path` a preflight draft carries.\n *\n * Preflight runs on every field blur and the upload happens once, at submit,\n * so there is no real path to send yet. The service checks this field's scheme\n * at preflight and its ownership only at creation, so a well-formed stand-in\n * validates exactly what preflight is for - the rest of the body - without\n * uploading an archive per keystroke.\n */\nexport const PREFLIGHT_TARBALL_PATH =\n \"oh-internal://uploads/00000000-0000-0000-0000-000000000000\";\n\n/**\n * Trigger properties a form field may fill, per trigger kind. A field under a\n * trigger kind whose name is listed here fills the trigger property of the same\n * name; anything else there, such as a phrase to match, is an input to `filter`.\n */\nconst TRIGGER_PROPERTIES: Record<SetupTriggerKind, readonly string[]> = {\n cron: [\"schedule\", \"timezone\"],\n event: [\"source\", \"on\"],\n};\n\nconst OPTIONAL_CREATE_PROPERTIES = [\"model\", \"timeout\"] as const;\n\n/**\n * Repository properties a form field may fill, read the same way\n * {@link TRIGGER_PROPERTIES} is: a field whose name is listed here fills the\n * `repos[]` property of the same name. `url` and `provider` are absent because\n * neither is answered by a field of its own — they come from the repo-picker's\n * value and from its declared provider.\n *\n * These two lists are the only field names the derivation knows; everything\n * else it reads it finds by field type. An entry that wants a base branch in\n * its request therefore has to name that field `ref`.\n */\nconst REPO_PROPERTIES: readonly string[] = [\"ref\"];\n\nconst FORM_PLACEHOLDER_PATTERN = /\\{\\{form\\.([A-Za-z0-9_.]+)\\}\\}/g;\n\n/** The repository input, which supplies `repos` and an event trigger's source. */\nfunction findRepoPickerField(\n setup: SetupBlock,\n selectedAction?: string | null,\n) {\n const match = Object.entries(collectFields(setup, null, selectedAction)).find(\n ([, field]) => field.type === \"repo-picker\",\n );\n return match ? { name: match[0], field: match[1] } : null;\n}\n\n/** Every repository the form collected, whether the picker takes one or many. */\nfunction repositories(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string[] {\n const picker = findRepoPickerField(setup, selectedAction);\n return picker ? fieldValues(values[picker.name]) : [];\n}\n\n/**\n * The created automation's name.\n *\n * One repository is worth naming; several are not, so the count stands in\n * rather than a list of names that would not fit.\n */\nfunction deriveName(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string {\n if (\n \"name\" in collectFields(entry.setup, null, selectedAction) &&\n \"name\" in values\n ) {\n return fieldText(values.name);\n }\n\n const repos = repositories(entry.setup, values, selectedAction);\n if (repos.length === 0) return entry.name;\n if (repos.length === 1) return `${entry.name} - ${repos[0]}`;\n // The count is the one word here the host writes rather than reads off the\n // entry, so it is translated. There is no translator to pass in: the\n // derivation runs from a memo, an upload and a test alike.\n const count = i18n.t(I18nKey.SETUP$REPOSITORY_COUNT, {\n total: repos.length,\n });\n return `${entry.name} - ${count}`;\n}\n\n/** The active trigger kind for a direct entry, with its fields. */\nfunction getTrigger(setup: SetupBlock, selectedTrigger?: string | null) {\n const triggers = setup.form.triggers ?? {};\n const entries = Object.entries(triggers);\n if (entries.length === 0) return null;\n const match =\n selectedTrigger && selectedTrigger in triggers\n ? [selectedTrigger, triggers[selectedTrigger as SetupTriggerKind]]\n : entries.length === 1\n ? entries[0]\n : null;\n if (!match) return null;\n const [kind, fields] = match;\n return { kind: kind as SetupTriggerKind, fields: fields ?? {} };\n}\n\nfunction hasPayloadValue(\n value: SetupRequestBody[string] | undefined,\n): value is SetupRequestBody[string] {\n return value !== undefined && value !== null && value !== \"\";\n}\n\nfunction fieldPayloadValue(\n fieldType: string | undefined,\n value: SetupFormValues[string],\n) {\n if (fieldType === \"number\") {\n const text = fieldText(value);\n return text === \"\" ? undefined : Number(text);\n }\n return fieldText(value);\n}\n\nfunction optionalCreateProperties(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n return Object.fromEntries(\n OPTIONAL_CREATE_PROPERTIES.flatMap((name) => {\n const field = collectFields(setup, null, selectedAction)[name];\n const value = fieldPayloadValue(field?.type, values[name]);\n return hasPayloadValue(value) ? [[name, value]] : [];\n }),\n ) as SetupRequestBody;\n}\n\nfunction repositoryProperty(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n const repoPicker = findRepoPickerField(setup, selectedAction);\n const repos = repositories(setup, values, selectedAction);\n if (repos.length === 0 || !repoPicker?.field.provider) return {};\n\n const declared = REPO_PROPERTIES.filter((name) =>\n hasPayloadValue(fieldText(values[name])),\n );\n return {\n repos: repos.map((url) => ({\n url,\n ...Object.fromEntries(\n declared.map((name) => [name, fieldText(values[name])]),\n ),\n provider: repoPicker.field.provider as string,\n })),\n };\n}\n\nfunction parsePluginSources(\n value: SetupRequestBody[string],\n): SetupRequestBody[string] {\n if (!hasPayloadValue(value) || typeof value !== \"string\") return value;\n const trimmed = value.trim();\n if (!trimmed) return [];\n if (trimmed.startsWith(\"[\")) {\n try {\n return JSON.parse(trimmed) as SetupRequestBody[string];\n } catch {\n return [{ source: trimmed }];\n }\n }\n return trimmed\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => {\n const [source, ...parts] = line.split(/\\s+/);\n const plugin: Record<string, string> = { source };\n parts.forEach((part) => {\n const [key, ...raw] = part.split(\"=\");\n const valuePart = raw.join(\"=\");\n if ((key === \"ref\" || key === \"repo_path\") && valuePart) {\n plugin[key] = valuePart;\n }\n });\n return plugin;\n });\n}\n\n/**\n * The create request body these form values produce.\n *\n * No entry declares it: `name` comes from the entry, `repos` from the\n * repo-picker field and its declared provider, and `trigger` from the key and\n * fields under `form.triggers`. Only `prompt` and an event `filter` are\n * declared, because only they cannot be read off the form.\n *\n * Returns null for an assisted entry, which hands setup to a conversation\n * instead of sending a body.\n */\nexport function buildCreatePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n /** Bundle/upload entries only: what the upload returned. */\n tarballPath: string = PREFLIGHT_TARBALL_PATH,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const { setup } = entry;\n if (setup.mode !== \"direct\") return null;\n if (setup.bundle) {\n return buildBundlePayload(entry, values, tarballPath, selectedTrigger);\n }\n if (setup.actions) {\n return buildActionPayload(\n entry,\n values,\n tarballPath,\n selectedTrigger,\n selectedActionKey,\n );\n }\n if (!setup.prompt) return null;\n\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n prompt: interpolateText(setup.prompt, scope),\n ...optionalCreateProperties(setup, values),\n ...repositoryProperty(setup, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\nfunction buildActionPayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const kind = selectedActionKind(entry, selectedActionKey);\n if (kind === \"bundle\") return null;\n\n const setup = entry.setup;\n const scope = { form: values, automation: entry };\n const payload: SetupRequestBody = {\n name: deriveName(entry, values, selectedActionKey),\n };\n\n const trigger = buildTrigger(\n entry,\n values,\n selectedTrigger,\n selectedActionKey,\n );\n if (trigger) payload.trigger = trigger;\n\n Object.assign(\n payload,\n optionalCreateProperties(setup, values, selectedActionKey),\n );\n\n if (kind === \"upload\") {\n const action = setup.actions?.upload;\n if (!action) return null;\n payload.tarball_path =\n tarballPath || interpolateText(action.tarballPath, scope);\n payload.entrypoint = interpolateText(action.entrypoint, scope);\n if (action.setupScript) {\n const setupScript = interpolateText(action.setupScript, scope);\n if (hasPayloadValue(setupScript)) payload.setup_script_path = setupScript;\n }\n return payload;\n }\n\n const action =\n kind === \"plugin\" ? setup.actions?.plugin : setup.actions?.prompt;\n if (!action) return null;\n payload.prompt = interpolateText(action.prompt, scope);\n Object.assign(payload, repositoryProperty(setup, values, selectedActionKey));\n\n if (kind === \"plugin\") {\n const pluginAction = setup.actions?.plugin;\n if (!pluginAction) return null;\n payload.plugins = parsePluginSources(\n interpolateValue(pluginAction.plugins, scope),\n );\n }\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\n/**\n * The `template` provenance a prompt entry sends, or undefined for one that\n * publishes no version.\n *\n * It is what lets the service recognise a second setup of the same entry as\n * the automation it already holds. The bundle path derives the same block from\n * the config the bundle declares; a prompt entry declares none, so the answer\n * to \"what was this created from\" is the form as submitted.\n */\nfunction buildTemplate(\n entry: SetupEntry,\n values: SetupFormValues,\n): SetupRequestBody | undefined {\n if (!entry.version) return undefined;\n return {\n id: entry.id,\n version: entry.version,\n config: { ...values } as SetupRequestBody,\n };\n}\n\n/**\n * The `trigger` object, read off the key and fields under `form.triggers`.\n *\n * Identical for both kinds of direct entry: only the create endpoint and what\n * the automation is told to do differ between a prompt and a bundle.\n */\nfunction buildTrigger(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | undefined {\n const trigger = getTrigger(entry.setup, selectedTrigger);\n if (!trigger) return undefined;\n\n const properties = TRIGGER_PROPERTIES[trigger.kind];\n const declared = Object.keys(trigger.fields).filter((name) =>\n properties.includes(name),\n );\n const repoPicker = findRepoPickerField(entry.setup, selectedAction);\n const derived = Object.fromEntries(\n declared\n .map((name) => [name, fieldText(values[name])])\n .filter(([, value]) => hasPayloadValue(value)),\n );\n\n const filter = entry.setup.filter\n ? interpolateText(entry.setup.filter, {\n form: values,\n automation: entry,\n })\n : undefined;\n\n return {\n type: trigger.kind,\n ...derived,\n ...(trigger.kind === \"event\" && {\n ...(!(\"source\" in derived) && {\n source: repoPicker?.field.provider ?? \"\",\n }),\n ...(hasPayloadValue(filter) && { filter }),\n }),\n };\n}\n\n/**\n * The raw create body a bundle entry produces.\n *\n * `tarball_path` is the one value neither declared nor derived: the host packs\n * and uploads the bundle first, and creates from what came back. `template` is\n * the provenance that makes enabling the same entry twice return the\n * automation that already exists rather than a second one.\n *\n * There is no `repos`: the raw endpoint has no such field, and a bundle's\n * script fetches what it needs itself.\n */\nfunction buildBundlePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n): SetupRequestBody {\n const bundle = entry.setup.bundle!;\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n payload.tarball_path = tarballPath;\n payload.entrypoint = bundle.entrypoint;\n if (bundle.setupScript) payload.setup_script_path = bundle.setupScript;\n if (bundle.timeout !== undefined) payload.timeout = bundle.timeout;\n payload.template = {\n id: entry.id,\n version: bundle.version,\n config: interpolateConfig(bundle.config, scope) as SetupRequestBody,\n };\n\n return payload;\n}\n\n/**\n * Placeholder substitution over the config tree. Only string leaves are\n * templated; a number, a boolean or a null is written through as itself, so an\n * entry can state a value the script reads as the type it expects.\n */\nfunction interpolateConfig(\n node: SetupBundleConfigValue,\n scope: Parameters<typeof interpolateText>[1],\n): SetupBundleConfigValue {\n if (typeof node === \"string\") {\n return interpolateValue(node, scope);\n }\n if (Array.isArray(node)) {\n return node.map((item) => interpolateConfig(item, scope));\n }\n if (typeof node === \"object\" && node !== null) {\n return Object.fromEntries(\n Object.entries(node).map(([key, value]) => [\n key,\n interpolateConfig(value, scope),\n ]),\n );\n }\n return node;\n}\n\n/**\n * The preflight body the host sends. The same shape for every entry, so no\n * entry declares it.\n */\nexport function buildPreflightBody(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | null {\n const draft = buildCreatePayload(\n entry,\n values,\n PREFLIGHT_TARBALL_PATH,\n selectedTrigger,\n selectedAction,\n );\n if (!draft) return null;\n\n return {\n automationId: entry.id,\n endpoint: automationCreateEndpoint(entry, selectedAction),\n draft,\n };\n}\n\n/**\n * What an assisted entry sends into the conversation that finishes setup.\n *\n * The command is not declared either: it lives once, in the owning skill's own\n * `triggers` frontmatter, and the skill defaults to the entry's id. An entry\n * whose skill is invoked by description rather than by command contributes\n * nothing here, and the answers alone open the conversation.\n */\nexport function buildAssistedMessage(\n entry: SetupEntry,\n values: SetupFormValues,\n): string {\n const command = findAutomationCommand(entry);\n const message = entry.setup.message\n ? interpolateText(entry.setup.message, { form: values, automation: entry })\n : \"\";\n\n return [command, message].filter(Boolean).join(\"\\n\\n\");\n}\n\nfunction collectPlaceholderFields(value: string): string[] {\n const names = Array.from(\n value.matchAll(FORM_PLACEHOLDER_PATTERN),\n (match) => match[1],\n );\n return Array.from(new Set(names));\n}\n\n/**\n * Which form fields built each payload path.\n *\n * Preflight and the create endpoint reject a draft by payload path, and the\n * host has to turn that back into a highlighted input. Building the body with\n * each field standing in for its own value recovers the mapping exactly, so an\n * entry does not declare it.\n */\nexport function deriveErrorMap(\n entry: SetupEntry,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): Record<string, string[]> {\n const mapping: Record<string, string[]> = {};\n\n const triggerEntries = Object.keys(entry.setup.form.triggers ?? {});\n const triggerVariants = selectedTrigger\n ? [selectedTrigger]\n : triggerEntries.length > 1\n ? triggerEntries\n : [null];\n\n const actionEntries = actionKinds(entry.setup);\n const actionVariants = selectedAction\n ? [selectedAction]\n : actionEntries.length > 1\n ? actionEntries\n : [null];\n\n const walk = (node: unknown, path: string): void => {\n if (Array.isArray(node)) {\n node.forEach((item, index) => walk(item, `${path}[${index}]`));\n return;\n }\n if (typeof node === \"object\" && node !== null) {\n Object.entries(node).forEach(([key, item]) =>\n walk(item, path ? `${path}.${key}` : key),\n );\n return;\n }\n if (typeof node === \"string\") {\n const names = collectPlaceholderFields(node);\n if (names.length > 0) {\n mapping[path] = Array.from(\n new Set([...(mapping[path] ?? []), ...names]),\n );\n }\n }\n };\n\n actionVariants.forEach((actionVariant) => {\n triggerVariants.forEach((triggerVariant) => {\n const names = Object.keys(\n collectFields(entry.setup, triggerVariant, actionVariant),\n );\n const variantStandIns = Object.fromEntries(\n names.map((name) => [name, `{{form.${name}}}`]),\n );\n const template = buildCreatePayload(\n entry,\n variantStandIns,\n actionVariant === \"upload\" ? \"\" : PREFLIGHT_TARBALL_PATH,\n triggerVariant,\n actionVariant,\n );\n if (template) walk(template, \"\");\n });\n });\n return mapping;\n}\n"],"mappings":"gNA6CA,SAAgB,EACd,EACA,EACQ,CACR,IAAM,EAAO,EAAmB,EAAO,CAAc,EAKrD,OAJI,IAAS,SAAiB,EAAA,sBAAsB,cAAc,EAC9D,IAAS,UAAY,IAAS,SACzB,EAAsB,cAAc,EAEtC,EAAA,sBAAsB,cAAc,CAC7C,CAGA,SAAgB,GAAmC,CACjD,OAAO,EAAsB,SAAS,CACxC,CAUA,SAAS,EACP,EACQ,CACR,IAAM,EAAO,EAAA,sBAAsB,CAAI,EACvC,GAAI,CAAC,EACH,MAAU,MACR,mDAAmD,EAAK,iFAE1D,EAEF,OAAO,CACT,CAoCA,SAAgB,EAAc,EAA4B,CACxD,OAAO,EAAM,MAAM,OAAS,UAAY,EAAM,MAAM,SAAW,IAAA,EACjE,CASA,SAAgB,EACd,EACA,EACuC,CACvC,GAAI,CAAC,EAAO,MAAO,SACnB,GAAI,EAAc,CAAK,EAAG,MAAO,SACjC,IAAM,EAAU,EAAM,MAAM,SAAW,CAAC,EACxC,GAAI,GAAkB,KAAkB,EACtC,OAAO,EAET,IAAM,EAAO,EAAA,YAAY,EAAM,KAAK,EAEpC,OADI,EAAK,SAAW,EAAU,EAAK,GAC5B,QACT"}
|
|
1
|
+
{"version":3,"file":"automation-setup.cjs","names":[],"sources":["../../src/manifests/automation-setup.ts"],"sourcesContent":["/**\n * The only module that knows a setup entry configures an *automation*.\n *\n * The published contract states just what varies between entries; everything\n * derivable is the host's to generate. That derivation is here, and nowhere\n * else, so the rest of `src/manifests/` stays about forms rather than about\n * automations.\n *\n * The algorithm mirrors `tests/test_automation_setup.py::_render_payload` in\n * `OpenHands/extensions`, which is the authoritative reference: it produces the\n * request bodies published in that repository's contract fixtures, and those\n * were verified against the live service. The create model is `extra=\"forbid\"`,\n * so any divergence is a hard 422 rather than a dropped field.\n */\n\nimport i18n from \"#/i18n\";\nimport { I18nKey } from \"#/i18n/declaration\";\nimport { findAutomationCommand } from \"#/utils/automation-catalog\";\nimport { getAutomationEndpoint } from \"./automation-interface\";\nimport {\n actionKinds,\n collectFields,\n fieldText,\n fieldValues,\n} from \"./manifest-local-validation\";\nimport { interpolateText, interpolateValue } from \"./manifest-template\";\nimport type {\n SetupActionKind,\n SetupBlock,\n SetupBundleConfigValue,\n SetupEntry,\n SetupFormValues,\n SetupRequestBody,\n SetupTriggerKind,\n} from \"./types\";\n\n/**\n * The creation endpoint a derived draft would be posted to. Resolved on call\n * rather than at import, because the endpoint is the interface manifest's and\n * this module loads whether or not one was admitted.\n *\n * A bundle entry is created through the raw endpoint, because what it sends is\n * a tarball it uploaded rather than arguments to a preset. Called without an\n * entry - as the import path does - it answers for a prompt.\n */\nexport function automationCreateEndpoint(\n entry?: SetupEntry,\n selectedAction?: string | null,\n): string {\n const kind = selectedActionKind(entry, selectedAction);\n if (kind === \"plugin\") return getAutomationEndpoint(\"createPlugin\");\n if (kind === \"upload\" || kind === \"bundle\") {\n return requireBundleEndpoint(\"createBundle\");\n }\n return getAutomationEndpoint(\"createPrompt\");\n}\n\n/** Where a bundle's tarball is uploaded, before the create call. */\nexport function automationUploadEndpoint(): string {\n return requireBundleEndpoint(\"uploads\");\n}\n\n/** The endpoints a bundle entry cannot be created without. */\nconst BUNDLE_ENDPOINTS = [\"createBundle\", \"uploads\"] as const;\n\n/**\n * An endpoint only a bundle needs. The interface manifest may predate bundles,\n * and the host holds no path of its own to fall back to, so this is where that\n * runs out rather than somewhere deep in a request.\n */\nfunction requireBundleEndpoint(\n name: (typeof BUNDLE_ENDPOINTS)[number],\n): string {\n const path = getAutomationEndpoint(name);\n if (!path) {\n throw new Error(\n `The published automation interface declares no '${name}' endpoint, ` +\n \"so this deployment cannot create an automation from a script bundle.\",\n );\n }\n return path;\n}\n\n/**\n * The endpoints this entry needs that the published interface does not declare.\n *\n * Asked before the form renders rather than discovered at the moment of\n * creating: a pinned package that predates bundles can never answer, and the\n * dialog can say so while nothing has been filled in yet. Empty for every\n * entry that is not a bundle, which needs nothing beyond what the block has\n * always declared.\n */\nfunction missingEndpointsForAction(kind: SetupActionKind | \"bundle\"): string[] {\n return kind === \"upload\" || kind === \"bundle\"\n ? BUNDLE_ENDPOINTS.filter((name) => !getAutomationEndpoint(name))\n : [];\n}\n\nexport function missingCreateEndpoints(\n entry: SetupEntry,\n selectedAction?: string | null,\n): string[] {\n if (isBundleEntry(entry)) return missingEndpointsForAction(\"bundle\");\n if (!entry.setup.actions) return [];\n\n if (selectedAction && selectedAction in entry.setup.actions) {\n return missingEndpointsForAction(selectedAction as SetupActionKind);\n }\n\n const missingByAction = actionKinds(entry.setup).map(\n missingEndpointsForAction,\n );\n if (missingByAction.some((missing) => missing.length === 0)) return [];\n return Array.from(new Set(missingByAction.flat()));\n}\n\n/** Whether this entry ships a script tarball instead of a prompt. */\nexport function isBundleEntry(entry: SetupEntry): boolean {\n return entry.setup.mode === \"direct\" && entry.setup.bundle !== undefined;\n}\n\nexport function isUploadAction(\n entry: SetupEntry,\n selectedAction?: string | null,\n): boolean {\n return selectedActionKind(entry, selectedAction) === \"upload\";\n}\n\nexport function selectedActionKind(\n entry: SetupEntry | undefined,\n selectedAction?: string | null,\n): SetupActionKind | \"bundle\" | \"prompt\" {\n if (!entry) return \"prompt\";\n if (isBundleEntry(entry)) return \"bundle\";\n const actions = entry.setup.actions ?? {};\n if (selectedAction && selectedAction in actions) {\n return selectedAction as SetupActionKind;\n }\n const keys = actionKinds(entry.setup);\n if (keys.length === 1) return keys[0];\n return \"prompt\";\n}\n\n/**\n * The `tarball_path` a preflight draft carries.\n *\n * Preflight runs on every field blur and the upload happens once, at submit,\n * so there is no real path to send yet. The service checks this field's scheme\n * at preflight and its ownership only at creation, so a well-formed stand-in\n * validates exactly what preflight is for - the rest of the body - without\n * uploading an archive per keystroke.\n */\nexport const PREFLIGHT_TARBALL_PATH =\n \"oh-internal://uploads/00000000-0000-0000-0000-000000000000\";\n\n/**\n * Trigger properties a form field may fill, per trigger kind. A field under a\n * trigger kind whose name is listed here fills the trigger property of the same\n * name; anything else there, such as a phrase to match, is an input to `filter`.\n */\nconst TRIGGER_PROPERTIES: Record<SetupTriggerKind, readonly string[]> = {\n cron: [\"schedule\", \"timezone\"],\n event: [\"source\", \"on\"],\n};\n\nconst OPTIONAL_CREATE_PROPERTIES = [\n \"model\",\n \"timeout\",\n \"agent_profile_id\",\n] as const;\n\n/**\n * Repository properties a form field may fill, read the same way\n * {@link TRIGGER_PROPERTIES} is: a field whose name is listed here fills the\n * `repos[]` property of the same name. `url` and `provider` are absent because\n * neither is answered by a field of its own — they come from the repo-picker's\n * value and from its declared provider.\n *\n * These two lists are the only field names the derivation knows; everything\n * else it reads it finds by field type. An entry that wants a base branch in\n * its request therefore has to name that field `ref`.\n */\nconst REPO_PROPERTIES: readonly string[] = [\"ref\"];\n\nconst FORM_PLACEHOLDER_PATTERN = /\\{\\{form\\.([A-Za-z0-9_.]+)\\}\\}/g;\n\n/** The repository input, which supplies `repos` and an event trigger's source. */\nfunction findRepoPickerField(\n setup: SetupBlock,\n selectedAction?: string | null,\n) {\n const match = Object.entries(collectFields(setup, null, selectedAction)).find(\n ([, field]) => field.type === \"repo-picker\",\n );\n return match ? { name: match[0], field: match[1] } : null;\n}\n\n/** Every repository the form collected, whether the picker takes one or many. */\nfunction repositories(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string[] {\n const picker = findRepoPickerField(setup, selectedAction);\n return picker ? fieldValues(values[picker.name]) : [];\n}\n\n/**\n * The created automation's name.\n *\n * One repository is worth naming; several are not, so the count stands in\n * rather than a list of names that would not fit.\n */\nfunction deriveName(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string {\n if (\n \"name\" in collectFields(entry.setup, null, selectedAction) &&\n \"name\" in values\n ) {\n return fieldText(values.name);\n }\n\n const repos = repositories(entry.setup, values, selectedAction);\n if (repos.length === 0) return entry.name;\n if (repos.length === 1) return `${entry.name} - ${repos[0]}`;\n // The count is the one word here the host writes rather than reads off the\n // entry, so it is translated. There is no translator to pass in: the\n // derivation runs from a memo, an upload and a test alike.\n const count = i18n.t(I18nKey.SETUP$REPOSITORY_COUNT, {\n total: repos.length,\n });\n return `${entry.name} - ${count}`;\n}\n\n/** The active trigger kind for a direct entry, with its fields. */\nfunction getTrigger(setup: SetupBlock, selectedTrigger?: string | null) {\n const triggers = setup.form.triggers ?? {};\n const entries = Object.entries(triggers);\n if (entries.length === 0) return null;\n const match =\n selectedTrigger && selectedTrigger in triggers\n ? [selectedTrigger, triggers[selectedTrigger as SetupTriggerKind]]\n : entries.length === 1\n ? entries[0]\n : null;\n if (!match) return null;\n const [kind, fields] = match;\n return { kind: kind as SetupTriggerKind, fields: fields ?? {} };\n}\n\nfunction hasPayloadValue(\n value: SetupRequestBody[string] | undefined,\n): value is SetupRequestBody[string] {\n return value !== undefined && value !== null && value !== \"\";\n}\n\nfunction fieldPayloadValue(\n fieldType: string | undefined,\n value: SetupFormValues[string],\n) {\n if (fieldType === \"number\") {\n const text = fieldText(value);\n return text === \"\" ? undefined : Number(text);\n }\n return fieldText(value);\n}\n\nfunction optionalCreateProperties(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n return Object.fromEntries(\n OPTIONAL_CREATE_PROPERTIES.flatMap((name) => {\n if (name === \"model\" && values.agent_profile_id) return [];\n const field = collectFields(setup, null, selectedAction)[name];\n const value = fieldPayloadValue(field?.type, values[name]);\n return hasPayloadValue(value) ? [[name, value]] : [];\n }),\n ) as SetupRequestBody;\n}\n\nfunction repositoryProperty(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n const repoPicker = findRepoPickerField(setup, selectedAction);\n const repos = repositories(setup, values, selectedAction);\n if (repos.length === 0 || !repoPicker?.field.provider) return {};\n\n const declared = REPO_PROPERTIES.filter((name) =>\n hasPayloadValue(fieldText(values[name])),\n );\n return {\n repos: repos.map((url) => ({\n url,\n ...Object.fromEntries(\n declared.map((name) => [name, fieldText(values[name])]),\n ),\n provider: repoPicker.field.provider as string,\n })),\n };\n}\n\nfunction parsePluginSources(\n value: SetupRequestBody[string],\n): SetupRequestBody[string] {\n if (!hasPayloadValue(value) || typeof value !== \"string\") return value;\n const trimmed = value.trim();\n if (!trimmed) return [];\n if (trimmed.startsWith(\"[\")) {\n try {\n return JSON.parse(trimmed) as SetupRequestBody[string];\n } catch {\n return [{ source: trimmed }];\n }\n }\n return trimmed\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => {\n const [source, ...parts] = line.split(/\\s+/);\n const plugin: Record<string, string> = { source };\n parts.forEach((part) => {\n const [key, ...raw] = part.split(\"=\");\n const valuePart = raw.join(\"=\");\n if ((key === \"ref\" || key === \"repo_path\") && valuePart) {\n plugin[key] = valuePart;\n }\n });\n return plugin;\n });\n}\n\n/**\n * The create request body these form values produce.\n *\n * No entry declares it: `name` comes from the entry, `repos` from the\n * repo-picker field and its declared provider, and `trigger` from the key and\n * fields under `form.triggers`. Only `prompt` and an event `filter` are\n * declared, because only they cannot be read off the form.\n *\n * Returns null for an assisted entry, which hands setup to a conversation\n * instead of sending a body.\n */\nexport function buildCreatePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n /** Bundle/upload entries only: what the upload returned. */\n tarballPath: string = PREFLIGHT_TARBALL_PATH,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const { setup } = entry;\n if (setup.mode !== \"direct\") return null;\n if (setup.bundle) {\n return buildBundlePayload(entry, values, tarballPath, selectedTrigger);\n }\n if (setup.actions) {\n return buildActionPayload(\n entry,\n values,\n tarballPath,\n selectedTrigger,\n selectedActionKey,\n );\n }\n if (!setup.prompt) return null;\n\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n prompt: interpolateText(setup.prompt, scope),\n ...optionalCreateProperties(setup, values),\n ...repositoryProperty(setup, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\nfunction buildActionPayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const kind = selectedActionKind(entry, selectedActionKey);\n if (kind === \"bundle\") return null;\n\n const setup = entry.setup;\n const scope = { form: values, automation: entry };\n const payload: SetupRequestBody = {\n name: deriveName(entry, values, selectedActionKey),\n };\n\n const trigger = buildTrigger(\n entry,\n values,\n selectedTrigger,\n selectedActionKey,\n );\n if (trigger) payload.trigger = trigger;\n\n Object.assign(\n payload,\n optionalCreateProperties(setup, values, selectedActionKey),\n );\n\n if (kind === \"upload\") {\n const action = setup.actions?.upload;\n if (!action) return null;\n payload.tarball_path =\n tarballPath || interpolateText(action.tarballPath, scope);\n payload.entrypoint = interpolateText(action.entrypoint, scope);\n if (action.setupScript) {\n const setupScript = interpolateText(action.setupScript, scope);\n if (hasPayloadValue(setupScript)) payload.setup_script_path = setupScript;\n }\n return payload;\n }\n\n const action =\n kind === \"plugin\" ? setup.actions?.plugin : setup.actions?.prompt;\n if (!action) return null;\n payload.prompt = interpolateText(action.prompt, scope);\n Object.assign(payload, repositoryProperty(setup, values, selectedActionKey));\n\n if (kind === \"plugin\") {\n const pluginAction = setup.actions?.plugin;\n if (!pluginAction) return null;\n payload.plugins = parsePluginSources(\n interpolateValue(pluginAction.plugins, scope),\n );\n }\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\n/**\n * The `template` provenance a prompt entry sends, or undefined for one that\n * publishes no version.\n *\n * It is what lets the service recognise a second setup of the same entry as\n * the automation it already holds. The bundle path derives the same block from\n * the config the bundle declares; a prompt entry declares none, so the answer\n * to \"what was this created from\" is the form as submitted.\n */\nfunction buildTemplate(\n entry: SetupEntry,\n values: SetupFormValues,\n): SetupRequestBody | undefined {\n if (!entry.version) return undefined;\n return {\n id: entry.id,\n version: entry.version,\n config: { ...values } as SetupRequestBody,\n };\n}\n\n/**\n * The `trigger` object, read off the key and fields under `form.triggers`.\n *\n * Identical for both kinds of direct entry: only the create endpoint and what\n * the automation is told to do differ between a prompt and a bundle.\n */\nfunction buildTrigger(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | undefined {\n const trigger = getTrigger(entry.setup, selectedTrigger);\n if (!trigger) return undefined;\n\n const properties = TRIGGER_PROPERTIES[trigger.kind];\n const declared = Object.keys(trigger.fields).filter((name) =>\n properties.includes(name),\n );\n const repoPicker = findRepoPickerField(entry.setup, selectedAction);\n const derived = Object.fromEntries(\n declared\n .map((name) => [name, fieldText(values[name])])\n .filter(([, value]) => hasPayloadValue(value)),\n );\n\n const filter = entry.setup.filter\n ? interpolateText(entry.setup.filter, {\n form: values,\n automation: entry,\n })\n : undefined;\n\n return {\n type: trigger.kind,\n ...derived,\n ...(trigger.kind === \"event\" && {\n ...(!(\"source\" in derived) && {\n source: repoPicker?.field.provider ?? \"\",\n }),\n ...(hasPayloadValue(filter) && { filter }),\n }),\n };\n}\n\n/**\n * The raw create body a bundle entry produces.\n *\n * `tarball_path` is the one value neither declared nor derived: the host packs\n * and uploads the bundle first, and creates from what came back. `template` is\n * the provenance that makes enabling the same entry twice return the\n * automation that already exists rather than a second one.\n *\n * There is no `repos`: the raw endpoint has no such field, and a bundle's\n * script fetches what it needs itself.\n */\nfunction buildBundlePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n): SetupRequestBody {\n const bundle = entry.setup.bundle!;\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n ...optionalCreateProperties(entry.setup, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n payload.tarball_path = tarballPath;\n payload.entrypoint = bundle.entrypoint;\n if (bundle.setupScript) payload.setup_script_path = bundle.setupScript;\n if (bundle.timeout !== undefined) payload.timeout = bundle.timeout;\n payload.template = {\n id: entry.id,\n version: bundle.version,\n config: interpolateConfig(bundle.config, scope) as SetupRequestBody,\n };\n\n return payload;\n}\n\n/**\n * Placeholder substitution over the config tree. Only string leaves are\n * templated; a number, a boolean or a null is written through as itself, so an\n * entry can state a value the script reads as the type it expects.\n */\nfunction interpolateConfig(\n node: SetupBundleConfigValue,\n scope: Parameters<typeof interpolateText>[1],\n): SetupBundleConfigValue {\n if (typeof node === \"string\") {\n return interpolateValue(node, scope);\n }\n if (Array.isArray(node)) {\n return node.map((item) => interpolateConfig(item, scope));\n }\n if (typeof node === \"object\" && node !== null) {\n return Object.fromEntries(\n Object.entries(node).map(([key, value]) => [\n key,\n interpolateConfig(value, scope),\n ]),\n );\n }\n return node;\n}\n\n/**\n * The preflight body the host sends. The same shape for every entry, so no\n * entry declares it.\n */\nexport function buildPreflightBody(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | null {\n const draft = buildCreatePayload(\n entry,\n values,\n PREFLIGHT_TARBALL_PATH,\n selectedTrigger,\n selectedAction,\n );\n if (!draft) return null;\n\n return {\n automationId: entry.id,\n endpoint: automationCreateEndpoint(entry, selectedAction),\n draft,\n };\n}\n\n/**\n * What an assisted entry sends into the conversation that finishes setup.\n *\n * The command is not declared either: it lives once, in the owning skill's own\n * `triggers` frontmatter, and the skill defaults to the entry's id. An entry\n * whose skill is invoked by description rather than by command contributes\n * nothing here, and the answers alone open the conversation.\n */\nexport function buildAssistedMessage(\n entry: SetupEntry,\n values: SetupFormValues,\n): string {\n const command = findAutomationCommand(entry);\n const message = entry.setup.message\n ? interpolateText(entry.setup.message, { form: values, automation: entry })\n : \"\";\n\n return [command, message].filter(Boolean).join(\"\\n\\n\");\n}\n\nfunction collectPlaceholderFields(value: string): string[] {\n const names = Array.from(\n value.matchAll(FORM_PLACEHOLDER_PATTERN),\n (match) => match[1],\n );\n return Array.from(new Set(names));\n}\n\n/**\n * Which form fields built each payload path.\n *\n * Preflight and the create endpoint reject a draft by payload path, and the\n * host has to turn that back into a highlighted input. Building the body with\n * each field standing in for its own value recovers the mapping exactly, so an\n * entry does not declare it.\n */\nexport function deriveErrorMap(\n entry: SetupEntry,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): Record<string, string[]> {\n const mapping: Record<string, string[]> = {};\n\n const triggerEntries = Object.keys(entry.setup.form.triggers ?? {});\n const triggerVariants = selectedTrigger\n ? [selectedTrigger]\n : triggerEntries.length > 1\n ? triggerEntries\n : [null];\n\n const actionEntries = actionKinds(entry.setup);\n const actionVariants = selectedAction\n ? [selectedAction]\n : actionEntries.length > 1\n ? actionEntries\n : [null];\n\n const walk = (node: unknown, path: string): void => {\n if (Array.isArray(node)) {\n node.forEach((item, index) => walk(item, `${path}[${index}]`));\n return;\n }\n if (typeof node === \"object\" && node !== null) {\n Object.entries(node).forEach(([key, item]) =>\n walk(item, path ? `${path}.${key}` : key),\n );\n return;\n }\n if (typeof node === \"string\") {\n const names = collectPlaceholderFields(node);\n if (names.length > 0) {\n mapping[path] = Array.from(\n new Set([...(mapping[path] ?? []), ...names]),\n );\n }\n }\n };\n\n actionVariants.forEach((actionVariant) => {\n triggerVariants.forEach((triggerVariant) => {\n const names = Object.keys(\n collectFields(entry.setup, triggerVariant, actionVariant),\n );\n const variantStandIns = Object.fromEntries(\n names.map((name) => [name, `{{form.${name}}}`]),\n );\n const template = buildCreatePayload(\n entry,\n variantStandIns,\n actionVariant === \"upload\" ? \"\" : PREFLIGHT_TARBALL_PATH,\n triggerVariant,\n actionVariant,\n );\n if (template) walk(template, \"\");\n });\n });\n return mapping;\n}\n"],"mappings":"gNA6CA,SAAgB,EACd,EACA,EACQ,CACR,IAAM,EAAO,EAAmB,EAAO,CAAc,EAKrD,OAJI,IAAS,SAAiB,EAAA,sBAAsB,cAAc,EAC9D,IAAS,UAAY,IAAS,SACzB,EAAsB,cAAc,EAEtC,EAAA,sBAAsB,cAAc,CAC7C,CAGA,SAAgB,GAAmC,CACjD,OAAO,EAAsB,SAAS,CACxC,CAUA,SAAS,EACP,EACQ,CACR,IAAM,EAAO,EAAA,sBAAsB,CAAI,EACvC,GAAI,CAAC,EACH,MAAU,MACR,mDAAmD,EAAK,iFAE1D,EAEF,OAAO,CACT,CAoCA,SAAgB,EAAc,EAA4B,CACxD,OAAO,EAAM,MAAM,OAAS,UAAY,EAAM,MAAM,SAAW,IAAA,EACjE,CASA,SAAgB,EACd,EACA,EACuC,CACvC,GAAI,CAAC,EAAO,MAAO,SACnB,GAAI,EAAc,CAAK,EAAG,MAAO,SACjC,IAAM,EAAU,EAAM,MAAM,SAAW,CAAC,EACxC,GAAI,GAAkB,KAAkB,EACtC,OAAO,EAET,IAAM,EAAO,EAAA,YAAY,EAAM,KAAK,EAEpC,OADI,EAAK,SAAW,EAAU,EAAK,GAC5B,QACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"automation-setup.js","names":[],"sources":["../../src/manifests/automation-setup.ts"],"sourcesContent":["/**\n * The only module that knows a setup entry configures an *automation*.\n *\n * The published contract states just what varies between entries; everything\n * derivable is the host's to generate. That derivation is here, and nowhere\n * else, so the rest of `src/manifests/` stays about forms rather than about\n * automations.\n *\n * The algorithm mirrors `tests/test_automation_setup.py::_render_payload` in\n * `OpenHands/extensions`, which is the authoritative reference: it produces the\n * request bodies published in that repository's contract fixtures, and those\n * were verified against the live service. The create model is `extra=\"forbid\"`,\n * so any divergence is a hard 422 rather than a dropped field.\n */\n\nimport i18n from \"#/i18n\";\nimport { I18nKey } from \"#/i18n/declaration\";\nimport { findAutomationCommand } from \"#/utils/automation-catalog\";\nimport { getAutomationEndpoint } from \"./automation-interface\";\nimport {\n actionKinds,\n collectFields,\n fieldText,\n fieldValues,\n} from \"./manifest-local-validation\";\nimport { interpolateText, interpolateValue } from \"./manifest-template\";\nimport type {\n SetupActionKind,\n SetupBlock,\n SetupBundleConfigValue,\n SetupEntry,\n SetupFormValues,\n SetupRequestBody,\n SetupTriggerKind,\n} from \"./types\";\n\n/**\n * The creation endpoint a derived draft would be posted to. Resolved on call\n * rather than at import, because the endpoint is the interface manifest's and\n * this module loads whether or not one was admitted.\n *\n * A bundle entry is created through the raw endpoint, because what it sends is\n * a tarball it uploaded rather than arguments to a preset. Called without an\n * entry - as the import path does - it answers for a prompt.\n */\nexport function automationCreateEndpoint(\n entry?: SetupEntry,\n selectedAction?: string | null,\n): string {\n const kind = selectedActionKind(entry, selectedAction);\n if (kind === \"plugin\") return getAutomationEndpoint(\"createPlugin\");\n if (kind === \"upload\" || kind === \"bundle\") {\n return requireBundleEndpoint(\"createBundle\");\n }\n return getAutomationEndpoint(\"createPrompt\");\n}\n\n/** Where a bundle's tarball is uploaded, before the create call. */\nexport function automationUploadEndpoint(): string {\n return requireBundleEndpoint(\"uploads\");\n}\n\n/** The endpoints a bundle entry cannot be created without. */\nconst BUNDLE_ENDPOINTS = [\"createBundle\", \"uploads\"] as const;\n\n/**\n * An endpoint only a bundle needs. The interface manifest may predate bundles,\n * and the host holds no path of its own to fall back to, so this is where that\n * runs out rather than somewhere deep in a request.\n */\nfunction requireBundleEndpoint(\n name: (typeof BUNDLE_ENDPOINTS)[number],\n): string {\n const path = getAutomationEndpoint(name);\n if (!path) {\n throw new Error(\n `The published automation interface declares no '${name}' endpoint, ` +\n \"so this deployment cannot create an automation from a script bundle.\",\n );\n }\n return path;\n}\n\n/**\n * The endpoints this entry needs that the published interface does not declare.\n *\n * Asked before the form renders rather than discovered at the moment of\n * creating: a pinned package that predates bundles can never answer, and the\n * dialog can say so while nothing has been filled in yet. Empty for every\n * entry that is not a bundle, which needs nothing beyond what the block has\n * always declared.\n */\nfunction missingEndpointsForAction(kind: SetupActionKind | \"bundle\"): string[] {\n return kind === \"upload\" || kind === \"bundle\"\n ? BUNDLE_ENDPOINTS.filter((name) => !getAutomationEndpoint(name))\n : [];\n}\n\nexport function missingCreateEndpoints(\n entry: SetupEntry,\n selectedAction?: string | null,\n): string[] {\n if (isBundleEntry(entry)) return missingEndpointsForAction(\"bundle\");\n if (!entry.setup.actions) return [];\n\n if (selectedAction && selectedAction in entry.setup.actions) {\n return missingEndpointsForAction(selectedAction as SetupActionKind);\n }\n\n const missingByAction = actionKinds(entry.setup).map(\n missingEndpointsForAction,\n );\n if (missingByAction.some((missing) => missing.length === 0)) return [];\n return Array.from(new Set(missingByAction.flat()));\n}\n\n/** Whether this entry ships a script tarball instead of a prompt. */\nexport function isBundleEntry(entry: SetupEntry): boolean {\n return entry.setup.mode === \"direct\" && entry.setup.bundle !== undefined;\n}\n\nexport function isUploadAction(\n entry: SetupEntry,\n selectedAction?: string | null,\n): boolean {\n return selectedActionKind(entry, selectedAction) === \"upload\";\n}\n\nexport function selectedActionKind(\n entry: SetupEntry | undefined,\n selectedAction?: string | null,\n): SetupActionKind | \"bundle\" | \"prompt\" {\n if (!entry) return \"prompt\";\n if (isBundleEntry(entry)) return \"bundle\";\n const actions = entry.setup.actions ?? {};\n if (selectedAction && selectedAction in actions) {\n return selectedAction as SetupActionKind;\n }\n const keys = actionKinds(entry.setup);\n if (keys.length === 1) return keys[0];\n return \"prompt\";\n}\n\n/**\n * The `tarball_path` a preflight draft carries.\n *\n * Preflight runs on every field blur and the upload happens once, at submit,\n * so there is no real path to send yet. The service checks this field's scheme\n * at preflight and its ownership only at creation, so a well-formed stand-in\n * validates exactly what preflight is for - the rest of the body - without\n * uploading an archive per keystroke.\n */\nexport const PREFLIGHT_TARBALL_PATH =\n \"oh-internal://uploads/00000000-0000-0000-0000-000000000000\";\n\n/**\n * Trigger properties a form field may fill, per trigger kind. A field under a\n * trigger kind whose name is listed here fills the trigger property of the same\n * name; anything else there, such as a phrase to match, is an input to `filter`.\n */\nconst TRIGGER_PROPERTIES: Record<SetupTriggerKind, readonly string[]> = {\n cron: [\"schedule\", \"timezone\"],\n event: [\"source\", \"on\"],\n};\n\nconst OPTIONAL_CREATE_PROPERTIES = [\"model\", \"timeout\"] as const;\n\n/**\n * Repository properties a form field may fill, read the same way\n * {@link TRIGGER_PROPERTIES} is: a field whose name is listed here fills the\n * `repos[]` property of the same name. `url` and `provider` are absent because\n * neither is answered by a field of its own — they come from the repo-picker's\n * value and from its declared provider.\n *\n * These two lists are the only field names the derivation knows; everything\n * else it reads it finds by field type. An entry that wants a base branch in\n * its request therefore has to name that field `ref`.\n */\nconst REPO_PROPERTIES: readonly string[] = [\"ref\"];\n\nconst FORM_PLACEHOLDER_PATTERN = /\\{\\{form\\.([A-Za-z0-9_.]+)\\}\\}/g;\n\n/** The repository input, which supplies `repos` and an event trigger's source. */\nfunction findRepoPickerField(\n setup: SetupBlock,\n selectedAction?: string | null,\n) {\n const match = Object.entries(collectFields(setup, null, selectedAction)).find(\n ([, field]) => field.type === \"repo-picker\",\n );\n return match ? { name: match[0], field: match[1] } : null;\n}\n\n/** Every repository the form collected, whether the picker takes one or many. */\nfunction repositories(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string[] {\n const picker = findRepoPickerField(setup, selectedAction);\n return picker ? fieldValues(values[picker.name]) : [];\n}\n\n/**\n * The created automation's name.\n *\n * One repository is worth naming; several are not, so the count stands in\n * rather than a list of names that would not fit.\n */\nfunction deriveName(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string {\n if (\n \"name\" in collectFields(entry.setup, null, selectedAction) &&\n \"name\" in values\n ) {\n return fieldText(values.name);\n }\n\n const repos = repositories(entry.setup, values, selectedAction);\n if (repos.length === 0) return entry.name;\n if (repos.length === 1) return `${entry.name} - ${repos[0]}`;\n // The count is the one word here the host writes rather than reads off the\n // entry, so it is translated. There is no translator to pass in: the\n // derivation runs from a memo, an upload and a test alike.\n const count = i18n.t(I18nKey.SETUP$REPOSITORY_COUNT, {\n total: repos.length,\n });\n return `${entry.name} - ${count}`;\n}\n\n/** The active trigger kind for a direct entry, with its fields. */\nfunction getTrigger(setup: SetupBlock, selectedTrigger?: string | null) {\n const triggers = setup.form.triggers ?? {};\n const entries = Object.entries(triggers);\n if (entries.length === 0) return null;\n const match =\n selectedTrigger && selectedTrigger in triggers\n ? [selectedTrigger, triggers[selectedTrigger as SetupTriggerKind]]\n : entries.length === 1\n ? entries[0]\n : null;\n if (!match) return null;\n const [kind, fields] = match;\n return { kind: kind as SetupTriggerKind, fields: fields ?? {} };\n}\n\nfunction hasPayloadValue(\n value: SetupRequestBody[string] | undefined,\n): value is SetupRequestBody[string] {\n return value !== undefined && value !== null && value !== \"\";\n}\n\nfunction fieldPayloadValue(\n fieldType: string | undefined,\n value: SetupFormValues[string],\n) {\n if (fieldType === \"number\") {\n const text = fieldText(value);\n return text === \"\" ? undefined : Number(text);\n }\n return fieldText(value);\n}\n\nfunction optionalCreateProperties(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n return Object.fromEntries(\n OPTIONAL_CREATE_PROPERTIES.flatMap((name) => {\n const field = collectFields(setup, null, selectedAction)[name];\n const value = fieldPayloadValue(field?.type, values[name]);\n return hasPayloadValue(value) ? [[name, value]] : [];\n }),\n ) as SetupRequestBody;\n}\n\nfunction repositoryProperty(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n const repoPicker = findRepoPickerField(setup, selectedAction);\n const repos = repositories(setup, values, selectedAction);\n if (repos.length === 0 || !repoPicker?.field.provider) return {};\n\n const declared = REPO_PROPERTIES.filter((name) =>\n hasPayloadValue(fieldText(values[name])),\n );\n return {\n repos: repos.map((url) => ({\n url,\n ...Object.fromEntries(\n declared.map((name) => [name, fieldText(values[name])]),\n ),\n provider: repoPicker.field.provider as string,\n })),\n };\n}\n\nfunction parsePluginSources(\n value: SetupRequestBody[string],\n): SetupRequestBody[string] {\n if (!hasPayloadValue(value) || typeof value !== \"string\") return value;\n const trimmed = value.trim();\n if (!trimmed) return [];\n if (trimmed.startsWith(\"[\")) {\n try {\n return JSON.parse(trimmed) as SetupRequestBody[string];\n } catch {\n return [{ source: trimmed }];\n }\n }\n return trimmed\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => {\n const [source, ...parts] = line.split(/\\s+/);\n const plugin: Record<string, string> = { source };\n parts.forEach((part) => {\n const [key, ...raw] = part.split(\"=\");\n const valuePart = raw.join(\"=\");\n if ((key === \"ref\" || key === \"repo_path\") && valuePart) {\n plugin[key] = valuePart;\n }\n });\n return plugin;\n });\n}\n\n/**\n * The create request body these form values produce.\n *\n * No entry declares it: `name` comes from the entry, `repos` from the\n * repo-picker field and its declared provider, and `trigger` from the key and\n * fields under `form.triggers`. Only `prompt` and an event `filter` are\n * declared, because only they cannot be read off the form.\n *\n * Returns null for an assisted entry, which hands setup to a conversation\n * instead of sending a body.\n */\nexport function buildCreatePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n /** Bundle/upload entries only: what the upload returned. */\n tarballPath: string = PREFLIGHT_TARBALL_PATH,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const { setup } = entry;\n if (setup.mode !== \"direct\") return null;\n if (setup.bundle) {\n return buildBundlePayload(entry, values, tarballPath, selectedTrigger);\n }\n if (setup.actions) {\n return buildActionPayload(\n entry,\n values,\n tarballPath,\n selectedTrigger,\n selectedActionKey,\n );\n }\n if (!setup.prompt) return null;\n\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n prompt: interpolateText(setup.prompt, scope),\n ...optionalCreateProperties(setup, values),\n ...repositoryProperty(setup, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\nfunction buildActionPayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const kind = selectedActionKind(entry, selectedActionKey);\n if (kind === \"bundle\") return null;\n\n const setup = entry.setup;\n const scope = { form: values, automation: entry };\n const payload: SetupRequestBody = {\n name: deriveName(entry, values, selectedActionKey),\n };\n\n const trigger = buildTrigger(\n entry,\n values,\n selectedTrigger,\n selectedActionKey,\n );\n if (trigger) payload.trigger = trigger;\n\n Object.assign(\n payload,\n optionalCreateProperties(setup, values, selectedActionKey),\n );\n\n if (kind === \"upload\") {\n const action = setup.actions?.upload;\n if (!action) return null;\n payload.tarball_path =\n tarballPath || interpolateText(action.tarballPath, scope);\n payload.entrypoint = interpolateText(action.entrypoint, scope);\n if (action.setupScript) {\n const setupScript = interpolateText(action.setupScript, scope);\n if (hasPayloadValue(setupScript)) payload.setup_script_path = setupScript;\n }\n return payload;\n }\n\n const action =\n kind === \"plugin\" ? setup.actions?.plugin : setup.actions?.prompt;\n if (!action) return null;\n payload.prompt = interpolateText(action.prompt, scope);\n Object.assign(payload, repositoryProperty(setup, values, selectedActionKey));\n\n if (kind === \"plugin\") {\n const pluginAction = setup.actions?.plugin;\n if (!pluginAction) return null;\n payload.plugins = parsePluginSources(\n interpolateValue(pluginAction.plugins, scope),\n );\n }\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\n/**\n * The `template` provenance a prompt entry sends, or undefined for one that\n * publishes no version.\n *\n * It is what lets the service recognise a second setup of the same entry as\n * the automation it already holds. The bundle path derives the same block from\n * the config the bundle declares; a prompt entry declares none, so the answer\n * to \"what was this created from\" is the form as submitted.\n */\nfunction buildTemplate(\n entry: SetupEntry,\n values: SetupFormValues,\n): SetupRequestBody | undefined {\n if (!entry.version) return undefined;\n return {\n id: entry.id,\n version: entry.version,\n config: { ...values } as SetupRequestBody,\n };\n}\n\n/**\n * The `trigger` object, read off the key and fields under `form.triggers`.\n *\n * Identical for both kinds of direct entry: only the create endpoint and what\n * the automation is told to do differ between a prompt and a bundle.\n */\nfunction buildTrigger(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | undefined {\n const trigger = getTrigger(entry.setup, selectedTrigger);\n if (!trigger) return undefined;\n\n const properties = TRIGGER_PROPERTIES[trigger.kind];\n const declared = Object.keys(trigger.fields).filter((name) =>\n properties.includes(name),\n );\n const repoPicker = findRepoPickerField(entry.setup, selectedAction);\n const derived = Object.fromEntries(\n declared\n .map((name) => [name, fieldText(values[name])])\n .filter(([, value]) => hasPayloadValue(value)),\n );\n\n const filter = entry.setup.filter\n ? interpolateText(entry.setup.filter, {\n form: values,\n automation: entry,\n })\n : undefined;\n\n return {\n type: trigger.kind,\n ...derived,\n ...(trigger.kind === \"event\" && {\n ...(!(\"source\" in derived) && {\n source: repoPicker?.field.provider ?? \"\",\n }),\n ...(hasPayloadValue(filter) && { filter }),\n }),\n };\n}\n\n/**\n * The raw create body a bundle entry produces.\n *\n * `tarball_path` is the one value neither declared nor derived: the host packs\n * and uploads the bundle first, and creates from what came back. `template` is\n * the provenance that makes enabling the same entry twice return the\n * automation that already exists rather than a second one.\n *\n * There is no `repos`: the raw endpoint has no such field, and a bundle's\n * script fetches what it needs itself.\n */\nfunction buildBundlePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n): SetupRequestBody {\n const bundle = entry.setup.bundle!;\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n payload.tarball_path = tarballPath;\n payload.entrypoint = bundle.entrypoint;\n if (bundle.setupScript) payload.setup_script_path = bundle.setupScript;\n if (bundle.timeout !== undefined) payload.timeout = bundle.timeout;\n payload.template = {\n id: entry.id,\n version: bundle.version,\n config: interpolateConfig(bundle.config, scope) as SetupRequestBody,\n };\n\n return payload;\n}\n\n/**\n * Placeholder substitution over the config tree. Only string leaves are\n * templated; a number, a boolean or a null is written through as itself, so an\n * entry can state a value the script reads as the type it expects.\n */\nfunction interpolateConfig(\n node: SetupBundleConfigValue,\n scope: Parameters<typeof interpolateText>[1],\n): SetupBundleConfigValue {\n if (typeof node === \"string\") {\n return interpolateValue(node, scope);\n }\n if (Array.isArray(node)) {\n return node.map((item) => interpolateConfig(item, scope));\n }\n if (typeof node === \"object\" && node !== null) {\n return Object.fromEntries(\n Object.entries(node).map(([key, value]) => [\n key,\n interpolateConfig(value, scope),\n ]),\n );\n }\n return node;\n}\n\n/**\n * The preflight body the host sends. The same shape for every entry, so no\n * entry declares it.\n */\nexport function buildPreflightBody(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | null {\n const draft = buildCreatePayload(\n entry,\n values,\n PREFLIGHT_TARBALL_PATH,\n selectedTrigger,\n selectedAction,\n );\n if (!draft) return null;\n\n return {\n automationId: entry.id,\n endpoint: automationCreateEndpoint(entry, selectedAction),\n draft,\n };\n}\n\n/**\n * What an assisted entry sends into the conversation that finishes setup.\n *\n * The command is not declared either: it lives once, in the owning skill's own\n * `triggers` frontmatter, and the skill defaults to the entry's id. An entry\n * whose skill is invoked by description rather than by command contributes\n * nothing here, and the answers alone open the conversation.\n */\nexport function buildAssistedMessage(\n entry: SetupEntry,\n values: SetupFormValues,\n): string {\n const command = findAutomationCommand(entry);\n const message = entry.setup.message\n ? interpolateText(entry.setup.message, { form: values, automation: entry })\n : \"\";\n\n return [command, message].filter(Boolean).join(\"\\n\\n\");\n}\n\nfunction collectPlaceholderFields(value: string): string[] {\n const names = Array.from(\n value.matchAll(FORM_PLACEHOLDER_PATTERN),\n (match) => match[1],\n );\n return Array.from(new Set(names));\n}\n\n/**\n * Which form fields built each payload path.\n *\n * Preflight and the create endpoint reject a draft by payload path, and the\n * host has to turn that back into a highlighted input. Building the body with\n * each field standing in for its own value recovers the mapping exactly, so an\n * entry does not declare it.\n */\nexport function deriveErrorMap(\n entry: SetupEntry,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): Record<string, string[]> {\n const mapping: Record<string, string[]> = {};\n\n const triggerEntries = Object.keys(entry.setup.form.triggers ?? {});\n const triggerVariants = selectedTrigger\n ? [selectedTrigger]\n : triggerEntries.length > 1\n ? triggerEntries\n : [null];\n\n const actionEntries = actionKinds(entry.setup);\n const actionVariants = selectedAction\n ? [selectedAction]\n : actionEntries.length > 1\n ? actionEntries\n : [null];\n\n const walk = (node: unknown, path: string): void => {\n if (Array.isArray(node)) {\n node.forEach((item, index) => walk(item, `${path}[${index}]`));\n return;\n }\n if (typeof node === \"object\" && node !== null) {\n Object.entries(node).forEach(([key, item]) =>\n walk(item, path ? `${path}.${key}` : key),\n );\n return;\n }\n if (typeof node === \"string\") {\n const names = collectPlaceholderFields(node);\n if (names.length > 0) {\n mapping[path] = Array.from(\n new Set([...(mapping[path] ?? []), ...names]),\n );\n }\n }\n };\n\n actionVariants.forEach((actionVariant) => {\n triggerVariants.forEach((triggerVariant) => {\n const names = Object.keys(\n collectFields(entry.setup, triggerVariant, actionVariant),\n );\n const variantStandIns = Object.fromEntries(\n names.map((name) => [name, `{{form.${name}}}`]),\n );\n const template = buildCreatePayload(\n entry,\n variantStandIns,\n actionVariant === \"upload\" ? \"\" : PREFLIGHT_TARBALL_PATH,\n triggerVariant,\n actionVariant,\n );\n if (template) walk(template, \"\");\n });\n });\n return mapping;\n}\n"],"mappings":";;;;;AA6CA,SAAgB,EACd,GACA,GACQ;CACR,IAAM,IAAO,EAAmB,GAAO,CAAc;CAKrD,OAJI,MAAS,WAAiB,EAAsB,cAAc,IAC9D,MAAS,YAAY,MAAS,WACzB,EAAsB,cAAc,IAEtC,EAAsB,cAAc;AAC7C;AAGA,SAAgB,IAAmC;CACjD,OAAO,EAAsB,SAAS;AACxC;AAUA,SAAS,EACP,GACQ;CACR,IAAM,IAAO,EAAsB,CAAI;CACvC,IAAI,CAAC,GACH,MAAU,MACR,mDAAmD,EAAK,iFAE1D;CAEF,OAAO;AACT;AAoCA,SAAgB,EAAc,GAA4B;CACxD,OAAO,EAAM,MAAM,SAAS,YAAY,EAAM,MAAM,WAAW,KAAA;AACjE;AASA,SAAgB,EACd,GACA,GACuC;CACvC,IAAI,CAAC,GAAO,OAAO;CACnB,IAAI,EAAc,CAAK,GAAG,OAAO;CACjC,IAAM,IAAU,EAAM,MAAM,WAAW,CAAC;CACxC,IAAI,KAAkB,KAAkB,GACtC,OAAO;CAET,IAAM,IAAO,EAAY,EAAM,KAAK;CAEpC,OADI,EAAK,WAAW,IAAU,EAAK,KAC5B;AACT"}
|
|
1
|
+
{"version":3,"file":"automation-setup.js","names":[],"sources":["../../src/manifests/automation-setup.ts"],"sourcesContent":["/**\n * The only module that knows a setup entry configures an *automation*.\n *\n * The published contract states just what varies between entries; everything\n * derivable is the host's to generate. That derivation is here, and nowhere\n * else, so the rest of `src/manifests/` stays about forms rather than about\n * automations.\n *\n * The algorithm mirrors `tests/test_automation_setup.py::_render_payload` in\n * `OpenHands/extensions`, which is the authoritative reference: it produces the\n * request bodies published in that repository's contract fixtures, and those\n * were verified against the live service. The create model is `extra=\"forbid\"`,\n * so any divergence is a hard 422 rather than a dropped field.\n */\n\nimport i18n from \"#/i18n\";\nimport { I18nKey } from \"#/i18n/declaration\";\nimport { findAutomationCommand } from \"#/utils/automation-catalog\";\nimport { getAutomationEndpoint } from \"./automation-interface\";\nimport {\n actionKinds,\n collectFields,\n fieldText,\n fieldValues,\n} from \"./manifest-local-validation\";\nimport { interpolateText, interpolateValue } from \"./manifest-template\";\nimport type {\n SetupActionKind,\n SetupBlock,\n SetupBundleConfigValue,\n SetupEntry,\n SetupFormValues,\n SetupRequestBody,\n SetupTriggerKind,\n} from \"./types\";\n\n/**\n * The creation endpoint a derived draft would be posted to. Resolved on call\n * rather than at import, because the endpoint is the interface manifest's and\n * this module loads whether or not one was admitted.\n *\n * A bundle entry is created through the raw endpoint, because what it sends is\n * a tarball it uploaded rather than arguments to a preset. Called without an\n * entry - as the import path does - it answers for a prompt.\n */\nexport function automationCreateEndpoint(\n entry?: SetupEntry,\n selectedAction?: string | null,\n): string {\n const kind = selectedActionKind(entry, selectedAction);\n if (kind === \"plugin\") return getAutomationEndpoint(\"createPlugin\");\n if (kind === \"upload\" || kind === \"bundle\") {\n return requireBundleEndpoint(\"createBundle\");\n }\n return getAutomationEndpoint(\"createPrompt\");\n}\n\n/** Where a bundle's tarball is uploaded, before the create call. */\nexport function automationUploadEndpoint(): string {\n return requireBundleEndpoint(\"uploads\");\n}\n\n/** The endpoints a bundle entry cannot be created without. */\nconst BUNDLE_ENDPOINTS = [\"createBundle\", \"uploads\"] as const;\n\n/**\n * An endpoint only a bundle needs. The interface manifest may predate bundles,\n * and the host holds no path of its own to fall back to, so this is where that\n * runs out rather than somewhere deep in a request.\n */\nfunction requireBundleEndpoint(\n name: (typeof BUNDLE_ENDPOINTS)[number],\n): string {\n const path = getAutomationEndpoint(name);\n if (!path) {\n throw new Error(\n `The published automation interface declares no '${name}' endpoint, ` +\n \"so this deployment cannot create an automation from a script bundle.\",\n );\n }\n return path;\n}\n\n/**\n * The endpoints this entry needs that the published interface does not declare.\n *\n * Asked before the form renders rather than discovered at the moment of\n * creating: a pinned package that predates bundles can never answer, and the\n * dialog can say so while nothing has been filled in yet. Empty for every\n * entry that is not a bundle, which needs nothing beyond what the block has\n * always declared.\n */\nfunction missingEndpointsForAction(kind: SetupActionKind | \"bundle\"): string[] {\n return kind === \"upload\" || kind === \"bundle\"\n ? BUNDLE_ENDPOINTS.filter((name) => !getAutomationEndpoint(name))\n : [];\n}\n\nexport function missingCreateEndpoints(\n entry: SetupEntry,\n selectedAction?: string | null,\n): string[] {\n if (isBundleEntry(entry)) return missingEndpointsForAction(\"bundle\");\n if (!entry.setup.actions) return [];\n\n if (selectedAction && selectedAction in entry.setup.actions) {\n return missingEndpointsForAction(selectedAction as SetupActionKind);\n }\n\n const missingByAction = actionKinds(entry.setup).map(\n missingEndpointsForAction,\n );\n if (missingByAction.some((missing) => missing.length === 0)) return [];\n return Array.from(new Set(missingByAction.flat()));\n}\n\n/** Whether this entry ships a script tarball instead of a prompt. */\nexport function isBundleEntry(entry: SetupEntry): boolean {\n return entry.setup.mode === \"direct\" && entry.setup.bundle !== undefined;\n}\n\nexport function isUploadAction(\n entry: SetupEntry,\n selectedAction?: string | null,\n): boolean {\n return selectedActionKind(entry, selectedAction) === \"upload\";\n}\n\nexport function selectedActionKind(\n entry: SetupEntry | undefined,\n selectedAction?: string | null,\n): SetupActionKind | \"bundle\" | \"prompt\" {\n if (!entry) return \"prompt\";\n if (isBundleEntry(entry)) return \"bundle\";\n const actions = entry.setup.actions ?? {};\n if (selectedAction && selectedAction in actions) {\n return selectedAction as SetupActionKind;\n }\n const keys = actionKinds(entry.setup);\n if (keys.length === 1) return keys[0];\n return \"prompt\";\n}\n\n/**\n * The `tarball_path` a preflight draft carries.\n *\n * Preflight runs on every field blur and the upload happens once, at submit,\n * so there is no real path to send yet. The service checks this field's scheme\n * at preflight and its ownership only at creation, so a well-formed stand-in\n * validates exactly what preflight is for - the rest of the body - without\n * uploading an archive per keystroke.\n */\nexport const PREFLIGHT_TARBALL_PATH =\n \"oh-internal://uploads/00000000-0000-0000-0000-000000000000\";\n\n/**\n * Trigger properties a form field may fill, per trigger kind. A field under a\n * trigger kind whose name is listed here fills the trigger property of the same\n * name; anything else there, such as a phrase to match, is an input to `filter`.\n */\nconst TRIGGER_PROPERTIES: Record<SetupTriggerKind, readonly string[]> = {\n cron: [\"schedule\", \"timezone\"],\n event: [\"source\", \"on\"],\n};\n\nconst OPTIONAL_CREATE_PROPERTIES = [\n \"model\",\n \"timeout\",\n \"agent_profile_id\",\n] as const;\n\n/**\n * Repository properties a form field may fill, read the same way\n * {@link TRIGGER_PROPERTIES} is: a field whose name is listed here fills the\n * `repos[]` property of the same name. `url` and `provider` are absent because\n * neither is answered by a field of its own — they come from the repo-picker's\n * value and from its declared provider.\n *\n * These two lists are the only field names the derivation knows; everything\n * else it reads it finds by field type. An entry that wants a base branch in\n * its request therefore has to name that field `ref`.\n */\nconst REPO_PROPERTIES: readonly string[] = [\"ref\"];\n\nconst FORM_PLACEHOLDER_PATTERN = /\\{\\{form\\.([A-Za-z0-9_.]+)\\}\\}/g;\n\n/** The repository input, which supplies `repos` and an event trigger's source. */\nfunction findRepoPickerField(\n setup: SetupBlock,\n selectedAction?: string | null,\n) {\n const match = Object.entries(collectFields(setup, null, selectedAction)).find(\n ([, field]) => field.type === \"repo-picker\",\n );\n return match ? { name: match[0], field: match[1] } : null;\n}\n\n/** Every repository the form collected, whether the picker takes one or many. */\nfunction repositories(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string[] {\n const picker = findRepoPickerField(setup, selectedAction);\n return picker ? fieldValues(values[picker.name]) : [];\n}\n\n/**\n * The created automation's name.\n *\n * One repository is worth naming; several are not, so the count stands in\n * rather than a list of names that would not fit.\n */\nfunction deriveName(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string {\n if (\n \"name\" in collectFields(entry.setup, null, selectedAction) &&\n \"name\" in values\n ) {\n return fieldText(values.name);\n }\n\n const repos = repositories(entry.setup, values, selectedAction);\n if (repos.length === 0) return entry.name;\n if (repos.length === 1) return `${entry.name} - ${repos[0]}`;\n // The count is the one word here the host writes rather than reads off the\n // entry, so it is translated. There is no translator to pass in: the\n // derivation runs from a memo, an upload and a test alike.\n const count = i18n.t(I18nKey.SETUP$REPOSITORY_COUNT, {\n total: repos.length,\n });\n return `${entry.name} - ${count}`;\n}\n\n/** The active trigger kind for a direct entry, with its fields. */\nfunction getTrigger(setup: SetupBlock, selectedTrigger?: string | null) {\n const triggers = setup.form.triggers ?? {};\n const entries = Object.entries(triggers);\n if (entries.length === 0) return null;\n const match =\n selectedTrigger && selectedTrigger in triggers\n ? [selectedTrigger, triggers[selectedTrigger as SetupTriggerKind]]\n : entries.length === 1\n ? entries[0]\n : null;\n if (!match) return null;\n const [kind, fields] = match;\n return { kind: kind as SetupTriggerKind, fields: fields ?? {} };\n}\n\nfunction hasPayloadValue(\n value: SetupRequestBody[string] | undefined,\n): value is SetupRequestBody[string] {\n return value !== undefined && value !== null && value !== \"\";\n}\n\nfunction fieldPayloadValue(\n fieldType: string | undefined,\n value: SetupFormValues[string],\n) {\n if (fieldType === \"number\") {\n const text = fieldText(value);\n return text === \"\" ? undefined : Number(text);\n }\n return fieldText(value);\n}\n\nfunction optionalCreateProperties(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n return Object.fromEntries(\n OPTIONAL_CREATE_PROPERTIES.flatMap((name) => {\n if (name === \"model\" && values.agent_profile_id) return [];\n const field = collectFields(setup, null, selectedAction)[name];\n const value = fieldPayloadValue(field?.type, values[name]);\n return hasPayloadValue(value) ? [[name, value]] : [];\n }),\n ) as SetupRequestBody;\n}\n\nfunction repositoryProperty(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n const repoPicker = findRepoPickerField(setup, selectedAction);\n const repos = repositories(setup, values, selectedAction);\n if (repos.length === 0 || !repoPicker?.field.provider) return {};\n\n const declared = REPO_PROPERTIES.filter((name) =>\n hasPayloadValue(fieldText(values[name])),\n );\n return {\n repos: repos.map((url) => ({\n url,\n ...Object.fromEntries(\n declared.map((name) => [name, fieldText(values[name])]),\n ),\n provider: repoPicker.field.provider as string,\n })),\n };\n}\n\nfunction parsePluginSources(\n value: SetupRequestBody[string],\n): SetupRequestBody[string] {\n if (!hasPayloadValue(value) || typeof value !== \"string\") return value;\n const trimmed = value.trim();\n if (!trimmed) return [];\n if (trimmed.startsWith(\"[\")) {\n try {\n return JSON.parse(trimmed) as SetupRequestBody[string];\n } catch {\n return [{ source: trimmed }];\n }\n }\n return trimmed\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => {\n const [source, ...parts] = line.split(/\\s+/);\n const plugin: Record<string, string> = { source };\n parts.forEach((part) => {\n const [key, ...raw] = part.split(\"=\");\n const valuePart = raw.join(\"=\");\n if ((key === \"ref\" || key === \"repo_path\") && valuePart) {\n plugin[key] = valuePart;\n }\n });\n return plugin;\n });\n}\n\n/**\n * The create request body these form values produce.\n *\n * No entry declares it: `name` comes from the entry, `repos` from the\n * repo-picker field and its declared provider, and `trigger` from the key and\n * fields under `form.triggers`. Only `prompt` and an event `filter` are\n * declared, because only they cannot be read off the form.\n *\n * Returns null for an assisted entry, which hands setup to a conversation\n * instead of sending a body.\n */\nexport function buildCreatePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n /** Bundle/upload entries only: what the upload returned. */\n tarballPath: string = PREFLIGHT_TARBALL_PATH,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const { setup } = entry;\n if (setup.mode !== \"direct\") return null;\n if (setup.bundle) {\n return buildBundlePayload(entry, values, tarballPath, selectedTrigger);\n }\n if (setup.actions) {\n return buildActionPayload(\n entry,\n values,\n tarballPath,\n selectedTrigger,\n selectedActionKey,\n );\n }\n if (!setup.prompt) return null;\n\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n prompt: interpolateText(setup.prompt, scope),\n ...optionalCreateProperties(setup, values),\n ...repositoryProperty(setup, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\nfunction buildActionPayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const kind = selectedActionKind(entry, selectedActionKey);\n if (kind === \"bundle\") return null;\n\n const setup = entry.setup;\n const scope = { form: values, automation: entry };\n const payload: SetupRequestBody = {\n name: deriveName(entry, values, selectedActionKey),\n };\n\n const trigger = buildTrigger(\n entry,\n values,\n selectedTrigger,\n selectedActionKey,\n );\n if (trigger) payload.trigger = trigger;\n\n Object.assign(\n payload,\n optionalCreateProperties(setup, values, selectedActionKey),\n );\n\n if (kind === \"upload\") {\n const action = setup.actions?.upload;\n if (!action) return null;\n payload.tarball_path =\n tarballPath || interpolateText(action.tarballPath, scope);\n payload.entrypoint = interpolateText(action.entrypoint, scope);\n if (action.setupScript) {\n const setupScript = interpolateText(action.setupScript, scope);\n if (hasPayloadValue(setupScript)) payload.setup_script_path = setupScript;\n }\n return payload;\n }\n\n const action =\n kind === \"plugin\" ? setup.actions?.plugin : setup.actions?.prompt;\n if (!action) return null;\n payload.prompt = interpolateText(action.prompt, scope);\n Object.assign(payload, repositoryProperty(setup, values, selectedActionKey));\n\n if (kind === \"plugin\") {\n const pluginAction = setup.actions?.plugin;\n if (!pluginAction) return null;\n payload.plugins = parsePluginSources(\n interpolateValue(pluginAction.plugins, scope),\n );\n }\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\n/**\n * The `template` provenance a prompt entry sends, or undefined for one that\n * publishes no version.\n *\n * It is what lets the service recognise a second setup of the same entry as\n * the automation it already holds. The bundle path derives the same block from\n * the config the bundle declares; a prompt entry declares none, so the answer\n * to \"what was this created from\" is the form as submitted.\n */\nfunction buildTemplate(\n entry: SetupEntry,\n values: SetupFormValues,\n): SetupRequestBody | undefined {\n if (!entry.version) return undefined;\n return {\n id: entry.id,\n version: entry.version,\n config: { ...values } as SetupRequestBody,\n };\n}\n\n/**\n * The `trigger` object, read off the key and fields under `form.triggers`.\n *\n * Identical for both kinds of direct entry: only the create endpoint and what\n * the automation is told to do differ between a prompt and a bundle.\n */\nfunction buildTrigger(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | undefined {\n const trigger = getTrigger(entry.setup, selectedTrigger);\n if (!trigger) return undefined;\n\n const properties = TRIGGER_PROPERTIES[trigger.kind];\n const declared = Object.keys(trigger.fields).filter((name) =>\n properties.includes(name),\n );\n const repoPicker = findRepoPickerField(entry.setup, selectedAction);\n const derived = Object.fromEntries(\n declared\n .map((name) => [name, fieldText(values[name])])\n .filter(([, value]) => hasPayloadValue(value)),\n );\n\n const filter = entry.setup.filter\n ? interpolateText(entry.setup.filter, {\n form: values,\n automation: entry,\n })\n : undefined;\n\n return {\n type: trigger.kind,\n ...derived,\n ...(trigger.kind === \"event\" && {\n ...(!(\"source\" in derived) && {\n source: repoPicker?.field.provider ?? \"\",\n }),\n ...(hasPayloadValue(filter) && { filter }),\n }),\n };\n}\n\n/**\n * The raw create body a bundle entry produces.\n *\n * `tarball_path` is the one value neither declared nor derived: the host packs\n * and uploads the bundle first, and creates from what came back. `template` is\n * the provenance that makes enabling the same entry twice return the\n * automation that already exists rather than a second one.\n *\n * There is no `repos`: the raw endpoint has no such field, and a bundle's\n * script fetches what it needs itself.\n */\nfunction buildBundlePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n): SetupRequestBody {\n const bundle = entry.setup.bundle!;\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n ...optionalCreateProperties(entry.setup, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n payload.tarball_path = tarballPath;\n payload.entrypoint = bundle.entrypoint;\n if (bundle.setupScript) payload.setup_script_path = bundle.setupScript;\n if (bundle.timeout !== undefined) payload.timeout = bundle.timeout;\n payload.template = {\n id: entry.id,\n version: bundle.version,\n config: interpolateConfig(bundle.config, scope) as SetupRequestBody,\n };\n\n return payload;\n}\n\n/**\n * Placeholder substitution over the config tree. Only string leaves are\n * templated; a number, a boolean or a null is written through as itself, so an\n * entry can state a value the script reads as the type it expects.\n */\nfunction interpolateConfig(\n node: SetupBundleConfigValue,\n scope: Parameters<typeof interpolateText>[1],\n): SetupBundleConfigValue {\n if (typeof node === \"string\") {\n return interpolateValue(node, scope);\n }\n if (Array.isArray(node)) {\n return node.map((item) => interpolateConfig(item, scope));\n }\n if (typeof node === \"object\" && node !== null) {\n return Object.fromEntries(\n Object.entries(node).map(([key, value]) => [\n key,\n interpolateConfig(value, scope),\n ]),\n );\n }\n return node;\n}\n\n/**\n * The preflight body the host sends. The same shape for every entry, so no\n * entry declares it.\n */\nexport function buildPreflightBody(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | null {\n const draft = buildCreatePayload(\n entry,\n values,\n PREFLIGHT_TARBALL_PATH,\n selectedTrigger,\n selectedAction,\n );\n if (!draft) return null;\n\n return {\n automationId: entry.id,\n endpoint: automationCreateEndpoint(entry, selectedAction),\n draft,\n };\n}\n\n/**\n * What an assisted entry sends into the conversation that finishes setup.\n *\n * The command is not declared either: it lives once, in the owning skill's own\n * `triggers` frontmatter, and the skill defaults to the entry's id. An entry\n * whose skill is invoked by description rather than by command contributes\n * nothing here, and the answers alone open the conversation.\n */\nexport function buildAssistedMessage(\n entry: SetupEntry,\n values: SetupFormValues,\n): string {\n const command = findAutomationCommand(entry);\n const message = entry.setup.message\n ? interpolateText(entry.setup.message, { form: values, automation: entry })\n : \"\";\n\n return [command, message].filter(Boolean).join(\"\\n\\n\");\n}\n\nfunction collectPlaceholderFields(value: string): string[] {\n const names = Array.from(\n value.matchAll(FORM_PLACEHOLDER_PATTERN),\n (match) => match[1],\n );\n return Array.from(new Set(names));\n}\n\n/**\n * Which form fields built each payload path.\n *\n * Preflight and the create endpoint reject a draft by payload path, and the\n * host has to turn that back into a highlighted input. Building the body with\n * each field standing in for its own value recovers the mapping exactly, so an\n * entry does not declare it.\n */\nexport function deriveErrorMap(\n entry: SetupEntry,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): Record<string, string[]> {\n const mapping: Record<string, string[]> = {};\n\n const triggerEntries = Object.keys(entry.setup.form.triggers ?? {});\n const triggerVariants = selectedTrigger\n ? [selectedTrigger]\n : triggerEntries.length > 1\n ? triggerEntries\n : [null];\n\n const actionEntries = actionKinds(entry.setup);\n const actionVariants = selectedAction\n ? [selectedAction]\n : actionEntries.length > 1\n ? actionEntries\n : [null];\n\n const walk = (node: unknown, path: string): void => {\n if (Array.isArray(node)) {\n node.forEach((item, index) => walk(item, `${path}[${index}]`));\n return;\n }\n if (typeof node === \"object\" && node !== null) {\n Object.entries(node).forEach(([key, item]) =>\n walk(item, path ? `${path}.${key}` : key),\n );\n return;\n }\n if (typeof node === \"string\") {\n const names = collectPlaceholderFields(node);\n if (names.length > 0) {\n mapping[path] = Array.from(\n new Set([...(mapping[path] ?? []), ...names]),\n );\n }\n }\n };\n\n actionVariants.forEach((actionVariant) => {\n triggerVariants.forEach((triggerVariant) => {\n const names = Object.keys(\n collectFields(entry.setup, triggerVariant, actionVariant),\n );\n const variantStandIns = Object.fromEntries(\n names.map((name) => [name, `{{form.${name}}}`]),\n );\n const template = buildCreatePayload(\n entry,\n variantStandIns,\n actionVariant === \"upload\" ? \"\" : PREFLIGHT_TARBALL_PATH,\n triggerVariant,\n actionVariant,\n );\n if (template) walk(template, \"\");\n });\n });\n return mapping;\n}\n"],"mappings":";;;;;AA6CA,SAAgB,EACd,GACA,GACQ;CACR,IAAM,IAAO,EAAmB,GAAO,CAAc;CAKrD,OAJI,MAAS,WAAiB,EAAsB,cAAc,IAC9D,MAAS,YAAY,MAAS,WACzB,EAAsB,cAAc,IAEtC,EAAsB,cAAc;AAC7C;AAGA,SAAgB,IAAmC;CACjD,OAAO,EAAsB,SAAS;AACxC;AAUA,SAAS,EACP,GACQ;CACR,IAAM,IAAO,EAAsB,CAAI;CACvC,IAAI,CAAC,GACH,MAAU,MACR,mDAAmD,EAAK,iFAE1D;CAEF,OAAO;AACT;AAoCA,SAAgB,EAAc,GAA4B;CACxD,OAAO,EAAM,MAAM,SAAS,YAAY,EAAM,MAAM,WAAW,KAAA;AACjE;AASA,SAAgB,EACd,GACA,GACuC;CACvC,IAAI,CAAC,GAAO,OAAO;CACnB,IAAI,EAAc,CAAK,GAAG,OAAO;CACjC,IAAM,IAAU,EAAM,MAAM,WAAW,CAAC;CACxC,IAAI,KAAkB,KAAkB,GACtC,OAAO;CAET,IAAM,IAAO,EAAY,EAAM,KAAK;CAEpC,OADI,EAAK,WAAW,IAAU,EAAK,KAC5B;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-local-validation.cjs","names":[],"sources":["../../src/manifests/manifest-local-validation.ts"],"sourcesContent":["/**\n * The form model, and the host's own check of what the user typed into it.\n *\n * Local validation is a convenience, not the authority: it is instant and needs\n * no round trip, so it catches empty required fields and obvious typos.\n * Deployment-specific questions (\"is a one-minute schedule allowed here?\")\n * belong to preflight, which is authoritative.\n *\n * Errors are returned as codes rather than sentences so the host can render\n * them through its own translations; only manifest-authored copy is literal.\n */\n\nimport type {\n DeploymentCapabilities,\n SetupActionKind,\n SetupBlock,\n SetupFieldOption,\n SetupFormField,\n SetupFormFields,\n SetupFormValue,\n SetupFormValues,\n} from \"./types\";\n\n/** Characters that would break out of an expression string literal. */\nconst UNSAFE_EXPRESSION_LITERAL_PATTERN = /[\"'\\\\]/;\n\nexport type SetupFieldError =\n | { code: \"required\" }\n | { code: \"minLength\"; length: number }\n | { code: \"maxLength\"; length: number }\n | { code: \"min\"; value: number }\n | { code: \"max\"; value: number }\n | { code: \"invalidOption\" }\n | { code: \"unsafeExpressionLiteral\" };\n\nexport type SetupFieldErrors = Record<string, SetupFieldError>;\n\n/** Field constraints supplied by the deployment rather than by the manifest. */\nexport interface SetupFieldOverride {\n options?: SetupFieldOption[];\n}\n\nexport function triggerKinds(setup: SetupBlock): string[] {\n return Object.keys(setup.form.triggers ?? {});\n}\n\nexport function initialTriggerKind(setup: SetupBlock): string | null {\n return triggerKinds(setup)[0] ?? null;\n}\n\nexport function actionKinds(setup: SetupBlock): SetupActionKind[] {\n return Object.keys(setup.actions ?? {}) as SetupActionKind[];\n}\n\nexport function initialActionKind(setup: SetupBlock): SetupActionKind | null {\n return actionKinds(setup)[0] ?? null;\n}\n\nexport type SetupFieldOverrides = Record<string, SetupFieldOverride>;\n\n/**\n * Every input the form declares, keyed by name, whichever half it is in.\n *\n * Trigger inputs come first so the user is asked when it runs before what it\n * runs on, and so every derived view of the form keeps that order. Admission\n * rejects a name declared in both halves, so the merge cannot lose a field.\n */\nexport function collectFields(\n setup: SetupBlock,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFormFields {\n const triggers = setup.form.triggers ?? {};\n const triggerEntries = Object.entries(triggers);\n const activeTrigger =\n selectedTrigger && selectedTrigger in triggers\n ? triggers[selectedTrigger as keyof typeof triggers]\n : triggerEntries.length === 1\n ? triggerEntries[0][1]\n : {};\n const actions = setup.actions ?? {};\n const actionEntries = Object.entries(actions);\n const activeAction =\n selectedAction && selectedAction in actions\n ? actions[selectedAction as SetupActionKind]?.args\n : actionEntries.length === 1\n ? actionEntries[0][1]?.args\n : {};\n return Object.assign(\n {},\n activeTrigger,\n setup.form.args,\n activeAction,\n ) as SetupFormFields;\n}\n\n/**\n * Feed deployment values into form field constraints, so the form offers only\n * what the deployment accepts.\n *\n * A `timezone` field is the case that needs it: a manifest declares no options\n * for one, because the accepted zones belong to the deployment. The cron\n * interval floor is deliberately left to preflight — the deployment owns that\n * limit and states it in its own words, and a local approximation would\n * pre-empt the authoritative message with a worse one.\n */\nexport function resolveFieldOverrides(\n setup: SetupBlock,\n capabilities: DeploymentCapabilities | null,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFieldOverrides {\n const overrides: SetupFieldOverrides = {};\n const fields = collectFields(setup, selectedTrigger, selectedAction);\n const timezones = capabilities?.triggers?.cron?.timezones;\n if (timezones?.length) {\n const options = timezones.map((zone) => ({ value: zone, label: zone }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"timezone\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n if (capabilities?.eventSources?.length) {\n const options = capabilities.eventSources.map((source) => ({\n value: source,\n label: source,\n }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"event-source\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n if (capabilities?.eventTypes?.length) {\n const options = capabilities.eventTypes.map((type) => ({\n value: type,\n label: type,\n }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"event-type\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n return overrides;\n}\n\n/** The options a field offers, after deployment constraints are applied. */\nexport function getFieldOptions(\n name: string,\n field: SetupFormField,\n overrides: SetupFieldOverrides = {},\n): SetupFieldOption[] {\n return overrides[name]?.options ?? field.options ?? [];\n}\n\n/**\n * Every value a field holds, whether it collects one or many.\n *\n * Reading both shapes as a list is what keeps validation, interpolation and\n * the payload mapping from branching on `multiple` at every use.\n */\nfunction isFileValue(value: SetupFormValue | undefined): value is File {\n return typeof File !== \"undefined\" && value instanceof File;\n}\n\nexport function fieldValues(value: SetupFormValue | undefined): string[] {\n if (Array.isArray(value)) return value.filter((item) => item.trim() !== \"\");\n if (isFileValue(value)) return value.name ? [value.name] : [];\n if (typeof value === \"string\" && value.trim() === \"\") return [];\n if (value === undefined || value === null) return [];\n return [String(value)];\n}\n\n/** The single value a field holds, or \"\" for a field collecting several. */\nexport function fieldText(value: SetupFormValue | undefined): string {\n if (Array.isArray(value) || value === undefined || value === null) return \"\";\n if (isFileValue(value)) return value.name;\n return String(value);\n}\n\n/** Initial form state: every declared field, seeded with its declared default. */\nexport function getInitialFormValues(\n setup: SetupBlock,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFormValues {\n return Object.fromEntries(\n Object.entries(collectFields(setup, selectedTrigger, selectedAction)).map(\n ([name, field]) => [\n name,\n // A field collecting several values starts empty rather than holding one\n // blank entry, so \"required\" means \"add one\" rather than \"fill this in\".\n field.multiple ? [] : (field.default ?? \"\"),\n ],\n ),\n );\n}\n\nfunction validateField(\n name: string,\n field: SetupFormField,\n rawValue: SetupFormValue | undefined,\n overrides: SetupFieldOverrides,\n): SetupFieldError | null {\n const entered = fieldValues(rawValue);\n\n if (entered.length === 0) {\n return field.required ? { code: \"required\" } : null;\n }\n\n // Every entry of a multi-value field answers the same field, so each is held\n // to the same rules and the first failure is the one reported.\n const failures = entered\n .map((item) => validateValue(name, field, item.trim(), overrides))\n .filter((error): error is SetupFieldError => error !== null);\n return failures[0] ?? null;\n}\n\nfunction validateValue(\n name: string,\n field: SetupFormField,\n value: string,\n overrides: SetupFieldOverrides,\n): SetupFieldError | null {\n const { minLength, maxLength, min, max, format } = field.constraints ?? {};\n if (minLength !== undefined && value.length < minLength) {\n return { code: \"minLength\", length: minLength };\n }\n if (maxLength !== undefined && value.length > maxLength) {\n return { code: \"maxLength\", length: maxLength };\n }\n if (field.type === \"number\") {\n const numeric = Number(value);\n if (Number.isNaN(numeric)) return { code: \"invalidOption\" };\n if (min !== undefined && numeric < min) return { code: \"min\", value: min };\n if (max !== undefined && numeric > max) return { code: \"max\", value: max };\n }\n if (\n format === \"safeExpressionLiteral\" &&\n UNSAFE_EXPRESSION_LITERAL_PATTERN.test(value)\n ) {\n return { code: \"unsafeExpressionLiteral\" };\n }\n\n // Any field offering a closed set of values, whether the manifest declared it\n // or the deployment supplied it, must be answered from that set.\n const options = getFieldOptions(name, field, overrides);\n if (options.length > 0 && !options.some((option) => option.value === value)) {\n return { code: \"invalidOption\" };\n }\n\n return null;\n}\n\n/** Check every declared field. Returns only the fields that failed. */\nexport function validateFormValues(\n setup: SetupBlock,\n values: SetupFormValues,\n overrides: SetupFieldOverrides = {},\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFieldErrors {\n return Object.entries(\n collectFields(setup, selectedTrigger, selectedAction),\n ).reduce<SetupFieldErrors>((errors, [name, field]) => {\n const error = validateField(name, field, values[name], overrides);\n return error ? { ...errors, [name]: error } : errors;\n }, {});\n}\n"],"mappings":"6CAkDA,SAAgB,EAAY,EAAsC,CAChE,OAAO,OAAO,KAAK,EAAM,SAAW,CAAC,CAAC,CACxC"}
|
|
1
|
+
{"version":3,"file":"manifest-local-validation.cjs","names":[],"sources":["../../src/manifests/manifest-local-validation.ts"],"sourcesContent":["/**\n * The form model, and the host's own check of what the user typed into it.\n *\n * Local validation is a convenience, not the authority: it is instant and needs\n * no round trip, so it catches empty required fields and obvious typos.\n * Deployment-specific questions (\"is a one-minute schedule allowed here?\")\n * belong to preflight, which is authoritative.\n *\n * Errors are returned as codes rather than sentences so the host can render\n * them through its own translations; only manifest-authored copy is literal.\n */\n\nimport type {\n DeploymentCapabilities,\n SetupActionKind,\n SetupBlock,\n SetupFieldOption,\n SetupFormField,\n SetupFormFields,\n SetupFormValue,\n SetupFormValues,\n} from \"./types\";\n\n/** Characters that would break out of an expression string literal. */\nconst UNSAFE_EXPRESSION_LITERAL_PATTERN = /[\"'\\\\]/;\n\nexport type SetupFieldError =\n | { code: \"required\" }\n | { code: \"minLength\"; length: number }\n | { code: \"maxLength\"; length: number }\n | { code: \"min\"; value: number }\n | { code: \"max\"; value: number }\n | { code: \"invalidOption\" }\n | { code: \"unsafeExpressionLiteral\" };\n\nexport type SetupFieldErrors = Record<string, SetupFieldError>;\n\n/** Field constraints supplied by the deployment rather than by the manifest. */\nexport interface SetupFieldOverride {\n options?: SetupFieldOption[];\n}\n\nexport function triggerKinds(setup: SetupBlock): string[] {\n return Object.keys(setup.form.triggers ?? {});\n}\n\nexport function initialTriggerKind(setup: SetupBlock): string | null {\n return triggerKinds(setup)[0] ?? null;\n}\n\nexport function actionKinds(setup: SetupBlock): SetupActionKind[] {\n return Object.keys(setup.actions ?? {}) as SetupActionKind[];\n}\n\nexport function initialActionKind(setup: SetupBlock): SetupActionKind | null {\n return actionKinds(setup)[0] ?? null;\n}\n\nexport type SetupFieldOverrides = Record<string, SetupFieldOverride>;\n\n/**\n * Every input the form declares, keyed by name, whichever half it is in.\n *\n * Trigger inputs come first so the user is asked when it runs before what it\n * runs on, and so every derived view of the form keeps that order. Admission\n * rejects a name declared in both halves, so the merge cannot lose a field.\n */\nexport function collectFields(\n setup: SetupBlock,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFormFields {\n const triggers = setup.form.triggers ?? {};\n const triggerEntries = Object.entries(triggers);\n const activeTrigger =\n selectedTrigger && selectedTrigger in triggers\n ? triggers[selectedTrigger as keyof typeof triggers]\n : triggerEntries.length === 1\n ? triggerEntries[0][1]\n : {};\n const actions = setup.actions ?? {};\n const actionEntries = Object.entries(actions);\n const activeAction =\n selectedAction && selectedAction in actions\n ? actions[selectedAction as SetupActionKind]?.args\n : actionEntries.length === 1\n ? actionEntries[0][1]?.args\n : {};\n return Object.assign(\n {},\n activeTrigger,\n setup.form.args,\n activeAction,\n ) as SetupFormFields;\n}\n\n/**\n * Feed deployment values into form field constraints, so the form offers only\n * what the deployment accepts.\n *\n * A `timezone` field is the case that needs it: a manifest declares no options\n * for one, because the accepted zones belong to the deployment. The cron\n * interval floor is deliberately left to preflight — the deployment owns that\n * limit and states it in its own words, and a local approximation would\n * pre-empt the authoritative message with a worse one.\n */\nexport function resolveFieldOverrides(\n setup: SetupBlock,\n capabilities: DeploymentCapabilities | null,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFieldOverrides {\n const overrides: SetupFieldOverrides = {};\n const fields = collectFields(setup, selectedTrigger, selectedAction);\n const timezones = capabilities?.triggers?.cron?.timezones;\n if (timezones?.length) {\n const options = timezones.map((zone) => ({ value: zone, label: zone }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"timezone\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n if (capabilities?.eventSources?.length) {\n const options = capabilities.eventSources.map((source) => ({\n value: source,\n label: source,\n }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"event-source\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n if (capabilities?.eventTypes?.length) {\n const options = capabilities.eventTypes.map((type) => ({\n value: type,\n label: type,\n }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"event-type\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n return overrides;\n}\n\n/** The options a field offers, after deployment constraints are applied. */\nexport function getFieldOptions(\n name: string,\n field: SetupFormField,\n overrides: SetupFieldOverrides = {},\n): SetupFieldOption[] {\n return overrides[name]?.options ?? field.options ?? [];\n}\n\n/**\n * Every value a field holds, whether it collects one or many.\n *\n * Reading both shapes as a list is what keeps validation, interpolation and\n * the payload mapping from branching on `multiple` at every use.\n */\nfunction isFileValue(value: SetupFormValue | undefined): value is File {\n return typeof File !== \"undefined\" && value instanceof File;\n}\n\nexport function fieldValues(value: SetupFormValue | undefined): string[] {\n if (Array.isArray(value)) return value.filter((item) => item.trim() !== \"\");\n if (isFileValue(value)) return value.name ? [value.name] : [];\n if (typeof value === \"string\" && value.trim() === \"\") return [];\n if (value === undefined || value === null) return [];\n return [String(value)];\n}\n\n/** The single value a field holds, or \"\" for a field collecting several. */\nexport function fieldText(value: SetupFormValue | undefined): string {\n if (Array.isArray(value) || value === undefined || value === null) return \"\";\n if (isFileValue(value)) return value.name;\n return String(value);\n}\n\n/** Initial form state: every declared field, seeded with its declared default. */\nexport function getInitialFormValues(\n setup: SetupBlock,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFormValues {\n return Object.fromEntries(\n Object.entries(collectFields(setup, selectedTrigger, selectedAction)).map(\n ([name, field]) => [\n name,\n // A field collecting several values starts empty rather than holding one\n // blank entry, so \"required\" means \"add one\" rather than \"fill this in\".\n field.multiple ? [] : (field.default ?? \"\"),\n ],\n ),\n );\n}\n\nfunction validateField(\n name: string,\n field: SetupFormField,\n rawValue: SetupFormValue | undefined,\n overrides: SetupFieldOverrides,\n): SetupFieldError | null {\n const entered = fieldValues(rawValue);\n\n if (entered.length === 0) {\n return field.required ? { code: \"required\" } : null;\n }\n\n // Every entry of a multi-value field answers the same field, so each is held\n // to the same rules and the first failure is the one reported.\n const failures = entered\n .map((item) => validateValue(name, field, item.trim(), overrides))\n .filter((error): error is SetupFieldError => error !== null);\n return failures[0] ?? null;\n}\n\nfunction validateValue(\n name: string,\n field: SetupFormField,\n value: string,\n overrides: SetupFieldOverrides,\n): SetupFieldError | null {\n const { minLength, maxLength, min, max, format } = field.constraints ?? {};\n if (minLength !== undefined && value.length < minLength) {\n return { code: \"minLength\", length: minLength };\n }\n if (maxLength !== undefined && value.length > maxLength) {\n return { code: \"maxLength\", length: maxLength };\n }\n if (field.type === \"number\") {\n const numeric = Number(value);\n if (Number.isNaN(numeric)) return { code: \"invalidOption\" };\n if (min !== undefined && numeric < min) return { code: \"min\", value: min };\n if (max !== undefined && numeric > max) return { code: \"max\", value: max };\n }\n if (\n format === \"safeExpressionLiteral\" &&\n UNSAFE_EXPRESSION_LITERAL_PATTERN.test(value)\n ) {\n return { code: \"unsafeExpressionLiteral\" };\n }\n\n // Any field offering a closed set of values, whether the manifest declared it\n // or the deployment supplied it, must be answered from that set.\n const options = getFieldOptions(name, field, overrides);\n if (options.length > 0 && !options.some((option) => option.value === value)) {\n return { code: \"invalidOption\" };\n }\n\n return null;\n}\n\n/** Check every declared field. Returns only the fields that failed. */\nexport function validateFormValues(\n setup: SetupBlock,\n values: SetupFormValues,\n overrides: SetupFieldOverrides = {},\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFieldErrors {\n return Object.entries(\n collectFields(setup, selectedTrigger, selectedAction),\n ).reduce<SetupFieldErrors>((errors, [name, field]) => {\n if (values.agent_profile_id && field.type === \"llm-profile\") return errors;\n const error = validateField(name, field, values[name], overrides);\n return error ? { ...errors, [name]: error } : errors;\n }, {});\n}\n"],"mappings":"6CAkDA,SAAgB,EAAY,EAAsC,CAChE,OAAO,OAAO,KAAK,EAAM,SAAW,CAAC,CAAC,CACxC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-local-validation.js","names":[],"sources":["../../src/manifests/manifest-local-validation.ts"],"sourcesContent":["/**\n * The form model, and the host's own check of what the user typed into it.\n *\n * Local validation is a convenience, not the authority: it is instant and needs\n * no round trip, so it catches empty required fields and obvious typos.\n * Deployment-specific questions (\"is a one-minute schedule allowed here?\")\n * belong to preflight, which is authoritative.\n *\n * Errors are returned as codes rather than sentences so the host can render\n * them through its own translations; only manifest-authored copy is literal.\n */\n\nimport type {\n DeploymentCapabilities,\n SetupActionKind,\n SetupBlock,\n SetupFieldOption,\n SetupFormField,\n SetupFormFields,\n SetupFormValue,\n SetupFormValues,\n} from \"./types\";\n\n/** Characters that would break out of an expression string literal. */\nconst UNSAFE_EXPRESSION_LITERAL_PATTERN = /[\"'\\\\]/;\n\nexport type SetupFieldError =\n | { code: \"required\" }\n | { code: \"minLength\"; length: number }\n | { code: \"maxLength\"; length: number }\n | { code: \"min\"; value: number }\n | { code: \"max\"; value: number }\n | { code: \"invalidOption\" }\n | { code: \"unsafeExpressionLiteral\" };\n\nexport type SetupFieldErrors = Record<string, SetupFieldError>;\n\n/** Field constraints supplied by the deployment rather than by the manifest. */\nexport interface SetupFieldOverride {\n options?: SetupFieldOption[];\n}\n\nexport function triggerKinds(setup: SetupBlock): string[] {\n return Object.keys(setup.form.triggers ?? {});\n}\n\nexport function initialTriggerKind(setup: SetupBlock): string | null {\n return triggerKinds(setup)[0] ?? null;\n}\n\nexport function actionKinds(setup: SetupBlock): SetupActionKind[] {\n return Object.keys(setup.actions ?? {}) as SetupActionKind[];\n}\n\nexport function initialActionKind(setup: SetupBlock): SetupActionKind | null {\n return actionKinds(setup)[0] ?? null;\n}\n\nexport type SetupFieldOverrides = Record<string, SetupFieldOverride>;\n\n/**\n * Every input the form declares, keyed by name, whichever half it is in.\n *\n * Trigger inputs come first so the user is asked when it runs before what it\n * runs on, and so every derived view of the form keeps that order. Admission\n * rejects a name declared in both halves, so the merge cannot lose a field.\n */\nexport function collectFields(\n setup: SetupBlock,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFormFields {\n const triggers = setup.form.triggers ?? {};\n const triggerEntries = Object.entries(triggers);\n const activeTrigger =\n selectedTrigger && selectedTrigger in triggers\n ? triggers[selectedTrigger as keyof typeof triggers]\n : triggerEntries.length === 1\n ? triggerEntries[0][1]\n : {};\n const actions = setup.actions ?? {};\n const actionEntries = Object.entries(actions);\n const activeAction =\n selectedAction && selectedAction in actions\n ? actions[selectedAction as SetupActionKind]?.args\n : actionEntries.length === 1\n ? actionEntries[0][1]?.args\n : {};\n return Object.assign(\n {},\n activeTrigger,\n setup.form.args,\n activeAction,\n ) as SetupFormFields;\n}\n\n/**\n * Feed deployment values into form field constraints, so the form offers only\n * what the deployment accepts.\n *\n * A `timezone` field is the case that needs it: a manifest declares no options\n * for one, because the accepted zones belong to the deployment. The cron\n * interval floor is deliberately left to preflight — the deployment owns that\n * limit and states it in its own words, and a local approximation would\n * pre-empt the authoritative message with a worse one.\n */\nexport function resolveFieldOverrides(\n setup: SetupBlock,\n capabilities: DeploymentCapabilities | null,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFieldOverrides {\n const overrides: SetupFieldOverrides = {};\n const fields = collectFields(setup, selectedTrigger, selectedAction);\n const timezones = capabilities?.triggers?.cron?.timezones;\n if (timezones?.length) {\n const options = timezones.map((zone) => ({ value: zone, label: zone }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"timezone\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n if (capabilities?.eventSources?.length) {\n const options = capabilities.eventSources.map((source) => ({\n value: source,\n label: source,\n }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"event-source\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n if (capabilities?.eventTypes?.length) {\n const options = capabilities.eventTypes.map((type) => ({\n value: type,\n label: type,\n }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"event-type\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n return overrides;\n}\n\n/** The options a field offers, after deployment constraints are applied. */\nexport function getFieldOptions(\n name: string,\n field: SetupFormField,\n overrides: SetupFieldOverrides = {},\n): SetupFieldOption[] {\n return overrides[name]?.options ?? field.options ?? [];\n}\n\n/**\n * Every value a field holds, whether it collects one or many.\n *\n * Reading both shapes as a list is what keeps validation, interpolation and\n * the payload mapping from branching on `multiple` at every use.\n */\nfunction isFileValue(value: SetupFormValue | undefined): value is File {\n return typeof File !== \"undefined\" && value instanceof File;\n}\n\nexport function fieldValues(value: SetupFormValue | undefined): string[] {\n if (Array.isArray(value)) return value.filter((item) => item.trim() !== \"\");\n if (isFileValue(value)) return value.name ? [value.name] : [];\n if (typeof value === \"string\" && value.trim() === \"\") return [];\n if (value === undefined || value === null) return [];\n return [String(value)];\n}\n\n/** The single value a field holds, or \"\" for a field collecting several. */\nexport function fieldText(value: SetupFormValue | undefined): string {\n if (Array.isArray(value) || value === undefined || value === null) return \"\";\n if (isFileValue(value)) return value.name;\n return String(value);\n}\n\n/** Initial form state: every declared field, seeded with its declared default. */\nexport function getInitialFormValues(\n setup: SetupBlock,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFormValues {\n return Object.fromEntries(\n Object.entries(collectFields(setup, selectedTrigger, selectedAction)).map(\n ([name, field]) => [\n name,\n // A field collecting several values starts empty rather than holding one\n // blank entry, so \"required\" means \"add one\" rather than \"fill this in\".\n field.multiple ? [] : (field.default ?? \"\"),\n ],\n ),\n );\n}\n\nfunction validateField(\n name: string,\n field: SetupFormField,\n rawValue: SetupFormValue | undefined,\n overrides: SetupFieldOverrides,\n): SetupFieldError | null {\n const entered = fieldValues(rawValue);\n\n if (entered.length === 0) {\n return field.required ? { code: \"required\" } : null;\n }\n\n // Every entry of a multi-value field answers the same field, so each is held\n // to the same rules and the first failure is the one reported.\n const failures = entered\n .map((item) => validateValue(name, field, item.trim(), overrides))\n .filter((error): error is SetupFieldError => error !== null);\n return failures[0] ?? null;\n}\n\nfunction validateValue(\n name: string,\n field: SetupFormField,\n value: string,\n overrides: SetupFieldOverrides,\n): SetupFieldError | null {\n const { minLength, maxLength, min, max, format } = field.constraints ?? {};\n if (minLength !== undefined && value.length < minLength) {\n return { code: \"minLength\", length: minLength };\n }\n if (maxLength !== undefined && value.length > maxLength) {\n return { code: \"maxLength\", length: maxLength };\n }\n if (field.type === \"number\") {\n const numeric = Number(value);\n if (Number.isNaN(numeric)) return { code: \"invalidOption\" };\n if (min !== undefined && numeric < min) return { code: \"min\", value: min };\n if (max !== undefined && numeric > max) return { code: \"max\", value: max };\n }\n if (\n format === \"safeExpressionLiteral\" &&\n UNSAFE_EXPRESSION_LITERAL_PATTERN.test(value)\n ) {\n return { code: \"unsafeExpressionLiteral\" };\n }\n\n // Any field offering a closed set of values, whether the manifest declared it\n // or the deployment supplied it, must be answered from that set.\n const options = getFieldOptions(name, field, overrides);\n if (options.length > 0 && !options.some((option) => option.value === value)) {\n return { code: \"invalidOption\" };\n }\n\n return null;\n}\n\n/** Check every declared field. Returns only the fields that failed. */\nexport function validateFormValues(\n setup: SetupBlock,\n values: SetupFormValues,\n overrides: SetupFieldOverrides = {},\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFieldErrors {\n return Object.entries(\n collectFields(setup, selectedTrigger, selectedAction),\n ).reduce<SetupFieldErrors>((errors, [name, field]) => {\n const error = validateField(name, field, values[name], overrides);\n return error ? { ...errors, [name]: error } : errors;\n }, {});\n}\n"],"mappings":";AAkDA,SAAgB,EAAY,GAAsC;CAChE,OAAO,OAAO,KAAK,EAAM,WAAW,CAAC,CAAC;AACxC"}
|
|
1
|
+
{"version":3,"file":"manifest-local-validation.js","names":[],"sources":["../../src/manifests/manifest-local-validation.ts"],"sourcesContent":["/**\n * The form model, and the host's own check of what the user typed into it.\n *\n * Local validation is a convenience, not the authority: it is instant and needs\n * no round trip, so it catches empty required fields and obvious typos.\n * Deployment-specific questions (\"is a one-minute schedule allowed here?\")\n * belong to preflight, which is authoritative.\n *\n * Errors are returned as codes rather than sentences so the host can render\n * them through its own translations; only manifest-authored copy is literal.\n */\n\nimport type {\n DeploymentCapabilities,\n SetupActionKind,\n SetupBlock,\n SetupFieldOption,\n SetupFormField,\n SetupFormFields,\n SetupFormValue,\n SetupFormValues,\n} from \"./types\";\n\n/** Characters that would break out of an expression string literal. */\nconst UNSAFE_EXPRESSION_LITERAL_PATTERN = /[\"'\\\\]/;\n\nexport type SetupFieldError =\n | { code: \"required\" }\n | { code: \"minLength\"; length: number }\n | { code: \"maxLength\"; length: number }\n | { code: \"min\"; value: number }\n | { code: \"max\"; value: number }\n | { code: \"invalidOption\" }\n | { code: \"unsafeExpressionLiteral\" };\n\nexport type SetupFieldErrors = Record<string, SetupFieldError>;\n\n/** Field constraints supplied by the deployment rather than by the manifest. */\nexport interface SetupFieldOverride {\n options?: SetupFieldOption[];\n}\n\nexport function triggerKinds(setup: SetupBlock): string[] {\n return Object.keys(setup.form.triggers ?? {});\n}\n\nexport function initialTriggerKind(setup: SetupBlock): string | null {\n return triggerKinds(setup)[0] ?? null;\n}\n\nexport function actionKinds(setup: SetupBlock): SetupActionKind[] {\n return Object.keys(setup.actions ?? {}) as SetupActionKind[];\n}\n\nexport function initialActionKind(setup: SetupBlock): SetupActionKind | null {\n return actionKinds(setup)[0] ?? null;\n}\n\nexport type SetupFieldOverrides = Record<string, SetupFieldOverride>;\n\n/**\n * Every input the form declares, keyed by name, whichever half it is in.\n *\n * Trigger inputs come first so the user is asked when it runs before what it\n * runs on, and so every derived view of the form keeps that order. Admission\n * rejects a name declared in both halves, so the merge cannot lose a field.\n */\nexport function collectFields(\n setup: SetupBlock,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFormFields {\n const triggers = setup.form.triggers ?? {};\n const triggerEntries = Object.entries(triggers);\n const activeTrigger =\n selectedTrigger && selectedTrigger in triggers\n ? triggers[selectedTrigger as keyof typeof triggers]\n : triggerEntries.length === 1\n ? triggerEntries[0][1]\n : {};\n const actions = setup.actions ?? {};\n const actionEntries = Object.entries(actions);\n const activeAction =\n selectedAction && selectedAction in actions\n ? actions[selectedAction as SetupActionKind]?.args\n : actionEntries.length === 1\n ? actionEntries[0][1]?.args\n : {};\n return Object.assign(\n {},\n activeTrigger,\n setup.form.args,\n activeAction,\n ) as SetupFormFields;\n}\n\n/**\n * Feed deployment values into form field constraints, so the form offers only\n * what the deployment accepts.\n *\n * A `timezone` field is the case that needs it: a manifest declares no options\n * for one, because the accepted zones belong to the deployment. The cron\n * interval floor is deliberately left to preflight — the deployment owns that\n * limit and states it in its own words, and a local approximation would\n * pre-empt the authoritative message with a worse one.\n */\nexport function resolveFieldOverrides(\n setup: SetupBlock,\n capabilities: DeploymentCapabilities | null,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFieldOverrides {\n const overrides: SetupFieldOverrides = {};\n const fields = collectFields(setup, selectedTrigger, selectedAction);\n const timezones = capabilities?.triggers?.cron?.timezones;\n if (timezones?.length) {\n const options = timezones.map((zone) => ({ value: zone, label: zone }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"timezone\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n if (capabilities?.eventSources?.length) {\n const options = capabilities.eventSources.map((source) => ({\n value: source,\n label: source,\n }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"event-source\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n if (capabilities?.eventTypes?.length) {\n const options = capabilities.eventTypes.map((type) => ({\n value: type,\n label: type,\n }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"event-type\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n return overrides;\n}\n\n/** The options a field offers, after deployment constraints are applied. */\nexport function getFieldOptions(\n name: string,\n field: SetupFormField,\n overrides: SetupFieldOverrides = {},\n): SetupFieldOption[] {\n return overrides[name]?.options ?? field.options ?? [];\n}\n\n/**\n * Every value a field holds, whether it collects one or many.\n *\n * Reading both shapes as a list is what keeps validation, interpolation and\n * the payload mapping from branching on `multiple` at every use.\n */\nfunction isFileValue(value: SetupFormValue | undefined): value is File {\n return typeof File !== \"undefined\" && value instanceof File;\n}\n\nexport function fieldValues(value: SetupFormValue | undefined): string[] {\n if (Array.isArray(value)) return value.filter((item) => item.trim() !== \"\");\n if (isFileValue(value)) return value.name ? [value.name] : [];\n if (typeof value === \"string\" && value.trim() === \"\") return [];\n if (value === undefined || value === null) return [];\n return [String(value)];\n}\n\n/** The single value a field holds, or \"\" for a field collecting several. */\nexport function fieldText(value: SetupFormValue | undefined): string {\n if (Array.isArray(value) || value === undefined || value === null) return \"\";\n if (isFileValue(value)) return value.name;\n return String(value);\n}\n\n/** Initial form state: every declared field, seeded with its declared default. */\nexport function getInitialFormValues(\n setup: SetupBlock,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFormValues {\n return Object.fromEntries(\n Object.entries(collectFields(setup, selectedTrigger, selectedAction)).map(\n ([name, field]) => [\n name,\n // A field collecting several values starts empty rather than holding one\n // blank entry, so \"required\" means \"add one\" rather than \"fill this in\".\n field.multiple ? [] : (field.default ?? \"\"),\n ],\n ),\n );\n}\n\nfunction validateField(\n name: string,\n field: SetupFormField,\n rawValue: SetupFormValue | undefined,\n overrides: SetupFieldOverrides,\n): SetupFieldError | null {\n const entered = fieldValues(rawValue);\n\n if (entered.length === 0) {\n return field.required ? { code: \"required\" } : null;\n }\n\n // Every entry of a multi-value field answers the same field, so each is held\n // to the same rules and the first failure is the one reported.\n const failures = entered\n .map((item) => validateValue(name, field, item.trim(), overrides))\n .filter((error): error is SetupFieldError => error !== null);\n return failures[0] ?? null;\n}\n\nfunction validateValue(\n name: string,\n field: SetupFormField,\n value: string,\n overrides: SetupFieldOverrides,\n): SetupFieldError | null {\n const { minLength, maxLength, min, max, format } = field.constraints ?? {};\n if (minLength !== undefined && value.length < minLength) {\n return { code: \"minLength\", length: minLength };\n }\n if (maxLength !== undefined && value.length > maxLength) {\n return { code: \"maxLength\", length: maxLength };\n }\n if (field.type === \"number\") {\n const numeric = Number(value);\n if (Number.isNaN(numeric)) return { code: \"invalidOption\" };\n if (min !== undefined && numeric < min) return { code: \"min\", value: min };\n if (max !== undefined && numeric > max) return { code: \"max\", value: max };\n }\n if (\n format === \"safeExpressionLiteral\" &&\n UNSAFE_EXPRESSION_LITERAL_PATTERN.test(value)\n ) {\n return { code: \"unsafeExpressionLiteral\" };\n }\n\n // Any field offering a closed set of values, whether the manifest declared it\n // or the deployment supplied it, must be answered from that set.\n const options = getFieldOptions(name, field, overrides);\n if (options.length > 0 && !options.some((option) => option.value === value)) {\n return { code: \"invalidOption\" };\n }\n\n return null;\n}\n\n/** Check every declared field. Returns only the fields that failed. */\nexport function validateFormValues(\n setup: SetupBlock,\n values: SetupFormValues,\n overrides: SetupFieldOverrides = {},\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFieldErrors {\n return Object.entries(\n collectFields(setup, selectedTrigger, selectedAction),\n ).reduce<SetupFieldErrors>((errors, [name, field]) => {\n if (values.agent_profile_id && field.type === \"llm-profile\") return errors;\n const error = validateField(name, field, values[name], overrides);\n return error ? { ...errors, [name]: error } : errors;\n }, {});\n}\n"],"mappings":";AAkDA,SAAgB,EAAY,GAAsC;CAChE,OAAO,OAAO,KAAK,EAAM,WAAW,CAAC,CAAC;AACxC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
require("../_virtual/_rolldown/runtime.cjs");const e=require("./types.cjs");var t=/^[a-z0-9]+(-[a-z0-9]+)*$/,n=/^[a-z][A-Za-z0-9]*$/,r=/<[A-Za-z/!]/,i=/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?$/,a=/^[A-Za-z0-9 ._/-]+$/,o=/^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/,s=/^(skills|automations)\/[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/,c=RegExp(`\\{\\{(?!(?:${e.SETUP_PLACEHOLDER_NAMESPACES.join(`|`)})\\.[A-Za-z0-9_.]+\\}\\})`),l=[`direct`,`assisted`],u=[`text`,`textarea`,`select`,`number`,`cron`,`timezone`,`repo-picker`,`llm-profile`,`event-source`,`event-type`,`plugin-sources`,`tarball-upload`],d=[`github`,`gitlab`,`bitbucket`],f=[`cron`,`event`],p=[`safeExpressionLiteral`],m=2e3;function h(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function g(e,t){return typeof e==`string`&&t.includes(e)}function _(e,t){return typeof e==`number`&&Number.isInteger(e)&&e>=t}var v=class{errors=[];fail(e,t){return this.errors.push(`${e}: ${t}`),!1}copy(e,t){return typeof e!=`string`||e.length===0?this.fail(t,`must be a non-empty string`):r.test(e)?this.fail(t,`must not contain markup`):!0}placeholders(e,t){return c.test(e)?this.fail(t,`uses an unknown placeholder namespace`):!0}templateCopy(e,t){return this.copy(e,t)&&this.placeholders(e,t)}templateValue(e,t){return typeof e!=`string`||e.length===0?this.fail(t,`must be a non-empty string`):this.placeholders(e,t)}absent(e,t,n,r){return t in e?this.fail(`${n}.${t}`,r):!0}};function y(e,t){return typeof e==`string`&&t.test(e)&&!e.split(`/`).some(e=>e===`.`||e===`..`)}function b(e){if(typeof e!=`string`||!a.test(e))return!1;let t=e.split(` `).filter(e=>e.length>0);return t.length>0&&t.every(e=>!e.startsWith(`/`)&&!e.split(`/`).some(e=>e===`..`))}function x(e,t){if(!h(t)){e.fail(`requires`,`must be an object`);return}let{integrations:n,features:r}=t;h(n)?Object.entries(n).forEach(([t,n])=>{let r=`requires.integrations.${t}`;if(!h(n)){e.fail(r,`must be an object`);return}e.copy(n.message,`${r}.message`),`required`in n&&n.required!==!1&&e.fail(`${r}.required`,`may only be false`);let i=[`message`,`required`];Object.keys(n).filter(e=>!i.includes(e)).forEach(t=>e.fail(`${r}.${t}`,`is not allowed`))}):e.fail(`requires.integrations`,`must be an object`),r!==void 0&&(!Array.isArray(r)||r.length===0?e.fail(`requires.features`,`must be a non-empty array`):r.forEach((t,n)=>{(typeof t!=`string`||t.length===0)&&e.fail(`requires.features[${n}]`,`must be a non-empty string`)}))}function S(e,t,n){if(!h(t)||Object.keys(t).length===0){e.fail(n,`must be a non-empty object`);return}let{minLength:r,maxLength:i,min:a,max:o,format:s}=t;r!==void 0&&!_(r,0)&&e.fail(`${n}.minLength`,`must be a non-negative integer`),i!==void 0&&!_(i,1)&&e.fail(`${n}.maxLength`,`must be a positive integer`),a!==void 0&&!_(a,0)&&e.fail(`${n}.min`,`must be a non-negative integer`),o!==void 0&&!_(o,1)&&e.fail(`${n}.max`,`must be a positive integer`),s!==void 0&&!g(s,p)&&e.fail(`${n}.format`,`is not a supported format`)}function C(e,t,n){if(!h(t)){e.fail(n,`must be an object`);return}let{type:r,label:i,help:a,placeholder:o,required:s,provider:c,options:l}=t;g(r,u)||e.fail(`${n}.type`,`is not a supported field type`),e.copy(i,`${n}.label`),e.copy(a,`${n}.help`),o!==void 0&&e.copy(o,`${n}.placeholder`),t.default!==void 0&&typeof t.default!=`string`&&typeof t.default!=`number`&&typeof t.default!=`boolean`&&t.default!==null&&e.fail(`${n}.default`,`must be a string, number, boolean or null`),typeof s!=`boolean`&&e.fail(`${n}.required`,`must be a boolean`),c!==void 0&&!g(c,d)&&e.fail(`${n}.provider`,`is not a supported provider`),r===`repo-picker`&&c===void 0&&e.fail(`${n}.provider`,`is required for a repository field`),t.multiple!==void 0&&(t.multiple!==!0||r!==`repo-picker`)&&e.fail(`${n}.multiple`,`may only be true, and only on a repository field`),l!==void 0&&(r===`select`?!Array.isArray(l)||l.length===0?e.fail(`${n}.options`,`must be a non-empty array`):l.forEach((t,r)=>{let i=`${n}.options[${r}]`;if(!h(t)){e.fail(i,`must be an object`);return}(typeof t.value!=`string`||t.value.length===0)&&e.fail(`${i}.value`,`must be a non-empty string`),e.copy(t.label,`${i}.label`)}):e.fail(`${n}.options`,`is only allowed on a select field`)),t.constraints!==void 0&&S(e,t.constraints,`${n}.constraints`)}function w(e,t,r){return!h(t)||Object.keys(t).length===0?(e.fail(r,`must be a non-empty object`),[]):Object.entries(t).map(([t,i])=>(n.test(t)||e.fail(`${r}.${t}`,`is not a valid field name`),C(e,i,`${r}.${t}`),t))}function T(e,t){if(!h(t))return e.fail(`setup.form`,`must be an object`),{kinds:[],fieldNames:[]};t.note!==void 0&&e.copy(t.note,`setup.form.note`);let n=w(e,t.args,`setup.form.args`),r=[];t.triggers!==void 0&&(!h(t.triggers)||Object.keys(t.triggers).length===0?e.fail(`setup.form.triggers`,`must be a non-empty object`):Object.entries(t.triggers).forEach(([t,i])=>{if(!g(t,f)){e.fail(`setup.form.triggers.${t}`,`is not a trigger kind`);return}r.push(t),n.push(...w(e,i,`setup.form.triggers.${t}`))}));let i=n.filter((e,t)=>n.indexOf(e)!==t);return Array.from(new Set(i)).forEach(t=>e.fail(`setup.form.${t}`,`is declared more than once`)),{kinds:r,fieldNames:n}}function E(e,t,n){if(!Array.isArray(t)||t.length===0){e.fail(n,`must be a non-empty array`);return}t.forEach((t,r)=>{(typeof t!=`string`||t.length===0)&&e.fail(`${n}[${r}]`,`must be a non-empty string`)})}function D(e,t,n,r){let i=`setup.actions.${t}`;if(!h(n)){e.fail(i,`must be an object`);return}e.copy(n.label,`${i}.label`),e.copy(n.help,`${i}.help`),E(e,n.features,`${i}.features`),w(e,n.args,`${i}.args`).filter(e=>r.includes(e)).forEach(t=>e.fail(`${i}.args.${t}`,`is already declared by the form`)),t===`prompt`?e.templateValue(n.prompt,`${i}.prompt`):t===`plugin`?(e.templateValue(n.prompt,`${i}.prompt`),e.templateValue(n.plugins,`${i}.plugins`)):t===`upload`?(e.templateValue(n.tarballPath,`${i}.tarballPath`),e.templateValue(n.entrypoint,`${i}.entrypoint`),n.setupScript!==void 0&&e.templateValue(n.setupScript,`${i}.setupScript`)):e.fail(i,`is not a supported action kind`);let a={prompt:[`label`,`help`,`features`,`args`,`prompt`],plugin:[`label`,`help`,`features`,`args`,`prompt`,`plugins`],upload:[`label`,`help`,`features`,`args`,`tarballPath`,`entrypoint`,`setupScript`]}[t]??[];Object.keys(n).filter(e=>!a.includes(e)).forEach(t=>e.fail(`${i}.${t}`,`is not allowed`))}function O(e,t,n){if(!h(t)||Object.keys(t).length===0){e.fail(`setup.actions`,`must be a non-empty object`);return}Object.entries(t).forEach(([t,r])=>D(e,t,r,n))}function k(e,t){return h(e)?Object.values(e).some(e=>h(e)&&e.type===t):!1}function A(e){return h(e)&&k(e.args,`repo-picker`)}function j(e){let t=h(e.form)?e.form:{},n=h(t.triggers)?t.triggers:{},r=n.event;return k(r,`event-source`)||`event`in n&&Object.keys(n).length===1&&k(r,`repo-picker`)||k(t.args,`repo-picker`)?!0:!h(e.actions)||Object.keys(e.actions).length===0?!1:Object.values(e.actions).every(A)}function M(e,t){e.templateCopy(t,`setup.message`)&&t.length>m&&e.fail(`setup.message`,`must be at most ${m} characters`)}function N(e,t){if(!h(t)){e.fail(`setup.bundle`,`must be an object`);return}(typeof t.version!=`string`||!i.test(t.version))&&e.fail(`setup.bundle.version`,`must be a semantic version`),b(t.entrypoint)||e.fail(`setup.bundle.entrypoint`,`must be a plain command that stays inside the archive`),t.setupScript!==void 0&&!y(t.setupScript,o)&&e.fail(`setup.bundle.setupScript`,`must be a relative path`),t.timeout!==void 0&&!_(t.timeout,1)&&e.fail(`setup.bundle.timeout`,`must be a positive integer`),!h(t.files)||Object.keys(t.files).length===0?e.fail(`setup.bundle.files`,`must be a non-empty object`):(Object.entries(t.files).forEach(([t,n])=>{y(t,o)||e.fail(`setup.bundle.files.${t}`,`is not a packed path`),t===`config.json`&&e.fail(`setup.bundle.files.${t}`,`is the name the rendered config is packed under`),y(n,s)||e.fail(`setup.bundle.files.${t}`,`must name a file under skills/ or automations/`)}),typeof t.setupScript==`string`&&!(t.setupScript in t.files)&&e.fail(`setup.bundle.setupScript`,`must name a packed file`)),!h(t.config)||Object.keys(t.config).length===0?e.fail(`setup.bundle.config`,`must be a non-empty object`):P(e,t.config,`setup.bundle.config`)}function P(e,t,n){if(typeof t==`string`){e.templateValue(t,n);return}if(Array.isArray(t)){t.forEach((t,r)=>P(e,t,`${n}[${r}]`));return}if(h(t)){Object.entries(t).forEach(([t,r])=>P(e,r,`${n}.${t}`));return}t!==null&&typeof t!=`number`&&typeof t!=`boolean`&&e.fail(n,`must be a string, number, boolean, null, array or object`)}function F(e,t,n,r){if(!g(t.mode,l)){e.fail(`setup.mode`,`is not a supported mode`);return}if(t.mode===`direct`){let i=t.prompt!==void 0,a=t.bundle!==void 0,o=t.actions!==void 0;[i,a,o].filter(Boolean).length===1?a?N(e,t.bundle):o?O(e,t.actions,r):e.templateValue(t.prompt,`setup.prompt`):e.fail(`setup`,`must declare exactly one of prompt, bundle or actions for direct setup`),t.message!==void 0&&M(e,t.message),n.length===0&&e.fail(`setup.form.triggers`,`must declare at least one trigger kind`),n.includes(`event`)&&!j(t)&&e.fail(`setup.form.triggers.event`,`must declare an event source field or repository picker`),t.filter!==void 0&&(n.includes(`event`)?e.templateValue(t.filter,`setup.filter`):e.fail(`setup.filter`,`is only allowed for an event trigger`));return}M(e,t.message),e.absent(t,`prompt`,`setup`,`is only allowed for direct setup`),e.absent(t,`bundle`,`setup`,`is only allowed for direct setup`),e.absent(t,`actions`,`setup`,`is only allowed for direct setup`),e.absent(t,`filter`,`setup`,`is only allowed for direct setup`)}function I(e){return h(e)&&e.setup!==void 0}function L(e){let n=new v;if(!h(e))return{valid:!1,errors:[`manifest: must be an object`]};let{setup:r}=e;if(!h(r))return{valid:!1,errors:[`setup: must be an object`]};if(r.version!==`1.0`)return{valid:!1,errors:[`setup.version: must be "1.0"`]};(typeof e.id!=`string`||!t.test(e.id))&&n.fail(`id`,`must be a lowercase slug`),n.copy(e.name,`name`),n.copy(e.description,`description`),e.version!==void 0&&(typeof e.version!=`string`||!i.test(e.version))&&n.fail(`version`,`must be a semantic version`),e.skill!==void 0&&(typeof e.skill!=`string`||!t.test(e.skill))&&n.fail(`skill`,`must be a lowercase slug`),x(n,e.requires);let{kinds:a,fieldNames:o}=T(n,r.form);return F(n,r,a,o),{valid:n.errors.length===0,errors:n.errors}}exports.hasSetupBlock=I,exports.validateSetupEntry=L;
|
|
1
|
+
require("../_virtual/_rolldown/runtime.cjs");const e=require("./types.cjs");var t=/^[a-z0-9]+(-[a-z0-9]+)*$/,n=/^[a-z][A-Za-z0-9]*$/,r=/<[A-Za-z/!]/,i=/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?$/,a=/^[A-Za-z0-9 ._/-]+$/,o=/^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/,s=/^(skills|automations|plugins)\/[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/,c=RegExp(`\\{\\{(?!(?:${e.SETUP_PLACEHOLDER_NAMESPACES.join(`|`)})\\.[A-Za-z0-9_.]+\\}\\})`),l=[`direct`,`assisted`],u=[`text`,`textarea`,`select`,`number`,`cron`,`timezone`,`repo-picker`,`llm-profile`,`event-source`,`event-type`,`plugin-sources`,`tarball-upload`],d=[`github`,`gitlab`,`bitbucket`],f=[`cron`,`event`],p=[`safeExpressionLiteral`],m=2e3;function h(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function g(e,t){return typeof e==`string`&&t.includes(e)}function _(e,t){return typeof e==`number`&&Number.isInteger(e)&&e>=t}var v=class{errors=[];fail(e,t){return this.errors.push(`${e}: ${t}`),!1}copy(e,t){return typeof e!=`string`||e.length===0?this.fail(t,`must be a non-empty string`):r.test(e)?this.fail(t,`must not contain markup`):!0}placeholders(e,t){return c.test(e)?this.fail(t,`uses an unknown placeholder namespace`):!0}templateCopy(e,t){return this.copy(e,t)&&this.placeholders(e,t)}templateValue(e,t){return typeof e!=`string`||e.length===0?this.fail(t,`must be a non-empty string`):this.placeholders(e,t)}absent(e,t,n,r){return t in e?this.fail(`${n}.${t}`,r):!0}};function y(e,t){return typeof e==`string`&&t.test(e)&&!e.split(`/`).some(e=>e===`.`||e===`..`)}function b(e){if(typeof e!=`string`||!a.test(e))return!1;let t=e.split(` `).filter(e=>e.length>0);return t.length>0&&t.every(e=>!e.startsWith(`/`)&&!e.split(`/`).some(e=>e===`..`))}function x(e,t){if(!h(t)){e.fail(`requires`,`must be an object`);return}let{integrations:n,features:r}=t;h(n)?Object.entries(n).forEach(([t,n])=>{let r=`requires.integrations.${t}`;if(!h(n)){e.fail(r,`must be an object`);return}e.copy(n.message,`${r}.message`),`required`in n&&n.required!==!1&&e.fail(`${r}.required`,`may only be false`);let i=[`message`,`required`];Object.keys(n).filter(e=>!i.includes(e)).forEach(t=>e.fail(`${r}.${t}`,`is not allowed`))}):e.fail(`requires.integrations`,`must be an object`),r!==void 0&&(!Array.isArray(r)||r.length===0?e.fail(`requires.features`,`must be a non-empty array`):r.forEach((t,n)=>{(typeof t!=`string`||t.length===0)&&e.fail(`requires.features[${n}]`,`must be a non-empty string`)}))}function S(e,t,n){if(!h(t)||Object.keys(t).length===0){e.fail(n,`must be a non-empty object`);return}let{minLength:r,maxLength:i,min:a,max:o,format:s}=t;r!==void 0&&!_(r,0)&&e.fail(`${n}.minLength`,`must be a non-negative integer`),i!==void 0&&!_(i,1)&&e.fail(`${n}.maxLength`,`must be a positive integer`),a!==void 0&&!_(a,0)&&e.fail(`${n}.min`,`must be a non-negative integer`),o!==void 0&&!_(o,1)&&e.fail(`${n}.max`,`must be a positive integer`),s!==void 0&&!g(s,p)&&e.fail(`${n}.format`,`is not a supported format`)}function C(e,t,n){if(!h(t)){e.fail(n,`must be an object`);return}let{type:r,label:i,help:a,placeholder:o,required:s,provider:c,options:l}=t;g(r,u)||e.fail(`${n}.type`,`is not a supported field type`),e.copy(i,`${n}.label`),e.copy(a,`${n}.help`),o!==void 0&&e.copy(o,`${n}.placeholder`),t.default!==void 0&&typeof t.default!=`string`&&typeof t.default!=`number`&&typeof t.default!=`boolean`&&t.default!==null&&e.fail(`${n}.default`,`must be a string, number, boolean or null`),typeof s!=`boolean`&&e.fail(`${n}.required`,`must be a boolean`),c!==void 0&&!g(c,d)&&e.fail(`${n}.provider`,`is not a supported provider`),r===`repo-picker`&&c===void 0&&e.fail(`${n}.provider`,`is required for a repository field`),t.multiple!==void 0&&(t.multiple!==!0||r!==`repo-picker`)&&e.fail(`${n}.multiple`,`may only be true, and only on a repository field`),l!==void 0&&(r===`select`?!Array.isArray(l)||l.length===0?e.fail(`${n}.options`,`must be a non-empty array`):l.forEach((t,r)=>{let i=`${n}.options[${r}]`;if(!h(t)){e.fail(i,`must be an object`);return}(typeof t.value!=`string`||t.value.length===0)&&e.fail(`${i}.value`,`must be a non-empty string`),e.copy(t.label,`${i}.label`)}):e.fail(`${n}.options`,`is only allowed on a select field`)),t.constraints!==void 0&&S(e,t.constraints,`${n}.constraints`)}function w(e,t,r){return!h(t)||Object.keys(t).length===0?(e.fail(r,`must be a non-empty object`),[]):Object.entries(t).map(([t,i])=>(n.test(t)||e.fail(`${r}.${t}`,`is not a valid field name`),C(e,i,`${r}.${t}`),t))}function T(e,t){if(!h(t))return e.fail(`setup.form`,`must be an object`),{kinds:[],fieldNames:[]};t.note!==void 0&&e.copy(t.note,`setup.form.note`);let n=w(e,t.args,`setup.form.args`),r=[];t.triggers!==void 0&&(!h(t.triggers)||Object.keys(t.triggers).length===0?e.fail(`setup.form.triggers`,`must be a non-empty object`):Object.entries(t.triggers).forEach(([t,i])=>{if(!g(t,f)){e.fail(`setup.form.triggers.${t}`,`is not a trigger kind`);return}r.push(t),n.push(...w(e,i,`setup.form.triggers.${t}`))}));let i=n.filter((e,t)=>n.indexOf(e)!==t);return Array.from(new Set(i)).forEach(t=>e.fail(`setup.form.${t}`,`is declared more than once`)),{kinds:r,fieldNames:n}}function E(e,t,n){if(!Array.isArray(t)||t.length===0){e.fail(n,`must be a non-empty array`);return}t.forEach((t,r)=>{(typeof t!=`string`||t.length===0)&&e.fail(`${n}[${r}]`,`must be a non-empty string`)})}function D(e,t,n,r){let i=`setup.actions.${t}`;if(!h(n)){e.fail(i,`must be an object`);return}e.copy(n.label,`${i}.label`),e.copy(n.help,`${i}.help`),E(e,n.features,`${i}.features`),w(e,n.args,`${i}.args`).filter(e=>r.includes(e)).forEach(t=>e.fail(`${i}.args.${t}`,`is already declared by the form`)),t===`prompt`?e.templateValue(n.prompt,`${i}.prompt`):t===`plugin`?(e.templateValue(n.prompt,`${i}.prompt`),e.templateValue(n.plugins,`${i}.plugins`)):t===`upload`?(e.templateValue(n.tarballPath,`${i}.tarballPath`),e.templateValue(n.entrypoint,`${i}.entrypoint`),n.setupScript!==void 0&&e.templateValue(n.setupScript,`${i}.setupScript`)):e.fail(i,`is not a supported action kind`);let a={prompt:[`label`,`help`,`features`,`args`,`prompt`],plugin:[`label`,`help`,`features`,`args`,`prompt`,`plugins`],upload:[`label`,`help`,`features`,`args`,`tarballPath`,`entrypoint`,`setupScript`]}[t]??[];Object.keys(n).filter(e=>!a.includes(e)).forEach(t=>e.fail(`${i}.${t}`,`is not allowed`))}function O(e,t,n){if(!h(t)||Object.keys(t).length===0){e.fail(`setup.actions`,`must be a non-empty object`);return}Object.entries(t).forEach(([t,r])=>D(e,t,r,n))}function k(e,t){return h(e)?Object.values(e).some(e=>h(e)&&e.type===t):!1}function A(e){return h(e)&&k(e.args,`repo-picker`)}function j(e){let t=h(e.form)?e.form:{},n=h(t.triggers)?t.triggers:{},r=n.event;return k(r,`event-source`)||`event`in n&&Object.keys(n).length===1&&k(r,`repo-picker`)||k(t.args,`repo-picker`)?!0:!h(e.actions)||Object.keys(e.actions).length===0?!1:Object.values(e.actions).every(A)}function M(e,t){e.templateCopy(t,`setup.message`)&&t.length>m&&e.fail(`setup.message`,`must be at most ${m} characters`)}function N(e,t){if(!h(t)){e.fail(`setup.bundle`,`must be an object`);return}(typeof t.version!=`string`||!i.test(t.version))&&e.fail(`setup.bundle.version`,`must be a semantic version`),b(t.entrypoint)||e.fail(`setup.bundle.entrypoint`,`must be a plain command that stays inside the archive`),t.setupScript!==void 0&&!y(t.setupScript,o)&&e.fail(`setup.bundle.setupScript`,`must be a relative path`),t.timeout!==void 0&&!_(t.timeout,1)&&e.fail(`setup.bundle.timeout`,`must be a positive integer`),!h(t.files)||Object.keys(t.files).length===0?e.fail(`setup.bundle.files`,`must be a non-empty object`):(Object.entries(t.files).forEach(([t,n])=>{y(t,o)||e.fail(`setup.bundle.files.${t}`,`is not a packed path`),t===`config.json`&&e.fail(`setup.bundle.files.${t}`,`is the name the rendered config is packed under`),y(n,s)||e.fail(`setup.bundle.files.${t}`,`must name a file under skills/, automations/, or plugins/`)}),typeof t.setupScript==`string`&&!(t.setupScript in t.files)&&e.fail(`setup.bundle.setupScript`,`must name a packed file`)),!h(t.config)||Object.keys(t.config).length===0?e.fail(`setup.bundle.config`,`must be a non-empty object`):P(e,t.config,`setup.bundle.config`)}function P(e,t,n){if(typeof t==`string`){e.templateValue(t,n);return}if(Array.isArray(t)){t.forEach((t,r)=>P(e,t,`${n}[${r}]`));return}if(h(t)){Object.entries(t).forEach(([t,r])=>P(e,r,`${n}.${t}`));return}t!==null&&typeof t!=`number`&&typeof t!=`boolean`&&e.fail(n,`must be a string, number, boolean, null, array or object`)}function F(e,t,n,r){if(!g(t.mode,l)){e.fail(`setup.mode`,`is not a supported mode`);return}if(t.mode===`direct`){let i=t.prompt!==void 0,a=t.bundle!==void 0,o=t.actions!==void 0;[i,a,o].filter(Boolean).length===1?a?N(e,t.bundle):o?O(e,t.actions,r):e.templateValue(t.prompt,`setup.prompt`):e.fail(`setup`,`must declare exactly one of prompt, bundle or actions for direct setup`),t.message!==void 0&&M(e,t.message),n.length===0&&e.fail(`setup.form.triggers`,`must declare at least one trigger kind`),n.includes(`event`)&&!j(t)&&e.fail(`setup.form.triggers.event`,`must declare an event source field or repository picker`),t.filter!==void 0&&(n.includes(`event`)?e.templateValue(t.filter,`setup.filter`):e.fail(`setup.filter`,`is only allowed for an event trigger`));return}M(e,t.message),e.absent(t,`prompt`,`setup`,`is only allowed for direct setup`),e.absent(t,`bundle`,`setup`,`is only allowed for direct setup`),e.absent(t,`actions`,`setup`,`is only allowed for direct setup`),e.absent(t,`filter`,`setup`,`is only allowed for direct setup`)}function I(e){return h(e)&&e.setup!==void 0}function L(e){let n=new v;if(!h(e))return{valid:!1,errors:[`manifest: must be an object`]};let{setup:r}=e;if(!h(r))return{valid:!1,errors:[`setup: must be an object`]};if(r.version!==`1.0`)return{valid:!1,errors:[`setup.version: must be "1.0"`]};(typeof e.id!=`string`||!t.test(e.id))&&n.fail(`id`,`must be a lowercase slug`),n.copy(e.name,`name`),n.copy(e.description,`description`),e.version!==void 0&&(typeof e.version!=`string`||!i.test(e.version))&&n.fail(`version`,`must be a semantic version`),e.skill!==void 0&&(typeof e.skill!=`string`||!t.test(e.skill))&&n.fail(`skill`,`must be a lowercase slug`),x(n,e.requires);let{kinds:a,fieldNames:o}=T(n,r.form);return F(n,r,a,o),{valid:n.errors.length===0,errors:n.errors}}exports.hasSetupBlock=I,exports.validateSetupEntry=L;
|
|
2
2
|
//# sourceMappingURL=manifest-validation.cjs.map
|