11import { shellQuote } from "../sandbox-support" ;
22import type { ProjectLocalRuntime } from "./project-sandbox-package-runtime" ;
33
4- const LOCAL_SOURCE_SYNC_SCRIPT = `
5- import hashlib
6- import json
7- import os
8- import shutil
9- import sys
10- import time
11-
12- IGNORED_DIRS = {".expo", ".next", ".turbo", "build", "coverage", "dist", "node_modules", "out"}
13- LOCAL_ONLY_NAMES = IGNORED_DIRS | {"next-env.d.ts"}
14- LOCK_STALE_SECONDS = 15 * 60
15-
16- def remove_path(path):
17- if os.path.isdir(path) and not os.path.islink(path):
18- shutil.rmtree(path)
19- elif os.path.lexists(path):
20- os.unlink(path)
21-
22- def sync_link(source, target):
23- link = os.readlink(source)
24- if os.path.islink(target) and os.readlink(target) == link:
25- return
26- remove_path(target)
27- os.symlink(link, target)
28-
29- def sync_file(source, target, force):
30- source_stat = os.stat(source, follow_symlinks=False)
31- try:
32- target_stat = os.stat(target, follow_symlinks=False)
33- except FileNotFoundError:
34- target_stat = None
35- changed = (
36- force
37- or target_stat is None
38- or not os.path.isfile(target)
39- or source_stat.st_size != target_stat.st_size
40- or source_stat.st_mtime_ns > target_stat.st_mtime_ns
41- )
42- if changed:
43- remove_path(target)
44- shutil.copyfile(source, target, follow_symlinks=False)
45-
46- def sync_directory(source, target, force=False):
47- if os.path.lexists(target) and not os.path.isdir(target):
48- remove_path(target)
49- os.makedirs(target, exist_ok=True)
50- source_names = set()
51- for entry in os.scandir(source):
52- if entry.name in IGNORED_DIRS:
53- continue
54- source_names.add(entry.name)
55- destination = os.path.join(target, entry.name)
56- try:
57- if entry.is_symlink():
58- sync_link(entry.path, destination)
59- elif entry.is_dir(follow_symlinks=False):
60- sync_directory(entry.path, destination, force)
61- elif entry.is_file(follow_symlinks=False):
62- sync_file(entry.path, destination, force)
63- except FileNotFoundError:
64- continue
65- for entry in os.scandir(target):
66- if entry.name not in source_names and entry.name not in LOCAL_ONLY_NAMES:
67- remove_path(entry.path)
68-
69- def digest(path):
70- value = hashlib.sha256()
71- with open(path, "rb") as source:
72- for chunk in iter(lambda: source.read(1024 * 1024), b""):
73- value.update(chunk)
74- return value.hexdigest()
75-
76- def tree_state(root):
77- state = {}
78- if not os.path.isdir(root):
79- return state
80- for current, dir_names, file_names in os.walk(root, topdown=True, followlinks=False):
81- dir_names[:] = sorted(name for name in dir_names if name not in IGNORED_DIRS)
82- relative_root = os.path.relpath(current, root)
83- for name in dir_names:
84- path = os.path.join(current, name)
85- relative = os.path.normpath(os.path.join(relative_root, name))
86- if os.path.islink(path):
87- state[relative] = ["link", os.readlink(path)]
88- else:
89- state[relative] = ["dir"]
90- for name in sorted(file_names):
91- if name in LOCAL_ONLY_NAMES:
92- continue
93- path = os.path.join(current, name)
94- relative = os.path.normpath(os.path.join(relative_root, name))
95- if os.path.islink(path):
96- state[relative] = ["link", os.readlink(path)]
97- else:
98- state[relative] = ["file", digest(path)]
99- return state
100-
101- def package_lock_active(lock_path):
102- try:
103- age = time.time() - os.path.getmtime(lock_path)
104- except FileNotFoundError:
105- return False
106- if age <= LOCK_STALE_SECONDS:
107- return True
108- try:
109- os.unlink(lock_path)
110- except FileNotFoundError:
111- pass
112- return False
113-
114- def wait_for_preview(lock_path, busy_path):
115- deadline = time.time() + 30
116- while os.path.exists(busy_path):
117- if time.time() >= deadline:
118- raise TimeoutError("local source mirror did not quiesce")
119- time.sleep(0.05)
120-
121- def preview_sync(source, target, mode, lock_path, busy_path):
122- force = mode == "preview-once"
123- while True:
124- if package_lock_active(lock_path):
125- if force:
126- time.sleep(0.05)
127- continue
128- time.sleep(0.25)
129- continue
130- os.makedirs(os.path.dirname(busy_path), exist_ok=True)
131- with open(busy_path, "w", encoding="utf-8"):
132- pass
133- try:
134- if package_lock_active(lock_path):
135- continue
136- sync_directory(source, target, force)
137- finally:
138- try:
139- os.unlink(busy_path)
140- except FileNotFoundError:
141- pass
142- if force:
143- return
144- time.sleep(0.75)
145-
146- def prepare(source, target, lock_path, busy_path, baseline_path):
147- os.makedirs(os.path.dirname(lock_path), exist_ok=True)
148- with open(lock_path, "w", encoding="utf-8") as lock:
149- lock.write(str(time.time()))
150- try:
151- wait_for_preview(lock_path, busy_path)
152- sync_directory(source, target, True)
153- with open(baseline_path, "w", encoding="utf-8") as baseline:
154- json.dump(tree_state(target), baseline, sort_keys=True, separators=(",", ":"))
155- except Exception:
156- cleanup(lock_path, baseline_path)
157- raise
158-
159- def copy_state(source_root, target_root, relative, state):
160- source = os.path.join(source_root, relative)
161- target = os.path.join(target_root, relative)
162- kind = state[0]
163- if kind == "dir":
164- if os.path.lexists(target) and not os.path.isdir(target):
165- remove_path(target)
166- os.makedirs(target, exist_ok=True)
167- return
168- os.makedirs(os.path.dirname(target), exist_ok=True)
169- if kind == "link":
170- sync_link(source, target)
171- return
172- if os.path.isdir(target) or os.path.islink(target):
173- remove_path(target)
174- shutil.copyfile(source, target, follow_symlinks=False)
175-
176- def cleanup(lock_path, baseline_path):
177- for path in (baseline_path, lock_path):
178- try:
179- os.unlink(path)
180- except FileNotFoundError:
181- pass
182-
183- def commit(source, target, lock_path, baseline_path):
184- try:
185- with open(baseline_path, encoding="utf-8") as baseline_file:
186- baseline = json.load(baseline_file)
187- local = tree_state(target)
188- durable = tree_state(source)
189- changed = sorted(path for path in set(baseline) | set(local) if baseline.get(path) != local.get(path))
190- conflicts = [
191- path
192- for path in changed
193- if durable.get(path) != baseline.get(path) and durable.get(path) != local.get(path)
194- ]
195- if conflicts:
196- raise RuntimeError("durable project changed during package command: " + ", ".join(conflicts[:8]))
197- for relative in sorted((path for path in changed if path not in local), key=lambda value: value.count(os.sep), reverse=True):
198- remove_path(os.path.join(source, relative))
199- for relative in sorted((path for path in changed if path in local), key=lambda value: value.count(os.sep)):
200- copy_state(target, source, relative, local[relative])
201- finally:
202- cleanup(lock_path, baseline_path)
203-
204- mode, source, target, lock_path, busy_path, baseline_path = sys.argv[1:7]
205- if mode in {"preview-once", "preview-loop"}:
206- preview_sync(source, target, mode, lock_path, busy_path)
207- elif mode == "package-prepare":
208- prepare(source, target, lock_path, busy_path, baseline_path)
209- elif mode == "package-commit":
210- commit(source, target, lock_path, baseline_path)
211- elif mode == "package-abort":
212- cleanup(lock_path, baseline_path)
213- else:
214- raise ValueError("unknown local source synchronization mode")
215- ` ;
4+ const LOCAL_SOURCE_SYNC_BINARY = "/opt/cheatcode/project-source-sync.py" ;
2165
2176function runtimeStatePaths ( runtime : ProjectLocalRuntime ) : {
2187 baselinePath : string ;
@@ -232,9 +21,7 @@ export function localSourceSyncCommand(
23221) : string {
23322 const { baselinePath, busyPath, lockPath } = runtimeStatePaths ( runtime ) ;
23423 return [
235- "python3" ,
236- "-c" ,
237- LOCAL_SOURCE_SYNC_SCRIPT ,
24+ LOCAL_SOURCE_SYNC_BINARY ,
23825 mode ,
23926 runtime . workspaceDir ,
24027 runtime . localSourceDir ,
0 commit comments