@pyric/cli 0.1.0-alpha.12 → 0.1.0-alpha.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/dist/cli/serve.js +4 -4
  2. package/dist/cli/serve.js.map +1 -1
  3. package/dist/serve/bundler.d.ts +13 -8
  4. package/dist/serve/bundler.d.ts.map +1 -1
  5. package/dist/serve/bundler.js +31 -36
  6. package/dist/serve/bundler.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 +1 -1
  13. package/dist/serve/docs-ui/docs/app-compat/index.html +1 -1
  14. package/dist/serve/docs-ui/docs/auth-compat/index.html +1 -1
  15. package/dist/serve/docs-ui/docs/build/ai-logic/index.html +44 -22
  16. package/dist/serve/docs-ui/docs/build/ai-logic.md +69 -21
  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 +2 -1
  28. package/dist/serve/docs-ui/docs/get-started/how-the-swap-works.md +2 -0
  29. package/dist/serve/docs-ui/docs/get-started/start-building/index.html +3 -2
  30. package/dist/serve/docs-ui/docs/get-started/start-building.md +3 -1
  31. package/dist/serve/docs-ui/docs/index.json +149 -4
  32. package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
  33. package/dist/serve/docs-ui/docs/observe/resolve-runtime-status/index.html +28 -0
  34. package/dist/serve/docs-ui/docs/observe/resolve-runtime-status.md +47 -0
  35. package/dist/serve/docs-ui/docs/observe/see-whats-happening/index.html +1 -1
  36. package/dist/serve/docs-ui/docs/observe/shape-your-data/index.html +1 -1
  37. package/dist/serve/docs-ui/docs/overview/index.html +1 -1
  38. package/dist/serve/docs-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
  39. package/dist/serve/docs-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
  40. package/dist/serve/docs-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
  41. package/dist/serve/docs-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
  42. package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api/index.html +1 -1
  43. package/dist/serve/docs-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
  44. package/dist/serve/docs-ui/docs/pyric-ai-reference-api/index.html +1 -1
  45. package/dist/serve/docs-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
  46. package/dist/serve/docs-ui/docs/pyric-app-reference-api/index.html +1 -1
  47. package/dist/serve/docs-ui/docs/pyric-auth-reference-api/index.html +1 -1
  48. package/dist/serve/docs-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
  49. package/dist/serve/docs-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
  50. package/dist/serve/docs-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
  51. package/dist/serve/docs-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
  52. package/dist/serve/docs-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
  53. package/dist/serve/docs-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
  54. package/dist/serve/docs-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
  55. package/dist/serve/docs-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
  56. package/dist/serve/docs-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
  57. package/dist/serve/docs-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
  58. package/dist/serve/docs-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
  59. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api/index.html +38 -11
  60. package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +26 -8
  61. package/dist/serve/docs-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
  62. package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api/index.html +11 -8
  63. package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api.md +17 -4
  64. package/dist/serve/docs-ui/docs/pyric-database-reference-api/index.html +1 -1
  65. package/dist/serve/docs-ui/docs/pyric-firestore-reference-api/index.html +1 -1
  66. package/dist/serve/docs-ui/docs/pyric-messaging-reference-api/index.html +1 -1
  67. package/dist/serve/docs-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
  68. package/dist/serve/docs-ui/docs/pyric-rules-reference-api/index.html +1 -1
  69. package/dist/serve/docs-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
  70. package/dist/serve/docs-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
  71. package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api/index.html +1 -1
  72. package/dist/serve/docs-ui/docs/pyric-storage-reference-api/index.html +1 -1
  73. package/dist/serve/docs-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
  74. package/dist/serve/docs-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
  75. package/dist/serve/docs-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
  76. package/dist/serve/docs-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
  77. package/dist/serve/docs-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
  78. package/dist/serve/docs-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
  79. package/dist/serve/docs-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
  80. package/dist/serve/docs-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
  81. package/dist/serve/docs-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
  82. package/dist/serve/docs-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
  83. package/dist/serve/docs-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
  84. package/dist/serve/docs-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
  85. package/dist/serve/docs-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
  86. package/dist/serve/docs-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
  87. package/dist/serve/docs-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
  88. package/dist/serve/docs-ui/docs/rules-compat/index.html +1 -1
  89. package/dist/serve/docs-ui/docs/secure/audit-your-rules/index.html +1 -1
  90. package/dist/serve/docs-ui/docs/secure/firestore-rules-limits/index.html +1 -1
  91. package/dist/serve/docs-ui/docs/secure/read-a-denial/index.html +1 -1
  92. package/dist/serve/docs-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
  93. package/dist/serve/docs-ui/docs/secure/rules-standard-library/index.html +1 -1
  94. package/dist/serve/docs-ui/docs/secure/secure-it-with-rules/index.html +1 -1
  95. package/dist/serve/docs-ui/docs/secure/simulate-and-lint/index.html +1 -1
  96. package/dist/serve/docs-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
  97. package/dist/serve/docs-ui/docs/ship/ship-to-production/index.html +1 -1
  98. package/dist/serve/docs-ui/docs/ship/test-in-node/index.html +1 -1
  99. package/dist/serve/docs-ui/docs/storage-compat/index.html +1 -1
  100. package/dist/serve/docs-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
  101. package/dist/serve/docs-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
  102. package/dist/serve/docs-ui/docs/tutorial/index.html +429 -0
  103. package/dist/serve/docs-ui/docs/tutorial.md +701 -0
  104. package/dist/serve/docs-ui/llms.txt +2 -0
  105. package/dist/serve/entries/init.d.ts.map +1 -1
  106. package/dist/serve/entries/init.js +3 -0
  107. package/dist/serve/entries/init.js.map +1 -1
  108. package/dist/serve/entries/runtime.d.ts.map +1 -1
  109. package/dist/serve/entries/runtime.js +20 -5
  110. package/dist/serve/entries/runtime.js.map +1 -1
  111. package/dist/serve/entries/worker-runtime.d.ts +2 -2
  112. package/dist/serve/entries/worker-runtime.d.ts.map +1 -1
  113. package/dist/serve/entries/worker-runtime.js +45 -10
  114. package/dist/serve/entries/worker-runtime.js.map +1 -1
  115. package/dist/serve/runtime/chip-config.d.ts +19 -0
  116. package/dist/serve/runtime/chip-config.d.ts.map +1 -0
  117. package/dist/serve/runtime/chip-config.js +19 -0
  118. package/dist/serve/runtime/chip-config.js.map +1 -0
  119. package/dist/serve/runtime/chip-install.d.ts +10 -0
  120. package/dist/serve/runtime/chip-install.d.ts.map +1 -0
  121. package/dist/serve/runtime/chip-install.js +15 -0
  122. package/dist/serve/runtime/chip-install.js.map +1 -0
  123. package/dist/serve/runtime/chip.d.ts +17 -0
  124. package/dist/serve/runtime/chip.d.ts.map +1 -0
  125. package/dist/serve/runtime/chip.js +257 -0
  126. package/dist/serve/runtime/chip.js.map +1 -0
  127. package/dist/serve/runtime/manifest.d.ts +22 -0
  128. package/dist/serve/runtime/manifest.d.ts.map +1 -0
  129. package/dist/serve/runtime/manifest.js +20 -0
  130. package/dist/serve/runtime/manifest.js.map +1 -0
  131. package/dist/serve/runtime/status.d.ts +49 -0
  132. package/dist/serve/runtime/status.d.ts.map +1 -0
  133. package/dist/serve/runtime/status.js +164 -0
  134. package/dist/serve/runtime/status.js.map +1 -0
  135. package/dist/serve/runtime/worker-connection.d.ts +4 -0
  136. package/dist/serve/runtime/worker-connection.d.ts.map +1 -0
  137. package/dist/serve/runtime/worker-connection.js +11 -0
  138. package/dist/serve/runtime/worker-connection.js.map +1 -0
  139. package/dist/serve/runtime/worker-generation.d.ts +14 -0
  140. package/dist/serve/runtime/worker-generation.d.ts.map +1 -0
  141. package/dist/serve/runtime/worker-generation.js +56 -0
  142. package/dist/serve/runtime/worker-generation.js.map +1 -0
  143. package/dist/serve/runtime/worker-replacement.d.ts +18 -0
  144. package/dist/serve/runtime/worker-replacement.d.ts.map +1 -0
  145. package/dist/serve/runtime/worker-replacement.js +29 -0
  146. package/dist/serve/runtime/worker-replacement.js.map +1 -0
  147. package/dist/serve/standalone-assets.d.ts +2 -2
  148. package/dist/serve/standalone-assets.d.ts.map +1 -1
  149. package/dist/serve/standalone-assets.js +1 -1
  150. package/dist/serve/standalone-assets.js.map +1 -1
  151. package/dist/serve/studio-ui/assets/{RulesCodeEditor-CxVYcZXw.js → RulesCodeEditor-BDlH-5lp.js} +1 -1
  152. package/dist/serve/studio-ui/assets/{index-cpSfgQ7H.js → index-Bf9SSE8L.js} +5 -5
  153. package/dist/serve/studio-ui/assets/{seed-DeuFKi4J.js → seed-D_UjxfOU.js} +1 -1
  154. package/dist/serve/studio-ui/index.html +1 -1
  155. package/dist/serve/vite-ai-config.d.ts +25 -0
  156. package/dist/serve/vite-ai-config.d.ts.map +1 -0
  157. package/dist/serve/vite-ai-config.js +45 -0
  158. package/dist/serve/vite-ai-config.js.map +1 -0
  159. package/dist/serve/vite-plugin.d.ts +17 -36
  160. package/dist/serve/vite-plugin.d.ts.map +1 -1
  161. package/dist/serve/vite-plugin.js +38 -57
  162. package/dist/serve/vite-plugin.js.map +1 -1
  163. package/dist/serve/vite-rules-source.d.ts +8 -0
  164. package/dist/serve/vite-rules-source.d.ts.map +1 -0
  165. package/dist/serve/vite-rules-source.js +23 -0
  166. package/dist/serve/vite-rules-source.js.map +1 -0
  167. package/dist/serve/vite-worker-runtime.d.ts +20 -0
  168. package/dist/serve/vite-worker-runtime.d.ts.map +1 -0
  169. package/dist/serve/vite-worker-runtime.js +25 -0
  170. package/dist/serve/vite-worker-runtime.js.map +1 -0
  171. package/dist/serve/worker/client/connection.d.ts +7 -2
  172. package/dist/serve/worker/client/connection.d.ts.map +1 -1
  173. package/dist/serve/worker/client/connection.js +13 -16
  174. package/dist/serve/worker/client/connection.js.map +1 -1
  175. package/dist/serve/worker/client/core.d.ts +7 -0
  176. package/dist/serve/worker/client/core.d.ts.map +1 -1
  177. package/dist/serve/worker/client/core.js +28 -0
  178. package/dist/serve/worker/client/core.js.map +1 -1
  179. package/dist/serve/worker/client/runtime-control.d.ts +8 -0
  180. package/dist/serve/worker/client/runtime-control.d.ts.map +1 -0
  181. package/dist/serve/worker/client/runtime-control.js +11 -0
  182. package/dist/serve/worker/client/runtime-control.js.map +1 -0
  183. package/dist/serve/worker/entry.js +44 -2
  184. package/dist/serve/worker/entry.js.map +1 -1
  185. package/dist/serve/worker/host/studio.js +1 -1
  186. package/dist/serve/worker/host/studio.js.map +1 -1
  187. package/dist/serve/worker/host-context.d.ts +1 -1
  188. package/dist/serve/worker/host-context.d.ts.map +1 -1
  189. package/dist/serve/worker/protocol.d.ts +15 -1
  190. package/dist/serve/worker/protocol.d.ts.map +1 -1
  191. package/dist/serve/worker/protocol.js.map +1 -1
  192. package/dist/serve/worker/retirement.d.ts +21 -0
  193. package/dist/serve/worker/retirement.d.ts.map +1 -0
  194. package/dist/serve/worker/retirement.js +97 -0
  195. package/dist/serve/worker/retirement.js.map +1 -0
  196. package/dist/serve/worker/serve-init.d.ts.map +1 -1
  197. package/dist/serve/worker/serve-init.js +5 -6
  198. package/dist/serve/worker/serve-init.js.map +1 -1
  199. package/dist/vite.d.ts +1 -0
  200. package/dist/vite.d.ts.map +1 -1
  201. package/dist/vite.js.map +1 -1
  202. package/package.json +5 -4
  203. package/src/cli/serve.ts +4 -4
  204. package/src/serve/bundler.ts +41 -15
  205. package/src/serve/entries/init.ts +3 -0
  206. package/src/serve/entries/runtime.ts +23 -7
  207. package/src/serve/entries/worker-runtime.ts +69 -9
  208. package/src/serve/runtime/chip-config.ts +30 -0
  209. package/src/serve/runtime/chip-install.ts +27 -0
  210. package/src/serve/runtime/chip.ts +282 -0
  211. package/src/serve/runtime/manifest.ts +37 -0
  212. package/src/serve/runtime/status.ts +216 -0
  213. package/src/serve/runtime/worker-connection.ts +14 -0
  214. package/src/serve/runtime/worker-generation.ts +75 -0
  215. package/src/serve/runtime/worker-replacement.ts +42 -0
  216. package/src/serve/standalone-assets.ts +2 -2
  217. package/src/serve/vite-ai-config.ts +77 -0
  218. package/src/serve/vite-plugin.ts +60 -87
  219. package/src/serve/vite-rules-source.ts +28 -0
  220. package/src/serve/vite-worker-runtime.ts +51 -0
  221. package/src/serve/worker/client/connection.ts +29 -4
  222. package/src/serve/worker/client/core.ts +34 -0
  223. package/src/serve/worker/client/runtime-control.ts +22 -0
  224. package/src/serve/worker/entry.ts +46 -2
  225. package/src/serve/worker/host/studio.ts +1 -1
  226. package/src/serve/worker/host-context.ts +1 -1
  227. package/src/serve/worker/protocol.ts +13 -1
  228. package/src/serve/worker/retirement.ts +125 -0
  229. package/src/serve/worker/serve-init.ts +5 -6
  230. package/src/vite.ts +1 -0
@@ -3,28 +3,62 @@ import {
3
3
  getFirestore,
4
4
  getWorkerVersion,
5
5
  startPresence,
6
+ subscribeEvents,
6
7
  subscribePresence,
7
8
  type ClientDb,
8
9
  } from '../worker/client.js';
9
10
  import { getServiceWorkerFirestore } from '../worker/client/service-worker-connection.js';
10
11
  import { isServiceWorkerRealm } from '../worker/service-worker-channel.js';
12
+ import {
13
+ PYRIC_WORKER_NAME,
14
+ PYRIC_WORKER_URL,
15
+ } from '../runtime/manifest.js';
16
+ import { getPyricRuntimeStatus } from '../runtime/status.js';
17
+ import { connectRuntimeWorker } from '../runtime/worker-connection.js';
18
+ import { createWorkerReplacement } from '../runtime/worker-replacement.js';
19
+ import {
20
+ preflightWorkerEpochStorage,
21
+ rememberWorkerEpoch,
22
+ workerNameForEpoch,
23
+ } from '../runtime/worker-generation.js';
24
+ import {
25
+ onWorkerRuntimeReload,
26
+ retireWorkerRuntime,
27
+ } from '../worker/client/runtime-control.js';
11
28
 
12
29
  const hasSharedWorker = typeof SharedWorker !== 'undefined';
30
+ const runtimeStatus = getPyricRuntimeStatus();
13
31
 
14
- export const useWorker =
32
+ const workerRequested =
15
33
  (hasSharedWorker || (isServiceWorkerRealm() && typeof BroadcastChannel !== 'undefined'))
16
34
  && !(globalThis as { __PYRIC_FORCE_INPAGE__?: boolean }).__PYRIC_FORCE_INPAGE__;
17
35
 
18
- export const WORKER_URL = '/__pyric/sdk/worker.js';
19
- export const WORKER_NAME = 'pyric-shared-worker';
36
+ export const WORKER_URL = PYRIC_WORKER_URL;
37
+ let epochStorage: Storage | undefined;
38
+ try {
39
+ epochStorage = typeof localStorage === 'undefined' ? undefined : localStorage;
40
+ } catch {
41
+ epochStorage = undefined;
42
+ }
43
+ export const WORKER_NAME = workerNameForEpoch(
44
+ runtimeStatus.getSnapshot().servedEpoch,
45
+ epochStorage,
46
+ );
20
47
 
21
48
  /** Control traffic only; Firebase apps receive independent app-owned ports. */
22
- export const workerDb: ClientDb | null = useWorker
49
+ export const workerDb: ClientDb | null = workerRequested
23
50
  ? hasSharedWorker
24
- ? getFirestore(WORKER_URL, WORKER_NAME)
51
+ ? connectRuntimeWorker(
52
+ () => getFirestore(WORKER_URL, WORKER_NAME, {
53
+ onError: (error) => runtimeStatus.reportError(error, 'worker'),
54
+ }),
55
+ (error) => runtimeStatus.reportError(error, 'worker'),
56
+ )
25
57
  : null
26
58
  : null;
27
59
 
60
+ export const useWorker = workerRequested && (!hasSharedWorker || workerDb !== null);
61
+
28
62
  export function openWorkerDb(appName: string): ClientDb {
29
63
  if (hasSharedWorker) return getFirestore(WORKER_URL, WORKER_NAME);
30
64
  if (isServiceWorkerRealm()) return getServiceWorkerFirestore(appName);
@@ -35,12 +69,38 @@ export const presenceSession = useWorker && workerDb
35
69
  ? startPresence({ db: workerDb, kind: 'app' })
36
70
  : null;
37
71
 
72
+ runtimeStatus.setWorker({
73
+ mode: useWorker ? 'shared-worker' : 'in-page',
74
+ runningEpoch: null,
75
+ });
76
+
77
+ const workerReplacement = useWorker && workerDb && typeof window !== 'undefined'
78
+ ? createWorkerReplacement({
79
+ targetEpoch: runtimeStatus.getSnapshot().servedEpoch!,
80
+ retire: () => retireWorkerRuntime(
81
+ workerDb,
82
+ runtimeStatus.getSnapshot().servedEpoch!,
83
+ ),
84
+ subscribeReload: onWorkerRuntimeReload,
85
+ preflight: () => preflightWorkerEpochStorage(epochStorage),
86
+ commitGeneration: (epoch) => rememberWorkerEpoch(epoch, epochStorage),
87
+ onPreparationError: (error) => runtimeStatus.reportError(error, 'worker'),
88
+ reload: () => window.location.reload(),
89
+ })
90
+ : null;
91
+ runtimeStatus.setWorkerUpdater(
92
+ workerReplacement ? () => workerReplacement.request() : null,
93
+ );
94
+
95
+ if (useWorker && workerDb) {
96
+ subscribeEvents(workerDb, (events) => runtimeStatus.recordSandboxEvents(events));
97
+ }
98
+
38
99
  if (useWorker && typeof document !== 'undefined') {
39
- const servedVersion = document
40
- .querySelector('meta[name="pyric-worker-v"]')
41
- ?.getAttribute('content');
100
+ const servedVersion = runtimeStatus.getSnapshot().servedEpoch;
42
101
  void getWorkerVersion(workerDb!)
43
102
  .then(async (runningVersion) => {
103
+ runtimeStatus.setWorker({ mode: 'shared-worker', runningEpoch: runningVersion });
44
104
  if (
45
105
  !servedVersion
46
106
  || !runningVersion
@@ -71,5 +131,5 @@ if (useWorker && typeof document !== 'undefined') {
71
131
  + `the old code running for everyone.)${othersHint}`,
72
132
  );
73
133
  })
74
- .catch(() => {});
134
+ .catch((error) => runtimeStatus.reportError(error, 'worker'));
75
135
  }
@@ -0,0 +1,30 @@
1
+ export type PyricRuntimeChipOption = boolean | { initiallyOpen?: boolean };
2
+
3
+ export interface PyricRuntimeChipConfig {
4
+ initiallyOpen: boolean;
5
+ studioEnabled: boolean;
6
+ }
7
+
8
+ export const PYRIC_RUNTIME_CHIP_META = 'pyric-runtime-chip';
9
+
10
+ /** Encode the Vite option as declarative HTML consumed by the init entry. */
11
+ export function runtimeChipMetaValue(option: PyricRuntimeChipOption | undefined): string {
12
+ if (option === false) return 'off';
13
+ return typeof option === 'object' && option.initiallyOpen ? 'expanded' : 'collapsed';
14
+ }
15
+
16
+ interface RuntimeChipDocument {
17
+ querySelector(selector: string): { getAttribute(name: string): string | null } | null;
18
+ }
19
+
20
+ /** Read only plugin-authored values; absent/unknown metadata keeps the UI off. */
21
+ export function readPyricRuntimeChipConfig(
22
+ documentLike: RuntimeChipDocument,
23
+ ): PyricRuntimeChipConfig | null {
24
+ const meta = documentLike.querySelector(`meta[name="${PYRIC_RUNTIME_CHIP_META}"]`);
25
+ const value = meta?.getAttribute('content');
26
+ const studioEnabled = meta?.getAttribute('data-studio') !== 'off';
27
+ if (value === 'collapsed') return { initiallyOpen: false, studioEnabled };
28
+ if (value === 'expanded') return { initiallyOpen: true, studioEnabled };
29
+ return null;
30
+ }
@@ -0,0 +1,27 @@
1
+ import {
2
+ mountPyricRuntimeChip,
3
+ type PyricRuntimeChip,
4
+ type PyricRuntimeChipOptions,
5
+ } from './chip.js';
6
+ import { readPyricRuntimeChipConfig } from './chip-config.js';
7
+ import type { PyricRuntimeStatus } from './status.js';
8
+
9
+ export interface InstallPyricRuntimeChipOptions {
10
+ runtime: PyricRuntimeStatus;
11
+ document: Document;
12
+ mount?: (options: PyricRuntimeChipOptions) => PyricRuntimeChip;
13
+ }
14
+
15
+ /** Mount once when the Vite plugin opted this page into runtime UI. */
16
+ export function installPyricRuntimeChip(
17
+ options: InstallPyricRuntimeChipOptions,
18
+ ): PyricRuntimeChip | null {
19
+ const config = readPyricRuntimeChipConfig(options.document);
20
+ if (!config || options.document.querySelector('[data-pyric-runtime-chip-host]')) return null;
21
+ return (options.mount ?? mountPyricRuntimeChip)({
22
+ runtime: options.runtime,
23
+ document: options.document,
24
+ initiallyOpen: config.initiallyOpen,
25
+ ...(config.studioEnabled ? {} : { studioUrl: null }),
26
+ });
27
+ }
@@ -0,0 +1,282 @@
1
+ import type {
2
+ PyricRuntimeError,
3
+ PyricRuntimeSnapshot,
4
+ PyricRuntimeStatus,
5
+ } from './status.js';
6
+
7
+ export interface PyricRuntimeChipOptions {
8
+ runtime: PyricRuntimeStatus;
9
+ document?: Document;
10
+ clipboard?: Pick<Clipboard, 'writeText'>;
11
+ initiallyOpen?: boolean;
12
+ /** Override Studio availability. Omitted uses the runtime manifest URL. */
13
+ studioUrl?: string | null;
14
+ }
15
+
16
+ export interface PyricRuntimeChip {
17
+ element: HTMLElement;
18
+ dispose(): void;
19
+ }
20
+
21
+ const styles = `
22
+ :host {
23
+ --pyric-bg: #1e1e24;
24
+ --pyric-content: #16161a;
25
+ --pyric-border: #33333f;
26
+ --pyric-border-soft: #2a2a35;
27
+ --pyric-text: #fbfbfe;
28
+ --pyric-muted: #89899f;
29
+ --pyric-accent: #19cc61;
30
+ --pyric-warning: #e6c79c;
31
+ --pyric-error: #f0a0a0;
32
+ all: initial;
33
+ position: fixed;
34
+ right: max(20px, env(safe-area-inset-right));
35
+ bottom: max(20px, env(safe-area-inset-bottom));
36
+ z-index: 2147483000;
37
+ color: var(--pyric-text);
38
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
39
+ font-synthesis: none;
40
+ }
41
+ *, *::before, *::after { box-sizing: border-box; }
42
+ .announcer { height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; clip: rect(0 0 0 0); white-space: nowrap; }
43
+ button, a { font: inherit; }
44
+ button { margin: 0; }
45
+ .chip {
46
+ align-items: center;
47
+ background: var(--pyric-bg);
48
+ border: 1px solid var(--pyric-border);
49
+ border-radius: 999px;
50
+ box-shadow: 0 12px 34px rgba(0, 0, 0, .38);
51
+ color: var(--pyric-text);
52
+ cursor: pointer;
53
+ display: flex;
54
+ gap: 10px;
55
+ height: 36px;
56
+ padding: 0 12px;
57
+ }
58
+ .chip:hover { border-color: #4a4a58; }
59
+ .brand, .signals, .signal, .panel-title, .worker-state { align-items: center; display: flex; }
60
+ .brand { gap: 8px; }
61
+ .brand-mark { color: rgba(251, 251, 254, .78); font: 600 11px/1 ui-monospace, monospace; }
62
+ .brand-label, .signals, .worker-state, code, .button { font-family: "JetBrains Mono", ui-monospace, monospace; }
63
+ .brand-label { font-size: 11px; }
64
+ .signals { color: var(--pyric-muted); font-size: 10px; gap: 8px; }
65
+ .signal { gap: 4px; white-space: nowrap; }
66
+ .dot { background: var(--pyric-accent); border-radius: 50%; height: 8px; width: 8px; }
67
+ .dot.error { background: var(--pyric-error); box-shadow: 0 0 0 3px rgba(240,160,160,.12); }
68
+ .signal.update { color: var(--pyric-warning); }
69
+ .chevron { color: var(--pyric-muted); height: 14px; width: 14px; }
70
+ .panel {
71
+ background: var(--pyric-bg);
72
+ border: 1px solid var(--pyric-border);
73
+ border-radius: 8px;
74
+ box-shadow: 0 18px 60px rgba(0, 0, 0, .48);
75
+ max-width: calc(100vw - 40px);
76
+ overflow: hidden;
77
+ width: 380px;
78
+ }
79
+ .panel-header { align-items: center; display: flex; height: 44px; justify-content: space-between; padding: 0 12px; }
80
+ .panel-title { gap: 8px; min-width: 0; }
81
+ .panel-title strong { font: 500 12px/1 ui-monospace, monospace; }
82
+ .count { background: rgba(58,42,42,.3); border: 1px solid #3a2a2a; border-radius: 999px; color: var(--pyric-error); font: 9px/1 ui-monospace, monospace; padding: 4px 6px; }
83
+ .icon-button { align-items: center; background: transparent; border: 0; border-radius: 4px; color: var(--pyric-muted); cursor: pointer; display: inline-flex; height: 28px; justify-content: center; padding: 0; width: 28px; }
84
+ .icon-button:hover { background: rgba(255,255,255,.05); color: var(--pyric-text); }
85
+ .icon-button:disabled { cursor: not-allowed; opacity: .42; }
86
+ .icon-button[data-copy-failed] { color: var(--pyric-error); }
87
+ .icon { height: 15px; width: 15px; }
88
+ .worker-state { border-top: 1px solid var(--pyric-border-soft); color: var(--pyric-muted); font-size: 10px; justify-content: space-between; min-height: 34px; padding: 7px 12px; }
89
+ .worker-state .available { color: var(--pyric-warning); }
90
+ .worker-state .state-label { align-items: center; display: flex; gap: 7px; }
91
+ .mini-dot { background: var(--pyric-accent); border-radius: 50%; height: 6px; width: 6px; }
92
+ .available .mini-dot { background: var(--pyric-warning); }
93
+ .epochs { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
94
+ .errors { background: var(--pyric-content); border-bottom: 1px solid var(--pyric-border-soft); border-top: 1px solid var(--pyric-border-soft); max-height: 184px; min-height: 58px; overflow-y: auto; }
95
+ .errors::-webkit-scrollbar { width: 8px; }
96
+ .errors::-webkit-scrollbar-thumb { background: #33333f; border-radius: 4px; }
97
+ .error-row { align-items: flex-start; border-bottom: 1px solid rgba(42,42,53,.7); display: grid; gap: 8px; grid-template-columns: 18px minmax(0,1fr) 28px; padding: 10px 8px 10px 12px; }
98
+ .error-row:last-child { border-bottom: 0; }
99
+ .error-number { color: var(--pyric-error); font: 10px/20px ui-monospace, monospace; }
100
+ .error-body { min-width: 0; }
101
+ .error-body code { color: #d7d7df; display: block; font-size: 11px; line-height: 1.55; overflow-wrap: anywhere; white-space: pre-wrap; }
102
+ .error-meta { color: var(--pyric-muted); font: 9px/1.4 ui-monospace, monospace; margin-top: 4px; overflow-wrap: anywhere; }
103
+ .empty { align-items: center; color: var(--pyric-muted); display: flex; font: 11px/1.5 ui-monospace, monospace; min-height: 57px; padding: 12px; }
104
+ .actions { display: grid; gap: 8px; grid-template-columns: 1fr 1fr; min-height: 56px; padding: 10px 12px; }
105
+ .button { align-items: center; background: transparent; border: 1px solid var(--pyric-border-soft); border-radius: 4px; color: var(--pyric-muted); display: inline-flex; font-size: 10px; justify-content: center; letter-spacing: .06em; min-height: 34px; padding: 6px 8px; text-decoration: none; text-transform: uppercase; }
106
+ button.button { cursor: pointer; }
107
+ .button:hover:not(:disabled):not([aria-disabled="true"]), a.button:hover { border-color: #3a3a48; color: var(--pyric-text); }
108
+ .button.update:not(:disabled):not([aria-disabled="true"]) { background: rgba(230,199,156,.1); border-color: rgba(230,199,156,.4); color: var(--pyric-warning); }
109
+ .button.update:not(:disabled):not([aria-disabled="true"]):hover { background: rgba(230,199,156,.15); }
110
+ .button:disabled, .button[aria-disabled="true"] { cursor: not-allowed; opacity: .42; }
111
+ .button svg { height: 14px; margin-left: 6px; width: 14px; }
112
+ @media (max-width: 460px) {
113
+ :host { bottom: max(12px, env(safe-area-inset-bottom)); right: 12px; }
114
+ .panel { max-width: calc(100vw - 24px); }
115
+ .worker-state { align-items: flex-start; flex-direction: column; gap: 4px; }
116
+ }
117
+ @media (prefers-reduced-motion: no-preference) {
118
+ .chip, .panel { animation: pyric-enter 120ms ease-out; transform-origin: bottom right; }
119
+ @keyframes pyric-enter { from { opacity: 0; transform: translateY(4px) scale(.98); } }
120
+ }
121
+ `;
122
+
123
+ const icons = {
124
+ close: '<svg class="icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="m6 6 12 12M18 6 6 18"/></svg>',
125
+ copy: '<svg class="icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="8" y="8" width="11" height="11" rx="1"/><path d="M16 8V5H5v11h3"/></svg>',
126
+ chevron: '<svg class="chevron" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="m6 15 6-6 6 6"/></svg>',
127
+ external: '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 5h5v5M19 5l-8 8"/><path d="M19 13v6H5V5h6"/></svg>',
128
+ };
129
+
130
+ function escapeAttribute(value: string): string {
131
+ return value.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;');
132
+ }
133
+
134
+ export function formatPyricRuntimeError(error: PyricRuntimeError): string {
135
+ const context = [
136
+ error.source,
137
+ error.service && error.method ? `${error.service}.${error.method}` : error.service ?? error.method,
138
+ error.path,
139
+ error.code,
140
+ ].filter(Boolean).join(' · ');
141
+ return `${error.message}${context ? `\n${context}` : ''}${error.stack ? `\n${error.stack}` : ''}`;
142
+ }
143
+
144
+ function renderErrors(snapshot: PyricRuntimeSnapshot, canCopy: boolean): string {
145
+ if (snapshot.errors.length === 0) return '<div class="empty">No sandbox errors.</div>';
146
+ return snapshot.errors.map((error, index) => `
147
+ <div class="error-row" data-error-id="${escapeAttribute(error.id)}">
148
+ <span class="error-number">${String(index + 1).padStart(2, '0')}</span>
149
+ <div class="error-body"><code></code><div class="error-meta"></div></div>
150
+ <button class="icon-button" type="button" data-copy-error="${escapeAttribute(error.id)}" aria-label="${canCopy ? `Copy error ${index + 1}` : 'Copy unavailable'}" title="${canCopy ? 'Copy error' : 'Clipboard unavailable'}" ${canCopy ? '' : 'disabled'}>${icons.copy}</button>
151
+ </div>
152
+ `).join('');
153
+ }
154
+
155
+ /** Mount the framework-independent runtime chip in an isolated shadow root. */
156
+ export function mountPyricRuntimeChip(options: PyricRuntimeChipOptions): PyricRuntimeChip {
157
+ const documentLike = options.document ?? document;
158
+ const host = documentLike.createElement('div');
159
+ host.setAttribute('data-pyric-runtime-chip-host', '');
160
+ const root = host.attachShadow({ mode: 'open' });
161
+ root.innerHTML = `<style>${styles}</style><div class="announcer" role="status" aria-live="polite" aria-atomic="true"></div><div data-view></div>`;
162
+ const view = root.querySelector<HTMLElement>('[data-view]')!;
163
+ const announcer = root.querySelector<HTMLElement>('.announcer')!;
164
+ const clipboard = options.clipboard
165
+ ?? documentLike.defaultView?.navigator.clipboard;
166
+ const studioUrl = 'studioUrl' in options
167
+ ? options.studioUrl
168
+ : options.runtime.getSnapshot().manifest.studioUrl;
169
+ let open = options.initiallyOpen ?? false;
170
+ let snapshot = options.runtime.getSnapshot();
171
+
172
+ const render = (next = snapshot): void => {
173
+ const active = root.activeElement;
174
+ const oldViewport = view.querySelector<HTMLElement>('[data-error-viewport]');
175
+ const oldScroll = oldViewport
176
+ ? {
177
+ top: oldViewport.scrollTop,
178
+ atBottom: oldViewport.scrollHeight - oldViewport.scrollTop - oldViewport.clientHeight <= 8,
179
+ }
180
+ : null;
181
+ const activeCopyId = active?.getAttribute('data-copy-error');
182
+ const activeControl = ['data-expand', 'data-collapse', 'data-update-worker', 'data-open-studio']
183
+ .find((attribute) => active?.hasAttribute(attribute));
184
+ const focusToken = activeCopyId !== null && activeCopyId !== undefined
185
+ ? { attribute: 'data-copy-error', value: activeCopyId }
186
+ : activeControl
187
+ ? { attribute: activeControl, value: null }
188
+ : null;
189
+ snapshot = next;
190
+ const errorCount = snapshot.errors.length;
191
+ const workerLabel = snapshot.updateAvailable
192
+ ? 'New worker available'
193
+ : snapshot.mode === 'starting'
194
+ ? 'Sandbox starting'
195
+ : snapshot.mode === 'in-page'
196
+ ? 'In-page sandbox'
197
+ : 'Worker current';
198
+ const epochs = snapshot.updateAvailable
199
+ ? `${snapshot.runningEpoch?.slice(0, 8) ?? 'unknown'} → ${snapshot.servedEpoch?.slice(0, 8) ?? 'unknown'}`
200
+ : snapshot.runningEpoch?.slice(0, 8) ?? '';
201
+
202
+ view.innerHTML = `${open ? `
203
+ <section class="panel" role="dialog" aria-label="Pyric runtime">
204
+ <header class="panel-header">
205
+ <div class="panel-title"><span class="brand-mark">&gt;_</span><strong>Pyric runtime</strong>${errorCount > 0 ? `<span class="count">${errorCount} ${errorCount === 1 ? 'error' : 'errors'}</span>` : ''}</div>
206
+ <button class="icon-button" type="button" data-collapse aria-label="Collapse Pyric runtime">${icons.close}</button>
207
+ </header>
208
+ <div class="worker-state"><span class="state-label${snapshot.updateAvailable ? ' available' : ''}"><span class="mini-dot"></span>${workerLabel}</span><span class="epochs">${epochs}</span></div>
209
+ <div class="errors" data-error-viewport>${renderErrors(snapshot, Boolean(clipboard))}</div>
210
+ <div class="actions">
211
+ <button class="button update" type="button" data-update-worker ${snapshot.updateAvailable ? '' : 'disabled'} aria-disabled="${snapshot.updateAvailable && !snapshot.updatingWorker ? 'false' : 'true'}">${snapshot.updatingWorker ? 'Updating…' : 'Update worker'}</button>
212
+ ${studioUrl
213
+ ? `<a class="button" data-open-studio href="${escapeAttribute(studioUrl)}" target="_blank" rel="noopener noreferrer">Studio${icons.external}</a>`
214
+ : `<span class="button" data-open-studio aria-disabled="true" title="Pyric Studio is disabled">Studio${icons.external}</span>`}
215
+ </div>
216
+ </section>
217
+ ` : `
218
+ <button class="chip" type="button" data-expand aria-label="Open Pyric runtime" aria-expanded="false">
219
+ <span class="brand"><span class="dot${errorCount > 0 ? ' error' : ''}"></span><span class="brand-label">Pyric</span></span>
220
+ <span class="signals">${snapshot.updateAvailable ? '<span class="signal update">update</span>' : ''}${errorCount > 0 ? `<span class="signal">${errorCount} ${errorCount === 1 ? 'error' : 'errors'}</span>` : '<span class="signal">ready</span>'}${icons.chevron}</span>
221
+ </button>
222
+ `}`;
223
+
224
+ const announcement = `${workerLabel}. ${errorCount === 0 ? 'No runtime errors' : `${errorCount} runtime ${errorCount === 1 ? 'error' : 'errors'}`}.`;
225
+ if (announcer.textContent !== announcement) announcer.textContent = announcement;
226
+ const newViewport = view.querySelector<HTMLElement>('[data-error-viewport]');
227
+ if (oldScroll && newViewport) {
228
+ newViewport.scrollTop = oldScroll.atBottom ? newViewport.scrollHeight : oldScroll.top;
229
+ }
230
+
231
+ for (const error of snapshot.errors) {
232
+ const row = [...root.querySelectorAll('[data-error-id]')]
233
+ .find((candidate) => candidate.getAttribute('data-error-id') === error.id);
234
+ const code = row?.querySelector('code');
235
+ const meta = row?.querySelector('.error-meta');
236
+ if (code) code.textContent = error.message;
237
+ if (meta) meta.textContent = [error.source, error.service && error.method ? `${error.service}.${error.method}` : error.service ?? error.method, error.path, error.code].filter(Boolean).join(' · ');
238
+ }
239
+
240
+ root.querySelector('[data-expand]')?.addEventListener('click', () => {
241
+ open = true;
242
+ render();
243
+ root.querySelector<HTMLButtonElement>('[data-collapse]')?.focus();
244
+ });
245
+ root.querySelector('[data-collapse]')?.addEventListener('click', () => {
246
+ open = false;
247
+ render();
248
+ root.querySelector<HTMLButtonElement>('[data-expand]')?.focus();
249
+ });
250
+ root.querySelector('[data-update-worker]')?.addEventListener('click', () => {
251
+ if (!snapshot.updateAvailable || snapshot.updatingWorker) return;
252
+ void options.runtime.updateWorker().catch(() => { /* status records and renders the failure */ });
253
+ });
254
+ for (const button of root.querySelectorAll<HTMLButtonElement>('[data-copy-error]')) {
255
+ button.addEventListener('click', () => {
256
+ const error = snapshot.errors.find((item) => item.id === button.dataset.copyError);
257
+ if (!error || !clipboard) return;
258
+ void clipboard.writeText(formatPyricRuntimeError(error)).catch(() => {
259
+ button.setAttribute('data-copy-failed', '');
260
+ button.setAttribute('aria-label', 'Copy failed');
261
+ button.title = 'Copy failed';
262
+ });
263
+ });
264
+ }
265
+ if (focusToken) {
266
+ const candidates = root.querySelectorAll<HTMLElement>(`[${focusToken.attribute}]`);
267
+ const replacement = [...candidates].find((candidate) =>
268
+ focusToken.value === null || candidate.getAttribute(focusToken.attribute) === focusToken.value);
269
+ replacement?.focus();
270
+ }
271
+ };
272
+
273
+ documentLike.body.append(host);
274
+ const unsubscribe = options.runtime.subscribe(render);
275
+ return {
276
+ element: host,
277
+ dispose() {
278
+ unsubscribe();
279
+ host.remove();
280
+ },
281
+ };
282
+ }
@@ -0,0 +1,37 @@
1
+ /** Stable routes and identity used by the served runtime and its UI. */
2
+ export const PYRIC_WORKER_URL = '/__pyric/sdk/worker.js';
3
+ export const PYRIC_WORKER_NAME = 'pyric-shared-worker';
4
+ export const PYRIC_STUDIO_URL = '/__pyric/ui/';
5
+
6
+ export interface PyricRuntimeManifest {
7
+ studioUrl: string;
8
+ worker: {
9
+ url: string;
10
+ name: string;
11
+ /** Content epoch served by the current dev server; null in in-page mode. */
12
+ servedEpoch: string | null;
13
+ };
14
+ }
15
+
16
+ interface RuntimeDocument {
17
+ querySelector(selector: string): { getAttribute(name: string): string | null } | null;
18
+ }
19
+
20
+ /** Read the server-stamped worker epoch before application code starts. */
21
+ export function readPyricRuntimeManifest(
22
+ documentLike: RuntimeDocument | undefined = typeof document === 'undefined' ? undefined : document,
23
+ ): PyricRuntimeManifest {
24
+ const servedEpoch = documentLike
25
+ ?.querySelector('meta[name="pyric-worker-v"]')
26
+ ?.getAttribute('content')
27
+ ?.trim() || null;
28
+
29
+ return {
30
+ studioUrl: PYRIC_STUDIO_URL,
31
+ worker: {
32
+ url: PYRIC_WORKER_URL,
33
+ name: PYRIC_WORKER_NAME,
34
+ servedEpoch,
35
+ },
36
+ };
37
+ }