@elie-laloum/outpost 2.0.0 → 3.0.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 (300) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/adapters/agents/claude-diagnostics.d.ts +1 -0
  3. package/dist/adapters/agents/claude-diagnostics.js +7 -0
  4. package/dist/adapters/agents/claude-diagnostics.js.map +1 -0
  5. package/dist/adapters/agents/cli-diagnostics.constants.d.ts +12 -0
  6. package/dist/adapters/agents/cli-diagnostics.constants.js +24 -0
  7. package/dist/adapters/agents/cli-diagnostics.constants.js.map +1 -0
  8. package/dist/adapters/agents/cli-diagnostics.d.ts +4 -0
  9. package/dist/adapters/agents/cli-diagnostics.js +14 -0
  10. package/dist/adapters/agents/cli-diagnostics.js.map +1 -0
  11. package/dist/adapters/agents/cli-diagnostics.types.d.ts +13 -0
  12. package/dist/adapters/agents/cli-diagnostics.types.js +2 -0
  13. package/dist/adapters/agents/cli-diagnostics.types.js.map +1 -0
  14. package/dist/adapters/agents/codex-diagnostics.d.ts +1 -0
  15. package/dist/adapters/agents/codex-diagnostics.js +7 -0
  16. package/dist/adapters/agents/codex-diagnostics.js.map +1 -0
  17. package/dist/adapters/agents/protocol-fixtures.constants.d.ts +2 -0
  18. package/dist/adapters/agents/protocol-fixtures.constants.js +68 -0
  19. package/dist/adapters/agents/protocol-fixtures.constants.js.map +1 -0
  20. package/dist/adapters/agents/protocol-fixtures.types.d.ts +6 -0
  21. package/dist/adapters/agents/protocol-fixtures.types.js +2 -0
  22. package/dist/adapters/agents/protocol-fixtures.types.js.map +1 -0
  23. package/dist/application/diagnostic-probe.d.ts +3 -0
  24. package/dist/application/diagnostic-probe.js +55 -0
  25. package/dist/application/diagnostic-probe.js.map +1 -0
  26. package/dist/application/doctor-agent.constants.d.ts +9 -0
  27. package/dist/application/doctor-agent.constants.js +8 -0
  28. package/dist/application/doctor-agent.constants.js.map +1 -0
  29. package/dist/application/doctor-agent.d.ts +3 -0
  30. package/dist/application/doctor-agent.js +53 -0
  31. package/dist/application/doctor-agent.js.map +1 -0
  32. package/dist/application/doctor-image.d.ts +3 -0
  33. package/dist/application/doctor-image.js +133 -0
  34. package/dist/application/doctor-image.js.map +1 -0
  35. package/dist/application/doctor-protocol.d.ts +3 -0
  36. package/dist/application/doctor-protocol.js +29 -0
  37. package/dist/application/doctor-protocol.js.map +1 -0
  38. package/dist/application/doctor-protocol.types.d.ts +10 -0
  39. package/dist/application/doctor-protocol.types.js +2 -0
  40. package/dist/application/doctor-protocol.types.js.map +1 -0
  41. package/dist/application/doctor-sandbox.constants.d.ts +5 -0
  42. package/dist/application/doctor-sandbox.constants.js +6 -0
  43. package/dist/application/doctor-sandbox.constants.js.map +1 -0
  44. package/dist/application/doctor-sandbox.d.ts +4 -0
  45. package/dist/application/doctor-sandbox.js +133 -0
  46. package/dist/application/doctor-sandbox.js.map +1 -0
  47. package/dist/application/doctor-sandbox.types.d.ts +23 -0
  48. package/dist/application/doctor-sandbox.types.js +2 -0
  49. package/dist/application/doctor-sandbox.types.js.map +1 -0
  50. package/dist/application/doctor-transfers.constants.d.ts +6 -0
  51. package/dist/application/doctor-transfers.constants.js +7 -0
  52. package/dist/application/doctor-transfers.constants.js.map +1 -0
  53. package/dist/application/doctor-transfers.d.ts +4 -0
  54. package/dist/application/doctor-transfers.js +88 -0
  55. package/dist/application/doctor-transfers.js.map +1 -0
  56. package/dist/application/doctor.constants.d.ts +9 -0
  57. package/dist/application/doctor.constants.js +15 -0
  58. package/dist/application/doctor.constants.js.map +1 -0
  59. package/dist/application/doctor.d.ts +3 -0
  60. package/dist/application/doctor.js +106 -0
  61. package/dist/application/doctor.js.map +1 -0
  62. package/dist/application/doctor.types.d.ts +39 -0
  63. package/dist/application/doctor.types.js +2 -0
  64. package/dist/application/doctor.types.js.map +1 -0
  65. package/dist/application/execution.js +1 -0
  66. package/dist/application/execution.js.map +1 -1
  67. package/dist/application/outpost.types.d.ts +2 -0
  68. package/dist/application/outpost.types.js.map +1 -1
  69. package/dist/application/recovery-checksum-capture.d.ts +2 -0
  70. package/dist/application/recovery-checksum-capture.js +32 -0
  71. package/dist/application/recovery-checksum-capture.js.map +1 -0
  72. package/dist/application/recovery-checksum-paths.d.ts +2 -0
  73. package/dist/application/recovery-checksum-paths.js +11 -0
  74. package/dist/application/recovery-checksum-paths.js.map +1 -0
  75. package/dist/application/recovery-checksums.constants.d.ts +5 -0
  76. package/dist/application/recovery-checksums.constants.js +6 -0
  77. package/dist/application/recovery-checksums.constants.js.map +1 -0
  78. package/dist/application/recovery-checksums.d.ts +3 -0
  79. package/dist/application/recovery-checksums.js +94 -0
  80. package/dist/application/recovery-checksums.js.map +1 -0
  81. package/dist/application/recovery-checksums.types.d.ts +17 -0
  82. package/dist/application/recovery-checksums.types.js +2 -0
  83. package/dist/application/recovery-checksums.types.js.map +1 -0
  84. package/dist/application/recovery-inspection.d.ts +2 -0
  85. package/dist/application/recovery-inspection.js +27 -0
  86. package/dist/application/recovery-inspection.js.map +1 -0
  87. package/dist/application/recovery-inspection.types.d.ts +15 -0
  88. package/dist/application/recovery-inspection.types.js +2 -0
  89. package/dist/application/recovery-inspection.types.js.map +1 -0
  90. package/dist/application/recovery-restorability.d.ts +2 -0
  91. package/dist/application/recovery-restorability.js +137 -0
  92. package/dist/application/recovery-restorability.js.map +1 -0
  93. package/dist/application/recovery-retention-policy.d.ts +2 -0
  94. package/dist/application/recovery-retention-policy.js +21 -0
  95. package/dist/application/recovery-retention-policy.js.map +1 -0
  96. package/dist/application/recovery-retention.constants.d.ts +3 -0
  97. package/dist/application/recovery-retention.constants.js +2 -0
  98. package/dist/application/recovery-retention.constants.js.map +1 -0
  99. package/dist/application/recovery-retention.d.ts +4 -0
  100. package/dist/application/recovery-retention.js +202 -0
  101. package/dist/application/recovery-retention.js.map +1 -0
  102. package/dist/application/recovery-retention.types.d.ts +52 -0
  103. package/dist/application/recovery-retention.types.js +2 -0
  104. package/dist/application/recovery-retention.types.js.map +1 -0
  105. package/dist/application/recovery-verification.constants.d.ts +5 -0
  106. package/dist/application/recovery-verification.constants.js +10 -0
  107. package/dist/application/recovery-verification.constants.js.map +1 -0
  108. package/dist/application/recovery-verification.d.ts +2 -0
  109. package/dist/application/recovery-verification.js +107 -0
  110. package/dist/application/recovery-verification.js.map +1 -0
  111. package/dist/application/recovery-verification.types.d.ts +26 -0
  112. package/dist/application/recovery-verification.types.js +2 -0
  113. package/dist/application/recovery-verification.types.js.map +1 -0
  114. package/dist/application/remote-apply.js +24 -6
  115. package/dist/application/remote-apply.js.map +1 -1
  116. package/dist/application/remote-backup.js +6 -2
  117. package/dist/application/remote-backup.js.map +1 -1
  118. package/dist/application/remote-download.js +36 -4
  119. package/dist/application/remote-download.js.map +1 -1
  120. package/dist/application/remote-validation.js +19 -0
  121. package/dist/application/remote-validation.js.map +1 -1
  122. package/dist/application/remote-workspace.constants.d.ts +3 -0
  123. package/dist/application/remote-workspace.constants.js +2 -0
  124. package/dist/application/remote-workspace.constants.js.map +1 -0
  125. package/dist/application/remote-workspace.js +6 -0
  126. package/dist/application/remote-workspace.js.map +1 -1
  127. package/dist/application/remote-workspace.types.d.ts +3 -1
  128. package/dist/application/remote-workspace.types.js.map +1 -1
  129. package/dist/application/sandbox.js +11 -0
  130. package/dist/application/sandbox.js.map +1 -1
  131. package/dist/application/task-usage.d.ts +4 -0
  132. package/dist/application/task-usage.js +41 -0
  133. package/dist/application/task-usage.js.map +1 -0
  134. package/dist/application/task-usage.types.d.ts +5 -0
  135. package/dist/application/task-usage.types.js +2 -0
  136. package/dist/application/task-usage.types.js.map +1 -0
  137. package/dist/application/tasks.js +23 -2
  138. package/dist/application/tasks.js.map +1 -1
  139. package/dist/cli/commands.js +4 -0
  140. package/dist/cli/commands.js.map +1 -1
  141. package/dist/cli/doctor-command.d.ts +3 -0
  142. package/dist/cli/doctor-command.js +38 -0
  143. package/dist/cli/doctor-command.js.map +1 -0
  144. package/dist/cli/main.constants.d.ts +28 -1
  145. package/dist/cli/main.constants.js +15 -0
  146. package/dist/cli/main.constants.js.map +1 -1
  147. package/dist/cli/recovery-command.d.ts +2 -0
  148. package/dist/cli/recovery-command.js +69 -0
  149. package/dist/cli/recovery-command.js.map +1 -0
  150. package/dist/cli/recovery-prune-command.d.ts +2 -0
  151. package/dist/cli/recovery-prune-command.js +34 -0
  152. package/dist/cli/recovery-prune-command.js.map +1 -0
  153. package/dist/cli/recovery-verify-command.d.ts +2 -0
  154. package/dist/cli/recovery-verify-command.js +43 -0
  155. package/dist/cli/recovery-verify-command.js.map +1 -0
  156. package/dist/domain/ports.d.ts +1 -1
  157. package/dist/domain/ports.js.map +1 -1
  158. package/dist/domain/sandbox.types.d.ts +12 -0
  159. package/dist/domain/sandbox.types.js.map +1 -1
  160. package/dist/domain/workflow/budget.constants.d.ts +1 -0
  161. package/dist/domain/workflow/budget.constants.js +7 -0
  162. package/dist/domain/workflow/budget.constants.js.map +1 -0
  163. package/dist/domain/workflow/budget.d.ts +9 -0
  164. package/dist/domain/workflow/budget.js +67 -0
  165. package/dist/domain/workflow/budget.js.map +1 -0
  166. package/dist/domain/workflow/budget.types.d.ts +15 -0
  167. package/dist/domain/workflow/budget.types.js +2 -0
  168. package/dist/domain/workflow/budget.types.js.map +1 -0
  169. package/dist/domain/workflow/task-runner.js +8 -1
  170. package/dist/domain/workflow/task-runner.js.map +1 -1
  171. package/dist/domain/workflow/workflow-scheduler.js +4 -2
  172. package/dist/domain/workflow/workflow-scheduler.js.map +1 -1
  173. package/dist/domain/workflow/workflow-state.js +29 -0
  174. package/dist/domain/workflow/workflow-state.js.map +1 -1
  175. package/dist/domain/workflow.d.ts +2 -0
  176. package/dist/domain/workflow.js +1 -0
  177. package/dist/domain/workflow.js.map +1 -1
  178. package/dist/domain/workflow.types.d.ts +10 -1
  179. package/dist/domain/workflow.types.js.map +1 -1
  180. package/dist/index.d.ts +12 -3
  181. package/dist/index.js +5 -1
  182. package/dist/index.js.map +1 -1
  183. package/dist/infrastructure/file-manifest.d.ts +6 -0
  184. package/dist/infrastructure/file-manifest.js +91 -0
  185. package/dist/infrastructure/file-manifest.js.map +1 -0
  186. package/dist/infrastructure/git/lock-inspection.constants.d.ts +4 -0
  187. package/dist/infrastructure/git/lock-inspection.constants.js +5 -0
  188. package/dist/infrastructure/git/lock-inspection.constants.js.map +1 -0
  189. package/dist/infrastructure/git/lock-inspection.d.ts +3 -0
  190. package/dist/infrastructure/git/lock-inspection.js +64 -0
  191. package/dist/infrastructure/git/lock-inspection.js.map +1 -0
  192. package/dist/infrastructure/git/lock-inspection.types.d.ts +27 -0
  193. package/dist/infrastructure/git/lock-inspection.types.js +2 -0
  194. package/dist/infrastructure/git/lock-inspection.types.js.map +1 -0
  195. package/dist/infrastructure/git/lock.d.ts +1 -0
  196. package/dist/infrastructure/git/lock.js +70 -29
  197. package/dist/infrastructure/git/lock.js.map +1 -1
  198. package/dist/infrastructure/git/process-identity.d.ts +3 -0
  199. package/dist/infrastructure/git/process-identity.js +73 -0
  200. package/dist/infrastructure/git/process-identity.js.map +1 -0
  201. package/dist/infrastructure/git/process-identity.types.d.ts +10 -0
  202. package/dist/infrastructure/git/process-identity.types.js +2 -0
  203. package/dist/infrastructure/git/process-identity.types.js.map +1 -0
  204. package/dist/infrastructure/git/verification-command.d.ts +1 -0
  205. package/dist/infrastructure/git/verification-command.js +36 -0
  206. package/dist/infrastructure/git/verification-command.js.map +1 -0
  207. package/dist/infrastructure/git/workspace-lease.js +1 -0
  208. package/dist/infrastructure/git/workspace-lease.js.map +1 -1
  209. package/dist/infrastructure/git/workspace.js +5 -1
  210. package/dist/infrastructure/git/workspace.js.map +1 -1
  211. package/dist/infrastructure/git/worktree-inspection.constants.d.ts +5 -0
  212. package/dist/infrastructure/git/worktree-inspection.constants.js +16 -0
  213. package/dist/infrastructure/git/worktree-inspection.constants.js.map +1 -0
  214. package/dist/infrastructure/git/worktree-inspection.d.ts +3 -0
  215. package/dist/infrastructure/git/worktree-inspection.js +120 -0
  216. package/dist/infrastructure/git/worktree-inspection.js.map +1 -0
  217. package/dist/infrastructure/git/worktree-inspection.types.d.ts +19 -0
  218. package/dist/infrastructure/git/worktree-inspection.types.js +2 -0
  219. package/dist/infrastructure/git/worktree-inspection.types.js.map +1 -0
  220. package/dist/infrastructure/inspection-file.constants.d.ts +1 -0
  221. package/dist/infrastructure/inspection-file.constants.js +5 -0
  222. package/dist/infrastructure/inspection-file.constants.js.map +1 -0
  223. package/dist/infrastructure/inspection-file.d.ts +1 -0
  224. package/dist/infrastructure/inspection-file.js +43 -0
  225. package/dist/infrastructure/inspection-file.js.map +1 -0
  226. package/dist/infrastructure/inspection-hash.constants.d.ts +1 -0
  227. package/dist/infrastructure/inspection-hash.constants.js +2 -0
  228. package/dist/infrastructure/inspection-hash.constants.js.map +1 -0
  229. package/dist/infrastructure/inspection-hash.d.ts +2 -0
  230. package/dist/infrastructure/inspection-hash.js +62 -0
  231. package/dist/infrastructure/inspection-hash.js.map +1 -0
  232. package/dist/infrastructure/inspection-hash.types.d.ts +5 -0
  233. package/dist/infrastructure/inspection-hash.types.js +2 -0
  234. package/dist/infrastructure/inspection-hash.types.js.map +1 -0
  235. package/dist/infrastructure/journal-retention.constants.d.ts +4 -0
  236. package/dist/infrastructure/journal-retention.constants.js +5 -0
  237. package/dist/infrastructure/journal-retention.constants.js.map +1 -0
  238. package/dist/infrastructure/journal-retention.d.ts +2 -0
  239. package/dist/infrastructure/journal-retention.js +51 -0
  240. package/dist/infrastructure/journal-retention.js.map +1 -0
  241. package/dist/infrastructure/journal.js +37 -13
  242. package/dist/infrastructure/journal.js.map +1 -1
  243. package/dist/infrastructure/opentelemetry.constants.d.ts +10 -0
  244. package/dist/infrastructure/opentelemetry.constants.js +11 -0
  245. package/dist/infrastructure/opentelemetry.constants.js.map +1 -0
  246. package/dist/infrastructure/opentelemetry.d.ts +3 -0
  247. package/dist/infrastructure/opentelemetry.js +131 -0
  248. package/dist/infrastructure/opentelemetry.js.map +1 -0
  249. package/dist/infrastructure/opentelemetry.types.d.ts +21 -0
  250. package/dist/infrastructure/opentelemetry.types.js +2 -0
  251. package/dist/infrastructure/opentelemetry.types.js.map +1 -0
  252. package/dist/infrastructure/storage-inventory.constants.d.ts +6 -0
  253. package/dist/infrastructure/storage-inventory.constants.js +11 -0
  254. package/dist/infrastructure/storage-inventory.constants.js.map +1 -0
  255. package/dist/infrastructure/storage-inventory.d.ts +2 -0
  256. package/dist/infrastructure/storage-inventory.js +123 -0
  257. package/dist/infrastructure/storage-inventory.js.map +1 -0
  258. package/dist/infrastructure/storage-inventory.types.d.ts +38 -0
  259. package/dist/infrastructure/storage-inventory.types.js +2 -0
  260. package/dist/infrastructure/storage-inventory.types.js.map +1 -0
  261. package/dist/infrastructure/transfer.js +12 -0
  262. package/dist/infrastructure/transfer.js.map +1 -1
  263. package/dist/providers/cloud.constants.d.ts +1 -0
  264. package/dist/providers/cloud.constants.js +1 -0
  265. package/dist/providers/cloud.constants.js.map +1 -1
  266. package/dist/providers/container-cache.constants.d.ts +11 -0
  267. package/dist/providers/container-cache.constants.js +9 -0
  268. package/dist/providers/container-cache.constants.js.map +1 -0
  269. package/dist/providers/container-cache.d.ts +4 -0
  270. package/dist/providers/container-cache.js +48 -0
  271. package/dist/providers/container-cache.js.map +1 -0
  272. package/dist/providers/container-cache.types.d.ts +8 -0
  273. package/dist/providers/container-cache.types.js +2 -0
  274. package/dist/providers/container-cache.types.js.map +1 -0
  275. package/dist/providers/container-plan.js +3 -1
  276. package/dist/providers/container-plan.js.map +1 -1
  277. package/dist/providers/container.js +31 -1
  278. package/dist/providers/container.js.map +1 -1
  279. package/dist/providers/container.types.d.ts +2 -0
  280. package/dist/providers/container.types.js.map +1 -1
  281. package/dist/providers/daytona-command.js +7 -4
  282. package/dist/providers/daytona-command.js.map +1 -1
  283. package/dist/providers/daytona.js +13 -4
  284. package/dist/providers/daytona.js.map +1 -1
  285. package/dist/providers/diagnostic-container.d.ts +4 -0
  286. package/dist/providers/diagnostic-container.js +15 -0
  287. package/dist/providers/diagnostic-container.js.map +1 -0
  288. package/dist/providers/docker.d.ts +1 -0
  289. package/dist/providers/docker.js.map +1 -1
  290. package/dist/providers/file-batches.constants.d.ts +10 -0
  291. package/dist/providers/file-batches.constants.js +57 -0
  292. package/dist/providers/file-batches.constants.js.map +1 -0
  293. package/dist/providers/file-batches.d.ts +2 -0
  294. package/dist/providers/file-batches.js +151 -0
  295. package/dist/providers/file-batches.js.map +1 -0
  296. package/dist/providers/podman.d.ts +1 -0
  297. package/dist/providers/podman.js.map +1 -1
  298. package/dist/providers/vercel.js +3 -1
  299. package/dist/providers/vercel.js.map +1 -1
  300. package/package.json +12 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery-retention.constants.js","sourceRoot":"","sources":["../../src/application/recovery-retention.constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,cAAc,EAAE,MAAM,EAAW,CAAC","sourcesContent":["export const recoveryRetentionLimits = { maxPolicyBytes: 65_536 } as const;\n"]}
@@ -0,0 +1,4 @@
1
+ import type { RecoveryPruneResult, RecoveryQuotaOptions, RecoveryRetentionOptions, RecoveryRetentionPlan } from "./recovery-retention.types.ts";
2
+ export declare function planRecoveryRetention(options: RecoveryRetentionOptions): Promise<RecoveryRetentionPlan>;
3
+ export declare function pruneRecoveryRetention(plan: RecoveryRetentionPlan): Promise<RecoveryPruneResult>;
4
+ export declare function assertRecoveryQuota(options: RecoveryQuotaOptions): Promise<void>;
@@ -0,0 +1,202 @@
1
+ import { lstat, rm } from "node:fs/promises";
2
+ import { OutpostError, invariant } from "../domain/errors.js";
3
+ import { git } from "../infrastructure/git/command.js";
4
+ import { lock, lockPath } from "../infrastructure/git/lock.js";
5
+ import { closedJournal } from "../infrastructure/journal-retention.js";
6
+ import { inspectRecovery } from "./recovery-inspection.js";
7
+ import { retentionPolicy } from "./recovery-retention-policy.js";
8
+ async function workspaceReason(entry, inspection) {
9
+ const workspace = inspection.git?.workspaces.find((value) => value.path === entry.path);
10
+ if (!workspace || workspace.state !== "registered")
11
+ return "WORKSPACE_UNREGISTERED_OR_UNKNOWN";
12
+ if (workspace.branch === null)
13
+ return "DETACHED_WORKSPACE";
14
+ if (workspace.dirty)
15
+ return "DIRTY_WORKSPACE";
16
+ if (workspace.locked)
17
+ return "GIT_LOCKED_WORKSPACE";
18
+ try {
19
+ await lstat(lockPath(inspection.repository, workspace.branch));
20
+ return "OPERATION_LOCK_PRESENT";
21
+ }
22
+ catch (error) {
23
+ if (!(error &&
24
+ typeof error === "object" &&
25
+ "code" in error &&
26
+ error.code === "ENOENT"))
27
+ return "OWNERSHIP_UNKNOWN";
28
+ }
29
+ try {
30
+ const ignored = await git(entry.path, [
31
+ "ls-files",
32
+ "--others",
33
+ "--ignored",
34
+ "--exclude-standard",
35
+ "-z",
36
+ ]);
37
+ if (ignored.length)
38
+ return "IGNORED_FILES";
39
+ }
40
+ catch {
41
+ return "WORKSPACE_STATE_UNKNOWN";
42
+ }
43
+ return "ELIGIBLE";
44
+ }
45
+ export async function planRecoveryRetention(options) {
46
+ retentionPolicy(options.policy);
47
+ const policy = structuredClone(options.policy);
48
+ const inspection = await inspectRecovery({
49
+ ...options,
50
+ git: true,
51
+ locks: true,
52
+ });
53
+ const entries = [];
54
+ const inspectedAt = new Date().toISOString();
55
+ const complete = inspection.complete && inspection.git?.complete !== false;
56
+ for (const category of inspection.categories) {
57
+ for (const entry of category.entries) {
58
+ let reason = "RECOVERY_DATA_PROTECTED";
59
+ if (category.name === "locks")
60
+ reason = "OWNERSHIP_RECORD_PROTECTED";
61
+ if (category.name === "logs")
62
+ reason = "LOG_ACTIVITY_OR_CONTENT_UNKNOWN";
63
+ if (category.name === "workspaces")
64
+ reason = "SCOPE_NOT_SELECTED";
65
+ if (category.name === "workspaces" &&
66
+ policy.scopes.includes("clean-workspaces"))
67
+ reason = await workspaceReason(entry, inspection);
68
+ if (category.name === "logs" &&
69
+ policy.scopes.includes("closed-logs") &&
70
+ entry.kind === "file" &&
71
+ entry.name.endsWith(".jsonl") &&
72
+ (await closedJournal(entry.path)))
73
+ reason = "ELIGIBLE";
74
+ if (category.name === "logs" && entry.name.endsWith(".closed.json"))
75
+ reason = "CLOSURE_RECORD_PROTECTED";
76
+ if (reason === "ELIGIBLE" &&
77
+ (!entry.modifiedAt ||
78
+ Date.parse(inspectedAt) - Date.parse(entry.modifiedAt) <
79
+ policy.minAgeMs))
80
+ reason = "RETENTION_AGE";
81
+ if (reason === "ELIGIBLE" && (!complete || !entry.complete))
82
+ reason = "INCOMPLETE_INVENTORY";
83
+ const workspace = inspection.git?.workspaces.find((value) => value.path === entry.path);
84
+ entries.push({
85
+ path: entry.path,
86
+ category: category.name,
87
+ bytes: entry.bytes,
88
+ eligible: reason === "ELIGIBLE",
89
+ reason,
90
+ ...(entry.modifiedAt ? { modifiedAt: entry.modifiedAt } : {}),
91
+ ...(workspace?.state === "registered" && workspace.branch
92
+ ? { branch: workspace.branch, head: workspace.head }
93
+ : {}),
94
+ });
95
+ }
96
+ }
97
+ const projectedBytes = inspection.usage.bytes -
98
+ entries
99
+ .filter((entry) => entry.eligible)
100
+ .reduce((sum, entry) => sum +
101
+ entry.bytes +
102
+ (entry.category === "logs"
103
+ ? (entries.find((other) => other.path === `${entry.path}.closed.json`)?.bytes ?? 0)
104
+ : 0), 0);
105
+ const workspaces = entries.filter((entry) => entry.category === "workspaces" && !entry.eligible).length;
106
+ const exceeded = (policy.maxBytes !== undefined && projectedBytes > policy.maxBytes) ||
107
+ (policy.maxWorkspaces !== undefined && workspaces > policy.maxWorkspaces);
108
+ return {
109
+ repository: inspection.repository,
110
+ policy,
111
+ inspectedAt,
112
+ inspection,
113
+ entries,
114
+ complete,
115
+ usageBytes: inspection.usage.bytes,
116
+ projectedBytes,
117
+ quota: !complete ? "unknown" : exceeded ? "exceeded" : "within",
118
+ };
119
+ }
120
+ export async function pruneRecoveryRetention(plan) {
121
+ retentionPolicy(plan.policy);
122
+ invariant(plan.complete, "Cannot prune an incomplete retention plan");
123
+ const removed = [];
124
+ const retained = [];
125
+ for (const candidate of plan.entries.filter((entry) => entry.eligible)) {
126
+ const fresh = await planRecoveryRetention({
127
+ repository: plan.repository,
128
+ policy: plan.policy,
129
+ });
130
+ const entry = fresh.entries.find((value) => value.path === candidate.path);
131
+ if (!entry?.eligible ||
132
+ entry.head !== candidate.head ||
133
+ entry.branch !== candidate.branch ||
134
+ entry.bytes !== candidate.bytes ||
135
+ entry.modifiedAt !== candidate.modifiedAt) {
136
+ retained.push({ path: candidate.path, reason: "PLAN_CHANGED" });
137
+ continue;
138
+ }
139
+ try {
140
+ if (entry.category === "workspaces" && entry.branch) {
141
+ const release = await lock(plan.repository, entry.branch);
142
+ try {
143
+ const status = await git(entry.path, [
144
+ "status",
145
+ "--porcelain=v1",
146
+ "--ignored",
147
+ "--untracked-files=all",
148
+ ]);
149
+ invariant(!status.length, "Workspace changed before pruning");
150
+ invariant((await git(entry.path, ["rev-parse", "HEAD"])).trim() ===
151
+ entry.head, "Workspace HEAD changed before pruning");
152
+ invariant((await git(entry.path, ["symbolic-ref", "--short", "HEAD"])).trim() === entry.branch, "Workspace branch changed before pruning");
153
+ await git(plan.repository, ["worktree", "remove", entry.path]);
154
+ }
155
+ finally {
156
+ await release();
157
+ }
158
+ }
159
+ else {
160
+ const release = await lock(plan.repository, `journal:${entry.path}`);
161
+ try {
162
+ invariant(entry.category === "logs" && (await closedJournal(entry.path)), "Journal changed before pruning");
163
+ await rm(entry.path);
164
+ await rm(`${entry.path}.closed.json`);
165
+ }
166
+ finally {
167
+ await release();
168
+ }
169
+ }
170
+ removed.push(entry.path);
171
+ }
172
+ catch {
173
+ retained.push({
174
+ path: candidate.path,
175
+ reason: "REVALIDATION_OR_REMOVAL_FAILED",
176
+ });
177
+ }
178
+ }
179
+ return {
180
+ removed,
181
+ retained,
182
+ after: await planRecoveryRetention({
183
+ repository: plan.repository,
184
+ policy: plan.policy,
185
+ }),
186
+ };
187
+ }
188
+ export async function assertRecoveryQuota(options) {
189
+ invariant(Number.isSafeInteger(options.maxBytes) && options.maxBytes >= 0, "maxBytes must be a nonnegative integer");
190
+ const reserve = options.reserveBytes ?? 0;
191
+ invariant(Number.isSafeInteger(reserve) && reserve >= 0, "reserveBytes must be a nonnegative integer");
192
+ const inspection = await inspectRecovery(options);
193
+ if (!inspection.complete ||
194
+ inspection.usage.bytes + reserve > options.maxBytes)
195
+ throw new OutpostError("workspace", "Outpost storage quota admission refused", {
196
+ usageBytes: inspection.usage.bytes,
197
+ reserveBytes: reserve,
198
+ maxBytes: options.maxBytes,
199
+ complete: inspection.complete,
200
+ });
201
+ }
202
+ //# sourceMappingURL=recovery-retention.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery-retention.js","sourceRoot":"","sources":["../../src/application/recovery-retention.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,GAAG,EAAE,MAAM,kCAAkC,CAAC;AACvD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAC;AAGvE,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAUjE,KAAK,UAAU,eAAe,CAC5B,KAAmB,EACnB,UAA8B;IAE9B,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE,UAAU,CAAC,IAAI,CAC/C,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CACrC,CAAC;IACF,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,KAAK,KAAK,YAAY;QAChD,OAAO,mCAAmC,CAAC;IAC7C,IAAI,SAAS,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,oBAAoB,CAAC;IAC3D,IAAI,SAAS,CAAC,KAAK;QAAE,OAAO,iBAAiB,CAAC;IAC9C,IAAI,SAAS,CAAC,MAAM;QAAE,OAAO,sBAAsB,CAAC;IACpD,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/D,OAAO,wBAAwB,CAAC;IAClC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,CACH,KAAK;YACL,OAAO,KAAK,KAAK,QAAQ;YACzB,MAAM,IAAI,KAAK;YACf,KAAK,CAAC,IAAI,KAAK,QAAQ,CACxB;YACC,OAAO,mBAAmB,CAAC;IAC/B,CAAC;IACD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE;YACpC,UAAU;YACV,UAAU;YACV,WAAW;YACX,oBAAoB;YACpB,IAAI;SACL,CAAC,CAAC;QACH,IAAI,OAAO,CAAC,MAAM;YAAE,OAAO,eAAe,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,yBAAyB,CAAC;IACnC,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,OAAiC;IAEjC,eAAe,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAChC,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/C,MAAM,UAAU,GAAG,MAAM,eAAe,CAAC;QACvC,GAAG,OAAO;QACV,GAAG,EAAE,IAAI;QACT,KAAK,EAAE,IAAI;KACZ,CAAC,CAAC;IACH,MAAM,OAAO,GAA6B,EAAE,CAAC;IAC7C,MAAM,WAAW,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC7C,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,IAAI,UAAU,CAAC,GAAG,EAAE,QAAQ,KAAK,KAAK,CAAC;IAC3E,KAAK,MAAM,QAAQ,IAAI,UAAU,CAAC,UAAU,EAAE,CAAC;QAC7C,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;YACrC,IAAI,MAAM,GAAG,yBAAyB,CAAC;YACvC,IAAI,QAAQ,CAAC,IAAI,KAAK,OAAO;gBAAE,MAAM,GAAG,4BAA4B,CAAC;YACrE,IAAI,QAAQ,CAAC,IAAI,KAAK,MAAM;gBAAE,MAAM,GAAG,iCAAiC,CAAC;YACzE,IAAI,QAAQ,CAAC,IAAI,KAAK,YAAY;gBAAE,MAAM,GAAG,oBAAoB,CAAC;YAClE,IACE,QAAQ,CAAC,IAAI,KAAK,YAAY;gBAC9B,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAC;gBAE1C,MAAM,GAAG,MAAM,eAAe,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;YACpD,IACE,QAAQ,CAAC,IAAI,KAAK,MAAM;gBACxB,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;gBACrC,KAAK,CAAC,IAAI,KAAK,MAAM;gBACrB,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAC7B,CAAC,MAAM,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAEjC,MAAM,GAAG,UAAU,CAAC;YACtB,IAAI,QAAQ,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC;gBACjE,MAAM,GAAG,0BAA0B,CAAC;YACtC,IACE,MAAM,KAAK,UAAU;gBACrB,CAAC,CAAC,KAAK,CAAC,UAAU;oBAChB,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC;wBACpD,MAAM,CAAC,QAAQ,CAAC;gBAEpB,MAAM,GAAG,eAAe,CAAC;YAC3B,IAAI,MAAM,KAAK,UAAU,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;gBACzD,MAAM,GAAG,sBAAsB,CAAC;YAClC,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE,UAAU,CAAC,IAAI,CAC/C,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CACrC,CAAC;YACF,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,QAAQ,EAAE,QAAQ,CAAC,IAAI;gBACvB,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,QAAQ,EAAE,MAAM,KAAK,UAAU;gBAC/B,MAAM;gBACN,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC7D,GAAG,CAAC,SAAS,EAAE,KAAK,KAAK,YAAY,IAAI,SAAS,CAAC,MAAM;oBACvD,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE;oBACpD,CAAC,CAAC,EAAE,CAAC;aACR,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,MAAM,cAAc,GAClB,UAAU,CAAC,KAAK,CAAC,KAAK;QACtB,OAAO;aACJ,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC;aACjC,MAAM,CACL,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CACb,GAAG;YACH,KAAK,CAAC,KAAK;YACX,CAAC,KAAK,CAAC,QAAQ,KAAK,MAAM;gBACxB,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CACX,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,KAAK,CAAC,IAAI,cAAc,CACtD,EAAE,KAAK,IAAI,CAAC,CAAC;gBAChB,CAAC,CAAC,CAAC,CAAC,EACR,CAAC,CACF,CAAC;IACN,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAC/B,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,YAAY,IAAI,CAAC,KAAK,CAAC,QAAQ,CAC9D,CAAC,MAAM,CAAC;IACT,MAAM,QAAQ,GACZ,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,cAAc,GAAG,MAAM,CAAC,QAAQ,CAAC;QACnE,CAAC,MAAM,CAAC,aAAa,KAAK,SAAS,IAAI,UAAU,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC;IAC5E,OAAO;QACL,UAAU,EAAE,UAAU,CAAC,UAAU;QACjC,MAAM;QACN,WAAW;QACX,UAAU;QACV,OAAO;QACP,QAAQ;QACR,UAAU,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK;QAClC,cAAc;QACd,KAAK,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ;KAChE,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,IAA2B;IAE3B,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7B,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,2CAA2C,CAAC,CAAC;IACtE,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,QAAQ,GAA4B,EAAE,CAAC;IAC7C,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QACvE,MAAM,KAAK,GAAG,MAAM,qBAAqB,CAAC;YACxC,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,MAAM,EAAE,IAAI,CAAC,MAAM;SACpB,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,CAAC,CAAC;QAC3E,IACE,CAAC,KAAK,EAAE,QAAQ;YAChB,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI;YAC7B,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM;YACjC,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,KAAK;YAC/B,KAAK,CAAC,UAAU,KAAK,SAAS,CAAC,UAAU,EACzC,CAAC;YACD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;YAChE,SAAS;QACX,CAAC;QACD,IAAI,CAAC;YACH,IAAI,KAAK,CAAC,QAAQ,KAAK,YAAY,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBACpD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;gBAC1D,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE;wBACnC,QAAQ;wBACR,gBAAgB;wBAChB,WAAW;wBACX,uBAAuB;qBACxB,CAAC,CAAC;oBACH,SAAS,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,kCAAkC,CAAC,CAAC;oBAC9D,SAAS,CACP,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE;wBACnD,KAAK,CAAC,IAAI,EACZ,uCAAuC,CACxC,CAAC;oBACF,SAAS,CACP,CACE,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,cAAc,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAC3D,CAAC,IAAI,EAAE,KAAK,KAAK,CAAC,MAAM,EACzB,yCAAyC,CAC1C,CAAC;oBACF,MAAM,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;gBACjE,CAAC;wBAAS,CAAC;oBACT,MAAM,OAAO,EAAE,CAAC;gBAClB,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;gBACrE,IAAI,CAAC;oBACH,SAAS,CACP,KAAK,CAAC,QAAQ,KAAK,MAAM,IAAI,CAAC,MAAM,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAC9D,gCAAgC,CACjC,CAAC;oBACF,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBACrB,MAAM,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,cAAc,CAAC,CAAC;gBACxC,CAAC;wBAAS,CAAC;oBACT,MAAM,OAAO,EAAE,CAAC;gBAClB,CAAC;YACH,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,SAAS,CAAC,IAAI;gBACpB,MAAM,EAAE,gCAAgC;aACzC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO;QACL,OAAO;QACP,QAAQ;QACR,KAAK,EAAE,MAAM,qBAAqB,CAAC;YACjC,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,MAAM,EAAE,IAAI,CAAC,MAAM;SACpB,CAAC;KACH,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAA6B;IAE7B,SAAS,CACP,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,QAAQ,IAAI,CAAC,EAC/D,wCAAwC,CACzC,CAAC;IACF,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,IAAI,CAAC,CAAC;IAC1C,SAAS,CACP,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC,EAC7C,4CAA4C,CAC7C,CAAC;IACF,MAAM,UAAU,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAClD,IACE,CAAC,UAAU,CAAC,QAAQ;QACpB,UAAU,CAAC,KAAK,CAAC,KAAK,GAAG,OAAO,GAAG,OAAO,CAAC,QAAQ;QAEnD,MAAM,IAAI,YAAY,CACpB,WAAW,EACX,yCAAyC,EACzC;YACE,UAAU,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK;YAClC,YAAY,EAAE,OAAO;YACrB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,QAAQ,EAAE,UAAU,CAAC,QAAQ;SAC9B,CACF,CAAC;AACN,CAAC","sourcesContent":["import { lstat, rm } from \"node:fs/promises\";\nimport { OutpostError, invariant } from \"../domain/errors.ts\";\nimport { git } from \"../infrastructure/git/command.ts\";\nimport { lock, lockPath } from \"../infrastructure/git/lock.ts\";\nimport { closedJournal } from \"../infrastructure/journal-retention.ts\";\nimport type { StorageEntry } from \"../infrastructure/storage-inventory.types.ts\";\nimport type { RecoveryInspection } from \"./recovery-inspection.types.ts\";\nimport { inspectRecovery } from \"./recovery-inspection.ts\";\nimport { retentionPolicy } from \"./recovery-retention-policy.ts\";\nimport type {\n RecoveryPruneResult,\n RecoveryRetainedEntry,\n RecoveryQuotaOptions,\n RecoveryRetentionEntry,\n RecoveryRetentionOptions,\n RecoveryRetentionPlan,\n} from \"./recovery-retention.types.ts\";\n\nasync function workspaceReason(\n entry: StorageEntry,\n inspection: RecoveryInspection,\n): Promise<string> {\n const workspace = inspection.git?.workspaces.find(\n (value) => value.path === entry.path,\n );\n if (!workspace || workspace.state !== \"registered\")\n return \"WORKSPACE_UNREGISTERED_OR_UNKNOWN\";\n if (workspace.branch === null) return \"DETACHED_WORKSPACE\";\n if (workspace.dirty) return \"DIRTY_WORKSPACE\";\n if (workspace.locked) return \"GIT_LOCKED_WORKSPACE\";\n try {\n await lstat(lockPath(inspection.repository, workspace.branch));\n return \"OPERATION_LOCK_PRESENT\";\n } catch (error) {\n if (!(\n error &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === \"ENOENT\"\n ))\n return \"OWNERSHIP_UNKNOWN\";\n }\n try {\n const ignored = await git(entry.path, [\n \"ls-files\",\n \"--others\",\n \"--ignored\",\n \"--exclude-standard\",\n \"-z\",\n ]);\n if (ignored.length) return \"IGNORED_FILES\";\n } catch {\n return \"WORKSPACE_STATE_UNKNOWN\";\n }\n return \"ELIGIBLE\";\n}\n\nexport async function planRecoveryRetention(\n options: RecoveryRetentionOptions,\n): Promise<RecoveryRetentionPlan> {\n retentionPolicy(options.policy);\n const policy = structuredClone(options.policy);\n const inspection = await inspectRecovery({\n ...options,\n git: true,\n locks: true,\n });\n const entries: RecoveryRetentionEntry[] = [];\n const inspectedAt = new Date().toISOString();\n const complete = inspection.complete && inspection.git?.complete !== false;\n for (const category of inspection.categories) {\n for (const entry of category.entries) {\n let reason = \"RECOVERY_DATA_PROTECTED\";\n if (category.name === \"locks\") reason = \"OWNERSHIP_RECORD_PROTECTED\";\n if (category.name === \"logs\") reason = \"LOG_ACTIVITY_OR_CONTENT_UNKNOWN\";\n if (category.name === \"workspaces\") reason = \"SCOPE_NOT_SELECTED\";\n if (\n category.name === \"workspaces\" &&\n policy.scopes.includes(\"clean-workspaces\")\n )\n reason = await workspaceReason(entry, inspection);\n if (\n category.name === \"logs\" &&\n policy.scopes.includes(\"closed-logs\") &&\n entry.kind === \"file\" &&\n entry.name.endsWith(\".jsonl\") &&\n (await closedJournal(entry.path))\n )\n reason = \"ELIGIBLE\";\n if (category.name === \"logs\" && entry.name.endsWith(\".closed.json\"))\n reason = \"CLOSURE_RECORD_PROTECTED\";\n if (\n reason === \"ELIGIBLE\" &&\n (!entry.modifiedAt ||\n Date.parse(inspectedAt) - Date.parse(entry.modifiedAt) <\n policy.minAgeMs)\n )\n reason = \"RETENTION_AGE\";\n if (reason === \"ELIGIBLE\" && (!complete || !entry.complete))\n reason = \"INCOMPLETE_INVENTORY\";\n const workspace = inspection.git?.workspaces.find(\n (value) => value.path === entry.path,\n );\n entries.push({\n path: entry.path,\n category: category.name,\n bytes: entry.bytes,\n eligible: reason === \"ELIGIBLE\",\n reason,\n ...(entry.modifiedAt ? { modifiedAt: entry.modifiedAt } : {}),\n ...(workspace?.state === \"registered\" && workspace.branch\n ? { branch: workspace.branch, head: workspace.head }\n : {}),\n });\n }\n }\n const projectedBytes =\n inspection.usage.bytes -\n entries\n .filter((entry) => entry.eligible)\n .reduce(\n (sum, entry) =>\n sum +\n entry.bytes +\n (entry.category === \"logs\"\n ? (entries.find(\n (other) => other.path === `${entry.path}.closed.json`,\n )?.bytes ?? 0)\n : 0),\n 0,\n );\n const workspaces = entries.filter(\n (entry) => entry.category === \"workspaces\" && !entry.eligible,\n ).length;\n const exceeded =\n (policy.maxBytes !== undefined && projectedBytes > policy.maxBytes) ||\n (policy.maxWorkspaces !== undefined && workspaces > policy.maxWorkspaces);\n return {\n repository: inspection.repository,\n policy,\n inspectedAt,\n inspection,\n entries,\n complete,\n usageBytes: inspection.usage.bytes,\n projectedBytes,\n quota: !complete ? \"unknown\" : exceeded ? \"exceeded\" : \"within\",\n };\n}\n\nexport async function pruneRecoveryRetention(\n plan: RecoveryRetentionPlan,\n): Promise<RecoveryPruneResult> {\n retentionPolicy(plan.policy);\n invariant(plan.complete, \"Cannot prune an incomplete retention plan\");\n const removed: string[] = [];\n const retained: RecoveryRetainedEntry[] = [];\n for (const candidate of plan.entries.filter((entry) => entry.eligible)) {\n const fresh = await planRecoveryRetention({\n repository: plan.repository,\n policy: plan.policy,\n });\n const entry = fresh.entries.find((value) => value.path === candidate.path);\n if (\n !entry?.eligible ||\n entry.head !== candidate.head ||\n entry.branch !== candidate.branch ||\n entry.bytes !== candidate.bytes ||\n entry.modifiedAt !== candidate.modifiedAt\n ) {\n retained.push({ path: candidate.path, reason: \"PLAN_CHANGED\" });\n continue;\n }\n try {\n if (entry.category === \"workspaces\" && entry.branch) {\n const release = await lock(plan.repository, entry.branch);\n try {\n const status = await git(entry.path, [\n \"status\",\n \"--porcelain=v1\",\n \"--ignored\",\n \"--untracked-files=all\",\n ]);\n invariant(!status.length, \"Workspace changed before pruning\");\n invariant(\n (await git(entry.path, [\"rev-parse\", \"HEAD\"])).trim() ===\n entry.head,\n \"Workspace HEAD changed before pruning\",\n );\n invariant(\n (\n await git(entry.path, [\"symbolic-ref\", \"--short\", \"HEAD\"])\n ).trim() === entry.branch,\n \"Workspace branch changed before pruning\",\n );\n await git(plan.repository, [\"worktree\", \"remove\", entry.path]);\n } finally {\n await release();\n }\n } else {\n const release = await lock(plan.repository, `journal:${entry.path}`);\n try {\n invariant(\n entry.category === \"logs\" && (await closedJournal(entry.path)),\n \"Journal changed before pruning\",\n );\n await rm(entry.path);\n await rm(`${entry.path}.closed.json`);\n } finally {\n await release();\n }\n }\n removed.push(entry.path);\n } catch {\n retained.push({\n path: candidate.path,\n reason: \"REVALIDATION_OR_REMOVAL_FAILED\",\n });\n }\n }\n return {\n removed,\n retained,\n after: await planRecoveryRetention({\n repository: plan.repository,\n policy: plan.policy,\n }),\n };\n}\n\nexport async function assertRecoveryQuota(\n options: RecoveryQuotaOptions,\n): Promise<void> {\n invariant(\n Number.isSafeInteger(options.maxBytes) && options.maxBytes >= 0,\n \"maxBytes must be a nonnegative integer\",\n );\n const reserve = options.reserveBytes ?? 0;\n invariant(\n Number.isSafeInteger(reserve) && reserve >= 0,\n \"reserveBytes must be a nonnegative integer\",\n );\n const inspection = await inspectRecovery(options);\n if (\n !inspection.complete ||\n inspection.usage.bytes + reserve > options.maxBytes\n )\n throw new OutpostError(\n \"workspace\",\n \"Outpost storage quota admission refused\",\n {\n usageBytes: inspection.usage.bytes,\n reserveBytes: reserve,\n maxBytes: options.maxBytes,\n complete: inspection.complete,\n },\n );\n}\n"]}
@@ -0,0 +1,52 @@
1
+ import type { RecoveryInspection } from "./recovery-inspection.types.ts";
2
+ export interface RecoveryRetentionPolicy {
3
+ readonly version: 1;
4
+ readonly scopes: readonly ("clean-workspaces" | "closed-logs")[];
5
+ readonly minAgeMs: number;
6
+ readonly maxBytes?: number;
7
+ readonly maxWorkspaces?: number;
8
+ }
9
+ export interface RecoveryRetentionOptions {
10
+ readonly repository?: string;
11
+ readonly policy: RecoveryRetentionPolicy;
12
+ readonly maxEntries?: number;
13
+ }
14
+ export interface RecoveryRetentionEntry {
15
+ readonly path: string;
16
+ readonly category: string;
17
+ readonly bytes: number;
18
+ readonly eligible: boolean;
19
+ readonly reason: string;
20
+ readonly branch?: string;
21
+ readonly head?: string;
22
+ readonly modifiedAt?: string;
23
+ }
24
+ export interface RecoveryRetentionPlan {
25
+ readonly repository: string;
26
+ readonly policy: RecoveryRetentionPolicy;
27
+ readonly inspectedAt: string;
28
+ readonly inspection: RecoveryInspection;
29
+ readonly entries: readonly RecoveryRetentionEntry[];
30
+ readonly complete: boolean;
31
+ readonly usageBytes: number;
32
+ readonly projectedBytes: number;
33
+ readonly quota: "within" | "exceeded" | "unknown";
34
+ }
35
+ export interface RecoveryRetainedEntry {
36
+ readonly path: string;
37
+ readonly reason: string;
38
+ }
39
+ export interface RecoveryPruneResult {
40
+ readonly removed: readonly string[];
41
+ readonly retained: readonly {
42
+ readonly path: string;
43
+ readonly reason: string;
44
+ }[];
45
+ readonly after: RecoveryRetentionPlan;
46
+ }
47
+ export interface RecoveryQuotaOptions {
48
+ readonly repository?: string;
49
+ readonly maxBytes: number;
50
+ readonly reserveBytes?: number;
51
+ readonly maxEntries?: number;
52
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=recovery-retention.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery-retention.types.js","sourceRoot":"","sources":["../../src/application/recovery-retention.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { RecoveryInspection } from \"./recovery-inspection.types.ts\";\nexport interface RecoveryRetentionPolicy {\n readonly version: 1;\n readonly scopes: readonly (\"clean-workspaces\" | \"closed-logs\")[];\n readonly minAgeMs: number;\n readonly maxBytes?: number;\n readonly maxWorkspaces?: number;\n}\nexport interface RecoveryRetentionOptions {\n readonly repository?: string;\n readonly policy: RecoveryRetentionPolicy;\n readonly maxEntries?: number;\n}\nexport interface RecoveryRetentionEntry {\n readonly path: string;\n readonly category: string;\n readonly bytes: number;\n readonly eligible: boolean;\n readonly reason: string;\n readonly branch?: string;\n readonly head?: string;\n readonly modifiedAt?: string;\n}\nexport interface RecoveryRetentionPlan {\n readonly repository: string;\n readonly policy: RecoveryRetentionPolicy;\n readonly inspectedAt: string;\n readonly inspection: RecoveryInspection;\n readonly entries: readonly RecoveryRetentionEntry[];\n readonly complete: boolean;\n readonly usageBytes: number;\n readonly projectedBytes: number;\n readonly quota: \"within\" | \"exceeded\" | \"unknown\";\n}\nexport interface RecoveryRetainedEntry {\n readonly path: string;\n readonly reason: string;\n}\nexport interface RecoveryPruneResult {\n readonly removed: readonly string[];\n readonly retained: readonly {\n readonly path: string;\n readonly reason: string;\n }[];\n readonly after: RecoveryRetentionPlan;\n}\nexport interface RecoveryQuotaOptions {\n readonly repository?: string;\n readonly maxBytes: number;\n readonly reserveBytes?: number;\n readonly maxEntries?: number;\n}\n"]}
@@ -0,0 +1,5 @@
1
+ export declare const recoveryVerificationDefaults: {
2
+ readonly maxStateBytes: 65536;
3
+ readonly maxPaths: 1000;
4
+ };
5
+ export declare const recoveryTransferPatches: readonly ["remote.patch", "previous.patch", "previous-index.patch"];
@@ -0,0 +1,10 @@
1
+ export const recoveryVerificationDefaults = {
2
+ maxStateBytes: 65_536,
3
+ maxPaths: 1000,
4
+ };
5
+ export const recoveryTransferPatches = [
6
+ "remote.patch",
7
+ "previous.patch",
8
+ "previous-index.patch",
9
+ ];
10
+ //# sourceMappingURL=recovery-verification.constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery-verification.constants.js","sourceRoot":"","sources":["../../src/application/recovery-verification.constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,4BAA4B,GAAG;IAC1C,aAAa,EAAE,MAAM;IACrB,QAAQ,EAAE,IAAI;CACN,CAAC;AAEX,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,cAAc;IACd,gBAAgB;IAChB,sBAAsB;CACd,CAAC","sourcesContent":["export const recoveryVerificationDefaults = {\n maxStateBytes: 65_536,\n maxPaths: 1000,\n} as const;\n\nexport const recoveryTransferPatches = [\n \"remote.patch\",\n \"previous.patch\",\n \"previous-index.patch\",\n] as const;\n"]}
@@ -0,0 +1,2 @@
1
+ import type { RecoveryVerification, RecoveryVerificationOptions } from "./recovery-verification.types.ts";
2
+ export declare function verifyRecoveryTransfer(path: string, options?: RecoveryVerificationOptions): Promise<RecoveryVerification>;
@@ -0,0 +1,107 @@
1
+ import { verifyRecoveryRestorability } from "./recovery-restorability.js";
2
+ import { verifyRecoveryChecksums } from "./recovery-checksums.js";
3
+ import { invariant, positive } from "../domain/errors.js";
4
+ import { lstat } from "node:fs/promises";
5
+ import { isAbsolute, join, win32 } from "node:path";
6
+ import { directory, safeDestination } from "../infrastructure/files.js";
7
+ import { readInspectionFile } from "../infrastructure/inspection-file.js";
8
+ import { recoveryTransferPatches, recoveryVerificationDefaults, } from "./recovery-verification.constants.js";
9
+ function paths(value) {
10
+ if (!Array.isArray(value) ||
11
+ value.length > recoveryVerificationDefaults.maxPaths)
12
+ return false;
13
+ return (value.every((path) => typeof path === "string" &&
14
+ path.length > 0 &&
15
+ !path.includes("\0") &&
16
+ !isAbsolute(path) &&
17
+ !win32.parse(path).root &&
18
+ !path
19
+ .split(/[\\/]/)
20
+ .some((part) => !part ||
21
+ part === "." ||
22
+ part === ".." ||
23
+ part.toLowerCase() === ".git")) && new Set(value).size === value.length);
24
+ }
25
+ function transferState(value) {
26
+ if (!value || typeof value !== "object")
27
+ return false;
28
+ if (!("previous" in value) ||
29
+ !("next" in value) ||
30
+ !("previousExtras" in value) ||
31
+ !("incoming" in value))
32
+ return false;
33
+ return (typeof value.previous === "string" &&
34
+ /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value.previous) &&
35
+ typeof value.next === "string" &&
36
+ /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value.next) &&
37
+ value.next.length === value.previous.length &&
38
+ paths(value.previousExtras) &&
39
+ paths(value.incoming));
40
+ }
41
+ async function checkFile(root, name, allowSymlink = false) {
42
+ try {
43
+ const path = await safeDestination(root, name);
44
+ const info = await lstat(path);
45
+ if (allowSymlink && info.isSymbolicLink())
46
+ return { path, status: "pass", code: "SYMLINK_PRESENT" };
47
+ if (info.isFile())
48
+ return { path, status: "pass", code: "FILE_PRESENT" };
49
+ return { path, status: "fail", code: "UNEXPECTED_TYPE" };
50
+ }
51
+ catch {
52
+ return { path: join(root, name), status: "fail", code: "FILE_UNAVAILABLE" };
53
+ }
54
+ }
55
+ export async function verifyRecoveryTransfer(path, options = {}) {
56
+ invariant(!options.restorability || !!options.repository, "Restorability verification requires a repository");
57
+ if (options.maxBytes !== undefined)
58
+ positive(options.maxBytes, "maxBytes");
59
+ const root = await directory(path);
60
+ const checks = [];
61
+ const report = () => ({
62
+ directory: root,
63
+ scope: options.restorability
64
+ ? "transfer-restorability"
65
+ : "transfer-structure",
66
+ complete: checks.every((check) => check.status === "pass"),
67
+ integrity: "unverified",
68
+ checks,
69
+ });
70
+ const statePath = join(root, "state.json");
71
+ let state;
72
+ try {
73
+ state = JSON.parse((await readInspectionFile(statePath, recoveryVerificationDefaults.maxStateBytes)).toString("utf8"));
74
+ }
75
+ catch {
76
+ checks.push({ path: statePath, status: "fail", code: "STATE_UNAVAILABLE" });
77
+ return report();
78
+ }
79
+ if (!transferState(state)) {
80
+ checks.push({ path: statePath, status: "fail", code: "INVALID_STATE" });
81
+ return report();
82
+ }
83
+ checks.push({
84
+ path: statePath,
85
+ status: "pass",
86
+ code: "STATE_STRUCTURE_VALID",
87
+ });
88
+ for (const name of recoveryTransferPatches)
89
+ checks.push(await checkFile(root, name));
90
+ if (state.previous !== state.next)
91
+ checks.push(await checkFile(root, "commits.bundle"));
92
+ for (const name of state.previousExtras)
93
+ checks.push(await checkFile(root, `previous-files/${name}`, true));
94
+ for (const name of state.incoming)
95
+ checks.push(await checkFile(root, `incoming/${name}`, true));
96
+ if (options.restorability &&
97
+ options.repository &&
98
+ checks.every((check) => check.status === "pass"))
99
+ checks.push(...(await verifyRecoveryRestorability(root, state, options.repository)));
100
+ if (options.checksums && checks.every((check) => check.status === "pass")) {
101
+ const checksums = await verifyRecoveryChecksums(root, state, options.maxBytes);
102
+ checks.push(...checksums.checks);
103
+ return { ...report(), integrity: checksums.integrity, checksums };
104
+ }
105
+ return report();
106
+ }
107
+ //# sourceMappingURL=recovery-verification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery-verification.js","sourceRoot":"","sources":["../../src/application/recovery-verification.ts"],"names":[],"mappings":"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","sourcesContent":["import { verifyRecoveryRestorability } from \"./recovery-restorability.ts\";\nimport { verifyRecoveryChecksums } from \"./recovery-checksums.ts\";\nimport { invariant, positive } from \"../domain/errors.ts\";\nimport { lstat } from \"node:fs/promises\";\nimport { isAbsolute, join, win32 } from \"node:path\";\nimport { directory, safeDestination } from \"../infrastructure/files.ts\";\nimport { readInspectionFile } from \"../infrastructure/inspection-file.ts\";\nimport {\n recoveryTransferPatches,\n recoveryVerificationDefaults,\n} from \"./recovery-verification.constants.ts\";\nimport type {\n RecoveryStructureCheck,\n RecoveryTransferState,\n RecoveryVerification,\n RecoveryVerificationOptions,\n} from \"./recovery-verification.types.ts\";\n\nfunction paths(value: unknown): value is readonly string[] {\n if (\n !Array.isArray(value) ||\n value.length > recoveryVerificationDefaults.maxPaths\n )\n return false;\n return (\n value.every(\n (path: unknown) =>\n typeof path === \"string\" &&\n path.length > 0 &&\n !path.includes(\"\\0\") &&\n !isAbsolute(path) &&\n !win32.parse(path).root &&\n !path\n .split(/[\\\\/]/)\n .some(\n (part) =>\n !part ||\n part === \".\" ||\n part === \"..\" ||\n part.toLowerCase() === \".git\",\n ),\n ) && new Set(value).size === value.length\n );\n}\n\nfunction transferState(value: unknown): value is RecoveryTransferState {\n if (!value || typeof value !== \"object\") return false;\n if (\n !(\"previous\" in value) ||\n !(\"next\" in value) ||\n !(\"previousExtras\" in value) ||\n !(\"incoming\" in value)\n )\n return false;\n return (\n typeof value.previous === \"string\" &&\n /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value.previous) &&\n typeof value.next === \"string\" &&\n /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value.next) &&\n value.next.length === value.previous.length &&\n paths(value.previousExtras) &&\n paths(value.incoming)\n );\n}\n\nasync function checkFile(\n root: string,\n name: string,\n allowSymlink = false,\n): Promise<RecoveryStructureCheck> {\n try {\n const path = await safeDestination(root, name);\n const info = await lstat(path);\n if (allowSymlink && info.isSymbolicLink())\n return { path, status: \"pass\", code: \"SYMLINK_PRESENT\" };\n if (info.isFile()) return { path, status: \"pass\", code: \"FILE_PRESENT\" };\n return { path, status: \"fail\", code: \"UNEXPECTED_TYPE\" };\n } catch {\n return { path: join(root, name), status: \"fail\", code: \"FILE_UNAVAILABLE\" };\n }\n}\n\nexport async function verifyRecoveryTransfer(\n path: string,\n options: RecoveryVerificationOptions = {},\n): Promise<RecoveryVerification> {\n invariant(\n !options.restorability || !!options.repository,\n \"Restorability verification requires a repository\",\n );\n if (options.maxBytes !== undefined) positive(options.maxBytes, \"maxBytes\");\n const root = await directory(path);\n const checks: RecoveryStructureCheck[] = [];\n const report = (): RecoveryVerification => ({\n directory: root,\n scope: options.restorability\n ? \"transfer-restorability\"\n : \"transfer-structure\",\n complete: checks.every((check) => check.status === \"pass\"),\n integrity: \"unverified\",\n checks,\n });\n const statePath = join(root, \"state.json\");\n let state: unknown;\n try {\n state = JSON.parse(\n (\n await readInspectionFile(\n statePath,\n recoveryVerificationDefaults.maxStateBytes,\n )\n ).toString(\"utf8\"),\n );\n } catch {\n checks.push({ path: statePath, status: \"fail\", code: \"STATE_UNAVAILABLE\" });\n return report();\n }\n if (!transferState(state)) {\n checks.push({ path: statePath, status: \"fail\", code: \"INVALID_STATE\" });\n return report();\n }\n checks.push({\n path: statePath,\n status: \"pass\",\n code: \"STATE_STRUCTURE_VALID\",\n });\n for (const name of recoveryTransferPatches)\n checks.push(await checkFile(root, name));\n if (state.previous !== state.next)\n checks.push(await checkFile(root, \"commits.bundle\"));\n for (const name of state.previousExtras)\n checks.push(await checkFile(root, `previous-files/${name}`, true));\n for (const name of state.incoming)\n checks.push(await checkFile(root, `incoming/${name}`, true));\n if (\n options.restorability &&\n options.repository &&\n checks.every((check) => check.status === \"pass\")\n )\n checks.push(\n ...(await verifyRecoveryRestorability(root, state, options.repository)),\n );\n if (options.checksums && checks.every((check) => check.status === \"pass\")) {\n const checksums = await verifyRecoveryChecksums(\n root,\n state,\n options.maxBytes,\n );\n checks.push(...checksums.checks);\n return { ...report(), integrity: checksums.integrity, checksums };\n }\n return report();\n}\n"]}
@@ -0,0 +1,26 @@
1
+ import type { RecoveryChecksumResult, RecoveryIntegrity } from "./recovery-checksums.types.ts";
2
+ export interface RecoveryTransferState {
3
+ readonly previous: string;
4
+ readonly next: string;
5
+ readonly previousExtras: readonly string[];
6
+ readonly incoming: readonly string[];
7
+ }
8
+ export interface RecoveryStructureCheck {
9
+ readonly path: string;
10
+ readonly status: "pass" | "fail";
11
+ readonly code: string;
12
+ }
13
+ export interface RecoveryVerification {
14
+ readonly directory: string;
15
+ readonly scope: "transfer-structure" | "transfer-restorability";
16
+ readonly complete: boolean;
17
+ readonly integrity: RecoveryIntegrity;
18
+ readonly checksums?: RecoveryChecksumResult;
19
+ readonly checks: readonly RecoveryStructureCheck[];
20
+ }
21
+ export interface RecoveryVerificationOptions {
22
+ readonly restorability?: boolean;
23
+ readonly repository?: string;
24
+ readonly checksums?: boolean;
25
+ readonly maxBytes?: number;
26
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=recovery-verification.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery-verification.types.js","sourceRoot":"","sources":["../../src/application/recovery-verification.types.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n RecoveryChecksumResult,\n RecoveryIntegrity,\n} from \"./recovery-checksums.types.ts\";\nexport interface RecoveryTransferState {\n readonly previous: string;\n readonly next: string;\n readonly previousExtras: readonly string[];\n readonly incoming: readonly string[];\n}\n\nexport interface RecoveryStructureCheck {\n readonly path: string;\n readonly status: \"pass\" | \"fail\";\n readonly code: string;\n}\n\nexport interface RecoveryVerification {\n readonly directory: string;\n readonly scope: \"transfer-structure\" | \"transfer-restorability\";\n readonly complete: boolean;\n readonly integrity: RecoveryIntegrity;\n readonly checksums?: RecoveryChecksumResult;\n readonly checks: readonly RecoveryStructureCheck[];\n}\n\nexport interface RecoveryVerificationOptions {\n readonly restorability?: boolean;\n readonly repository?: string;\n readonly checksums?: boolean;\n readonly maxBytes?: number;\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { cp, mkdir, readFile, rm } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
1
+ import { cp, lstat, mkdir, readFile, rm, rmdir } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
3
  import { OutpostError } from "../domain/errors.js";
4
4
  import { safeDestination } from "../infrastructure/files.js";
5
5
  import { git } from "../infrastructure/git/command.js";
@@ -18,10 +18,23 @@ export async function applyChanges(context, changes, backup, synchronized, expec
18
18
  previousPatch,
19
19
  ]);
20
20
  await git(workspace.directory, ["reset", "--mixed", "HEAD"]);
21
- for (const file of previousExtras.filter((file) => !protectedFiles.includes(file)))
22
- await rm(await safeDestination(workspace.directory, file), {
23
- force: true,
24
- });
21
+ for (const file of previousExtras.filter((file) => !protectedFiles.includes(file))) {
22
+ const target = await safeDestination(workspace.directory, file);
23
+ await rm(target, { force: true });
24
+ let parent = dirname(target);
25
+ while (parent !== resolve(workspace.directory)) {
26
+ const removed = await rmdir(parent).then(() => true, (error) => {
27
+ if (error.code === "ENOTEMPTY" ||
28
+ error.code === "ENOENT" ||
29
+ error.code === "EEXIST")
30
+ return false;
31
+ throw error;
32
+ });
33
+ if (!removed)
34
+ break;
35
+ parent = dirname(parent);
36
+ }
37
+ }
25
38
  if (head !== synchronized)
26
39
  await git(workspace.directory, ["merge", "--ff-only", "FETCH_HEAD"]);
27
40
  if ((await readFile(patch)).length)
@@ -37,9 +50,14 @@ export async function applyChanges(context, changes, backup, synchronized, expec
37
50
  ]);
38
51
  for (const file of incoming) {
39
52
  const target = await safeDestination(workspace.directory, file);
53
+ if ((await lstat(target).catch(() => undefined))?.isDirectory())
54
+ await rmdir(target);
40
55
  await mkdir(dirname(target), { recursive: true });
41
56
  await cp(join(transfer, "incoming", file), target, {
42
57
  dereference: false,
58
+ verbatimSymlinks: true,
59
+ force: false,
60
+ errorOnExist: true,
43
61
  });
44
62
  }
45
63
  }
@@ -1 +1 @@
1
- {"version":3,"file":"remote-apply.js","sourceRoot":"","sources":["../../src/application/remote-apply.ts"],"names":[],"mappings":"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","sourcesContent":["import { cp, mkdir, readFile, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { OutpostError } from \"../domain/errors.ts\";\nimport { safeDestination } from \"../infrastructure/files.ts\";\nimport { git } from \"../infrastructure/git/command.ts\";\nimport type {\n HostBackup,\n RemoteChanges,\n RemoteWorkspaceContext,\n} from \"./remote-workspace.types.ts\";\nimport { digest } from \"./workspace-fingerprint.ts\";\n\nexport async function applyChanges(\n context: RemoteWorkspaceContext,\n changes: RemoteChanges,\n backup: HostBackup,\n synchronized: string,\n expected: string,\n transfer: string,\n): Promise<void> {\n const {\n workspace,\n options,\n recovery,\n protectedFiles,\n initialPatch,\n initialIndex,\n } = context;\n const { head, patch, incoming } = changes;\n const { previousPatch, previousExtras } = backup;\n if ((await digest(workspace.directory, recovery)) !== expected)\n throw new OutpostError(\n \"conflict\",\n \"Host workspace changed during synchronization\",\n { recovery },\n );\n if ((await readFile(previousPatch)).length)\n await git(workspace.directory, [\n \"apply\",\n \"--reverse\",\n \"--binary\",\n previousPatch,\n ]);\n await git(workspace.directory, [\"reset\", \"--mixed\", \"HEAD\"]);\n for (const file of previousExtras.filter(\n (file) => !protectedFiles.includes(file),\n ))\n await rm(await safeDestination(workspace.directory, file), {\n force: true,\n });\n if (head !== synchronized)\n await git(workspace.directory, [\"merge\", \"--ff-only\", \"FETCH_HEAD\"]);\n if ((await readFile(patch)).length)\n await git(workspace.directory, [\"apply\", \"--binary\", patch]);\n if (!options.includeUncommitted && (await readFile(initialPatch)).length)\n await git(workspace.directory, [\"apply\", \"--binary\", initialPatch]);\n if (!options.includeUncommitted && (await readFile(initialIndex)).length)\n await git(workspace.directory, [\n \"apply\",\n \"--cached\",\n \"--binary\",\n initialIndex,\n ]);\n for (const file of incoming) {\n const target = await safeDestination(workspace.directory, file);\n await mkdir(dirname(target), { recursive: true });\n await cp(join(transfer, \"incoming\", file), target, {\n dereference: false,\n });\n }\n}\n"]}
1
+ {"version":3,"file":"remote-apply.js","sourceRoot":"","sources":["../../src/application/remote-apply.ts"],"names":[],"mappings":"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","sourcesContent":["import { cp, lstat, mkdir, readFile, rm, rmdir } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { OutpostError } from \"../domain/errors.ts\";\nimport { safeDestination } from \"../infrastructure/files.ts\";\nimport { git } from \"../infrastructure/git/command.ts\";\nimport type {\n HostBackup,\n RemoteChanges,\n RemoteWorkspaceContext,\n} from \"./remote-workspace.types.ts\";\nimport { digest } from \"./workspace-fingerprint.ts\";\n\nexport async function applyChanges(\n context: RemoteWorkspaceContext,\n changes: RemoteChanges,\n backup: HostBackup,\n synchronized: string,\n expected: string,\n transfer: string,\n): Promise<void> {\n const {\n workspace,\n options,\n recovery,\n protectedFiles,\n initialPatch,\n initialIndex,\n } = context;\n const { head, patch, incoming } = changes;\n const { previousPatch, previousExtras } = backup;\n if ((await digest(workspace.directory, recovery)) !== expected)\n throw new OutpostError(\n \"conflict\",\n \"Host workspace changed during synchronization\",\n { recovery },\n );\n if ((await readFile(previousPatch)).length)\n await git(workspace.directory, [\n \"apply\",\n \"--reverse\",\n \"--binary\",\n previousPatch,\n ]);\n await git(workspace.directory, [\"reset\", \"--mixed\", \"HEAD\"]);\n for (const file of previousExtras.filter(\n (file) => !protectedFiles.includes(file),\n )) {\n const target = await safeDestination(workspace.directory, file);\n await rm(target, { force: true });\n let parent = dirname(target);\n while (parent !== resolve(workspace.directory)) {\n const removed = await rmdir(parent).then(\n () => true,\n (error: NodeJS.ErrnoException) => {\n if (\n error.code === \"ENOTEMPTY\" ||\n error.code === \"ENOENT\" ||\n error.code === \"EEXIST\"\n )\n return false;\n throw error;\n },\n );\n if (!removed) break;\n parent = dirname(parent);\n }\n }\n if (head !== synchronized)\n await git(workspace.directory, [\"merge\", \"--ff-only\", \"FETCH_HEAD\"]);\n if ((await readFile(patch)).length)\n await git(workspace.directory, [\"apply\", \"--binary\", patch]);\n if (!options.includeUncommitted && (await readFile(initialPatch)).length)\n await git(workspace.directory, [\"apply\", \"--binary\", initialPatch]);\n if (!options.includeUncommitted && (await readFile(initialIndex)).length)\n await git(workspace.directory, [\n \"apply\",\n \"--cached\",\n \"--binary\",\n initialIndex,\n ]);\n for (const file of incoming) {\n const target = await safeDestination(workspace.directory, file);\n if ((await lstat(target).catch(() => undefined))?.isDirectory())\n await rmdir(target);\n await mkdir(dirname(target), { recursive: true });\n await cp(join(transfer, \"incoming\", file), target, {\n dereference: false,\n verbatimSymlinks: true,\n force: false,\n errorOnExist: true,\n });\n }\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { captureRecoveryChecksums } from "./recovery-checksum-capture.js";
1
2
  import { cp, mkdir, writeFile } from "node:fs/promises";
2
3
  import { dirname, join } from "node:path";
3
4
  import { safeDestination } from "../infrastructure/files.js";
@@ -25,14 +26,17 @@ export async function backupHost(context, changes, synchronized, transfer) {
25
26
  await mkdir(dirname(backup), { recursive: true });
26
27
  await cp(await safeDestination(workspace.directory, file), backup, {
27
28
  dereference: false,
29
+ verbatimSymlinks: true,
28
30
  });
29
31
  }
30
- await writeFile(join(transfer, "state.json"), JSON.stringify({
32
+ const state = {
31
33
  previous: synchronized,
32
34
  next: head,
33
35
  previousExtras,
34
36
  incoming,
35
- }));
37
+ };
38
+ await writeFile(join(transfer, "state.json"), JSON.stringify(state));
39
+ await captureRecoveryChecksums(transfer, state);
36
40
  return { previousPatch, previousExtras };
37
41
  }
38
42
  //# sourceMappingURL=remote-backup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"remote-backup.js","sourceRoot":"","sources":["../../src/application/remote-backup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,GAAG,EAAE,MAAM,kCAAkC,CAAC;AAMvD,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAEpD,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,OAA+B,EAC/B,OAAsB,EACtB,YAAoB,EACpB,QAAgB;IAEhB,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IAC9B,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IACnC,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;IACvD,MAAM,GAAG,CAAC,SAAS,CAAC,SAAS,EAAE;QAC7B,MAAM;QACN,UAAU;QACV,MAAM;QACN,YAAY,aAAa,EAAE;KAC5B,CAAC,CAAC;IACH,MAAM,GAAG,CAAC,SAAS,CAAC,SAAS,EAAE;QAC7B,MAAM;QACN,UAAU;QACV,UAAU;QACV,YAAY,IAAI,CAAC,QAAQ,EAAE,sBAAsB,CAAC,EAAE;KACrD,CAAC,CAAC;IACH,MAAM,cAAc,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACzD,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,MAAM,eAAe,CAClC,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC,EAChC,IAAI,CACL,CAAC;QACF,MAAM,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,MAAM,EAAE,CAAC,MAAM,eAAe,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE;YACjE,WAAW,EAAE,KAAK;SACnB,CAAC,CAAC;IACL,CAAC;IACD,MAAM,SAAS,CACb,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,EAC5B,IAAI,CAAC,SAAS,CAAC;QACb,QAAQ,EAAE,YAAY;QACtB,IAAI,EAAE,IAAI;QACV,cAAc;QACd,QAAQ;KACT,CAAC,CACH,CAAC;IAEF,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC;AAC3C,CAAC","sourcesContent":["import { cp, mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { safeDestination } from \"../infrastructure/files.ts\";\nimport { git } from \"../infrastructure/git/command.ts\";\nimport type {\n HostBackup,\n RemoteChanges,\n RemoteWorkspaceContext,\n} from \"./remote-workspace.types.ts\";\nimport { extras } from \"./workspace-fingerprint.ts\";\n\nexport async function backupHost(\n context: RemoteWorkspaceContext,\n changes: RemoteChanges,\n synchronized: string,\n transfer: string,\n): Promise<HostBackup> {\n const { workspace } = context;\n const { head, incoming } = changes;\n const previousPatch = join(transfer, \"previous.patch\");\n await git(workspace.directory, [\n \"diff\",\n \"--binary\",\n \"HEAD\",\n `--output=${previousPatch}`,\n ]);\n await git(workspace.directory, [\n \"diff\",\n \"--cached\",\n \"--binary\",\n `--output=${join(transfer, \"previous-index.patch\")}`,\n ]);\n const previousExtras = await extras(workspace.directory);\n for (const file of previousExtras) {\n const backup = await safeDestination(\n join(transfer, \"previous-files\"),\n file,\n );\n await mkdir(dirname(backup), { recursive: true });\n await cp(await safeDestination(workspace.directory, file), backup, {\n dereference: false,\n });\n }\n await writeFile(\n join(transfer, \"state.json\"),\n JSON.stringify({\n previous: synchronized,\n next: head,\n previousExtras,\n incoming,\n }),\n );\n\n return { previousPatch, previousExtras };\n}\n"]}
1
+ {"version":3,"file":"remote-backup.js","sourceRoot":"","sources":["../../src/application/remote-backup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,GAAG,EAAE,MAAM,kCAAkC,CAAC;AAMvD,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAEpD,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,OAA+B,EAC/B,OAAsB,EACtB,YAAoB,EACpB,QAAgB;IAEhB,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IAC9B,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IACnC,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;IACvD,MAAM,GAAG,CAAC,SAAS,CAAC,SAAS,EAAE;QAC7B,MAAM;QACN,UAAU;QACV,MAAM;QACN,YAAY,aAAa,EAAE;KAC5B,CAAC,CAAC;IACH,MAAM,GAAG,CAAC,SAAS,CAAC,SAAS,EAAE;QAC7B,MAAM;QACN,UAAU;QACV,UAAU;QACV,YAAY,IAAI,CAAC,QAAQ,EAAE,sBAAsB,CAAC,EAAE;KACrD,CAAC,CAAC;IACH,MAAM,cAAc,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACzD,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,MAAM,eAAe,CAClC,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC,EAChC,IAAI,CACL,CAAC;QACF,MAAM,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,MAAM,EAAE,CAAC,MAAM,eAAe,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE;YACjE,WAAW,EAAE,KAAK;YAClB,gBAAgB,EAAE,IAAI;SACvB,CAAC,CAAC;IACL,CAAC;IACD,MAAM,KAAK,GAAG;QACZ,QAAQ,EAAE,YAAY;QACtB,IAAI,EAAE,IAAI;QACV,cAAc;QACd,QAAQ;KACT,CAAC;IACF,MAAM,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;IACrE,MAAM,wBAAwB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAEhD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC;AAC3C,CAAC","sourcesContent":["import { captureRecoveryChecksums } from \"./recovery-checksum-capture.ts\";\nimport { cp, mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { safeDestination } from \"../infrastructure/files.ts\";\nimport { git } from \"../infrastructure/git/command.ts\";\nimport type {\n HostBackup,\n RemoteChanges,\n RemoteWorkspaceContext,\n} from \"./remote-workspace.types.ts\";\nimport { extras } from \"./workspace-fingerprint.ts\";\n\nexport async function backupHost(\n context: RemoteWorkspaceContext,\n changes: RemoteChanges,\n synchronized: string,\n transfer: string,\n): Promise<HostBackup> {\n const { workspace } = context;\n const { head, incoming } = changes;\n const previousPatch = join(transfer, \"previous.patch\");\n await git(workspace.directory, [\n \"diff\",\n \"--binary\",\n \"HEAD\",\n `--output=${previousPatch}`,\n ]);\n await git(workspace.directory, [\n \"diff\",\n \"--cached\",\n \"--binary\",\n `--output=${join(transfer, \"previous-index.patch\")}`,\n ]);\n const previousExtras = await extras(workspace.directory);\n for (const file of previousExtras) {\n const backup = await safeDestination(\n join(transfer, \"previous-files\"),\n file,\n );\n await mkdir(dirname(backup), { recursive: true });\n await cp(await safeDestination(workspace.directory, file), backup, {\n dereference: false,\n verbatimSymlinks: true,\n });\n }\n const state = {\n previous: synchronized,\n next: head,\n previousExtras,\n incoming,\n };\n await writeFile(join(transfer, \"state.json\"), JSON.stringify(state));\n await captureRecoveryChecksums(transfer, state);\n\n return { previousPatch, previousExtras };\n}\n"]}