hyperframes 0.8.151 → 0.8.152
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.
- package/dist/{add-QKZZG7A2.js → add-SA33REHW.js} +14 -14
- package/dist/{animatedGifPrep-YQDPYGOD.js → animatedGifPrep-3YXJQZ7L.js} +9 -9
- package/dist/api.js +15 -15
- package/dist/{asset-3Q737Y4H.js → asset-PUVPTWPD.js} +2 -2
- package/dist/{assetCataloger-BTHEBJJB.js → assetCataloger-VWE2PLBX.js} +4 -4
- package/dist/{auth-XL7EU3SO.js → auth-PS3M6AI7.js} +1 -1
- package/dist/{autoUpdate-PJFLTQ3U.js → autoUpdate-CMO4SHWU.js} +2 -2
- package/dist/{backgroundChecks-T5TIR5WK.js → backgroundChecks-EDZ2A55J.js} +3 -3
- package/dist/backgroundChecksWorker.js +3 -3
- package/dist/{batchRender-NFLVIDB4.js → batchRender-7PLJISCN.js} +5 -5
- package/dist/{beats-JU4GZ26R.js → beats-2MDSIOE7.js} +14 -14
- package/dist/{benchmark-7U2YWLTW.js → benchmark-KBBXAPI4.js} +10 -10
- package/dist/{browser-D57UPYDA.js → browser-OMWKZRSW.js} +3 -3
- package/dist/{bundleWithLocalizedFonts-26MXQOZG.js → bundleWithLocalizedFonts-76DMCRIW.js} +2 -2
- package/dist/{capture-47BV25G3.js → capture-LRUUXH5Q.js} +2 -2
- package/dist/{capture-LT252COQ.js → capture-YGDC7GN3.js} +12 -12
- package/dist/{catalog-BEZXOV4O.js → catalog-SMEBKEVO.js} +14 -14
- package/dist/{catch-up-KVV5X6BF.js → catch-up-OMILX3WE.js} +4 -4
- package/dist/{check-6KQ433E6.js → check-NUZ62LIW.js} +131 -43
- package/dist/{checkBrowser-PRDR6XBY.js → checkBrowser-MSTVEL4V.js} +39 -38
- package/dist/{chunk-L6RIMCMG.js → chunk-3DZBQ7MH.js} +3 -3
- package/dist/{chunk-POEEDPJI.js → chunk-3E4OCOC5.js} +1 -1
- package/dist/{chunk-YZTVHRJ2.js → chunk-3HPJM5IQ.js} +1 -1
- package/dist/{chunk-OZ4K46OV.js → chunk-3LO7BBJX.js} +2 -2
- package/dist/{chunk-APDLWWFG.js → chunk-3VGU2SH7.js} +1 -1
- package/dist/{chunk-RRBKEHST.js → chunk-4UMKAUZI.js} +1 -1
- package/dist/{chunk-DHPFXAUF.js → chunk-57GN6KWT.js} +17 -3
- package/dist/{chunk-374QQGBQ.js → chunk-5CS4T6C6.js} +63 -12
- package/dist/{chunk-XQKVEQNY.js → chunk-5ERVRJMV.js} +2 -2
- package/dist/{chunk-S22FKF6G.js → chunk-5VMSUW4Z.js} +2 -2
- package/dist/{chunk-N4IXP3MY.js → chunk-6Z4YJ5Q3.js} +3 -3
- package/dist/{chunk-FJWEYVEM.js → chunk-7FYFEXD2.js} +658 -198
- package/dist/{chunk-4LE27ST4.js → chunk-7NPSXBIN.js} +2 -2
- package/dist/chunk-7TASSBN2.js +215 -0
- package/dist/{chunk-IP73NMQT.js → chunk-ANS56TPS.js} +10 -10
- package/dist/{chunk-PUIELIFA.js → chunk-BQK3J7XO.js} +1 -1
- package/dist/{chunk-RJRGR5KF.js → chunk-BYJYQWEX.js} +9 -9
- package/dist/{chunk-XXPYZBZ3.js → chunk-C6TPGUWD.js} +2 -2
- package/dist/{chunk-FAIYEIZ4.js → chunk-D6RJXQLD.js} +1 -1
- package/dist/{chunk-N3I6RAOR.js → chunk-DFXK7KUO.js} +11 -11
- package/dist/{chunk-DIOBKUBV.js → chunk-E6EYZ33P.js} +2 -2
- package/dist/{chunk-ZGCRVM5M.js → chunk-EVTHG34H.js} +2 -2
- package/dist/{chunk-IZPN423R.js → chunk-EXH6L2XZ.js} +2 -2
- package/dist/{chunk-AIJS74LG.js → chunk-FFDC5OWV.js} +8 -2
- package/dist/{chunk-KQGWP4KH.js → chunk-FNN5QINS.js} +1 -1
- package/dist/{chunk-DMKI64EZ.js → chunk-FSZAFCU7.js} +1 -1
- package/dist/{chunk-L3N7RJHS.js → chunk-GQETH4PE.js} +3 -3
- package/dist/{chunk-ZD3RDNZI.js → chunk-H2SGM2V2.js} +1 -1
- package/dist/{chunk-OSAH7S5M.js → chunk-H7FXBK3Q.js} +1 -1
- package/dist/{chunk-BRF3O35X.js → chunk-HBCRRCTX.js} +109 -31
- package/dist/{chunk-KXX6NLOT.js → chunk-IBYC3VUA.js} +1 -1
- package/dist/{chunk-IPDS2SWI.js → chunk-IEDT4EXE.js} +1 -1
- package/dist/{chunk-TCWMDCBX.js → chunk-IZIRIDJH.js} +2 -2
- package/dist/{chunk-4PKQEKAI.js → chunk-IZKXUYZE.js} +2 -2
- package/dist/chunk-KCFF4ENE.js +24 -0
- package/dist/{chunk-IZ7L6V76.js → chunk-KZ3L3W4Q.js} +1 -1
- package/dist/{chunk-YEBKEWKB.js → chunk-LECTCGGS.js} +1 -1
- package/dist/{chunk-O3DUX6P2.js → chunk-LVPSWUS6.js} +4 -4
- package/dist/{chunk-LKQS72D5.js → chunk-MNHPG2UO.js} +1 -1
- package/dist/{chunk-XKKBIFE5.js → chunk-NHAG2KCE.js} +7 -7
- package/dist/{chunk-HVXRYMGQ.js → chunk-PKO2EWO7.js} +1 -1
- package/dist/{chunk-XNDXENJQ.js → chunk-PRXAOZWZ.js} +3 -3
- package/dist/{chunk-4YZYYP7V.js → chunk-PTXZDSW5.js} +3 -3
- package/dist/{chunk-QF7AE3DB.js → chunk-QECEOV6W.js} +1 -1
- package/dist/{chunk-XGLNQDLN.js → chunk-QESSGUKX.js} +1 -1
- package/dist/{chunk-3SVC7SW4.js → chunk-RN5I2DP5.js} +3 -3
- package/dist/{chunk-QBYHNRHU.js → chunk-S3RCK56N.js} +1 -1
- package/dist/{chunk-RYL6ZIJP.js → chunk-SWCSL6WS.js} +5 -5
- package/dist/{chunk-2PSPARWP.js → chunk-TYXH3EKP.js} +4 -0
- package/dist/{chunk-FMNVTKX2.js → chunk-U3BCDVZO.js} +1 -1
- package/dist/{chunk-FLVI7FHU.js → chunk-VS732BUG.js} +1 -1
- package/dist/{chunk-LFNQDTSH.js → chunk-VYKPBRCB.js} +63 -12
- package/dist/{chunk-QLGBNGXC.js → chunk-XEFFJRYX.js} +2 -2
- package/dist/{chunk-3ZPFRDEJ.js → chunk-XINGD6MS.js} +4 -4
- package/dist/chunk-XSEGMQYA.js +30 -0
- package/dist/{chunk-GNTVYF7E.js → chunk-YOEGUBBY.js} +5 -5
- package/dist/{chunk-35PWWBGZ.js → chunk-YYEYOFHW.js} +1 -1
- package/dist/{chunk-XYHBVSWD.js → chunk-ZTM3RMAD.js} +2 -2
- package/dist/{clean-RLTHFTQA.js → clean-F75G45BI.js} +52 -31
- package/dist/cli.js +53 -53
- package/dist/{cloud-Q5TGBTW3.js → cloud-KVJAOUWH.js} +4 -4
- package/dist/{cloudrun-4UZF74MV.js → cloudrun-Q7Q7O4GY.js} +6 -6
- package/dist/{compare-Q5UNR2V4.js → compare-DGUJ4QWU.js} +30 -30
- package/dist/{compiler-ZXT27Z62.js → compiler-NCN6PTVH.js} +6 -6
- package/dist/{component-H734P32V.js → component-PSYGT3JV.js} +3 -3
- package/dist/{compositions-YEHDSTDR.js → compositions-J3GAETIC.js} +13 -13
- package/dist/{delete-LDBMPWRT.js → delete-MR7LRGRP.js} +2 -2
- package/dist/{deterministicFonts-C5JQLCTS.js → deterministicFonts-EOMDR6ZF.js} +3 -3
- package/dist/{dist-RGE4QASU.js → dist-2YYJYYPY.js} +3 -3
- package/dist/{dist-7JXLFD6U.js → dist-3MHXG6EJ.js} +4 -4
- package/dist/{doctor-XNJFIWJ7.js → doctor-JOUGR5UK.js} +11 -11
- package/dist/{events-UKCPUWMH.js → events-63BKDWVO.js} +3 -3
- package/dist/{events-G6UFLNXR.js → events-FZFBJCIH.js} +2 -2
- package/dist/{feedback-CK3KZ7DE.js → feedback-QLKWUDXR.js} +8 -8
- package/dist/{figma-3D5C737B.js → figma-YJ7HGVQC.js} +3 -3
- package/dist/fontLocalizeCli.js +12 -12
- package/dist/{get-QGTQUWFT.js → get-54MUUXMR.js} +2 -2
- package/dist/{gpuPolicy-G5QAQDYF.js → gpuPolicy-5OVNR75D.js} +9 -9
- package/dist/{grade-compare-XIHOM62P.js → grade-compare-7IDUF673.js} +30 -30
- package/dist/{gsapParserAcorn-VXZ3ZIF2.js → gsapParserAcorn-7NCK2LVT.js} +2 -2
- package/dist/{help-U5UBXT52.js → help-GQFYO5LU.js} +1 -1
- package/dist/{history-5J73QZSA.js → history-UI3O7CSO.js} +77 -205
- package/dist/hyperframes-player.global.js +1 -1
- package/dist/{info-RJHQQQUP.js → info-VQV3APEX.js} +9 -9
- package/dist/{init-M6UQOYYC.js → init-2UBFV5D7.js} +24 -24
- package/dist/{inspect-PRQWDBTP.js → inspect-36ACJ3CO.js} +31 -31
- package/dist/{installer-ZL7UKYK5.js → installer-GDUZDJG4.js} +7 -7
- package/dist/{keyframes-VMUJYTCQ.js → keyframes-GPBU34BG.js} +227 -82
- package/dist/{lambda-ON42DRQD.js → lambda-CBJVLTHY.js} +7 -7
- package/dist/{layout-QKBFVDVY.js → layout-2IHFV6EI.js} +31 -31
- package/dist/{lint-ZQHHL2SV.js → lint-IR6IYCEO.js} +9 -9
- package/dist/{lintProject-3Z6OJCSO.js → lintProject-XQWXJYSC.js} +3 -3
- package/dist/{list-YBRKIWGF.js → list-UAGKWARH.js} +2 -2
- package/dist/{login-4B2YXGLB.js → login-2GYCNCZF.js} +4 -4
- package/dist/{media-treatment-5C5EJAAP.js → media-treatment-Z553F7XI.js} +12 -12
- package/dist/{models-R7ZDWBJ6.js → models-6AZTNNSO.js} +12 -12
- package/dist/{motionShot-ZHESZNUL.js → motionShot-ARRL7WXT.js} +28 -15
- package/dist/{normalize-audio-EVWZVTBM.js → normalize-audio-CME4NUB6.js} +6 -6
- package/dist/{open-TV6HT4UT.js → open-HU2VXUFD.js} +4 -4
- package/dist/{parakeetRunner-YVVF4CG6.js → parakeetRunner-X5MKFMCB.js} +12 -12
- package/dist/{pipeline-ETR5SEBS.js → pipeline-MNFTEBJU.js} +9 -9
- package/dist/{play-C6WONRZV.js → play-2BBJDDAZ.js} +24 -24
- package/dist/{present-RANXTVPA.js → present-OE2WL2NY.js} +20 -20
- package/dist/{preview-KF6UMBT2.js → preview-RYSCCAWJ.js} +19 -19
- package/dist/{producer-CY2GXBWN.js → producer-SQWNRO66.js} +1 -1
- package/dist/{projectConfig-KPXCNMBM.js → projectConfig-B55Z7YKQ.js} +9 -9
- package/dist/{publication-Y36N4A7E.js → publication-MDZCOBJP.js} +4 -4
- package/dist/{publish-RCQJO35C.js → publish-AILV36BO.js} +12 -12
- package/dist/{remove-background-3ZMXVWLB.js → remove-background-6F72LKHG.js} +10 -10
- package/dist/{render-PTNEFO5U.js → render-2MZKU3L5.js} +10 -10
- package/dist/{render-FKVRCDT2.js → render-5EREH6YD.js} +57 -22
- package/dist/{render-WUSTRMUD.js → render-6GH5FSXU.js} +5 -5
- package/dist/{render-batch-AQHU2JWZ.js → render-batch-UMBUQWWS.js} +5 -5
- package/dist/renderSetupWorker.js +3 -3
- package/dist/{resolver-L5BDZJKU.js → resolver-KO6WWO5P.js} +6 -6
- package/dist/{sherpa-3S3ISG7G.js → sherpa-DADGYWNR.js} +11 -11
- package/dist/skills/hyperframes-cli/SKILL.md +3 -0
- package/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +5 -1
- package/dist/{skills-X4WQEKNF.js → skills-QLKSNU4Y.js} +5 -5
- package/dist/{skillsUpdateCheck-UYCVHDZ6.js → skillsUpdateCheck-Z2QQGF2W.js} +3 -3
- package/dist/{snapshot-YEYHQEZQ.js → snapshot-D5BBFXOS.js} +118 -41
- package/dist/{src-2NN2WB6X.js → src-AVQBAECC.js} +8 -8
- package/dist/{src-OLBDPEG6.js → src-V7263E47.js} +11 -11
- package/dist/{staticProjectServer-7H35NMWB.js → staticProjectServer-HNWSVEWN.js} +20 -20
- package/dist/studio/assets/{hyperframes-player-DJt8E7k4.js → hyperframes-player-0BY7-PK7.js} +1 -1
- package/dist/studio/assets/{index-DWIHrAY2.js → index-COrWdzlj.js} +1 -1
- package/dist/studio/assets/{index-BGks9KLI.js → index-p-VXwuJ2.js} +132 -132
- package/dist/studio/assets/{index-DgbLblGR.js → index-rmD5k_ES.js} +1 -1
- package/dist/studio/index.html +1 -1
- package/dist/{studio-api-HGM2A5I3.js → studio-api-UXKTDWLT.js} +30 -16
- package/dist/{studioServer-M6XFL7N6.js → studioServer-JOVR24O3.js} +43 -43
- package/dist/{synthesize-22B7L5GX.js → synthesize-MBDVUSH3.js} +2 -1
- package/dist/{telemetry-KVCZKVTE.js → telemetry-UW7XAM5W.js} +3 -3
- package/dist/{timeline-VYYKZR4V.js → timeline-JSGTZ3AN.js} +20 -20
- package/dist/{tokens-B5NFFL55.js → tokens-ADDBGTQ7.js} +2 -2
- package/dist/{transcribe-SBETOIWI.js → transcribe-FEHOK626.js} +10 -10
- package/dist/{transcribe-WNFRYZAC.js → transcribe-P6MJGSZJ.js} +16 -16
- package/dist/{tts-QSAQZVEZ.js → tts-POI553RG.js} +1 -1
- package/dist/{updateCheck-5MM2DFUC.js → updateCheck-IBWMLXQ4.js} +2 -2
- package/dist/{upgrade-LWID54R3.js → upgrade-NHZ7AYPO.js} +2 -2
- package/dist/{urlDownloader-CVLNXFDG.js → urlDownloader-ULDRBWS5.js} +8 -8
- package/dist/{validate-V4CGQ6OF.js → validate-NVWNXNBV.js} +25 -25
- package/dist/{video-VJB2KRSS.js → video-74DRD2DN.js} +9 -9
- package/package.json +1 -1
- package/dist/chunk-WBLXWOYK.js +0 -15
|
@@ -5,8 +5,9 @@ var require = __hf_createRequire(import.meta.url);
|
|
|
5
5
|
var __filename = __hf_fileURLToPath(import.meta.url);
|
|
6
6
|
var __dirname = __hf_dirname(__filename);
|
|
7
7
|
import {
|
|
8
|
+
pinProjectRecord,
|
|
8
9
|
realFilePath
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-FFDC5OWV.js";
|
|
10
11
|
import {
|
|
11
12
|
atomicTempPath,
|
|
12
13
|
isAtomicTempPath,
|
|
@@ -17,41 +18,57 @@ import {
|
|
|
17
18
|
mkdirWithinProject
|
|
18
19
|
} from "./chunk-OFQWTKPW.js";
|
|
19
20
|
|
|
20
|
-
// ../studio-server/dist/chunk-
|
|
21
|
-
import { randomUUID as
|
|
21
|
+
// ../studio-server/dist/chunk-CW5G6XXR.js
|
|
22
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
22
23
|
import { mkdir as mkdir2, readdir as readdir3, rm as rm3, rmdir, writeFile } from "fs/promises";
|
|
23
24
|
import {
|
|
24
|
-
existsSync as
|
|
25
|
-
lstatSync as
|
|
26
|
-
statSync as
|
|
25
|
+
existsSync as existsSync3,
|
|
26
|
+
lstatSync as lstatSync3,
|
|
27
|
+
statSync as statSync4,
|
|
27
28
|
mkdirSync as mkdirSync4,
|
|
28
29
|
readdirSync as readdirSync3,
|
|
29
|
-
readFileSync as
|
|
30
|
+
readFileSync as readFileSync9,
|
|
30
31
|
realpathSync,
|
|
31
32
|
rmSync as rmSync2
|
|
32
33
|
} from "fs";
|
|
33
34
|
import { homedir } from "os";
|
|
34
|
-
import { dirname as dirname4, isAbsolute as
|
|
35
|
+
import { dirname as dirname4, isAbsolute as isAbsolute5, join as join7, relative as relative4, resolve as resolve6, sep as sep4 } from "path";
|
|
35
36
|
import { createHash, randomUUID } from "crypto";
|
|
37
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
38
|
+
import { isAbsolute, relative, resolve, sep } from "path";
|
|
39
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
40
|
+
import { linkSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
41
|
+
import { join } from "path";
|
|
36
42
|
import { createHash as createHash2 } from "crypto";
|
|
37
|
-
import { lstatSync, readFileSync, readdirSync } from "fs";
|
|
38
|
-
import { extname, isAbsolute, relative, resolve, sep } from "path";
|
|
39
|
-
import { createHash as createHash3, randomUUID as
|
|
43
|
+
import { lstatSync, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
44
|
+
import { extname, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
45
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
|
|
40
46
|
import { constants, createReadStream, renameSync } from "fs";
|
|
41
47
|
import { copyFile, mkdir, readdir, readFile, rename, rm, stat } from "fs/promises";
|
|
42
|
-
import { dirname, join } from "path";
|
|
48
|
+
import { dirname, join as join2 } from "path";
|
|
43
49
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
44
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
50
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
45
51
|
import { lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
|
|
46
52
|
import { tmpdir } from "os";
|
|
47
53
|
import { dirname as dirname2, join as join4 } from "path";
|
|
48
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
49
|
-
import { existsSync, mkdirSync, readFileSync as readFileSync2, statSync, writeFileSync } from "fs";
|
|
50
|
-
import { basename, join as join2, resolve as resolve2 } from "path";
|
|
51
54
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
52
|
-
import {
|
|
53
|
-
import { join as join3 } from "path";
|
|
54
|
-
import {
|
|
55
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
56
|
+
import { basename, join as join3, resolve as resolve3 } from "path";
|
|
57
|
+
import { createHash as createHash5, randomUUID as randomUUID6 } from "crypto";
|
|
58
|
+
import { createReadStream as createReadStream2, existsSync as existsSync2, readFileSync as readFileSync7, renameSync as renameSync3, statSync as statSync3 } from "fs";
|
|
59
|
+
import { isAbsolute as isAbsolute4, join as join6, resolve as resolve5 } from "path";
|
|
60
|
+
import { createHash as createHash4 } from "crypto";
|
|
61
|
+
import {
|
|
62
|
+
closeSync,
|
|
63
|
+
constants as constants2,
|
|
64
|
+
fstatSync,
|
|
65
|
+
lstatSync as lstatSync2,
|
|
66
|
+
openSync,
|
|
67
|
+
readFileSync as readFileSync6,
|
|
68
|
+
unlinkSync
|
|
69
|
+
} from "fs";
|
|
70
|
+
import { isAbsolute as isAbsolute3, join as join5, relative as relative3, resolve as resolve4, sep as sep3 } from "path";
|
|
71
|
+
import { closeSync as closeSync2, constants as constants3, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync8, writeSync } from "fs";
|
|
55
72
|
import { dirname as dirname3 } from "path";
|
|
56
73
|
var RECEIPT_TTL_MS = 1e4;
|
|
57
74
|
var receipts = /* @__PURE__ */ new Map();
|
|
@@ -128,6 +145,127 @@ function newestReceipt(absPath, expectedVersion) {
|
|
|
128
145
|
return receipt;
|
|
129
146
|
}
|
|
130
147
|
var consumeFileWriteReceipt = identifyFileWrite;
|
|
148
|
+
var HistoryBusyError = class extends Error {
|
|
149
|
+
constructor(pid) {
|
|
150
|
+
super(`This project's history is open in another process (pid ${pid}).`);
|
|
151
|
+
this.pid = pid;
|
|
152
|
+
this.name = "HistoryBusyError";
|
|
153
|
+
}
|
|
154
|
+
pid;
|
|
155
|
+
};
|
|
156
|
+
function alive(pid) {
|
|
157
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
158
|
+
try {
|
|
159
|
+
process.kill(pid, 0);
|
|
160
|
+
return true;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return error.code === "EPERM";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function ownerOf(file) {
|
|
166
|
+
try {
|
|
167
|
+
const text = readFileSync(file, "utf-8");
|
|
168
|
+
return /^\d+$/.test(text) ? Number(text) : Number.NaN;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error.code === "ENOENT") return null;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function claim(file) {
|
|
175
|
+
const draft = `${file}-${process.pid}-${randomUUID2()}.tmp`;
|
|
176
|
+
writeFileSync(draft, String(process.pid));
|
|
177
|
+
try {
|
|
178
|
+
linkSync(draft, file);
|
|
179
|
+
return true;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (error.code !== "EEXIST") throw error;
|
|
182
|
+
return false;
|
|
183
|
+
} finally {
|
|
184
|
+
rmSync(draft, { force: true });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function releaseOwn(file) {
|
|
188
|
+
if (ownerOf(file) === process.pid) rmSync(file, { force: true });
|
|
189
|
+
}
|
|
190
|
+
function evictDeadOwner(file) {
|
|
191
|
+
const evictor = `${file}.evict`;
|
|
192
|
+
if (!claim(evictor)) {
|
|
193
|
+
const holder = ownerOf(evictor);
|
|
194
|
+
if (holder !== null && !alive(holder)) rmSync(evictor, { force: true });
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const pid = ownerOf(file);
|
|
199
|
+
if (pid !== null && !alive(pid)) rmSync(file, { force: true });
|
|
200
|
+
return true;
|
|
201
|
+
} finally {
|
|
202
|
+
releaseOwn(evictor);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function takeHistoryOwnership(home, waitMs) {
|
|
206
|
+
const file = join(home, "owner.pid");
|
|
207
|
+
const deadline = Date.now() + waitMs;
|
|
208
|
+
mkdirSync(home, { recursive: true });
|
|
209
|
+
for (; ; ) {
|
|
210
|
+
if (claim(file)) {
|
|
211
|
+
let held = true;
|
|
212
|
+
return () => {
|
|
213
|
+
if (held) releaseOwn(file);
|
|
214
|
+
held = false;
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const pid = ownerOf(file);
|
|
218
|
+
if (pid === null) continue;
|
|
219
|
+
if (!alive(pid) && evictDeadOwner(file)) continue;
|
|
220
|
+
if (Date.now() >= deadline) throw new HistoryBusyError(pid);
|
|
221
|
+
await new Promise((settle) => setTimeout(settle, 50));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
var manifest = ".hyperframes/render-outputs.json";
|
|
225
|
+
function readRenderOutputs(projectDir) {
|
|
226
|
+
const path = pinProjectRecord(projectDir, manifest);
|
|
227
|
+
if (!path) return [];
|
|
228
|
+
let text;
|
|
229
|
+
try {
|
|
230
|
+
text = readFileSync2(path, "utf8");
|
|
231
|
+
} catch (error) {
|
|
232
|
+
const code = error.code;
|
|
233
|
+
if (code === "ENOENT" || code === "ENOTDIR") return [];
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
const value = JSON.parse(text);
|
|
237
|
+
if (value.version !== 1 || !Array.isArray(value.paths) || !value.paths.every(
|
|
238
|
+
(p) => typeof p === "string" && p !== "" && !isAbsolute(p) && !p.split(/[\\/]/).some((part) => part === ".." || part === ".")
|
|
239
|
+
))
|
|
240
|
+
throw new Error("Invalid render output registry.");
|
|
241
|
+
return value.paths;
|
|
242
|
+
}
|
|
243
|
+
function readRenderOutputsOrNone(projectDir) {
|
|
244
|
+
try {
|
|
245
|
+
return readRenderOutputs(projectDir);
|
|
246
|
+
} catch {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function isRenderOutput(projectDir, path, outputs) {
|
|
251
|
+
const candidate = relative(resolve(projectDir), resolve(path)).split(sep).join("/");
|
|
252
|
+
return outputs.some((output) => candidate === output || candidate.startsWith(`${output}/`));
|
|
253
|
+
}
|
|
254
|
+
async function registerRenderOutput(projectDir, output) {
|
|
255
|
+
const path = relative(resolve(projectDir), resolve(output)).split(sep).join("/");
|
|
256
|
+
if (path === "") throw new Error("Render output cannot replace the project directory.");
|
|
257
|
+
if (isAbsolute(path) || path === ".." || path.startsWith("../")) return;
|
|
258
|
+
const registry = pinProjectRecord(projectDir, manifest);
|
|
259
|
+
const lock = pinProjectRecord(projectDir, ".hyperframes/render-outputs-lock");
|
|
260
|
+
if (!registry || !lock) throw new Error("Render output registry must stay inside the project.");
|
|
261
|
+
const release = await takeHistoryOwnership(lock, 5e3);
|
|
262
|
+
try {
|
|
263
|
+
const paths = [.../* @__PURE__ */ new Set([...readRenderOutputs(projectDir), path])].sort();
|
|
264
|
+
replaceFileAtomically(registry, JSON.stringify({ version: 1, paths }) + "\n", 420);
|
|
265
|
+
} finally {
|
|
266
|
+
release();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
131
269
|
var SIGNATURE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
132
270
|
".cjs",
|
|
133
271
|
".css",
|
|
@@ -162,25 +300,36 @@ var STUDIO_SIGNATURE_MANIFEST_PATHS = [
|
|
|
162
300
|
".hyperframes/studio-motion.json"
|
|
163
301
|
];
|
|
164
302
|
function affectsProjectSignature(projectDir, changedPath) {
|
|
165
|
-
const relativePath =
|
|
166
|
-
if (relativePath === "" || relativePath.startsWith("..") ||
|
|
303
|
+
const relativePath = relative2(resolve2(projectDir), resolve2(changedPath));
|
|
304
|
+
if (relativePath === "" || relativePath.startsWith("..") || isAbsolute2(relativePath) || isAtomicTempPath(relativePath)) {
|
|
167
305
|
return false;
|
|
168
306
|
}
|
|
169
|
-
|
|
307
|
+
if (isRenderOutput(projectDir, changedPath, readRenderOutputsOrNone(projectDir))) return false;
|
|
308
|
+
const segments = relativePath.split(sep2);
|
|
170
309
|
if (STUDIO_SIGNATURE_MANIFEST_PATHS.includes(segments.join("/"))) return true;
|
|
171
310
|
return !segments.some((segment) => SIGNATURE_EXCLUDED_DIRS.has(segment));
|
|
172
311
|
}
|
|
173
312
|
var projectSignatureCache = /* @__PURE__ */ new Map();
|
|
174
313
|
var COARSEST_FILE_TIME_TICK_MS = 2e3;
|
|
175
314
|
function isPathWithin(parentDir, childPath) {
|
|
176
|
-
const childRelativePath =
|
|
177
|
-
return childRelativePath === "" || !childRelativePath.startsWith("..") && !
|
|
315
|
+
const childRelativePath = relative2(parentDir, childPath);
|
|
316
|
+
return childRelativePath === "" || !childRelativePath.startsWith("..") && !isAbsolute2(childRelativePath);
|
|
178
317
|
}
|
|
179
318
|
function isTextContentEligible(file, size) {
|
|
180
319
|
return SIGNATURE_TEXT_EXTENSIONS.has(extname(file).toLowerCase()) && size <= MAX_SIGNATURE_TEXT_BYTES;
|
|
181
320
|
}
|
|
182
321
|
var isSkippedEntry = (entry) => SIGNATURE_EXCLUDED_DIRS.has(entry) || isAtomicTempPath(entry);
|
|
183
|
-
function
|
|
322
|
+
function signatureEntryStat(projectDir, entry, file, outputs) {
|
|
323
|
+
if (isSkippedEntry(entry) || !isPathWithin(projectDir, file)) return null;
|
|
324
|
+
if (isRenderOutput(projectDir, file, outputs)) return null;
|
|
325
|
+
try {
|
|
326
|
+
const stat2 = lstatSync(file);
|
|
327
|
+
return stat2.isSymbolicLink() ? null : stat2;
|
|
328
|
+
} catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function collectProjectSignatureFiles(projectDir, dir, files, outputs = readRenderOutputsOrNone(projectDir)) {
|
|
184
333
|
let entries;
|
|
185
334
|
try {
|
|
186
335
|
entries = readdirSync(dir).sort();
|
|
@@ -188,18 +337,11 @@ function collectProjectSignatureFiles(projectDir, dir, files) {
|
|
|
188
337
|
return;
|
|
189
338
|
}
|
|
190
339
|
for (const entry of entries) {
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
if (!
|
|
194
|
-
let stat2;
|
|
195
|
-
try {
|
|
196
|
-
stat2 = lstatSync(file);
|
|
197
|
-
} catch {
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
if (stat2.isSymbolicLink()) continue;
|
|
340
|
+
const file = resolve2(dir, entry);
|
|
341
|
+
const stat2 = signatureEntryStat(projectDir, entry, file, outputs);
|
|
342
|
+
if (!stat2) continue;
|
|
201
343
|
if (stat2.isDirectory()) {
|
|
202
|
-
collectProjectSignatureFiles(projectDir, file, files);
|
|
344
|
+
collectProjectSignatureFiles(projectDir, file, files, outputs);
|
|
203
345
|
} else if (stat2.isFile()) {
|
|
204
346
|
files.push({
|
|
205
347
|
file,
|
|
@@ -214,7 +356,7 @@ function collectProjectSignatureFiles(projectDir, dir, files) {
|
|
|
214
356
|
function collectProjectSignatureManifestFiles(projectDir, files) {
|
|
215
357
|
const seen = new Set(files.map((entry) => entry.file));
|
|
216
358
|
for (const manifestPath of STUDIO_SIGNATURE_MANIFEST_PATHS) {
|
|
217
|
-
const file =
|
|
359
|
+
const file = resolve2(projectDir, manifestPath);
|
|
218
360
|
if (seen.has(file) || !isPathWithin(projectDir, file)) continue;
|
|
219
361
|
let stat2;
|
|
220
362
|
try {
|
|
@@ -236,7 +378,7 @@ function collectProjectSignatureManifestFiles(projectDir, files) {
|
|
|
236
378
|
function createProjectFingerprint(projectDir, files) {
|
|
237
379
|
const hash = createHash2("sha256");
|
|
238
380
|
for (const entry of files) {
|
|
239
|
-
hash.update(
|
|
381
|
+
hash.update(relative2(projectDir, entry.file));
|
|
240
382
|
hash.update("\0");
|
|
241
383
|
hash.update(String(entry.size));
|
|
242
384
|
hash.update("\0");
|
|
@@ -256,9 +398,9 @@ function collectProjectFiles(normalizedProjectDir) {
|
|
|
256
398
|
return collected;
|
|
257
399
|
}
|
|
258
400
|
function listProjectFiles(projectDir) {
|
|
259
|
-
const normalizedProjectDir =
|
|
401
|
+
const normalizedProjectDir = resolve2(projectDir);
|
|
260
402
|
return collectProjectFiles(normalizedProjectDir).map((entry) => ({
|
|
261
|
-
path:
|
|
403
|
+
path: relative2(normalizedProjectDir, entry.file).split(sep2).join("/"),
|
|
262
404
|
size: entry.size,
|
|
263
405
|
mtimeMs: entry.mtimeMs,
|
|
264
406
|
ctimeMs: entry.ctimeMs
|
|
@@ -273,11 +415,11 @@ async function resolveProjectAndSignature(adapter, projectId) {
|
|
|
273
415
|
return { project, signature: resolveProjectSignature(adapter, project.dir) };
|
|
274
416
|
}
|
|
275
417
|
function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set()) {
|
|
276
|
-
const normalizedProjectDir =
|
|
418
|
+
const normalizedProjectDir = resolve2(projectDir);
|
|
277
419
|
const signedAt = Date.now();
|
|
278
420
|
const collected = collectProjectFiles(normalizedProjectDir);
|
|
279
421
|
const files = collected.filter(
|
|
280
|
-
(entry) => !excluding.has(
|
|
422
|
+
(entry) => !excluding.has(relative2(normalizedProjectDir, entry.file).split(sep2).join("/"))
|
|
281
423
|
);
|
|
282
424
|
files.sort((a, b) => a.file.localeCompare(b.file));
|
|
283
425
|
const fingerprint = createProjectFingerprint(normalizedProjectDir, files);
|
|
@@ -286,14 +428,14 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
|
|
|
286
428
|
if (cached?.fingerprint === fingerprint) return cached.signature;
|
|
287
429
|
const hash = createHash2("sha256");
|
|
288
430
|
for (const entry of files) {
|
|
289
|
-
const relativePath =
|
|
431
|
+
const relativePath = relative2(normalizedProjectDir, entry.file);
|
|
290
432
|
hash.update(relativePath);
|
|
291
433
|
hash.update("\0");
|
|
292
434
|
hash.update(String(entry.size));
|
|
293
435
|
hash.update("\0");
|
|
294
436
|
if (entry.textContentEligible) {
|
|
295
437
|
try {
|
|
296
|
-
hash.update(
|
|
438
|
+
hash.update(readFileSync3(entry.file));
|
|
297
439
|
} catch {
|
|
298
440
|
hash.update(String(entry.mtimeMs));
|
|
299
441
|
}
|
|
@@ -312,7 +454,7 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
|
|
|
312
454
|
var BLOB_HASH = /^[0-9a-f]{64}$/;
|
|
313
455
|
function blobPath(dir, hash) {
|
|
314
456
|
if (!BLOB_HASH.test(hash)) throw new Error("That is not a history blob.");
|
|
315
|
-
return
|
|
457
|
+
return join2(dir, hash.slice(0, 2), hash);
|
|
316
458
|
}
|
|
317
459
|
var readBlob = (dir, hash) => readFile(blobPath(dir, hash));
|
|
318
460
|
async function hashFile(path) {
|
|
@@ -336,13 +478,13 @@ async function openBlobStore(dir) {
|
|
|
336
478
|
const sizes = /* @__PURE__ */ new Map();
|
|
337
479
|
await mkdir(dir, { recursive: true });
|
|
338
480
|
for (const shard of await readdir(dir))
|
|
339
|
-
for (const hash of await readdir(
|
|
340
|
-
sizes.set(hash, (await stat(
|
|
481
|
+
for (const hash of await readdir(join2(dir, shard)).catch(() => []))
|
|
482
|
+
sizes.set(hash, (await stat(join2(dir, shard, hash))).size);
|
|
341
483
|
const pathOf = (hash) => blobPath(dir, hash);
|
|
342
484
|
let total = [...sizes.values()].reduce((sum, size) => sum + size, 0);
|
|
343
485
|
return {
|
|
344
486
|
async put(absPath) {
|
|
345
|
-
const temp =
|
|
487
|
+
const temp = join2(dir, `incoming-${randomUUID3()}`);
|
|
346
488
|
await mkdir(dir, { recursive: true });
|
|
347
489
|
await copyFile(absPath, temp, constants.COPYFILE_FICLONE);
|
|
348
490
|
const hash = await hashFile(temp);
|
|
@@ -372,12 +514,12 @@ async function openBlobStore(dir) {
|
|
|
372
514
|
}
|
|
373
515
|
};
|
|
374
516
|
}
|
|
375
|
-
var ID_PATH =
|
|
517
|
+
var ID_PATH = join3(".hyperframes", "history-id");
|
|
376
518
|
var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
377
519
|
var isHistoryId = (name) => ID_SHAPE.test(name);
|
|
378
520
|
function readId(projectDir) {
|
|
379
521
|
try {
|
|
380
|
-
const id =
|
|
522
|
+
const id = readFileSync4(join3(projectDir, ID_PATH), "utf-8").trim();
|
|
381
523
|
return ID_SHAPE.test(id) ? id : null;
|
|
382
524
|
} catch {
|
|
383
525
|
return null;
|
|
@@ -394,7 +536,7 @@ function isRecordedFolder(historyDir, folder) {
|
|
|
394
536
|
}
|
|
395
537
|
function readRecord(historyDir) {
|
|
396
538
|
try {
|
|
397
|
-
return JSON.parse(
|
|
539
|
+
return JSON.parse(readFileSync4(join3(historyDir, "project.json"), "utf-8"));
|
|
398
540
|
} catch {
|
|
399
541
|
return null;
|
|
400
542
|
}
|
|
@@ -409,20 +551,20 @@ function isCopyOf0878Folder(recordedDir, id, folder) {
|
|
|
409
551
|
return !!there && !sameFolder(there, folder) && readId(recordedDir) === id;
|
|
410
552
|
}
|
|
411
553
|
function projectHistoryId(projectDir, historyRoot) {
|
|
412
|
-
const dir =
|
|
554
|
+
const dir = resolve3(projectDir);
|
|
413
555
|
const folder = statSync(dir);
|
|
414
556
|
let id = readId(dir);
|
|
415
|
-
if (!id && existsSync(
|
|
557
|
+
if (!id && existsSync(join3(dir, ID_PATH)))
|
|
416
558
|
throw new HistoryIdError(
|
|
417
|
-
`${
|
|
559
|
+
`${join3(dir, ID_PATH)} holds no history id this version can read; move it aside to start anew.`
|
|
418
560
|
);
|
|
419
|
-
if (!id || existsSync(
|
|
420
|
-
id =
|
|
421
|
-
mkdirWithinProject(dir,
|
|
422
|
-
|
|
561
|
+
if (!id || existsSync(join3(historyRoot, id)) && !isRecordedFolder(join3(historyRoot, id), folder)) {
|
|
562
|
+
id = randomUUID4();
|
|
563
|
+
mkdirWithinProject(dir, join3(dir, ".hyperframes"));
|
|
564
|
+
writeFileSync2(join3(dir, ID_PATH), `${id}
|
|
423
565
|
`);
|
|
424
566
|
}
|
|
425
|
-
recordProject(
|
|
567
|
+
recordProject(join3(historyRoot, id), dir, folder);
|
|
426
568
|
return id;
|
|
427
569
|
}
|
|
428
570
|
var HistoryIdError = class extends Error {
|
|
@@ -433,84 +575,8 @@ var HistoryIdError = class extends Error {
|
|
|
433
575
|
};
|
|
434
576
|
function recordProject(historyDir, dir, folder) {
|
|
435
577
|
const record = { dir, ino: folder.ino, born: folder.birthtimeMs, dev: folder.dev };
|
|
436
|
-
|
|
437
|
-
replaceFileAtomically(
|
|
438
|
-
}
|
|
439
|
-
var HistoryBusyError = class extends Error {
|
|
440
|
-
constructor(pid) {
|
|
441
|
-
super(`This project's history is open in another process (pid ${pid}).`);
|
|
442
|
-
this.pid = pid;
|
|
443
|
-
this.name = "HistoryBusyError";
|
|
444
|
-
}
|
|
445
|
-
pid;
|
|
446
|
-
};
|
|
447
|
-
function alive(pid) {
|
|
448
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
449
|
-
try {
|
|
450
|
-
process.kill(pid, 0);
|
|
451
|
-
return true;
|
|
452
|
-
} catch (error) {
|
|
453
|
-
return error.code === "EPERM";
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
function ownerOf(file) {
|
|
457
|
-
try {
|
|
458
|
-
const text = readFileSync3(file, "utf-8");
|
|
459
|
-
return /^\d+$/.test(text) ? Number(text) : Number.NaN;
|
|
460
|
-
} catch (error) {
|
|
461
|
-
if (error.code === "ENOENT") return null;
|
|
462
|
-
throw error;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
function claim(file) {
|
|
466
|
-
const draft = `${file}-${randomUUID4()}.tmp`;
|
|
467
|
-
writeFileSync2(draft, String(process.pid));
|
|
468
|
-
try {
|
|
469
|
-
linkSync(draft, file);
|
|
470
|
-
return true;
|
|
471
|
-
} catch (error) {
|
|
472
|
-
if (error.code !== "EEXIST") throw error;
|
|
473
|
-
return false;
|
|
474
|
-
} finally {
|
|
475
|
-
rmSync(draft, { force: true });
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
function releaseOwn(file) {
|
|
479
|
-
if (ownerOf(file) === process.pid) rmSync(file, { force: true });
|
|
480
|
-
}
|
|
481
|
-
function evictDeadOwner(file) {
|
|
482
|
-
const evictor = `${file}.evict`;
|
|
483
|
-
if (!claim(evictor)) {
|
|
484
|
-
const holder = ownerOf(evictor);
|
|
485
|
-
if (holder !== null && !alive(holder)) rmSync(evictor, { force: true });
|
|
486
|
-
return false;
|
|
487
|
-
}
|
|
488
|
-
try {
|
|
489
|
-
const pid = ownerOf(file);
|
|
490
|
-
if (pid !== null && !alive(pid)) rmSync(file, { force: true });
|
|
491
|
-
return true;
|
|
492
|
-
} finally {
|
|
493
|
-
releaseOwn(evictor);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
async function takeHistoryOwnership(home, waitMs) {
|
|
497
|
-
const file = join3(home, "owner.pid");
|
|
498
|
-
const deadline = Date.now() + waitMs;
|
|
499
|
-
mkdirSync2(home, { recursive: true });
|
|
500
|
-
for (; ; ) {
|
|
501
|
-
if (claim(file)) {
|
|
502
|
-
let held = true;
|
|
503
|
-
return () => {
|
|
504
|
-
if (held) releaseOwn(file);
|
|
505
|
-
held = false;
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
const pid = ownerOf(file);
|
|
509
|
-
if (pid === null) continue;
|
|
510
|
-
if (!alive(pid) && evictDeadOwner(file)) continue;
|
|
511
|
-
if (Date.now() >= deadline) throw new HistoryBusyError(pid);
|
|
512
|
-
await new Promise((settle) => setTimeout(settle, 50));
|
|
513
|
-
}
|
|
578
|
+
mkdirSync2(historyDir, { recursive: true });
|
|
579
|
+
replaceFileAtomically(join3(historyDir, "project.json"), JSON.stringify(record), 420);
|
|
514
580
|
}
|
|
515
581
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
516
582
|
var KEEP_GONE_PROJECT_HISTORY_MS = 14 * DAY_MS;
|
|
@@ -641,7 +707,7 @@ async function bytesIn(dir) {
|
|
|
641
707
|
}
|
|
642
708
|
function prunedWithinADay(stamp, now) {
|
|
643
709
|
try {
|
|
644
|
-
const last = Number(
|
|
710
|
+
const last = Number(readFileSync5(stamp, "utf-8"));
|
|
645
711
|
return last <= now && now - last < DAY_MS;
|
|
646
712
|
} catch {
|
|
647
713
|
return false;
|
|
@@ -656,6 +722,332 @@ function pruneGoneProjectHistoriesDaily(historyRoot, now, { onError, budgetMs }
|
|
|
656
722
|
pruneWithin(historyRoot, { now, onError, budgetMs, measure: false }).then(({ finished }) => finished && writeFileSync3(stamp, String(now))).catch((error) => onError?.(error)).finally(() => pruning.delete(historyRoot));
|
|
657
723
|
});
|
|
658
724
|
}
|
|
725
|
+
var kinds = ["checks", "snapshots"];
|
|
726
|
+
var records = (state, kind) => [
|
|
727
|
+
...Object.values(state[kind].revisions).flat(),
|
|
728
|
+
...state[kind].unversioned
|
|
729
|
+
];
|
|
730
|
+
var protectedRevisions = (state) => /* @__PURE__ */ new Set([state.currentRevision, ...state.renders?.retainedRevisions ?? []]);
|
|
731
|
+
function managedPath(projectDir, path, kind) {
|
|
732
|
+
const canonicalProject = realFilePath(resolve4(projectDir));
|
|
733
|
+
const canonicalPath = realFilePath(path);
|
|
734
|
+
const root = join5(canonicalProject, ".hyperframes", "artifacts", kind);
|
|
735
|
+
const rel = relative3(root, canonicalPath);
|
|
736
|
+
return isAbsolute3(path) && rel !== "" && rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute3(rel) && pinProjectRecord(canonicalProject, relative3(canonicalProject, canonicalPath)) === canonicalPath;
|
|
737
|
+
}
|
|
738
|
+
function updateLatest(state) {
|
|
739
|
+
for (const kind of kinds) {
|
|
740
|
+
const available = records(state, kind).filter((record) => record.status !== "cleaned");
|
|
741
|
+
if (available.some(
|
|
742
|
+
(record) => record.id === state[kind].latest?.id && record.revision === state[kind].latest?.revision
|
|
743
|
+
))
|
|
744
|
+
continue;
|
|
745
|
+
const latest = available.sort((a, b) => b.createdAt - a.createdAt)[0];
|
|
746
|
+
state[kind].latest = latest ? { revision: latest.revision, id: latest.id } : null;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function planArtifactCleanup(projectDir, state, render) {
|
|
750
|
+
const protectedIds = protectedRevisions(state);
|
|
751
|
+
const candidates = [];
|
|
752
|
+
const keepPaths = /* @__PURE__ */ new Set();
|
|
753
|
+
const removable = (record, path, kind) => Boolean(record.revision) && !protectedIds.has(record.revision) && record.createdAt <= render.completedAt && managedPath(projectDir, path, kind);
|
|
754
|
+
for (const kind of kinds) {
|
|
755
|
+
for (const record of records(state, kind)) {
|
|
756
|
+
const path = record.image ?? record.report;
|
|
757
|
+
if (!path || record.status === "cleaned") continue;
|
|
758
|
+
if (removable(record, path, kind)) candidates.push({ kind, record, path });
|
|
759
|
+
else keepPaths.add(realFilePath(path));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
for (const { kind, record, path } of candidates) {
|
|
763
|
+
if (!keepPaths.has(realFilePath(path))) markCleaned(state, kind, record, path, render.id);
|
|
764
|
+
}
|
|
765
|
+
updateLatest(state);
|
|
766
|
+
}
|
|
767
|
+
function markCleaned(state, kind, record, path, renderId) {
|
|
768
|
+
(state.pendingArtifactCleanup ??= []).push({
|
|
769
|
+
path,
|
|
770
|
+
hash: record.contentHash,
|
|
771
|
+
revision: record.revision,
|
|
772
|
+
id: record.id,
|
|
773
|
+
kind
|
|
774
|
+
});
|
|
775
|
+
delete record.image;
|
|
776
|
+
delete record.report;
|
|
777
|
+
record.status = "cleaned";
|
|
778
|
+
record.cleanedAfterRender = renderId;
|
|
779
|
+
}
|
|
780
|
+
function restoreCleaned(state, item) {
|
|
781
|
+
const record = state[item.kind].revisions[item.revision]?.find(
|
|
782
|
+
(candidate) => candidate.id === item.id
|
|
783
|
+
);
|
|
784
|
+
if (record?.status !== "cleaned") return;
|
|
785
|
+
if (item.kind === "checks") record.report = item.path;
|
|
786
|
+
else record.image = item.path;
|
|
787
|
+
delete record.status;
|
|
788
|
+
delete record.cleanedAfterRender;
|
|
789
|
+
}
|
|
790
|
+
function stillPendingAfterUnlink(item) {
|
|
791
|
+
let descriptor;
|
|
792
|
+
try {
|
|
793
|
+
descriptor = openSync(item.path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
794
|
+
const stat2 = fstatSync(descriptor);
|
|
795
|
+
if (!stat2.isFile() || stat2.isSymbolicLink()) return false;
|
|
796
|
+
const hash = createHash4("sha256").update(readFileSync6(descriptor)).digest("hex");
|
|
797
|
+
if (hash !== item.hash) return false;
|
|
798
|
+
const current = lstatSync2(item.path);
|
|
799
|
+
if (current.dev !== stat2.dev || current.ino !== stat2.ino) return false;
|
|
800
|
+
unlinkSync(item.path);
|
|
801
|
+
return false;
|
|
802
|
+
} catch (error) {
|
|
803
|
+
return error.code !== "ENOENT";
|
|
804
|
+
} finally {
|
|
805
|
+
if (descriptor !== void 0) closeSync(descriptor);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
function finishArtifactCleanup(projectDir, state) {
|
|
809
|
+
const protectedIds = protectedRevisions(state);
|
|
810
|
+
const referenced = new Set(
|
|
811
|
+
kinds.flatMap(
|
|
812
|
+
(kind) => records(state, kind).flatMap((record) => record.image ?? record.report ?? []).map(realFilePath)
|
|
813
|
+
)
|
|
814
|
+
);
|
|
815
|
+
state.pendingArtifactCleanup = (state.pendingArtifactCleanup ?? []).filter((item) => {
|
|
816
|
+
if (!managedPath(projectDir, item.path, item.kind)) return false;
|
|
817
|
+
const canonicalPath = realFilePath(item.path);
|
|
818
|
+
if (protectedIds.has(item.revision)) {
|
|
819
|
+
restoreCleaned(state, item);
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
if (state.pendingChanges) return true;
|
|
823
|
+
if (referenced.has(canonicalPath)) return false;
|
|
824
|
+
return stillPendingAfterUnlink({ ...item, path: canonicalPath });
|
|
825
|
+
});
|
|
826
|
+
updateLatest(state);
|
|
827
|
+
}
|
|
828
|
+
function statePath(dir) {
|
|
829
|
+
const path = pinProjectRecord(dir, ".hyperframes/video-state.json");
|
|
830
|
+
if (!path) throw new Error("Video state must stay inside the project.");
|
|
831
|
+
return path;
|
|
832
|
+
}
|
|
833
|
+
var collection = () => ({ latest: null, revisions: {}, unversioned: [] });
|
|
834
|
+
var emptyState = () => ({
|
|
835
|
+
version: 1,
|
|
836
|
+
historyId: null,
|
|
837
|
+
currentRevision: null,
|
|
838
|
+
pendingChanges: true,
|
|
839
|
+
files: {},
|
|
840
|
+
revisions: {},
|
|
841
|
+
checks: collection(),
|
|
842
|
+
snapshots: collection()
|
|
843
|
+
});
|
|
844
|
+
function readState(dir) {
|
|
845
|
+
if (!existsSync2(statePath(dir))) return emptyState();
|
|
846
|
+
const state = JSON.parse(readFileSync7(statePath(dir), "utf8"));
|
|
847
|
+
if (state.version !== 1 || !state.checks || !state.snapshots || !state.revisions || !state.files)
|
|
848
|
+
throw new Error("Unsupported video-state schema.");
|
|
849
|
+
return state;
|
|
850
|
+
}
|
|
851
|
+
async function updateState(dir, update) {
|
|
852
|
+
const lock = pinProjectRecord(dir, ".hyperframes/video-state-lock");
|
|
853
|
+
if (!lock) throw new Error("Video state lock must stay inside the project.");
|
|
854
|
+
const release = await takeHistoryOwnership(lock, 5e3);
|
|
855
|
+
try {
|
|
856
|
+
let state;
|
|
857
|
+
try {
|
|
858
|
+
state = readState(dir);
|
|
859
|
+
} catch (error) {
|
|
860
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
861
|
+
renameSync3(statePath(dir), `${statePath(dir)}.invalid-${randomUUID6()}`);
|
|
862
|
+
state = emptyState();
|
|
863
|
+
}
|
|
864
|
+
update(state);
|
|
865
|
+
replaceFileAtomically(statePath(dir), JSON.stringify(state, null, 2) + "\n", 420);
|
|
866
|
+
} finally {
|
|
867
|
+
release();
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
async function writeHistoryState(projectDir, historyId, historyDirectory, entries, currentFiles, committedFiles) {
|
|
871
|
+
const blob = (hash) => hash === null ? null : resolve5(historyDirectory, "blobs", hash.slice(0, 2), hash);
|
|
872
|
+
await updateState(projectDir, (state) => {
|
|
873
|
+
if (state.historyId && state.historyId !== historyId) {
|
|
874
|
+
state.checks = collection();
|
|
875
|
+
state.snapshots = collection();
|
|
876
|
+
}
|
|
877
|
+
state.historyId = historyId;
|
|
878
|
+
state.currentRevision = entries.at(-1)?.id ?? null;
|
|
879
|
+
state.pendingChanges = currentFiles.size !== committedFiles.size || [...currentFiles].some(([path, hash]) => committedFiles.get(path) !== hash);
|
|
880
|
+
state.files = Object.fromEntries(
|
|
881
|
+
[...currentFiles].map(([path, hash]) => [path, { hash, content: blob(hash) }])
|
|
882
|
+
);
|
|
883
|
+
state.revisions = Object.fromEntries(
|
|
884
|
+
entries.map((entry) => [entry.id, revisionState(entry, blob)])
|
|
885
|
+
);
|
|
886
|
+
dropArtifactsOfGoneRevisions(state);
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
function revisionState(entry, blob) {
|
|
890
|
+
const targets = entry.targets?.filter(
|
|
891
|
+
(target) => entry.files.some((file) => file.path === target.file)
|
|
892
|
+
);
|
|
893
|
+
return {
|
|
894
|
+
actor: entry.who,
|
|
895
|
+
...targets?.length && { targets },
|
|
896
|
+
label: entry.label,
|
|
897
|
+
startedAt: entry.startedAt,
|
|
898
|
+
endedAt: entry.endedAt,
|
|
899
|
+
...entry.undoes && { undoes: entry.undoes },
|
|
900
|
+
...entry.restoredTo && { restoredTo: entry.restoredTo },
|
|
901
|
+
files: Object.fromEntries(
|
|
902
|
+
entry.files.map((file) => [
|
|
903
|
+
file.path,
|
|
904
|
+
{ before: blob(file.before), after: blob(file.after) }
|
|
905
|
+
])
|
|
906
|
+
)
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
function dropArtifactsOfGoneRevisions(state) {
|
|
910
|
+
const kept = (revision) => Boolean(state.revisions[revision]) || Boolean(state.renders?.retainedRevisions?.includes(revision));
|
|
911
|
+
for (const artifacts of [state.checks, state.snapshots]) {
|
|
912
|
+
for (const revision of Object.keys(artifacts.revisions)) {
|
|
913
|
+
if (!kept(revision)) delete artifacts.revisions[revision];
|
|
914
|
+
}
|
|
915
|
+
if (artifacts.latest?.revision && !state.revisions[artifacts.latest.revision])
|
|
916
|
+
artifacts.latest = null;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
async function fileHashes(dir) {
|
|
920
|
+
const files = {};
|
|
921
|
+
for (const { path } of listProjectFiles(dir).sort((a, b) => a.path.localeCompare(b.path))) {
|
|
922
|
+
const hash = createHash5("sha256");
|
|
923
|
+
for await (const chunk of createReadStream2(join6(dir, path))) hash.update(chunk);
|
|
924
|
+
files[path] = hash.digest("hex");
|
|
925
|
+
}
|
|
926
|
+
return files;
|
|
927
|
+
}
|
|
928
|
+
async function beginArtifactCapture(projectDir) {
|
|
929
|
+
const files = await fileHashes(projectDir);
|
|
930
|
+
const state = readState(projectDir);
|
|
931
|
+
const matches = state.historyId !== null && readId(projectDir) === state.historyId && !state.pendingChanges && Object.entries(state.files).every(([path, file]) => files[path] === file.hash) && Object.keys(files).filter((path) => !path.split("/").some((part) => part.startsWith("."))).every((path) => state.files[path]?.hash === files[path]);
|
|
932
|
+
return {
|
|
933
|
+
revision: matches ? state.currentRevision : null,
|
|
934
|
+
fingerprint: createHash5("sha256").update(JSON.stringify(files)).digest("hex")
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
async function recordArtifact(projectDir, capture, artifact) {
|
|
938
|
+
return recordArtifacts(projectDir, capture, [artifact]);
|
|
939
|
+
}
|
|
940
|
+
async function recordArtifacts(projectDir, capture, artifactsToRecord) {
|
|
941
|
+
const records2 = [];
|
|
942
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
943
|
+
for (const artifact of artifactsToRecord) {
|
|
944
|
+
const path = artifact.kind === "check" ? artifact.report : artifact.image;
|
|
945
|
+
if (!path || !isAbsolute4(path)) throw new Error("Artifact needs an absolute output path.");
|
|
946
|
+
hashes.set(artifact.id, createHash5("sha256").update(readFileSync7(path)).digest("hex"));
|
|
947
|
+
}
|
|
948
|
+
const after = await beginArtifactCapture(projectDir);
|
|
949
|
+
const unchanged = after.fingerprint === capture.fingerprint;
|
|
950
|
+
await updateState(projectDir, (state) => {
|
|
951
|
+
const revision = unchanged ? knownRevision(state, capture.revision) : null;
|
|
952
|
+
const attribution = attributionOf(unchanged, revision);
|
|
953
|
+
for (const artifact of artifactsToRecord) {
|
|
954
|
+
const { kind, ...fields } = artifact;
|
|
955
|
+
const record = {
|
|
956
|
+
...fields,
|
|
957
|
+
createdAt: Date.now(),
|
|
958
|
+
contentHash: hashes.get(artifact.id),
|
|
959
|
+
revision,
|
|
960
|
+
inputFingerprint: capture.fingerprint,
|
|
961
|
+
inputScope: "local-project-files",
|
|
962
|
+
attribution
|
|
963
|
+
};
|
|
964
|
+
const artifacts = kind === "check" ? state.checks : state.snapshots;
|
|
965
|
+
dropSupersededArtifacts(artifacts, artifact);
|
|
966
|
+
const list = revision ? artifacts.revisions[revision] ??= [] : artifacts.unversioned;
|
|
967
|
+
list.push(record);
|
|
968
|
+
records2.push(record);
|
|
969
|
+
artifacts.latest = { revision, id: record.id };
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
return records2;
|
|
973
|
+
}
|
|
974
|
+
function knownRevision(state, revision) {
|
|
975
|
+
return revision && state.revisions[revision] ? revision : null;
|
|
976
|
+
}
|
|
977
|
+
function attributionOf(unchanged, revision) {
|
|
978
|
+
if (!unchanged) return "inputs-changed";
|
|
979
|
+
return revision ? "recorded-revision" : "unversioned";
|
|
980
|
+
}
|
|
981
|
+
function supersedes(artifact, previous) {
|
|
982
|
+
if (previous.status !== "cleaned" && !existsSync2(previous.image ?? previous.report ?? ""))
|
|
983
|
+
return true;
|
|
984
|
+
if (previous.id === artifact.id) return true;
|
|
985
|
+
if (artifact.image && previous.image === artifact.image) return true;
|
|
986
|
+
return Boolean(artifact.report && previous.report === artifact.report);
|
|
987
|
+
}
|
|
988
|
+
function dropSupersededArtifacts(artifacts, artifact) {
|
|
989
|
+
for (const list of [...Object.values(artifacts.revisions), artifacts.unversioned]) {
|
|
990
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
991
|
+
if (supersedes(artifact, list[i])) list.splice(i, 1);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function listsOf(artifacts) {
|
|
996
|
+
return [...Object.values(artifacts.revisions), artifacts.unversioned];
|
|
997
|
+
}
|
|
998
|
+
async function forgetRecordedArtifacts(projectDir, paths) {
|
|
999
|
+
const gone = new Set(paths.map((path) => resolve5(path)));
|
|
1000
|
+
if (gone.size === 0) return;
|
|
1001
|
+
await updateState(projectDir, (state) => {
|
|
1002
|
+
for (const artifacts of [state.checks, state.snapshots]) {
|
|
1003
|
+
for (const list of listsOf(artifacts)) {
|
|
1004
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
1005
|
+
const record = list[i];
|
|
1006
|
+
const path = record.image ?? record.report;
|
|
1007
|
+
if (path && gone.has(resolve5(path))) list.splice(i, 1);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
if (artifacts.latest && !listsOf(artifacts).some(
|
|
1011
|
+
(list) => list.some((record) => record.id === artifacts.latest?.id)
|
|
1012
|
+
)) {
|
|
1013
|
+
artifacts.latest = null;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
function artifactDirectory(projectDir, kind) {
|
|
1019
|
+
return join6(resolve5(projectDir), ".hyperframes", "artifacts", kind, randomUUID6());
|
|
1020
|
+
}
|
|
1021
|
+
function retainRevision(retained, revision) {
|
|
1022
|
+
if (!revision) return [...retained];
|
|
1023
|
+
return [revision, ...retained.filter((id) => id !== revision)].slice(0, 2);
|
|
1024
|
+
}
|
|
1025
|
+
async function recordRenderSuccess(projectDir, output, status = "completed", capture) {
|
|
1026
|
+
const outputFile = statSync3(output);
|
|
1027
|
+
if (!outputFile.isFile() && !outputFile.isDirectory() || outputFile.isFile() && outputFile.size === 0)
|
|
1028
|
+
throw new Error("Render output is missing or empty.");
|
|
1029
|
+
const after = capture ? await beginArtifactCapture(projectDir) : null;
|
|
1030
|
+
const record = {
|
|
1031
|
+
id: randomUUID6(),
|
|
1032
|
+
completedAt: Date.now(),
|
|
1033
|
+
output: resolve5(output),
|
|
1034
|
+
status
|
|
1035
|
+
};
|
|
1036
|
+
const unchanged = Boolean(capture && after?.fingerprint === capture.fingerprint);
|
|
1037
|
+
await updateState(projectDir, (state) => {
|
|
1038
|
+
record.revision = unchanged ? knownRevision(state, capture?.revision) : null;
|
|
1039
|
+
state.renders = {
|
|
1040
|
+
latest: record,
|
|
1041
|
+
retainedRevisions: retainRevision(state.renders?.retainedRevisions ?? [], record.revision)
|
|
1042
|
+
};
|
|
1043
|
+
if (outputFile.isFile() && record.revision && !state.pendingChanges)
|
|
1044
|
+
planArtifactCleanup(projectDir, state, record);
|
|
1045
|
+
});
|
|
1046
|
+
await updateState(projectDir, (state) => {
|
|
1047
|
+
finishArtifactCleanup(projectDir, state);
|
|
1048
|
+
});
|
|
1049
|
+
return record;
|
|
1050
|
+
}
|
|
659
1051
|
var START = "start";
|
|
660
1052
|
function parseRecord(line) {
|
|
661
1053
|
try {
|
|
@@ -667,7 +1059,7 @@ function parseRecord(line) {
|
|
|
667
1059
|
function readLog(file, onUnreadable) {
|
|
668
1060
|
let text;
|
|
669
1061
|
try {
|
|
670
|
-
text =
|
|
1062
|
+
text = readFileSync8(file, "utf-8");
|
|
671
1063
|
} catch {
|
|
672
1064
|
return null;
|
|
673
1065
|
}
|
|
@@ -691,7 +1083,7 @@ function applyRecord(log, record) {
|
|
|
691
1083
|
function saveRecord(file, log, record) {
|
|
692
1084
|
let fd;
|
|
693
1085
|
try {
|
|
694
|
-
fd =
|
|
1086
|
+
fd = openSync2(file, constants3.O_WRONLY | constants3.O_APPEND);
|
|
695
1087
|
} catch (error) {
|
|
696
1088
|
if (error.code !== "ENOENT") throw error;
|
|
697
1089
|
return writeLog(file, log);
|
|
@@ -700,7 +1092,7 @@ function saveRecord(file, log, record) {
|
|
|
700
1092
|
writeSync(fd, `${JSON.stringify(record)}
|
|
701
1093
|
`);
|
|
702
1094
|
} finally {
|
|
703
|
-
|
|
1095
|
+
closeSync2(fd);
|
|
704
1096
|
}
|
|
705
1097
|
}
|
|
706
1098
|
function baselineRecord(log) {
|
|
@@ -711,30 +1103,30 @@ function baselineRecord(log) {
|
|
|
711
1103
|
};
|
|
712
1104
|
}
|
|
713
1105
|
function writeLog(file, log) {
|
|
714
|
-
const
|
|
1106
|
+
const records2 = [
|
|
715
1107
|
baselineRecord(log),
|
|
716
1108
|
...log.entries.map((entry) => ({ type: "entry", entry })),
|
|
717
1109
|
...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
|
|
718
1110
|
];
|
|
719
1111
|
mkdirSync3(dirname3(file), { recursive: true });
|
|
720
|
-
replaceFileAtomically(file,
|
|
1112
|
+
replaceFileAtomically(file, records2.map((r) => `${JSON.stringify(r)}
|
|
721
1113
|
`).join(""), 420);
|
|
722
1114
|
}
|
|
723
|
-
function applyEntry(
|
|
1115
|
+
function applyEntry(manifest2, entry, side) {
|
|
724
1116
|
for (const file of entry.files) {
|
|
725
1117
|
const hash = file[side];
|
|
726
|
-
if (hash === null)
|
|
727
|
-
else
|
|
1118
|
+
if (hash === null) manifest2.delete(file.path);
|
|
1119
|
+
else manifest2.set(file.path, hash);
|
|
728
1120
|
}
|
|
729
1121
|
}
|
|
730
1122
|
function manifestAt(log, point) {
|
|
731
1123
|
return point === START ? new Map(log.baseline) : manifestAround(log, point, "after");
|
|
732
1124
|
}
|
|
733
1125
|
function manifestAround(log, id, side) {
|
|
734
|
-
const
|
|
1126
|
+
const manifest2 = new Map(log.baseline);
|
|
735
1127
|
for (const entry of log.entries) {
|
|
736
|
-
applyEntry(
|
|
737
|
-
if (entry.id === id) return
|
|
1128
|
+
applyEntry(manifest2, entry, entry.id === id ? side : "after");
|
|
1129
|
+
if (entry.id === id) return manifest2;
|
|
738
1130
|
}
|
|
739
1131
|
return null;
|
|
740
1132
|
}
|
|
@@ -787,7 +1179,7 @@ function referencedHashes(log, current) {
|
|
|
787
1179
|
}
|
|
788
1180
|
return keep;
|
|
789
1181
|
}
|
|
790
|
-
var DEFAULT_HISTORY_ROOT =
|
|
1182
|
+
var DEFAULT_HISTORY_ROOT = join7(homedir(), ".cache", "hyperframes", "history");
|
|
791
1183
|
var MAX_WINDOW_IDLE_MS = 10 * 6e4;
|
|
792
1184
|
var UNDO_MODES = ["just-this", "back-to-before", "keep-later-edits"];
|
|
793
1185
|
var OUTSIDE = { kind: "outside", name: "Outside" };
|
|
@@ -800,7 +1192,7 @@ function isHistoryPath(path) {
|
|
|
800
1192
|
if (KEPT_HIDDEN_PATHS.has(path)) return true;
|
|
801
1193
|
return !path.split("/").some((segment) => segment.startsWith("."));
|
|
802
1194
|
}
|
|
803
|
-
var logFileIn = (home) =>
|
|
1195
|
+
var logFileIn = (home) => join7(home, "log.jsonl");
|
|
804
1196
|
function listOf(log) {
|
|
805
1197
|
const undone = undoneIds(log.entries);
|
|
806
1198
|
return log.entries.map((entry) => ({
|
|
@@ -840,20 +1232,20 @@ async function removeEmptyFolders(dir) {
|
|
|
840
1232
|
throw error;
|
|
841
1233
|
}
|
|
842
1234
|
for (const entry of entries)
|
|
843
|
-
if (entry.isDirectory()) await removeEmptyFolders(
|
|
1235
|
+
if (entry.isDirectory()) await removeEmptyFolders(join7(dir, entry.name));
|
|
844
1236
|
await rmdir(dir);
|
|
845
1237
|
}
|
|
846
1238
|
function fileLeftIn(dir, at, deleted) {
|
|
847
1239
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
848
1240
|
const path = `${at}/${entry.name}`;
|
|
849
|
-
const left = entry.isDirectory() ? fileLeftIn(
|
|
1241
|
+
const left = entry.isDirectory() ? fileLeftIn(join7(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
|
|
850
1242
|
if (left) return left;
|
|
851
1243
|
}
|
|
852
1244
|
return void 0;
|
|
853
1245
|
}
|
|
854
1246
|
function ignoresCase(dir) {
|
|
855
|
-
const self =
|
|
856
|
-
const other =
|
|
1247
|
+
const self = statSync4(join7(dir, ID_PATH), { throwIfNoEntry: false });
|
|
1248
|
+
const other = statSync4(join7(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
|
|
857
1249
|
return self !== void 0 && other?.ino === self.ino && other.dev === self.dev;
|
|
858
1250
|
}
|
|
859
1251
|
var blocks = (removed, added) => added.startsWith(`${removed}/`) || removed.startsWith(`${added}/`);
|
|
@@ -876,15 +1268,24 @@ function addChange(group, path, before, after) {
|
|
|
876
1268
|
if (from === after) group.changes.delete(path);
|
|
877
1269
|
else group.changes.set(path, { path, before: from, after });
|
|
878
1270
|
}
|
|
1271
|
+
var targetKey = (target) => JSON.stringify([target.file, target.id, target.hfId, target.selector, target.selectorIndex]);
|
|
1272
|
+
function keepTargetsOfItsFiles(entry) {
|
|
1273
|
+
const targets = entry.targets?.filter(
|
|
1274
|
+
(target) => entry.files.some((file) => file.path === target.file)
|
|
1275
|
+
);
|
|
1276
|
+
if (targets?.length) entry.targets = targets;
|
|
1277
|
+
else delete entry.targets;
|
|
1278
|
+
return entry;
|
|
1279
|
+
}
|
|
879
1280
|
var Engine = class {
|
|
880
1281
|
constructor(options, projectId, blobs) {
|
|
881
1282
|
this.options = options;
|
|
882
1283
|
this.projectId = projectId;
|
|
883
1284
|
this.blobs = blobs;
|
|
884
|
-
this.dir =
|
|
885
|
-
this.home =
|
|
1285
|
+
this.dir = resolve6(options.projectDir);
|
|
1286
|
+
this.home = join7(options.historyRoot, projectId);
|
|
886
1287
|
this.foldsCase = ignoresCase(this.dir);
|
|
887
|
-
const folder =
|
|
1288
|
+
const folder = statSync4(this.dir, { throwIfNoEntry: false });
|
|
888
1289
|
if (!folder || readId(this.dir) !== projectId || !isRecordedFolder(this.home, folder))
|
|
889
1290
|
throw this.replaced();
|
|
890
1291
|
this.folder = { dev: folder.dev, ino: folder.ino, birthtimeMs: folder.birthtimeMs };
|
|
@@ -928,13 +1329,28 @@ var Engine = class {
|
|
|
928
1329
|
queue(task) {
|
|
929
1330
|
if (this.closed)
|
|
930
1331
|
return Promise.reject(new HistoryClosedError("This project's history is closed."));
|
|
931
|
-
const run = this.tail.then(
|
|
1332
|
+
const run = this.tail.then(async () => {
|
|
1333
|
+
try {
|
|
1334
|
+
return await task();
|
|
1335
|
+
} finally {
|
|
1336
|
+
if (this.whereFolder() === "here") {
|
|
1337
|
+
await writeHistoryState(
|
|
1338
|
+
this.dir,
|
|
1339
|
+
this.projectId,
|
|
1340
|
+
this.home,
|
|
1341
|
+
this.log.entries,
|
|
1342
|
+
this.manifest(),
|
|
1343
|
+
manifestAt(this.log, this.log.entries.at(-1)?.id ?? START) ?? /* @__PURE__ */ new Map()
|
|
1344
|
+
).catch((error) => this.options.onError?.(error));
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
932
1348
|
this.tail = run.catch(() => void 0);
|
|
933
1349
|
return run;
|
|
934
1350
|
}
|
|
935
1351
|
/** Another project if the folder or its id changed, or the id is gone: a recreated folder may reuse the inode. */
|
|
936
1352
|
whereFolder() {
|
|
937
|
-
const now =
|
|
1353
|
+
const now = statSync4(this.dir, { throwIfNoEntry: false });
|
|
938
1354
|
if (!now) return "gone";
|
|
939
1355
|
const same = now.dev === this.folder.dev && sameFolder(now, this.folder);
|
|
940
1356
|
return same && readId(this.dir) === this.projectId ? "here" : "replaced";
|
|
@@ -947,7 +1363,7 @@ var Engine = class {
|
|
|
947
1363
|
persistLog(record) {
|
|
948
1364
|
if (record) saveRecord(this.logFile, this.log, record);
|
|
949
1365
|
else writeLog(this.logFile, this.log);
|
|
950
|
-
if (!
|
|
1366
|
+
if (!existsSync3(join7(this.home, "project.json")))
|
|
951
1367
|
recordProject(this.home, this.dir, this.folder);
|
|
952
1368
|
}
|
|
953
1369
|
replaced() {
|
|
@@ -986,7 +1402,7 @@ var Engine = class {
|
|
|
986
1402
|
if (!after || !affectsProjectSignature(realDir, absPath)) return;
|
|
987
1403
|
const replaced = hashOfVersion(fileContentVersion(bytes));
|
|
988
1404
|
if (!replaced || replaced === after) return;
|
|
989
|
-
const path =
|
|
1405
|
+
const path = relative4(realDir, absPath).split(sep4).join("/");
|
|
990
1406
|
if (!isHistoryPath(path)) return;
|
|
991
1407
|
this.overwritten.set(
|
|
992
1408
|
path,
|
|
@@ -998,7 +1414,7 @@ var Engine = class {
|
|
|
998
1414
|
});
|
|
999
1415
|
}
|
|
1000
1416
|
async storeBytes(bytes) {
|
|
1001
|
-
const staged =
|
|
1417
|
+
const staged = join7(this.home, `overwrote-${randomUUID7()}`);
|
|
1002
1418
|
await mkdir2(this.home, { recursive: true });
|
|
1003
1419
|
await writeFile(staged, bytes);
|
|
1004
1420
|
try {
|
|
@@ -1052,7 +1468,7 @@ var Engine = class {
|
|
|
1052
1468
|
this.saveAdopting();
|
|
1053
1469
|
}
|
|
1054
1470
|
saveAdopting() {
|
|
1055
|
-
const file =
|
|
1471
|
+
const file = join7(this.home, "adopting.json");
|
|
1056
1472
|
if (!this.adopting.size) return rmSync2(file, { force: true });
|
|
1057
1473
|
mkdirSync4(this.home, { recursive: true });
|
|
1058
1474
|
replaceFileAtomically(file, JSON.stringify([...this.adopting.keys()]), 420);
|
|
@@ -1088,7 +1504,7 @@ var Engine = class {
|
|
|
1088
1504
|
}
|
|
1089
1505
|
readStatCache() {
|
|
1090
1506
|
try {
|
|
1091
|
-
const files = JSON.parse(
|
|
1507
|
+
const files = JSON.parse(readFileSync9(join7(this.home, "stat.json"), "utf-8"));
|
|
1092
1508
|
return new Map(Object.entries(files));
|
|
1093
1509
|
} catch {
|
|
1094
1510
|
return /* @__PURE__ */ new Map();
|
|
@@ -1097,7 +1513,7 @@ var Engine = class {
|
|
|
1097
1513
|
readAdopting() {
|
|
1098
1514
|
try {
|
|
1099
1515
|
return new Set(
|
|
1100
|
-
JSON.parse(
|
|
1516
|
+
JSON.parse(readFileSync9(join7(this.home, "adopting.json"), "utf-8"))
|
|
1101
1517
|
);
|
|
1102
1518
|
} catch {
|
|
1103
1519
|
return /* @__PURE__ */ new Set();
|
|
@@ -1106,7 +1522,7 @@ var Engine = class {
|
|
|
1106
1522
|
saveStatCache() {
|
|
1107
1523
|
const files = JSON.stringify(Object.fromEntries(this.tracked));
|
|
1108
1524
|
mkdirSync4(this.home, { recursive: true });
|
|
1109
|
-
replaceFileAtomically(
|
|
1525
|
+
replaceFileAtomically(join7(this.home, "stat.json"), files, 420);
|
|
1110
1526
|
}
|
|
1111
1527
|
/**
|
|
1112
1528
|
* Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
|
|
@@ -1148,7 +1564,7 @@ var Engine = class {
|
|
|
1148
1564
|
/** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
|
|
1149
1565
|
async storeIfPresent(path) {
|
|
1150
1566
|
try {
|
|
1151
|
-
return await this.blobs.put(
|
|
1567
|
+
return await this.blobs.put(join7(this.dir, path));
|
|
1152
1568
|
} catch (error) {
|
|
1153
1569
|
if (error.code === "ENOENT") return null;
|
|
1154
1570
|
throw error;
|
|
@@ -1174,7 +1590,7 @@ var Engine = class {
|
|
|
1174
1590
|
addChange(window ?? this.outsideGroup(), path, before, after);
|
|
1175
1591
|
}
|
|
1176
1592
|
/** ponytail: another's write between Studio's write and its sweep folds into Studio's entry (per-write tokens). */
|
|
1177
|
-
async claimNow(who, label, paths, { coalesceKey, idleMs, overwrote = {} }) {
|
|
1593
|
+
async claimNow(who, label, paths, { coalesceKey, idleMs, overwrote = {}, targets = [] }) {
|
|
1178
1594
|
await this.sweep();
|
|
1179
1595
|
const taken = await this.takeClaimed(who, paths, overwrote);
|
|
1180
1596
|
if (!taken.length) return this.claimNothing(coalesceKey);
|
|
@@ -1183,10 +1599,19 @@ var Engine = class {
|
|
|
1183
1599
|
await this.commitPending();
|
|
1184
1600
|
const group = held ?? this.newGroup(who, label);
|
|
1185
1601
|
for (const change of taken) addChange(group, change.path, change.before, change.after);
|
|
1602
|
+
this.addTargets(group, targets);
|
|
1186
1603
|
if (coalesceKey) return this.holdClaim(group, coalesceKey, idleMs);
|
|
1187
1604
|
const entry = await this.commit(group);
|
|
1188
1605
|
return entry && { id: entry.id };
|
|
1189
1606
|
}
|
|
1607
|
+
addTargets(group, targets) {
|
|
1608
|
+
const unique = new Map((group.targets ?? []).map((target) => [targetKey(target), target]));
|
|
1609
|
+
for (const target of targets) {
|
|
1610
|
+
const normalized = { ...target, file: this.logPath(target.file) };
|
|
1611
|
+
if (group.changes.has(normalized.file)) unique.set(targetKey(normalized), normalized);
|
|
1612
|
+
}
|
|
1613
|
+
group.targets = [...unique.values()];
|
|
1614
|
+
}
|
|
1190
1615
|
/** A claim that took nothing still ends a held claim under another key. */
|
|
1191
1616
|
async claimNothing(coalesceKey) {
|
|
1192
1617
|
if (coalesceKey !== this.claimed?.key) await this.commitClaim();
|
|
@@ -1213,7 +1638,7 @@ var Engine = class {
|
|
|
1213
1638
|
return group && taken.every((change) => after(change.path, change.before) === change.before) && [...group.changes.values()].every(untouched) ? group : null;
|
|
1214
1639
|
}
|
|
1215
1640
|
logPath(path) {
|
|
1216
|
-
return
|
|
1641
|
+
return relative4(this.dir, resolve6(this.dir, path)).split(sep4).join("/");
|
|
1217
1642
|
}
|
|
1218
1643
|
/** Takes `paths`' uncommitted changes from others, cut at what `who` overwrote. */
|
|
1219
1644
|
async takeClaimed(who, paths, overwrote) {
|
|
@@ -1304,7 +1729,7 @@ var Engine = class {
|
|
|
1304
1729
|
return this.outside;
|
|
1305
1730
|
}
|
|
1306
1731
|
newGroup(who, label) {
|
|
1307
|
-
return { id:
|
|
1732
|
+
return { id: randomUUID7(), who, label, startedAt: this.now(), changes: /* @__PURE__ */ new Map() };
|
|
1308
1733
|
}
|
|
1309
1734
|
async commitOutside() {
|
|
1310
1735
|
if (this.outside?.changes.size) await this.commitClaim();
|
|
@@ -1319,7 +1744,7 @@ var Engine = class {
|
|
|
1319
1744
|
if (!group.changes.size && !undoesEmptied) return null;
|
|
1320
1745
|
const pending = this.pendingEntry(group);
|
|
1321
1746
|
const endedAt = Math.max(pending.endedAt, this.log.entries.at(-1)?.endedAt ?? 0);
|
|
1322
|
-
const entry = { ...pending, endedAt, ...extra };
|
|
1747
|
+
const entry = keepTargetsOfItsFiles({ ...pending, endedAt, ...extra });
|
|
1323
1748
|
this.log.entries.push(entry);
|
|
1324
1749
|
try {
|
|
1325
1750
|
this.persistLog({ type: "entry", entry });
|
|
@@ -1374,9 +1799,9 @@ var Engine = class {
|
|
|
1374
1799
|
}
|
|
1375
1800
|
/** A watcher saw a write: one sweep per burst takes it in (a deleted folder is reported by its name alone). */
|
|
1376
1801
|
noteChange(path) {
|
|
1377
|
-
const absolute =
|
|
1802
|
+
const absolute = resolve6(this.dir, path);
|
|
1378
1803
|
if (this.notedTimer || !affectsProjectSignature(this.dir, absolute)) return;
|
|
1379
|
-
if (!isHistoryPath(
|
|
1804
|
+
if (!isHistoryPath(relative4(this.dir, absolute).split(sep4).join("/"))) return;
|
|
1380
1805
|
this.notedTimer = setTimeout(() => {
|
|
1381
1806
|
this.notedTimer = null;
|
|
1382
1807
|
this.background(() => this.sweep());
|
|
@@ -1388,9 +1813,19 @@ var Engine = class {
|
|
|
1388
1813
|
if (!(error instanceof HistoryClosedError)) this.options.onError?.(error);
|
|
1389
1814
|
});
|
|
1390
1815
|
}
|
|
1391
|
-
beginWindow(who, label, idleMs) {
|
|
1816
|
+
beginWindow(who, label, idleMs, exclusive = false) {
|
|
1392
1817
|
return this.queue(async () => {
|
|
1393
1818
|
await this.settle();
|
|
1819
|
+
const at = this.now();
|
|
1820
|
+
if (exclusive) {
|
|
1821
|
+
const blocking = this.windows.find(
|
|
1822
|
+
(open) => !sameWho(open.who, who) && takesWrite(open, at)
|
|
1823
|
+
);
|
|
1824
|
+
if (blocking)
|
|
1825
|
+
throw new Error(`${blocking.who.name}'s turn is open; it has to end before yours begins`);
|
|
1826
|
+
for (const open of [...this.windows])
|
|
1827
|
+
if (!takesWrite(open, at) || sameWho(open.who, who)) await this.endWindow(open);
|
|
1828
|
+
}
|
|
1394
1829
|
const window = { ...this.newGroup(who, label), idleMs };
|
|
1395
1830
|
this.windows.push(window);
|
|
1396
1831
|
this.touch(window, this.now());
|
|
@@ -1442,7 +1877,7 @@ var Engine = class {
|
|
|
1442
1877
|
const parts = path.split("/");
|
|
1443
1878
|
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
1444
1879
|
const at = parts.slice(0, depth).join("/");
|
|
1445
|
-
const stat2 =
|
|
1880
|
+
const stat2 = lstatSync3(join7(this.dir, at), { throwIfNoEntry: false });
|
|
1446
1881
|
if (!stat2) return void 0;
|
|
1447
1882
|
if (at === path || !stat2.isDirectory()) return { at, stat: stat2 };
|
|
1448
1883
|
}
|
|
@@ -1454,8 +1889,8 @@ var Engine = class {
|
|
|
1454
1889
|
if (!standing) return void 0;
|
|
1455
1890
|
if (standing.stat.isSymbolicLink()) return standing.at;
|
|
1456
1891
|
if (standing.at === path && !standing.stat.isDirectory()) return void 0;
|
|
1457
|
-
const real = realpathSync.native(
|
|
1458
|
-
const at =
|
|
1892
|
+
const real = realpathSync.native(join7(this.dir, standing.at));
|
|
1893
|
+
const at = relative4(realpathSync.native(this.dir), real).split(sep4).join("/");
|
|
1459
1894
|
if (standing.at !== path) return deleted.has(at) ? void 0 : at;
|
|
1460
1895
|
return fileLeftIn(real, at, deleted);
|
|
1461
1896
|
}
|
|
@@ -1479,7 +1914,7 @@ var Engine = class {
|
|
|
1479
1914
|
/** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
|
|
1480
1915
|
async writeProjectFile(path, hash) {
|
|
1481
1916
|
this.assertWritable();
|
|
1482
|
-
const absPath =
|
|
1917
|
+
const absPath = join7(this.dir, path);
|
|
1483
1918
|
const { writeToken } = this;
|
|
1484
1919
|
if (writeToken) {
|
|
1485
1920
|
const version = hash === null ? DELETED_VERSION : hashVersion(hash);
|
|
@@ -1520,7 +1955,10 @@ var Engine = class {
|
|
|
1520
1955
|
);
|
|
1521
1956
|
return {
|
|
1522
1957
|
ok: true,
|
|
1523
|
-
entry: await this.writeAs(who, this.undoLabel(entry), target, {
|
|
1958
|
+
entry: await this.writeAs(who, this.undoLabel(entry), target, {
|
|
1959
|
+
undoes: id,
|
|
1960
|
+
...entry.targets && { targets: entry.targets }
|
|
1961
|
+
})
|
|
1524
1962
|
};
|
|
1525
1963
|
}
|
|
1526
1964
|
movedOn(entry) {
|
|
@@ -1544,7 +1982,15 @@ var Engine = class {
|
|
|
1544
1982
|
pendingEntry(group) {
|
|
1545
1983
|
const files = [...group.changes.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
1546
1984
|
const { id, who, label, startedAt, lastWriteAt } = group;
|
|
1547
|
-
return {
|
|
1985
|
+
return {
|
|
1986
|
+
id,
|
|
1987
|
+
who,
|
|
1988
|
+
label,
|
|
1989
|
+
startedAt,
|
|
1990
|
+
endedAt: lastWriteAt ?? this.now(),
|
|
1991
|
+
files,
|
|
1992
|
+
...group.targets?.length && { targets: group.targets }
|
|
1993
|
+
};
|
|
1548
1994
|
}
|
|
1549
1995
|
conflict(entry, changed) {
|
|
1550
1996
|
const paths = new Set(changed.map((file) => file.path));
|
|
@@ -1579,7 +2025,12 @@ var Engine = class {
|
|
|
1579
2025
|
api() {
|
|
1580
2026
|
return {
|
|
1581
2027
|
projectId: this.projectId,
|
|
1582
|
-
beginWindow: (who, label, options = {}) => this.beginWindow(
|
|
2028
|
+
beginWindow: (who, label, options = {}) => this.beginWindow(
|
|
2029
|
+
who,
|
|
2030
|
+
label,
|
|
2031
|
+
options.idleMs ?? this.options.maxGroupMs ?? 3e4,
|
|
2032
|
+
options.exclusive === true
|
|
2033
|
+
),
|
|
1583
2034
|
claim: async (who, label, paths, options = {}) => {
|
|
1584
2035
|
await Promise.all(paths.map((path) => this.adopting.get(this.logPath(path))));
|
|
1585
2036
|
return this.queue(() => this.claimNow(who, label, paths, options));
|
|
@@ -1606,18 +2057,18 @@ var Engine = class {
|
|
|
1606
2057
|
},
|
|
1607
2058
|
checkout: (entryId, side, emptyDir) => this.queue(async () => {
|
|
1608
2059
|
this.entry(entryId);
|
|
1609
|
-
if (!
|
|
1610
|
-
const dest =
|
|
2060
|
+
if (!existsSync3(this.dir)) throw new Error(`The project folder is gone: ${this.dir}`);
|
|
2061
|
+
const dest = resolve6(emptyDir);
|
|
1611
2062
|
if (isWithin(this.dir, dest))
|
|
1612
2063
|
throw new Error(`Checkout writes outside the project only: ${dest}`);
|
|
1613
2064
|
if ((await readdir3(dest).catch(missingIsEmpty)).length > 0)
|
|
1614
2065
|
throw new Error(`Checkout writes into an empty folder only: ${dest}`);
|
|
1615
2066
|
const files = manifestAround(this.log, entryId, side);
|
|
1616
2067
|
try {
|
|
1617
|
-
for (const [path, hash] of files) await this.blobs.writeTo(hash,
|
|
2068
|
+
for (const [path, hash] of files) await this.blobs.writeTo(hash, join7(dest, path));
|
|
1618
2069
|
} catch (error) {
|
|
1619
2070
|
for (const name of await readdir3(dest).catch(() => []))
|
|
1620
|
-
await rm3(
|
|
2071
|
+
await rm3(join7(dest, name), { recursive: true, force: true });
|
|
1621
2072
|
throw error;
|
|
1622
2073
|
}
|
|
1623
2074
|
}),
|
|
@@ -1640,6 +2091,7 @@ var Engine = class {
|
|
|
1640
2091
|
this.listeners.add(listener);
|
|
1641
2092
|
return () => this.listeners.delete(listener);
|
|
1642
2093
|
},
|
|
2094
|
+
checkpoint: () => this.queue(() => this.settle()),
|
|
1643
2095
|
flush: () => this.queue(() => this.settleAll()),
|
|
1644
2096
|
replacedAtPath: () => this.whereFolder() === "replaced",
|
|
1645
2097
|
close: () => this.closing ??= (async () => {
|
|
@@ -1654,10 +2106,10 @@ var Engine = class {
|
|
|
1654
2106
|
}
|
|
1655
2107
|
};
|
|
1656
2108
|
function isWithin(dir, path) {
|
|
1657
|
-
let probe =
|
|
1658
|
-
while (!
|
|
1659
|
-
const rel =
|
|
1660
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${
|
|
2109
|
+
let probe = resolve6(path);
|
|
2110
|
+
while (!existsSync3(probe) && dirname4(probe) !== probe) probe = dirname4(probe);
|
|
2111
|
+
const rel = relative4(realpathSync.native(dir), realpathSync.native(probe));
|
|
2112
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute5(rel);
|
|
1661
2113
|
}
|
|
1662
2114
|
function missingIsEmpty(error) {
|
|
1663
2115
|
if (error.code === "ENOENT") return [];
|
|
@@ -1670,16 +2122,17 @@ var HistoryClosedError = class extends Error {
|
|
|
1670
2122
|
}
|
|
1671
2123
|
};
|
|
1672
2124
|
async function openProjectHistory(options) {
|
|
1673
|
-
if (!
|
|
2125
|
+
if (!existsSync3(options.projectDir))
|
|
1674
2126
|
throw new HistoryClosedError(`${options.projectDir} is gone or now another project.`);
|
|
1675
2127
|
const projectId = projectHistoryId(options.projectDir, options.historyRoot);
|
|
1676
2128
|
const release = await takeHistoryOwnership(
|
|
1677
|
-
|
|
2129
|
+
join7(options.historyRoot, projectId),
|
|
1678
2130
|
options.ownerWaitMs ?? 5e3
|
|
1679
2131
|
);
|
|
1680
2132
|
try {
|
|
1681
|
-
const blobs = await openBlobStore(
|
|
1682
|
-
const
|
|
2133
|
+
const blobs = await openBlobStore(join7(options.historyRoot, projectId, "blobs"));
|
|
2134
|
+
const closedWindow = options.closedWindowAfterOwnership?.() ?? options.closedWindow;
|
|
2135
|
+
const engine = new Engine({ ...options, closedWindow }, projectId, blobs);
|
|
1683
2136
|
await engine.queue(async () => {
|
|
1684
2137
|
await engine.start();
|
|
1685
2138
|
engine.hearWrites();
|
|
@@ -1699,9 +2152,9 @@ async function openProjectHistory(options) {
|
|
|
1699
2152
|
}
|
|
1700
2153
|
}
|
|
1701
2154
|
function readProjectHistory(options) {
|
|
1702
|
-
const dir =
|
|
2155
|
+
const dir = resolve6(options.projectDir);
|
|
1703
2156
|
const id = readId(dir);
|
|
1704
|
-
const home = id && isRecordedFolder(
|
|
2157
|
+
const home = id && isRecordedFolder(join7(options.historyRoot, id), statSync4(dir)) ? join7(options.historyRoot, id) : null;
|
|
1705
2158
|
const read = home && readLog(logFileIn(home), () => {
|
|
1706
2159
|
});
|
|
1707
2160
|
const log = withoutHiddenPaths(read || { baseline: /* @__PURE__ */ new Map(), entries: [], pins: /* @__PURE__ */ new Set() });
|
|
@@ -1710,7 +2163,7 @@ function readProjectHistory(options) {
|
|
|
1710
2163
|
peek: (point) => peekOf(log, point),
|
|
1711
2164
|
readBlob: async (hash) => {
|
|
1712
2165
|
if (!home) throw new Error("That is not a history blob.");
|
|
1713
|
-
return readBlob(
|
|
2166
|
+
return readBlob(join7(home, "blobs"), hash);
|
|
1714
2167
|
}
|
|
1715
2168
|
};
|
|
1716
2169
|
}
|
|
@@ -1746,15 +2199,22 @@ export {
|
|
|
1746
2199
|
clearFileWriteReceipt,
|
|
1747
2200
|
identifyFileWrite,
|
|
1748
2201
|
consumeFileWriteReceipt,
|
|
2202
|
+
HistoryBusyError,
|
|
2203
|
+
registerRenderOutput,
|
|
1749
2204
|
STUDIO_SIGNATURE_MANIFEST_PATHS,
|
|
1750
2205
|
affectsProjectSignature,
|
|
1751
2206
|
resolveProjectSignature,
|
|
1752
2207
|
resolveProjectAndSignature,
|
|
1753
2208
|
createProjectSignature,
|
|
1754
2209
|
HistoryIdError,
|
|
1755
|
-
HistoryBusyError,
|
|
1756
2210
|
listProjectHistories,
|
|
1757
2211
|
pruneGoneProjectHistories,
|
|
2212
|
+
beginArtifactCapture,
|
|
2213
|
+
recordArtifact,
|
|
2214
|
+
recordArtifacts,
|
|
2215
|
+
forgetRecordedArtifacts,
|
|
2216
|
+
artifactDirectory,
|
|
2217
|
+
recordRenderSuccess,
|
|
1758
2218
|
START,
|
|
1759
2219
|
DEFAULT_HISTORY_ROOT,
|
|
1760
2220
|
MAX_WINDOW_IDLE_MS,
|