@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,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pyric database rules *` subcommands — local tooling for Realtime Database
|
|
3
|
+
* rules JSON.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { dirname, resolve as resolvePath } from 'node:path';
|
|
8
|
+
import {
|
|
9
|
+
compileRtdbRules,
|
|
10
|
+
simulateRtdbRules,
|
|
11
|
+
type CompiledRtdbRules,
|
|
12
|
+
type RtdbNode,
|
|
13
|
+
type SimulationInput,
|
|
14
|
+
type SimulateResult,
|
|
15
|
+
} from 'pyric/rules/internal/rtdb';
|
|
16
|
+
import type { ParsedArgs } from './parse-args.js';
|
|
17
|
+
import { readFirebaseJson, type FirebaseJson } from './firebase-json.js';
|
|
18
|
+
import {
|
|
19
|
+
loadRtdbRulesDocument,
|
|
20
|
+
type LoadRtdbRulesDocumentResult,
|
|
21
|
+
} from '../rtdb/load-rules-document.js';
|
|
22
|
+
|
|
23
|
+
export interface DatabaseRulesDeps {
|
|
24
|
+
readFile?: typeof readFile;
|
|
25
|
+
writeFile?: typeof writeFile;
|
|
26
|
+
mkdir?: typeof mkdir;
|
|
27
|
+
readFirebaseJson?: (cwd: string) => Promise<FirebaseJson>;
|
|
28
|
+
simulate?: (compiled: CompiledRtdbRules, input: SimulationInput) => SimulateResult;
|
|
29
|
+
loadRulesDocument?: typeof loadRtdbRulesDocument;
|
|
30
|
+
cwd?: string;
|
|
31
|
+
readStdin?: () => Promise<string>;
|
|
32
|
+
stdout?: { write(s: string): void };
|
|
33
|
+
stderr?: { write(s: string): void };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface RuleFinding {
|
|
37
|
+
path: string;
|
|
38
|
+
rule: '.read' | '.write' | '.validate';
|
|
39
|
+
code: string;
|
|
40
|
+
message: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function defaultReadStdin(): Promise<string> {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
let data = '';
|
|
46
|
+
process.stdin.setEncoding('utf-8');
|
|
47
|
+
process.stdin.on('data', (chunk) => {
|
|
48
|
+
data += chunk;
|
|
49
|
+
});
|
|
50
|
+
process.stdin.on('end', () => resolve(data));
|
|
51
|
+
process.stdin.on('error', reject);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseRulesJson(raw: string): CompiledRtdbRules {
|
|
56
|
+
return compileRtdbRules(JSON.parse(raw));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function collectFindings(node: RtdbNode, kind: 'errors' | 'warnings'): RuleFinding[] {
|
|
60
|
+
const findings: RuleFinding[] = [];
|
|
61
|
+
const rules = [
|
|
62
|
+
['.read', node.read],
|
|
63
|
+
['.write', node.write],
|
|
64
|
+
['.validate', node.validate],
|
|
65
|
+
] as const;
|
|
66
|
+
for (const [rule, expr] of rules) {
|
|
67
|
+
for (const finding of expr?.parsed[kind] ?? []) {
|
|
68
|
+
findings.push({
|
|
69
|
+
path: node.path,
|
|
70
|
+
rule,
|
|
71
|
+
code: finding.code,
|
|
72
|
+
message: finding.message,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const child of node.children) {
|
|
77
|
+
findings.push(...collectFindings(child, kind));
|
|
78
|
+
}
|
|
79
|
+
return findings;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function readRulesFile(
|
|
83
|
+
path: string,
|
|
84
|
+
deps: DatabaseRulesDeps,
|
|
85
|
+
): Promise<{ ok: true; raw: string } | { ok: false; message: string }> {
|
|
86
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
87
|
+
const readFileFn = deps.readFile ?? readFile;
|
|
88
|
+
try {
|
|
89
|
+
return { ok: true, raw: await readFileFn(resolvePath(cwd, path), 'utf-8') };
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return { ok: false, message: e instanceof Error ? e.message : String(e) };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function jsonError(code: string, message: string): { errors: RuleFinding[]; warnings: RuleFinding[] } {
|
|
96
|
+
return {
|
|
97
|
+
errors: [{ path: '/', rule: '.read', code, message }],
|
|
98
|
+
warnings: [],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function runDatabaseRulesLint(
|
|
103
|
+
parsed: ParsedArgs,
|
|
104
|
+
deps: DatabaseRulesDeps = {},
|
|
105
|
+
): Promise<number> {
|
|
106
|
+
const out = deps.stdout ?? process.stdout;
|
|
107
|
+
const err = deps.stderr ?? process.stderr;
|
|
108
|
+
const path = parsed.positional[0];
|
|
109
|
+
if (!path) {
|
|
110
|
+
err.write('pyric database rules lint: missing rules-file path. Usage: pyric database rules lint <path>\n');
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const file = await readRulesFile(path, deps);
|
|
115
|
+
if (!file.ok) {
|
|
116
|
+
err.write(`pyric database rules lint: ${file.message}\n`);
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const compiled = parseRulesJson(file.raw);
|
|
122
|
+
out.write(`${JSON.stringify({ warnings: collectFindings(compiled, 'warnings') }, null, 2)}\n`);
|
|
123
|
+
return 0;
|
|
124
|
+
} catch (e) {
|
|
125
|
+
out.write(`${JSON.stringify(jsonError('INVALID_RULES_JSON', e instanceof Error ? e.message : String(e)), null, 2)}\n`);
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function runDatabaseRulesValidate(
|
|
131
|
+
parsed: ParsedArgs,
|
|
132
|
+
deps: DatabaseRulesDeps = {},
|
|
133
|
+
): Promise<number> {
|
|
134
|
+
const out = deps.stdout ?? process.stdout;
|
|
135
|
+
const err = deps.stderr ?? process.stderr;
|
|
136
|
+
const path = parsed.positional[0];
|
|
137
|
+
if (!path) {
|
|
138
|
+
err.write('pyric database rules validate: missing rules-file path. Usage: pyric database rules validate <path>\n');
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const file = await readRulesFile(path, deps);
|
|
143
|
+
if (!file.ok) {
|
|
144
|
+
err.write(`pyric database rules validate: ${file.message}\n`);
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const compiled = parseRulesJson(file.raw);
|
|
150
|
+
out.write(`${JSON.stringify({ errors: collectFindings(compiled, 'errors') }, null, 2)}\n`);
|
|
151
|
+
return 0;
|
|
152
|
+
} catch (e) {
|
|
153
|
+
out.write(`${JSON.stringify(jsonError('INVALID_RULES_JSON', e instanceof Error ? e.message : String(e)), null, 2)}\n`);
|
|
154
|
+
return 2;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface SimulatePayload {
|
|
159
|
+
rulesJson?: unknown;
|
|
160
|
+
rules?: unknown;
|
|
161
|
+
rulesPath?: string;
|
|
162
|
+
operation?: SimulationInput['operation'];
|
|
163
|
+
path?: string;
|
|
164
|
+
auth?: SimulationInput['auth'];
|
|
165
|
+
mockData?: Record<string, unknown>;
|
|
166
|
+
newData?: unknown;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function readFirebaseRules(
|
|
170
|
+
deps: DatabaseRulesDeps,
|
|
171
|
+
): Promise<{ ok: true; raw: string } | { ok: false; message: string }> {
|
|
172
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
173
|
+
const fjRead = deps.readFirebaseJson ?? readFirebaseJson;
|
|
174
|
+
let firebaseJson: FirebaseJson;
|
|
175
|
+
try {
|
|
176
|
+
firebaseJson = await fjRead(cwd);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
return { ok: false, message: e instanceof Error ? e.message : String(e) };
|
|
179
|
+
}
|
|
180
|
+
const rulesPath = firebaseJson.database?.rules;
|
|
181
|
+
if (!rulesPath) {
|
|
182
|
+
return { ok: false, message: 'firebase.json has no `database.rules` path.' };
|
|
183
|
+
}
|
|
184
|
+
return readRulesFile(rulesPath, deps);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function runDatabaseRulesSimulate(
|
|
188
|
+
parsed: ParsedArgs,
|
|
189
|
+
deps: DatabaseRulesDeps = {},
|
|
190
|
+
): Promise<number> {
|
|
191
|
+
const out = deps.stdout ?? process.stdout;
|
|
192
|
+
const err = deps.stderr ?? process.stderr;
|
|
193
|
+
const readFileFn = deps.readFile ?? readFile;
|
|
194
|
+
const simulateFn =
|
|
195
|
+
deps.simulate ??
|
|
196
|
+
simulateRtdbRules;
|
|
197
|
+
|
|
198
|
+
let rulesJson: unknown;
|
|
199
|
+
let input: SimulationInput;
|
|
200
|
+
|
|
201
|
+
if (parsed.flags.get('stdin') === true) {
|
|
202
|
+
const readStdinFn = deps.readStdin ?? defaultReadStdin;
|
|
203
|
+
let payload: SimulatePayload;
|
|
204
|
+
try {
|
|
205
|
+
payload = JSON.parse(await readStdinFn()) as SimulatePayload;
|
|
206
|
+
} catch (e) {
|
|
207
|
+
err.write(`pyric database rules simulate: failed to parse stdin JSON: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
if (!payload.operation || !payload.path) {
|
|
211
|
+
err.write('pyric database rules simulate: stdin payload must include `operation` and `path`.\n');
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
if (payload.rulesJson !== undefined || payload.rules !== undefined) {
|
|
215
|
+
rulesJson = payload.rulesJson ?? payload.rules;
|
|
216
|
+
} else if (payload.rulesPath) {
|
|
217
|
+
try {
|
|
218
|
+
rulesJson = JSON.parse(await readFileFn(resolvePath(deps.cwd ?? process.cwd(), payload.rulesPath), 'utf-8'));
|
|
219
|
+
} catch (e) {
|
|
220
|
+
err.write(`pyric database rules simulate: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
const file = await readFirebaseRules(deps);
|
|
225
|
+
if (!file.ok) {
|
|
226
|
+
err.write(`pyric database rules simulate: ${file.message}\n`);
|
|
227
|
+
return 1;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
rulesJson = JSON.parse(file.raw);
|
|
231
|
+
} catch (e) {
|
|
232
|
+
err.write(`pyric database rules simulate: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
233
|
+
return 2;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
input = {
|
|
237
|
+
operation: payload.operation,
|
|
238
|
+
path: payload.path,
|
|
239
|
+
auth: payload.auth ?? null,
|
|
240
|
+
mockData: payload.mockData ?? {},
|
|
241
|
+
...(payload.newData !== undefined ? { newData: payload.newData } : {}),
|
|
242
|
+
};
|
|
243
|
+
} else {
|
|
244
|
+
const file = await readFirebaseRules(deps);
|
|
245
|
+
if (!file.ok) {
|
|
246
|
+
err.write(`pyric database rules simulate: ${file.message}\n`);
|
|
247
|
+
return 1;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
rulesJson = JSON.parse(file.raw);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
err.write(`pyric database rules simulate: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
253
|
+
return 2;
|
|
254
|
+
}
|
|
255
|
+
input = {
|
|
256
|
+
operation: 'read',
|
|
257
|
+
path: '/sample/x',
|
|
258
|
+
auth: null,
|
|
259
|
+
mockData: {},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const compiled = compileRtdbRules(rulesJson);
|
|
265
|
+
const result = simulateFn(compiled, input);
|
|
266
|
+
out.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
267
|
+
return result.success ? 0 : 2;
|
|
268
|
+
} catch (e) {
|
|
269
|
+
err.write(`pyric database rules simulate: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
270
|
+
return 2;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* `pyric database rules generate [--config <path>] [--out <path>]`
|
|
276
|
+
*
|
|
277
|
+
* Loads a user's RTDB constraints module (a file that calls
|
|
278
|
+
* `defineRtdbRules(...)` from `pyric/rules`), compiles it via
|
|
279
|
+
* `RtdbRulesDocument#toJSON()` — the same primitive RTDB deploy uses —
|
|
280
|
+
* and writes the static `database.rules.json` shape to disk so it can
|
|
281
|
+
* be inspected, diffed, and committed before deploying.
|
|
282
|
+
*
|
|
283
|
+
* `--config` defaults to `database.rules.ts`. `--out` defaults to
|
|
284
|
+
* `firebase.json`'s `database.rules` path, falling back to
|
|
285
|
+
* `database.rules.json` when firebase.json has no such entry.
|
|
286
|
+
*/
|
|
287
|
+
export async function runDatabaseRulesGenerate(
|
|
288
|
+
parsed: ParsedArgs,
|
|
289
|
+
deps: DatabaseRulesDeps = {},
|
|
290
|
+
): Promise<number> {
|
|
291
|
+
const out = deps.stdout ?? process.stdout;
|
|
292
|
+
const err = deps.stderr ?? process.stderr;
|
|
293
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
294
|
+
const writeFileFn = deps.writeFile ?? writeFile;
|
|
295
|
+
const mkdirFn = deps.mkdir ?? mkdir;
|
|
296
|
+
const loadRulesDocument = deps.loadRulesDocument ?? loadRtdbRulesDocument;
|
|
297
|
+
|
|
298
|
+
const configFlag = parsed.flags.get('config');
|
|
299
|
+
const configPath = typeof configFlag === 'string' ? configFlag : 'database.rules.ts';
|
|
300
|
+
|
|
301
|
+
const loaded: LoadRtdbRulesDocumentResult = await loadRulesDocument(configPath, { cwd });
|
|
302
|
+
if (!loaded.ok) {
|
|
303
|
+
err.write(`pyric database rules generate: ${loaded.message}\n`);
|
|
304
|
+
return 1;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const outFlag = parsed.flags.get('out');
|
|
308
|
+
let outPath: string;
|
|
309
|
+
if (typeof outFlag === 'string') {
|
|
310
|
+
outPath = outFlag;
|
|
311
|
+
} else {
|
|
312
|
+
const fjRead = deps.readFirebaseJson ?? readFirebaseJson;
|
|
313
|
+
let firebaseJson: FirebaseJson | null = null;
|
|
314
|
+
try {
|
|
315
|
+
firebaseJson = await fjRead(cwd);
|
|
316
|
+
} catch {
|
|
317
|
+
firebaseJson = null;
|
|
318
|
+
}
|
|
319
|
+
outPath = firebaseJson?.database?.rules ?? 'database.rules.json';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const resolvedOut = resolvePath(cwd, outPath);
|
|
323
|
+
const rulesJson = loaded.document.toJSON();
|
|
324
|
+
await mkdirFn(dirname(resolvedOut), { recursive: true });
|
|
325
|
+
await writeFileFn(resolvedOut, `${JSON.stringify(rulesJson, null, 2)}\n`, 'utf-8');
|
|
326
|
+
|
|
327
|
+
out.write(`pyric database rules generate: wrote ${resolvedOut}\n`);
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `pyric dev` child runner — "one command, not two" (adoption design):
|
|
3
|
+
* after the host is up, run the user's OWN dev command with the environment
|
|
4
|
+
* activated so their unchanged firebase-admin/firebase imports resolve to the
|
|
5
|
+
* pyric sandbox:
|
|
6
|
+
*
|
|
7
|
+
* PYRIC_SANDBOX=remote:<serve url> (the activator)
|
|
8
|
+
* NODE_OPTIONS += --import @pyric/cli/register (the substitution seam)
|
|
9
|
+
*
|
|
10
|
+
* Child-command precedence: explicit `pyric dev -- <cmd>` wins; else the
|
|
11
|
+
* project package.json `dev` script (via the detected package manager); else
|
|
12
|
+
* host-only (today's behavior). `--no-run` forces host-only; `--json`
|
|
13
|
+
* defaults to host-only (an explicit `--` still wins — agents can opt in).
|
|
14
|
+
*
|
|
15
|
+
* Everything decision-shaped here is pure and exported for the unit suite;
|
|
16
|
+
* only `spawnDevChild` touches the process table.
|
|
17
|
+
*/
|
|
18
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
|
|
22
|
+
// ─── First-run race guard ───────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Wait (bounded) for a browser tab to register as the bridge's sandbox peer
|
|
26
|
+
* before the child spawns. `pyric dev` opens the tab and spawns the child in
|
|
27
|
+
* the same breath; a child whose first line is a sandbox op otherwise races
|
|
28
|
+
* the tab's boot and dies on the no-tab fail-fast — the exact first-run of
|
|
29
|
+
* the two-command story. Polls `/__pyric/health` (`sandboxConnected`).
|
|
30
|
+
*
|
|
31
|
+
* Returns true when a peer connected, false on timeout (caller warns and
|
|
32
|
+
* spawns anyway — the child may not touch the sandbox at all). Injectable
|
|
33
|
+
* clock/fetch for the unit suite.
|
|
34
|
+
*/
|
|
35
|
+
export async function waitForSandboxPeer(
|
|
36
|
+
serveUrl: string,
|
|
37
|
+
opts: {
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
intervalMs?: number;
|
|
40
|
+
fetchImpl?: typeof fetch;
|
|
41
|
+
sleep?: (ms: number) => Promise<void>;
|
|
42
|
+
} = {},
|
|
43
|
+
): Promise<boolean> {
|
|
44
|
+
const timeoutMs = opts.timeoutMs ?? 30_000;
|
|
45
|
+
const intervalMs = opts.intervalMs ?? 250;
|
|
46
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
47
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
48
|
+
const deadline = Date.now() + timeoutMs;
|
|
49
|
+
for (;;) {
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetchImpl(`${serveUrl.replace(/\/$/, '')}/__pyric/health`);
|
|
52
|
+
if (res.ok) {
|
|
53
|
+
const health = (await res.json()) as { sandboxConnected?: boolean };
|
|
54
|
+
if (health.sandboxConnected === true) return true;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Health not up yet — keep polling until the deadline.
|
|
58
|
+
}
|
|
59
|
+
if (Date.now() >= deadline) return false;
|
|
60
|
+
await sleep(intervalMs);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Child-command resolution (pure) ───────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export type PackageManager = 'bun' | 'pnpm' | 'yarn' | 'npm';
|
|
67
|
+
|
|
68
|
+
/** Lockfile sniff, mirroring how init/vendor pick their install hints. */
|
|
69
|
+
export function detectPackageManager(cwd: string): PackageManager {
|
|
70
|
+
if (existsSync(join(cwd, 'bun.lock')) || existsSync(join(cwd, 'bun.lockb'))) return 'bun';
|
|
71
|
+
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
72
|
+
if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn';
|
|
73
|
+
return 'npm';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The project's `dev` script, or null (no package.json / no script). */
|
|
77
|
+
export function readDevScript(cwd: string): string | null {
|
|
78
|
+
try {
|
|
79
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')) as {
|
|
80
|
+
scripts?: Record<string, unknown>;
|
|
81
|
+
};
|
|
82
|
+
const dev = pkg.scripts?.dev;
|
|
83
|
+
return typeof dev === 'string' && dev.trim().length > 0 ? dev : null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface DevChildPlan {
|
|
90
|
+
/** argv to spawn — argv[0] is the executable, no shell. */
|
|
91
|
+
argv: string[];
|
|
92
|
+
/** Human-readable command for the pre-spawn line. */
|
|
93
|
+
label: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Decide what (if anything) to run after the host is up.
|
|
98
|
+
*
|
|
99
|
+
* Matrix:
|
|
100
|
+
* --no-run → null, always
|
|
101
|
+
* `-- <cmd>` → that command (wins over everything else)
|
|
102
|
+
* --json (no `--`) → null (machine mode defaults to host-only)
|
|
103
|
+
* package.json dev script → `<pm> run dev`
|
|
104
|
+
* none of the above → null (host-only, today's behavior)
|
|
105
|
+
*
|
|
106
|
+
* A `dev` script that itself invokes `pyric dev` is treated as absent —
|
|
107
|
+
* running it would recurse forever (pyric dev → npm run dev → pyric dev …).
|
|
108
|
+
*/
|
|
109
|
+
export function resolveDevChild(opts: {
|
|
110
|
+
passthrough: string[];
|
|
111
|
+
noRun: boolean;
|
|
112
|
+
json: boolean;
|
|
113
|
+
devScript: string | null;
|
|
114
|
+
packageManager: PackageManager;
|
|
115
|
+
}): DevChildPlan | null {
|
|
116
|
+
if (opts.noRun) return null;
|
|
117
|
+
if (opts.passthrough.length > 0) {
|
|
118
|
+
return { argv: [...opts.passthrough], label: opts.passthrough.join(' ') };
|
|
119
|
+
}
|
|
120
|
+
if (opts.json) return null;
|
|
121
|
+
if (opts.devScript && !/(^|[\s;&|])pyric\s+dev\b/.test(opts.devScript)) {
|
|
122
|
+
const label = `${opts.packageManager} run dev`;
|
|
123
|
+
return { argv: [opts.packageManager, 'run', 'dev'], label };
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Environment for the child (pure) ──────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The absolute `file:` URL of the register module, resolved from
|
|
132
|
+
* @pyric/cli' OWN installed location — `import.meta.resolve` walks this
|
|
133
|
+
* package's `exports` (self-reference), so it lands on the right file no
|
|
134
|
+
* matter how the user's node_modules are laid out (hoisted, pnpm-isolated,
|
|
135
|
+
* vendored). Fallback: relative to this compiled file (dist/cli/ →
|
|
136
|
+
* dist/register/), for runtimes without a usable import.meta.resolve.
|
|
137
|
+
*/
|
|
138
|
+
export function registerModuleUrl(): string {
|
|
139
|
+
try {
|
|
140
|
+
const url = import.meta.resolve('@pyric/cli/register');
|
|
141
|
+
if (typeof url === 'string' && url.length > 0) return url;
|
|
142
|
+
} catch {
|
|
143
|
+
// fall through
|
|
144
|
+
}
|
|
145
|
+
return new URL('../register/index.js', import.meta.url).href;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Child env: sets the activator and APPENDS the loader to NODE_OPTIONS —
|
|
150
|
+
* never replaces it (the user's own --inspect / --max-old-space-size etc.
|
|
151
|
+
* must survive). `file:` URLs are percent-encoded, so no quoting is needed
|
|
152
|
+
* even for paths with spaces.
|
|
153
|
+
*/
|
|
154
|
+
export function buildChildEnv(
|
|
155
|
+
base: NodeJS.ProcessEnv,
|
|
156
|
+
opts: { serveUrl: string; registerUrl: string },
|
|
157
|
+
): NodeJS.ProcessEnv {
|
|
158
|
+
const importFlag = `--import ${opts.registerUrl}`;
|
|
159
|
+
return {
|
|
160
|
+
...base,
|
|
161
|
+
PYRIC_SANDBOX: `remote:${opts.serveUrl}`,
|
|
162
|
+
NODE_OPTIONS: base.NODE_OPTIONS ? `${base.NODE_OPTIONS} ${importFlag}` : importFlag,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Line prefixing (pure core) ────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Incremental line prefixer: every child output line gets `[dev] `, partial
|
|
170
|
+
* lines are buffered until their newline (or flush at stream end).
|
|
171
|
+
*/
|
|
172
|
+
export function createLinePrefixer(
|
|
173
|
+
prefix: string,
|
|
174
|
+
write: (line: string) => void,
|
|
175
|
+
): { push(chunk: string): void; flush(): void } {
|
|
176
|
+
let buffer = '';
|
|
177
|
+
return {
|
|
178
|
+
push(chunk: string): void {
|
|
179
|
+
buffer += chunk;
|
|
180
|
+
let nl: number;
|
|
181
|
+
while ((nl = buffer.indexOf('\n')) !== -1) {
|
|
182
|
+
write(`${prefix}${buffer.slice(0, nl)}\n`);
|
|
183
|
+
buffer = buffer.slice(nl + 1);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
flush(): void {
|
|
187
|
+
if (buffer.length > 0) {
|
|
188
|
+
write(`${prefix}${buffer}\n`);
|
|
189
|
+
buffer = '';
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Spawn + lifecycle ─────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
export interface DevChildHandle {
|
|
198
|
+
/** Resolves with the exit code to propagate once the child is gone. */
|
|
199
|
+
exited: Promise<number>;
|
|
200
|
+
/** Forward a signal (Ctrl-C / SIGTERM). Marks the exit as user-initiated
|
|
201
|
+
* so the propagated code is 0, and SIGKILLs a child that lingers. */
|
|
202
|
+
signal(sig: NodeJS.Signals): void;
|
|
203
|
+
child: ChildProcess;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** How long a signalled child may linger before SIGKILL. */
|
|
207
|
+
const FORCE_KILL_AFTER_MS = 2_000;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Spawn the child with inherited stdin and `[dev]`-prefixed stdout/stderr.
|
|
211
|
+
* In `--json` mode ALL child output goes to stderr — stdout carries exactly
|
|
212
|
+
* the one machine line (the serve --json contract).
|
|
213
|
+
*/
|
|
214
|
+
export function spawnDevChild(
|
|
215
|
+
plan: DevChildPlan,
|
|
216
|
+
opts: { cwd: string; env: NodeJS.ProcessEnv; json: boolean },
|
|
217
|
+
): DevChildHandle {
|
|
218
|
+
const [command, ...args] = plan.argv;
|
|
219
|
+
const child = spawn(command!, args, {
|
|
220
|
+
cwd: opts.cwd,
|
|
221
|
+
env: opts.env,
|
|
222
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
223
|
+
// Own a process group so shutdown reaches package-manager grandchildren
|
|
224
|
+
// (for example `bun run dev` -> `node server.js`) rather than orphaning
|
|
225
|
+
// the actual dev server when only the runner exits.
|
|
226
|
+
detached: process.platform !== 'win32',
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const outTarget = opts.json ? process.stderr : process.stdout;
|
|
230
|
+
const out = createLinePrefixer('[dev] ', (line) => outTarget.write(line));
|
|
231
|
+
const err = createLinePrefixer('[dev] ', (line) => process.stderr.write(line));
|
|
232
|
+
child.stdout?.setEncoding('utf8');
|
|
233
|
+
child.stderr?.setEncoding('utf8');
|
|
234
|
+
child.stdout?.on('data', (chunk: string) => out.push(chunk));
|
|
235
|
+
child.stderr?.on('data', (chunk: string) => err.push(chunk));
|
|
236
|
+
|
|
237
|
+
let signalled = false;
|
|
238
|
+
let forceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
239
|
+
const killTree = (signal: NodeJS.Signals): void => {
|
|
240
|
+
if (process.platform !== 'win32' && child.pid !== undefined) {
|
|
241
|
+
process.kill(-child.pid, signal);
|
|
242
|
+
} else {
|
|
243
|
+
child.kill(signal);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const exited = new Promise<number>((resolve) => {
|
|
248
|
+
child.once('error', (e) => {
|
|
249
|
+
process.stderr.write(`pyric dev: failed to run \`${plan.label}\`: ${e.message}\n`);
|
|
250
|
+
resolve(1);
|
|
251
|
+
});
|
|
252
|
+
child.once('exit', (code, signal) => {
|
|
253
|
+
out.flush();
|
|
254
|
+
err.flush();
|
|
255
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
256
|
+
// User-initiated stop (Ctrl-C forwarded) is a clean shutdown → 0.
|
|
257
|
+
if (signalled) resolve(0);
|
|
258
|
+
else if (typeof code === 'number') resolve(code);
|
|
259
|
+
else resolve(signal ? 1 : 0);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
exited,
|
|
265
|
+
child,
|
|
266
|
+
signal(sig: NodeJS.Signals): void {
|
|
267
|
+
if (child.exitCode !== null || signalled) return;
|
|
268
|
+
signalled = true;
|
|
269
|
+
try {
|
|
270
|
+
killTree(sig);
|
|
271
|
+
} catch {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
forceTimer = setTimeout(() => {
|
|
275
|
+
try {
|
|
276
|
+
killTree('SIGKILL');
|
|
277
|
+
} catch {}
|
|
278
|
+
}, FORCE_KILL_AFTER_MS);
|
|
279
|
+
forceTimer.unref();
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal `firebase.json` / `.firebaserc` reader for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces only the fields the subcommands actually touch — the
|
|
5
|
+
* full firebase.json schema is huge and a full parse isn't needed
|
|
6
|
+
* for any current subcommand. New fields land alongside the
|
|
7
|
+
* subcommand that consumes them.
|
|
8
|
+
*
|
|
9
|
+
* `.firebaserc` carries the project id under `projects.default`;
|
|
10
|
+
* the standard Firebase CLI uses the same convention.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFile } from 'node:fs/promises';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
export interface FirebaseJson {
|
|
17
|
+
firestore?: {
|
|
18
|
+
rules?: string;
|
|
19
|
+
indexes?: string;
|
|
20
|
+
database?: string;
|
|
21
|
+
};
|
|
22
|
+
/** Realtime Database rules path plus optional explicit instance URL. */
|
|
23
|
+
database?: {
|
|
24
|
+
rules?: string;
|
|
25
|
+
url?: string;
|
|
26
|
+
};
|
|
27
|
+
hosting?: unknown;
|
|
28
|
+
functions?: unknown;
|
|
29
|
+
/** Storage rules + (optional) target bucket. A single object, or an array for
|
|
30
|
+
* multi-bucket projects. `rules` is a path; `bucket` overrides the default
|
|
31
|
+
* `{projectId}.firebasestorage.app`. */
|
|
32
|
+
storage?: { rules?: string; bucket?: string } | Array<{ rules?: string; bucket?: string }>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface FirebaseRc {
|
|
36
|
+
projects?: {
|
|
37
|
+
default?: string;
|
|
38
|
+
[alias: string]: string | undefined;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Deploy-target maps, keyed project → resource type → target name →
|
|
42
|
+
* resource ids (mirrors firebase-tools' RCData,
|
|
43
|
+
* clones/firebase-tools/src/rc.ts:28-36). Only `hosting` (target →
|
|
44
|
+
* site ids) is consumed today.
|
|
45
|
+
*/
|
|
46
|
+
targets?: {
|
|
47
|
+
[projectId: string]: {
|
|
48
|
+
[targetType: string]: {
|
|
49
|
+
[targetName: string]: string[] | undefined;
|
|
50
|
+
} | undefined;
|
|
51
|
+
} | undefined;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read + parse `firebase.json` from `cwd`. Throws when missing — every
|
|
57
|
+
* subcommand that needs project metadata expects a present file.
|
|
58
|
+
*/
|
|
59
|
+
export async function readFirebaseJson(cwd: string = process.cwd()): Promise<FirebaseJson> {
|
|
60
|
+
const path = join(cwd, 'firebase.json');
|
|
61
|
+
let raw: string;
|
|
62
|
+
try {
|
|
63
|
+
raw = await readFile(path, 'utf-8');
|
|
64
|
+
} catch (e) {
|
|
65
|
+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
66
|
+
throw new Error(`pyric: no firebase.json found at ${path}. Run \`pyric init\` to scaffold one.`);
|
|
67
|
+
}
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(raw) as FirebaseJson;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`pyric: failed to parse firebase.json: ${e instanceof Error ? e.message : String(e)}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read + parse `.firebaserc` from `cwd`. Returns null when missing —
|
|
81
|
+
* the project id can also come from `--project` / `PYRIC_PROJECT`, so
|
|
82
|
+
* absence is not fatal at this layer.
|
|
83
|
+
*/
|
|
84
|
+
export async function readFirebaseRc(cwd: string = process.cwd()): Promise<FirebaseRc | null> {
|
|
85
|
+
const path = join(cwd, '.firebaserc');
|
|
86
|
+
let raw: string;
|
|
87
|
+
try {
|
|
88
|
+
raw = await readFile(path, 'utf-8');
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(raw) as FirebaseRc;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`pyric: failed to parse .firebaserc: ${e instanceof Error ? e.message : String(e)}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|