@pyric/cli 0.1.0-alpha.11 → 0.1.0-alpha.13
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/cli/serve.js +4 -4
- package/dist/cli/serve.js.map +1 -1
- package/dist/conformance/.generated/can-i-use-browser.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
- package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use.js +2 -2
- package/dist/conformance/.generated/can-i-use.js.map +1 -1
- package/dist/serve/bundler.d.ts +13 -8
- package/dist/serve/bundler.d.ts.map +1 -1
- package/dist/serve/bundler.js +31 -36
- package/dist/serve/bundler.js.map +1 -1
- package/dist/serve/docs-ui/docs/_rhythm/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/set-up-your-agent/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/watch-and-review/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/work-with-an-agent/index.html +1 -1
- package/dist/serve/docs-ui/docs/ai-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/api-reference/index.html +2 -2
- package/dist/serve/docs-ui/docs/api-reference.md +1 -1
- package/dist/serve/docs-ui/docs/app-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/auth-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/ai-logic/index.html +44 -22
- package/dist/serve/docs-ui/docs/build/ai-logic.md +69 -21
- package/dist/serve/docs-ui/docs/build/authentication/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-firestore/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-messaging/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-storage/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/realtime-database/index.html +1 -1
- package/dist/serve/docs-ui/docs/conformance-scores/index.html +1 -1
- package/dist/serve/docs-ui/docs/create-pyric-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/database-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/firestore-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/functions-rtdb-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/get-started/how-the-swap-works/index.html +2 -1
- package/dist/serve/docs-ui/docs/get-started/how-the-swap-works.md +2 -0
- package/dist/serve/docs-ui/docs/get-started/start-building/index.html +3 -2
- package/dist/serve/docs-ui/docs/get-started/start-building.md +3 -1
- package/dist/serve/docs-ui/docs/index.json +349 -29
- package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/observe/resolve-runtime-status/index.html +28 -0
- package/dist/serve/docs-ui/docs/observe/resolve-runtime-status.md +47 -0
- package/dist/serve/docs-ui/docs/observe/see-whats-happening/index.html +1 -1
- package/dist/serve/docs-ui/docs/observe/shape-your-data/index.html +1 -1
- package/dist/serve/docs-ui/docs/overview/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api/index.html +916 -0
- package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api.md +706 -0
- package/dist/serve/docs-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ai-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-app-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api/index.html +168 -76
- package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +156 -73
- package/dist/serve/docs-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api/index.html +11 -8
- package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api.md +17 -4
- package/dist/serve/docs-ui/docs/pyric-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-messaging-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-rules-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api/index.html +37 -14
- package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api.md +28 -8
- package/dist/serve/docs-ui/docs/pyric-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/rules-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/audit-your-rules/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/firestore-rules-limits/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/read-a-denial/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/rules-standard-library/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/secure-it-with-rules/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/simulate-and-lint/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
- package/dist/serve/docs-ui/docs/ship/ship-to-production/index.html +1 -1
- package/dist/serve/docs-ui/docs/ship/test-in-node/index.html +1 -1
- package/dist/serve/docs-ui/docs/storage-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
- package/dist/serve/docs-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
- package/dist/serve/docs-ui/docs/tutorial/index.html +429 -0
- package/dist/serve/docs-ui/docs/tutorial.md +701 -0
- package/dist/serve/docs-ui/llms.txt +3 -1
- package/dist/serve/entries/init.d.ts.map +1 -1
- package/dist/serve/entries/init.js +3 -0
- package/dist/serve/entries/init.js.map +1 -1
- package/dist/serve/entries/runtime.d.ts.map +1 -1
- package/dist/serve/entries/runtime.js +20 -5
- package/dist/serve/entries/runtime.js.map +1 -1
- package/dist/serve/entries/worker-runtime.d.ts +2 -2
- package/dist/serve/entries/worker-runtime.d.ts.map +1 -1
- package/dist/serve/entries/worker-runtime.js +45 -10
- package/dist/serve/entries/worker-runtime.js.map +1 -1
- package/dist/serve/runtime/chip-config.d.ts +19 -0
- package/dist/serve/runtime/chip-config.d.ts.map +1 -0
- package/dist/serve/runtime/chip-config.js +19 -0
- package/dist/serve/runtime/chip-config.js.map +1 -0
- package/dist/serve/runtime/chip-install.d.ts +10 -0
- package/dist/serve/runtime/chip-install.d.ts.map +1 -0
- package/dist/serve/runtime/chip-install.js +15 -0
- package/dist/serve/runtime/chip-install.js.map +1 -0
- package/dist/serve/runtime/chip.d.ts +17 -0
- package/dist/serve/runtime/chip.d.ts.map +1 -0
- package/dist/serve/runtime/chip.js +257 -0
- package/dist/serve/runtime/chip.js.map +1 -0
- package/dist/serve/runtime/manifest.d.ts +22 -0
- package/dist/serve/runtime/manifest.d.ts.map +1 -0
- package/dist/serve/runtime/manifest.js +20 -0
- package/dist/serve/runtime/manifest.js.map +1 -0
- package/dist/serve/runtime/status.d.ts +49 -0
- package/dist/serve/runtime/status.d.ts.map +1 -0
- package/dist/serve/runtime/status.js +164 -0
- package/dist/serve/runtime/status.js.map +1 -0
- package/dist/serve/runtime/worker-connection.d.ts +4 -0
- package/dist/serve/runtime/worker-connection.d.ts.map +1 -0
- package/dist/serve/runtime/worker-connection.js +11 -0
- package/dist/serve/runtime/worker-connection.js.map +1 -0
- package/dist/serve/runtime/worker-generation.d.ts +14 -0
- package/dist/serve/runtime/worker-generation.d.ts.map +1 -0
- package/dist/serve/runtime/worker-generation.js +56 -0
- package/dist/serve/runtime/worker-generation.js.map +1 -0
- package/dist/serve/runtime/worker-replacement.d.ts +18 -0
- package/dist/serve/runtime/worker-replacement.d.ts.map +1 -0
- package/dist/serve/runtime/worker-replacement.js +29 -0
- package/dist/serve/runtime/worker-replacement.js.map +1 -0
- package/dist/serve/standalone-assets.d.ts +2 -2
- package/dist/serve/standalone-assets.d.ts.map +1 -1
- package/dist/serve/standalone-assets.js +1 -1
- package/dist/serve/standalone-assets.js.map +1 -1
- package/dist/serve/studio-ui/assets/{RulesCodeEditor-CxVYcZXw.js → RulesCodeEditor-BDlH-5lp.js} +1 -1
- package/dist/serve/studio-ui/assets/{index-cpSfgQ7H.js → index-Bf9SSE8L.js} +5 -5
- package/dist/serve/studio-ui/assets/{seed-DeuFKi4J.js → seed-D_UjxfOU.js} +1 -1
- package/dist/serve/studio-ui/index.html +1 -1
- package/dist/serve/vite-ai-config.d.ts +25 -0
- package/dist/serve/vite-ai-config.d.ts.map +1 -0
- package/dist/serve/vite-ai-config.js +45 -0
- package/dist/serve/vite-ai-config.js.map +1 -0
- package/dist/serve/vite-plugin.d.ts +17 -36
- package/dist/serve/vite-plugin.d.ts.map +1 -1
- package/dist/serve/vite-plugin.js +38 -57
- package/dist/serve/vite-plugin.js.map +1 -1
- package/dist/serve/vite-rules-source.d.ts +8 -0
- package/dist/serve/vite-rules-source.d.ts.map +1 -0
- package/dist/serve/vite-rules-source.js +23 -0
- package/dist/serve/vite-rules-source.js.map +1 -0
- package/dist/serve/vite-worker-runtime.d.ts +20 -0
- package/dist/serve/vite-worker-runtime.d.ts.map +1 -0
- package/dist/serve/vite-worker-runtime.js +25 -0
- package/dist/serve/vite-worker-runtime.js.map +1 -0
- package/dist/serve/worker/client/connection.d.ts +7 -2
- package/dist/serve/worker/client/connection.d.ts.map +1 -1
- package/dist/serve/worker/client/connection.js +13 -16
- package/dist/serve/worker/client/connection.js.map +1 -1
- package/dist/serve/worker/client/core.d.ts +7 -0
- package/dist/serve/worker/client/core.d.ts.map +1 -1
- package/dist/serve/worker/client/core.js +28 -0
- package/dist/serve/worker/client/core.js.map +1 -1
- package/dist/serve/worker/client/runtime-control.d.ts +8 -0
- package/dist/serve/worker/client/runtime-control.d.ts.map +1 -0
- package/dist/serve/worker/client/runtime-control.js +11 -0
- package/dist/serve/worker/client/runtime-control.js.map +1 -0
- package/dist/serve/worker/entry.js +44 -2
- package/dist/serve/worker/entry.js.map +1 -1
- package/dist/serve/worker/host/core.js +1 -1
- package/dist/serve/worker/host/core.js.map +1 -1
- package/dist/serve/worker/host/firestore-writes.d.ts.map +1 -1
- package/dist/serve/worker/host/firestore-writes.js +3 -2
- package/dist/serve/worker/host/firestore-writes.js.map +1 -1
- package/dist/serve/worker/host/studio.js +1 -1
- package/dist/serve/worker/host/studio.js.map +1 -1
- package/dist/serve/worker/host-context.d.ts +1 -1
- package/dist/serve/worker/host-context.d.ts.map +1 -1
- package/dist/serve/worker/index.d.ts +2 -1
- package/dist/serve/worker/index.d.ts.map +1 -1
- package/dist/serve/worker/index.js +2 -1
- package/dist/serve/worker/index.js.map +1 -1
- package/dist/serve/worker/protocol.d.ts +15 -1
- package/dist/serve/worker/protocol.d.ts.map +1 -1
- package/dist/serve/worker/protocol.js +2 -2
- package/dist/serve/worker/protocol.js.map +1 -1
- package/dist/serve/worker/retirement.d.ts +21 -0
- package/dist/serve/worker/retirement.d.ts.map +1 -0
- package/dist/serve/worker/retirement.js +97 -0
- package/dist/serve/worker/retirement.js.map +1 -0
- package/dist/serve/worker/serve-init.d.ts.map +1 -1
- package/dist/serve/worker/serve-init.js +5 -6
- package/dist/serve/worker/serve-init.js.map +1 -1
- package/dist/vite.d.ts +1 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js.map +1 -1
- package/package.json +6 -4
- package/src/assurance/.generated/conformance-verdicts.ts +1090 -0
- package/src/assurance/attachment.ts +211 -0
- package/src/assurance/browser.ts +80 -0
- package/src/assurance/campaign.ts +496 -0
- package/src/assurance/capabilities.ts +446 -0
- package/src/assurance/cases.ts +43 -0
- package/src/assurance/index.ts +75 -0
- package/src/assurance/runner.ts +885 -0
- package/src/assurance/tool-names.ts +15 -0
- package/src/assurance/tools.ts +830 -0
- package/src/assurance/types.ts +352 -0
- package/src/assurance/validation.ts +434 -0
- package/src/bridge/client/bridge.ts +550 -0
- package/src/bridge/client/dispatch.ts +137 -0
- package/src/bridge/client.ts +45 -0
- package/src/bridge/protocol.ts +351 -0
- package/src/bridge/server/audit.ts +53 -0
- package/src/bridge/server/bridge.ts +597 -0
- package/src/bridge/server/headless.ts +160 -0
- package/src/bridge/server/json-schema-to-zod.ts +108 -0
- package/src/bridge/server/local-bridge.ts +66 -0
- package/src/bridge/server/logger.ts +50 -0
- package/src/bridge/server/mcp-contract.ts +93 -0
- package/src/bridge/server/mcp.ts +129 -0
- package/src/bridge/server/peer.ts +232 -0
- package/src/bridge/server/standalone.ts +347 -0
- package/src/bridge/server/tool-metadata.ts +99 -0
- package/src/bridge/server.ts +28 -0
- package/src/cli/can-i-use.ts +50 -0
- package/src/cli/cli.test.ts +493 -0
- package/src/cli/database-rules.ts +329 -0
- package/src/cli/dev-runner.ts +282 -0
- package/src/cli/firebase-json.ts +100 -0
- package/src/cli/firestore-indexes.ts +70 -0
- package/src/cli/index.ts +425 -0
- package/src/cli/init.ts +260 -0
- package/src/cli/mcp-proxy.ts +196 -0
- package/src/cli/parse-args.ts +80 -0
- package/src/cli/rules.ts +259 -0
- package/src/cli/scope.ts +67 -0
- package/src/cli/serve.ts +996 -0
- package/src/cli/service-commands.ts +73 -0
- package/src/cli/snapshot.ts +164 -0
- package/src/cli/storage-rules.ts +172 -0
- package/src/cli/verify.ts +416 -0
- package/src/conformance/.generated/can-i-use-browser.ts +113 -0
- package/src/conformance/.generated/can-i-use.ts +117 -0
- package/src/conformance/.generated/conformance-docs.ts +14 -0
- package/src/conformance/browser.ts +24 -0
- package/src/conformance/can-i-use-tool.ts +51 -0
- package/src/conformance/can-i-use.ts +18 -0
- package/src/conformance/docs.ts +8 -0
- package/src/conformance/index.ts +20 -0
- package/src/conformance/tools.ts +16 -0
- package/src/credentials/core/memoize-ttl.ts +142 -0
- package/src/credentials/core/types.ts +10 -0
- package/src/credentials/node/from-adc.ts +96 -0
- package/src/credentials/node/from-service-account.ts +133 -0
- package/src/credentials/node/index.ts +6 -0
- package/src/discover/concurrency.ts +127 -0
- package/src/discover/crawler-adapter.ts +142 -0
- package/src/discover/crawler.ts +1127 -0
- package/src/discover/credential-free.ts +27 -0
- package/src/discover/findCollectionGroup.ts +131 -0
- package/src/discover/firestore-source.ts +59 -0
- package/src/discover/index.ts +5 -0
- package/src/discover/merge.ts +523 -0
- package/src/discover/session.ts +402 -0
- package/src/discover/tools.ts +201 -0
- package/src/discover/types.ts +187 -0
- package/src/discover/wire.ts +324 -0
- package/src/functions-rtdb/child.ts +382 -0
- package/src/functions-rtdb/delivery.ts +7 -0
- package/src/functions-rtdb/discovery.ts +136 -0
- package/src/functions-rtdb/event.ts +64 -0
- package/src/functions-rtdb/execution.ts +111 -0
- package/src/functions-rtdb/in-memory-delivery.ts +40 -0
- package/src/functions-rtdb/project.ts +74 -0
- package/src/functions-rtdb/projection.ts +104 -0
- package/src/functions-rtdb/reference-pattern.ts +20 -0
- package/src/functions-rtdb/remote-delivery.ts +23 -0
- package/src/pkg-version.ts +49 -0
- package/src/register/esm-exports.ts +63 -0
- package/src/register/hooks.ts +30 -0
- package/src/register/index.ts +150 -0
- package/src/register/mapping.ts +33 -0
- package/src/remote/index.ts +1050 -0
- package/src/rtdb/crawl-snapshot.ts +118 -0
- package/src/rtdb/inspection.ts +137 -0
- package/src/rtdb/load-rules-document.ts +56 -0
- package/src/rtdb/rules-generation-tool.ts +38 -0
- package/src/rtdb/rules-json.ts +29 -0
- package/src/serve/activity-guard.ts +29 -0
- package/src/serve/activity-route.ts +164 -0
- package/src/serve/activity-warning.ts +26 -0
- package/src/serve/bridge-mount.ts +200 -0
- package/src/serve/bundler.ts +519 -0
- package/src/serve/capture-store.ts +65 -0
- package/src/serve/discovery.ts +199 -0
- package/src/serve/entries/ai.ts +292 -0
- package/src/serve/entries/app-backend.ts +4 -0
- package/src/serve/entries/app-client.ts +26 -0
- package/src/serve/entries/app-session-store.ts +47 -0
- package/src/serve/entries/app.ts +11 -0
- package/src/serve/entries/auth-helper-core.ts +181 -0
- package/src/serve/entries/auth-helper-dom.ts +134 -0
- package/src/serve/entries/auth-helper-runtime.ts +20 -0
- package/src/serve/entries/auth.ts +255 -0
- package/src/serve/entries/bridge-url.ts +31 -0
- package/src/serve/entries/database.ts +129 -0
- package/src/serve/entries/firestore.ts +232 -0
- package/src/serve/entries/init.ts +49 -0
- package/src/serve/entries/keepalive.ts +46 -0
- package/src/serve/entries/messaging-sw.ts +75 -0
- package/src/serve/entries/messaging.ts +96 -0
- package/src/serve/entries/runtime.ts +556 -0
- package/src/serve/entries/session-store.ts +97 -0
- package/src/serve/entries/storage.ts +92 -0
- package/src/serve/entries/tab-sync-wiring.ts +274 -0
- package/src/serve/entries/worker-runtime.ts +135 -0
- package/src/serve/init-payload.ts +42 -0
- package/src/serve/namespace.ts +675 -0
- package/src/serve/open-browser.ts +68 -0
- package/src/serve/rules.ts +272 -0
- package/src/serve/runtime/chip-config.ts +30 -0
- package/src/serve/runtime/chip-install.ts +27 -0
- package/src/serve/runtime/chip.ts +282 -0
- package/src/serve/runtime/manifest.ts +37 -0
- package/src/serve/runtime/status.ts +216 -0
- package/src/serve/runtime/worker-connection.ts +14 -0
- package/src/serve/runtime/worker-generation.ts +75 -0
- package/src/serve/runtime/worker-replacement.ts +42 -0
- package/src/serve/sandbox-marker.ts +31 -0
- package/src/serve/server.ts +428 -0
- package/src/serve/standalone-assets.ts +180 -0
- package/src/serve/state-store.ts +166 -0
- package/src/serve/studio/disk-project-store.ts +185 -0
- package/src/serve/studio/disk-workspace.ts +162 -0
- package/src/serve/studio/index.ts +20 -0
- package/src/serve/studio/routes.ts +237 -0
- package/src/serve/studio/store-types.ts +48 -0
- package/src/serve/studio/studio-storage.test.ts +289 -0
- package/src/serve/vite-ai-config.ts +77 -0
- package/src/serve/vite-plugin.ts +1075 -0
- package/src/serve/vite-rules-source.ts +28 -0
- package/src/serve/vite-worker-runtime.ts +51 -0
- package/src/serve/worker/activity-bootstrap.ts +24 -0
- package/src/serve/worker/client/admin-firestore.ts +38 -0
- package/src/serve/worker/client/ai.ts +124 -0
- package/src/serve/worker/client/auth.ts +489 -0
- package/src/serve/worker/client/connection.ts +227 -0
- package/src/serve/worker/client/core.ts +384 -0
- package/src/serve/worker/client/disconnect.ts +45 -0
- package/src/serve/worker/client/firestore-reads.ts +181 -0
- package/src/serve/worker/client/firestore-refs.ts +226 -0
- package/src/serve/worker/client/firestore-writes.ts +232 -0
- package/src/serve/worker/client/handles.ts +91 -0
- package/src/serve/worker/client/messaging.ts +119 -0
- package/src/serve/worker/client/presence.ts +174 -0
- package/src/serve/worker/client/rtdb.ts +311 -0
- package/src/serve/worker/client/rules.ts +63 -0
- package/src/serve/worker/client/runtime-control.ts +22 -0
- package/src/serve/worker/client/service-worker-connection.ts +59 -0
- package/src/serve/worker/client/snapshots.ts +86 -0
- package/src/serve/worker/client/storage.ts +195 -0
- package/src/serve/worker/client/studio.ts +88 -0
- package/src/serve/worker/client.ts +57 -0
- package/src/serve/worker/durable-persistence.ts +137 -0
- package/src/serve/worker/entry.ts +223 -0
- package/src/serve/worker/host/admin-firestore.ts +88 -0
- package/src/serve/worker/host/connection.ts +179 -0
- package/src/serve/worker/host/core.ts +387 -0
- package/src/serve/worker/host/dispatch.ts +316 -0
- package/src/serve/worker/host/firestore-reads.ts +117 -0
- package/src/serve/worker/host/firestore-writes.ts +456 -0
- package/src/serve/worker/host/presence.ts +312 -0
- package/src/serve/worker/host/rtdb.ts +136 -0
- package/src/serve/worker/host/rules.ts +128 -0
- package/src/serve/worker/host/storage.ts +312 -0
- package/src/serve/worker/host/studio.ts +75 -0
- package/src/serve/worker/host/subscriptions.ts +238 -0
- package/src/serve/worker/host-ai.ts +165 -0
- package/src/serve/worker/host-auth.ts +469 -0
- package/src/serve/worker/host-context.ts +273 -0
- package/src/serve/worker/host-events.ts +94 -0
- package/src/serve/worker/host-messaging.ts +240 -0
- package/src/serve/worker/host.ts +47 -0
- package/src/serve/worker/index.ts +196 -0
- package/src/serve/worker/presence-timing.ts +15 -0
- package/src/serve/worker/protocol.ts +1124 -0
- package/src/serve/worker/retirement.ts +125 -0
- package/src/serve/worker/serve-init.ts +605 -0
- package/src/serve/worker/service-worker-channel.ts +33 -0
- package/src/serve/worker/service-worker-relay.ts +86 -0
- package/src/serve/writer-lock.ts +53 -0
- package/src/verify/cases.ts +233 -0
- package/src/verify/fixture.ts +258 -0
- package/src/verify/index.ts +519 -0
- package/src/verify/tools.ts +108 -0
- package/src/version/compat-target.ts +17 -0
- package/src/vite.ts +18 -0
- package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api/index.html +0 -31
- package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api.md +0 -29
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio storage routes — the HTTP surface for `@pyric/studio` in `local` mode.
|
|
3
|
+
* Mounted inside the `/__pyric/` namespace; the browser-side `httpWorkspace`
|
|
4
|
+
* / `httpProjectStore` clients in `packages/studio/src/clients` speak exactly
|
|
5
|
+
* these endpoints.
|
|
6
|
+
*
|
|
7
|
+
* WORKSPACE (a single project's file tree)
|
|
8
|
+
* GET /__pyric/workspace?path=src/app.tsx → file body (200) | 404
|
|
9
|
+
* PUT /__pyric/workspace?path=... → write body (204)
|
|
10
|
+
* DELETE /__pyric/workspace?path=... → remove (204)
|
|
11
|
+
* GET /__pyric/workspace/list?dir=src → WorkspaceEntry[] (200)
|
|
12
|
+
* GET /__pyric/workspace/watch → SSE stream of WorkspaceChange
|
|
13
|
+
*
|
|
14
|
+
* PROJECTS (sessions/projects)
|
|
15
|
+
* GET /__pyric/projects → ProjectMeta[] (200)
|
|
16
|
+
* POST /__pyric/projects {title?} → ProjectMeta (created) (200)
|
|
17
|
+
* GET /__pyric/projects/<id> → ProjectMeta (200) | 404
|
|
18
|
+
* PATCH /__pyric/projects/<id> {title?,...} → (204)
|
|
19
|
+
* DELETE /__pyric/projects/<id> → (204)
|
|
20
|
+
*
|
|
21
|
+
* The `watch` stream reuses the SSE pattern already used for `/__pyric/events`
|
|
22
|
+
* (the hot-reload hub): one `text/event-stream` response per subscriber, frames
|
|
23
|
+
* are `event: change\ndata: <json>\n\n`, and the subscription is torn down on
|
|
24
|
+
* `req.close`.
|
|
25
|
+
*/
|
|
26
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
27
|
+
|
|
28
|
+
import { WorkspacePathError } from './disk-workspace.js';
|
|
29
|
+
import { ProjectIdError } from './disk-project-store.js';
|
|
30
|
+
import type {
|
|
31
|
+
ProjectMeta,
|
|
32
|
+
ProjectStore,
|
|
33
|
+
WorkspaceChange,
|
|
34
|
+
WorkspaceStore,
|
|
35
|
+
} from './store-types.js';
|
|
36
|
+
|
|
37
|
+
export interface StudioRouteOptions {
|
|
38
|
+
/** The single-project file tree served at `/__pyric/workspace`. */
|
|
39
|
+
workspace?: WorkspaceStore;
|
|
40
|
+
/** The project list served at `/__pyric/projects`. */
|
|
41
|
+
projects?: ProjectStore;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Collect the raw request body as a UTF-8 string. */
|
|
45
|
+
function readBody(req: IncomingMessage): Promise<string> {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
let raw = '';
|
|
48
|
+
req.setEncoding('utf8');
|
|
49
|
+
req.on('data', (c: string) => (raw += c));
|
|
50
|
+
req.on('end', () => resolve(raw));
|
|
51
|
+
req.on('error', reject);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Collect + parse the request body as JSON (null on empty/invalid). */
|
|
56
|
+
async function readJson<T>(req: IncomingMessage): Promise<T | null> {
|
|
57
|
+
const raw = await readBody(req);
|
|
58
|
+
if (raw.trim() === '') return null;
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(raw) as T;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
67
|
+
res.writeHead(status, {
|
|
68
|
+
'content-type': 'application/json; charset=utf-8',
|
|
69
|
+
'cache-control': 'no-store',
|
|
70
|
+
});
|
|
71
|
+
res.end(JSON.stringify(body));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sendError(res: ServerResponse, e: unknown): void {
|
|
75
|
+
const status =
|
|
76
|
+
e instanceof WorkspacePathError || e instanceof ProjectIdError ? 400 : 500;
|
|
77
|
+
res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
|
|
78
|
+
res.end(e instanceof Error ? e.message : String(e));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function handleWorkspace(
|
|
82
|
+
ws: WorkspaceStore,
|
|
83
|
+
req: IncomingMessage,
|
|
84
|
+
res: ServerResponse,
|
|
85
|
+
url: URL,
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
// ── SSE watch ────────────────────────────────────────────────────────
|
|
88
|
+
if (url.pathname === '/__pyric/workspace/watch') {
|
|
89
|
+
res.writeHead(200, {
|
|
90
|
+
'content-type': 'text/event-stream',
|
|
91
|
+
'cache-control': 'no-store',
|
|
92
|
+
connection: 'keep-alive',
|
|
93
|
+
});
|
|
94
|
+
res.write(': connected\n\n');
|
|
95
|
+
const unsub = ws.watch((change: WorkspaceChange) => {
|
|
96
|
+
// The write can race the socket teardown (page reload while a
|
|
97
|
+
// workspace change fires) — a throw here would escape into the
|
|
98
|
+
// fs.watch callback and kill the serve process.
|
|
99
|
+
try {
|
|
100
|
+
res.write(`event: change\ndata: ${JSON.stringify(change)}\n\n`);
|
|
101
|
+
} catch {
|
|
102
|
+
unsub();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
req.on('close', () => unsub());
|
|
106
|
+
res.on('error', () => unsub());
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── list ─────────────────────────────────────────────────────────────
|
|
111
|
+
if (url.pathname === '/__pyric/workspace/list') {
|
|
112
|
+
if (req.method !== 'GET') {
|
|
113
|
+
res.writeHead(405, { allow: 'GET' }).end('method not allowed');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const dir = url.searchParams.get('dir') ?? undefined;
|
|
117
|
+
sendJson(res, 200, await ws.list(dir));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── read / write / remove a single path ──────────────────────────────
|
|
122
|
+
const path = url.searchParams.get('path');
|
|
123
|
+
if (path === null) {
|
|
124
|
+
res.writeHead(400).end('missing ?path');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (req.method === 'GET') {
|
|
128
|
+
const content = await ws.read(path);
|
|
129
|
+
if (content === null) {
|
|
130
|
+
res.writeHead(404).end('not found');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
res.writeHead(200, {
|
|
134
|
+
'content-type': 'application/octet-stream',
|
|
135
|
+
'cache-control': 'no-store',
|
|
136
|
+
});
|
|
137
|
+
res.end(content);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (req.method === 'PUT') {
|
|
141
|
+
const body = await readBody(req);
|
|
142
|
+
await ws.write(path, body);
|
|
143
|
+
res.writeHead(204).end();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (req.method === 'DELETE') {
|
|
147
|
+
await ws.remove(path);
|
|
148
|
+
res.writeHead(204).end();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
res.writeHead(405, { allow: 'GET, PUT, DELETE' }).end('method not allowed');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function handleProjects(
|
|
155
|
+
store: ProjectStore,
|
|
156
|
+
req: IncomingMessage,
|
|
157
|
+
res: ServerResponse,
|
|
158
|
+
url: URL,
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
// /__pyric/projects/<id> | /__pyric/projects
|
|
161
|
+
const rest = url.pathname.slice('/__pyric/projects'.length);
|
|
162
|
+
const id = rest.replace(/^\//, '');
|
|
163
|
+
|
|
164
|
+
if (id === '') {
|
|
165
|
+
if (req.method === 'GET') {
|
|
166
|
+
sendJson(res, 200, await store.list());
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (req.method === 'POST') {
|
|
170
|
+
const body = await readJson<{ title?: string }>(req);
|
|
171
|
+
const meta = await store.create({ title: body?.title });
|
|
172
|
+
sendJson(res, 200, meta);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
res.writeHead(405, { allow: 'GET, POST' }).end('method not allowed');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// single project by id
|
|
180
|
+
if (req.method === 'GET') {
|
|
181
|
+
try {
|
|
182
|
+
const handle = await store.open(decodeURIComponent(id));
|
|
183
|
+
sendJson(res, 200, handle.meta);
|
|
184
|
+
} catch {
|
|
185
|
+
res.writeHead(404).end('not found');
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (req.method === 'PATCH') {
|
|
190
|
+
const body = await readJson<Record<string, unknown>>(req);
|
|
191
|
+
// Only forward known, mutable ProjectMeta fields — never `id`.
|
|
192
|
+
const patch: Partial<Omit<ProjectMeta, 'id'>> = {};
|
|
193
|
+
if (body && typeof body.title === 'string') patch.title = body.title;
|
|
194
|
+
if (body && typeof body.createdAt === 'number') patch.createdAt = body.createdAt;
|
|
195
|
+
if (body && typeof body.updatedAt === 'number') patch.updatedAt = body.updatedAt;
|
|
196
|
+
await store.update(decodeURIComponent(id), patch);
|
|
197
|
+
res.writeHead(204).end();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (req.method === 'DELETE') {
|
|
201
|
+
await store.remove(decodeURIComponent(id));
|
|
202
|
+
res.writeHead(204).end();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
res
|
|
206
|
+
.writeHead(405, { allow: 'GET, PATCH, DELETE' })
|
|
207
|
+
.end('method not allowed');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Build a namespace fragment for the Studio storage routes. Returns a handler
|
|
212
|
+
* shaped like the other `/__pyric/*` fragments: it resolves `true` when it
|
|
213
|
+
* owned the request, `false` otherwise (so the caller can fall through).
|
|
214
|
+
*/
|
|
215
|
+
export function createStudioRoutes(opts: StudioRouteOptions) {
|
|
216
|
+
return async (
|
|
217
|
+
req: IncomingMessage,
|
|
218
|
+
res: ServerResponse,
|
|
219
|
+
url: URL,
|
|
220
|
+
): Promise<boolean> => {
|
|
221
|
+
try {
|
|
222
|
+
if (opts.workspace && url.pathname.startsWith('/__pyric/workspace')) {
|
|
223
|
+
await handleWorkspace(opts.workspace, req, res, url);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (opts.projects && url.pathname.startsWith('/__pyric/projects')) {
|
|
227
|
+
await handleProjects(opts.projects, req, res, url);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
} catch (e) {
|
|
231
|
+
if (!res.headersSent) sendError(res, e);
|
|
232
|
+
else res.end();
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio storage port shapes — server-side MIRROR of `@pyric/studio/ports`.
|
|
3
|
+
*
|
|
4
|
+
* `@pyric/studio` is a CONSUMER of @pyric/cli' serve routes, not a dependency
|
|
5
|
+
* of it; adding a `@pyric/studio` dep here would invert the layering (and risk
|
|
6
|
+
* a cycle). So the disk impls in this directory type-check against these local
|
|
7
|
+
* copies, which are structurally identical to the port interfaces. Keep them in
|
|
8
|
+
* lockstep with `packages/studio/src/ports.ts` — that file is the contract.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface WorkspaceEntry {
|
|
12
|
+
/** Project-relative POSIX path. */
|
|
13
|
+
path: string;
|
|
14
|
+
kind: 'file' | 'dir';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WorkspaceChange {
|
|
18
|
+
path: string;
|
|
19
|
+
type: 'create' | 'update' | 'delete';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WorkspaceStore {
|
|
23
|
+
read(path: string): Promise<string | null>;
|
|
24
|
+
write(path: string, content: string): Promise<void>;
|
|
25
|
+
list(dir?: string): Promise<WorkspaceEntry[]>;
|
|
26
|
+
remove(path: string): Promise<void>;
|
|
27
|
+
watch(cb: (change: WorkspaceChange) => void): () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ProjectMeta {
|
|
31
|
+
id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
createdAt: number;
|
|
34
|
+
updatedAt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ProjectHandle {
|
|
38
|
+
meta: ProjectMeta;
|
|
39
|
+
workspace: WorkspaceStore;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ProjectStore {
|
|
43
|
+
list(): Promise<ProjectMeta[]>;
|
|
44
|
+
open(id: string): Promise<ProjectHandle>;
|
|
45
|
+
create(input: { title?: string }): Promise<ProjectMeta>;
|
|
46
|
+
update(id: string, patch: Partial<Omit<ProjectMeta, 'id'>>): Promise<void>;
|
|
47
|
+
remove(id: string): Promise<void>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Track T3 — disk-backed Studio storage + serve routes.
|
|
3
|
+
*
|
|
4
|
+
* Round-trips `diskWorkspace` / `diskProjectStore` in a temp dir (read/write/
|
|
5
|
+
* list/remove, path-traversal refusal, project CRUD) and smoke-tests the route
|
|
6
|
+
* handler by importing it directly and driving it with mock req/res (no live
|
|
7
|
+
* server).
|
|
8
|
+
*/
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
10
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { PassThrough } from 'node:stream';
|
|
15
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
diskWorkspace,
|
|
19
|
+
WorkspacePathError,
|
|
20
|
+
} from './disk-workspace.js';
|
|
21
|
+
import {
|
|
22
|
+
diskProjectStore,
|
|
23
|
+
slugifyProjectId,
|
|
24
|
+
ProjectIdError,
|
|
25
|
+
} from './disk-project-store.js';
|
|
26
|
+
import { createStudioRoutes } from './routes.js';
|
|
27
|
+
|
|
28
|
+
let dir: string;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
dir = mkdtempSync(join(tmpdir(), 'pyric-studio-'));
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
rmSync(dir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('diskWorkspace', () => {
|
|
37
|
+
it('write → read round-trips, creating parent dirs', async () => {
|
|
38
|
+
const ws = diskWorkspace(dir);
|
|
39
|
+
await ws.write('src/app.tsx', 'export const x = 1;');
|
|
40
|
+
expect(await ws.read('src/app.tsx')).toBe('export const x = 1;');
|
|
41
|
+
expect(existsSync(join(dir, 'src', 'app.tsx'))).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('read of a missing file is null', async () => {
|
|
45
|
+
const ws = diskWorkspace(dir);
|
|
46
|
+
expect(await ws.read('nope.txt')).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('list returns sorted file/dir entries (project-relative POSIX)', async () => {
|
|
50
|
+
const ws = diskWorkspace(dir);
|
|
51
|
+
await ws.write('b.txt', '2');
|
|
52
|
+
await ws.write('a.txt', '1');
|
|
53
|
+
await ws.write('sub/c.txt', '3');
|
|
54
|
+
const root = await ws.list();
|
|
55
|
+
expect(root).toEqual([
|
|
56
|
+
{ path: 'a.txt', kind: 'file' },
|
|
57
|
+
{ path: 'b.txt', kind: 'file' },
|
|
58
|
+
{ path: 'sub', kind: 'dir' },
|
|
59
|
+
]);
|
|
60
|
+
const sub = await ws.list('sub');
|
|
61
|
+
expect(sub).toEqual([{ path: 'sub/c.txt', kind: 'file' }]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('remove deletes a file and is idempotent', async () => {
|
|
65
|
+
const ws = diskWorkspace(dir);
|
|
66
|
+
await ws.write('gone.txt', 'x');
|
|
67
|
+
await ws.remove('gone.txt');
|
|
68
|
+
expect(await ws.read('gone.txt')).toBeNull();
|
|
69
|
+
await ws.remove('gone.txt'); // no throw
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('refuses path traversal outside the root', async () => {
|
|
73
|
+
const ws = diskWorkspace(dir);
|
|
74
|
+
await expect(ws.read('../escape.txt')).rejects.toBeInstanceOf(
|
|
75
|
+
WorkspacePathError,
|
|
76
|
+
);
|
|
77
|
+
await expect(ws.write('../../x', 'y')).rejects.toBeInstanceOf(
|
|
78
|
+
WorkspacePathError,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('refuses removing the root itself', async () => {
|
|
83
|
+
const ws = diskWorkspace(dir);
|
|
84
|
+
await expect(ws.remove('')).rejects.toBeInstanceOf(WorkspacePathError);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('watch fires on a write and unsubscribes cleanly', async () => {
|
|
88
|
+
const ws = diskWorkspace(dir);
|
|
89
|
+
const seen: string[] = [];
|
|
90
|
+
const unsub = ws.watch((c) => seen.push(`${c.type}:${c.path}`));
|
|
91
|
+
// Let the recursive watcher attach before mutating.
|
|
92
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
93
|
+
await ws.write('watched.txt', 'hi');
|
|
94
|
+
// fs.watch is async; give it a tick.
|
|
95
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
96
|
+
unsub();
|
|
97
|
+
// At least one change for the file should have surfaced (event coalescing
|
|
98
|
+
// varies by platform, so assert membership not exact count).
|
|
99
|
+
expect(seen.some((s) => s.includes('watched.txt'))).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('diskProjectStore', () => {
|
|
104
|
+
it('create → list → open → update → remove round-trips', async () => {
|
|
105
|
+
const store = diskProjectStore(dir);
|
|
106
|
+
expect(await store.list()).toEqual([]);
|
|
107
|
+
|
|
108
|
+
const meta = await store.create({ title: 'My App' });
|
|
109
|
+
expect(meta.id).toBe('my-app');
|
|
110
|
+
expect(meta.title).toBe('My App');
|
|
111
|
+
expect(meta.createdAt).toBeGreaterThan(0);
|
|
112
|
+
|
|
113
|
+
const list = await store.list();
|
|
114
|
+
expect(list.map((m) => m.id)).toEqual(['my-app']);
|
|
115
|
+
|
|
116
|
+
const handle = await store.open('my-app');
|
|
117
|
+
expect(handle.meta.title).toBe('My App');
|
|
118
|
+
await handle.workspace.write('index.html', '<h1>hi</h1>');
|
|
119
|
+
expect(await handle.workspace.read('index.html')).toBe('<h1>hi</h1>');
|
|
120
|
+
|
|
121
|
+
await store.update('my-app', { title: 'Renamed' });
|
|
122
|
+
expect((await store.open('my-app')).meta.title).toBe('Renamed');
|
|
123
|
+
|
|
124
|
+
await store.remove('my-app');
|
|
125
|
+
expect(await store.list()).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('disambiguates ids when titles collide', async () => {
|
|
129
|
+
const store = diskProjectStore(dir);
|
|
130
|
+
const a = await store.create({ title: 'Dup' });
|
|
131
|
+
const b = await store.create({ title: 'Dup' });
|
|
132
|
+
expect(a.id).toBe('dup');
|
|
133
|
+
expect(b.id).toBe('dup-2');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('open of a missing project rejects', async () => {
|
|
137
|
+
const store = diskProjectStore(dir);
|
|
138
|
+
await expect(store.open('ghost')).rejects.toThrow(/not found/);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('rejects unsafe ids', async () => {
|
|
142
|
+
const store = diskProjectStore(dir);
|
|
143
|
+
await expect(store.open('../evil')).rejects.toBeInstanceOf(ProjectIdError);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('slugifyProjectId is filesystem-safe', () => {
|
|
147
|
+
expect(slugifyProjectId('Hello, World!')).toBe('hello-world');
|
|
148
|
+
expect(slugifyProjectId(' spaced out ')).toBe('spaced-out');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ── Route smoke tests (handler imported directly) ─────────────────────────
|
|
153
|
+
|
|
154
|
+
interface MockRes {
|
|
155
|
+
res: ServerResponse;
|
|
156
|
+
statusCode: number;
|
|
157
|
+
body: string;
|
|
158
|
+
headers: Record<string, unknown>;
|
|
159
|
+
done: Promise<void>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function mockReq(method: string): IncomingMessage {
|
|
163
|
+
const req = new PassThrough() as unknown as IncomingMessage;
|
|
164
|
+
(req as unknown as { method: string }).method = method;
|
|
165
|
+
return req;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function mockRes(): MockRes {
|
|
169
|
+
const state: MockRes = {
|
|
170
|
+
res: undefined as unknown as ServerResponse,
|
|
171
|
+
statusCode: 0,
|
|
172
|
+
body: '',
|
|
173
|
+
headers: {},
|
|
174
|
+
done: undefined as unknown as Promise<void>,
|
|
175
|
+
};
|
|
176
|
+
let resolveDone!: () => void;
|
|
177
|
+
state.done = new Promise<void>((r) => (resolveDone = r));
|
|
178
|
+
const res = {
|
|
179
|
+
headersSent: false,
|
|
180
|
+
writeHead(code: number, headers?: Record<string, unknown>) {
|
|
181
|
+
state.statusCode = code;
|
|
182
|
+
if (headers) state.headers = headers;
|
|
183
|
+
(res as { headersSent: boolean }).headersSent = true;
|
|
184
|
+
return res;
|
|
185
|
+
},
|
|
186
|
+
write(chunk: string) {
|
|
187
|
+
state.body += chunk;
|
|
188
|
+
return true;
|
|
189
|
+
},
|
|
190
|
+
end(chunk?: string) {
|
|
191
|
+
if (chunk) state.body += chunk;
|
|
192
|
+
resolveDone();
|
|
193
|
+
return res;
|
|
194
|
+
},
|
|
195
|
+
} as unknown as ServerResponse;
|
|
196
|
+
state.res = res;
|
|
197
|
+
return state;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
describe('createStudioRoutes', () => {
|
|
201
|
+
it('GET /__pyric/projects lists, POST creates', async () => {
|
|
202
|
+
const routes = createStudioRoutes({ projects: diskProjectStore(dir) });
|
|
203
|
+
|
|
204
|
+
const listRes = mockRes();
|
|
205
|
+
const handled = await routes(
|
|
206
|
+
mockReq('GET'),
|
|
207
|
+
listRes.res,
|
|
208
|
+
new URL('http://x/__pyric/projects'),
|
|
209
|
+
);
|
|
210
|
+
await listRes.done;
|
|
211
|
+
expect(handled).toBe(true);
|
|
212
|
+
expect(listRes.statusCode).toBe(200);
|
|
213
|
+
expect(JSON.parse(listRes.body)).toEqual([]);
|
|
214
|
+
|
|
215
|
+
const postReq = mockReq('POST');
|
|
216
|
+
const postRes = mockRes();
|
|
217
|
+
const p = routes(postReq, postRes.res, new URL('http://x/__pyric/projects'));
|
|
218
|
+
postReq.emit('data', JSON.stringify({ title: 'Via Route' }));
|
|
219
|
+
postReq.emit('end');
|
|
220
|
+
await p;
|
|
221
|
+
await postRes.done;
|
|
222
|
+
expect(postRes.statusCode).toBe(200);
|
|
223
|
+
expect(JSON.parse(postRes.body).title).toBe('Via Route');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('PUT then GET /__pyric/workspace round-trips a file', async () => {
|
|
227
|
+
const routes = createStudioRoutes({ workspace: diskWorkspace(dir) });
|
|
228
|
+
|
|
229
|
+
const putReq = mockReq('PUT');
|
|
230
|
+
const putRes = mockRes();
|
|
231
|
+
const p = routes(
|
|
232
|
+
putReq,
|
|
233
|
+
putRes.res,
|
|
234
|
+
new URL('http://x/__pyric/workspace?path=note.txt'),
|
|
235
|
+
);
|
|
236
|
+
putReq.emit('data', 'hello');
|
|
237
|
+
putReq.emit('end');
|
|
238
|
+
await p;
|
|
239
|
+
await putRes.done;
|
|
240
|
+
expect(putRes.statusCode).toBe(204);
|
|
241
|
+
expect(await readFile(join(dir, 'note.txt'), 'utf8')).toBe('hello');
|
|
242
|
+
|
|
243
|
+
const getRes = mockRes();
|
|
244
|
+
await routes(
|
|
245
|
+
mockReq('GET'),
|
|
246
|
+
getRes.res,
|
|
247
|
+
new URL('http://x/__pyric/workspace?path=note.txt'),
|
|
248
|
+
);
|
|
249
|
+
await getRes.done;
|
|
250
|
+
expect(getRes.statusCode).toBe(200);
|
|
251
|
+
expect(getRes.body).toBe('hello');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('GET /__pyric/workspace/list returns entries', async () => {
|
|
255
|
+
const ws = diskWorkspace(dir);
|
|
256
|
+
await ws.write('a.txt', '1');
|
|
257
|
+
const routes = createStudioRoutes({ workspace: ws });
|
|
258
|
+
const res = mockRes();
|
|
259
|
+
await routes(
|
|
260
|
+
mockReq('GET'),
|
|
261
|
+
res.res,
|
|
262
|
+
new URL('http://x/__pyric/workspace/list'),
|
|
263
|
+
);
|
|
264
|
+
await res.done;
|
|
265
|
+
expect(JSON.parse(res.body)).toEqual([{ path: 'a.txt', kind: 'file' }]);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('returns 400 on path traversal', async () => {
|
|
269
|
+
const routes = createStudioRoutes({ workspace: diskWorkspace(dir) });
|
|
270
|
+
const res = mockRes();
|
|
271
|
+
await routes(
|
|
272
|
+
mockReq('GET'),
|
|
273
|
+
res.res,
|
|
274
|
+
new URL('http://x/__pyric/workspace?path=../escape'),
|
|
275
|
+
);
|
|
276
|
+
await res.done;
|
|
277
|
+
expect(res.statusCode).toBe(400);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('returns false for unrelated /__pyric/* paths', async () => {
|
|
281
|
+
const routes = createStudioRoutes({ workspace: diskWorkspace(dir) });
|
|
282
|
+
const handled = await routes(
|
|
283
|
+
mockReq('GET'),
|
|
284
|
+
mockRes().res,
|
|
285
|
+
new URL('http://x/__pyric/init.json'),
|
|
286
|
+
);
|
|
287
|
+
expect(handled).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { AIOptions } from 'pyric/ai';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadEnv } from 'vite';
|
|
4
|
+
import type { AiEngineConfigWire } from './worker/protocol.js';
|
|
5
|
+
|
|
6
|
+
const AI_PROXY_PATH = '/__pyric/ai-proxy';
|
|
7
|
+
|
|
8
|
+
/** Declarative AI engines that can cross Vite's page/SharedWorker boundary. */
|
|
9
|
+
export type PyricAiEngineConfig = Extract<NonNullable<AIOptions['engine']>, { kind: string }>;
|
|
10
|
+
|
|
11
|
+
export interface PyricAiOptions {
|
|
12
|
+
/** Simple OpenAI-compatible model selection through Pyric's same-origin proxy. */
|
|
13
|
+
model?: string;
|
|
14
|
+
/** Advanced declarative engine configuration. */
|
|
15
|
+
engine?: PyricAiEngineConfig;
|
|
16
|
+
/** OpenAI-compatible upstream used by the same-origin proxy. */
|
|
17
|
+
proxyUpstream?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ResolvedViteAiConfig {
|
|
21
|
+
engineWire: AiEngineConfigWire | undefined;
|
|
22
|
+
proxyUpstream: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Load env using Vite's `envDir`-relative-to-root convention. */
|
|
26
|
+
export function loadViteAiEnv(
|
|
27
|
+
mode: string,
|
|
28
|
+
root: string | undefined,
|
|
29
|
+
envDir: string | undefined,
|
|
30
|
+
): Record<string, string> {
|
|
31
|
+
const resolvedRoot = path.resolve(root ?? process.cwd());
|
|
32
|
+
const resolvedEnvDir = envDir === undefined
|
|
33
|
+
? resolvedRoot
|
|
34
|
+
: path.resolve(resolvedRoot, envDir);
|
|
35
|
+
return loadEnv(mode, resolvedEnvDir, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Convert a public declarative engine into the JSON-safe worker wire shape. */
|
|
39
|
+
export function engineConfigToWire(engine: PyricAiEngineConfig): AiEngineConfigWire {
|
|
40
|
+
if (engine.kind === 'openai') {
|
|
41
|
+
return {
|
|
42
|
+
kind: 'openai',
|
|
43
|
+
baseUrl: engine.baseUrl ?? AI_PROXY_PATH,
|
|
44
|
+
...(engine.model !== undefined ? { model: engine.model } : {}),
|
|
45
|
+
...(engine.modelMap !== undefined ? { modelMap: engine.modelMap } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
kind: 'scripted',
|
|
50
|
+
...(engine.script !== undefined
|
|
51
|
+
? { script: engine.script as unknown as Array<Record<string, unknown>> }
|
|
52
|
+
: {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Resolve the plugin's explicit-options-over-Vite-env AI convention. */
|
|
57
|
+
export function resolveViteAiConfig(
|
|
58
|
+
options: PyricAiOptions | undefined,
|
|
59
|
+
env: Record<string, string | undefined>,
|
|
60
|
+
): ResolvedViteAiConfig {
|
|
61
|
+
if (options?.model !== undefined && options.engine !== undefined) {
|
|
62
|
+
throw new Error('@pyric/cli/vite: Choose either ai.model or ai.engine, not both.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const model = options?.model?.trim() || env.PYRIC_AI_MODEL?.trim();
|
|
66
|
+
const engineWire = options?.engine
|
|
67
|
+
? engineConfigToWire(options.engine)
|
|
68
|
+
: model
|
|
69
|
+
? engineConfigToWire({ kind: 'openai', baseUrl: AI_PROXY_PATH, model })
|
|
70
|
+
: undefined;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
engineWire,
|
|
74
|
+
proxyUpstream:
|
|
75
|
+
options?.proxyUpstream?.trim() || env.PYRIC_AI_PROXY_UPSTREAM?.trim() || undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|