claudesync 0.3.1__tar.gz → 0.3.2__tar.gz
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.
- {claudesync-0.3.1/src/claudesync.egg-info → claudesync-0.3.2}/PKG-INFO +1 -1
- {claudesync-0.3.1 → claudesync-0.3.2}/pyproject.toml +1 -1
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync/cli/api.py +0 -2
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync/cli/sync.py +6 -4
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync/config_manager.py +2 -2
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync/utils.py +61 -47
- {claudesync-0.3.1 → claudesync-0.3.2/src/claudesync.egg-info}/PKG-INFO +1 -1
- {claudesync-0.3.1 → claudesync-0.3.2}/tests/test_utils.py +4 -2
- {claudesync-0.3.1 → claudesync-0.3.2}/LICENSE +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/README.md +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/setup.cfg +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/setup.py +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync/__init__.py +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync/cli/__init__.py +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync/cli/main.py +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync/cli/organization.py +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync/cli/project.py +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync/exceptions.py +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync/provider_factory.py +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync/providers/__init__.py +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync/providers/claude_ai.py +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync.egg-info/SOURCES.txt +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync.egg-info/dependency_links.txt +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync.egg-info/entry_points.txt +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync.egg-info/requires.txt +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/src/claudesync.egg-info/top_level.txt +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/tests/test_config_manager.py +0 -0
- {claudesync-0.3.1 → claudesync-0.3.2}/tests/test_provider_factory.py +0 -0
|
@@ -55,7 +55,7 @@ def sync(config):
|
|
|
55
55
|
local_files = get_local_files(local_path)
|
|
56
56
|
|
|
57
57
|
# Track remote files to delete
|
|
58
|
-
remote_files_to_delete = set(rf[
|
|
58
|
+
remote_files_to_delete = set(rf["file_name"] for rf in remote_files)
|
|
59
59
|
|
|
60
60
|
for local_file, local_checksum in local_files.items():
|
|
61
61
|
remote_file = next(
|
|
@@ -69,7 +69,7 @@ def sync(config):
|
|
|
69
69
|
active_organization_id, active_project_id, remote_file["uuid"]
|
|
70
70
|
)
|
|
71
71
|
with open(
|
|
72
|
-
|
|
72
|
+
os.path.join(local_path, local_file), "r", encoding="utf-8"
|
|
73
73
|
) as file:
|
|
74
74
|
content = file.read()
|
|
75
75
|
provider.upload_file(
|
|
@@ -80,7 +80,7 @@ def sync(config):
|
|
|
80
80
|
else:
|
|
81
81
|
click.echo(f"Uploading new file {local_file} to remote...")
|
|
82
82
|
with open(
|
|
83
|
-
|
|
83
|
+
os.path.join(local_path, local_file), "r", encoding="utf-8"
|
|
84
84
|
) as file:
|
|
85
85
|
content = file.read()
|
|
86
86
|
provider.upload_file(
|
|
@@ -91,7 +91,9 @@ def sync(config):
|
|
|
91
91
|
# Delete remote files that no longer exist locally
|
|
92
92
|
for file_to_delete in remote_files_to_delete:
|
|
93
93
|
click.echo(f"Deleting {file_to_delete} from remote...")
|
|
94
|
-
remote_file = next(
|
|
94
|
+
remote_file = next(
|
|
95
|
+
rf for rf in remote_files if rf["file_name"] == file_to_delete
|
|
96
|
+
)
|
|
95
97
|
provider.delete_file(
|
|
96
98
|
active_organization_id, active_project_id, remote_file["uuid"]
|
|
97
99
|
)
|
|
@@ -14,7 +14,7 @@ class ConfigManager:
|
|
|
14
14
|
return {
|
|
15
15
|
"log_level": "INFO",
|
|
16
16
|
"upload_delay": 0.5,
|
|
17
|
-
"max_file_size": 32 * 1024 # Default 32 KB
|
|
17
|
+
"max_file_size": 32 * 1024, # Default 32 KB
|
|
18
18
|
}
|
|
19
19
|
with open(self.config_file, "r") as f:
|
|
20
20
|
config = json.load(f)
|
|
@@ -35,4 +35,4 @@ class ConfigManager:
|
|
|
35
35
|
|
|
36
36
|
def set(self, key, value):
|
|
37
37
|
self.config[key] = value
|
|
38
|
-
self._save_config()
|
|
38
|
+
self._save_config()
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import hashlib
|
|
3
|
-
import mimetypes
|
|
4
3
|
from functools import wraps
|
|
5
4
|
|
|
6
5
|
import click
|
|
@@ -15,64 +14,79 @@ logger = logging.getLogger(__name__)
|
|
|
15
14
|
|
|
16
15
|
config_manager = ConfigManager()
|
|
17
16
|
|
|
17
|
+
|
|
18
18
|
def calculate_checksum(content):
|
|
19
19
|
normalized_content = content.replace("\r\n", "\n").replace("\r", "\n").strip()
|
|
20
20
|
return hashlib.md5(normalized_content.encode("utf-8")).hexdigest()
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def load_gitignore(base_path):
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
def should_ignore(gitignore, local_path):
|
|
43
|
-
# Check file type
|
|
44
|
-
mime_type, _ = mimetypes.guess_type(local_path)
|
|
45
|
-
if mime_type and not mime_type.startswith("text/"):
|
|
46
|
-
return True
|
|
47
|
-
# Check if .git dir
|
|
48
|
-
if ".git" in local_path.split(os.sep):
|
|
49
|
-
return True
|
|
50
|
-
# Check if temporary editor file
|
|
51
|
-
if local_path.endswith("~"):
|
|
52
|
-
return True
|
|
53
|
-
# Check if too big
|
|
54
|
-
max_file_size = config_manager.get("max_file_size", 32 * 1024) # Default to 32 KB if not set
|
|
55
|
-
if os.path.getsize(local_path) > max_file_size:
|
|
56
|
-
return True
|
|
57
|
-
# Check .gitignore
|
|
58
|
-
return gitignore.match_file(local_path) if gitignore else False
|
|
24
|
+
gitignore_path = os.path.join(base_path, ".gitignore")
|
|
25
|
+
if os.path.exists(gitignore_path):
|
|
26
|
+
with open(gitignore_path, "r") as f:
|
|
27
|
+
return pathspec.PathSpec.from_lines("gitwildmatch", f)
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_text_file(file_path, sample_size=8192):
|
|
32
|
+
try:
|
|
33
|
+
with open(file_path, "rb") as file:
|
|
34
|
+
return b"\x00" not in file.read(sample_size)
|
|
35
|
+
except IOError:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def calculate_checksum(content):
|
|
40
|
+
return hashlib.md5(content.encode("utf-8")).hexdigest()
|
|
59
41
|
|
|
60
42
|
|
|
61
43
|
def get_local_files(local_path):
|
|
62
44
|
gitignore = load_gitignore(local_path)
|
|
63
45
|
files = {}
|
|
64
|
-
|
|
46
|
+
|
|
47
|
+
# List of directories to exclude
|
|
48
|
+
exclude_dirs = {".git", ".svn", ".hg", ".bzr", "_darcs", "CVS"}
|
|
49
|
+
|
|
50
|
+
for root, dirs, filenames in os.walk(local_path):
|
|
51
|
+
# Remove excluded directories
|
|
52
|
+
dirs[:] = [d for d in dirs if d not in exclude_dirs]
|
|
53
|
+
|
|
54
|
+
rel_root = os.path.relpath(root, local_path)
|
|
55
|
+
if rel_root == ".":
|
|
56
|
+
rel_root = ""
|
|
57
|
+
|
|
65
58
|
for filename in filenames:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
rel_path = os.path.join(rel_root, filename)
|
|
60
|
+
full_path = os.path.join(root, filename)
|
|
61
|
+
|
|
62
|
+
# Skip files larger than 200KB
|
|
63
|
+
max_file_size = config_manager.get("max_file_size", 32 * 1024)
|
|
64
|
+
if os.path.getsize(full_path) > max_file_size:
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# Skip temporary editor files
|
|
68
|
+
if filename.endswith("~"):
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
# Use gitignore rules if available
|
|
72
|
+
if gitignore and gitignore.match_file(rel_path):
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
# Check if it's a text file
|
|
76
|
+
if not is_text_file(full_path):
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
with open(full_path, "r", encoding="utf-8") as file:
|
|
81
|
+
content = file.read()
|
|
82
|
+
files[rel_path] = calculate_checksum(content)
|
|
83
|
+
except UnicodeDecodeError:
|
|
84
|
+
# If UTF-8 decoding fails, it's likely not a text file we can handle
|
|
85
|
+
logger.debug(f"Unable to read {full_path} as UTF-8 text. Skipping.")
|
|
86
|
+
continue
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.error(f"Error reading file {full_path}: {str(e)}")
|
|
89
|
+
|
|
76
90
|
return files
|
|
77
91
|
|
|
78
92
|
|
|
@@ -40,8 +40,10 @@ class TestUtils(unittest.TestCase):
|
|
|
40
40
|
f.write("Content of file3")
|
|
41
41
|
|
|
42
42
|
# Create a .git file
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
for vcs in {".git", ".svn", ".hg", ".bzr", "_darcs", "CVS"}:
|
|
44
|
+
os.makedirs(os.path.join(tmpdir, vcs), exist_ok=True)
|
|
45
|
+
with open(os.path.join(tmpdir, vcs, ".gitignore"), "w") as f:
|
|
46
|
+
f.write("*.log\n")
|
|
45
47
|
|
|
46
48
|
# Create a test~ file
|
|
47
49
|
with open(os.path.join(tmpdir, "test~"), "w") as f:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|