diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 0b83039..5321353 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -9,7 +9,7 @@ on: jobs: build: runs-on: ubuntu-latest - timeout-minutes: 1 + timeout-minutes: 2 strategy: fail-fast: false matrix: diff --git a/pyproject.toml b/pyproject.toml index a47a5c9..1d6ce81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "claudesync" -version = "0.6.1" +version = "0.6.2" authors = [ {name = "Jahziah Wagner", email = "540380+jahwag@users.noreply.github.com"}, ] @@ -27,7 +27,7 @@ dependencies = [ "crontab>=1.0.1", "python-crontab>=3.2.0", "Brotli>=1.1.0", - "anthropic>=0.34.2", + "anthropic>=0.37.1", "cryptography>=3.4.7", ] keywords = [ diff --git a/requirements.txt b/requirements.txt index fc27d9f..96a49c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,5 +11,5 @@ claudesync>=0.5.4 crontab>=1.0.1 python-crontab>=3.2.0 Brotli>=1.1.0 -anthropic>=0.34.2 +anthropic>=0.37.1 cryptography>=3.4.7 \ No newline at end of file diff --git a/src/claudesync/cli/project.py b/src/claudesync/cli/project.py index 03cb3a2..c47b678 100644 --- a/src/claudesync/cli/project.py +++ b/src/claudesync/cli/project.py @@ -1,14 +1,17 @@ import click import os +import logging +from tqdm import tqdm from ..provider_factory import get_provider from ..utils import handle_errors, validate_and_get_provider from ..exceptions import ProviderError, ConfigurationError -from tqdm import tqdm from .file import file from .submodule import submodule from ..syncmanager import retry_on_403 +logger = logging.getLogger(__name__) + @click.group() def project(): @@ -69,43 +72,91 @@ def create(ctx, name, description, local_path, provider, organization): f"Project '{new_project['name']}' (uuid: {new_project['uuid']}) has been created successfully." ) + # Update configuration config.set("active_provider", provider, local=True) config.set("active_organization_id", organization, local=True) config.set("active_project_id", new_project["uuid"], local=True) config.set("active_project_name", new_project["name"], local=True) config.set("local_path", local_path, local=True) + # Create .claudesync directory and save config claudesync_dir = os.path.join(local_path, ".claudesync") os.makedirs(claudesync_dir, exist_ok=True) + config_file_path = os.path.join(claudesync_dir, "config.local.json") config._save_local_config() - click.echo( - f"\nProject setup complete. You can now start syncing files with this project. " - f"URL: https://claude.ai/project/{new_project['uuid']}" - ) + click.echo("\nProject created:") + click.echo(f" - Project location: {local_path}") + click.echo(f" - Project config location: {config_file_path}") + click.echo(f" - Remote URL: https://claude.ai/project/{new_project['uuid']}") except (ProviderError, ConfigurationError) as e: click.echo(f"Failed to create project: {str(e)}") @project.command() +@click.option( + "-a", + "--all", + "archive_all", + is_flag=True, + help="Archive all active projects", +) +@click.option( + "-y", + "--yes", + is_flag=True, + help="Skip confirmation prompt", +) @click.pass_obj @handle_errors -def archive(config): - """Archive an existing project.""" +def archive(config, archive_all, yes): + """Archive existing projects.""" provider = validate_and_get_provider(config) active_organization_id = config.get("active_organization_id") projects = provider.get_projects(active_organization_id, include_archived=False) + if not projects: click.echo("No active projects found.") return + + if archive_all: + if not yes: + click.echo("The following projects will be archived:") + for project in projects: + click.echo(f" - {project['name']} (ID: {project['id']})") + if not click.confirm("Are you sure you want to archive all projects?"): + click.echo("Operation cancelled.") + return + + with click.progressbar( + projects, + label="Archiving projects", + item_show_func=lambda p: p["name"] if p else "", + ) as bar: + for project in bar: + try: + provider.archive_project(active_organization_id, project["id"]) + except Exception as e: + click.echo( + f"\nFailed to archive project '{project['name']}': {str(e)}" + ) + + click.echo("\nArchive operation completed.") + return + + single_project_archival(projects, yes, provider, active_organization_id) + + +def single_project_archival(projects, yes, provider, active_organization_id): click.echo("Available projects to archive:") for idx, project in enumerate(projects, 1): click.echo(f" {idx}. {project['name']} (ID: {project['id']})") + selection = click.prompt("Enter the number of the project to archive", type=int) if 1 <= selection <= len(projects): selected_project = projects[selection - 1] - if click.confirm( + if yes or click.confirm( f"Are you sure you want to archive the project '{selected_project['name']}'? " f"Archived projects cannot be modified but can still be viewed." ): @@ -191,7 +242,13 @@ def set(ctx, show_all, provider): # Create .claudesync directory in the current working directory if it doesn't exist os.makedirs(".claudesync", exist_ok=True) - click.echo(f"Ensured .claudesync directory exists in {os.getcwd()}") + claudesync_dir = os.path.abspath(".claudesync") + config_file_path = os.path.join(claudesync_dir, "config.local.json") + config._save_local_config() + + click.echo("\nProject created:") + click.echo(f" - Project location: {os.getcwd()}") + click.echo(f" - Project config location: {config_file_path}") else: click.echo("Invalid selection. Please try again.") diff --git a/src/claudesync/configmanager/file_config_manager.py b/src/claudesync/configmanager/file_config_manager.py index 533f7b1..0eef5b9 100644 --- a/src/claudesync/configmanager/file_config_manager.py +++ b/src/claudesync/configmanager/file_config_manager.py @@ -67,7 +67,7 @@ def _find_local_config_dir(self, max_depth=100): current_dir = Path.cwd() root_dir = Path(current_dir.root) home_dir = Path.home() - depth = 0 # Initialize depth counter + depth = 0 while current_dir != root_dir: claudesync_dir = current_dir / ".claudesync" @@ -75,9 +75,8 @@ def _find_local_config_dir(self, max_depth=100): return current_dir current_dir = current_dir.parent - depth += 1 # Increment depth counter + depth += 1 - # Sanity check: stop if max_depth is reached if depth > max_depth: return None @@ -132,9 +131,12 @@ def set(self, key, value, local=False): local (bool): If True, sets the value in the local configuration. Otherwise, sets it in the global configuration. """ if local: - if not self.local_config_dir: - self.local_config_dir = Path.cwd() + # Update local_config_dir when setting local_path + if key == "local_path": + self.local_config_dir = Path(value) + # Create .claudesync directory in the specified path (self.local_config_dir / ".claudesync").mkdir(exist_ok=True) + self.local_config[key] = value self._save_local_config() else: @@ -159,6 +161,7 @@ def _save_local_config(self): local_config_file = ( self.local_config_dir / ".claudesync" / "config.local.json" ) + local_config_file.parent.mkdir(exist_ok=True) with open(local_config_file, "w") as f: json.dump(self.local_config, f, indent=2) diff --git a/src/claudesync/utils.py b/src/claudesync/utils.py index 9659f2b..c8a8eda 100644 --- a/src/claudesync/utils.py +++ b/src/claudesync/utils.py @@ -289,26 +289,26 @@ def validate_and_get_provider(config, require_org=True, require_project=False): or if require_project is True and no active project ID is set. ProviderError: If the session key has expired. """ - active_provider = config.get_active_provider() - if not active_provider: + if require_org and not config.get("active_organization_id"): raise ConfigurationError( - "No active provider set. Please select a provider for this project." + "No active organization set. Please select an organization (claudesync organization set)." ) - session_key, session_key_expiry = config.get_session_key(active_provider) - if not session_key: + if require_project and not config.get("active_project_id"): raise ConfigurationError( - f"No valid session key found for {active_provider}. Please log in again." + "No active project set. Please select or create a project (claudesync project set)." ) - if require_org and not config.get("active_organization_id"): + active_provider = config.get_active_provider() + if not active_provider: raise ConfigurationError( - "No active organization set. Please select an organization." + "No active provider set. Please select a provider for this project." ) - if require_project and not config.get("active_project_id"): + session_key, session_key_expiry = config.get_session_key(active_provider) + if not session_key: raise ConfigurationError( - "No active project set. Please select or create a project." + f"No valid session key found for {active_provider}. Please log in again." ) return get_provider(config, active_provider) diff --git a/tests/test_happy_path.py b/tests/test_happy_path.py index affa35c..4137a6a 100644 --- a/tests/test_happy_path.py +++ b/tests/test_happy_path.py @@ -26,7 +26,6 @@ def setUp(self): @patch("claudesync.utils.get_local_files") def test_happy_path(self, mock_get_local_files): - # Mock the API calls mock_get_local_files.return_value = {"test.txt": "content_hash"} @@ -58,19 +57,18 @@ def test_happy_path(self, mock_get_local_files): obj=self.config, ) self.assertEqual(result.exit_code, 0) + self.assertIn( - "Project 'New Project' (uuid: new_proj) has been created successfully." - "\n\nProject setup complete. You can now start syncing files with this project. " - "URL: https://claude.ai/project/new_proj\n", + "Project 'New Project' (uuid: new_proj) has been created successfully", result.output, ) + self.assertIn("Project created:", result.output) + self.assertIn("Project location:", result.output) + self.assertIn("Project config location:", result.output) + self.assertIn("Remote URL: https://claude.ai/project/new_proj", result.output) # Push project result = self.runner.invoke(cli, ["push"], obj=self.config) - print("Login output:", result.output) - print("Login exit code:", result.exit_code) - if result.exception: - print("Login exception:", result.exception) self.assertEqual(result.exit_code, 0) self.assertIn("Main project 'New Project' synced successfully", result.output)