@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.
Files changed (301) hide show
  1. package/dist/conformance/.generated/can-i-use-browser.d.ts.map +1 -1
  2. package/dist/conformance/.generated/can-i-use-browser.js +1 -1
  3. package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
  4. package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
  5. package/dist/conformance/.generated/can-i-use.js +2 -2
  6. package/dist/conformance/.generated/can-i-use.js.map +1 -1
  7. package/dist/serve/docs-ui/docs/_rhythm/index.html +1 -1
  8. package/dist/serve/docs-ui/docs/agent/set-up-your-agent/index.html +1 -1
  9. package/dist/serve/docs-ui/docs/agent/watch-and-review/index.html +1 -1
  10. package/dist/serve/docs-ui/docs/agent/work-with-an-agent/index.html +1 -1
  11. package/dist/serve/docs-ui/docs/ai-compat/index.html +1 -1
  12. package/dist/serve/docs-ui/docs/api-reference/index.html +2 -2
  13. package/dist/serve/docs-ui/docs/api-reference.md +1 -1
  14. package/dist/serve/docs-ui/docs/app-compat/index.html +1 -1
  15. package/dist/serve/docs-ui/docs/auth-compat/index.html +1 -1
  16. package/dist/serve/docs-ui/docs/build/ai-logic/index.html +1 -1
  17. package/dist/serve/docs-ui/docs/build/authentication/index.html +1 -1
  18. package/dist/serve/docs-ui/docs/build/cloud-firestore/index.html +1 -1
  19. package/dist/serve/docs-ui/docs/build/cloud-messaging/index.html +1 -1
  20. package/dist/serve/docs-ui/docs/build/cloud-storage/index.html +1 -1
  21. package/dist/serve/docs-ui/docs/build/realtime-database/index.html +1 -1
  22. package/dist/serve/docs-ui/docs/conformance-scores/index.html +1 -1
  23. package/dist/serve/docs-ui/docs/create-pyric-reference-api/index.html +1 -1
  24. package/dist/serve/docs-ui/docs/database-compat/index.html +1 -1
  25. package/dist/serve/docs-ui/docs/firestore-compat/index.html +1 -1
  26. package/dist/serve/docs-ui/docs/functions-rtdb-compat/index.html +1 -1
  27. package/dist/serve/docs-ui/docs/get-started/how-the-swap-works/index.html +1 -1
  28. package/dist/serve/docs-ui/docs/get-started/start-building/index.html +1 -1
  29. package/dist/serve/docs-ui/docs/index.json +200 -25
  30. package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
  31. package/dist/serve/docs-ui/docs/observe/see-whats-happening/index.html +1 -1
  32. package/dist/serve/docs-ui/docs/observe/shape-your-data/index.html +1 -1
  33. package/dist/serve/docs-ui/docs/overview/index.html +1 -1
  34. package/dist/serve/docs-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
  35. package/dist/serve/docs-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
  36. package/dist/serve/docs-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
  37. package/dist/serve/docs-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
  38. package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api/index.html +916 -0
  39. package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api.md +706 -0
  40. package/dist/serve/docs-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
  41. package/dist/serve/docs-ui/docs/pyric-ai-reference-api/index.html +1 -1
  42. package/dist/serve/docs-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
  43. package/dist/serve/docs-ui/docs/pyric-app-reference-api/index.html +1 -1
  44. package/dist/serve/docs-ui/docs/pyric-auth-reference-api/index.html +1 -1
  45. package/dist/serve/docs-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
  46. package/dist/serve/docs-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
  47. package/dist/serve/docs-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
  48. package/dist/serve/docs-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
  49. package/dist/serve/docs-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
  50. package/dist/serve/docs-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
  51. package/dist/serve/docs-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
  52. package/dist/serve/docs-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
  53. package/dist/serve/docs-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
  54. package/dist/serve/docs-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
  55. package/dist/serve/docs-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
  56. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api/index.html +131 -66
  57. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +130 -65
  58. package/dist/serve/docs-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
  59. package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api/index.html +1 -1
  60. package/dist/serve/docs-ui/docs/pyric-database-reference-api/index.html +1 -1
  61. package/dist/serve/docs-ui/docs/pyric-firestore-reference-api/index.html +1 -1
  62. package/dist/serve/docs-ui/docs/pyric-messaging-reference-api/index.html +1 -1
  63. package/dist/serve/docs-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
  64. package/dist/serve/docs-ui/docs/pyric-rules-reference-api/index.html +1 -1
  65. package/dist/serve/docs-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
  66. package/dist/serve/docs-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
  67. package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api/index.html +37 -14
  68. package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api.md +28 -8
  69. package/dist/serve/docs-ui/docs/pyric-storage-reference-api/index.html +1 -1
  70. package/dist/serve/docs-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
  71. package/dist/serve/docs-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
  72. package/dist/serve/docs-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
  73. package/dist/serve/docs-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
  74. package/dist/serve/docs-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
  75. package/dist/serve/docs-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
  76. package/dist/serve/docs-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
  77. package/dist/serve/docs-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
  78. package/dist/serve/docs-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
  79. package/dist/serve/docs-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
  80. package/dist/serve/docs-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
  81. package/dist/serve/docs-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
  82. package/dist/serve/docs-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
  83. package/dist/serve/docs-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
  84. package/dist/serve/docs-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
  85. package/dist/serve/docs-ui/docs/rules-compat/index.html +1 -1
  86. package/dist/serve/docs-ui/docs/secure/audit-your-rules/index.html +1 -1
  87. package/dist/serve/docs-ui/docs/secure/firestore-rules-limits/index.html +1 -1
  88. package/dist/serve/docs-ui/docs/secure/read-a-denial/index.html +1 -1
  89. package/dist/serve/docs-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
  90. package/dist/serve/docs-ui/docs/secure/rules-standard-library/index.html +1 -1
  91. package/dist/serve/docs-ui/docs/secure/secure-it-with-rules/index.html +1 -1
  92. package/dist/serve/docs-ui/docs/secure/simulate-and-lint/index.html +1 -1
  93. package/dist/serve/docs-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
  94. package/dist/serve/docs-ui/docs/ship/ship-to-production/index.html +1 -1
  95. package/dist/serve/docs-ui/docs/ship/test-in-node/index.html +1 -1
  96. package/dist/serve/docs-ui/docs/storage-compat/index.html +1 -1
  97. package/dist/serve/docs-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
  98. package/dist/serve/docs-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
  99. package/dist/serve/docs-ui/llms.txt +1 -1
  100. package/dist/serve/worker/host/core.js +1 -1
  101. package/dist/serve/worker/host/core.js.map +1 -1
  102. package/dist/serve/worker/host/firestore-writes.d.ts.map +1 -1
  103. package/dist/serve/worker/host/firestore-writes.js +3 -2
  104. package/dist/serve/worker/host/firestore-writes.js.map +1 -1
  105. package/dist/serve/worker/index.d.ts +2 -1
  106. package/dist/serve/worker/index.d.ts.map +1 -1
  107. package/dist/serve/worker/index.js +2 -1
  108. package/dist/serve/worker/index.js.map +1 -1
  109. package/dist/serve/worker/protocol.js +2 -2
  110. package/dist/serve/worker/protocol.js.map +1 -1
  111. package/package.json +5 -4
  112. package/src/assurance/.generated/conformance-verdicts.ts +1090 -0
  113. package/src/assurance/attachment.ts +211 -0
  114. package/src/assurance/browser.ts +80 -0
  115. package/src/assurance/campaign.ts +496 -0
  116. package/src/assurance/capabilities.ts +446 -0
  117. package/src/assurance/cases.ts +43 -0
  118. package/src/assurance/index.ts +75 -0
  119. package/src/assurance/runner.ts +885 -0
  120. package/src/assurance/tool-names.ts +15 -0
  121. package/src/assurance/tools.ts +830 -0
  122. package/src/assurance/types.ts +352 -0
  123. package/src/assurance/validation.ts +434 -0
  124. package/src/bridge/client/bridge.ts +550 -0
  125. package/src/bridge/client/dispatch.ts +137 -0
  126. package/src/bridge/client.ts +45 -0
  127. package/src/bridge/protocol.ts +351 -0
  128. package/src/bridge/server/audit.ts +53 -0
  129. package/src/bridge/server/bridge.ts +597 -0
  130. package/src/bridge/server/headless.ts +160 -0
  131. package/src/bridge/server/json-schema-to-zod.ts +108 -0
  132. package/src/bridge/server/local-bridge.ts +66 -0
  133. package/src/bridge/server/logger.ts +50 -0
  134. package/src/bridge/server/mcp-contract.ts +93 -0
  135. package/src/bridge/server/mcp.ts +129 -0
  136. package/src/bridge/server/peer.ts +232 -0
  137. package/src/bridge/server/standalone.ts +347 -0
  138. package/src/bridge/server/tool-metadata.ts +99 -0
  139. package/src/bridge/server.ts +28 -0
  140. package/src/cli/can-i-use.ts +50 -0
  141. package/src/cli/cli.test.ts +493 -0
  142. package/src/cli/database-rules.ts +329 -0
  143. package/src/cli/dev-runner.ts +282 -0
  144. package/src/cli/firebase-json.ts +100 -0
  145. package/src/cli/firestore-indexes.ts +70 -0
  146. package/src/cli/index.ts +425 -0
  147. package/src/cli/init.ts +260 -0
  148. package/src/cli/mcp-proxy.ts +196 -0
  149. package/src/cli/parse-args.ts +80 -0
  150. package/src/cli/rules.ts +259 -0
  151. package/src/cli/scope.ts +67 -0
  152. package/src/cli/serve.ts +996 -0
  153. package/src/cli/service-commands.ts +73 -0
  154. package/src/cli/snapshot.ts +164 -0
  155. package/src/cli/storage-rules.ts +172 -0
  156. package/src/cli/verify.ts +416 -0
  157. package/src/conformance/.generated/can-i-use-browser.ts +113 -0
  158. package/src/conformance/.generated/can-i-use.ts +117 -0
  159. package/src/conformance/.generated/conformance-docs.ts +14 -0
  160. package/src/conformance/browser.ts +24 -0
  161. package/src/conformance/can-i-use-tool.ts +51 -0
  162. package/src/conformance/can-i-use.ts +18 -0
  163. package/src/conformance/docs.ts +8 -0
  164. package/src/conformance/index.ts +20 -0
  165. package/src/conformance/tools.ts +16 -0
  166. package/src/credentials/core/memoize-ttl.ts +142 -0
  167. package/src/credentials/core/types.ts +10 -0
  168. package/src/credentials/node/from-adc.ts +96 -0
  169. package/src/credentials/node/from-service-account.ts +133 -0
  170. package/src/credentials/node/index.ts +6 -0
  171. package/src/discover/concurrency.ts +127 -0
  172. package/src/discover/crawler-adapter.ts +142 -0
  173. package/src/discover/crawler.ts +1127 -0
  174. package/src/discover/credential-free.ts +27 -0
  175. package/src/discover/findCollectionGroup.ts +131 -0
  176. package/src/discover/firestore-source.ts +59 -0
  177. package/src/discover/index.ts +5 -0
  178. package/src/discover/merge.ts +523 -0
  179. package/src/discover/session.ts +402 -0
  180. package/src/discover/tools.ts +201 -0
  181. package/src/discover/types.ts +187 -0
  182. package/src/discover/wire.ts +324 -0
  183. package/src/functions-rtdb/child.ts +382 -0
  184. package/src/functions-rtdb/delivery.ts +7 -0
  185. package/src/functions-rtdb/discovery.ts +136 -0
  186. package/src/functions-rtdb/event.ts +64 -0
  187. package/src/functions-rtdb/execution.ts +111 -0
  188. package/src/functions-rtdb/in-memory-delivery.ts +40 -0
  189. package/src/functions-rtdb/project.ts +74 -0
  190. package/src/functions-rtdb/projection.ts +104 -0
  191. package/src/functions-rtdb/reference-pattern.ts +20 -0
  192. package/src/functions-rtdb/remote-delivery.ts +23 -0
  193. package/src/pkg-version.ts +49 -0
  194. package/src/register/esm-exports.ts +63 -0
  195. package/src/register/hooks.ts +30 -0
  196. package/src/register/index.ts +150 -0
  197. package/src/register/mapping.ts +33 -0
  198. package/src/remote/index.ts +1050 -0
  199. package/src/rtdb/crawl-snapshot.ts +118 -0
  200. package/src/rtdb/inspection.ts +137 -0
  201. package/src/rtdb/load-rules-document.ts +56 -0
  202. package/src/rtdb/rules-generation-tool.ts +38 -0
  203. package/src/rtdb/rules-json.ts +29 -0
  204. package/src/serve/activity-guard.ts +29 -0
  205. package/src/serve/activity-route.ts +164 -0
  206. package/src/serve/activity-warning.ts +26 -0
  207. package/src/serve/bridge-mount.ts +200 -0
  208. package/src/serve/bundler.ts +493 -0
  209. package/src/serve/capture-store.ts +65 -0
  210. package/src/serve/discovery.ts +199 -0
  211. package/src/serve/entries/ai.ts +292 -0
  212. package/src/serve/entries/app-backend.ts +4 -0
  213. package/src/serve/entries/app-client.ts +26 -0
  214. package/src/serve/entries/app-session-store.ts +47 -0
  215. package/src/serve/entries/app.ts +11 -0
  216. package/src/serve/entries/auth-helper-core.ts +181 -0
  217. package/src/serve/entries/auth-helper-dom.ts +134 -0
  218. package/src/serve/entries/auth-helper-runtime.ts +20 -0
  219. package/src/serve/entries/auth.ts +255 -0
  220. package/src/serve/entries/bridge-url.ts +31 -0
  221. package/src/serve/entries/database.ts +129 -0
  222. package/src/serve/entries/firestore.ts +232 -0
  223. package/src/serve/entries/init.ts +46 -0
  224. package/src/serve/entries/keepalive.ts +46 -0
  225. package/src/serve/entries/messaging-sw.ts +75 -0
  226. package/src/serve/entries/messaging.ts +96 -0
  227. package/src/serve/entries/runtime.ts +540 -0
  228. package/src/serve/entries/session-store.ts +97 -0
  229. package/src/serve/entries/storage.ts +92 -0
  230. package/src/serve/entries/tab-sync-wiring.ts +274 -0
  231. package/src/serve/entries/worker-runtime.ts +75 -0
  232. package/src/serve/init-payload.ts +42 -0
  233. package/src/serve/namespace.ts +675 -0
  234. package/src/serve/open-browser.ts +68 -0
  235. package/src/serve/rules.ts +272 -0
  236. package/src/serve/sandbox-marker.ts +31 -0
  237. package/src/serve/server.ts +428 -0
  238. package/src/serve/standalone-assets.ts +180 -0
  239. package/src/serve/state-store.ts +166 -0
  240. package/src/serve/studio/disk-project-store.ts +185 -0
  241. package/src/serve/studio/disk-workspace.ts +162 -0
  242. package/src/serve/studio/index.ts +20 -0
  243. package/src/serve/studio/routes.ts +237 -0
  244. package/src/serve/studio/store-types.ts +48 -0
  245. package/src/serve/studio/studio-storage.test.ts +289 -0
  246. package/src/serve/vite-plugin.ts +1102 -0
  247. package/src/serve/worker/activity-bootstrap.ts +24 -0
  248. package/src/serve/worker/client/admin-firestore.ts +38 -0
  249. package/src/serve/worker/client/ai.ts +124 -0
  250. package/src/serve/worker/client/auth.ts +489 -0
  251. package/src/serve/worker/client/connection.ts +202 -0
  252. package/src/serve/worker/client/core.ts +350 -0
  253. package/src/serve/worker/client/disconnect.ts +45 -0
  254. package/src/serve/worker/client/firestore-reads.ts +181 -0
  255. package/src/serve/worker/client/firestore-refs.ts +226 -0
  256. package/src/serve/worker/client/firestore-writes.ts +232 -0
  257. package/src/serve/worker/client/handles.ts +91 -0
  258. package/src/serve/worker/client/messaging.ts +119 -0
  259. package/src/serve/worker/client/presence.ts +174 -0
  260. package/src/serve/worker/client/rtdb.ts +311 -0
  261. package/src/serve/worker/client/rules.ts +63 -0
  262. package/src/serve/worker/client/service-worker-connection.ts +59 -0
  263. package/src/serve/worker/client/snapshots.ts +86 -0
  264. package/src/serve/worker/client/storage.ts +195 -0
  265. package/src/serve/worker/client/studio.ts +88 -0
  266. package/src/serve/worker/client.ts +57 -0
  267. package/src/serve/worker/durable-persistence.ts +137 -0
  268. package/src/serve/worker/entry.ts +179 -0
  269. package/src/serve/worker/host/admin-firestore.ts +88 -0
  270. package/src/serve/worker/host/connection.ts +179 -0
  271. package/src/serve/worker/host/core.ts +387 -0
  272. package/src/serve/worker/host/dispatch.ts +316 -0
  273. package/src/serve/worker/host/firestore-reads.ts +117 -0
  274. package/src/serve/worker/host/firestore-writes.ts +456 -0
  275. package/src/serve/worker/host/presence.ts +312 -0
  276. package/src/serve/worker/host/rtdb.ts +136 -0
  277. package/src/serve/worker/host/rules.ts +128 -0
  278. package/src/serve/worker/host/storage.ts +312 -0
  279. package/src/serve/worker/host/studio.ts +75 -0
  280. package/src/serve/worker/host/subscriptions.ts +238 -0
  281. package/src/serve/worker/host-ai.ts +165 -0
  282. package/src/serve/worker/host-auth.ts +469 -0
  283. package/src/serve/worker/host-context.ts +273 -0
  284. package/src/serve/worker/host-events.ts +94 -0
  285. package/src/serve/worker/host-messaging.ts +240 -0
  286. package/src/serve/worker/host.ts +47 -0
  287. package/src/serve/worker/index.ts +196 -0
  288. package/src/serve/worker/presence-timing.ts +15 -0
  289. package/src/serve/worker/protocol.ts +1112 -0
  290. package/src/serve/worker/serve-init.ts +606 -0
  291. package/src/serve/worker/service-worker-channel.ts +33 -0
  292. package/src/serve/worker/service-worker-relay.ts +86 -0
  293. package/src/serve/writer-lock.ts +53 -0
  294. package/src/verify/cases.ts +233 -0
  295. package/src/verify/fixture.ts +258 -0
  296. package/src/verify/index.ts +519 -0
  297. package/src/verify/tools.ts +108 -0
  298. package/src/version/compat-target.ts +17 -0
  299. package/src/vite.ts +17 -0
  300. package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api/index.html +0 -31
  301. package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api.md +0 -29
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `@pyric/cli/bridge` — Node-side entry.
3
+ *
4
+ * Resolved by the `node` condition in `package.json`'s exports map.
5
+ * Exports the bridge core (`createBridge`) and the standalone server
6
+ * (`startServer`). The Vite integration is the `pyric({ bridge })`
7
+ * plugin in `@pyric/cli/vite` (the firebase→sandbox swap AND the bridge in
8
+ * one plugin), not a bridge-only plugin here.
9
+ *
10
+ * The browser-side `connectBridge` lives in `./client.ts` and is
11
+ * resolved by the `browser` condition. Wire format shared by both
12
+ * lives in `./protocol.ts` (type-only, no runtime imports across the
13
+ * boundary).
14
+ */
15
+
16
+ export type { HealthReport } from './protocol.js';
17
+ export {
18
+ DEFAULT_BRIDGE_PORT,
19
+ DEFAULT_HEALTH_PATH,
20
+ DEFAULT_MCP_PATH,
21
+ DEFAULT_SANDBOX_PATH,
22
+ } from './protocol.js';
23
+
24
+ export { createBridge } from './server/bridge.js';
25
+ export type { Bridge, BridgeOptions } from './server/bridge.js';
26
+
27
+ export { startServer } from './server/standalone.js';
28
+ export type { ServerHandle, StartServerOptions } from './server/standalone.js';
@@ -0,0 +1,50 @@
1
+ import type { ParsedArgs } from './parse-args.js';
2
+ import { canIUse } from '../conformance/index.js';
3
+
4
+ /** Query feature support. This is discovery, independent of rules-fixture
5
+ * verification, so it owns a top-level CLI command. */
6
+ export function runCanIUse(parsed: ParsedArgs): number {
7
+ for (const [flag, value] of parsed.flags) {
8
+ const validJsonValue = flag === 'json' && (value === true || value === 'true' || value === 'false');
9
+ if (!validJsonValue) {
10
+ const rendered = value === true ? `--${flag}` : `--${flag}=${String(value)}`;
11
+ process.stderr.write(`pyric can-i-use: unknown option '${rendered}'\n`);
12
+ return 1;
13
+ }
14
+ }
15
+ const feature = parsed.positional.join(' ');
16
+ if (!feature.trim()) {
17
+ process.stderr.write('pyric can-i-use: provide a developer feature name\n');
18
+ return 1;
19
+ }
20
+ const result = canIUse(feature);
21
+ const jsonFlag = parsed.flags.get('json');
22
+ if (jsonFlag === true || jsonFlag === 'true') {
23
+ process.stdout.write(JSON.stringify(result) + '\n');
24
+ return result.match === 'exact' ? 0 : 1;
25
+ }
26
+ if (result.match === 'none') {
27
+ process.stdout.write(`No conformance feature matched "${feature}".\n`);
28
+ return 1;
29
+ }
30
+ if (result.match === 'suggestions') {
31
+ process.stdout.write(`No exact conformance feature matched "${feature}". Did you mean:\n`);
32
+ for (const support of result.supports) process.stdout.write(` ${support.surface}/${support.feature}\n`);
33
+ return 1;
34
+ }
35
+ if (result.match === 'ambiguous') {
36
+ process.stdout.write(`"${feature}" matches more than one surface:\n`);
37
+ }
38
+ for (const support of result.supports) {
39
+ process.stdout.write(
40
+ `${support.feature} (${support.surface})\n` +
41
+ ` availability: ${support.availability}\n` +
42
+ ` fidelity: ${support.fidelity}\n` +
43
+ ` assurance: ${support.assurance}\n` +
44
+ ` evidence: ${support.evidenceSlug}\n` +
45
+ ` ${support.summary}\n`,
46
+ );
47
+ for (const caveat of support.caveats) process.stdout.write(` caveat: ${caveat}\n`);
48
+ }
49
+ return result.match === 'exact' ? 0 : 1;
50
+ }
@@ -0,0 +1,493 @@
1
+ /**
2
+ * CLI argument-parsing + dispatch tests.
3
+ *
4
+ * Each subcommand handler accepts an injectable `deps` arg so the
5
+ * tests assert dispatch happens with the right shape without going
6
+ * near the real Google APIs, real disk reads, or real service-account
7
+ * loading. The injection seam is the contract — these tests pin it.
8
+ *
9
+ * Conventions:
10
+ * - `bufferIo()` returns `{ stdout, stderr, getOut, getErr }`
11
+ * suitable for handing to any subcommand's `deps`.
12
+ * - Argv is parsed via the real `parseArgs` to exercise the CLI's
13
+ * flag handling end-to-end (no shortcuts).
14
+ */
15
+
16
+ import { describe, it, expect, mock } from 'bun:test';
17
+
18
+ import { parseArgs } from './parse-args.js';
19
+ import { runRulesLint, runRulesValidate, runRulesSimulate } from './rules.js';
20
+ import {
21
+ runDatabaseRulesLint,
22
+ runDatabaseRulesValidate,
23
+ runDatabaseRulesSimulate,
24
+ runDatabaseRulesGenerate,
25
+ } from './database-rules.js';
26
+ import { runInit } from './init.js';
27
+
28
+ // ── Test helpers ──────────────────────────────────────────────────────
29
+
30
+ function bufferIo() {
31
+ let outBuf = '';
32
+ let errBuf = '';
33
+ return {
34
+ stdout: { write(s: string): void { outBuf += s; } },
35
+ stderr: { write(s: string): void { errBuf += s; } },
36
+ getOut: () => outBuf,
37
+ getErr: () => errBuf,
38
+ };
39
+ }
40
+
41
+ function serviceArgs(argv: string[]) {
42
+ const parsed = parseArgs(argv);
43
+ return { ...parsed, positional: parsed.positional.slice(2) };
44
+ }
45
+
46
+ // ── parseArgs ─────────────────────────────────────────────────────────
47
+
48
+ describe('parseArgs', () => {
49
+ it('captures subcommand + positionals + long flags', () => {
50
+ const p = parseArgs(['verify', 'capture.json', '--project', 'my-proj']);
51
+ expect(p.subcommand).toBe('verify');
52
+ expect(p.positional).toEqual(['capture.json']);
53
+ expect(p.flags.get('project')).toBe('my-proj');
54
+ });
55
+
56
+ it('parses --flag=value syntax', () => {
57
+ const p = parseArgs(['firestore', 'rules', 'simulate', '--stdin=true']);
58
+ expect(p.flags.get('stdin')).toBe('true');
59
+ });
60
+
61
+ it('treats a flag without a value as boolean true', () => {
62
+ const p = parseArgs(['firestore', 'rules', 'simulate', '--stdin']);
63
+ expect(p.flags.get('stdin')).toBe(true);
64
+ });
65
+
66
+ it('preserves repeated long flags in order', () => {
67
+ const p = parseArgs([
68
+ 'verify',
69
+ '--service',
70
+ 'firestore',
71
+ '--service',
72
+ 'rtdb',
73
+ '--rules=firestore=firestore.rules',
74
+ '--rules',
75
+ 'rtdb=database.rules.json',
76
+ ]);
77
+ expect(p.flags.get('service')).toEqual(['firestore', 'rtdb']);
78
+ expect(p.flags.get('rules')).toEqual([
79
+ 'firestore=firestore.rules',
80
+ 'rtdb=database.rules.json',
81
+ ]);
82
+ });
83
+ });
84
+
85
+ // ── rules ─────────────────────────────────────────────────────────────
86
+
87
+ describe('runRulesLint', () => {
88
+ it('errors out when no path given', async () => {
89
+ const io = bufferIo();
90
+ const code = await runRulesLint(serviceArgs(['firestore', 'rules', 'lint']), { ...io });
91
+ expect(code).toBe(1);
92
+ expect(io.getErr()).toContain('missing rules-file path');
93
+ });
94
+
95
+ it('passes the source to the linter and prints JSON', async () => {
96
+ const io = bufferIo();
97
+ const lintFn = mock(() => ({ warnings: [], metrics: { sourceSize: 5 } as never }));
98
+ const code = await runRulesLint(serviceArgs(['firestore', 'rules', 'lint', 'firestore.rules']), {
99
+ ...io,
100
+ cwd: '/tmp',
101
+ readFile: (async () => 'source') as never,
102
+ lintFirestoreRules: lintFn as never,
103
+ });
104
+ expect(code).toBe(0);
105
+ expect(lintFn).toHaveBeenCalledTimes(1);
106
+ expect(lintFn).toHaveBeenCalledWith('source');
107
+ const out = JSON.parse(io.getOut()) as { warnings: unknown[] };
108
+ expect(out.warnings).toEqual([]);
109
+ });
110
+ });
111
+
112
+ describe('runRulesValidate', () => {
113
+ it('errors out when no path given', async () => {
114
+ const io = bufferIo();
115
+ const code = await runRulesValidate(serviceArgs(['firestore', 'rules', 'validate']), { ...io });
116
+ expect(code).toBe(1);
117
+ });
118
+
119
+ it('reports parse failure with exit 2 on invalid rules', async () => {
120
+ const io = bufferIo();
121
+ const code = await runRulesValidate(serviceArgs(['firestore', 'rules', 'validate', 'bad.rules']), {
122
+ ...io,
123
+ cwd: '/tmp',
124
+ readFile: (async () => 'not valid rules at all') as never,
125
+ });
126
+ expect(code).toBe(2);
127
+ expect(io.getErr()).toContain('failed to parse');
128
+ });
129
+ });
130
+
131
+ describe('runRulesSimulate', () => {
132
+ it('runs a sample test against firebase.json rules path', async () => {
133
+ const io = bufferIo();
134
+ const simulateFn = mock(() => ({
135
+ success: true,
136
+ data: { results: [], passed: 0, failed: 0 },
137
+ } as never));
138
+ const code = await runRulesSimulate(serviceArgs(['firestore', 'rules', 'simulate']), {
139
+ ...io,
140
+ cwd: '/tmp',
141
+ readFirebaseJson: async () => ({ firestore: { rules: 'firestore.rules' } }),
142
+ readFile: (async () => 'rules source') as never,
143
+ simulate: simulateFn as never,
144
+ });
145
+ expect(code).toBe(0);
146
+ expect(simulateFn).toHaveBeenCalledTimes(1);
147
+ const callArgs = (simulateFn.mock.calls[0] ?? []) as [string, unknown[]];
148
+ expect(callArgs[0]).toBe('rules source');
149
+ expect(callArgs[1]).toHaveLength(1);
150
+ });
151
+
152
+ it('reads request from stdin when --stdin is set', async () => {
153
+ const io = bufferIo();
154
+ const simulateFn = mock(() => ({ success: true, data: { results: [], passed: 0, failed: 0 } } as never));
155
+ const code = await runRulesSimulate(serviceArgs(['firestore', 'rules', 'simulate', '--stdin']), {
156
+ ...io,
157
+ cwd: '/tmp',
158
+ readStdin: async () =>
159
+ JSON.stringify({
160
+ source: 'inline rules',
161
+ testCases: [
162
+ { description: 't', expectation: 'ALLOW', method: 'get', path: 'x/1', auth: null },
163
+ { description: 't2', expectation: 'DENY', method: 'get', path: 'x/2', auth: null },
164
+ ],
165
+ }),
166
+ simulate: simulateFn as never,
167
+ });
168
+ expect(code).toBe(0);
169
+ const callArgs = (simulateFn.mock.calls[0] ?? []) as [string, unknown[]];
170
+ expect(callArgs[0]).toBe('inline rules');
171
+ expect(callArgs[1]).toHaveLength(2);
172
+ });
173
+ });
174
+
175
+ // ── database rules ───────────────────────────────────────────────────
176
+
177
+ describe('runDatabaseRulesLint', () => {
178
+ it('errors out when no path given', async () => {
179
+ const io = bufferIo();
180
+ const code = await runDatabaseRulesLint(serviceArgs(['database', 'rules', 'lint']), { ...io });
181
+ expect(code).toBe(1);
182
+ expect(io.getErr()).toContain('missing rules-file path');
183
+ });
184
+
185
+ it('prints RTDB expression lints as JSON', async () => {
186
+ const io = bufferIo();
187
+ const code = await runDatabaseRulesLint(serviceArgs(['database', 'rules', 'lint', 'database.rules.json']), {
188
+ ...io,
189
+ cwd: '/tmp',
190
+ readFile: (async () => '{"rules":{".read":true,".write":false}}') as never,
191
+ });
192
+ expect(code).toBe(0);
193
+ const out = JSON.parse(io.getOut()) as { warnings: Array<{ code: string }> };
194
+ expect(out.warnings.map((finding) => finding.code).sort()).toEqual([
195
+ 'HARDCODED_FALSE',
196
+ 'HARDCODED_TRUE',
197
+ ]);
198
+ });
199
+ });
200
+
201
+ describe('runDatabaseRulesValidate', () => {
202
+ it('reports RTDB expression parse errors', async () => {
203
+ const io = bufferIo();
204
+ const code = await runDatabaseRulesValidate(serviceArgs(['database', 'rules', 'validate', 'database.rules.json']), {
205
+ ...io,
206
+ cwd: '/tmp',
207
+ readFile: (async () => '{"rules":{".read":"auth.uid =="}}') as never,
208
+ });
209
+ expect(code).toBe(0);
210
+ const out = JSON.parse(io.getOut()) as { errors: Array<{ code: string }> };
211
+ expect(out.errors.some((finding) => finding.code === 'PARSE_ERROR')).toBe(true);
212
+ });
213
+ });
214
+
215
+ describe('runDatabaseRulesSimulate', () => {
216
+ it('simulates inline RTDB rules from stdin', async () => {
217
+ const io = bufferIo();
218
+ const code = await runDatabaseRulesSimulate(serviceArgs(['database', 'rules', 'simulate', '--stdin']), {
219
+ ...io,
220
+ readStdin: async () =>
221
+ JSON.stringify({
222
+ rulesJson: { rules: { '.read': true } },
223
+ operation: 'read',
224
+ path: '/sample',
225
+ auth: null,
226
+ mockData: {},
227
+ }),
228
+ });
229
+ expect(code).toBe(0);
230
+ const result = JSON.parse(io.getOut()) as { success: boolean; data: { allowed: boolean } };
231
+ expect(result.success).toBe(true);
232
+ expect(result.data.allowed).toBe(true);
233
+ });
234
+ });
235
+
236
+ describe('runDatabaseRulesGenerate', () => {
237
+ it('loads the constraints module, compiles via toJSON(), and writes the file', async () => {
238
+ const { defineRtdbRules, allow, deny } = await import('pyric/rules');
239
+ const doc = defineRtdbRules({
240
+ paths: { '/': { read: allow(), write: deny() } },
241
+ });
242
+
243
+ const io = bufferIo();
244
+ const writes: Array<{ path: unknown; contents: unknown }> = [];
245
+ const code = await runDatabaseRulesGenerate(serviceArgs(['database', 'rules', 'generate']), {
246
+ ...io,
247
+ cwd: '/project',
248
+ loadRulesDocument: (async () => ({ ok: true, document: doc })) as never,
249
+ readFirebaseJson: (async () => ({ database: { rules: 'database.rules.json' } })) as never,
250
+ mkdir: (async () => undefined) as never,
251
+ writeFile: (async (path: unknown, contents: unknown) => {
252
+ writes.push({ path, contents });
253
+ }) as never,
254
+ });
255
+
256
+ expect(code).toBe(0);
257
+ expect(writes).toHaveLength(1);
258
+ expect(writes[0]?.path).toBe('/project/database.rules.json');
259
+ expect(writes[0]?.contents).toBe(`${JSON.stringify(doc.toJSON(), null, 2)}\n`);
260
+ expect(io.getOut()).toContain('/project/database.rules.json');
261
+ });
262
+
263
+ it('reports a clear error when no constraints module is found', async () => {
264
+ const io = bufferIo();
265
+ const code = await runDatabaseRulesGenerate(serviceArgs(['database', 'rules', 'generate']), {
266
+ ...io,
267
+ cwd: '/project',
268
+ loadRulesDocument: (async () => ({
269
+ ok: false,
270
+ message: 'no constraints module found',
271
+ })) as never,
272
+ });
273
+
274
+ expect(code).toBe(1);
275
+ expect(io.getErr()).toContain('no constraints module found');
276
+ });
277
+
278
+ it('honors --config and --out flags', async () => {
279
+ const { defineRtdbRules, allow, deny } = await import('pyric/rules');
280
+ const doc = defineRtdbRules({ paths: { '/': { read: deny(), write: deny() } } });
281
+
282
+ const io = bufferIo();
283
+ let loadedPath: string | undefined;
284
+ const writes: Array<{ path: unknown }> = [];
285
+ const code = await runDatabaseRulesGenerate(
286
+ serviceArgs(['database', 'rules', 'generate', '--config', 'rtdb.rules.ts', '--out', 'out/rules.json']),
287
+ {
288
+ ...io,
289
+ cwd: '/project',
290
+ loadRulesDocument: (async (configPath: string) => {
291
+ loadedPath = configPath;
292
+ return { ok: true, document: doc };
293
+ }) as never,
294
+ mkdir: (async () => undefined) as never,
295
+ writeFile: (async (path: unknown) => {
296
+ writes.push({ path });
297
+ }) as never,
298
+ },
299
+ );
300
+
301
+ expect(code).toBe(0);
302
+ expect(loadedPath).toBe('rtdb.rules.ts');
303
+ expect(writes[0]?.path).toBe('/project/out/rules.json');
304
+ });
305
+ });
306
+
307
+ // ── init ──────────────────────────────────────────────────────────────
308
+
309
+ describe('runInit', () => {
310
+ it('writes the full local-first Pyric scaffold when none exist', async () => {
311
+ const io = bufferIo();
312
+ const writeFn = mock(async () => undefined);
313
+ const mkdirFn = mock(async () => undefined);
314
+ const code = await runInit(parseArgs(['init', '--template=node']), {
315
+ ...io,
316
+ cwd: '/tmp/scaffold',
317
+ writeFile: writeFn as never,
318
+ mkdir: mkdirFn as never,
319
+ exists: async () => false,
320
+ });
321
+ expect(code).toBe(0);
322
+ // 9 files: package.json, src/app.ts, .env.example, src/seed.ts,
323
+ // firestore.rules, firebase.json, firestore.indexes.json, README.md,
324
+ // .gitignore
325
+ expect(writeFn).toHaveBeenCalledTimes(9);
326
+ const paths = writeFn.mock.calls.map((c) => c[0]);
327
+ expect(paths).toContain('/tmp/scaffold/package.json');
328
+ expect(paths).toContain('/tmp/scaffold/src/app.ts');
329
+ expect(paths).toContain('/tmp/scaffold/src/seed.ts');
330
+ expect(paths).toContain('/tmp/scaffold/firestore.rules');
331
+ expect(paths).toContain('/tmp/scaffold/firebase.json');
332
+ expect(paths).toContain('/tmp/scaffold/firestore.indexes.json');
333
+ expect(paths).toContain('/tmp/scaffold/README.md');
334
+ expect(paths).toContain('/tmp/scaffold/.gitignore');
335
+ // src/ dir is mkdir'd before writing into it
336
+ expect(mkdirFn).toHaveBeenCalledWith('/tmp/scaffold/src', { recursive: true });
337
+ // package.json uses the directory basename as the project name
338
+ const pkgCall = writeFn.mock.calls.find((c) => c[0] === '/tmp/scaffold/package.json');
339
+ expect(pkgCall?.[1]).toContain('"name": "scaffold"');
340
+ // App template stays canonical; the dev command owns package resolution.
341
+ const appCall = writeFn.mock.calls.find((c) => c[0] === '/tmp/scaffold/src/app.ts');
342
+ expect(appCall?.[1]).toContain("from 'firebase/app'");
343
+ expect(appCall?.[1]).toContain("from 'firebase/firestore'");
344
+ expect(appCall?.[1]).not.toContain("from 'pyric/");
345
+ expect(appCall?.[1]).toContain("projectId: process.env.FIREBASE_PROJECT_ID ?? 'pyric-local'");
346
+ expect(appCall?.[1]).not.toContain('initializeApp({ sandbox:');
347
+ });
348
+
349
+ it('honors --name override for the package name', async () => {
350
+ const io = bufferIo();
351
+ const writeFn = mock(async () => undefined);
352
+ const code = await runInit(parseArgs(['init', '--name=my-app']), {
353
+ ...io,
354
+ cwd: '/tmp/scaffold-named',
355
+ writeFile: writeFn as never,
356
+ mkdir: mock(async () => undefined) as never,
357
+ exists: async () => false,
358
+ });
359
+ expect(code).toBe(0);
360
+ const pkgCall = writeFn.mock.calls.find((c) => c[0] === '/tmp/scaffold-named/package.json');
361
+ expect(pkgCall?.[1]).toContain('"name": "my-app"');
362
+ });
363
+
364
+ it('skips files that already exist (non-package.json)', async () => {
365
+ const io = bufferIo();
366
+ const writeFn = mock(async () => undefined);
367
+ const code = await runInit(parseArgs(['init', '--template=node']), {
368
+ ...io,
369
+ cwd: '/tmp/scaffold2',
370
+ writeFile: writeFn as never,
371
+ mkdir: mock(async () => undefined) as never,
372
+ exists: async (p) => p.endsWith('firebase.json'),
373
+ });
374
+ expect(code).toBe(0);
375
+ // 1 package.json + 7 non-package.json files written, firebase.json skipped
376
+ expect(writeFn).toHaveBeenCalledTimes(8);
377
+ expect(io.getOut()).toContain('skipped firebase.json');
378
+ });
379
+
380
+ it('merges into an existing package.json — adds missing scripts + deps, never overwrites', async () => {
381
+ const io = bufferIo();
382
+ const writeFn = mock(async () => undefined);
383
+ const existingPkg = JSON.stringify(
384
+ {
385
+ name: 'my-existing-app',
386
+ type: 'module',
387
+ scripts: { test: 'jest', start: 'node existing.js' },
388
+ dependencies: { express: '^4.0.0' },
389
+ },
390
+ null,
391
+ 2,
392
+ );
393
+ const code = await runInit(parseArgs(['init', '--template=node']), {
394
+ ...io,
395
+ cwd: '/tmp/merge-test',
396
+ writeFile: writeFn as never,
397
+ readFile: mock(async () => existingPkg) as never,
398
+ mkdir: mock(async () => undefined) as never,
399
+ // Only package.json exists; all others don't.
400
+ exists: async (p) => p.endsWith('package.json'),
401
+ });
402
+ expect(code).toBe(0);
403
+ const pkgCall = writeFn.mock.calls.find((c) => c[0] === '/tmp/merge-test/package.json');
404
+ expect(pkgCall).toBeDefined();
405
+ const written = JSON.parse(pkgCall![1] as string);
406
+
407
+ // User's existing values preserved.
408
+ expect(written.name).toBe('my-existing-app');
409
+ expect(written.scripts.test).toBe('jest');
410
+ expect(written.scripts.start).toBe('node existing.js'); // NOT overwritten
411
+ expect(written.dependencies.express).toBe('^4.0.0');
412
+
413
+ // Missing Pyric development scripts + deps added.
414
+ expect(written.scripts.bridge).toBe('pyric bridge');
415
+ expect(written.scripts.dev).toBe('pyric dev --no-open -- node --env-file-if-exists=.env --experimental-strip-types src/app.ts');
416
+ expect(Object.keys(written.scripts).some((name) => name.startsWith('deploy'))).toBe(false);
417
+ expect(written.dependencies.firebase).toBe('^12.12.0');
418
+ expect(written.dependencies.pyric).toBeUndefined();
419
+ expect(written.devDependencies['@pyric/cli']).toBe('*');
420
+ expect(written.devDependencies.typescript).toBe('^5.7.0');
421
+
422
+ // User-facing report mentions the conflict on `start`.
423
+ const out = io.getOut();
424
+ expect(out).toContain('merged package.json');
425
+ expect(out).toContain('scripts.start');
426
+ expect(out).toContain('node existing.js');
427
+ });
428
+
429
+ it('reports unchanged when existing package.json already has everything', async () => {
430
+ const io = bufferIo();
431
+ const writeFn = mock(async () => undefined);
432
+ // A package.json that already contains every required field.
433
+ const completePkg = JSON.stringify(
434
+ {
435
+ name: 'complete',
436
+ type: 'module',
437
+ private: true,
438
+ scripts: {
439
+ start: 'node --env-file-if-exists=.env --experimental-strip-types src/app.ts',
440
+ dev: 'pyric dev --no-open -- node --env-file-if-exists=.env --experimental-strip-types src/app.ts',
441
+ bridge: 'pyric bridge',
442
+ },
443
+ dependencies: { firebase: '^12.12.0' },
444
+ devDependencies: { '@pyric/cli': '*', '@types/node': '^22.0.0', typescript: '^5.7.0' },
445
+ },
446
+ null,
447
+ 2,
448
+ );
449
+ const code = await runInit(parseArgs(['init', '--template=node']), {
450
+ ...io,
451
+ cwd: '/tmp/complete',
452
+ writeFile: writeFn as never,
453
+ readFile: mock(async () => completePkg) as never,
454
+ mkdir: mock(async () => undefined) as never,
455
+ exists: async (p) => p.endsWith('package.json'),
456
+ });
457
+ expect(code).toBe(0);
458
+ // package.json should NOT have been rewritten — 6 other files written.
459
+ const pkgCall = writeFn.mock.calls.find((c) => c[0] === '/tmp/complete/package.json');
460
+ expect(pkgCall).toBeUndefined();
461
+ expect(io.getOut()).toContain('skipped package.json'); // unchanged merge
462
+ });
463
+ });
464
+
465
+ describe('scanForInlinedFirebase', () => {
466
+ it('flags a bundled chunk that inlines real-SDK endpoint hosts', async () => {
467
+ const { scanForInlinedFirebase } = await import('./serve.js');
468
+ const { mkdtempSync, mkdirSync, writeFileSync } = await import('node:fs');
469
+ const { tmpdir } = await import('node:os');
470
+ const { join } = await import('node:path');
471
+ const dir = mkdtempSync(join(tmpdir(), 'pyric-inline-scan-'));
472
+ mkdirSync(join(dir, 'assets'));
473
+ writeFileSync(
474
+ join(dir, 'assets', 'index-abc.js'),
475
+ 'fetch("https://identitytoolkit.googleapis.com/v1/projects?key="+k)',
476
+ );
477
+ const hits = scanForInlinedFirebase(dir);
478
+ expect(hits).toEqual(['assets/index-abc.js']);
479
+ });
480
+
481
+ it('stays clean for an unbundled app importing firebase by bare specifier', async () => {
482
+ const { scanForInlinedFirebase } = await import('./serve.js');
483
+ const { mkdtempSync, writeFileSync } = await import('node:fs');
484
+ const { tmpdir } = await import('node:os');
485
+ const { join } = await import('node:path');
486
+ const dir = mkdtempSync(join(tmpdir(), 'pyric-inline-clean-'));
487
+ writeFileSync(
488
+ join(dir, 'main.js'),
489
+ "import { getAuth } from 'firebase/auth'; import { getFirestore } from 'firebase/firestore';",
490
+ );
491
+ expect(scanForInlinedFirebase(dir)).toEqual([]);
492
+ });
493
+ });