@voltro/cli 0.51.0 → 0.52.0

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 (164) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/dist/{agentsMd-D6yD7IQv.js → agentsMd-0l980yhL.js} +4 -1
  3. package/dist/agentsMd-SDDSkyl4.js +2 -0
  4. package/dist/apiBuild-BYBpL7Pz.js +2 -0
  5. package/dist/{apiBuild-CPDHXF72.js → apiBuild-CSFI8QGq.js} +3 -3
  6. package/dist/bin.js +1 -1
  7. package/dist/build-CPgcMQug.js +793 -0
  8. package/dist/checkCommand-2SbqzukH.js +2 -0
  9. package/dist/{checkCommand-DNuPiWMc.js → checkCommand-COmqc2cB.js} +92 -46
  10. package/dist/{cloudCmd-F4YJeqM3.js → cloudCmd-QUXh-b5w.js} +1 -1
  11. package/dist/codegen-CctkDO-1.js +2 -0
  12. package/dist/{codegen-CrMXs4hb.js → codegen-VF479Cnb.js} +1 -1
  13. package/dist/{codegenCommand-BNBHcNNj.js → codegenCommand-DCdG2JN-.js} +12 -12
  14. package/dist/{codemodRunner-BDVixlSw.js → codemodRunner-r7J9lIa7.js} +588 -540
  15. package/dist/{commands-B1OiS9bX.js → commands-Cc_nV8WI.js} +35 -35
  16. package/dist/{dashboardCommand-C-vvPY1B.js → dashboardCommand-C-gKvwqh.js} +5 -5
  17. package/dist/{dataCommand-C1GxXW5q.js → dataCommand-BgpBHnlB.js} +27 -27
  18. package/dist/dbCommand-DNb6yeOG.js +2 -0
  19. package/dist/{dbCommand-If4Y1xQ-.js → dbCommand-sHedr-NJ.js} +2 -2
  20. package/dist/dev--A3nsxA3.js +3 -0
  21. package/dist/{dev-kdAg9Q7l.js → dev-CRHoCEiy.js} +2142 -2103
  22. package/dist/doctorCommand-CqoWA2p5.js +2 -0
  23. package/dist/{doctorCommand-nKmeW78u.js → doctorCommand-DtfJ3FA6.js} +314 -234
  24. package/dist/{dormancyCommand-CY3wa_SW.js → dormancyCommand-Drn7o0No.js} +1 -1
  25. package/dist/{embeddingsCommand-BDLIgje_.js → embeddingsCommand-Z-jO1fWN.js} +1 -1
  26. package/dist/{envCommand-C6V_xVlT.js → envCommand-D4gCrrTZ.js} +8 -8
  27. package/dist/{evolveCommand-D3c4DSfN.js → evolveCommand-CMROeKeA.js} +2 -2
  28. package/dist/fileConventions-DOqD3lPS.js +34 -0
  29. package/dist/{fileTaxonomy-CJfgOllU.js → fileTaxonomy-DvDUV9wq.js} +1 -1
  30. package/dist/frameworkTableAssembly-C6ETawPR.js +2 -0
  31. package/dist/{frameworkTableAssembly-BwJVEKLr.js → frameworkTableAssembly-w-XnLa3q.js} +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/{infoCommand-BnRFEF1o.js → infoCommand-DXM868o_.js} +1 -1
  34. package/dist/inspectMetrics-CGF94puw.js +143 -0
  35. package/dist/{metaCommands-CfRLra0s.js → metaCommands-C6RFmF1r.js} +2 -2
  36. package/dist/{migrate-DehuBakM.js → migrate-D0F-eTlK.js} +2 -2
  37. package/dist/{pageConvention-cEiRxdab.js → pageConvention-CzUiSbtU.js} +1 -1
  38. package/dist/{privacyCommand-XejDMvmu.js → privacyCommand-DGdopOI6.js} +1 -1
  39. package/dist/{projectScaffold-mIX_DpSe.js → projectScaffold-B4dmTlwT.js} +1 -1
  40. package/dist/{projectScaffold-BIl97_E6.js → projectScaffold-EzlErR4E.js} +1 -1
  41. package/dist/{renderModeScan-D7J1B7Kw.js → renderModeScan-CcH2X1_D.js} +25 -10
  42. package/dist/{renderProfile-1OWWAAtx.js → renderProfile-Ck32Fzxr.js} +2 -2
  43. package/dist/{runtimeTrace-ZsBU7Tkx.js → runtimeTrace-BPQyCmC5.js} +1 -1
  44. package/dist/{sdkgen-O4XqWOjM.js → sdkgen-Se88ifTd.js} +1 -1
  45. package/dist/serveCommand-DLc-BznW.js +2 -0
  46. package/dist/{serveCommand-DdiYNBBu.js → serveCommand-DkP3OT0W.js} +885 -868
  47. package/dist/serveEntry.js +1 -1
  48. package/dist/start-DfL3fOiN.js +3 -0
  49. package/dist/start-jw89Xbqy.js +1339 -0
  50. package/dist/startEntry.js +1 -1
  51. package/dist/{staticCommand-Dr2M6tpU.js → staticCommand-BwNEDlSU.js} +1 -1
  52. package/dist/{test-rXFq4S76.js → test-f3amja6a.js} +1 -1
  53. package/dist/updateCommand-5gFVfK5q.js +2 -0
  54. package/dist/{updateCommand-Bs322Q78.js → updateCommand-BMk2e4ky.js} +1 -1
  55. package/dist/{webDev-B-ubQEMX.js → webDev-BgWL9gKV.js} +1156 -835
  56. package/dist/webDev-CZbTsDcH.js +2 -0
  57. package/dist/{webhooksCommand-FLYY9IXh.js → webhooksCommand-CoIO3jbj.js} +1 -1
  58. package/package.json +30 -17
  59. package/templates/AGENTS.core.md +11 -0
  60. package/templates/AGENTS.md +15 -4
  61. package/templates/agent-docs/_index.md +4 -4
  62. package/templates/agent-docs/_manifest.json +11 -11
  63. package/templates/agent-docs/cli.md +96 -14
  64. package/templates/agent-docs/data.md +210 -7
  65. package/templates/agent-docs/database/schema.md +1 -1
  66. package/templates/agent-docs/database/seedsdialects.md +1 -1
  67. package/templates/agent-docs/deployment.md +22 -3
  68. package/templates/agent-docs/introduction.md +46 -0
  69. package/templates/agent-docs/local-first-mobile.md +34 -7
  70. package/templates/agent-docs/plugins/ai-flows.md +1 -1
  71. package/templates/agent-docs/plugins/audit.md +5 -5
  72. package/templates/agent-docs/plugins/cdc-out.md +2 -2
  73. package/templates/agent-docs/plugins/{versioning.md → row-history.md} +22 -22
  74. package/templates/agent-docs/plugins/storage.md +2 -2
  75. package/templates/agent-docs/plugins.md +29 -7
  76. package/templates/agent-docs/reference.md +39 -2
  77. package/templates/agent-docs/routing.md +341 -47
  78. package/templates/agent-docs/schema-driven-ui.md +78 -2
  79. package/templates/agent-docs/security.md +125 -8
  80. package/templates/agent-docs/templates/apibackends.md +14 -14
  81. package/templates/agent-docs/templates/overview.md +1 -1
  82. package/templates/agent-docs/whats-new.md +76 -53
  83. package/templates/apps/api-ai/package.json +6 -7
  84. package/templates/apps/api-ai/tests/summarize.test.ts +1 -1
  85. package/templates/apps/api-auth/package.json +8 -8
  86. package/templates/apps/api-backend/package.json +7 -7
  87. package/templates/apps/api-backend-deactivation/package.json +7 -7
  88. package/templates/apps/api-backend-mail/package.json +8 -8
  89. package/templates/apps/api-backend-mariadb/package.json +9 -9
  90. package/templates/apps/api-backend-sqlite/package.json +8 -8
  91. package/templates/apps/api-backend-storage/package.json +8 -8
  92. package/templates/apps/api-cms/package.json +9 -10
  93. package/templates/apps/api-collab/package.json +8 -8
  94. package/templates/apps/api-data-advanced/package.json +8 -8
  95. package/templates/apps/api-durable/package.json +8 -8
  96. package/templates/apps/api-feature-flags/package.json +9 -9
  97. package/templates/apps/api-governance/package.json +8 -8
  98. package/templates/apps/api-kv/package.json +8 -8
  99. package/templates/apps/api-moderation/package.json +8 -8
  100. package/templates/apps/api-observability/package.json +8 -8
  101. package/templates/apps/api-ratelimit/package.json +8 -8
  102. package/templates/apps/api-rbac/package.json +8 -8
  103. package/templates/apps/api-rest/package.json +7 -7
  104. package/templates/apps/{api-versioning → api-row-history}/README.md +3 -3
  105. package/templates/apps/{api-versioning → api-row-history}/actions/documents.asOf.action.server.ts +1 -1
  106. package/templates/apps/{api-versioning → api-row-history}/actions/documents.history.action.server.ts +1 -1
  107. package/templates/apps/{api-versioning → api-row-history}/app.config.ts +3 -3
  108. package/templates/apps/{api-versioning → api-row-history}/database/schema.ts +1 -1
  109. package/templates/apps/{api-versioning → api-row-history}/mutations/documents.create.mutation.server.ts +1 -1
  110. package/templates/apps/{api-versioning → api-row-history}/mutations/documents.create.mutation.ts +1 -1
  111. package/templates/apps/{api-versioning → api-row-history}/mutations/documents.update.mutation.ts +1 -1
  112. package/templates/apps/{api-versioning → api-row-history}/package.json +8 -8
  113. package/templates/apps/api-row-history/template.json +6 -0
  114. package/templates/apps/{api-versioning → api-row-history}/tests/documents.create.test.ts +1 -1
  115. package/templates/apps/api-saas/app.config.ts +1 -0
  116. package/templates/apps/api-saas/package.json +10 -11
  117. package/templates/apps/api-saas-starter/package.json +10 -10
  118. package/templates/apps/api-search/package.json +8 -8
  119. package/templates/apps/api-status/package.json +8 -8
  120. package/templates/apps/api-webhooks/package.json +9 -9
  121. package/templates/apps/changelog/package.json +6 -6
  122. package/templates/apps/edge-functions/package.json +2 -2
  123. package/templates/apps/frontend-admin/package.json +8 -8
  124. package/templates/apps/frontend-app/package.json +9 -9
  125. package/templates/apps/frontend-auth/package.json +8 -8
  126. package/templates/apps/frontend-blank/package.json +7 -7
  127. package/templates/apps/frontend-cms/package.json +9 -9
  128. package/templates/apps/frontend-collab/package.json +10 -10
  129. package/templates/apps/frontend-collab/src/pages/page.test.tsx +10 -9
  130. package/templates/apps/frontend-collab/src/pages/page.tsx +42 -65
  131. package/templates/apps/frontend-contact/package.json +7 -7
  132. package/templates/apps/frontend-dashboard/package.json +7 -7
  133. package/templates/apps/frontend-docs/package.json +6 -7
  134. package/templates/apps/frontend-i18n/package.json +6 -6
  135. package/templates/apps/frontend-landing/package.json +6 -7
  136. package/templates/apps/frontend-portal/package.json +8 -8
  137. package/templates/apps/frontend-saas/package.json +8 -8
  138. package/templates/apps/frontend-spa/package.json +7 -7
  139. package/templates/apps/frontend-ssr/package.json +7 -7
  140. package/templates/apps/frontend-ssr-api/package.json +8 -8
  141. package/templates/apps/frontend-static-blog/package.json +6 -6
  142. package/templates/apps/frontend-status/package.json +8 -8
  143. package/templates/apps/mobile-app/package.json +4 -4
  144. package/dist/agentsMd-Bu_XQgVf.js +0 -2
  145. package/dist/apiBuild-GDKuGOMV.js +0 -2
  146. package/dist/build-DETLZAFt.js +0 -752
  147. package/dist/checkCommand-CWcnDArJ.js +0 -2
  148. package/dist/codegen-DiMn2KkZ.js +0 -2
  149. package/dist/dbCommand-C27HIsGE.js +0 -2
  150. package/dist/dev-CK522MV5.js +0 -3
  151. package/dist/doctorCommand-BK4l18eG.js +0 -2
  152. package/dist/fileConventions-Cof68_BL.js +0 -33
  153. package/dist/frameworkTableAssembly-CVDB2hCq.js +0 -2
  154. package/dist/inspectMetrics-CfdKLh6t.js +0 -72
  155. package/dist/serveCommand-BRnPCxVd.js +0 -2
  156. package/dist/start-BLNmWkLa.js +0 -1154
  157. package/dist/start-Dzicuyw8.js +0 -3
  158. package/dist/updateCommand-eXB35SEv.js +0 -2
  159. package/dist/webDev-DposiF3j.js +0 -2
  160. package/templates/apps/api-versioning/template.json +0 -6
  161. /package/templates/apps/{api-versioning → api-row-history}/actions/documents.asOf.action.ts +0 -0
  162. /package/templates/apps/{api-versioning → api-row-history}/actions/documents.history.action.ts +0 -0
  163. /package/templates/apps/{api-versioning → api-row-history}/mutations/documents.update.mutation.server.ts +0 -0
  164. /package/templates/apps/{api-versioning → api-row-history}/tsconfig.json +0 -0
@@ -12,8 +12,8 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
- "@voltro/cli": "0.51.0",
16
- "@voltro/serverless": "0.51.0",
15
+ "@voltro/cli": "0.52.0",
16
+ "@voltro/serverless": "0.52.0",
17
17
  "effect": "^3.22.0"
18
18
  },
19
19
  "devDependencies": {
@@ -12,18 +12,18 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.51.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/client": "0.51.0",
18
- "@voltro/env": "0.51.0",
19
- "@voltro/i18n": "0.51.0",
20
- "@voltro/ui-shadcn": "0.51.0",
21
- "@voltro/web": "0.51.0",
15
+ "@{{projectName}}/api": "0.52.0",
16
+ "@voltro/cli": "0.52.0",
17
+ "@voltro/client": "0.52.0",
18
+ "@voltro/env": "0.52.0",
19
+ "@voltro/i18n": "0.52.0",
20
+ "@voltro/ui-shadcn": "0.52.0",
21
+ "@voltro/web": "0.52.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.51.0",
26
+ "@voltro/devtools": "0.52.0",
27
27
  "@types/react": "^19.0.0",
28
28
  "@types/react-dom": "^19.0.0",
29
29
  "jsdom": "^30.0.0",
@@ -12,19 +12,19 @@
12
12
  "typecheck": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.51.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/client": "0.51.0",
18
- "@voltro/env": "0.51.0",
19
- "@voltro/i18n": "0.51.0",
20
- "@voltro/ui-shadcn": "0.51.0",
21
- "@voltro/web": "0.51.0",
15
+ "@{{projectName}}/api": "0.52.0",
16
+ "@voltro/cli": "0.52.0",
17
+ "@voltro/client": "0.52.0",
18
+ "@voltro/env": "0.52.0",
19
+ "@voltro/i18n": "0.52.0",
20
+ "@voltro/ui-shadcn": "0.52.0",
21
+ "@voltro/web": "0.52.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.51.0",
27
- "@voltro/testing": "0.51.0",
26
+ "@voltro/devtools": "0.52.0",
27
+ "@voltro/testing": "0.52.0",
28
28
  "@types/react": "^19.0.0",
29
29
  "@types/react-dom": "^19.0.0",
30
30
  "jsdom": "^30.0.0",
@@ -12,18 +12,18 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.51.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/client": "0.51.0",
18
- "@voltro/env": "0.51.0",
19
- "@voltro/i18n": "0.51.0",
20
- "@voltro/ui-shadcn": "0.51.0",
21
- "@voltro/web": "0.51.0",
15
+ "@{{projectName}}/api": "0.52.0",
16
+ "@voltro/cli": "0.52.0",
17
+ "@voltro/client": "0.52.0",
18
+ "@voltro/env": "0.52.0",
19
+ "@voltro/i18n": "0.52.0",
20
+ "@voltro/ui-shadcn": "0.52.0",
21
+ "@voltro/web": "0.52.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.51.0",
26
+ "@voltro/devtools": "0.52.0",
27
27
  "@types/react": "^19.0.0",
28
28
  "@types/react-dom": "^19.0.0",
29
29
  "jsdom": "^30.0.0",
@@ -12,17 +12,17 @@
12
12
  "typecheck": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.51.0",
16
- "@voltro/client": "0.51.0",
17
- "@voltro/env": "0.51.0",
18
- "@voltro/i18n": "0.51.0",
19
- "@voltro/ui-shadcn": "0.51.0",
20
- "@voltro/web": "0.51.0",
15
+ "@voltro/cli": "0.52.0",
16
+ "@voltro/client": "0.52.0",
17
+ "@voltro/env": "0.52.0",
18
+ "@voltro/i18n": "0.52.0",
19
+ "@voltro/ui-shadcn": "0.52.0",
20
+ "@voltro/web": "0.52.0",
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/devtools": "0.51.0",
25
+ "@voltro/devtools": "0.52.0",
26
26
  "@types/react": "^19.0.0",
27
27
  "@types/react-dom": "^19.0.0",
28
28
  "jsdom": "^30.0.0",
@@ -12,19 +12,19 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.51.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/client": "0.51.0",
18
- "@voltro/cms": "0.51.0",
19
- "@voltro/env": "0.51.0",
20
- "@voltro/i18n": "0.51.0",
21
- "@voltro/ui-shadcn": "0.51.0",
22
- "@voltro/web": "0.51.0",
15
+ "@{{projectName}}/api": "0.52.0",
16
+ "@voltro/cli": "0.52.0",
17
+ "@voltro/client": "0.52.0",
18
+ "@voltro/cms": "0.52.0",
19
+ "@voltro/env": "0.52.0",
20
+ "@voltro/i18n": "0.52.0",
21
+ "@voltro/ui-shadcn": "0.52.0",
22
+ "@voltro/web": "0.52.0",
23
23
  "react": "^19.0.0",
24
24
  "react-dom": "^19.0.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/devtools": "0.51.0",
27
+ "@voltro/devtools": "0.52.0",
28
28
  "@types/react": "^19.0.0",
29
29
  "@types/react-dom": "^19.0.0",
30
30
  "jsdom": "^30.0.0",
@@ -12,20 +12,20 @@
12
12
  "typecheck": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.51.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/client": "0.51.0",
18
- "@voltro/env": "0.51.0",
19
- "@voltro/i18n": "0.51.0",
20
- "@voltro/local-first": "0.51.0",
21
- "@voltro/ui-shadcn": "0.51.0",
22
- "@voltro/web": "0.51.0",
15
+ "@{{projectName}}/api": "0.52.0",
16
+ "@voltro/cli": "0.52.0",
17
+ "@voltro/client": "0.52.0",
18
+ "@voltro/env": "0.52.0",
19
+ "@voltro/i18n": "0.52.0",
20
+ "@voltro/local-first": "0.52.0",
21
+ "@voltro/ui-shadcn": "0.52.0",
22
+ "@voltro/web": "0.52.0",
23
23
  "react": "^19.0.0",
24
24
  "react-dom": "^19.0.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/devtools": "0.51.0",
28
- "@voltro/testing": "0.51.0",
27
+ "@voltro/devtools": "0.52.0",
28
+ "@voltro/testing": "0.52.0",
29
29
  "@types/react": "^19.0.0",
30
30
  "@types/react-dom": "^19.0.0",
31
31
  "jsdom": "^30.0.0",
@@ -3,21 +3,22 @@
3
3
  // `/` — the collaborative editor, driven through `makeVoltroTestClient`
4
4
  // (`@voltro/testing/client`) rather than a hand-rolled `vi.mock('@voltro/client')`.
5
5
  //
6
- // WHY THE HARNESS HERE and not a mock. This page is stateful: a local CRDT
7
- // handle in a ref, seeded from the server body, folding every INCOMING body
8
- // back in. A `vi.mock` can return one snapshot per mount, so the fold —
9
- // the whole point of the page — was untestable, and the suite that replaced
10
- // this one never pushed a second value into a mounted tree. `setSubscription`
11
- // is a real server delta, so the last case below asserts convergence.
6
+ // WHY THE HARNESS HERE and not a mock. This page is stateful: `useCrdtText`
7
+ // owns a sync client per document cell, seeded from the server body and
8
+ // folding every INCOMING body back in. A `vi.mock` can return one snapshot per
9
+ // mount, so the fold — the whole point of the page — was untestable, and the
10
+ // suite that replaced this one never pushed a second value into a mounted
11
+ // tree. `setSubscription` is a real server delta, so the last case below
12
+ // asserts convergence.
12
13
  //
13
14
  // Two other things fall out of it: the real `@voltro/client` hooks run (so a
14
15
  // change in their loading/error semantics is caught here instead of being
15
16
  // re-implemented by the mock), and the page can be imported statically —
16
17
  // `await import('./page')` only existed to sequence around `vi.mock`'s hoist.
17
18
  //
18
- // `@voltro/local-first` is NOT faked either: the editor's real CRDT handle
19
- // decodes the seeded body and turns keystrokes into encoded updates, which is
20
- // the behaviour worth asserting.
19
+ // `@voltro/local-first` is NOT faked either: the real `useCrdtText` hook
20
+ // decodes the seeded body and turns keystrokes into encoded updates (via its
21
+ // `crdtTextEdit` span diff), which is the behaviour worth asserting.
21
22
 
22
23
  import { afterEach, beforeEach, describe, expect, test } from 'vitest'
23
24
  import { act, createElement, type ReactNode } from 'react'
@@ -2,26 +2,30 @@
2
2
  // by the framework's file-convention page discovery.
3
3
  //
4
4
  // THIS is local-first collaboration over the reactive loop. The api's
5
- // `documents` table has a `crdtText()` `body`; this page:
5
+ // `documents` table has a `crdtText()` `body`; this page binds ONE
6
+ // `useCrdtText` cell (`@voltro/local-first/react`) to it:
6
7
  // 1. `useSubscription('app','documents.list')` — streams the shared document
7
- // (with its MERGED body) live over WebSocket,
8
- // 2. keeps a LOCAL CRDT handle (`@voltro/local-first`'s `crdtText()`) seeded
9
- // from the server state — every keystroke applies a local insert/delete
10
- // and sends the encoded update via `useMutation('app','documents.setBody')`,
11
- // 3. folds every incoming server body back into that handle.
8
+ // (with its MERGED body) live over WebSocket; the streamed body is the
9
+ // hook's `remote`, folded into the local CRDT on every change,
10
+ // 2. `useMutation('app','documents.setBody')` is the hook's `push` — every
11
+ // keystroke goes through `setText`, which diffs to a minimal single-span
12
+ // edit (the framework's `crdtTextEdit`) and queues the encoded update.
12
13
  //
13
14
  // The convergence is authoritative and SERVER-SIDE: the api merges each update
14
15
  // into the stored state before broadcasting, so two tabs typing at once both
15
16
  // survive — no last-write-wins loser. Open this page in two tabs to see it.
16
17
  //
17
- // This is the app-level binding the docs describe as the SyncClient transport:
18
- // `push` = the setBody mutation, `onRemoteState` = the documents subscription.
18
+ // This is the app-level binding the local-first docs describe the two names
19
+ // nothing can derive: `push` = the setBody mutation, `remote` = the documents
20
+ // subscription. Everything else (sync client lifecycle, offline queue, edit
21
+ // encoding) lives in the hook.
19
22
 
20
23
  import { useMutation, useSubscription } from '@voltro/client'
21
- import { useEffect, useRef, useState, type ReactNode } from 'react'
24
+ import { useMemo, type ReactNode } from 'react'
22
25
  import type { PageMeta } from '@voltro/web'
23
26
  import { T, useTFn } from '@voltro/i18n'
24
- import { crdtText, emptyCrdtState, type CrdtState, type CrdtText } from '@voltro/local-first'
27
+ import type { CrdtState } from '@voltro/local-first'
28
+ import { useCrdtText } from '@voltro/local-first/react'
25
29
  import { getCatalog } from '../locales'
26
30
 
27
31
  export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
@@ -47,33 +51,8 @@ interface DocRow {
47
51
  const base64ToBytes = (b64: string): Uint8Array =>
48
52
  Uint8Array.from(atob(b64), (ch) => ch.charCodeAt(0))
49
53
 
50
- // A null/absent body decodes to the empty document — the CRDT merge identity.
51
- const toState = (body: Uint8Array | string | null): CrdtState =>
52
- body instanceof Uint8Array ? body : typeof body === 'string' ? base64ToBytes(body) : emptyCrdtState()
53
-
54
- // Turn a whole-string edit into a single contiguous CRDT insert/delete: the
55
- // common prefix + suffix are unchanged; the middle is what the user replaced.
56
- // Covers ordinary typing, deletion, and paste (one contiguous run).
57
- const applyDiff = (handle: CrdtText, prev: string, next: string): void => {
58
- if (prev === next) return
59
- let start = 0
60
- const shorter = Math.min(prev.length, next.length)
61
- while (start < shorter && prev[start] === next[start]) start += 1
62
- let end = 0
63
- while (
64
- end < shorter - start &&
65
- prev[prev.length - 1 - end] === next[next.length - 1 - end]
66
- ) {
67
- end += 1
68
- }
69
- const deleteLen = prev.length - start - end
70
- const inserted = next.slice(start, next.length - end)
71
- if (deleteLen > 0) handle.delete(start, deleteLen)
72
- if (inserted.length > 0) handle.insert(start, inserted)
73
- }
74
-
75
54
  // One editor bound to ONE document. Remounted (via `key={doc.id}`) when the
76
- // bound document changes, so the local CRDT handle's lifecycle is scoped to it.
55
+ // bound document changes, so the hook's sync-client lifecycle is scoped to it.
77
56
  const DocumentEditor = ({
78
57
  doc,
79
58
  saving,
@@ -81,30 +60,30 @@ const DocumentEditor = ({
81
60
  }: {
82
61
  readonly doc: DocRow
83
62
  readonly saving: boolean
84
- readonly onUpdate: (update: CrdtState) => void
63
+ readonly onUpdate: (update: CrdtState) => Promise<unknown>
85
64
  }): ReactNode => {
86
65
  const t = useTFn()
87
- // One local CRDT handle per mount, seeded from the current server state.
88
- const handleRef = useRef<CrdtText | null>(null)
89
- if (handleRef.current === null) handleRef.current = crdtText().merge(toState(doc.body))
90
- const [text, setText] = useState<string>(() => handleRef.current!.toString())
91
-
92
- // Fold every incoming server body into the local handle — remote edits appear
93
- // and converge. Idempotent, so re-merging our own echoed update is a no-op.
94
- const serverBody = doc.body
95
- useEffect(() => {
96
- const handle = handleRef.current!
97
- handle.merge(toState(serverBody))
98
- const merged = handle.toString()
99
- setText((prev) => (prev === merged ? prev : merged))
100
- }, [serverBody])
101
-
102
- const handleChange = (next: string): void => {
103
- const handle = handleRef.current!
104
- applyDiff(handle, handle.toString(), next)
105
- setText(handle.toString())
106
- onUpdate(handle.encode()) // full encoded state; the server folds it in
107
- }
66
+ // The authoritative state as the subscription streams it. `null` while the
67
+ // body is absent useCrdtText folds nothing then, NOT an empty document.
68
+ const remote = useMemo(
69
+ () =>
70
+ doc.body instanceof Uint8Array
71
+ ? doc.body
72
+ : typeof doc.body === 'string'
73
+ ? base64ToBytes(doc.body)
74
+ : null,
75
+ [doc.body],
76
+ )
77
+ const body = useCrdtText({
78
+ cell: { table: 'documents', id: doc.id, column: 'body' },
79
+ remote,
80
+ // A rejected push re-drives the queued write; log it and keep the rejection.
81
+ push: (write) =>
82
+ onUpdate(write.update).catch((mutationError: unknown) => {
83
+ console.error('documents.setBody failed:', mutationError)
84
+ throw mutationError
85
+ }),
86
+ })
108
87
 
109
88
  return (
110
89
  <section>
@@ -113,8 +92,8 @@ const DocumentEditor = ({
113
92
  {saving ? <T id="editor.status.saving" /> : null}
114
93
  </p>
115
94
  <textarea
116
- value={text}
117
- onChange={(event) => handleChange(event.target.value)}
95
+ value={body.text}
96
+ onChange={(event) => body.setText(event.target.value)}
118
97
  placeholder={t('editor.placeholder')}
119
98
  rows={12}
120
99
  style={{ width: '100%', padding: '10px 12px', fontSize: 14, fontFamily: 'ui-monospace, monospace', resize: 'vertical', boxSizing: 'border-box' }}
@@ -140,11 +119,9 @@ const IndexPage = (): ReactNode => {
140
119
  }
141
120
  }
142
121
 
143
- const handleUpdate = (id: string) => (update: CrdtState): void => {
144
- setBody.mutate({ id, update }).catch((mutationError) => {
145
- console.error('documents.setBody failed:', mutationError)
146
- })
147
- }
122
+ // Returned promise = the hook's ack; a rejection triggers its bounded retry.
123
+ const handleUpdate = (id: string) => (update: CrdtState): Promise<unknown> =>
124
+ setBody.mutate({ id, update })
148
125
 
149
126
  return (
150
127
  <div>
@@ -15,18 +15,18 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@effect/platform": "^0.97.0",
18
- "@voltro/cli": "0.51.0",
19
- "@voltro/client": "0.51.0",
20
- "@voltro/env": "0.51.0",
21
- "@voltro/i18n": "0.51.0",
22
- "@voltro/serverless": "0.51.0",
23
- "@voltro/web": "0.51.0",
18
+ "@voltro/cli": "0.52.0",
19
+ "@voltro/client": "0.52.0",
20
+ "@voltro/env": "0.52.0",
21
+ "@voltro/i18n": "0.52.0",
22
+ "@voltro/serverless": "0.52.0",
23
+ "@voltro/web": "0.52.0",
24
24
  "effect": "^3.22.0",
25
25
  "react": "^19.0.0",
26
26
  "react-dom": "^19.0.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@voltro/devtools": "0.51.0",
29
+ "@voltro/devtools": "0.52.0",
30
30
  "@types/react": "^19.0.0",
31
31
  "@types/react-dom": "^19.0.0",
32
32
  "jsdom": "^30.0.0",
@@ -12,17 +12,17 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.51.0",
16
- "@voltro/client": "0.51.0",
17
- "@voltro/env": "0.51.0",
18
- "@voltro/i18n": "0.51.0",
19
- "@voltro/ui-shadcn": "0.51.0",
20
- "@voltro/web": "0.51.0",
15
+ "@voltro/cli": "0.52.0",
16
+ "@voltro/client": "0.52.0",
17
+ "@voltro/env": "0.52.0",
18
+ "@voltro/i18n": "0.52.0",
19
+ "@voltro/ui-shadcn": "0.52.0",
20
+ "@voltro/web": "0.52.0",
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/devtools": "0.51.0",
25
+ "@voltro/devtools": "0.52.0",
26
26
  "@types/react": "^19.0.0",
27
27
  "@types/react-dom": "^19.0.0",
28
28
  "jsdom": "^30.0.0",
@@ -12,17 +12,16 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.51.0",
16
- "@voltro/client": "0.51.0",
17
- "@voltro/env": "0.51.0",
18
- "@voltro/i18n": "0.51.0",
19
- "@voltro/ui-shadcn": "0.51.0",
20
- "@voltro/web": "0.51.0",
15
+ "@voltro/cli": "0.52.0",
16
+ "@voltro/client": "0.52.0",
17
+ "@voltro/env": "0.52.0",
18
+ "@voltro/i18n": "0.52.0",
19
+ "@voltro/web": "0.52.0",
21
20
  "react": "^19.0.0",
22
21
  "react-dom": "^19.0.0"
23
22
  },
24
23
  "devDependencies": {
25
- "@voltro/devtools": "0.51.0",
24
+ "@voltro/devtools": "0.52.0",
26
25
  "@types/react": "^19.0.0",
27
26
  "@types/react-dom": "^19.0.0",
28
27
  "jsdom": "^30.0.0",
@@ -12,16 +12,16 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.51.0",
16
- "@voltro/client": "0.51.0",
17
- "@voltro/env": "0.51.0",
18
- "@voltro/i18n": "0.51.0",
19
- "@voltro/web": "0.51.0",
15
+ "@voltro/cli": "0.52.0",
16
+ "@voltro/client": "0.52.0",
17
+ "@voltro/env": "0.52.0",
18
+ "@voltro/i18n": "0.52.0",
19
+ "@voltro/web": "0.52.0",
20
20
  "react": "^19.0.0",
21
21
  "react-dom": "^19.0.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/devtools": "0.51.0",
24
+ "@voltro/devtools": "0.52.0",
25
25
  "@types/react": "^19.0.0",
26
26
  "@types/react-dom": "^19.0.0",
27
27
  "jsdom": "^30.0.0",
@@ -12,17 +12,16 @@
12
12
  "typecheck": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.51.0",
16
- "@voltro/client": "0.51.0",
17
- "@voltro/env": "0.51.0",
18
- "@voltro/i18n": "0.51.0",
19
- "@voltro/ui-shadcn": "0.51.0",
20
- "@voltro/web": "0.51.0",
15
+ "@voltro/cli": "0.52.0",
16
+ "@voltro/client": "0.52.0",
17
+ "@voltro/env": "0.52.0",
18
+ "@voltro/i18n": "0.52.0",
19
+ "@voltro/web": "0.52.0",
21
20
  "react": "^19.0.0",
22
21
  "react-dom": "^19.0.0"
23
22
  },
24
23
  "devDependencies": {
25
- "@voltro/devtools": "0.51.0",
24
+ "@voltro/devtools": "0.52.0",
26
25
  "@types/react": "^19.0.0",
27
26
  "@types/react-dom": "^19.0.0",
28
27
  "jsdom": "^30.0.0",
@@ -12,18 +12,18 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.51.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/client": "0.51.0",
18
- "@voltro/env": "0.51.0",
19
- "@voltro/i18n": "0.51.0",
20
- "@voltro/ui-shadcn": "0.51.0",
21
- "@voltro/web": "0.51.0",
15
+ "@{{projectName}}/api": "0.52.0",
16
+ "@voltro/cli": "0.52.0",
17
+ "@voltro/client": "0.52.0",
18
+ "@voltro/env": "0.52.0",
19
+ "@voltro/i18n": "0.52.0",
20
+ "@voltro/ui-shadcn": "0.52.0",
21
+ "@voltro/web": "0.52.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.51.0",
26
+ "@voltro/devtools": "0.52.0",
27
27
  "@types/react": "^19.0.0",
28
28
  "@types/react-dom": "^19.0.0",
29
29
  "jsdom": "^30.0.0",
@@ -12,18 +12,18 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.51.0",
16
- "@voltro/cli": "0.51.0",
17
- "@voltro/client": "0.51.0",
18
- "@voltro/env": "0.51.0",
19
- "@voltro/i18n": "0.51.0",
20
- "@voltro/ui-shadcn": "0.51.0",
21
- "@voltro/web": "0.51.0",
15
+ "@{{projectName}}/api": "0.52.0",
16
+ "@voltro/cli": "0.52.0",
17
+ "@voltro/client": "0.52.0",
18
+ "@voltro/env": "0.52.0",
19
+ "@voltro/i18n": "0.52.0",
20
+ "@voltro/ui-shadcn": "0.52.0",
21
+ "@voltro/web": "0.52.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.51.0",
26
+ "@voltro/devtools": "0.52.0",
27
27
  "@types/react": "^19.0.0",
28
28
  "@types/react-dom": "^19.0.0",
29
29
  "jsdom": "^30.0.0",
@@ -12,17 +12,17 @@
12
12
  "typecheck": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.51.0",
16
- "@voltro/client": "0.51.0",
17
- "@voltro/env": "0.51.0",
18
- "@voltro/i18n": "0.51.0",
19
- "@voltro/ui-shadcn": "0.51.0",
20
- "@voltro/web": "0.51.0",
15
+ "@voltro/cli": "0.52.0",
16
+ "@voltro/client": "0.52.0",
17
+ "@voltro/env": "0.52.0",
18
+ "@voltro/i18n": "0.52.0",
19
+ "@voltro/ui-shadcn": "0.52.0",
20
+ "@voltro/web": "0.52.0",
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/devtools": "0.51.0",
25
+ "@voltro/devtools": "0.52.0",
26
26
  "@types/react": "^19.0.0",
27
27
  "@types/react-dom": "^19.0.0",
28
28
  "jsdom": "^30.0.0",
@@ -12,17 +12,17 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.51.0",
16
- "@voltro/client": "0.51.0",
17
- "@voltro/env": "0.51.0",
18
- "@voltro/i18n": "0.51.0",
19
- "@voltro/ui-shadcn": "0.51.0",
20
- "@voltro/web": "0.51.0",
15
+ "@voltro/cli": "0.52.0",
16
+ "@voltro/client": "0.52.0",
17
+ "@voltro/env": "0.52.0",
18
+ "@voltro/i18n": "0.52.0",
19
+ "@voltro/ui-shadcn": "0.52.0",
20
+ "@voltro/web": "0.52.0",
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/devtools": "0.51.0",
25
+ "@voltro/devtools": "0.52.0",
26
26
  "@types/react": "^19.0.0",
27
27
  "@types/react-dom": "^19.0.0",
28
28
  "jsdom": "^30.0.0",