@covro/covro 0.2.1-dev.5 → 0.2.1-dev.7

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 (133) hide show
  1. package/README.md +12 -4
  2. package/README.zh-CN.md +9 -1
  3. package/dist/backups/factory.js +22 -0
  4. package/dist/backups/factory.js.map +7 -0
  5. package/dist/backups/lock.js +22 -0
  6. package/dist/backups/lock.js.map +7 -0
  7. package/dist/backups/manager.js +170 -0
  8. package/dist/backups/manager.js.map +7 -0
  9. package/dist/backups/restore.js +188 -0
  10. package/dist/backups/restore.js.map +7 -0
  11. package/dist/backups/store.js +274 -0
  12. package/dist/backups/store.js.map +7 -0
  13. package/dist/cli/backup.js +84 -0
  14. package/dist/cli/backup.js.map +7 -0
  15. package/dist/cli/daemon.js +7 -0
  16. package/dist/cli/daemon.js.map +2 -2
  17. package/dist/cli/main.js +35 -5
  18. package/dist/cli/main.js.map +2 -2
  19. package/dist/cli/paths.js.map +2 -2
  20. package/dist/codex/auto-review.js +55 -0
  21. package/dist/codex/auto-review.js.map +2 -2
  22. package/dist/server/db/history.js +60 -0
  23. package/dist/server/db/history.js.map +7 -0
  24. package/dist/server/db/index.js +11 -1
  25. package/dist/server/db/index.js.map +2 -2
  26. package/dist/server/db/migrate.js +8 -18
  27. package/dist/server/db/migrate.js.map +2 -2
  28. package/dist/server/http.js +21 -3
  29. package/dist/server/http.js.map +2 -2
  30. package/dist/server/main.js +14 -3
  31. package/dist/server/main.js.map +2 -2
  32. package/dist/server/project-clones.js +53 -17
  33. package/dist/server/project-clones.js.map +2 -2
  34. package/dist/server/service.js +9 -5
  35. package/dist/server/service.js.map +2 -2
  36. package/dist/shared/backups.js +55 -0
  37. package/dist/shared/backups.js.map +7 -0
  38. package/dist/shared/database.js +15 -0
  39. package/dist/shared/database.js.map +7 -0
  40. package/dist/update-worker.cjs +934 -388
  41. package/dist/updates/files.js +2 -1
  42. package/dist/updates/files.js.map +2 -2
  43. package/dist/updates/transaction.js +3 -24
  44. package/dist/updates/transaction.js.map +2 -2
  45. package/dist/updates/worker.js +69 -19
  46. package/dist/updates/worker.js.map +2 -2
  47. package/dist/web/assets/{abnfDiagram-WEYOIA3C-CwLX554E.js → abnfDiagram-WEYOIA3C-Baw8Q2RL.js} +1 -1
  48. package/dist/web/assets/{arc-DAaDyyqc.js → arc-BvZHXzPr.js} +1 -1
  49. package/dist/web/assets/{architecture-WOLXFQ4H-y6MnEqN0.js → architecture-WOLXFQ4H-DrQ2OxuY.js} +1 -1
  50. package/dist/web/assets/{architectureDiagram-ZHVYKT53-_B_4k-Pp.js → architectureDiagram-ZHVYKT53-D96hX75H.js} +1 -1
  51. package/dist/web/assets/{blockDiagram-RJAE7QA3-C9jZ7FHy.js → blockDiagram-RJAE7QA3-Csce-aWb.js} +1 -1
  52. package/dist/web/assets/{c4Diagram-XDPPPE54-DQAtMvqN.js → c4Diagram-XDPPPE54-Dfu8ZQ6Y.js} +1 -1
  53. package/dist/web/assets/channel-DQhtAfM4.js +1 -0
  54. package/dist/web/assets/{chunk-2E7WHVMD-Bu427yab.js → chunk-2E7WHVMD-Vb63e-lu.js} +1 -1
  55. package/dist/web/assets/{chunk-4EMNKR2T-BrGrBG7B.js → chunk-4EMNKR2T-DrY1P7q3.js} +1 -1
  56. package/dist/web/assets/{chunk-55XCU76K-l4f0OZWA.js → chunk-55XCU76K-D70bjQ9E.js} +1 -1
  57. package/dist/web/assets/{chunk-7S6XSTFO-D7ixF313.js → chunk-7S6XSTFO-CfW90MUC.js} +1 -1
  58. package/dist/web/assets/{chunk-AQDTSKZB-Cesd-BF2.js → chunk-AQDTSKZB-u4KDDGBt.js} +1 -1
  59. package/dist/web/assets/{chunk-DSNLSHJZ-B7dim5uT.js → chunk-DSNLSHJZ-Cax3giJD.js} +1 -1
  60. package/dist/web/assets/{chunk-HAGFOTED-B-f2I7TA.js → chunk-HAGFOTED-BG9UMwzK.js} +1 -1
  61. package/dist/web/assets/{chunk-HNE4Q36P-CoIvaeX6.js → chunk-HNE4Q36P-BNxIfOgZ.js} +1 -1
  62. package/dist/web/assets/{chunk-NQOKRVIF-BybRxR7n.js → chunk-NQOKRVIF-DJ60e0Al.js} +1 -1
  63. package/dist/web/assets/{chunk-UHW7PFBP-CV5WG4tq.js → chunk-UHW7PFBP-C9gHYcjJ.js} +1 -1
  64. package/dist/web/assets/{chunk-VQ5W62QI-BO4AjXi1.js → chunk-VQ5W62QI-TyHPGVfQ.js} +1 -1
  65. package/dist/web/assets/{chunk-WQQL4CDK-CADNDGXO.js → chunk-WQQL4CDK-DeAauheh.js} +1 -1
  66. package/dist/web/assets/{chunk-XXDRQBXY-C6QZT7KW.js → chunk-XXDRQBXY-V-L20Tbi.js} +1 -1
  67. package/dist/web/assets/{chunk-YZO67HFG-Cm6eGReX.js → chunk-YZO67HFG-LV5QVBiU.js} +1 -1
  68. package/dist/web/assets/{chunk-ZAUX2LTX-CUBoGbKg.js → chunk-ZAUX2LTX-CBsBBk7L.js} +1 -1
  69. package/dist/web/assets/{chunk-ZCHMQSOR-B0ImOOtq.js → chunk-ZCHMQSOR-a2_9JHHn.js} +1 -1
  70. package/dist/web/assets/{classDiagram-v2-4MYWR3UA-D5NWZ9vm.js → classDiagram-v2-4MYWR3UA-77eiBCO3.js} +1 -1
  71. package/dist/web/assets/{cose-bilkent-JH36ORCC-BzckEyjA.js → cose-bilkent-JH36ORCC-DDP73pnY.js} +1 -1
  72. package/dist/web/assets/{cynefin-EF2NZ3EQ-Da_J6-Jb.js → cynefin-EF2NZ3EQ-onpnpyxc.js} +1 -1
  73. package/dist/web/assets/{cynefinDiagram-WF24UDRP-b4g_B1A-.js → cynefinDiagram-WF24UDRP-BmLplbnH.js} +1 -1
  74. package/dist/web/assets/{dagre-NK4GIZVX-89H2mfs9.js → dagre-NK4GIZVX-BhaMf_Gx.js} +1 -1
  75. package/dist/web/assets/{diagram-CBK3MWTC-CW4f_P0l.js → diagram-CBK3MWTC-BaalTqFt.js} +1 -1
  76. package/dist/web/assets/{diagram-I5XIIV2D-Cv5Qndgc.js → diagram-I5XIIV2D-4Tdshcyt.js} +1 -1
  77. package/dist/web/assets/{diagram-ODIL2UCW-LrXxeoCt.js → diagram-ODIL2UCW-SlEcZgod.js} +1 -1
  78. package/dist/web/assets/{diagram-PFBEJUXE-uJV2PooB.js → diagram-PFBEJUXE-CosCAXbh.js} +1 -1
  79. package/dist/web/assets/{diagram-RJA3I4VV-DlY0PGRs.js → diagram-RJA3I4VV-HQElVL4B.js} +1 -1
  80. package/dist/web/assets/{diagram-TYUNFSNR-B1KoXBEE.js → diagram-TYUNFSNR-BT2WDc21.js} +1 -1
  81. package/dist/web/assets/{dist-B4eX4BCP.js → dist-CrpGQKqw.js} +1 -1
  82. package/dist/web/assets/{ebnfDiagram-V4P3TTPT-xCvQVbdj.js → ebnfDiagram-V4P3TTPT-BhK6AM0_.js} +1 -1
  83. package/dist/web/assets/{elk-QONTBCTQ-Cl8Hu-Uy.js → elk-QONTBCTQ-CS7DfCnD.js} +1 -1
  84. package/dist/web/assets/{erDiagram-KYQIGQAA-Xp-AvSwE.js → erDiagram-KYQIGQAA-DxVnuqFK.js} +1 -1
  85. package/dist/web/assets/{eventmodeling-K75KTNOO-C0truZGU.js → eventmodeling-K75KTNOO-oC_YeomD.js} +1 -1
  86. package/dist/web/assets/flowDiagram-IUGLLWBU-Bspg0rQz.js +1 -0
  87. package/dist/web/assets/{ganttDiagram-R7GPNTJY-rPaYp-D7.js → ganttDiagram-R7GPNTJY-CquUpiE1.js} +1 -1
  88. package/dist/web/assets/{gitGraph-VSP46ZUC-Bgfpho41.js → gitGraph-VSP46ZUC-BTtGQYsh.js} +1 -1
  89. package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-kDcmuv6k.js → gitGraphDiagram-PDYL7XHY-BsokGXYo.js} +1 -1
  90. package/dist/web/assets/index-B8tsymVC.js +147 -0
  91. package/dist/web/assets/{index-B7tnf28H.css → index-COSho7m2.css} +1 -1
  92. package/dist/web/assets/{info-OHQRW6UA-CAWg5k9R.js → info-OHQRW6UA-CvGN4gWh.js} +1 -1
  93. package/dist/web/assets/{infoDiagram-MVDRKAX5-Cd08ebhn.js → infoDiagram-MVDRKAX5-BF9Rp3BT.js} +1 -1
  94. package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-tAXBLt9k.js → ishikawaDiagram-HPEHV2GE-DtD20ngU.js} +1 -1
  95. package/dist/web/assets/{journeyDiagram-BOSHW7O7-ZgnWqXkg.js → journeyDiagram-BOSHW7O7-gB16trHV.js} +1 -1
  96. package/dist/web/assets/{kanban-definition-FNSA4HC5-BUjtjd-h.js → kanban-definition-FNSA4HC5-D0aKOsE2.js} +1 -1
  97. package/dist/web/assets/{line-DG48HRVK.js → line-BR5dqPmF.js} +1 -1
  98. package/dist/web/assets/{linear-CqnXVCVw.js → linear-OsiQaPpK.js} +1 -1
  99. package/dist/web/assets/{mermaid-parser.core-gIvgB9WK.js → mermaid-parser.core-MEuwYVot.js} +3 -3
  100. package/dist/web/assets/{mermaid.core-C_R7Y1XB.js → mermaid.core-CVKnnz65.js} +4 -4
  101. package/dist/web/assets/{mindmap-definition-6OY57UG3-CwUEEl_y.js → mindmap-definition-6OY57UG3-5aHA-kPJ.js} +1 -1
  102. package/dist/web/assets/{packet-JDAUHWVQ-BrXmpRQ3.js → packet-JDAUHWVQ-uj48AF5r.js} +1 -1
  103. package/dist/web/assets/{pegDiagram-ECIUJNBX-lLApO85Q.js → pegDiagram-ECIUJNBX-BkVIHLBX.js} +1 -1
  104. package/dist/web/assets/{pie-XZMESJXO-C_8kR5WX.js → pie-XZMESJXO-D3hy3nD7.js} +1 -1
  105. package/dist/web/assets/{pieDiagram-GNMPY74T-CawGmJZn.js → pieDiagram-GNMPY74T-CBAqGMEo.js} +1 -1
  106. package/dist/web/assets/{quadrantDiagram-D57K4MC3-Dg6Yq3s_.js → quadrantDiagram-D57K4MC3-C35kvNRB.js} +1 -1
  107. package/dist/web/assets/{radar-ABXABTNO-Bs1xY09D.js → radar-ABXABTNO-3LWE6J2Y.js} +1 -1
  108. package/dist/web/assets/{railroad-I3PHUGI6-Bw_qS6e3.js → railroad-I3PHUGI6-B6Mc9Djl.js} +1 -1
  109. package/dist/web/assets/{railroad-abnf-LNEFI6M7-CQr33CuI.js → railroad-abnf-LNEFI6M7-DJnbwbpn.js} +1 -1
  110. package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-BY9nk5pg.js → railroad-ebnf-DWYD2UWJ-BFXAvY4k.js} +1 -1
  111. package/dist/web/assets/{railroad-peg-JR7BVNR7-CcFsAEI4.js → railroad-peg-JR7BVNR7-JYGb2Oq5.js} +1 -1
  112. package/dist/web/assets/{railroadDiagram-Y72NRBL7-rkZbchmc.js → railroadDiagram-Y72NRBL7-uBIggR0w.js} +1 -1
  113. package/dist/web/assets/{requirementDiagram-EQYSW5RA-CSOcijj5.js → requirementDiagram-EQYSW5RA-CDWglrxJ.js} +1 -1
  114. package/dist/web/assets/{sankeyDiagram-SY24JHO5-E0uo0rRp.js → sankeyDiagram-SY24JHO5-BT16hnTl.js} +1 -1
  115. package/dist/web/assets/{sequenceDiagram-BLEHNHQU-BKWAAcRu.js → sequenceDiagram-BLEHNHQU-eFJ27rVk.js} +1 -1
  116. package/dist/web/assets/{src-F6L8HcEC.js → src-BlOCBHYS.js} +1 -1
  117. package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-BBk2tpPh.js → stateDiagram-v2-A6IESKWZ-CK_s-2Qv.js} +1 -1
  118. package/dist/web/assets/{swimlanes-B3ENRKFQ-CAUrhGwF.js → swimlanes-B3ENRKFQ-WLMrk9Lm.js} +1 -1
  119. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-DmjebfnI.js +8 -0
  120. package/dist/web/assets/{timeline-definition-JXYKGLGB-D0FWj9fa.js → timeline-definition-JXYKGLGB-D0-xZS_V.js} +1 -1
  121. package/dist/web/assets/{treeView-D4JQ5SDB-CkJOmf93.js → treeView-D4JQ5SDB-TTivyB8T.js} +1 -1
  122. package/dist/web/assets/{treemap-SAJKECNS-BSFsardB.js → treemap-SAJKECNS-DtU16wOO.js} +1 -1
  123. package/dist/web/assets/{usecaseDiagram-WPOLBNNX-B2U-2o4g.js → usecaseDiagram-WPOLBNNX-DLFybW0F.js} +1 -1
  124. package/dist/web/assets/{vennDiagram-Q3Y3IDCW-BijTH0fT.js → vennDiagram-Q3Y3IDCW-DRDuYw70.js} +1 -1
  125. package/dist/web/assets/{wardley-7MLQ67FV-DDOa5RzL.js → wardley-7MLQ67FV-tZ-5qrqv.js} +1 -1
  126. package/dist/web/assets/{wardleyDiagram-IVAIDKWA-mbb5GvW5.js → wardleyDiagram-IVAIDKWA-wRKeGCMl.js} +1 -1
  127. package/dist/web/assets/{xychartDiagram-4URNHDU4-DHd60D_Y.js → xychartDiagram-4URNHDU4-B5-k-DHF.js} +1 -1
  128. package/dist/web/index.html +4 -4
  129. package/package.json +10 -3
  130. package/dist/web/assets/channel-ByzMCsOB.js +0 -1
  131. package/dist/web/assets/flowDiagram-IUGLLWBU-T67jQ-T_.js +0 -1
  132. package/dist/web/assets/index-3DUP8YCe.js +0 -142
  133. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-BCnJymLL.js +0 -8
package/README.md CHANGED
@@ -30,7 +30,8 @@
30
30
  ## ✨ What you can do
31
31
 
32
32
  - 📱 **Step away from your desk. Stay with the task.** Follow tool activity, inspect file changes, handle permission requests, and answer Codex's questions from your computer or phone. Closing the page doesn't cancel the task.
33
- - 💬 **Start without a project.** Use **新建会话** (New conversation) to start in an automatically assigned directory. Conversations and files stay available after refresh, restart, or archive.
33
+ - 💬 **Start without a project.** Use **New session** to start in an automatically assigned directory. Conversations and files stay available after refresh, restart, or archive.
34
+ - 🌐 **Choose your interface language.** Switch between 简体中文, English, 日本語 and 한국어 without leaving your session. Prompts, replies, tool output and paths remain unchanged. [Localization details](docs/i18n.md).
34
35
  - 🗂️ **See every project in one place.** Run independent tasks in parallel and spot which conversations are working, waiting for approval, or ready to read.
35
36
  - 💬 **Pick up your Codex CLI conversations.** Browse saved history, resume existing work, or fork a new direction. Rename conversations to find them later and archive them when you're done. View and copy the full Thread ID in conversation details or diagnostics.
36
37
  - ⚡ **Open long conversations sooner.** Recent messages appear first; scrolling preloads older history. On this device, cached conversation lists and history pages appear while the server checks for updates. [Loading and cache behavior](docs/session-history.md).
@@ -38,6 +39,7 @@
38
39
  - 🔎 **Inspect code changes.** Open Codex’s saved patches for a turn or tool in the same read-only panel. Scroll through all files, inspect line numbers, see the exact words changed within each line, and jump between change blocks. Long diffs start collapsed and render when expanded. File headers stay visible while scrolling so you can collapse the current file.
39
40
  - ✍️ **Switch tasks without losing your draft.** Each conversation keeps its own draft, pasted images, and model settings in the current page. Copy or export results when you're done.
40
41
  - 🖥️ **Run a command beside your task.** Open **Shell** to execute commands in the selected project's directory, follow stdout/stderr, interrupt a run, and reuse recent commands. The panel uses Codex's configured sandbox, with a ten-minute limit and no interactive input. Closing it keeps the command running. [Capabilities and limits](docs/shell.md).
42
+ - 🛟 **Keep a recovery point.** Open **Backups and recovery** in the sidebar to create a server-side backup, follow its progress and review verified snapshots. Upgrades use the same backup engine. Recovery runs offline through the CLI. [Scope and recovery instructions](docs/backups.md).
41
43
 
42
44
  Mermaid diagrams render locally after their code fence closes, with automatic light/dark colors. Failed diagrams keep their source; rendering is limited to 50,000 source characters and 500 edges.
43
45
 
@@ -51,7 +53,7 @@ Mermaid diagrams render locally after their code fence closes, with automatic li
51
53
 
52
54
  ## 🚀 Get started
53
55
 
54
- You need **Node.js 24.19 or later in the 24.x line** and an installed, signed-in **Codex CLI**. The interface currently uses Simplified Chinese.
56
+ You need **Node.js 24.19 or later in the 24.x line** and an installed, signed-in **Codex CLI**. The interface supports Simplified Chinese, English, Japanese and Korean. Choose **Language** in the sidebar; the first visit follows your browser preferences, and manual choices are saved on this device.
55
57
 
56
58
  <!-- codex-compatibility:start -->
57
59
 
@@ -105,9 +107,9 @@ pnpm build
105
107
  pnpm start -- --config ./config.local.json
106
108
  ```
107
109
 
108
- Open **<http://127.0.0.1:8787>** and click **+** beside **未关联项目** (Unassigned) in the sidebar to send a task without choosing a project.
110
+ Open **<http://127.0.0.1:8787>** and click **+** beside **Unassigned** in the sidebar to send a task without choosing a project.
109
111
 
110
- To work in a project directory, click **添加项目** (Add project), then choose an existing host directory or **克隆 Git 仓库** (Clone Git repository). For cloning, enter an HTTPS/SSH repository address, an absolute destination on the host, and an optional project name. Git must be available to the service and uses the host’s existing credentials. Follow progress or cancel in the dialog; once imported, open the project and send your first task. [Clone requirements and recovery](docs/project-clone.md).
112
+ To work in a project directory, click **Add project**, then choose an existing host directory or **Clone Git repository**. For cloning, enter an HTTPS/SSH repository address, an absolute destination on the host, and an optional project name. Git must be available to the service and uses the host’s existing credentials. Follow progress or cancel in the dialog; once imported, open the project and send your first task. [Clone requirements and recovery](docs/project-clone.md).
111
113
 
112
114
  Unassigned conversations use separate directories under `<dataDir>/workspaces/`, allocated on the first send. Their files are retained until you explicitly remove them; archiving does not delete files. See [conversation workspaces](docs/workspaces.md) for directory, recovery, and backup details.
113
115
 
@@ -149,6 +151,12 @@ Each release declares its recommended and verified Codex CLI versions. The updat
149
151
 
150
152
  Automatic installation is off by default. Managed upgrades support **macOS and Linux npm global installations**; source, linked, container, and Windows installations use their original update method. A failed candidate is rolled back before business writes resume; downgrades must pass database compatibility checks. Old backups are retained without automatic rotation.
151
153
 
154
+ DB Schema **6** is tracked independently of the application version. Releases declare the schema and the verified backup migration range; an older application cannot open a newer database. See [database compatibility](docs/database.md).
155
+
156
+ Managed upgrades create and validate a private backup of the Covro database and service configuration before replacing the installation. A failed backup blocks activation. These snapshots do not include Codex history, credentials or project files. See [backup scope](docs/backups.md).
157
+
158
+ Use `covro backup create`, `covro backup list` and `covro backup verify ID` for manual snapshots. Preview offline recovery with `covro backup restore ID`; stop the service and repeat with `--confirm ID` to apply it. See [recovery instructions](docs/backups.md#manual-backups-and-offline-recovery).
159
+
152
160
  See [distribution and recovery](docs/distribution.md) and [release operations](docs/releases.md) for details.
153
161
 
154
162
  ## Your computer, your workspace
package/README.zh-CN.md CHANGED
@@ -31,6 +31,7 @@
31
31
 
32
32
  - 📱 **离开电脑,也能跟进任务。** 在电脑或手机上查看工具活动与文件变更、处理权限审批、回答 Codex 的问题。关闭页面,任务仍会继续。
33
33
  - 💬 **无需项目,也能直接开始。** 点击 **新建会话**,应用自动分配独立目录;刷新、重启或归档后,会话和文件仍保留。
34
+ - 🌐 **选择界面语言。** 支持简体中文、English、日本語和 한국어,会话内即可切换;输入、回复、工具原文和路径保持不变。[多语言说明](docs/i18n.md)。
34
35
  - 🗂️ **多个项目,一眼掌握进展。** 让独立任务并行执行,随时看到哪些会话正在工作、等待审批,哪些已有结果可读。
35
36
  - 💬 **接着你的 Codex CLI 会话。** 浏览已有历史、恢复工作,或 Fork 一个新方向;为会话命名以便查找,完成后归档。在会话详情或状态诊断中查看、复制完整 Thread ID。
36
37
  - ⚡ **更快打开长会话。** 优先显示最近消息,向上滚动时提前加载历史;本机会话列表和历史分片可先显示缓存,再由服务端核对更新。[加载与缓存说明](docs/session-history.md)。
@@ -38,6 +39,7 @@
38
39
  - 🔎 **逐行查看代码变更。** 在同一只读预览窗口查看某轮或某工具的 Codex 补丁快照,按顺序连续浏览全部文件,支持行号、行内文字高亮和区块跳转;长 diff 默认折叠,展开后按需渲染,阅读时文件标题栏吸顶,随时收起当前文件。
39
40
  - ✍️ **切换任务,草稿还在。** 每个会话分别保留当前页面中的草稿、粘贴图片和模型设置;完成后可以复制或导出结果。
40
41
  - 🖥️ **在任务旁运行命令。** 打开 **Shell**,在所选项目目录执行命令、查看实时标准输出和错误输出、中断执行,并复用近期命令。面板沿用 Codex 配置的沙箱,最长运行十分钟,不支持交互输入;关闭面板后命令仍继续运行。[能力与限制](docs/shell.md)。
42
+ - 🛟 **保留可恢复的数据快照。** 从侧栏打开**备份与恢复**,手动创建服务端备份,查看进度和已验证的记录;升级前使用同一套备份能力。恢复通过停服后的 CLI 执行。[范围与恢复步骤](docs/backups.md)。
41
43
 
42
44
  Mermaid 图表在代码块闭合后于本地渲染,自动适配浅色/深色。渲染失败时保留源码;渲染上限为 50,000 个源码字符、500 条边。
43
45
 
@@ -51,7 +53,7 @@ Mermaid 图表在代码块闭合后于本地渲染,自动适配浅色/深色
51
53
 
52
54
  ## 🚀 开始使用
53
55
 
54
- 需要 **Node.js 24.x,版本不低于 24.19**,以及已经安装、登录的 **Codex CLI**。界面目前使用简体中文。
56
+ 需要 **Node.js 24.x,版本不低于 24.19**,以及已经安装、登录的 **Codex CLI**。界面支持简体中文、英语、日语和韩语。在侧栏选择「语言」;首次访问按浏览器偏好匹配,手动选择会保存在当前设备。
55
57
 
56
58
  <!-- codex-compatibility:start -->
57
59
 
@@ -149,6 +151,12 @@ covro update --channel dev
149
151
 
150
152
  自动安装默认关闭。受管升级支持 **macOS 和 Linux 的 npm 全局安装**;源码、链接、容器和 Windows 安装沿用原更新方式。候选版本失败时,会在重新开放业务写入前回滚;降级需通过数据库兼容检查。旧备份会保留,暂不自动轮转。
151
153
 
154
+ DB Schema **6** 独立于应用版本管理。发行版声明 Schema 及经过验证的备份迁移范围;旧应用不能打开更新的数据库。详见[数据库兼容规则](docs/database.md)。
155
+
156
+ 受管升级会在替换安装前创建并验证私有备份,覆盖 Covro 数据库与服务配置;备份失败会阻止激活。快照不包含 Codex 历史、凭据或项目文件。详见[备份范围](docs/backups.md)。
157
+
158
+ 使用 `covro backup create`、`covro backup list` 和 `covro backup verify ID` 创建与检查手动备份。先运行 `covro backup restore ID` 预检;停服后加 `--confirm ID` 执行恢复。详见[恢复步骤](docs/backups.md#manual-backups-and-offline-recovery)。
159
+
152
160
  详细说明见[分发与恢复](docs/distribution.md)、[发版操作](docs/releases.md)。
153
161
 
154
162
  ## 你的电脑,你的工作台
@@ -0,0 +1,22 @@
1
+ import Database from "better-sqlite3";
2
+ import { packageInfo, paths } from "../cli/paths.js";
3
+ import { databaseCompatibilitySchema } from "../shared/database.js";
4
+ import { readMigrations } from "../server/db/history.js";
5
+ import { runtimeMigrationsFolder } from "../server/db/migrate.js";
6
+ import { BackupStore, backupDirectory } from "./store.js";
7
+ function createBackupStore(config, dataDir, migrationsFolder = runtimeMigrationsFolder) {
8
+ return new BackupStore({
9
+ directory: backupDirectory(paths(config).home, dataDir),
10
+ config,
11
+ dataDir,
12
+ appVersion: packageInfo.version,
13
+ commit: packageInfo.covroCommit,
14
+ contract: databaseCompatibilitySchema.parse(packageInfo.covroDatabase),
15
+ migrations: readMigrations(migrationsFolder),
16
+ Sqlite: Database
17
+ });
18
+ }
19
+ export {
20
+ createBackupStore
21
+ };
22
+ //# sourceMappingURL=factory.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/backups/factory.ts"],
4
+ "sourcesContent": ["import Database from 'better-sqlite3';\nimport { packageInfo, paths } from '../cli/paths.js';\nimport { databaseCompatibilitySchema } from '../shared/database.js';\nimport { readMigrations } from '../server/db/history.js';\nimport { runtimeMigrationsFolder } from '../server/db/migrate.js';\nimport { BackupStore, backupDirectory } from './store.js';\n\nexport function createBackupStore(\n config: string,\n dataDir: string,\n migrationsFolder = runtimeMigrationsFolder,\n) {\n return new BackupStore({\n directory: backupDirectory(paths(config).home, dataDir),\n config,\n dataDir,\n appVersion: packageInfo.version,\n commit: packageInfo.covroCommit,\n contract: databaseCompatibilitySchema.parse(packageInfo.covroDatabase),\n migrations: readMigrations(migrationsFolder),\n Sqlite: Database,\n });\n}\n"],
5
+ "mappings": "AAAA,OAAO,cAAc;AACrB,SAAS,aAAa,aAAa;AACnC,SAAS,mCAAmC;AAC5C,SAAS,sBAAsB;AAC/B,SAAS,+BAA+B;AACxC,SAAS,aAAa,uBAAuB;AAEtC,SAAS,kBACd,QACA,SACA,mBAAmB,yBACnB;AACA,SAAO,IAAI,YAAY;AAAA,IACrB,WAAW,gBAAgB,MAAM,MAAM,EAAE,MAAM,OAAO;AAAA,IACtD;AAAA,IACA;AAAA,IACA,YAAY,YAAY;AAAA,IACxB,QAAQ,YAAY;AAAA,IACpB,UAAU,4BAA4B,MAAM,YAAY,aAAa;AAAA,IACrE,YAAY,eAAe,gBAAgB;AAAA,IAC3C,QAAQ;AAAA,EACV,CAAC;AACH;",
6
+ "names": []
7
+ }
@@ -0,0 +1,22 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import lockfile from "proper-lockfile";
4
+ import { AppError } from "../shared/errors.js";
5
+ async function acquireMaintenanceLock(dataDir) {
6
+ try {
7
+ const directory = join(dataDir, ".covro-maintenance");
8
+ await mkdir(directory, { recursive: true, mode: 448 });
9
+ return await lockfile.lock(directory, {
10
+ realpath: true,
11
+ stale: 1e4,
12
+ update: 2e3,
13
+ retries: 0
14
+ });
15
+ } catch {
16
+ throw new AppError("MAINTENANCE_BUSY", "\u5DF2\u6709\u5907\u4EFD\u3001\u5347\u7EA7\u6216\u6062\u590D\u6B63\u5728\u5904\u7406\uFF0C\u8BF7\u5B8C\u6210\u540E\u91CD\u8BD5\u3002", 409);
17
+ }
18
+ }
19
+ export {
20
+ acquireMaintenanceLock
21
+ };
22
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/backups/lock.ts"],
4
+ "sourcesContent": ["import { mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport lockfile from 'proper-lockfile';\nimport { AppError } from '../shared/errors.js';\n\n/** Shared by live manual snapshots, update workers and offline restoration. */\nexport async function acquireMaintenanceLock(dataDir: string) {\n try {\n const directory = join(dataDir, '.covro-maintenance');\n await mkdir(directory, { recursive: true, mode: 0o700 });\n return await lockfile.lock(directory, {\n realpath: true,\n stale: 10_000,\n update: 2_000,\n retries: 0,\n });\n } catch {\n throw new AppError('MAINTENANCE_BUSY', '\u5DF2\u6709\u5907\u4EFD\u3001\u5347\u7EA7\u6216\u6062\u590D\u6B63\u5728\u5904\u7406\uFF0C\u8BF7\u5B8C\u6210\u540E\u91CD\u8BD5\u3002', 409);\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,aAAa;AACtB,SAAS,YAAY;AACrB,OAAO,cAAc;AACrB,SAAS,gBAAgB;AAGzB,eAAsB,uBAAuB,SAAiB;AAC5D,MAAI;AACF,UAAM,YAAY,KAAK,SAAS,oBAAoB;AACpD,UAAM,MAAM,WAAW,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AACvD,WAAO,MAAM,SAAS,KAAK,WAAW;AAAA,MACpC,UAAU;AAAA,MACV,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AAAA,EACH,QAAQ;AACN,UAAM,IAAI,SAAS,oBAAoB,wIAA0B,GAAG;AAAA,EACtE;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,170 @@
1
+ import { setTimeout } from "node:timers/promises";
2
+ import { z } from "zod";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { acquireMaintenanceLock } from "./lock.js";
5
+ import { backupFailure } from "./store.js";
6
+ const backupRequestSchema = z.object({ clientOperationId: z.string().uuid() }).strict();
7
+ const ongoing = (record) => ["waiting", "copying", "verifying"].includes(record.phase);
8
+ class BackupManager {
9
+ constructor(store, service, options = {}) {
10
+ this.store = store;
11
+ this.service = service;
12
+ this.options = options;
13
+ }
14
+ store;
15
+ service;
16
+ options;
17
+ active;
18
+ abort;
19
+ task;
20
+ closed = false;
21
+ async status() {
22
+ const records = (await this.store.list()).map(
23
+ (record) => this.active?.id === record.id ? this.active : ongoing(record) ? {
24
+ ...record,
25
+ phase: "interrupted",
26
+ message: "\u5907\u4EFD\u8FDB\u7A0B\u5DF2\u4E2D\u65AD\uFF0C\u672A\u751F\u6210\u53EF\u6062\u590D\u5FEB\u7167\u3002"
27
+ } : record
28
+ );
29
+ return {
30
+ directory: this.store.options.directory,
31
+ schemaVersion: this.store.options.contract.schemaVersion,
32
+ records,
33
+ active: this.active,
34
+ supported: !this.options.unsupportedReason,
35
+ unsupportedReason: this.options.unsupportedReason
36
+ };
37
+ }
38
+ async start(input) {
39
+ const { clientOperationId: id } = backupRequestSchema.parse(input);
40
+ if (this.closed) throw new AppError("BACKUP_UNAVAILABLE", "\u670D\u52A1\u6B63\u5728\u5173\u95ED\u3002", 503);
41
+ if (this.options.unsupportedReason)
42
+ throw new AppError("BACKUP_UNAVAILABLE", this.options.unsupportedReason, 409);
43
+ if (this.active?.id === id || (await this.store.list()).some((record2) => record2.id === id))
44
+ return this.status();
45
+ if (this.active) throw new AppError("BACKUP_BUSY", "\u5DF2\u6709\u5907\u4EFD\u6B63\u5728\u5904\u7406\u3002", 409);
46
+ if (this.service.isInMaintenance()) throw this.service.maintenanceError();
47
+ const release = await acquireMaintenanceLock(this.store.options.dataDir);
48
+ if (this.closed) {
49
+ await release();
50
+ throw new AppError("BACKUP_UNAVAILABLE", "\u670D\u52A1\u6B63\u5728\u5173\u95ED\u3002", 503);
51
+ }
52
+ const abort = new AbortController();
53
+ const record = {
54
+ id,
55
+ phase: "waiting",
56
+ trigger: "manual",
57
+ createdAt: Date.now(),
58
+ updatedAt: Date.now(),
59
+ path: this.store.path(id),
60
+ message: "\u7B49\u5F85\u4EFB\u52A1\u3001\u5BA1\u6279\u3001\u8F93\u5165\u53CA\u5F85\u6838\u5BF9\u64CD\u4F5C\u7ED3\u675F"
61
+ };
62
+ this.active = record;
63
+ this.abort = abort;
64
+ const preparation = this.store.record(record);
65
+ this.task = (async () => {
66
+ try {
67
+ await preparation;
68
+ } catch {
69
+ await release();
70
+ this.active = void 0;
71
+ this.abort = void 0;
72
+ return;
73
+ }
74
+ await this.run(record, abort, release);
75
+ })();
76
+ try {
77
+ await preparation;
78
+ } catch (error) {
79
+ throw new AppError("BACKUP_FAILED", backupFailure(error), 500);
80
+ }
81
+ return this.status();
82
+ }
83
+ async run(record, abort, release) {
84
+ let gated = false;
85
+ try {
86
+ for (; ; ) {
87
+ abort.signal.throwIfAborted();
88
+ if (Date.now() - record.createdAt > (this.options.timeoutMs ?? 30 * 6e4))
89
+ throw new AppError("BACKUP_TIMEOUT", "\u7B49\u5F85\u5B89\u5168\u70B9\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", 409);
90
+ const result = this.service.prepareUpdate("backup");
91
+ if (result.ready) {
92
+ gated = true;
93
+ break;
94
+ }
95
+ if (record.message !== result.reason) {
96
+ record.message = result.reason ?? "\u7B49\u5F85\u4EFB\u52A1\u7ED3\u675F";
97
+ record.updatedAt = Date.now();
98
+ await this.store.record(record);
99
+ }
100
+ await setTimeout(this.options.pollMs ?? 250, void 0, { signal: abort.signal });
101
+ }
102
+ await this.store.create({
103
+ id: record.id,
104
+ trigger: "manual",
105
+ reserved: true,
106
+ signal: abort.signal,
107
+ progress: (next) => {
108
+ this.active = next;
109
+ }
110
+ });
111
+ } catch (error) {
112
+ if (this.active?.phase !== "failed" && this.active?.phase !== "cancelled") {
113
+ record.phase = abort.signal.aborted ? "cancelled" : "failed";
114
+ record.message = abort.signal.aborted ? "\u5907\u4EFD\u5DF2\u53D6\u6D88\u3002" : error instanceof AppError ? error.message : backupFailure(error);
115
+ record.updatedAt = Date.now();
116
+ await this.store.record(record).catch(() => {
117
+ });
118
+ }
119
+ } finally {
120
+ if (gated) this.service.setMaintenance(false);
121
+ await release();
122
+ this.active = void 0;
123
+ this.abort = void 0;
124
+ }
125
+ }
126
+ async cancel(id) {
127
+ if (this.active?.id === id) {
128
+ this.abort?.abort();
129
+ await this.task;
130
+ }
131
+ return this.status();
132
+ }
133
+ async verify(id) {
134
+ if (this.active && !ongoing(this.active)) await this.task;
135
+ if (this.active) throw new AppError("BACKUP_BUSY", "\u8BF7\u7B49\u5F85\u5F53\u524D\u5907\u4EFD\u5B8C\u6210\u3002", 409);
136
+ try {
137
+ await this.store.verify(id);
138
+ const record = (await this.store.list()).find((entry) => entry.id === id);
139
+ if (record)
140
+ await this.store.record({
141
+ ...record,
142
+ phase: "succeeded",
143
+ message: "\u5907\u4EFD\u5DF2\u91CD\u65B0\u9A8C\u8BC1\uFF0C\u5B8C\u6574\u6027\u68C0\u67E5\u901A\u8FC7\u3002",
144
+ updatedAt: Date.now()
145
+ });
146
+ } catch {
147
+ const record = (await this.store.list()).find((entry) => entry.id === id);
148
+ if (record)
149
+ await this.store.record({
150
+ ...record,
151
+ phase: "corrupt",
152
+ message: "\u5907\u4EFD\u635F\u574F\u3001\u4E0D\u5B8C\u6574\u6216\u4E0E\u5F53\u524D\u7248\u672C\u4E0D\u517C\u5BB9\uFF0C\u4E0D\u80FD\u6062\u590D\u3002",
153
+ updatedAt: Date.now()
154
+ }).catch(() => {
155
+ });
156
+ throw new AppError("BACKUP_INVALID", "\u5907\u4EFD\u635F\u574F\u3001\u4E0D\u5B8C\u6574\u6216\u4E0E\u5F53\u524D\u7248\u672C\u4E0D\u517C\u5BB9\uFF0C\u4E0D\u80FD\u6062\u590D\u3002", 409);
157
+ }
158
+ return this.status();
159
+ }
160
+ async close() {
161
+ this.closed = true;
162
+ this.abort?.abort();
163
+ await this.task;
164
+ }
165
+ }
166
+ export {
167
+ BackupManager,
168
+ backupRequestSchema
169
+ };
170
+ //# sourceMappingURL=manager.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/backups/manager.ts"],
4
+ "sourcesContent": ["import { setTimeout } from 'node:timers/promises';\nimport { z } from 'zod';\nimport type { BackupController, BackupRecord, BackupStatus } from '../shared/backups.js';\nimport { AppError } from '../shared/errors.js';\nimport type { GatewayService } from '../server/service.js';\nimport { acquireMaintenanceLock } from './lock.js';\nimport { backupFailure, type BackupStore } from './store.js';\n\nexport const backupRequestSchema = z.object({ clientOperationId: z.string().uuid() }).strict();\nconst ongoing = (record: BackupRecord) =>\n ['waiting', 'copying', 'verifying'].includes(record.phase);\n\nexport class BackupManager implements BackupController {\n private active?: BackupRecord;\n private abort?: AbortController;\n private task?: Promise<void>;\n private closed = false;\n constructor(\n readonly store: BackupStore,\n private readonly service: GatewayService,\n private readonly options: {\n pollMs?: number;\n timeoutMs?: number;\n unsupportedReason?: string;\n } = {},\n ) {}\n async status(): Promise<BackupStatus> {\n const records = (await this.store.list()).map((record) =>\n this.active?.id === record.id\n ? this.active\n : ongoing(record)\n ? {\n ...record,\n phase: 'interrupted' as const,\n message: '\u5907\u4EFD\u8FDB\u7A0B\u5DF2\u4E2D\u65AD\uFF0C\u672A\u751F\u6210\u53EF\u6062\u590D\u5FEB\u7167\u3002',\n }\n : record,\n );\n return {\n directory: this.store.options.directory,\n schemaVersion: this.store.options.contract.schemaVersion,\n records,\n active: this.active,\n supported: !this.options.unsupportedReason,\n unsupportedReason: this.options.unsupportedReason,\n };\n }\n async start(input: { clientOperationId: string }) {\n const { clientOperationId: id } = backupRequestSchema.parse(input);\n if (this.closed) throw new AppError('BACKUP_UNAVAILABLE', '\u670D\u52A1\u6B63\u5728\u5173\u95ED\u3002', 503);\n if (this.options.unsupportedReason)\n throw new AppError('BACKUP_UNAVAILABLE', this.options.unsupportedReason, 409);\n if (this.active?.id === id || (await this.store.list()).some((record) => record.id === id))\n return this.status();\n if (this.active) throw new AppError('BACKUP_BUSY', '\u5DF2\u6709\u5907\u4EFD\u6B63\u5728\u5904\u7406\u3002', 409);\n if (this.service.isInMaintenance()) throw this.service.maintenanceError();\n const release = await acquireMaintenanceLock(this.store.options.dataDir);\n if (this.closed) {\n await release();\n throw new AppError('BACKUP_UNAVAILABLE', '\u670D\u52A1\u6B63\u5728\u5173\u95ED\u3002', 503);\n }\n const abort = new AbortController();\n const record: BackupRecord = {\n id,\n phase: 'waiting',\n trigger: 'manual',\n createdAt: Date.now(),\n updatedAt: Date.now(),\n path: this.store.path(id),\n message: '\u7B49\u5F85\u4EFB\u52A1\u3001\u5BA1\u6279\u3001\u8F93\u5165\u53CA\u5F85\u6838\u5BF9\u64CD\u4F5C\u7ED3\u675F',\n };\n this.active = record;\n this.abort = abort;\n const preparation = this.store.record(record);\n this.task = (async () => {\n try {\n await preparation;\n } catch {\n await release();\n this.active = undefined;\n this.abort = undefined;\n return;\n }\n await this.run(record, abort, release);\n })();\n try {\n await preparation;\n } catch (error) {\n throw new AppError('BACKUP_FAILED', backupFailure(error), 500);\n }\n return this.status();\n }\n private async run(record: BackupRecord, abort: AbortController, release: () => Promise<void>) {\n let gated = false;\n try {\n for (;;) {\n abort.signal.throwIfAborted();\n if (Date.now() - record.createdAt > (this.options.timeoutMs ?? 30 * 60_000))\n throw new AppError('BACKUP_TIMEOUT', '\u7B49\u5F85\u5B89\u5168\u70B9\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002', 409);\n const result = this.service.prepareUpdate('backup');\n if (result.ready) {\n gated = true;\n break;\n }\n if (record.message !== result.reason) {\n record.message = result.reason ?? '\u7B49\u5F85\u4EFB\u52A1\u7ED3\u675F';\n record.updatedAt = Date.now();\n await this.store.record(record);\n }\n await setTimeout(this.options.pollMs ?? 250, undefined, { signal: abort.signal });\n }\n await this.store.create({\n id: record.id,\n trigger: 'manual',\n reserved: true,\n signal: abort.signal,\n progress: (next) => {\n this.active = next;\n },\n });\n } catch (error) {\n if (this.active?.phase !== 'failed' && this.active?.phase !== 'cancelled') {\n record.phase = abort.signal.aborted ? 'cancelled' : 'failed';\n record.message = abort.signal.aborted\n ? '\u5907\u4EFD\u5DF2\u53D6\u6D88\u3002'\n : error instanceof AppError\n ? error.message\n : backupFailure(error);\n record.updatedAt = Date.now();\n await this.store.record(record).catch(() => {});\n }\n } finally {\n if (gated) this.service.setMaintenance(false);\n await release();\n this.active = undefined;\n this.abort = undefined;\n }\n }\n async cancel(id: string) {\n if (this.active?.id === id) {\n this.abort?.abort();\n await this.task;\n }\n return this.status();\n }\n async verify(id: string) {\n if (this.active && !ongoing(this.active)) await this.task;\n if (this.active) throw new AppError('BACKUP_BUSY', '\u8BF7\u7B49\u5F85\u5F53\u524D\u5907\u4EFD\u5B8C\u6210\u3002', 409);\n try {\n await this.store.verify(id);\n const record = (await this.store.list()).find((entry) => entry.id === id);\n if (record)\n await this.store.record({\n ...record,\n phase: 'succeeded',\n message: '\u5907\u4EFD\u5DF2\u91CD\u65B0\u9A8C\u8BC1\uFF0C\u5B8C\u6574\u6027\u68C0\u67E5\u901A\u8FC7\u3002',\n updatedAt: Date.now(),\n });\n } catch {\n const record = (await this.store.list()).find((entry) => entry.id === id);\n if (record)\n await this.store\n .record({\n ...record,\n phase: 'corrupt',\n message: '\u5907\u4EFD\u635F\u574F\u3001\u4E0D\u5B8C\u6574\u6216\u4E0E\u5F53\u524D\u7248\u672C\u4E0D\u517C\u5BB9\uFF0C\u4E0D\u80FD\u6062\u590D\u3002',\n updatedAt: Date.now(),\n })\n .catch(() => {});\n throw new AppError('BACKUP_INVALID', '\u5907\u4EFD\u635F\u574F\u3001\u4E0D\u5B8C\u6574\u6216\u4E0E\u5F53\u524D\u7248\u672C\u4E0D\u517C\u5BB9\uFF0C\u4E0D\u80FD\u6062\u590D\u3002', 409);\n }\n return this.status();\n }\n async close() {\n this.closed = true;\n this.abort?.abort();\n await this.task;\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["record"]
7
+ }
@@ -0,0 +1,188 @@
1
+ import { randomUUID, createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { chmod, lstat, mkdir, readFile, rename, rm } from "node:fs/promises";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { z } from "zod";
6
+ import { AppError } from "../shared/errors.js";
7
+ import { acquireInstanceLock } from "../server/instance-lock.js";
8
+ import { parseConfig } from "../server/config.js";
9
+ import { migrateSqlite } from "../server/db/history.js";
10
+ import { copyDurable, readJson, saveJson, syncDirectory } from "../updates/files.js";
11
+ import { acquireMaintenanceLock } from "./lock.js";
12
+ const fileNames = ["app.sqlite", "app.sqlite-wal", "app.sqlite-shm", "config.json"];
13
+ const journalSchema = z.object({
14
+ id: z.string().uuid(),
15
+ backupId: z.string(),
16
+ phase: z.enum(["prepared", "replacing", "committed", "rolledBack"]),
17
+ originals: z.array(
18
+ z.object({ name: z.enum(fileNames), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict()
19
+ ).max(4)
20
+ }).strict();
21
+ const journalPath = (store) => join(store.options.directory, "restore.json");
22
+ const stagePath = (store, id) => join(store.options.dataDir, `.covro-restore-${id}`);
23
+ const configStage = (store, id) => join(dirname(store.options.config), `.covro-restore-${id}.tmp`);
24
+ async function hash(path) {
25
+ const info = await lstat(path);
26
+ if (!info.isFile() || info.isSymbolicLink())
27
+ throw new Error("Recovery file is not a regular file");
28
+ const digest = createHash("sha256");
29
+ for await (const chunk of createReadStream(path)) digest.update(chunk);
30
+ return digest.digest("hex");
31
+ }
32
+ async function assertNoPendingRestore(directory) {
33
+ const value = await readJson(join(directory, "restore.json"));
34
+ if (value === void 0) return;
35
+ const journal = journalSchema.parse(value);
36
+ if (["prepared", "replacing"].includes(journal.phase))
37
+ throw new AppError(
38
+ "RESTORE_PENDING",
39
+ "\u4E0A\u6B21\u6062\u590D\u5C1A\u672A\u5B8C\u6210\uFF0C\u62D2\u7EDD\u5F00\u653E\u5199\u5165\uFF1B\u8BF7\u8FD0\u884C covro backup repair\u3002",
40
+ 503
41
+ );
42
+ }
43
+ async function restoreOriginals(store, journal) {
44
+ const directory = stagePath(store, journal.id);
45
+ for (const file of journal.originals)
46
+ if (await hash(join(directory, "original", file.name)) !== file.sha256)
47
+ throw new Error("Recovery copy checksum mismatch");
48
+ for (const name of fileNames) {
49
+ const target = name === "config.json" ? store.options.config : join(store.options.dataDir, name);
50
+ const original = journal.originals.find((file) => file.name === name);
51
+ if (original) {
52
+ await copyDurable(join(directory, "original", name), target);
53
+ await chmod(target, 384);
54
+ } else await rm(target, { force: true });
55
+ }
56
+ await rm(configStage(store, journal.id), { force: true });
57
+ await saveJson(journalPath(store), { ...journal, phase: "rolledBack" });
58
+ }
59
+ async function previewRestore(store, id) {
60
+ const manifest = await store.verify(id);
61
+ if (resolve(manifest.configPath) !== resolve(store.options.config) || resolve(manifest.dataDir) !== resolve(store.options.dataDir))
62
+ throw new AppError(
63
+ "RESTORE_INSTANCE_MISMATCH",
64
+ "\u6B64\u5907\u4EFD\u5C5E\u4E8E\u5176\u4ED6\u914D\u7F6E\u6216\u6570\u636E\u76EE\u5F55\uFF0C\u4E0D\u80FD\u8986\u76D6\u5F53\u524D\u5B9E\u4F8B\u3002",
65
+ 409
66
+ );
67
+ const config = parseConfig(
68
+ JSON.parse(await readFile(join(store.path(id), "config.json"), "utf8")),
69
+ store.options.config
70
+ );
71
+ if (config.dataDir !== resolve(store.options.dataDir))
72
+ throw new AppError("RESTORE_CONFIG_MISMATCH", "\u5907\u4EFD\u914D\u7F6E\u4F1A\u5207\u6362\u6570\u636E\u76EE\u5F55\uFF0C\u62D2\u7EDD\u6062\u590D\u3002", 409);
73
+ return {
74
+ backupId: id,
75
+ appVersion: manifest.appVersion,
76
+ backupSchema: manifest.schemaVersion,
77
+ targetSchema: store.options.contract.schemaVersion,
78
+ overwrite: [store.options.config, join(store.options.dataDir, "app.sqlite")],
79
+ warning: "\u5C06\u8986\u76D6\u670D\u52A1\u914D\u7F6E\u53CA Covro \u5E94\u7528\u6570\u636E\uFF0C\u4E22\u5F03\u5907\u4EFD\u4E4B\u540E\u7684\u5E94\u7528\u6570\u636E\uFF1BCodex \u5386\u53F2\u4E0E\u9879\u76EE\u6587\u4EF6\u4E0D\u53C2\u4E0E\u6062\u590D\u3002",
80
+ confirmation: `covro backup restore ${id} --confirm ${id} --config ${JSON.stringify(store.options.config)}`
81
+ };
82
+ }
83
+ async function restoreBackup(store, id, confirm) {
84
+ const preview = await previewRestore(store, id);
85
+ if (confirm === void 0) return { ...preview, restored: false };
86
+ if (confirm !== id)
87
+ throw new AppError("RESTORE_CONFIRMATION", "\u786E\u8BA4\u503C\u5FC5\u987B\u4E0E\u5F85\u6062\u590D\u7684\u5907\u4EFD ID \u5B8C\u5168\u4E00\u81F4\u3002", 400);
88
+ const releaseInstance = await acquireInstanceLock(store.options.dataDir);
89
+ let releaseMaintenance;
90
+ let journal;
91
+ try {
92
+ releaseMaintenance = await acquireMaintenanceLock(store.options.dataDir);
93
+ await assertNoPendingRestore(store.options.directory);
94
+ await previewRestore(store, id);
95
+ const operation = randomUUID();
96
+ const directory = stagePath(store, operation);
97
+ await mkdir(join(directory, "original"), { recursive: true, mode: 448 });
98
+ const originals = [];
99
+ for (const name of fileNames) {
100
+ const source = name === "config.json" ? store.options.config : join(store.options.dataDir, name);
101
+ try {
102
+ await hash(source);
103
+ await copyDurable(source, join(directory, "original", name));
104
+ await chmod(join(directory, "original", name), 384);
105
+ originals.push({ name, sha256: await hash(join(directory, "original", name)) });
106
+ } catch (error) {
107
+ if (error.code !== "ENOENT") throw error;
108
+ }
109
+ }
110
+ await copyDurable(join(store.path(id), "app.sqlite"), join(directory, "app.sqlite"));
111
+ const staged = new store.options.Sqlite(join(directory, "app.sqlite"));
112
+ try {
113
+ migrateSqlite(staged, store.options.migrations);
114
+ staged.pragma("journal_mode = DELETE");
115
+ if (staged.pragma("integrity_check", { simple: true }) !== "ok" || staged.pragma("foreign_key_check").length)
116
+ throw new Error("Staged migration failed integrity check");
117
+ } finally {
118
+ staged.close();
119
+ }
120
+ await chmod(join(directory, "app.sqlite"), 384);
121
+ await copyDurable(join(store.path(id), "config.json"), configStage(store, operation));
122
+ await chmod(configStage(store, operation), 384);
123
+ journal = { id: operation, backupId: id, phase: "prepared", originals };
124
+ await saveJson(journalPath(store), journal);
125
+ journal.phase = "replacing";
126
+ await saveJson(journalPath(store), journal);
127
+ for (const name of ["app.sqlite-wal", "app.sqlite-shm"])
128
+ await rm(join(store.options.dataDir, name), { force: true });
129
+ await rename(join(directory, "app.sqlite"), join(store.options.dataDir, "app.sqlite"));
130
+ await rename(configStage(store, operation), store.options.config);
131
+ await syncDirectory(store.options.dataDir);
132
+ await syncDirectory(dirname(store.options.config));
133
+ await saveJson(journalPath(store), { ...journal, phase: "committed" });
134
+ return {
135
+ ...preview,
136
+ restored: true,
137
+ recoveryCopy: directory,
138
+ message: "\u6062\u590D\u5B8C\u6210\uFF0C\u670D\u52A1\u4FDD\u6301\u505C\u6B62\uFF1B\u8FD0\u884C covro start \u542F\u52A8\u3002"
139
+ };
140
+ } catch (error) {
141
+ if (journal) {
142
+ try {
143
+ await restoreOriginals(store, journal);
144
+ } catch {
145
+ throw new AppError(
146
+ "RESTORE_PENDING",
147
+ "\u6062\u590D\u5931\u8D25\u4E14\u539F\u6570\u636E\u5C1A\u672A\u6062\u590D\uFF0C\u5DF2\u4FDD\u7559\u73B0\u573A\uFF1B\u8BF7\u8FD0\u884C covro backup repair\u3002",
148
+ 500
149
+ );
150
+ }
151
+ }
152
+ if (!journal && error instanceof AppError) throw error;
153
+ throw new AppError(
154
+ "RESTORE_FAILED",
155
+ "\u6062\u590D\u672A\u5B8C\u6210\uFF0C\u539F\u6570\u636E\u5DF2\u4FDD\u7559\uFF1B\u8BF7\u68C0\u67E5\u78C1\u76D8\u3001\u6743\u9650\u53CA\u5907\u4EFD\u540E\u91CD\u8BD5\u3002",
156
+ 500
157
+ );
158
+ } finally {
159
+ await releaseMaintenance?.();
160
+ await releaseInstance();
161
+ }
162
+ }
163
+ async function repairRestore(store) {
164
+ const releaseInstance = await acquireInstanceLock(store.options.dataDir);
165
+ let releaseMaintenance;
166
+ try {
167
+ releaseMaintenance = await acquireMaintenanceLock(store.options.dataDir);
168
+ const journal = journalSchema.parse(await readJson(journalPath(store)));
169
+ if (["committed", "rolledBack"].includes(journal.phase))
170
+ return { repaired: false, phase: journal.phase };
171
+ await restoreOriginals(store, journal);
172
+ return {
173
+ repaired: true,
174
+ phase: "rolledBack",
175
+ message: "\u5DF2\u6062\u590D\u6062\u590D\u64CD\u4F5C\u524D\u7684\u6587\u4EF6\uFF0C\u670D\u52A1\u4FDD\u6301\u505C\u6B62\u3002"
176
+ };
177
+ } finally {
178
+ await releaseMaintenance?.();
179
+ await releaseInstance();
180
+ }
181
+ }
182
+ export {
183
+ assertNoPendingRestore,
184
+ previewRestore,
185
+ repairRestore,
186
+ restoreBackup
187
+ };
188
+ //# sourceMappingURL=restore.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/backups/restore.ts"],
4
+ "sourcesContent": ["import { randomUUID, createHash } from 'node:crypto';\nimport { createReadStream } from 'node:fs';\nimport { chmod, lstat, mkdir, readFile, rename, rm } from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport { z } from 'zod';\nimport { AppError } from '../shared/errors.js';\nimport { acquireInstanceLock } from '../server/instance-lock.js';\nimport { parseConfig } from '../server/config.js';\nimport { migrateSqlite } from '../server/db/history.js';\nimport { copyDurable, readJson, saveJson, syncDirectory } from '../updates/files.js';\nimport { acquireMaintenanceLock } from './lock.js';\nimport { type BackupStore } from './store.js';\n\nconst fileNames = ['app.sqlite', 'app.sqlite-wal', 'app.sqlite-shm', 'config.json'] as const;\nconst journalSchema = z\n .object({\n id: z.string().uuid(),\n backupId: z.string(),\n phase: z.enum(['prepared', 'replacing', 'committed', 'rolledBack']),\n originals: z\n .array(\n z.object({ name: z.enum(fileNames), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict(),\n )\n .max(4),\n })\n .strict();\ntype Journal = z.infer<typeof journalSchema>;\nconst journalPath = (store: BackupStore) => join(store.options.directory, 'restore.json');\nconst stagePath = (store: BackupStore, id: string) =>\n join(store.options.dataDir, `.covro-restore-${id}`);\nconst configStage = (store: BackupStore, id: string) =>\n join(dirname(store.options.config), `.covro-restore-${id}.tmp`);\nasync function hash(path: string) {\n const info = await lstat(path);\n if (!info.isFile() || info.isSymbolicLink())\n throw new Error('Recovery file is not a regular file');\n const digest = createHash('sha256');\n for await (const chunk of createReadStream(path)) digest.update(chunk);\n return digest.digest('hex');\n}\nexport async function assertNoPendingRestore(directory: string) {\n const value = await readJson(join(directory, 'restore.json'));\n if (value === undefined) return;\n const journal = journalSchema.parse(value);\n if (['prepared', 'replacing'].includes(journal.phase))\n throw new AppError(\n 'RESTORE_PENDING',\n '\u4E0A\u6B21\u6062\u590D\u5C1A\u672A\u5B8C\u6210\uFF0C\u62D2\u7EDD\u5F00\u653E\u5199\u5165\uFF1B\u8BF7\u8FD0\u884C covro backup repair\u3002',\n 503,\n );\n}\nasync function restoreOriginals(store: BackupStore, journal: Journal) {\n const directory = stagePath(store, journal.id);\n for (const file of journal.originals)\n if ((await hash(join(directory, 'original', file.name))) !== file.sha256)\n throw new Error('Recovery copy checksum mismatch');\n for (const name of fileNames) {\n const target =\n name === 'config.json' ? store.options.config : join(store.options.dataDir, name);\n const original = journal.originals.find((file) => file.name === name);\n if (original) {\n await copyDurable(join(directory, 'original', name), target);\n await chmod(target, 0o600);\n } else await rm(target, { force: true });\n }\n await rm(configStage(store, journal.id), { force: true });\n await saveJson(journalPath(store), { ...journal, phase: 'rolledBack' });\n}\nexport async function previewRestore(store: BackupStore, id: string) {\n const manifest = await store.verify(id);\n if (\n resolve(manifest.configPath) !== resolve(store.options.config) ||\n resolve(manifest.dataDir) !== resolve(store.options.dataDir)\n )\n throw new AppError(\n 'RESTORE_INSTANCE_MISMATCH',\n '\u6B64\u5907\u4EFD\u5C5E\u4E8E\u5176\u4ED6\u914D\u7F6E\u6216\u6570\u636E\u76EE\u5F55\uFF0C\u4E0D\u80FD\u8986\u76D6\u5F53\u524D\u5B9E\u4F8B\u3002',\n 409,\n );\n const config = parseConfig(\n JSON.parse(await readFile(join(store.path(id), 'config.json'), 'utf8')),\n store.options.config,\n );\n if (config.dataDir !== resolve(store.options.dataDir))\n throw new AppError('RESTORE_CONFIG_MISMATCH', '\u5907\u4EFD\u914D\u7F6E\u4F1A\u5207\u6362\u6570\u636E\u76EE\u5F55\uFF0C\u62D2\u7EDD\u6062\u590D\u3002', 409);\n return {\n backupId: id,\n appVersion: manifest.appVersion,\n backupSchema: manifest.schemaVersion,\n targetSchema: store.options.contract.schemaVersion,\n overwrite: [store.options.config, join(store.options.dataDir, 'app.sqlite')],\n warning:\n '\u5C06\u8986\u76D6\u670D\u52A1\u914D\u7F6E\u53CA Covro \u5E94\u7528\u6570\u636E\uFF0C\u4E22\u5F03\u5907\u4EFD\u4E4B\u540E\u7684\u5E94\u7528\u6570\u636E\uFF1BCodex \u5386\u53F2\u4E0E\u9879\u76EE\u6587\u4EF6\u4E0D\u53C2\u4E0E\u6062\u590D\u3002',\n confirmation: `covro backup restore ${id} --confirm ${id} --config ${JSON.stringify(store.options.config)}`,\n };\n}\n/** Offline only: gateway startup, explicit migrations and restore all share the instance lock. */\nexport async function restoreBackup(store: BackupStore, id: string, confirm?: string) {\n const preview = await previewRestore(store, id);\n if (confirm === undefined) return { ...preview, restored: false as const };\n if (confirm !== id)\n throw new AppError('RESTORE_CONFIRMATION', '\u786E\u8BA4\u503C\u5FC5\u987B\u4E0E\u5F85\u6062\u590D\u7684\u5907\u4EFD ID \u5B8C\u5168\u4E00\u81F4\u3002', 400);\n const releaseInstance = await acquireInstanceLock(store.options.dataDir);\n let releaseMaintenance: (() => Promise<void>) | undefined;\n let journal: Journal | undefined;\n try {\n releaseMaintenance = await acquireMaintenanceLock(store.options.dataDir);\n await assertNoPendingRestore(store.options.directory);\n await previewRestore(store, id); // Revalidate after obtaining exclusive ownership.\n const operation = randomUUID();\n const directory = stagePath(store, operation);\n await mkdir(join(directory, 'original'), { recursive: true, mode: 0o700 });\n const originals: Journal['originals'] = [];\n for (const name of fileNames) {\n const source =\n name === 'config.json' ? store.options.config : join(store.options.dataDir, name);\n try {\n await hash(source);\n await copyDurable(source, join(directory, 'original', name));\n await chmod(join(directory, 'original', name), 0o600);\n originals.push({ name, sha256: await hash(join(directory, 'original', name)) });\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n }\n await copyDurable(join(store.path(id), 'app.sqlite'), join(directory, 'app.sqlite'));\n const staged = new store.options.Sqlite(join(directory, 'app.sqlite'));\n try {\n migrateSqlite(staged, store.options.migrations);\n staged.pragma('journal_mode = DELETE');\n if (\n staged.pragma('integrity_check', { simple: true }) !== 'ok' ||\n (staged.pragma('foreign_key_check') as unknown[]).length\n )\n throw new Error('Staged migration failed integrity check');\n } finally {\n staged.close();\n }\n await chmod(join(directory, 'app.sqlite'), 0o600);\n await copyDurable(join(store.path(id), 'config.json'), configStage(store, operation));\n await chmod(configStage(store, operation), 0o600);\n journal = { id: operation, backupId: id, phase: 'prepared', originals };\n await saveJson(journalPath(store), journal);\n journal.phase = 'replacing';\n await saveJson(journalPath(store), journal);\n for (const name of ['app.sqlite-wal', 'app.sqlite-shm'])\n await rm(join(store.options.dataDir, name), { force: true });\n await rename(join(directory, 'app.sqlite'), join(store.options.dataDir, 'app.sqlite'));\n await rename(configStage(store, operation), store.options.config);\n await syncDirectory(store.options.dataDir);\n await syncDirectory(dirname(store.options.config));\n await saveJson(journalPath(store), { ...journal, phase: 'committed' });\n return {\n ...preview,\n restored: true as const,\n recoveryCopy: directory,\n message: '\u6062\u590D\u5B8C\u6210\uFF0C\u670D\u52A1\u4FDD\u6301\u505C\u6B62\uFF1B\u8FD0\u884C covro start \u542F\u52A8\u3002',\n };\n } catch (error) {\n if (journal) {\n try {\n await restoreOriginals(store, journal);\n } catch {\n throw new AppError(\n 'RESTORE_PENDING',\n '\u6062\u590D\u5931\u8D25\u4E14\u539F\u6570\u636E\u5C1A\u672A\u6062\u590D\uFF0C\u5DF2\u4FDD\u7559\u73B0\u573A\uFF1B\u8BF7\u8FD0\u884C covro backup repair\u3002',\n 500,\n );\n }\n }\n if (!journal && error instanceof AppError) throw error;\n throw new AppError(\n 'RESTORE_FAILED',\n '\u6062\u590D\u672A\u5B8C\u6210\uFF0C\u539F\u6570\u636E\u5DF2\u4FDD\u7559\uFF1B\u8BF7\u68C0\u67E5\u78C1\u76D8\u3001\u6743\u9650\u53CA\u5907\u4EFD\u540E\u91CD\u8BD5\u3002',\n 500,\n );\n } finally {\n await releaseMaintenance?.();\n await releaseInstance();\n }\n}\nexport async function repairRestore(store: BackupStore) {\n const releaseInstance = await acquireInstanceLock(store.options.dataDir);\n let releaseMaintenance: (() => Promise<void>) | undefined;\n try {\n releaseMaintenance = await acquireMaintenanceLock(store.options.dataDir);\n const journal = journalSchema.parse(await readJson(journalPath(store)));\n if (['committed', 'rolledBack'].includes(journal.phase))\n return { repaired: false, phase: journal.phase };\n await restoreOriginals(store, journal);\n return {\n repaired: true,\n phase: 'rolledBack',\n message: '\u5DF2\u6062\u590D\u6062\u590D\u64CD\u4F5C\u524D\u7684\u6587\u4EF6\uFF0C\u670D\u52A1\u4FDD\u6301\u505C\u6B62\u3002',\n };\n } finally {\n await releaseMaintenance?.();\n await releaseInstance();\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }