@pyric/cli 0.1.0-alpha.11 → 0.1.0-alpha.12
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/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/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 +1 -1
- 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 +1 -1
- package/dist/serve/docs-ui/docs/get-started/start-building/index.html +1 -1
- package/dist/serve/docs-ui/docs/index.json +200 -25
- package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
- 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 +131 -66
- package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +130 -65
- 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 +1 -1
- 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/llms.txt +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/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.js +2 -2
- package/dist/serve/worker/protocol.js.map +1 -1
- package/package.json +5 -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 +493 -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 +46 -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 +540 -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 +75 -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/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-plugin.ts +1102 -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 +202 -0
- package/src/serve/worker/client/core.ts +350 -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/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 +179 -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 +1112 -0
- package/src/serve/worker/serve-init.ts +606 -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 +17 -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,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk state store for `pyric dev --persist` — the substrate side of
|
|
3
|
+
* flow doc section 3c ("persistence = an autosaved seed").
|
|
4
|
+
*
|
|
5
|
+
* The file is an ENVELOPE around two sections:
|
|
6
|
+
* - `firestore`: the `pyric/sandbox` persistence controller's own blob
|
|
7
|
+
* (`{version, savedAt, firestore: {path: fields}}`), stored verbatim as
|
|
8
|
+
* parsed JSON. The page's controller wrote it and will read it back
|
|
9
|
+
* through its own (de)serializer — wrapper types (Timestamp, Bytes, …)
|
|
10
|
+
* live as marker shapes and re-hydrate to class instances on restore.
|
|
11
|
+
* This store never interprets it.
|
|
12
|
+
* - `auth`: `{users: SeedUser[]}` — what `sandbox.exportUsers` emits and
|
|
13
|
+
* `sandbox.seedUsers` accepts.
|
|
14
|
+
*
|
|
15
|
+
* Everything is plain, diffable JSON on purpose: the persisted state
|
|
16
|
+
* doubles as a context artifact agents can read and promote to a fixture
|
|
17
|
+
* (`pyric snapshot`, plan P3).
|
|
18
|
+
*
|
|
19
|
+
* Durability stance (judgment zone 6, recorded): writes are atomic
|
|
20
|
+
* (tmp + rename, same volume) so a crash never truncates the file. A
|
|
21
|
+
* CORRUPT or version-mismatched file makes `load()` throw rather than
|
|
22
|
+
* silently discarding user data — serve fails fast and tells the user to
|
|
23
|
+
* inspect or delete it. Last-writer-wins between section writes is
|
|
24
|
+
* accepted (single serve process, sync fs — effectively serialized).
|
|
25
|
+
*/
|
|
26
|
+
import { copyFileSync, mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from 'node:fs';
|
|
27
|
+
import { dirname, join } from 'node:path';
|
|
28
|
+
import type { sandbox as authSandbox } from 'pyric/auth';
|
|
29
|
+
|
|
30
|
+
export type ExportedUsers = ReturnType<typeof authSandbox.exportUsers>;
|
|
31
|
+
|
|
32
|
+
export const STATE_FILE_VERSION = 1 as const;
|
|
33
|
+
|
|
34
|
+
/** Expected inner version of the sandbox persistence controller's blob.
|
|
35
|
+
* MIRRORS pyric's `serialize.ts` `SCHEMA_VERSION` — bump in lockstep when
|
|
36
|
+
* pyric does (they ship together). Drift-risk noted: not importable
|
|
37
|
+
* (pyric doesn't export it), so a check, not a re-use. */
|
|
38
|
+
export const EXPECTED_CONTROLLER_BLOB_VERSION = 1;
|
|
39
|
+
|
|
40
|
+
export interface PyricStateFile {
|
|
41
|
+
version: typeof STATE_FILE_VERSION;
|
|
42
|
+
/** The sandbox persistence controller's blob, verbatim. Null = never flushed. */
|
|
43
|
+
firestore: unknown | null;
|
|
44
|
+
auth: { users: ExportedUsers } | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type StateSection = 'firestore' | 'auth';
|
|
48
|
+
|
|
49
|
+
export class StateFileError extends Error {
|
|
50
|
+
constructor(message: string) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = 'StateFileError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface StateStore {
|
|
57
|
+
/** The project directory this store persists for (for the
|
|
58
|
+
* promote-the-wrong-project guard). */
|
|
59
|
+
projectDir: string;
|
|
60
|
+
/** Absolute path of the state file. */
|
|
61
|
+
path: string;
|
|
62
|
+
/** Absolute path of the one-deep recovery backup (`state.json.bak`). */
|
|
63
|
+
backupPath: string;
|
|
64
|
+
/** Parsed envelope, or null when the file doesn't exist. Throws
|
|
65
|
+
* {@link StateFileError} on corrupt JSON or version mismatch —
|
|
66
|
+
* fail fast over silent data loss. */
|
|
67
|
+
load(): PyricStateFile | null;
|
|
68
|
+
readSection(section: StateSection): unknown | null;
|
|
69
|
+
/** Read-modify-write the envelope; atomic tmp+rename. A firestore write
|
|
70
|
+
* that would collapse a non-empty doc set to empty first preserves the
|
|
71
|
+
* prior file as `.bak` (pre-mortem #2). */
|
|
72
|
+
writeSection(section: StateSection, value: unknown): void;
|
|
73
|
+
exists(): boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const STATE_RELATIVE_PATH = join('.pyric', 'state', 'state.json');
|
|
77
|
+
|
|
78
|
+
export function createStateStore(projectDir: string): StateStore {
|
|
79
|
+
const path = join(projectDir, STATE_RELATIVE_PATH);
|
|
80
|
+
const backupPath = `${path}.bak`;
|
|
81
|
+
|
|
82
|
+
const load = (): PyricStateFile | null => {
|
|
83
|
+
if (!existsSync(path)) return null;
|
|
84
|
+
let parsed: unknown;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
87
|
+
} catch (e) {
|
|
88
|
+
throw new StateFileError(
|
|
89
|
+
`state file at ${path} is not valid JSON (${e instanceof Error ? e.message : String(e)}). ` +
|
|
90
|
+
'Inspect or delete it to continue — pyric will not overwrite it silently.',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
94
|
+
throw new StateFileError(`state file at ${path} is not an object.`);
|
|
95
|
+
}
|
|
96
|
+
const file = parsed as PyricStateFile;
|
|
97
|
+
if (file.version !== STATE_FILE_VERSION) {
|
|
98
|
+
throw new StateFileError(
|
|
99
|
+
`state file at ${path} has version ${String(file.version)}; this @pyric/cli expects ` +
|
|
100
|
+
`${STATE_FILE_VERSION}. Delete it (or promote it with a matching @pyric/cli) to continue.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
// Inner controller-blob version (pre-mortem #6): the page's
|
|
104
|
+
// `deserializeSnapshot` validates this at restore — so a pyric upgrade
|
|
105
|
+
// that bumps its SCHEMA_VERSION would otherwise fail IN THE PAGE while
|
|
106
|
+
// the banner cheerfully reports "N docs restored". Peek it server-side
|
|
107
|
+
// and fail fast here instead. (Read-only peek; the section is still
|
|
108
|
+
// never re-encoded.) EXPECTED_CONTROLLER_BLOB_VERSION mirrors pyric's
|
|
109
|
+
// serialize `SCHEMA_VERSION` and must bump in lockstep — they ship
|
|
110
|
+
// together in this monorepo.
|
|
111
|
+
const inner = (file.firestore as { version?: unknown } | null)?.version;
|
|
112
|
+
if (inner !== undefined && inner !== EXPECTED_CONTROLLER_BLOB_VERSION) {
|
|
113
|
+
throw new StateFileError(
|
|
114
|
+
`state file at ${path} holds a firestore blob of version ${String(inner)}; this ` +
|
|
115
|
+
`pyric expects ${EXPECTED_CONTROLLER_BLOB_VERSION} (pyric was likely upgraded). ` +
|
|
116
|
+
'Delete the state file or re-promote it with a matching pyric to continue.',
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return file;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const writeAtomic = (file: PyricStateFile): void => {
|
|
123
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
124
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
125
|
+
writeFileSync(tmp, JSON.stringify(file, null, 2) + '\n', 'utf8');
|
|
126
|
+
renameSync(tmp, path); // same volume — atomic replace
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
projectDir,
|
|
131
|
+
path,
|
|
132
|
+
backupPath,
|
|
133
|
+
load,
|
|
134
|
+
exists: () => existsSync(path),
|
|
135
|
+
readSection(section) {
|
|
136
|
+
return load()?.[section] ?? null;
|
|
137
|
+
},
|
|
138
|
+
writeSection(section, value) {
|
|
139
|
+
const current = load() ?? { version: STATE_FILE_VERSION, firestore: null, auth: null };
|
|
140
|
+
// Data-loss guard (flow doc section 3c / pre-mortem #2): a `sandbox.reset()`
|
|
141
|
+
// makes the persistence controller flush EMPTY firestore over the
|
|
142
|
+
// live file (pyric flushes on session_boundary by design), and the
|
|
143
|
+
// backend write can't tell that from a legitimate delete-all. Before
|
|
144
|
+
// a firestore write would collapse a NON-empty doc set to empty, keep
|
|
145
|
+
// the prior file as `.bak` (one-deep) so it's recoverable. Intent-
|
|
146
|
+
// agnostic on purpose — covers reset, agent error, and accidental
|
|
147
|
+
// delete-all uniformly.
|
|
148
|
+
if (
|
|
149
|
+
section === 'firestore' &&
|
|
150
|
+
firestoreDocCount(value) === 0 &&
|
|
151
|
+
firestoreDocCount(current.firestore) > 0 &&
|
|
152
|
+
existsSync(path)
|
|
153
|
+
) {
|
|
154
|
+
copyFileSync(path, backupPath);
|
|
155
|
+
}
|
|
156
|
+
writeAtomic({ ...current, [section]: value } as PyricStateFile);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Doc count inside a controller firestore blob (`{…, firestore: {path:
|
|
162
|
+
* fields}}`), 0 for null/empty/malformed. */
|
|
163
|
+
export function firestoreDocCount(section: unknown): number {
|
|
164
|
+
const docs = (section as { firestore?: Record<string, unknown> } | null)?.firestore;
|
|
165
|
+
return docs && typeof docs === 'object' ? Object.keys(docs).length : 0;
|
|
166
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `diskProjectStore(root)` — a {@link ProjectStore} where each project is a
|
|
3
|
+
* subdirectory of `root` carrying a small metadata file (`.pyric-project.json`,
|
|
4
|
+
* shape {@link ProjectMeta}). `open(id)` hands back a {@link ProjectHandle}
|
|
5
|
+
* whose `workspace` is a {@link diskWorkspace} rooted at that subdirectory.
|
|
6
|
+
*
|
|
7
|
+
* Project ids ARE the directory names; they're slugged from the title (or
|
|
8
|
+
* generated) and validated to a safe charset so an id can never traverse out of
|
|
9
|
+
* `root`. The metadata file lives alongside the project's working tree — it's
|
|
10
|
+
* skipped by `diskWorkspace.list` callers only by convention (the file tree
|
|
11
|
+
* shows it; that's acceptable for a `.pyric-*` dotfile).
|
|
12
|
+
*/
|
|
13
|
+
import { mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { diskWorkspace } from './disk-workspace.js';
|
|
17
|
+
import type {
|
|
18
|
+
ProjectHandle,
|
|
19
|
+
ProjectMeta,
|
|
20
|
+
ProjectStore,
|
|
21
|
+
} from './store-types.js';
|
|
22
|
+
|
|
23
|
+
const META_FILE = '.pyric-project.json';
|
|
24
|
+
|
|
25
|
+
/** Thrown when an id isn't a safe single path segment. */
|
|
26
|
+
export class ProjectIdError extends Error {
|
|
27
|
+
constructor(id: string) {
|
|
28
|
+
super(`invalid project id: '${id}'`);
|
|
29
|
+
this.name = 'ProjectIdError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Slug a title into a filesystem-safe id segment. */
|
|
34
|
+
export function slugifyProjectId(title: string): string {
|
|
35
|
+
const slug = title
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.trim()
|
|
38
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
39
|
+
.replace(/^-+|-+$/g, '')
|
|
40
|
+
.slice(0, 48);
|
|
41
|
+
return slug;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A safe id is a single non-dot path segment of `[a-z0-9-_]`. */
|
|
45
|
+
function assertSafeId(id: string): void {
|
|
46
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(id) || id.includes('/') || id.includes('..')) {
|
|
47
|
+
throw new ProjectIdError(id);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function diskProjectStore(rootDir: string): ProjectStore {
|
|
52
|
+
const root = resolve(rootDir);
|
|
53
|
+
|
|
54
|
+
function projectDir(id: string): string {
|
|
55
|
+
assertSafeId(id);
|
|
56
|
+
return join(root, id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readMeta(id: string): Promise<ProjectMeta | null> {
|
|
60
|
+
try {
|
|
61
|
+
const raw = await readFile(join(projectDir(id), META_FILE), 'utf8');
|
|
62
|
+
const parsed = JSON.parse(raw) as Partial<ProjectMeta>;
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
return {
|
|
65
|
+
id,
|
|
66
|
+
title: typeof parsed.title === 'string' ? parsed.title : id,
|
|
67
|
+
createdAt: typeof parsed.createdAt === 'number' ? parsed.createdAt : now,
|
|
68
|
+
updatedAt: typeof parsed.updatedAt === 'number' ? parsed.updatedAt : now,
|
|
69
|
+
};
|
|
70
|
+
} catch (e) {
|
|
71
|
+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function writeMeta(meta: ProjectMeta): Promise<void> {
|
|
77
|
+
const dir = projectDir(meta.id);
|
|
78
|
+
await mkdir(dir, { recursive: true });
|
|
79
|
+
await writeFile(
|
|
80
|
+
join(dir, META_FILE),
|
|
81
|
+
JSON.stringify(meta, null, 2) + '\n',
|
|
82
|
+
'utf8',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
async list() {
|
|
88
|
+
let names: string[];
|
|
89
|
+
try {
|
|
90
|
+
names = await readdir(root);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
93
|
+
throw e;
|
|
94
|
+
}
|
|
95
|
+
const metas: ProjectMeta[] = [];
|
|
96
|
+
for (const name of names) {
|
|
97
|
+
if (name.startsWith('.')) continue;
|
|
98
|
+
let isDir = false;
|
|
99
|
+
try {
|
|
100
|
+
isDir = (await stat(join(root, name))).isDirectory();
|
|
101
|
+
} catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!isDir) continue;
|
|
105
|
+
try {
|
|
106
|
+
const meta = await readMeta(name);
|
|
107
|
+
// A directory without a meta file is still a project — synthesize.
|
|
108
|
+
metas.push(
|
|
109
|
+
meta ?? {
|
|
110
|
+
id: name,
|
|
111
|
+
title: name,
|
|
112
|
+
createdAt: 0,
|
|
113
|
+
updatedAt: 0,
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
if (e instanceof ProjectIdError) continue; // not a valid project dir
|
|
118
|
+
throw e;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
metas.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
122
|
+
return metas;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async open(id) {
|
|
126
|
+
const dir = projectDir(id);
|
|
127
|
+
let meta = await readMeta(id);
|
|
128
|
+
if (meta === null) {
|
|
129
|
+
// Allow opening a bare directory that exists but lacks metadata.
|
|
130
|
+
try {
|
|
131
|
+
if ((await stat(dir)).isDirectory()) {
|
|
132
|
+
meta = { id, title: id, createdAt: Date.now(), updatedAt: Date.now() };
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
/* fall through to throw */
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (meta === null) {
|
|
139
|
+
throw new Error(`project not found: '${id}'`);
|
|
140
|
+
}
|
|
141
|
+
const handle: ProjectHandle = {
|
|
142
|
+
meta,
|
|
143
|
+
workspace: diskWorkspace(dir),
|
|
144
|
+
};
|
|
145
|
+
return handle;
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
async create(input) {
|
|
149
|
+
const title = input.title?.trim() || 'untitled';
|
|
150
|
+
const base = slugifyProjectId(title) || 'project';
|
|
151
|
+
// Disambiguate against existing dirs.
|
|
152
|
+
let id = base;
|
|
153
|
+
let n = 1;
|
|
154
|
+
while (true) {
|
|
155
|
+
try {
|
|
156
|
+
await stat(join(root, id));
|
|
157
|
+
id = `${base}-${++n}`;
|
|
158
|
+
} catch {
|
|
159
|
+
break; // free
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const now = Date.now();
|
|
163
|
+
const meta: ProjectMeta = { id, title, createdAt: now, updatedAt: now };
|
|
164
|
+
await writeMeta(meta);
|
|
165
|
+
return meta;
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
async update(id, patch) {
|
|
169
|
+
const existing = await readMeta(id);
|
|
170
|
+
if (existing === null) throw new Error(`project not found: '${id}'`);
|
|
171
|
+
const next: ProjectMeta = {
|
|
172
|
+
...existing,
|
|
173
|
+
...patch,
|
|
174
|
+
id, // id is immutable
|
|
175
|
+
updatedAt: Date.now(),
|
|
176
|
+
};
|
|
177
|
+
await writeMeta(next);
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async remove(id) {
|
|
181
|
+
const dir = projectDir(id);
|
|
182
|
+
await rm(dir, { recursive: true, force: true });
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `diskWorkspace(dir)` — a {@link WorkspaceStore} over a real filesystem
|
|
3
|
+
* directory (the Studio `local` mode's project file tree, served at
|
|
4
|
+
* `/__pyric/workspace`).
|
|
5
|
+
*
|
|
6
|
+
* Paths are project-relative POSIX strings (`src/app.tsx`, `firestore.rules`).
|
|
7
|
+
* Every op resolves the path inside `dir` and REFUSES anything that escapes the
|
|
8
|
+
* root (`..` traversal, absolute paths) — the same posture the static server
|
|
9
|
+
* takes for `publicDir`. `watch(cb)` uses a recursive `fs.watch` and reports
|
|
10
|
+
* create/update/delete as {@link WorkspaceChange} events.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
mkdir,
|
|
14
|
+
readFile,
|
|
15
|
+
readdir,
|
|
16
|
+
rm,
|
|
17
|
+
stat,
|
|
18
|
+
writeFile,
|
|
19
|
+
} from 'node:fs/promises';
|
|
20
|
+
import { existsSync, mkdirSync, watch as fsWatch, type FSWatcher } from 'node:fs';
|
|
21
|
+
import { dirname, join, posix, relative, resolve, sep } from 'node:path';
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
WorkspaceChange,
|
|
25
|
+
WorkspaceEntry,
|
|
26
|
+
WorkspaceStore,
|
|
27
|
+
} from './store-types.js';
|
|
28
|
+
|
|
29
|
+
/** Thrown when a request path escapes the workspace root. */
|
|
30
|
+
export class WorkspacePathError extends Error {
|
|
31
|
+
constructor(path: string) {
|
|
32
|
+
super(`workspace path escapes root: '${path}'`);
|
|
33
|
+
this.name = 'WorkspacePathError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a project-relative POSIX path to an absolute OS path inside `root`,
|
|
39
|
+
* or throw {@link WorkspacePathError} if it would escape. Leading slashes are
|
|
40
|
+
* stripped (treated as project-root-relative); empty / `.` resolves to the
|
|
41
|
+
* root itself.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveWorkspacePath(root: string, rel: string): string {
|
|
44
|
+
const cleaned = rel.replace(/^\/+/, '');
|
|
45
|
+
// Normalize via POSIX semantics first so `a/../b` collapses predictably,
|
|
46
|
+
// then map separators to the host OS.
|
|
47
|
+
const normalized = posix.normalize(cleaned === '' ? '.' : cleaned);
|
|
48
|
+
if (normalized === '..' || normalized.startsWith('../')) {
|
|
49
|
+
throw new WorkspacePathError(rel);
|
|
50
|
+
}
|
|
51
|
+
const abs = resolve(root, normalized.split('/').join(sep));
|
|
52
|
+
if (abs !== root && !abs.startsWith(root + sep)) {
|
|
53
|
+
throw new WorkspacePathError(rel);
|
|
54
|
+
}
|
|
55
|
+
return abs;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Convert an absolute OS path under `root` to a project-relative POSIX path. */
|
|
59
|
+
function toPosixRel(root: string, abs: string): string {
|
|
60
|
+
return relative(root, abs).split(sep).join('/');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function diskWorkspace(dir: string): WorkspaceStore {
|
|
64
|
+
const root = resolve(dir);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
async read(path) {
|
|
68
|
+
const abs = resolveWorkspacePath(root, path);
|
|
69
|
+
try {
|
|
70
|
+
return await readFile(abs, 'utf8');
|
|
71
|
+
} catch (e) {
|
|
72
|
+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
73
|
+
// A directory read errors as EISDIR — treat as "no file content".
|
|
74
|
+
if ((e as NodeJS.ErrnoException).code === 'EISDIR') return null;
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async write(path, content) {
|
|
80
|
+
const abs = resolveWorkspacePath(root, path);
|
|
81
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
82
|
+
await writeFile(abs, content, 'utf8');
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
async list(dir) {
|
|
86
|
+
const abs = resolveWorkspacePath(root, dir ?? '');
|
|
87
|
+
let names: string[];
|
|
88
|
+
try {
|
|
89
|
+
names = await readdir(abs);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
92
|
+
throw e;
|
|
93
|
+
}
|
|
94
|
+
const entries: WorkspaceEntry[] = [];
|
|
95
|
+
for (const name of names) {
|
|
96
|
+
const childAbs = join(abs, name);
|
|
97
|
+
let isDir = false;
|
|
98
|
+
try {
|
|
99
|
+
isDir = (await stat(childAbs)).isDirectory();
|
|
100
|
+
} catch {
|
|
101
|
+
continue; // raced away between readdir and stat
|
|
102
|
+
}
|
|
103
|
+
entries.push({
|
|
104
|
+
path: toPosixRel(root, childAbs),
|
|
105
|
+
kind: isDir ? 'dir' : 'file',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
109
|
+
return entries;
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
async remove(path) {
|
|
113
|
+
const abs = resolveWorkspacePath(root, path);
|
|
114
|
+
if (abs === root) throw new WorkspacePathError(path); // never nuke the root
|
|
115
|
+
await rm(abs, { recursive: true, force: true });
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
watch(cb) {
|
|
119
|
+
if (!existsSync(root)) {
|
|
120
|
+
try {
|
|
121
|
+
// best-effort: ensure the dir exists so the watcher can attach.
|
|
122
|
+
mkdirSync(root, { recursive: true });
|
|
123
|
+
} catch {
|
|
124
|
+
/* watcher just won't fire */
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
let watcher: FSWatcher | null = null;
|
|
128
|
+
try {
|
|
129
|
+
watcher = fsWatch(root, { recursive: true }, (_event, filename) => {
|
|
130
|
+
if (filename == null) return;
|
|
131
|
+
const relPath = String(filename).split(sep).join('/');
|
|
132
|
+
// fs.watch doesn't tell us create vs update vs delete reliably across
|
|
133
|
+
// platforms; derive it from current existence.
|
|
134
|
+
let abs: string;
|
|
135
|
+
try {
|
|
136
|
+
abs = resolveWorkspacePath(root, relPath);
|
|
137
|
+
} catch {
|
|
138
|
+
return; // ignore anything outside the root
|
|
139
|
+
}
|
|
140
|
+
const type: WorkspaceChange['type'] = existsSync(abs)
|
|
141
|
+
? 'update'
|
|
142
|
+
: 'delete';
|
|
143
|
+
cb({ path: relPath, type });
|
|
144
|
+
});
|
|
145
|
+
} catch {
|
|
146
|
+
// Recursive watch unsupported here — degrade to no live updates.
|
|
147
|
+
return () => {};
|
|
148
|
+
}
|
|
149
|
+
// Watcher failure (EMFILE, root renamed) must not surface as an
|
|
150
|
+
// unhandled 'error' event — that would kill the serve process.
|
|
151
|
+
// Degrade to no live updates instead.
|
|
152
|
+
watcher.on('error', () => {
|
|
153
|
+
watcher?.close();
|
|
154
|
+
watcher = null;
|
|
155
|
+
});
|
|
156
|
+
return () => {
|
|
157
|
+
watcher?.close();
|
|
158
|
+
watcher = null;
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio storage (Track T3) — disk-backed `WorkspaceStore` / `ProjectStore`
|
|
3
|
+
* impls + the `/__pyric/workspace` and `/__pyric/projects` serve routes that
|
|
4
|
+
* `@pyric/studio`'s `local` mode talks to. See the design rationale.
|
|
5
|
+
*/
|
|
6
|
+
export { diskWorkspace, resolveWorkspacePath, WorkspacePathError } from './disk-workspace.js';
|
|
7
|
+
export {
|
|
8
|
+
diskProjectStore,
|
|
9
|
+
slugifyProjectId,
|
|
10
|
+
ProjectIdError,
|
|
11
|
+
} from './disk-project-store.js';
|
|
12
|
+
export { createStudioRoutes, type StudioRouteOptions } from './routes.js';
|
|
13
|
+
export type {
|
|
14
|
+
ProjectHandle,
|
|
15
|
+
ProjectMeta,
|
|
16
|
+
ProjectStore,
|
|
17
|
+
WorkspaceChange,
|
|
18
|
+
WorkspaceEntry,
|
|
19
|
+
WorkspaceStore,
|
|
20
|
+
} from './store-types.js';
|