@desplega.ai/agent-swarm 2.7.5 → 2.7.7
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/dist/{acp-adapter-g3ads55s.js → acp-adapter-7b4vk3h5.js} +5 -4
- package/dist/{actions-zwfykqqk.js → actions-3tt153af.js} +18 -17
- package/dist/{amp-adapter-z3s0ssdt.js → amp-adapter-njpts0yr.js} +1 -1
- package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
- package/dist/{app-vmp4fgep.js → app-nky4378f.js} +3 -3
- package/dist/{assistant-vwde653f.js → assistant-jnb669pa.js} +24 -22
- package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
- package/dist/{boot-reembed-375t7tbk.js → boot-reembed-9mgvaztt.js} +1 -1
- package/dist/{boot-reembed-gq9rcpnj.js → boot-reembed-qs2djdej.js} +1 -1
- package/dist/{boot-scrub-sweep-nd938kyf.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
- package/dist/{boot-seeds-aykx51v3.js → boot-seeds-5cm6ambd.js} +4 -4
- package/dist/{boxd-k1j26g3h.js → boxd-nv507va8.js} +5 -5
- package/dist/{claude-adapter-8j4hm099.js → claude-adapter-fn287zmm.js} +5 -4
- package/dist/{claude-managed-setup-0wyynpn1.js → claude-managed-setup-wtdz2mj2.js} +1 -1
- package/dist/{claude-sdk-session-q6wj5hmy.js → claude-sdk-session-x91kqyhm.js} +9677 -9514
- package/dist/{cli-j3jvnpe8.js → cli-08bmz9z9.js} +1 -1
- package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
- package/dist/{cli-y24sc29k.js → cli-1c6tzt8g.js} +30 -8
- package/dist/{cli-mjzynrse.js → cli-35rsp8ye.js} +1 -1
- package/dist/{cli-rpm2sgv0.js → cli-3fg0bdcv.js} +19 -2
- package/dist/{cli-nn4d8dne.js → cli-5b456bpf.js} +2 -2
- package/dist/{cli-bhjt02p8.js → cli-5jnhv72d.js} +1 -1
- package/dist/{cli-fqzx7gwa.js → cli-5zx699j9.js} +2 -2
- package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
- package/dist/{cli-vqypxzh4.js → cli-6g0hb4cp.js} +8 -8
- package/dist/{cli-tqgg40ek.js → cli-6pfd54vx.js} +9 -8
- package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
- package/dist/{cli-zppwg9p9.js → cli-72v799xn.js} +1 -1
- package/dist/cli-82j36f2m.js +45 -0
- package/dist/{cli-hn9hxmz9.js → cli-8305gbq0.js} +29 -16
- package/dist/{cli-z8eh3ex8.js → cli-8sckt74b.js} +8 -8
- package/dist/{cli-bnz7a7qf.js → cli-95jzxcp3.js} +1 -1
- package/dist/{cli-0bxqnvq9.js → cli-adeegh7d.js} +29 -7
- package/dist/{cli-g61qxmge.js → cli-afkqkrtw.js} +3 -3
- package/dist/{cli-7a42nfy0.js → cli-ayayvfg4.js} +24 -4
- package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
- package/dist/{cli-z6jkx23q.js → cli-bfv7vdg6.js} +9 -9
- package/dist/{cli-pywf3eh5.js → cli-caj33vcq.js} +5 -5
- package/dist/{cli-5pag6rmn.js → cli-cb2a8pna.js} +1 -1
- package/dist/{cli-wvaawacd.js → cli-cppkf404.js} +5236 -72
- package/dist/{cli-mry0qzym.js → cli-ddaanqkd.js} +123 -10
- package/dist/{cli-0gey5fzy.js → cli-e0kmphd6.js} +2 -2
- package/dist/{cli-zh3fp8v2.js → cli-e46hg0xw.js} +1 -1
- package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
- package/dist/{cli-qxnf8z2z.js → cli-excnnjg2.js} +165 -153
- package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
- package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
- package/dist/{cli-rm6hqj3z.js → cli-fyt2g4wd.js} +33 -139
- package/dist/cli-g08zdaec.js +55 -0
- package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
- package/dist/{cli-82ae98pz.js → cli-gz6q4eve.js} +3 -3
- package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
- package/dist/{cli-rqkxrmgw.js → cli-je518txm.js} +39 -14
- package/dist/{cli-8hs41zy2.js → cli-jtjtabxe.js} +1 -1
- package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
- package/dist/{cli-nzxr556s.js → cli-k1nje3v3.js} +38 -43
- package/dist/{cli-3h4nhjn8.js → cli-k8196sh2.js} +9 -7
- package/dist/{cli-afrrppsw.js → cli-k899ehvh.js} +2 -2
- package/dist/{cli-0731hvbw.js → cli-m0da8j35.js} +23 -9
- package/dist/cli-m2z8jagn.js +12078 -0
- package/dist/{cli-8veytvbh.js → cli-mkk9rnzz.js} +5 -5
- package/dist/{cli-7wspm5gm.js → cli-n6bv12jn.js} +528 -6
- package/dist/{cli-spknvxbq.js → cli-nmzhp1b5.js} +14 -12
- package/dist/{cli-p599f1qm.js → cli-p82cnnjq.js} +1 -1
- package/dist/{cli-xtjhfmpc.js → cli-pc6n7fre.js} +1 -1
- package/dist/{cli-vbyfwh92.js → cli-pjctyea3.js} +2 -2
- package/dist/{cli-nmbmy64z.js → cli-pq73h5mz.js} +3 -3
- package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
- package/dist/{cli-7wcyk2s5.js → cli-q3tkegby.js} +1 -1
- package/dist/cli-qghe0h66.js +67 -0
- package/dist/{cli-6s7vdcta.js → cli-qh4d4tvk.js} +4 -4
- package/dist/{cli-mmwm7zbf.js → cli-rtt8g4v9.js} +1 -1
- package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
- package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
- package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
- package/dist/{cli-a47xy8eq.js → cli-vz22fyqv.js} +186 -54
- package/dist/{cli-1ayk1eas.js → cli-wk5yrfwd.js} +285 -42
- package/dist/{cli-1338796d.js → cli-ws19768n.js} +1755 -119
- package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
- package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
- package/dist/{cli-xbkdm710.js → cli-yndmfxb3.js} +3 -3
- package/dist/{cli-rhw40jgb.js → cli-yyj3j5m4.js} +2 -2
- package/dist/cli.js +26 -25
- package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
- package/dist/{codex-adapter-q6k3gnyg.js → codex-adapter-cf2yxe9m.js} +3 -2
- package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
- package/dist/{codex-session-runner-3mwmk3d9.js → codex-session-runner-6vfzdgbd.js} +3 -2
- package/dist/{commands-evrenjn9.js → commands-t3mfy7vn.js} +9 -3
- package/dist/{controller-xjp8vswh.js → controller-q7xrbaz0.js} +8 -3
- package/dist/{controllers-connect-pp5jnxvc.js → controllers-connect-c6nm70kg.js} +20 -19
- package/dist/{cursor-adapter-yf47w4t0.js → cursor-adapter-ftd81ga8.js} +1 -1
- package/dist/{db-zxpw6aht.js → db-dk572x25.js} +3 -3
- package/dist/{db-rgfsh91f.js → db-vemtpmcc.js} +14 -4
- package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
- package/dist/{dispatcher-smasgws7.js → dispatcher-kh7ecxh9.js} +17 -16
- package/dist/{docker-rrjhaqt9.js → docker-c1a3gqmf.js} +89 -22
- package/dist/{dsh-adapter-ftyy8dnp.js → dsh-adapter-v8628r89.js} +1 -1
- package/dist/{e2b-w1p0bnpm.js → e2b-fr12yccx.js} +5 -5
- package/dist/{e2b-qcaj675y.js → e2b-x6ywzak3.js} +3 -3
- package/dist/{easter-egg-3d-yyts7p16.js → easter-egg-3d-nenw4x63.js} +1 -1
- package/dist/{embedding-space-1z0mbg95.js → embedding-space-0jepb239.js} +6 -6
- package/dist/{embeddings-ygeg7tm7.js → embeddings-xxt49yxg.js} +1 -1
- package/dist/{execute-14qhqp4d.js → execute-1mhw6e4x.js} +1 -1
- package/dist/{external-channel-ttgrs3sx.js → external-channel-55v12e60.js} +2 -2
- package/dist/{fx-adapter-gt6pncj2.js → fx-adapter-bs8wqrk5.js} +6 -5
- package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-v94hr7h0.js} +5 -5
- package/dist/{google-vertex-jrtdh9gs.js → google-vertex-g7vr6cf8.js} +5 -5
- package/dist/{grok-adapter-sm4m5k4g.js → grok-adapter-gpjqmkcq.js} +6 -5
- package/dist/handlers-zxmp1ta4.js +32 -0
- package/dist/{heartbeat-qahz4p62.js → heartbeat-k42svnr0.js} +20 -19
- package/dist/{hook-4fd5wq5w.js → hook-114m7hap.js} +4 -3
- package/dist/{hook-kj3d398m.js → hook-6nphweh0.js} +24 -9
- package/dist/{http-1ted0j1k.js → http-r8cc4wz7.js} +1196 -211
- package/dist/index-a8phxqnk.js +37 -0
- package/dist/{index-q4b917zs.js → index-e3s8wmxy.js} +1 -1
- package/dist/{index-6rq3r5zb.js → index-fpk4bcd9.js} +1 -1
- package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
- package/dist/index-k6h00t4c.js +30 -0
- package/dist/{index-7v51awbn.js → index-nh3wyqdt.js} +1027 -577
- package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
- package/dist/index-xrz7te8h.js +33 -0
- package/dist/{keepalive-6cf311t5.js → keepalive-x060nt9p.js} +3 -3
- package/dist/lead-956sd2yc.js +71 -0
- package/dist/{maintenance-a4bm1bmt.js → maintenance-2xxfexhf.js} +1 -1
- package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
- package/dist/{oauth-refresh-sweep-3h0dfw1k.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
- package/dist/{onboard-myn5hraa.js → onboard-zc48j237.js} +5 -5
- package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
- package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
- package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
- package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
- package/dist/{opencode-adapter-p2nkb5am.js → opencode-adapter-r690h1cr.js} +4 -3
- package/dist/{otel-impl-a1a0ef6b.js → otel-impl-r5xbh81q.js} +4 -4
- package/dist/{pi-mono-adapter-rzmpb72a.js → pi-mono-adapter-c7v8sdec.js} +7 -6
- package/dist/{pricing-refresh-83drrnxj.js → pricing-refresh-3pmre11v.js} +3 -3
- package/dist/{rbac-roles-s8y6ecb5.js → rbac-roles-3vbshwsb.js} +1 -1
- package/dist/{rbac-roles-0fb6e7pj.js → rbac-roles-910mmkgz.js} +2 -2
- package/dist/{render-v2-twmbgrz4.js → render-v2-pxykkbab.js} +3 -3
- package/dist/{resume-zfqc36xj.js → resume-adda0bwy.js} +6 -6
- package/dist/{runtime-qwggdpqr.js → runtime-fnbm7stk.js} +1 -1
- package/dist/{session-access-sc9pn9da.js → session-access-2fvamrdc.js} +4 -4
- package/dist/{session-source-backfill-qh07xr8g.js → session-source-backfill-08xvv0sm.js} +2 -2
- package/dist/{setup-4fr0m7a1.js → setup-1aqmvmh1.js} +3 -3
- package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
- package/dist/{virtual-modules-fh4ha15b.js → virtual-modules-d2pjk9j1.js} +5 -5
- package/dist/{work-objects-0wr8zxct.js → work-objects-9s0yrm5j.js} +20 -19
- package/dist/{worker-follow-up-ktyyazyp.js → worker-follow-up-3q78fqmr.js} +17 -16
- package/dist/worker-y15hy08h.js +71 -0
- package/openapi.json +289 -4
- package/package.json +9 -8
- package/src/apps/definition.ts +6 -5
- package/src/be/db/page-theme-tokens.ts +12 -0
- package/src/be/db/script-runs.ts +23 -0
- package/src/be/db/tasks/read.ts +131 -4
- package/src/be/db/tasks/write.ts +19 -0
- package/src/be/db-client.ts +114 -11
- package/src/be/db.ts +49 -6
- package/src/be/memory/providers/sqlite-store.ts +49 -12
- package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
- package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
- package/src/be/migrations/263_task_list_filter_index.sql +11 -0
- package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
- package/src/be/migrations/266_script_runs_source_task.sql +11 -0
- package/src/be/migrations/269_pages_design_system.sql +23 -0
- package/src/be/runtime-pools/cache-cleanup.ts +109 -0
- package/src/be/runtime-pools/db.ts +65 -6
- package/src/be/runtime-pools/remote-controller.ts +5 -0
- package/src/be/runtime-pools/supervisor.ts +33 -2
- package/src/be/scripts/typecheck.ts +22 -0
- package/src/be/seed/schedules-seeder.ts +0 -10
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
- package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
- package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
- package/src/be/seed-scripts/index.ts +9 -0
- package/src/be/seed-skills/bundled-files.generated.json +3 -3
- package/src/be/swarm-config-guard.ts +32 -0
- package/src/be/task-auto-title-listener.ts +127 -0
- package/src/commands/controller.ts +4 -0
- package/src/commands/entrypoint-api-readiness.ts +74 -0
- package/src/commands/onboard/agent-fs-version.ts +1 -1
- package/src/commands/profile-sync.ts +151 -16
- package/src/commands/runner.ts +15 -7
- package/src/hooks/hook.ts +20 -9
- package/src/http/index.ts +4 -1
- package/src/http/list-total-cache.ts +14 -7
- package/src/http/memory.ts +38 -9
- package/src/http/pages-public.ts +93 -129
- package/src/http/pages.ts +94 -21
- package/src/http/script-runs.ts +174 -5
- package/src/http/status.ts +10 -2
- package/src/http/tasks.ts +4 -2
- package/src/pages/browser-sdk.ts +48 -21
- package/src/pages/color-literals.ts +115 -0
- package/src/pages/page-head.ts +766 -0
- package/src/pages/version.ts +3 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +1 -0
- package/src/providers/codex-app-server.ts +27 -1
- package/src/providers/fx-adapter.ts +1 -1
- package/src/providers/pi-mono-extension.ts +30 -15
- package/src/runtime-providers/controller-protocol.ts +14 -1
- package/src/runtime-providers/controller.ts +5 -0
- package/src/runtime-providers/docker.ts +108 -23
- package/src/runtime-providers/types.ts +16 -0
- package/src/script-workflows/workflow-ctx.ts +8 -0
- package/src/scripts-runtime/types/stdlib.d.ts +22 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +22 -0
- package/src/server-user.ts +1 -0
- package/src/slack/commands.ts +5 -1
- package/src/slack/handlers.ts +37 -2
- package/src/slack/left-threads.ts +27 -4
- package/src/tasks/follow-up-gate.ts +318 -0
- package/src/tasks/follow-up-redelegation-guard.ts +155 -0
- package/src/tasks/worker-follow-up.ts +21 -1
- package/src/tests/claude-artifacts-read-script.test.ts +209 -0
- package/src/tests/codex-app-server.test.ts +59 -0
- package/src/tests/codex-gateway-route.test.ts +50 -1
- package/src/tests/create-page-tool.test.ts +501 -0
- package/src/tests/db-client-busy-retry.test.ts +81 -0
- package/src/tests/db-client.test.ts +21 -0
- package/src/tests/db-task-read-contract.test.ts +9 -2
- package/src/tests/db-task-write-contract.test.ts +7 -1
- package/src/tests/db-tasks-read.test.ts +21 -0
- package/src/tests/entrypoint-api-readiness.test.ts +416 -210
- package/src/tests/explicit-model-validation.test.ts +14 -8
- package/src/tests/fixtures/pages/head-v1.html +56 -0
- package/src/tests/follow-up-gate-shadow.test.ts +513 -0
- package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
- package/src/tests/identity-field-budget.test.ts +1 -1
- package/src/tests/internal-ai/credentials.test.ts +62 -0
- package/src/tests/internal-ai/task-title.test.ts +283 -0
- package/src/tests/list-total-cache.test.ts +27 -2
- package/src/tests/memory-fts-populate.test.ts +11 -9
- package/src/tests/memory-retrieval-purge.test.ts +80 -0
- package/src/tests/merge-gate-detect-changes.test.ts +145 -0
- package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/page-session-dev-cookie.test.ts +188 -0
- package/src/tests/pages-frame-ancestors.test.ts +28 -1
- package/src/tests/pages-head-v2.test.ts +534 -0
- package/src/tests/pages-http.test.ts +330 -0
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-storage.test.ts +182 -0
- package/src/tests/pages-versioning.test.ts +72 -0
- package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
- package/src/tests/profile-sync.test.ts +11 -15
- package/src/tests/query-audit-plans.test.ts +27 -0
- package/src/tests/runtime-cache-cleanup.test.ts +367 -0
- package/src/tests/runtime-e2b-pools.test.ts +41 -0
- package/src/tests/runtime-pools-db.test.ts +98 -0
- package/src/tests/runtime-pools-http.test.ts +29 -0
- package/src/tests/runtime-providers-docker.test.ts +253 -0
- package/src/tests/runtime-remote-controller.test.ts +66 -2
- package/src/tests/runtime-supervisor.test.ts +66 -2
- package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
- package/src/tests/script-connections.test.ts +13 -0
- package/src/tests/script-runs-http.test.ts +684 -2
- package/src/tests/seed-registry.test.ts +28 -6
- package/src/tests/seed-schedules.test.ts +61 -8
- package/src/tests/seed-scripts.test.ts +168 -2
- package/src/tests/slack-leave-thread.test.ts +145 -5
- package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
- package/src/tests/status.test.ts +11 -0
- package/src/tests/swarm-diff.test.ts +121 -2
- package/src/tests/task-auto-title-config-guard.test.ts +37 -0
- package/src/tests/task-auto-title-listener.test.ts +165 -0
- package/src/tests/tasks-readable-row.test.ts +155 -0
- package/src/tests/theme-import.test.ts +758 -0
- package/src/tests/themes-page-theme.test.ts +155 -0
- package/src/tests/themes-presets.test.ts +172 -0
- package/src/tests/workflow-script-principal.test.ts +2 -1
- package/src/themes/classics.ts +949 -0
- package/src/themes/import.ts +420 -0
- package/src/themes/page-fields.ts +40 -0
- package/src/themes/page-theme.ts +102 -0
- package/src/themes/presets.ts +561 -0
- package/src/themes/tokens.ts +218 -0
- package/src/tools/app-upsert.ts +1 -1
- package/src/tools/create-page.ts +118 -12
- package/src/tools/send-task.ts +35 -47
- package/src/tools/slack-leave-thread.ts +1 -1
- package/src/tools/task-dedup.ts +45 -21
- package/src/tools/utils.ts +33 -0
- package/src/types.ts +83 -3
- package/src/utils/constants.ts +19 -0
- package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
- package/src/utils/internal-ai/credentials.ts +146 -93
- package/src/utils/internal-ai/index.ts +10 -0
- package/src/utils/internal-ai/models.ts +18 -0
- package/src/utils/internal-ai/task-title.ts +282 -0
- package/src/utils/page-session.ts +37 -0
- package/templates/schedules/daily-status-report/content.md +88 -2
- package/templates/schedules/daily-swarm-update-check/content.md +16 -40
- package/templates/skills/apps/SKILL.md +1 -1
- package/templates/skills/apps/content.md +1 -1
- package/templates/skills/composio-google-docs/content.md +10 -0
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/pages/config.json +1 -1
- package/templates/skills/pages/content.md +539 -86
- package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
- package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
- package/templates/skills/pages/files/examples/report-page.html +125 -124
- package/templates/skills/script-workflows/content.md +19 -1
- package/templates/skills/swarm-docs/content.md +1 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -0
- package/templates/skills/swarm-scripts/content.md +2 -0
- package/dist/cli-2pnswgh8.js +0 -189
- package/dist/cli-dznrgaep.js +0 -30
- package/dist/cli-w3hcrxht.js +0 -6985
- package/dist/handlers-vwmba1m2.js +0 -31
- package/dist/index-5r71rf98.js +0 -32
- package/dist/index-anmwxs87.js +0 -29
- package/dist/index-spp0ra5c.js +0 -36
- package/dist/lead-fjy418cw.js +0 -69
- package/dist/worker-0c5tyzqe.js +0 -69
- package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
- package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme importers: the boundary between outside theme formats and the page
|
|
3
|
+
* token contract (`tokens.ts`).
|
|
4
|
+
*
|
|
5
|
+
* An importer turns an outside format into `PageThemeTokens`. Each importer
|
|
6
|
+
* in `THEME_IMPORTERS` only reads its format into raw `name: value`
|
|
7
|
+
* declarations per mode. `importThemeTokens` then maps the names onto the
|
|
8
|
+
* contract and checks every value, the same way for every format. Pages
|
|
9
|
+
* store the normalized result and render it, so nothing after this module
|
|
10
|
+
* knows which format a theme came from.
|
|
11
|
+
*
|
|
12
|
+
* Formats today:
|
|
13
|
+
* - `tokens` (the default): JSON maps `{ light, dark }` of token name to value.
|
|
14
|
+
* - `css`: a CSS custom-property block, as shadcn, tweakcn and Tailwind v4
|
|
15
|
+
* produce. A selector list is split at its commas, and the block feeds each
|
|
16
|
+
* mode that one of its parts names (`:root, .dark` feeds both). Light:
|
|
17
|
+
* `:root`, `:host`, `html`, and `@theme` / `@theme inline`. Dark: `.dark`,
|
|
18
|
+
* `:root.dark`, `html.dark`, `[data-theme=dark]` (quoted or not), and a
|
|
19
|
+
* light selector inside `@media (prefers-color-scheme: dark)`.
|
|
20
|
+
* `@layer` wrappers are read through (shadcn for Tailwind v3 puts `:root`
|
|
21
|
+
* inside `@layer base`). The custom properties of every other block (other
|
|
22
|
+
* selectors, other media queries, nested rules) go to `ignored` with the
|
|
23
|
+
* block, for example `--ring (in .card)`. Comments and plain properties are
|
|
24
|
+
* skipped. Only CSS without a light or dark block throws.
|
|
25
|
+
*
|
|
26
|
+
* To add a format (for example Figma / DTCG), add three things:
|
|
27
|
+
* 1. The importer: `importDtcg` reads a DTCG document into raw declarations.
|
|
28
|
+
* Register it in `THEME_IMPORTERS`, and add its input shape to
|
|
29
|
+
* `ThemeImportInput`.
|
|
30
|
+
* 2. "dtcg" in `THEME_IMPORT_FORMATS`. The page input schema
|
|
31
|
+
* (`PageThemeTokensInputSchema` in src/types.ts) builds its `format` enum
|
|
32
|
+
* from this list.
|
|
33
|
+
* 3. Only when the format needs a new payload field (for example a DTCG
|
|
34
|
+
* `document`): that one field in `PageThemeTokensInputSchema`.
|
|
35
|
+
* Storage and rendering stay the same.
|
|
36
|
+
*
|
|
37
|
+
* Normalization, for every format:
|
|
38
|
+
* - `--color-primary`, `--primary`, `primary` and `color-primary` all name
|
|
39
|
+
* `--color-primary`. Font and radius names map the same way (`font-sans`).
|
|
40
|
+
* - shadcn `--radius` expands to the four radius tokens. An explicit radius
|
|
41
|
+
* token wins over the expansion.
|
|
42
|
+
* - A bare HSL triplet (`222.2 47.4% 11.2%`) is wrapped as `hsl(...)`.
|
|
43
|
+
* - `var()` references are renamed the same way (`var(--primary)` becomes
|
|
44
|
+
* `var(--color-primary)`, and `var(--radius)` becomes `var(--radius-lg)`).
|
|
45
|
+
* A declaration that references its own token (the `@theme inline`
|
|
46
|
+
* plumbing, `--color-primary: var(--primary)`) adds nothing and is dropped.
|
|
47
|
+
* A reference to a name outside the contract cannot resolve on the page,
|
|
48
|
+
* so that declaration goes to `ignored` with the reference, for example
|
|
49
|
+
* `--font-sans (references --font-geist-sans)`.
|
|
50
|
+
* - Names outside the contract (`--sidebar`, `--shadow-2xs`, `--font-serif`)
|
|
51
|
+
* go to `ignored`. They never throw.
|
|
52
|
+
* - A value must be 1 to 200 characters (a trailing `!important` is dropped
|
|
53
|
+
* first), without `; { } < > \`, control characters (newlines included),
|
|
54
|
+
* resource functions (`url`, `src`, `image`, `image-set`), `expression(`,
|
|
55
|
+
* `@import`, `javascript:` or comment markers, and
|
|
56
|
+
* with balanced quotes and parentheses, so it cannot break out of its
|
|
57
|
+
* declaration. A bad value throws. The length check runs before any regex,
|
|
58
|
+
* so a huge value costs one pass.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
import {
|
|
62
|
+
PAGE_THEME_TOKENS,
|
|
63
|
+
type PageThemeToken,
|
|
64
|
+
type PageThemeTokens,
|
|
65
|
+
type PageThemeTokenValues,
|
|
66
|
+
} from "./tokens";
|
|
67
|
+
|
|
68
|
+
export const THEME_IMPORT_FORMATS = ["tokens", "css"] as const;
|
|
69
|
+
export type ThemeImportFormat = (typeof THEME_IMPORT_FORMATS)[number];
|
|
70
|
+
|
|
71
|
+
export type ThemeImportInput =
|
|
72
|
+
| { format?: "tokens"; light?: Record<string, string>; dark?: Record<string, string> }
|
|
73
|
+
| { format: "css"; css: string };
|
|
74
|
+
|
|
75
|
+
/** A bad input shape or an unsafe value. The message names the bad key or value. */
|
|
76
|
+
export class ThemeImportError extends Error {
|
|
77
|
+
override name = "ThemeImportError";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type ThemeMode = "light" | "dark";
|
|
81
|
+
type RawDeclaration = [name: string, value: string];
|
|
82
|
+
/**
|
|
83
|
+
* Declarations per mode, in source order, with names not normalized yet, and
|
|
84
|
+
* the `ignored` entries for what the importer read but could not place.
|
|
85
|
+
*/
|
|
86
|
+
type RawThemeTokens = Record<ThemeMode, RawDeclaration[]> & { skipped: string[] };
|
|
87
|
+
type ThemeImporter = (input: Record<string, unknown>) => RawThemeTokens;
|
|
88
|
+
|
|
89
|
+
const THEME_IMPORTERS: Record<ThemeImportFormat, ThemeImporter> = {
|
|
90
|
+
tokens: readTokenMaps,
|
|
91
|
+
css: readCssBlocks,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const MAX_VALUE_LENGTH = 200;
|
|
95
|
+
/** Room for a trailing `!important` and the spaces around it, on top of the value limit. */
|
|
96
|
+
const IMPORTANT_ROOM = 16;
|
|
97
|
+
/** Linear time: the only quantifier comes after the literal `!`. */
|
|
98
|
+
const IMPORTANT_SUFFIX = /!\s*important$/i;
|
|
99
|
+
const UNSAFE_CHARACTERS = ";{}<>\\";
|
|
100
|
+
const UNSAFE_PATTERN = /(?:url|src|image(?:-set)?|expression)\(|@import|javascript:|\/\*|\*\//i;
|
|
101
|
+
const HSL_TRIPLET = /^-?[\d.]+(?:deg)?\s+-?[\d.]+%\s+-?[\d.]+%(?:\s*\/\s*[\d.]+%?)?$/;
|
|
102
|
+
const VAR_REFERENCE = /var\(\s*--([\w-]+)/gi;
|
|
103
|
+
const CONTRACT_TOKENS: ReadonlySet<string> = new Set<string>(PAGE_THEME_TOKENS);
|
|
104
|
+
const RADIUS_STEPS: ReadonlyArray<[PageThemeToken, (radius: string) => string]> = [
|
|
105
|
+
["--radius-sm", (radius) => `calc(${radius} - 4px)`],
|
|
106
|
+
["--radius-md", (radius) => `calc(${radius} - 2px)`],
|
|
107
|
+
["--radius-lg", (radius) => radius],
|
|
108
|
+
["--radius-xl", (radius) => `calc(${radius} + 4px)`],
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
export function importThemeTokens(input: ThemeImportInput): {
|
|
112
|
+
tokens: PageThemeTokens;
|
|
113
|
+
ignored: string[];
|
|
114
|
+
} {
|
|
115
|
+
const fields = readRecord(input, "themeTokens");
|
|
116
|
+
const raw = THEME_IMPORTERS[readFormat(fields.format)](fields);
|
|
117
|
+
const ignored = new Set<string>();
|
|
118
|
+
const tokens: PageThemeTokens = {
|
|
119
|
+
light: normalizeDeclarations(raw.light, "light", ignored),
|
|
120
|
+
dark: normalizeDeclarations(raw.dark, "dark", ignored),
|
|
121
|
+
};
|
|
122
|
+
for (const entry of raw.skipped) ignored.add(entry);
|
|
123
|
+
return { tokens, ignored: [...ignored] };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readFormat(format: unknown): ThemeImportFormat {
|
|
127
|
+
if (format === undefined || format === null) return "tokens";
|
|
128
|
+
const known = THEME_IMPORT_FORMATS.find((candidate) => candidate === format);
|
|
129
|
+
if (known) return known;
|
|
130
|
+
throw new ThemeImportError(
|
|
131
|
+
`unknown themeTokens format ${quote(format)}: use one of ${THEME_IMPORT_FORMATS.join(", ")}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readTokenMaps(input: Record<string, unknown>): RawThemeTokens {
|
|
136
|
+
assertFields(input, "tokens", ["format", "light", "dark"]);
|
|
137
|
+
return {
|
|
138
|
+
light: readTokenMap(input.light, "light"),
|
|
139
|
+
dark: readTokenMap(input.dark, "dark"),
|
|
140
|
+
skipped: [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readTokenMap(value: unknown, mode: ThemeMode): RawDeclaration[] {
|
|
145
|
+
if (value === undefined || value === null) return [];
|
|
146
|
+
return Object.entries(readRecord(value, `themeTokens.${mode}`)).map(
|
|
147
|
+
([name, tokenValue]): RawDeclaration => {
|
|
148
|
+
if (typeof tokenValue !== "string") {
|
|
149
|
+
throw new ThemeImportError(`themeTokens.${mode}[${quote(name)}] must be a string`);
|
|
150
|
+
}
|
|
151
|
+
return [name, tokenValue];
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readCssBlocks(input: Record<string, unknown>): RawThemeTokens {
|
|
157
|
+
assertFields(input, "css", ["format", "css"]);
|
|
158
|
+
if (typeof input.css !== "string") {
|
|
159
|
+
throw new ThemeImportError('themeTokens.css must be a string of CSS (format "css")');
|
|
160
|
+
}
|
|
161
|
+
return parseCss(input.css);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* How a CSS block is read. A wrapper (the top level, `@layer`, the dark
|
|
166
|
+
* color-scheme media query) reads the rules inside it. A theme rule feeds its
|
|
167
|
+
* custom properties to its modes. Any other block reports its custom
|
|
168
|
+
* properties as ignored, labeled with the outermost block that was not read.
|
|
169
|
+
*/
|
|
170
|
+
type CssBlock =
|
|
171
|
+
| { kind: "wrapper"; dark: boolean }
|
|
172
|
+
| { kind: "theme"; modes: ThemeMode[] }
|
|
173
|
+
| { kind: "other"; label: string };
|
|
174
|
+
|
|
175
|
+
const TOP_LEVEL: CssBlock = { kind: "wrapper", dark: false };
|
|
176
|
+
const LIGHT_SELECTORS: ReadonlySet<string> = new Set([":root", ":host", "html"]);
|
|
177
|
+
const DARK_SELECTORS: ReadonlySet<string> = new Set([".dark", ":root.dark", "html.dark"]);
|
|
178
|
+
const DARK_ATTRIBUTE_SELECTOR = /^\[ ?data-theme ?= ?(["']?)dark\1 ?\]$/;
|
|
179
|
+
const DARK_MEDIA_QUERY = /^@media ?\( ?prefers-color-scheme ?: ?dark ?\)$/i;
|
|
180
|
+
const THEME_AT_RULE = /^@theme(?: [\w-]+)*$/;
|
|
181
|
+
const MAX_LABEL_LENGTH = 40;
|
|
182
|
+
|
|
183
|
+
function openCssBlock(prelude: string, parent: CssBlock): CssBlock {
|
|
184
|
+
if (parent.kind === "other") return parent;
|
|
185
|
+
const selector = prelude.trim().replace(/\s+/g, " ");
|
|
186
|
+
if (parent.kind === "wrapper") {
|
|
187
|
+
if (/^@layer(?: |$)/.test(selector)) return parent;
|
|
188
|
+
if (DARK_MEDIA_QUERY.test(selector)) return { kind: "wrapper", dark: true };
|
|
189
|
+
// `@theme` is a top-level Tailwind rule, so it does not count inside a media query.
|
|
190
|
+
const modes: ThemeMode[] =
|
|
191
|
+
THEME_AT_RULE.test(selector) && !parent.dark ? ["light"] : selectorModes(selector);
|
|
192
|
+
if (modes.length > 0) return { kind: "theme", modes: parent.dark ? ["dark"] : modes };
|
|
193
|
+
}
|
|
194
|
+
const label =
|
|
195
|
+
selector.length > MAX_LABEL_LENGTH ? `${selector.slice(0, MAX_LABEL_LENGTH)}...` : selector;
|
|
196
|
+
return { kind: "other", label };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The modes that the parts of a selector list name. A part that names no mode adds nothing. */
|
|
200
|
+
function selectorModes(selector: string): ThemeMode[] {
|
|
201
|
+
const modes = new Set<ThemeMode>();
|
|
202
|
+
for (const part of selectorListParts(selector)) {
|
|
203
|
+
if (LIGHT_SELECTORS.has(part)) modes.add("light");
|
|
204
|
+
else if (DARK_SELECTORS.has(part) || DARK_ATTRIBUTE_SELECTOR.test(part)) modes.add("dark");
|
|
205
|
+
}
|
|
206
|
+
return [...modes];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Splits a selector list at the commas outside parentheses, brackets and strings. */
|
|
210
|
+
function selectorListParts(selector: string): string[] {
|
|
211
|
+
const parts: string[] = [];
|
|
212
|
+
let depth = 0;
|
|
213
|
+
let start = 0;
|
|
214
|
+
for (let index = 0; index < selector.length; index++) {
|
|
215
|
+
const char = selector[index];
|
|
216
|
+
if (char === '"' || char === "'") index = closingQuoteIndex(selector, index);
|
|
217
|
+
else if (char === "(" || char === "[") depth++;
|
|
218
|
+
else if (char === ")" || char === "]") depth = Math.max(0, depth - 1);
|
|
219
|
+
else if (char === "," && depth === 0) {
|
|
220
|
+
parts.push(selector.slice(start, index).trim());
|
|
221
|
+
start = index + 1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
parts.push(selector.slice(start).trim());
|
|
225
|
+
return parts;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Splits the CSS at `{`, `}` and `;` outside strings and parentheses, tracking block nesting. */
|
|
229
|
+
function parseCss(css: string): RawThemeTokens {
|
|
230
|
+
const text = css.replace(/\/\*[\s\S]*?(?:\*\/|$)/g, " ");
|
|
231
|
+
const raw: RawThemeTokens = { light: [], dark: [], skipped: [] };
|
|
232
|
+
const blocks: CssBlock[] = [];
|
|
233
|
+
let themeBlocks = 0;
|
|
234
|
+
let start = 0;
|
|
235
|
+
let parens = 0;
|
|
236
|
+
for (let index = 0; index < text.length; index++) {
|
|
237
|
+
const char = text[index];
|
|
238
|
+
if (char === '"' || char === "'") {
|
|
239
|
+
index = closingQuoteIndex(text, index);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (char === "(") parens++;
|
|
243
|
+
else if (char === ")") parens = Math.max(0, parens - 1);
|
|
244
|
+
if (parens > 0 || (char !== "{" && char !== "}" && char !== ";")) continue;
|
|
245
|
+
const segment = text.slice(start, index);
|
|
246
|
+
start = index + 1;
|
|
247
|
+
const current = blocks.at(-1) ?? TOP_LEVEL;
|
|
248
|
+
if (char === "{") {
|
|
249
|
+
const block = openCssBlock(segment, current);
|
|
250
|
+
if (block.kind === "theme") themeBlocks++;
|
|
251
|
+
blocks.push(block);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
readCssDeclaration(segment, current, raw);
|
|
255
|
+
if (char === "}" && blocks.pop() === undefined) {
|
|
256
|
+
throw new ThemeImportError('themeTokens.css has an unmatched "}"');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (blocks.length > 0) throw new ThemeImportError('themeTokens.css has an unclosed "{" block');
|
|
260
|
+
if (themeBlocks === 0) {
|
|
261
|
+
throw new ThemeImportError("themeTokens.css has no :root, .dark, or @theme block");
|
|
262
|
+
}
|
|
263
|
+
return raw;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Index of the quote that closes the string opened at `open`. A CSS string also ends at a newline. */
|
|
267
|
+
function closingQuoteIndex(text: string, open: number): number {
|
|
268
|
+
for (let index = open + 1; index < text.length; index++) {
|
|
269
|
+
const char = text[index];
|
|
270
|
+
if (char === "\\") index++;
|
|
271
|
+
else if (char === text[open] || char === "\n") return index;
|
|
272
|
+
}
|
|
273
|
+
return text.length;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Keeps custom properties only: a plain property such as `color` is not a theme token. */
|
|
277
|
+
function readCssDeclaration(segment: string, block: CssBlock, raw: RawThemeTokens): void {
|
|
278
|
+
if (block.kind === "wrapper") return;
|
|
279
|
+
const colon = segment.indexOf(":");
|
|
280
|
+
if (colon === -1) return;
|
|
281
|
+
const name = segment.slice(0, colon).trim();
|
|
282
|
+
if (!name.startsWith("--")) return;
|
|
283
|
+
if (block.kind === "other") {
|
|
284
|
+
raw.skipped.push(`${name} (in ${block.label})`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const value = segment.slice(colon + 1).replace(/\s+/g, " ");
|
|
288
|
+
for (const mode of block.modes) raw[mode].push([name, value]);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function normalizeDeclarations(
|
|
292
|
+
declarations: RawDeclaration[],
|
|
293
|
+
mode: ThemeMode,
|
|
294
|
+
ignored: Set<string>,
|
|
295
|
+
): PageThemeTokenValues {
|
|
296
|
+
const explicit: PageThemeTokenValues = {};
|
|
297
|
+
const expanded: PageThemeTokenValues = {};
|
|
298
|
+
for (const [rawName, rawValue] of declarations) {
|
|
299
|
+
const name = rawName.trim();
|
|
300
|
+
const bare = name.replace(/^--/, "");
|
|
301
|
+
const token = contractToken(bare);
|
|
302
|
+
if (!token && bare !== "radius") {
|
|
303
|
+
ignored.add(name);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const renamed = renameReferences(checkedValue(rawValue, name, mode));
|
|
307
|
+
if (renamed.outside.length > 0) {
|
|
308
|
+
ignored.add(`${name} (references ${renamed.outside.join(", ")})`);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (token) {
|
|
312
|
+
if (!renamed.references.has(token)) explicit[token] = renamed.value;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
for (const [step, size] of RADIUS_STEPS) {
|
|
316
|
+
if (!renamed.references.has(step)) expanded[step] = size(renamed.value);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { ...expanded, ...explicit };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function isContractToken(name: string): name is PageThemeToken {
|
|
323
|
+
return CONTRACT_TOKENS.has(name);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** `primary` and `color-primary` (so also `--primary` and `--color-primary`) name `--color-primary`. */
|
|
327
|
+
function contractToken(bare: string): PageThemeToken | null {
|
|
328
|
+
const direct = `--${bare}`;
|
|
329
|
+
if (isContractToken(direct)) return direct;
|
|
330
|
+
const color = `--color-${bare}`;
|
|
331
|
+
return isContractToken(color) ? color : null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Renames each `var(--x)` to its contract name, and lists the references outside the contract. */
|
|
335
|
+
function renameReferences(value: string): {
|
|
336
|
+
value: string;
|
|
337
|
+
references: Set<PageThemeToken>;
|
|
338
|
+
outside: string[];
|
|
339
|
+
} {
|
|
340
|
+
const references = new Set<PageThemeToken>();
|
|
341
|
+
const outside = new Set<string>();
|
|
342
|
+
const renamed = value.replace(VAR_REFERENCE, (reference, bare: string) => {
|
|
343
|
+
const token = bare === "radius" ? "--radius-lg" : contractToken(bare);
|
|
344
|
+
if (!token) {
|
|
345
|
+
outside.add(`--${bare}`);
|
|
346
|
+
return reference;
|
|
347
|
+
}
|
|
348
|
+
references.add(token);
|
|
349
|
+
return `var(${token}`;
|
|
350
|
+
});
|
|
351
|
+
return { value: renamed, references, outside: [...outside] };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function checkedValue(rawValue: string, name: string, mode: ThemeMode): string {
|
|
355
|
+
const trimmed = rawValue.trim();
|
|
356
|
+
// An over-long value skips the strip, so it fails on its length before any regex reads it.
|
|
357
|
+
const value =
|
|
358
|
+
trimmed.length > MAX_VALUE_LENGTH + IMPORTANT_ROOM
|
|
359
|
+
? trimmed
|
|
360
|
+
: trimmed.replace(IMPORTANT_SUFFIX, "").trimEnd();
|
|
361
|
+
const problem = valueProblem(value);
|
|
362
|
+
if (problem) throw new ThemeImportError(`${mode} token ${quote(name)}: ${problem}`);
|
|
363
|
+
return HSL_TRIPLET.test(value) ? `hsl(${value})` : value;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function valueProblem(value: string): string | null {
|
|
367
|
+
if (value.length === 0 || value.length > MAX_VALUE_LENGTH) {
|
|
368
|
+
return `value must be 1 to ${MAX_VALUE_LENGTH} characters`;
|
|
369
|
+
}
|
|
370
|
+
for (const char of value) {
|
|
371
|
+
const code = char.charCodeAt(0);
|
|
372
|
+
if (code < 0x20 || code === 0x7f) {
|
|
373
|
+
return "value must not contain a newline or a control character";
|
|
374
|
+
}
|
|
375
|
+
if (UNSAFE_CHARACTERS.includes(char)) return `value must not contain ${quote(char)}`;
|
|
376
|
+
}
|
|
377
|
+
const pattern = UNSAFE_PATTERN.exec(value);
|
|
378
|
+
if (pattern) return `value must not contain ${quote(pattern[0])}`;
|
|
379
|
+
return balanced(value) ? null : "value has an unclosed quote or parenthesis";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function balanced(value: string): boolean {
|
|
383
|
+
let depth = 0;
|
|
384
|
+
let open: string | null = null;
|
|
385
|
+
for (const char of value) {
|
|
386
|
+
if (open) {
|
|
387
|
+
if (char === open) open = null;
|
|
388
|
+
} else if (char === '"' || char === "'") open = char;
|
|
389
|
+
else if (char === "(") depth++;
|
|
390
|
+
else if (char === ")" && --depth < 0) return false;
|
|
391
|
+
}
|
|
392
|
+
return depth === 0 && open === null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function readRecord(value: unknown, label: string): Record<string, unknown> {
|
|
396
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
397
|
+
throw new ThemeImportError(`${label} must be an object`);
|
|
398
|
+
}
|
|
399
|
+
return Object.fromEntries(Object.entries(value));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Rejects a field the format does not take. A field set to `undefined` counts as absent. */
|
|
403
|
+
function assertFields(
|
|
404
|
+
input: Record<string, unknown>,
|
|
405
|
+
format: ThemeImportFormat,
|
|
406
|
+
allowed: readonly string[],
|
|
407
|
+
): void {
|
|
408
|
+
const extra = Object.keys(input).find(
|
|
409
|
+
(key) => input[key] !== undefined && !allowed.includes(key),
|
|
410
|
+
);
|
|
411
|
+
if (extra !== undefined) {
|
|
412
|
+
throw new ThemeImportError(
|
|
413
|
+
`unknown themeTokens field ${quote(extra)} for format "${format}" (allowed: ${allowed.join(", ")})`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function quote(value: unknown): string {
|
|
419
|
+
return String(JSON.stringify(value)).slice(0, 80);
|
|
420
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The theme fields of a page write, checked the same way for the HTTP create
|
|
3
|
+
* and update routes (src/http/pages.ts) and the `create_page` tool.
|
|
4
|
+
*
|
|
5
|
+
* This module imports only `src/themes/*` and types, and it registers no
|
|
6
|
+
* OpenAPI component, so its place in the module load order cannot reorder the
|
|
7
|
+
* generated openapi.json.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Page, PageThemeTokensInput } from "../types";
|
|
11
|
+
import { importThemeTokens, ThemeImportError, type ThemeImportInput } from "./import";
|
|
12
|
+
import { isThemePresetId, THEME_PRESET_IDS } from "./presets";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Checks the theme fields. `theme` must name a preset. `themeTokens` goes
|
|
16
|
+
* through the importer, so what gets stored is the normalized token form,
|
|
17
|
+
* whatever format the caller sent. For both fields, undefined keeps the stored
|
|
18
|
+
* value and null clears it.
|
|
19
|
+
*/
|
|
20
|
+
export function readThemeFields(input: {
|
|
21
|
+
theme?: string | null;
|
|
22
|
+
themeTokens?: PageThemeTokensInput | null;
|
|
23
|
+
}):
|
|
24
|
+
| { error: string }
|
|
25
|
+
| { theme?: string | null; themeTokens?: Page["themeTokens"]; ignored: string[] } {
|
|
26
|
+
if (typeof input.theme === "string" && !isThemePresetId(input.theme)) {
|
|
27
|
+
return { error: `Unknown theme "${input.theme}". Use one of: ${THEME_PRESET_IDS.join(", ")}` };
|
|
28
|
+
}
|
|
29
|
+
if (!input.themeTokens) {
|
|
30
|
+
return { theme: input.theme, themeTokens: input.themeTokens, ignored: [] };
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
// The importer checks which fields go with the chosen format.
|
|
34
|
+
const { tokens, ignored } = importThemeTokens(input.themeTokens as ThemeImportInput);
|
|
35
|
+
return { theme: input.theme, themeTokens: tokens, ignored };
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (err instanceof ThemeImportError) return { error: `Invalid themeTokens: ${err.message}` };
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme resolution and CSS for HTML pages (design version 2).
|
|
3
|
+
*
|
|
4
|
+
* Resolution picks one preset, then applies the page's own overrides:
|
|
5
|
+
* - Preset: the page `theme` when it names a known preset, else the viewer
|
|
6
|
+
* theme (the dashboard's `__swarm-theme` param) when it names a known
|
|
7
|
+
* preset, else `hive`. Direct links pass no viewer theme, so they get Hive.
|
|
8
|
+
* - Tokens: the Hive values, then the preset's contract keys, then the page
|
|
9
|
+
* `themeTokens` overrides. A shared token (font, radius) that the overrides
|
|
10
|
+
* set only in `light` applies to dark too.
|
|
11
|
+
*
|
|
12
|
+
* The CSS carries both modes. `.dark` on `<html>` picks dark, and print is
|
|
13
|
+
* always light.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { DEFAULT_THEME_ID, getThemePreset } from "./presets";
|
|
17
|
+
import {
|
|
18
|
+
HIVE_PAGE_TOKENS,
|
|
19
|
+
PAGE_THEME_SHARED_TOKENS,
|
|
20
|
+
PAGE_THEME_TOKENS,
|
|
21
|
+
type PageThemeToken,
|
|
22
|
+
type PageThemeTokens,
|
|
23
|
+
type PageThemeTokenValues,
|
|
24
|
+
} from "./tokens";
|
|
25
|
+
|
|
26
|
+
export type ResolvedPageTheme = {
|
|
27
|
+
presetId: string;
|
|
28
|
+
light: Record<PageThemeToken, string>;
|
|
29
|
+
dark: Record<PageThemeToken, string>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The design version 1 names, kept as aliases so page CSS written against
|
|
34
|
+
* them follows the theme. They are `var()` references declared on `:root`
|
|
35
|
+
* only, so they pick up the dark values when `.dark` is on `<html>`.
|
|
36
|
+
*/
|
|
37
|
+
const SWARM_ALIASES = [
|
|
38
|
+
["--swarm-bg", "--color-background"],
|
|
39
|
+
["--swarm-card", "--color-card"],
|
|
40
|
+
["--swarm-border", "--color-border"],
|
|
41
|
+
["--swarm-text", "--color-foreground"],
|
|
42
|
+
["--swarm-muted", "--color-muted-foreground"],
|
|
43
|
+
["--swarm-primary", "--color-primary"],
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
/** The contract keys of a preset's vars or of a page override map. Other keys drop out. */
|
|
47
|
+
function contractValues(
|
|
48
|
+
vars: Readonly<Record<string, string | undefined>> | undefined,
|
|
49
|
+
): PageThemeTokenValues {
|
|
50
|
+
const values: PageThemeTokenValues = {};
|
|
51
|
+
if (!vars) return values;
|
|
52
|
+
for (const token of PAGE_THEME_TOKENS) {
|
|
53
|
+
const value = vars[token];
|
|
54
|
+
if (value !== undefined) values[token] = value;
|
|
55
|
+
}
|
|
56
|
+
return values;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolvePageTheme({
|
|
60
|
+
pageTheme,
|
|
61
|
+
viewerTheme,
|
|
62
|
+
overrides,
|
|
63
|
+
}: {
|
|
64
|
+
pageTheme?: string | null;
|
|
65
|
+
viewerTheme?: string | null;
|
|
66
|
+
overrides?: PageThemeTokens | null;
|
|
67
|
+
}): ResolvedPageTheme {
|
|
68
|
+
const preset =
|
|
69
|
+
getThemePreset(pageTheme) ?? getThemePreset(viewerTheme) ?? getThemePreset(DEFAULT_THEME_ID);
|
|
70
|
+
const lightOverrides = contractValues(overrides?.light);
|
|
71
|
+
const darkOverrides = contractValues(overrides?.dark);
|
|
72
|
+
const light = {
|
|
73
|
+
...HIVE_PAGE_TOKENS.light,
|
|
74
|
+
...contractValues(preset?.vars.light),
|
|
75
|
+
...lightOverrides,
|
|
76
|
+
};
|
|
77
|
+
const dark = { ...HIVE_PAGE_TOKENS.dark, ...contractValues(preset?.vars.dark), ...darkOverrides };
|
|
78
|
+
for (const token of PAGE_THEME_SHARED_TOKENS) {
|
|
79
|
+
const value = lightOverrides[token];
|
|
80
|
+
if (value !== undefined && darkOverrides[token] === undefined) dark[token] = value;
|
|
81
|
+
}
|
|
82
|
+
return { presetId: preset?.id ?? DEFAULT_THEME_ID, light, dark };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function declarationLines(values: Record<PageThemeToken, string>, indent: string): string {
|
|
86
|
+
return PAGE_THEME_TOKENS.map((token) => `${indent}${token}: ${values[token]};`).join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Unlayered CSS for a resolved theme: light tokens and the `--swarm-*`
|
|
91
|
+
* aliases on `:root`, dark tokens on `:root.dark`, and a print block that
|
|
92
|
+
* puts the light tokens back. Unlayered rules beat Tailwind's layered theme
|
|
93
|
+
* defaults, so utilities such as `bg-primary` read these values.
|
|
94
|
+
*/
|
|
95
|
+
export function renderPageThemeCss(theme: ResolvedPageTheme): string {
|
|
96
|
+
const aliases = SWARM_ALIASES.map(([alias, token]) => ` ${alias}: var(${token});`).join("\n");
|
|
97
|
+
return [
|
|
98
|
+
`:root {\n color-scheme: light;\n${declarationLines(theme.light, " ")}\n${aliases}\n}`,
|
|
99
|
+
`:root.dark {\n color-scheme: dark;\n${declarationLines(theme.dark, " ")}\n}`,
|
|
100
|
+
`@media print {\n :root.dark {\n color-scheme: light;\n${declarationLines(theme.light, " ")}\n }\n}`,
|
|
101
|
+
].join("\n\n");
|
|
102
|
+
}
|