@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
@@ -1,36 +1,77 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { mkdir, open, readFile, rm } from "node:fs/promises";
2
+ import { mkdir, open, rm } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { OutpostError } from "../../domain/errors.js";
5
- export async function lock(root, key) {
6
- const folder = join(root, ".outpost", "locks");
7
- await mkdir(folder, { recursive: true });
8
- const path = join(folder, `${createHash("sha256").update(key).digest("hex").slice(0, 20)}.json`);
9
- for (let attempt = 0; attempt < 2; attempt++) {
10
- try {
11
- const file = await open(path, "wx", 0o600);
12
- await file.writeFile(JSON.stringify({ pid: process.pid, nonce: randomUUID() }));
13
- await file.close();
14
- return () => rm(path, { force: true });
15
- }
16
- catch (cause) {
17
- if (cause.code !== "EEXIST")
18
- throw cause;
19
- const owner = JSON.parse(await readFile(path, "utf8"));
20
- let alive = true;
21
- try {
22
- if (!owner.pid)
23
- throw new Error();
24
- process.kill(owner.pid, 0);
25
- }
26
- catch (error) {
27
- alive = error.code === "EPERM";
28
- }
29
- if (alive)
30
- throw new OutpostError("conflict", `Workspace is already in use: ${key}`, { lock: path, pid: owner.pid });
5
+ import { localProcessIdentity, observeOwnership } from "./process-identity.js";
6
+ import { readInspectionFile } from "../inspection-file.js";
7
+ import { lockInspectionDefaults } from "./lock-inspection.constants.js";
8
+ export function lockPath(root, key) {
9
+ return join(root, ".outpost", "locks", `${createHash("sha256").update(key).digest("hex").slice(0, 20)}.json`);
10
+ }
11
+ async function readOwner(path) {
12
+ return JSON.parse((await readInspectionFile(path, lockInspectionDefaults.maxBytes)).toString("utf8"));
13
+ }
14
+ async function createLock(path) {
15
+ const file = await open(path, "wx", 0o600);
16
+ const nonce = randomUUID();
17
+ try {
18
+ await file.writeFile(JSON.stringify({
19
+ pid: process.pid,
20
+ nonce,
21
+ identity: await localProcessIdentity(),
22
+ }));
23
+ }
24
+ finally {
25
+ await file.close();
26
+ }
27
+ return async () => {
28
+ const current = await readOwner(path).catch(() => null);
29
+ if (current &&
30
+ typeof current === "object" &&
31
+ "nonce" in current &&
32
+ current.nonce === nonce)
31
33
  await rm(path, { force: true });
32
- }
34
+ };
35
+ }
36
+ export async function lock(root, key) {
37
+ await mkdir(join(root, ".outpost", "locks"), { recursive: true });
38
+ const path = lockPath(root, key);
39
+ try {
40
+ return await createLock(path);
41
+ }
42
+ catch (cause) {
43
+ if (!(cause &&
44
+ typeof cause === "object" &&
45
+ "code" in cause &&
46
+ cause.code === "EEXIST"))
47
+ throw cause;
48
+ }
49
+ const conflict = () => new OutpostError("conflict", `Workspace is already in use or ownership is unknown: ${key}`, { lock: path });
50
+ // Serialize stale reclamation so another contender cannot unlink a new owner.
51
+ const releaseGuard = await createLock(`${path}.reclaim`).catch(() => {
52
+ throw conflict();
53
+ });
54
+ try {
55
+ const owner = await readOwner(path).catch(() => null);
56
+ const pid = owner && typeof owner === "object" && "pid" in owner
57
+ ? owner.pid
58
+ : undefined;
59
+ const identity = owner && typeof owner === "object" && "identity" in owner
60
+ ? owner.identity
61
+ : undefined;
62
+ const ownership = typeof pid === "number" &&
63
+ Number.isSafeInteger(pid) &&
64
+ pid > 0 &&
65
+ pid <= lockInspectionDefaults.maxPid
66
+ ? await observeOwnership(pid, identity)
67
+ : undefined;
68
+ if (ownership?.status !== "inactive")
69
+ throw conflict();
70
+ await rm(path, { force: true });
71
+ return await createLock(path);
72
+ }
73
+ finally {
74
+ await releaseGuard();
33
75
  }
34
- throw new OutpostError("conflict", "Unable to acquire workspace lock");
35
76
  }
36
77
  //# sourceMappingURL=lock.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../../src/infrastructure/git/lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAGtD,MAAM,CAAC,KAAK,UAAU,IAAI,CACxB,IAAY,EACZ,GAAW;IAEX,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;IAC/C,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,CACf,MAAM,EACN,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CACtE,CAAC;IACF,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC;QAC7C,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YAC3C,MAAM,IAAI,CAAC,SAAS,CAClB,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC,CAC1D,CAAC;YACF,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YACnB,OAAO,GAAG,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,KAAK,CAAC;YACpE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAc,CAAC;YACpE,IAAI,KAAK,GAAG,IAAI,CAAC;YACjB,IAAI,CAAC;gBACH,IAAI,CAAC,KAAK,CAAC,GAAG;oBAAE,MAAM,IAAI,KAAK,EAAE,CAAC;gBAClC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YAC7B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,KAAK,GAAI,KAA+B,CAAC,IAAI,KAAK,OAAO,CAAC;YAC5D,CAAC;YACD,IAAI,KAAK;gBACP,MAAM,IAAI,YAAY,CACpB,UAAU,EACV,gCAAgC,GAAG,EAAE,EACrC,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,CAC/B,CAAC;YACJ,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IACD,MAAM,IAAI,YAAY,CAAC,UAAU,EAAE,kCAAkC,CAAC,CAAC;AACzE,CAAC","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport { mkdir, open, readFile, rm } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { OutpostError } from \"../../domain/errors.ts\";\nimport type { LockOwner } from \"./lock.types.ts\";\n\nexport async function lock(\n root: string,\n key: string,\n): Promise<() => Promise<void>> {\n const folder = join(root, \".outpost\", \"locks\");\n await mkdir(folder, { recursive: true });\n const path = join(\n folder,\n `${createHash(\"sha256\").update(key).digest(\"hex\").slice(0, 20)}.json`,\n );\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n const file = await open(path, \"wx\", 0o600);\n await file.writeFile(\n JSON.stringify({ pid: process.pid, nonce: randomUUID() }),\n );\n await file.close();\n return () => rm(path, { force: true });\n } catch (cause) {\n if ((cause as NodeJS.ErrnoException).code !== \"EEXIST\") throw cause;\n const owner = JSON.parse(await readFile(path, \"utf8\")) as LockOwner;\n let alive = true;\n try {\n if (!owner.pid) throw new Error();\n process.kill(owner.pid, 0);\n } catch (error) {\n alive = (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n if (alive)\n throw new OutpostError(\n \"conflict\",\n `Workspace is already in use: ${key}`,\n { lock: path, pid: owner.pid },\n );\n await rm(path, { force: true });\n }\n }\n throw new OutpostError(\"conflict\", \"Unable to acquire workspace lock\");\n}\n"]}
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../../src/infrastructure/git/lock.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport { mkdir, open, rm } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { OutpostError } from \"../../domain/errors.ts\";\nimport { localProcessIdentity, observeOwnership } from \"./process-identity.ts\";\nimport { readInspectionFile } from \"../inspection-file.ts\";\nimport { lockInspectionDefaults } from \"./lock-inspection.constants.ts\";\n\nexport function lockPath(root: string, key: string): string {\n return join(\n root,\n \".outpost\",\n \"locks\",\n `${createHash(\"sha256\").update(key).digest(\"hex\").slice(0, 20)}.json`,\n );\n}\n\nasync function readOwner(path: string): Promise<unknown> {\n return JSON.parse(\n (await readInspectionFile(path, lockInspectionDefaults.maxBytes)).toString(\n \"utf8\",\n ),\n );\n}\n\nasync function createLock(path: string): Promise<() => Promise<void>> {\n const file = await open(path, \"wx\", 0o600);\n const nonce = randomUUID();\n try {\n await file.writeFile(\n JSON.stringify({\n pid: process.pid,\n nonce,\n identity: await localProcessIdentity(),\n }),\n );\n } finally {\n await file.close();\n }\n return async () => {\n const current = await readOwner(path).catch(() => null);\n if (\n current &&\n typeof current === \"object\" &&\n \"nonce\" in current &&\n current.nonce === nonce\n )\n await rm(path, { force: true });\n };\n}\n\nexport async function lock(\n root: string,\n key: string,\n): Promise<() => Promise<void>> {\n await mkdir(join(root, \".outpost\", \"locks\"), { recursive: true });\n const path = lockPath(root, key);\n try {\n return await createLock(path);\n } catch (cause) {\n if (!(\n cause &&\n typeof cause === \"object\" &&\n \"code\" in cause &&\n cause.code === \"EEXIST\"\n ))\n throw cause;\n }\n const conflict = () =>\n new OutpostError(\n \"conflict\",\n `Workspace is already in use or ownership is unknown: ${key}`,\n { lock: path },\n );\n // Serialize stale reclamation so another contender cannot unlink a new owner.\n const releaseGuard = await createLock(`${path}.reclaim`).catch(() => {\n throw conflict();\n });\n try {\n const owner = await readOwner(path).catch(() => null);\n const pid =\n owner && typeof owner === \"object\" && \"pid\" in owner\n ? owner.pid\n : undefined;\n const identity =\n owner && typeof owner === \"object\" && \"identity\" in owner\n ? owner.identity\n : undefined;\n const ownership =\n typeof pid === \"number\" &&\n Number.isSafeInteger(pid) &&\n pid > 0 &&\n pid <= lockInspectionDefaults.maxPid\n ? await observeOwnership(pid, identity)\n : undefined;\n if (ownership?.status !== \"inactive\") throw conflict();\n await rm(path, { force: true });\n return await createLock(path);\n } finally {\n await releaseGuard();\n }\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import type { LocalProcessIdentity, LockOwnership } from "./process-identity.types.ts";
2
+ export declare function localProcessIdentity(pid?: number): Promise<LocalProcessIdentity | undefined>;
3
+ export declare function observeOwnership(pid: number, identity: unknown): Promise<LockOwnership>;
@@ -0,0 +1,73 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, readlink } from "node:fs/promises";
3
+ export async function localProcessIdentity(pid = process.pid) {
4
+ if (process.platform !== "linux")
5
+ return undefined;
6
+ try {
7
+ const [machine, boot, namespace, stat] = await Promise.all([
8
+ readFile("/etc/machine-id", "utf8"),
9
+ readFile("/proc/sys/kernel/random/boot_id", "utf8"),
10
+ readlink(`/proc/${pid}/ns/pid`),
11
+ readFile(`/proc/${pid}/stat`, "utf8"),
12
+ ]);
13
+ const started = stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19];
14
+ if (!machine.trim() || !boot.trim() || !started || !/^\d+$/.test(started))
15
+ return undefined;
16
+ return {
17
+ host: createHash("sha256").update(machine.trim()).digest("hex"),
18
+ boot: boot.trim(),
19
+ namespace,
20
+ started,
21
+ };
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ function validIdentity(value) {
28
+ if (!value || typeof value !== "object")
29
+ return false;
30
+ return ("host" in value &&
31
+ typeof value.host === "string" &&
32
+ value.host.length > 0 &&
33
+ "boot" in value &&
34
+ typeof value.boot === "string" &&
35
+ value.boot.length > 0 &&
36
+ "namespace" in value &&
37
+ typeof value.namespace === "string" &&
38
+ value.namespace.length > 0 &&
39
+ "started" in value &&
40
+ typeof value.started === "string" &&
41
+ /^\d+$/.test(value.started));
42
+ }
43
+ export async function observeOwnership(pid, identity) {
44
+ if (!validIdentity(identity))
45
+ return { status: "unknown", reason: "LEGACY_OR_INVALID_IDENTITY" };
46
+ const local = await localProcessIdentity();
47
+ if (!local)
48
+ return { status: "unknown", reason: "LOCAL_IDENTITY_UNAVAILABLE" };
49
+ if (identity.host !== local.host)
50
+ return { status: "unknown", reason: "OTHER_HOST" };
51
+ if (identity.boot !== local.boot)
52
+ return { status: "unknown", reason: "OTHER_BOOT" };
53
+ if (identity.namespace !== local.namespace)
54
+ return { status: "unknown", reason: "OTHER_PID_NAMESPACE" };
55
+ try {
56
+ process.kill(pid, 0);
57
+ }
58
+ catch (error) {
59
+ if (error &&
60
+ typeof error === "object" &&
61
+ "code" in error &&
62
+ error.code === "ESRCH")
63
+ return { status: "inactive", reason: "PROCESS_EXITED" };
64
+ return { status: "unknown", reason: "PROCESS_INACCESSIBLE" };
65
+ }
66
+ const current = await localProcessIdentity(pid);
67
+ if (!current)
68
+ return { status: "unknown", reason: "PROCESS_IDENTITY_UNAVAILABLE" };
69
+ if (current.started !== identity.started)
70
+ return { status: "unknown", reason: "PID_REUSED" };
71
+ return { status: "active", reason: "LOCAL_IDENTITY_MATCH" };
72
+ }
73
+ //# sourceMappingURL=process-identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-identity.js","sourceRoot":"","sources":["../../../src/infrastructure/git/process-identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAMtD,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,GAAG,GAAG,OAAO,CAAC,GAAG;IAEjB,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,SAAS,CAAC;IACnD,IAAI,CAAC;QACH,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACzD,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;YACnC,QAAQ,CAAC,iCAAiC,EAAE,MAAM,CAAC;YACnD,QAAQ,CAAC,SAAS,GAAG,SAAS,CAAC;YAC/B,QAAQ,CAAC,SAAS,GAAG,OAAO,EAAE,MAAM,CAAC;SACtC,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;YACvE,OAAO,SAAS,CAAC;QACnB,OAAO;YACL,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;YAC/D,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;YACjB,SAAS;YACT,OAAO;SACR,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,OAAO,CACL,MAAM,IAAI,KAAK;QACf,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;QAC9B,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;QACrB,MAAM,IAAI,KAAK;QACf,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;QAC9B,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;QACrB,WAAW,IAAI,KAAK;QACpB,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ;QACnC,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC;QAC1B,SAAS,IAAI,KAAK;QAClB,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ;QACjC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAC5B,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAW,EACX,QAAiB;IAEjB,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC;QAC1B,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,4BAA4B,EAAE,CAAC;IACrE,MAAM,KAAK,GAAG,MAAM,oBAAoB,EAAE,CAAC;IAC3C,IAAI,CAAC,KAAK;QACR,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,4BAA4B,EAAE,CAAC;IACrE,IAAI,QAAQ,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI;QAC9B,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IACrD,IAAI,QAAQ,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI;QAC9B,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IACrD,IAAI,QAAQ,CAAC,SAAS,KAAK,KAAK,CAAC,SAAS;QACxC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAC9D,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IACE,KAAK;YACL,OAAO,KAAK,KAAK,QAAQ;YACzB,MAAM,IAAI,KAAK;YACf,KAAK,CAAC,IAAI,KAAK,OAAO;YAEtB,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QAC1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;IAC/D,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,GAAG,CAAC,CAAC;IAChD,IAAI,CAAC,OAAO;QACV,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,8BAA8B,EAAE,CAAC;IACvE,IAAI,OAAO,CAAC,OAAO,KAAK,QAAQ,CAAC,OAAO;QACtC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IACrD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;AAC9D,CAAC","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { readFile, readlink } from \"node:fs/promises\";\nimport type {\n LocalProcessIdentity,\n LockOwnership,\n} from \"./process-identity.types.ts\";\n\nexport async function localProcessIdentity(\n pid = process.pid,\n): Promise<LocalProcessIdentity | undefined> {\n if (process.platform !== \"linux\") return undefined;\n try {\n const [machine, boot, namespace, stat] = await Promise.all([\n readFile(\"/etc/machine-id\", \"utf8\"),\n readFile(\"/proc/sys/kernel/random/boot_id\", \"utf8\"),\n readlink(`/proc/${pid}/ns/pid`),\n readFile(`/proc/${pid}/stat`, \"utf8\"),\n ]);\n const started = stat.slice(stat.lastIndexOf(\")\") + 2).split(\" \")[19];\n if (!machine.trim() || !boot.trim() || !started || !/^\\d+$/.test(started))\n return undefined;\n return {\n host: createHash(\"sha256\").update(machine.trim()).digest(\"hex\"),\n boot: boot.trim(),\n namespace,\n started,\n };\n } catch {\n return undefined;\n }\n}\n\nfunction validIdentity(value: unknown): value is LocalProcessIdentity {\n if (!value || typeof value !== \"object\") return false;\n return (\n \"host\" in value &&\n typeof value.host === \"string\" &&\n value.host.length > 0 &&\n \"boot\" in value &&\n typeof value.boot === \"string\" &&\n value.boot.length > 0 &&\n \"namespace\" in value &&\n typeof value.namespace === \"string\" &&\n value.namespace.length > 0 &&\n \"started\" in value &&\n typeof value.started === \"string\" &&\n /^\\d+$/.test(value.started)\n );\n}\n\nexport async function observeOwnership(\n pid: number,\n identity: unknown,\n): Promise<LockOwnership> {\n if (!validIdentity(identity))\n return { status: \"unknown\", reason: \"LEGACY_OR_INVALID_IDENTITY\" };\n const local = await localProcessIdentity();\n if (!local)\n return { status: \"unknown\", reason: \"LOCAL_IDENTITY_UNAVAILABLE\" };\n if (identity.host !== local.host)\n return { status: \"unknown\", reason: \"OTHER_HOST\" };\n if (identity.boot !== local.boot)\n return { status: \"unknown\", reason: \"OTHER_BOOT\" };\n if (identity.namespace !== local.namespace)\n return { status: \"unknown\", reason: \"OTHER_PID_NAMESPACE\" };\n try {\n process.kill(pid, 0);\n } catch (error) {\n if (\n error &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === \"ESRCH\"\n )\n return { status: \"inactive\", reason: \"PROCESS_EXITED\" };\n return { status: \"unknown\", reason: \"PROCESS_INACCESSIBLE\" };\n }\n const current = await localProcessIdentity(pid);\n if (!current)\n return { status: \"unknown\", reason: \"PROCESS_IDENTITY_UNAVAILABLE\" };\n if (current.started !== identity.started)\n return { status: \"unknown\", reason: \"PID_REUSED\" };\n return { status: \"active\", reason: \"LOCAL_IDENTITY_MATCH\" };\n}\n"]}
@@ -0,0 +1,10 @@
1
+ export interface LocalProcessIdentity {
2
+ readonly host: string;
3
+ readonly boot: string;
4
+ readonly namespace: string;
5
+ readonly started: string;
6
+ }
7
+ export interface LockOwnership {
8
+ readonly status: "active" | "inactive" | "unknown";
9
+ readonly reason: string;
10
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=process-identity.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-identity.types.js","sourceRoot":"","sources":["../../../src/infrastructure/git/process-identity.types.ts"],"names":[],"mappings":"","sourcesContent":["export interface LocalProcessIdentity {\n readonly host: string;\n readonly boot: string;\n readonly namespace: string;\n readonly started: string;\n}\nexport interface LockOwnership {\n readonly status: \"active\" | \"inactive\" | \"unknown\";\n readonly reason: string;\n}\n"]}
@@ -0,0 +1 @@
1
+ export declare function verificationGit(directory: string, args: readonly string[]): Promise<string>;
@@ -0,0 +1,36 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { gitDefaults } from "./git.constants.js";
4
+ export async function verificationGit(directory, args) {
5
+ const environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith("GIT_")));
6
+ const result = await promisify(execFile)("git", [
7
+ "-c",
8
+ `core.hooksPath=${process.platform === "win32" ? "NUL" : "/dev/null"}`,
9
+ "-c",
10
+ "core.fsmonitor=false",
11
+ "-c",
12
+ "maintenance.auto=false",
13
+ "-c",
14
+ "gc.auto=0",
15
+ ...args,
16
+ ], {
17
+ cwd: directory,
18
+ env: {
19
+ ...environment,
20
+ GIT_CONFIG_NOSYSTEM: "1",
21
+ GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null",
22
+ GIT_ATTR_NOSYSTEM: "1",
23
+ GIT_NO_LAZY_FETCH: "1",
24
+ GIT_ALLOW_PROTOCOL: "file",
25
+ GIT_TERMINAL_PROMPT: "0",
26
+ GIT_OPTIONAL_LOCKS: "0",
27
+ LC_ALL: "C",
28
+ },
29
+ timeout: gitDefaults.deadlineMs,
30
+ maxBuffer: gitDefaults.retainBytes,
31
+ encoding: "utf8",
32
+ windowsHide: true,
33
+ });
34
+ return result.stdout;
35
+ }
36
+ //# sourceMappingURL=verification-command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verification-command.js","sourceRoot":"","sources":["../../../src/infrastructure/git/verification-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,SAAiB,EACjB,IAAuB;IAEvB,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CACpC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAChC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CACjD,CACF,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,CACtC,KAAK,EACL;QACE,IAAI;QACJ,kBAAkB,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,EAAE;QACtE,IAAI;QACJ,sBAAsB;QACtB,IAAI;QACJ,wBAAwB;QACxB,IAAI;QACJ,WAAW;QACX,GAAG,IAAI;KACR,EACD;QACE,GAAG,EAAE,SAAS;QACd,GAAG,EAAE;YACH,GAAG,WAAW;YACd,mBAAmB,EAAE,GAAG;YACxB,iBAAiB,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW;YACrE,iBAAiB,EAAE,GAAG;YACtB,iBAAiB,EAAE,GAAG;YACtB,kBAAkB,EAAE,MAAM;YAC1B,mBAAmB,EAAE,GAAG;YACxB,kBAAkB,EAAE,GAAG;YACvB,MAAM,EAAE,GAAG;SACZ;QACD,OAAO,EAAE,WAAW,CAAC,UAAU;QAC/B,SAAS,EAAE,WAAW,CAAC,WAAW;QAClC,QAAQ,EAAE,MAAM;QAChB,WAAW,EAAE,IAAI;KAClB,CACF,CAAC;IACF,OAAO,MAAM,CAAC,MAAM,CAAC;AACvB,CAAC","sourcesContent":["import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport { gitDefaults } from \"./git.constants.ts\";\n\nexport async function verificationGit(\n directory: string,\n args: readonly string[],\n): Promise<string> {\n const environment = Object.fromEntries(\n Object.entries(process.env).filter(\n ([key]) => !key.toUpperCase().startsWith(\"GIT_\"),\n ),\n );\n const result = await promisify(execFile)(\n \"git\",\n [\n \"-c\",\n `core.hooksPath=${process.platform === \"win32\" ? \"NUL\" : \"/dev/null\"}`,\n \"-c\",\n \"core.fsmonitor=false\",\n \"-c\",\n \"maintenance.auto=false\",\n \"-c\",\n \"gc.auto=0\",\n ...args,\n ],\n {\n cwd: directory,\n env: {\n ...environment,\n GIT_CONFIG_NOSYSTEM: \"1\",\n GIT_CONFIG_GLOBAL: process.platform === \"win32\" ? \"NUL\" : \"/dev/null\",\n GIT_ATTR_NOSYSTEM: \"1\",\n GIT_NO_LAZY_FETCH: \"1\",\n GIT_ALLOW_PROTOCOL: \"file\",\n GIT_TERMINAL_PROMPT: \"0\",\n GIT_OPTIONAL_LOCKS: \"0\",\n LC_ALL: \"C\",\n },\n timeout: gitDefaults.deadlineMs,\n maxBuffer: gitDefaults.retainBytes,\n encoding: \"utf8\",\n windowsHide: true,\n },\n );\n return result.stdout;\n}\n"]}
@@ -45,6 +45,7 @@ export function workspaceLease(record, options, unlock) {
45
45
  "status",
46
46
  "--porcelain",
47
47
  "--untracked-files=normal",
48
+ "--ignored",
48
49
  ]);
49
50
  if (dirty.trim())
50
51
  return { retainedDirectory: workdir };
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-lease.js","sourceRoot":"","sources":["../../../src/infrastructure/git/workspace-lease.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAKtD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAMjC,MAAM,UAAU,cAAc,CAC5B,MAAuB,EACvB,OAAgC,EAChC,MAA2B;IAE3B,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;IAC9E,IAAI,QAAuC,CAAC;IAC5C,OAAO;QACL,GAAG,MAAM;QACT,KAAK,CAAC,SAAS;YACb,IAAI,MAAM,CAAC,IAAI,KAAK,WAAW;gBAAE,OAAO;YACxC,MAAM,OAAO,GAAG,CACd,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC,cAAc,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAC3D,CAAC,IAAI,EAAE,CAAC;YACT,IAAI,OAAO,KAAK,UAAU;gBACxB,MAAM,IAAI,YAAY,CACpB,UAAU,EACV,oCAAoC,EACpC,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CACtD,CAAC;YACJ,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,SAAS,UAAU,EAAE,CAAC,CAAC;YACnE,IAAI,CAAC;gBACH,MAAM,GAAG,CACP,UAAU,EACV,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,CAAC,EAC9B,OAAO,CAAC,MAAM,EAAE,OAAO,CACxB,CAAC;YACJ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,IAAI,YAAY,CACpB,UAAU,EACV,kDAAkD,EAClD,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,EAC9B,KAAK,CACN,CAAC;YACJ,CAAC;oBAAS,CAAC;gBACT,MAAM,YAAY,EAAE,CAAC;YACvB,CAAC;QACH,CAAC;QACD,OAAO,CAAC,QAAQ,GAAG,KAAK;YACtB,IAAI,QAAQ;gBAAE,OAAO,QAAQ,CAAC;YAC9B,QAAQ,GAAG,CAAC,KAAK,IAAI,EAAE;gBACrB,IAAI,CAAC;oBACH,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;wBAAE,OAAO,EAAE,CAAC;oBACzC,IAAI,QAAQ;wBAAE,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC;oBACpD,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,OAAO,EAAE;wBAClC,cAAc;wBACd,SAAS;wBACT,MAAM;qBACP,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;oBACnB,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE;wBAAE,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC;oBAC5D,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,OAAO,EAAE;wBAC/B,QAAQ;wBACR,aAAa;wBACb,0BAA0B;qBAC3B,CAAC,CAAC;oBACH,IAAI,KAAK,CAAC,IAAI,EAAE;wBAAE,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC;oBACxD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,OAAO,CAAC;wBAC9D,MAAM,IAAI,YAAY,CACpB,WAAW,EACX,2CAA2C,CAC5C,CAAC;oBACJ,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;oBACvD,IAAI,MAAM,CAAC,IAAI,KAAK,WAAW;wBAC7B,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CACnD,GAAG,EAAE,CAAC,SAAS,CAChB,CAAC;oBACJ,OAAO,EAAE,CAAC;gBACZ,CAAC;wBAAS,CAAC;oBACT,MAAM,MAAM,EAAE,CAAC;gBACjB,CAAC;YACH,CAAC,CAAC,EAAE,CAAC;YACL,OAAO,QAAQ,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { join } from \"node:path\";\nimport { OutpostError } from \"../../domain/errors.ts\";\nimport type {\n Disposal,\n WorkspaceRecord,\n} from \"../../domain/workspace.types.ts\";\nimport { inside } from \"../files.ts\";\nimport { git } from \"./command.ts\";\nimport { lock } from \"./lock.ts\";\nimport type {\n AcquireWorkspaceOptions,\n WorkspaceLease,\n} from \"./workspace.types.ts\";\n\nexport function workspaceLease(\n record: WorkspaceRecord,\n options: AcquireWorkspaceOptions,\n unlock: () => Promise<void>,\n): WorkspaceLease {\n const { policy, repository, baseBranch, branch, directory: workdir } = record;\n let disposal: Promise<Disposal> | undefined;\n return {\n ...record,\n async integrate() {\n if (policy.mode !== \"integrate\") return;\n const current = (\n await git(repository, [\"symbolic-ref\", \"--short\", \"HEAD\"])\n ).trim();\n if (current !== baseBranch)\n throw new OutpostError(\n \"conflict\",\n \"Host branch changed during the job\",\n { expected: baseBranch, current, directory: workdir },\n );\n const releaseMerge = await lock(repository, `merge:${baseBranch}`);\n try {\n await git(\n repository,\n [\"merge\", \"--no-edit\", branch],\n options.limits?.mergeMs,\n );\n } catch (cause) {\n throw new OutpostError(\n \"conflict\",\n \"Automatic integration failed; workspace retained\",\n { directory: workdir, branch },\n cause,\n );\n } finally {\n await releaseMerge();\n }\n },\n dispose(preserve = false) {\n if (disposal) return disposal;\n disposal = (async () => {\n try {\n if (policy.mode === \"current\") return {};\n if (preserve) return { retainedDirectory: workdir };\n const attached = await git(workdir, [\n \"symbolic-ref\",\n \"--quiet\",\n \"HEAD\",\n ]).catch(() => \"\");\n if (!attached.trim()) return { retainedDirectory: workdir };\n const dirty = await git(workdir, [\n \"status\",\n \"--porcelain\",\n \"--untracked-files=normal\",\n ]);\n if (dirty.trim()) return { retainedDirectory: workdir };\n if (!inside(join(repository, \".outpost\", \"workspaces\"), workdir))\n throw new OutpostError(\n \"workspace\",\n \"Refusing to remove an unmanaged workspace\",\n );\n await git(repository, [\"worktree\", \"remove\", workdir]);\n if (policy.mode === \"integrate\")\n await git(repository, [\"branch\", \"-d\", branch]).catch(\n () => undefined,\n );\n return {};\n } finally {\n await unlock();\n }\n })();\n return disposal;\n },\n };\n}\n"]}
1
+ {"version":3,"file":"workspace-lease.js","sourceRoot":"","sources":["../../../src/infrastructure/git/workspace-lease.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport { OutpostError } from \"../../domain/errors.ts\";\nimport type {\n Disposal,\n WorkspaceRecord,\n} from \"../../domain/workspace.types.ts\";\nimport { inside } from \"../files.ts\";\nimport { git } from \"./command.ts\";\nimport { lock } from \"./lock.ts\";\nimport type {\n AcquireWorkspaceOptions,\n WorkspaceLease,\n} from \"./workspace.types.ts\";\n\nexport function workspaceLease(\n record: WorkspaceRecord,\n options: AcquireWorkspaceOptions,\n unlock: () => Promise<void>,\n): WorkspaceLease {\n const { policy, repository, baseBranch, branch, directory: workdir } = record;\n let disposal: Promise<Disposal> | undefined;\n return {\n ...record,\n async integrate() {\n if (policy.mode !== \"integrate\") return;\n const current = (\n await git(repository, [\"symbolic-ref\", \"--short\", \"HEAD\"])\n ).trim();\n if (current !== baseBranch)\n throw new OutpostError(\n \"conflict\",\n \"Host branch changed during the job\",\n { expected: baseBranch, current, directory: workdir },\n );\n const releaseMerge = await lock(repository, `merge:${baseBranch}`);\n try {\n await git(\n repository,\n [\"merge\", \"--no-edit\", branch],\n options.limits?.mergeMs,\n );\n } catch (cause) {\n throw new OutpostError(\n \"conflict\",\n \"Automatic integration failed; workspace retained\",\n { directory: workdir, branch },\n cause,\n );\n } finally {\n await releaseMerge();\n }\n },\n dispose(preserve = false) {\n if (disposal) return disposal;\n disposal = (async () => {\n try {\n if (policy.mode === \"current\") return {};\n if (preserve) return { retainedDirectory: workdir };\n const attached = await git(workdir, [\n \"symbolic-ref\",\n \"--quiet\",\n \"HEAD\",\n ]).catch(() => \"\");\n if (!attached.trim()) return { retainedDirectory: workdir };\n const dirty = await git(workdir, [\n \"status\",\n \"--porcelain\",\n \"--untracked-files=normal\",\n \"--ignored\",\n ]);\n if (dirty.trim()) return { retainedDirectory: workdir };\n if (!inside(join(repository, \".outpost\", \"workspaces\"), workdir))\n throw new OutpostError(\n \"workspace\",\n \"Refusing to remove an unmanaged workspace\",\n );\n await git(repository, [\"worktree\", \"remove\", workdir]);\n if (policy.mode === \"integrate\")\n await git(repository, [\"branch\", \"-d\", branch]).catch(\n () => undefined,\n );\n return {};\n } finally {\n await unlock();\n }\n })();\n return disposal;\n },\n };\n}\n"]}
@@ -59,7 +59,11 @@ export async function acquireWorkspace(options) {
59
59
  }
60
60
  catch (error) {
61
61
  if (created) {
62
- const clean = await git(workdir, ["status", "--porcelain"]).then((output) => !output.trim(), () => false);
62
+ const clean = await git(workdir, [
63
+ "status",
64
+ "--porcelain",
65
+ "--ignored",
66
+ ]).then((output) => !output.trim(), () => false);
63
67
  if (clean && inside(join(repository, ".outpost", "workspaces"), workdir))
64
68
  await git(repository, ["worktree", "remove", workdir]).catch(() => undefined);
65
69
  else
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../../src/infrastructure/git/workspace.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { join } from \"node:path\";\nimport { OutpostError, recordRecovery } from \"../../domain/errors.ts\";\nimport { copySelected, inside } from \"../files.ts\";\nimport { git } from \"./command.ts\";\nimport { lock } from \"./lock.ts\";\nimport { managedWorktree } from \"./managed-worktree.ts\";\nimport { prepareRepository } from \"./repository.ts\";\nimport { workspaceLease } from \"./workspace-lease.ts\";\nimport type {\n AcquireWorkspaceOptions,\n WorkspaceLease,\n} from \"./workspace.types.ts\";\n\nexport async function acquireWorkspace(\n options: AcquireWorkspaceOptions,\n): Promise<WorkspaceLease> {\n const repository = await prepareRepository(options);\n const policy = options.branch ?? { mode: \"current\" };\n const baseBranch = (\n await git(repository, [\"symbolic-ref\", \"--quiet\", \"--short\", \"HEAD\"]).catch(\n () => \"\",\n )\n ).trim();\n if (policy.mode === \"integrate\" && !baseBranch)\n throw new OutpostError(\n \"workspace\",\n \"Automatic integration requires an attached host branch\",\n );\n if (policy.mode === \"current\" && options.copies?.length)\n throw new OutpostError(\n \"configuration\",\n \"Copied inputs require a separate workspace\",\n );\n const branch =\n policy.mode === \"current\"\n ? baseBranch || \"HEAD\"\n : policy.mode === \"named\"\n ? policy.name\n : `outpost/${\n options.label\n ? options.label\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .slice(0, 48) + \"-\"\n : \"job-\"\n }${randomUUID()}`;\n if (policy.mode !== \"current\")\n await git(repository, [\"check-ref-format\", \"--branch\", branch]);\n const unlock = await lock(\n repository,\n policy.mode === \"current\" ? repository : branch,\n );\n let workdir = repository;\n let created = false;\n try {\n if (policy.mode !== \"current\") {\n ({ workdir, created } = await managedWorktree({\n repository,\n policy,\n branch,\n options,\n }));\n }\n await copySelected(\n repository,\n workdir,\n options.copies ?? [],\n options.limits?.copyMs,\n );\n const baseline = (await git(workdir, [\"rev-parse\", \"HEAD\"])).trim();\n const gitDir = (\n await git(workdir, [\"rev-parse\", \"--absolute-git-dir\"])\n ).trim();\n const common = (\n await git(workdir, [\n \"rev-parse\",\n \"--path-format=absolute\",\n \"--git-common-dir\",\n ])\n ).trim();\n return workspaceLease(\n {\n repository,\n directory: workdir,\n branch,\n baseBranch,\n baseline,\n policy,\n gitDirectories: [...new Set([gitDir, common])],\n },\n options,\n unlock,\n );\n } catch (error) {\n if (created) {\n const clean = await git(workdir, [\"status\", \"--porcelain\"]).then(\n (output) => !output.trim(),\n () => false,\n );\n if (clean && inside(join(repository, \".outpost\", \"workspaces\"), workdir))\n await git(repository, [\"worktree\", \"remove\", workdir]).catch(\n () => undefined,\n );\n else recordRecovery(error, { branch, directory: workdir });\n }\n await unlock();\n throw error;\n }\n}\n"]}
1
+ {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../../src/infrastructure/git/workspace.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { join } from \"node:path\";\nimport { OutpostError, recordRecovery } from \"../../domain/errors.ts\";\nimport { copySelected, inside } from \"../files.ts\";\nimport { git } from \"./command.ts\";\nimport { lock } from \"./lock.ts\";\nimport { managedWorktree } from \"./managed-worktree.ts\";\nimport { prepareRepository } from \"./repository.ts\";\nimport { workspaceLease } from \"./workspace-lease.ts\";\nimport type {\n AcquireWorkspaceOptions,\n WorkspaceLease,\n} from \"./workspace.types.ts\";\n\nexport async function acquireWorkspace(\n options: AcquireWorkspaceOptions,\n): Promise<WorkspaceLease> {\n const repository = await prepareRepository(options);\n const policy = options.branch ?? { mode: \"current\" };\n const baseBranch = (\n await git(repository, [\"symbolic-ref\", \"--quiet\", \"--short\", \"HEAD\"]).catch(\n () => \"\",\n )\n ).trim();\n if (policy.mode === \"integrate\" && !baseBranch)\n throw new OutpostError(\n \"workspace\",\n \"Automatic integration requires an attached host branch\",\n );\n if (policy.mode === \"current\" && options.copies?.length)\n throw new OutpostError(\n \"configuration\",\n \"Copied inputs require a separate workspace\",\n );\n const branch =\n policy.mode === \"current\"\n ? baseBranch || \"HEAD\"\n : policy.mode === \"named\"\n ? policy.name\n : `outpost/${\n options.label\n ? options.label\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .slice(0, 48) + \"-\"\n : \"job-\"\n }${randomUUID()}`;\n if (policy.mode !== \"current\")\n await git(repository, [\"check-ref-format\", \"--branch\", branch]);\n const unlock = await lock(\n repository,\n policy.mode === \"current\" ? repository : branch,\n );\n let workdir = repository;\n let created = false;\n try {\n if (policy.mode !== \"current\") {\n ({ workdir, created } = await managedWorktree({\n repository,\n policy,\n branch,\n options,\n }));\n }\n await copySelected(\n repository,\n workdir,\n options.copies ?? [],\n options.limits?.copyMs,\n );\n const baseline = (await git(workdir, [\"rev-parse\", \"HEAD\"])).trim();\n const gitDir = (\n await git(workdir, [\"rev-parse\", \"--absolute-git-dir\"])\n ).trim();\n const common = (\n await git(workdir, [\n \"rev-parse\",\n \"--path-format=absolute\",\n \"--git-common-dir\",\n ])\n ).trim();\n return workspaceLease(\n {\n repository,\n directory: workdir,\n branch,\n baseBranch,\n baseline,\n policy,\n gitDirectories: [...new Set([gitDir, common])],\n },\n options,\n unlock,\n );\n } catch (error) {\n if (created) {\n const clean = await git(workdir, [\n \"status\",\n \"--porcelain\",\n \"--ignored\",\n ]).then(\n (output) => !output.trim(),\n () => false,\n );\n if (clean && inside(join(repository, \".outpost\", \"workspaces\"), workdir))\n await git(repository, [\"worktree\", \"remove\", workdir]).catch(\n () => undefined,\n );\n else recordRecovery(error, { branch, directory: workdir });\n }\n await unlock();\n throw error;\n }\n}\n"]}
@@ -0,0 +1,5 @@
1
+ export declare const worktreeInspectionDefaults: {
2
+ readonly deadlineMs: 10000;
3
+ readonly retainBytes: 1048576;
4
+ };
5
+ export declare const worktreeInspectionArguments: readonly ["--no-optional-locks", "-c", "core.fsmonitor=false", "-c", string, "-c", "maintenance.auto=false", "-c", "gc.auto=0"];
@@ -0,0 +1,16 @@
1
+ export const worktreeInspectionDefaults = {
2
+ deadlineMs: 10_000,
3
+ retainBytes: 1_048_576,
4
+ };
5
+ export const worktreeInspectionArguments = [
6
+ "--no-optional-locks",
7
+ "-c",
8
+ "core.fsmonitor=false",
9
+ "-c",
10
+ "core.hooksPath=" + (process.platform === "win32" ? "NUL" : "/dev/null"),
11
+ "-c",
12
+ "maintenance.auto=false",
13
+ "-c",
14
+ "gc.auto=0",
15
+ ];
16
+ //# sourceMappingURL=worktree-inspection.constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree-inspection.constants.js","sourceRoot":"","sources":["../../../src/infrastructure/git/worktree-inspection.constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,0BAA0B,GAAG;IACxC,UAAU,EAAE,MAAM;IAClB,WAAW,EAAE,SAAS;CACd,CAAC;AAEX,MAAM,CAAC,MAAM,2BAA2B,GAAG;IACzC,qBAAqB;IACrB,IAAI;IACJ,sBAAsB;IACtB,IAAI;IACJ,iBAAiB,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC;IACxE,IAAI;IACJ,wBAAwB;IACxB,IAAI;IACJ,WAAW;CACH,CAAC","sourcesContent":["export const worktreeInspectionDefaults = {\n deadlineMs: 10_000,\n retainBytes: 1_048_576,\n} as const;\n\nexport const worktreeInspectionArguments = [\n \"--no-optional-locks\",\n \"-c\",\n \"core.fsmonitor=false\",\n \"-c\",\n \"core.hooksPath=\" + (process.platform === \"win32\" ? \"NUL\" : \"/dev/null\"),\n \"-c\",\n \"maintenance.auto=false\",\n \"-c\",\n \"gc.auto=0\",\n] as const;\n"]}
@@ -0,0 +1,3 @@
1
+ import type { StorageEntry } from "../storage-inventory.types.ts";
2
+ import type { WorkspaceGitInspection } from "./worktree-inspection.types.ts";
3
+ export declare function inspectWorktreeGit(repository: string, entries: readonly StorageEntry[]): Promise<WorkspaceGitInspection>;
@@ -0,0 +1,120 @@
1
+ import { lstat, realpath } from "node:fs/promises";
2
+ import { resolve, join } from "node:path";
3
+ import { OutpostError } from "../../domain/errors.js";
4
+ import { requireSuccess } from "../process.js";
5
+ import { worktreeInspectionArguments, worktreeInspectionDefaults, } from "./worktree-inspection.constants.js";
6
+ async function inspectGit(path, args) {
7
+ let length = 0;
8
+ const result = await requireSuccess({
9
+ executable: "git",
10
+ arguments: [...worktreeInspectionArguments, ...args],
11
+ directory: path,
12
+ variables: { LC_ALL: "C", GIT_TERMINAL_PROMPT: "0" },
13
+ deadlineMs: worktreeInspectionDefaults.deadlineMs,
14
+ retain: worktreeInspectionDefaults.retainBytes,
15
+ observe(channel, text) {
16
+ if (channel !== "stdout")
17
+ return;
18
+ length += Buffer.byteLength(text);
19
+ if (length > worktreeInspectionDefaults.retainBytes)
20
+ throw new OutpostError("workspace", "Git inspection output limit exceeded");
21
+ },
22
+ });
23
+ return result.stdout;
24
+ }
25
+ async function inspectEntry(entry, records, commonDirectory) {
26
+ if (entry.kind !== "directory")
27
+ return { state: "skipped", reason: "NOT_DIRECTORY" };
28
+ const fields = records.find((record) => {
29
+ const path = record
30
+ .find((field) => field.startsWith("worktree "))
31
+ ?.slice(9);
32
+ return path !== undefined && resolve(path) === resolve(entry.path);
33
+ });
34
+ if (!fields)
35
+ return { state: "unregistered" };
36
+ const head = fields.find((field) => field.startsWith("HEAD "))?.slice(5);
37
+ if (!head)
38
+ return { state: "unavailable", reason: "INVALID_REGISTRATION" };
39
+ // A listed worktree may have been replaced since registration or inventory.
40
+ if (!(await lstat(entry.path)).isDirectory() ||
41
+ (await realpath(entry.path)) !== resolve(entry.path))
42
+ return { state: "unavailable", reason: "WORKSPACE_CHANGED" };
43
+ const marker = await lstat(join(entry.path, ".git"));
44
+ if (!marker.isFile())
45
+ return { state: "unavailable", reason: "INVALID_GIT_MARKER" };
46
+ const top = (await inspectGit(entry.path, ["rev-parse", "--show-toplevel"])).trim();
47
+ if (resolve(top) !== resolve(entry.path))
48
+ return { state: "unavailable", reason: "WORKSPACE_CHANGED" };
49
+ const common = (await inspectGit(entry.path, [
50
+ "rev-parse",
51
+ "--path-format=absolute",
52
+ "--git-common-dir",
53
+ ])).trim();
54
+ if ((await realpath(common)) !== commonDirectory)
55
+ return { state: "unavailable", reason: "REGISTRATION_MISMATCH" };
56
+ const status = await inspectGit(entry.path, [
57
+ "status",
58
+ "--porcelain=v1",
59
+ "-z",
60
+ "--untracked-files=normal",
61
+ "--ignore-submodules=none",
62
+ ]);
63
+ return {
64
+ state: "registered",
65
+ head,
66
+ branch: fields
67
+ .find((field) => field.startsWith("branch refs/heads/"))
68
+ ?.slice(18) ?? null,
69
+ dirty: status.length > 0,
70
+ locked: fields.some((field) => field === "locked" || field.startsWith("locked ")),
71
+ };
72
+ }
73
+ export async function inspectWorktreeGit(repository, entries) {
74
+ const workspaces = [];
75
+ const issues = [];
76
+ if (!entries.length)
77
+ return { complete: true, workspaces, issues };
78
+ let records;
79
+ let commonDirectory;
80
+ try {
81
+ commonDirectory = await realpath((await inspectGit(repository, [
82
+ "rev-parse",
83
+ "--path-format=absolute",
84
+ "--git-common-dir",
85
+ ])).trim());
86
+ const list = await inspectGit(repository, [
87
+ "worktree",
88
+ "list",
89
+ "--porcelain",
90
+ "-z",
91
+ ]);
92
+ records = list
93
+ .split("\0\0")
94
+ .filter(Boolean)
95
+ .map((record) => record.split("\0"));
96
+ }
97
+ catch {
98
+ return {
99
+ complete: false,
100
+ workspaces: entries.map(({ name, path }) => ({
101
+ name,
102
+ path,
103
+ state: "unavailable",
104
+ reason: "GIT_LIST_FAILED",
105
+ })),
106
+ issues: [{ path: repository, code: "GIT_LIST_FAILED" }],
107
+ };
108
+ }
109
+ for (const entry of entries) {
110
+ const state = await inspectEntry(entry, records, commonDirectory).catch(() => ({
111
+ state: "unavailable",
112
+ reason: "GIT_INSPECTION_FAILED",
113
+ }));
114
+ workspaces.push({ name: entry.name, path: entry.path, ...state });
115
+ if (state.state === "unavailable")
116
+ issues.push({ path: entry.path, code: state.reason });
117
+ }
118
+ return { complete: issues.length === 0, workspaces, issues };
119
+ }
120
+ //# sourceMappingURL=worktree-inspection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree-inspection.js","sourceRoot":"","sources":["../../../src/infrastructure/git/worktree-inspection.ts"],"names":[],"mappings":"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","sourcesContent":["import { lstat, realpath } from \"node:fs/promises\";\nimport { resolve, join } from \"node:path\";\nimport { OutpostError } from \"../../domain/errors.ts\";\nimport { requireSuccess } from \"../process.ts\";\nimport type { StorageEntry, StorageIssue } from \"../storage-inventory.types.ts\";\nimport {\n worktreeInspectionArguments,\n worktreeInspectionDefaults,\n} from \"./worktree-inspection.constants.ts\";\nimport type {\n WorkspaceGitEntry,\n WorkspaceGitInspection,\n WorkspaceGitState,\n} from \"./worktree-inspection.types.ts\";\n\nasync function inspectGit(\n path: string,\n args: readonly string[],\n): Promise<string> {\n let length = 0;\n const result = await requireSuccess({\n executable: \"git\",\n arguments: [...worktreeInspectionArguments, ...args],\n directory: path,\n variables: { LC_ALL: \"C\", GIT_TERMINAL_PROMPT: \"0\" },\n deadlineMs: worktreeInspectionDefaults.deadlineMs,\n retain: worktreeInspectionDefaults.retainBytes,\n observe(channel, text) {\n if (channel !== \"stdout\") return;\n length += Buffer.byteLength(text);\n if (length > worktreeInspectionDefaults.retainBytes)\n throw new OutpostError(\n \"workspace\",\n \"Git inspection output limit exceeded\",\n );\n },\n });\n return result.stdout;\n}\n\nasync function inspectEntry(\n entry: StorageEntry,\n records: readonly (readonly string[])[],\n commonDirectory: string,\n): Promise<WorkspaceGitState> {\n if (entry.kind !== \"directory\")\n return { state: \"skipped\", reason: \"NOT_DIRECTORY\" };\n const fields = records.find((record) => {\n const path = record\n .find((field) => field.startsWith(\"worktree \"))\n ?.slice(9);\n return path !== undefined && resolve(path) === resolve(entry.path);\n });\n if (!fields) return { state: \"unregistered\" };\n const head = fields.find((field) => field.startsWith(\"HEAD \"))?.slice(5);\n if (!head) return { state: \"unavailable\", reason: \"INVALID_REGISTRATION\" };\n // A listed worktree may have been replaced since registration or inventory.\n if (\n !(await lstat(entry.path)).isDirectory() ||\n (await realpath(entry.path)) !== resolve(entry.path)\n )\n return { state: \"unavailable\", reason: \"WORKSPACE_CHANGED\" };\n const marker = await lstat(join(entry.path, \".git\"));\n if (!marker.isFile())\n return { state: \"unavailable\", reason: \"INVALID_GIT_MARKER\" };\n const top = (\n await inspectGit(entry.path, [\"rev-parse\", \"--show-toplevel\"])\n ).trim();\n if (resolve(top) !== resolve(entry.path))\n return { state: \"unavailable\", reason: \"WORKSPACE_CHANGED\" };\n const common = (\n await inspectGit(entry.path, [\n \"rev-parse\",\n \"--path-format=absolute\",\n \"--git-common-dir\",\n ])\n ).trim();\n if ((await realpath(common)) !== commonDirectory)\n return { state: \"unavailable\", reason: \"REGISTRATION_MISMATCH\" };\n const status = await inspectGit(entry.path, [\n \"status\",\n \"--porcelain=v1\",\n \"-z\",\n \"--untracked-files=normal\",\n \"--ignore-submodules=none\",\n ]);\n return {\n state: \"registered\",\n head,\n branch:\n fields\n .find((field) => field.startsWith(\"branch refs/heads/\"))\n ?.slice(18) ?? null,\n dirty: status.length > 0,\n locked: fields.some(\n (field) => field === \"locked\" || field.startsWith(\"locked \"),\n ),\n };\n}\n\nexport async function inspectWorktreeGit(\n repository: string,\n entries: readonly StorageEntry[],\n): Promise<WorkspaceGitInspection> {\n const workspaces: WorkspaceGitEntry[] = [];\n const issues: StorageIssue[] = [];\n if (!entries.length) return { complete: true, workspaces, issues };\n let records: string[][];\n let commonDirectory: string;\n try {\n commonDirectory = await realpath(\n (\n await inspectGit(repository, [\n \"rev-parse\",\n \"--path-format=absolute\",\n \"--git-common-dir\",\n ])\n ).trim(),\n );\n const list = await inspectGit(repository, [\n \"worktree\",\n \"list\",\n \"--porcelain\",\n \"-z\",\n ]);\n records = list\n .split(\"\\0\\0\")\n .filter(Boolean)\n .map((record) => record.split(\"\\0\"));\n } catch {\n return {\n complete: false,\n workspaces: entries.map(({ name, path }) => ({\n name,\n path,\n state: \"unavailable\",\n reason: \"GIT_LIST_FAILED\",\n })),\n issues: [{ path: repository, code: \"GIT_LIST_FAILED\" }],\n };\n }\n for (const entry of entries) {\n const state = await inspectEntry(entry, records, commonDirectory).catch(\n (): WorkspaceGitState => ({\n state: \"unavailable\",\n reason: \"GIT_INSPECTION_FAILED\",\n }),\n );\n workspaces.push({ name: entry.name, path: entry.path, ...state });\n if (state.state === \"unavailable\")\n issues.push({ path: entry.path, code: state.reason });\n }\n return { complete: issues.length === 0, workspaces, issues };\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import type { StorageEntry, StorageIssue } from "../storage-inventory.types.ts";
2
+ export type WorkspaceGitState = {
3
+ readonly state: "registered";
4
+ readonly head: string;
5
+ readonly branch: string | null;
6
+ readonly dirty: boolean;
7
+ readonly locked: boolean;
8
+ } | {
9
+ readonly state: "unregistered";
10
+ } | {
11
+ readonly state: "skipped" | "unavailable";
12
+ readonly reason: string;
13
+ };
14
+ export type WorkspaceGitEntry = Pick<StorageEntry, "name" | "path"> & WorkspaceGitState;
15
+ export interface WorkspaceGitInspection {
16
+ readonly complete: boolean;
17
+ readonly workspaces: readonly WorkspaceGitEntry[];
18
+ readonly issues: readonly StorageIssue[];
19
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=worktree-inspection.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree-inspection.types.js","sourceRoot":"","sources":["../../../src/infrastructure/git/worktree-inspection.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { StorageEntry, StorageIssue } from \"../storage-inventory.types.ts\";\n\nexport type WorkspaceGitState =\n | {\n readonly state: \"registered\";\n readonly head: string;\n readonly branch: string | null;\n readonly dirty: boolean;\n readonly locked: boolean;\n }\n | { readonly state: \"unregistered\" }\n | { readonly state: \"skipped\" | \"unavailable\"; readonly reason: string };\n\nexport type WorkspaceGitEntry = Pick<StorageEntry, \"name\" | \"path\"> &\n WorkspaceGitState;\n\nexport interface WorkspaceGitInspection {\n readonly complete: boolean;\n readonly workspaces: readonly WorkspaceGitEntry[];\n readonly issues: readonly StorageIssue[];\n}\n"]}
@@ -0,0 +1 @@
1
+ export declare const inspectionFileFlags: number;
@@ -0,0 +1,5 @@
1
+ import { constants } from "node:fs";
2
+ export const inspectionFileFlags = constants.O_RDONLY |
3
+ (constants.O_NOFOLLOW ?? 0) |
4
+ (constants.O_NONBLOCK ?? 0);
5
+ //# sourceMappingURL=inspection-file.constants.js.map