diff --git a/.gitignore b/.gitignore index 9db96a8..627de84 100644 --- a/.gitignore +++ b/.gitignore @@ -129,7 +129,6 @@ dmypy.json .pyre/ # IDE -.vscode/ .idea/ *.swp *.swo diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..747ecd2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "ss: subscription list", + "type": "debugpy", + "request": "launch", + "module": "ss", + "args": ["subscription", "list"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "justMyCode": true + } + ] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3a69fca --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,76 @@ +# AGENTS.md + +## Overview + +This document describes the autonomous agents, subagents, and their roles within the scientific-surfing project. + +--- + +## Agent List + +### 1. SubscriptionManager Agent +- **Purpose:** Handles all subscription-related operations, including adding, refreshing, deleting, renaming, and activating subscriptions. +- **Key Methods:** + - `add_subscription` + - `refresh_subscription` + - `delete_subscription` + - `rename_subscription` + - `set_subscription_url` + - `activate_subscription` + - `list_subscriptions` +- **Notes:** Supports backup option on refresh. + +### 2. StorageManager Agent +- **Purpose:** Manages persistent storage for configuration and subscription data. +- **Key Methods:** + - `load_subscriptions` + - `save_subscriptions` + - `load_config` + - `get_storage_info` + +### 3. CoreConfigManager Agent +- **Purpose:** Handles core configuration management, including import/export, editing, resetting, and applying configuration. +- **Key Methods:** + - `import_config` + - `export_config` + - `edit_config` + - `reset_config` + - `show_config` + - `apply` + +### 4. CoreManager Agent +- **Purpose:** Manages core components and system service operations (install, uninstall, start, stop, restart, reload, status, update). +- **Key Methods:** + - `install_service` + - `uninstall_service` + - `start_service` + - `stop_service` + - `restart_service` + - `reload_service` + - `get_service_status` + - `update` + +### 5. HookManager Agent +- **Purpose:** Manages hook scripts for automation and customization. +- **Key Methods:** + - `init` + - `list_hooks` + - `edit` + - `rm` + +--- + +## Agent Interactions +- The CLI acts as the orchestrator, parsing user commands and delegating tasks to the appropriate agent. +- Agents interact via method calls and shared data models. + +--- + +## Extending Agents +- To add a new agent, implement a new manager class and update the CLI to route commands. +- Document new agents and their responsibilities in this file. + +--- + +## Last updated +May 26, 2026 diff --git a/README.md b/README.md index e9c607d..9929945 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ pip install -r requirements.txt # add a subscription python -m ss subscription add -# refresh a subscription -python -m ss subscription refresh +# refresh a subscription (with optional backup) +python -m ss subscription refresh [--backup] # delete a subscription python -m ss subscription rm @@ -69,23 +69,30 @@ python -m ss hook rm ### Core Configuration Management ```bash # import configuration from file -python -m ss core config import +python -m ss core config import [--config-file ] # export configuration to file -python -m ss core config export +python -m ss core config export [--config-file ] # edit configuration with system editor -python -m ss core config edit +python -m ss core config edit [--config-file ] # reset configuration to default values -python -m ss core config reset +python -m ss core config reset [--config-file ] # show current configuration -python -m ss core config show +python -m ss core config show [--config-file ] -# apply active subscription to generate final config -python -m ss core config apply +# apply subscription to generate final config (with advanced options) +python -m ss core config apply \ + [--config-file ] \ + [--output-file ] \ + [--subscription ] ``` +**Options:** +- `--config-file `: Use a custom config file instead of the default. +- `--output-file `: Specify the output path for the generated config file. +- `--subscription `: Use a specific subscription (not just the active one) for config generation. ### Core Management ```bash diff --git a/ss/cli.py b/ss/cli.py index 454aafb..4383009 100644 --- a/ss/cli.py +++ b/ss/cli.py @@ -30,6 +30,7 @@ def create_parser() -> argparse.ArgumentParser: # Refresh subscription command refresh_parser = subscription_subparsers.add_parser('refresh', help='Refresh a subscription') refresh_parser.add_argument('name', help='Name of the subscription to refresh') + refresh_parser.add_argument('--backup', action='store_true', help='Backup the existing file before refreshing') # Delete subscription command (rm) delete_parser = subscription_subparsers.add_parser('rm', help='Delete a subscription') @@ -67,6 +68,9 @@ def create_parser() -> argparse.ArgumentParser: # Config commands config_parser = subparsers.add_parser('config', help='Manage core configuration') config_subparsers = config_parser.add_subparsers(dest='config_command', help='Configuration operations') + config_parser.add_argument('--config-file', type=str, default=None, help='Path to the user config YAML file (default: core-config.yaml in config dir)') + config_parser.add_argument('--output-file', type=str, default=None, help='Path to the generated config file (default: generated_config.yaml in config dir)') + config_parser.add_argument('--subscription', type=str, default=None, help='Name of the subscription to use for config generation (default: active subscription)') # Import config import_parser = config_subparsers.add_parser('import', help='Import configuration from file') @@ -151,7 +155,8 @@ def handle_subscription_command(args, subscription_manager: SubscriptionManager, if args.subcommand == 'add': subscription_manager.add_subscription(args.name, args.url) elif args.subcommand == 'refresh': - subscription_manager.refresh_subscription(args.name) + subscription_manager.refresh_subscription(args.name, backup=getattr(args, 'backup', False)) + core_manager.reload_service() elif args.subcommand == 'rm': subscription_manager.delete_subscription(args.name) elif args.subcommand == 'rename': @@ -274,7 +279,13 @@ def main() -> None: storage = StorageManager() subscription_manager = SubscriptionManager(storage) - core_config_manager = CoreConfigManager(subscription_manager) + # Pass config_file argument to CoreConfigManager if provided + core_config_manager = CoreConfigManager( + subscription_manager, + config_file=getattr(args, 'config_file', None), + output_file=getattr(args, 'output_file', None), + subscription_name=getattr(args, 'subscription', None) + ) core_manager = CoreManager(core_config_manager) hook_manager = HookManager(storage) diff --git a/ss/corecfg_manager.py b/ss/corecfg_manager.py index d8fdfa0..e18c53b 100644 --- a/ss/corecfg_manager.py +++ b/ss/corecfg_manager.py @@ -19,10 +19,18 @@ from .utils import open_file_in_editor class CoreConfigManager: """Manages user configuration with import, export, and edit operations.""" - def __init__(self, subscription_manager: SubscriptionManager): + def __init__(self, subscription_manager: SubscriptionManager, config_file: str = None, output_file: str = None, subscription_name: str = None): self.subscription_manager = subscription_manager self.storage = subscription_manager.storage - self.config_file = self.storage.config_dir / "core-config.yaml" + if config_file: + self.config_file = Path(config_file).expanduser().resolve() + else: + self.config_file = self.storage.config_dir / "core-config.yaml" + if output_file: + self.generated_path = Path(output_file).expanduser().resolve() + else: + self.generated_path = self.storage.config_dir / "generated_config.yaml" + self.subscription_name = subscription_name self.default_config_path = Path(__file__).parent / "templates" / "default-core-config.yaml" def _ensure_config_exists(self) -> bool: @@ -258,17 +266,22 @@ class CoreConfigManager: # Load current configuration config = self.load_config() - # Load subscriptions to get active subscription - active_subscription = self.subscription_manager.subscriptions_data.get_active_subscription() + # Load subscriptions to get the selected or active subscription + if self.subscription_name: + subscription = self.subscription_manager.subscriptions_data.subscriptions.get(self.subscription_name) + if not subscription: + print(f"❌ Subscription '{self.subscription_name}' not found") + return False + else: + subscription = self.subscription_manager.subscriptions_data.get_active_subscription() + if not subscription: + print("❌ No active subscription found") + return False - if not active_subscription: - print("❌ No active subscription found") - return False - - file_path = active_subscription.get_file_path(self.storage.config_dir) + file_path = subscription.get_file_path(self.storage.config_dir) if not file_path or not Path(file_path).exists(): - print("❌ Active subscription file not found. Please refresh the subscription first.") + print(f"❌ Subscription file not found for '{subscription.name}'. Please refresh the subscription first.") return False try: @@ -322,16 +335,14 @@ class CoreConfigManager: } # Generate final config file - generated_path = self.storage.config_dir / "generated_config.yaml" - - with open(generated_path, 'w', encoding='utf-8') as f: + with open(self.generated_path, 'w', encoding='utf-8') as f: yaml.dump(final_config, f, default_flow_style=False, allow_unicode=True) - print(f"✅ Generated final configuration: {generated_path}") - print(f" Active subscription: {active_subscription.name}") + print(f"✅ Generated final configuration: {self.generated_path}") + print(f" Active subscription: {subscription.name}") # Execute hooks after successful config generation - self._execute_hooks(generated_path) + self._execute_hooks(self.generated_path) return True diff --git a/ss/subscription_manager.py b/ss/subscription_manager.py index 6330bcc..0027bd0 100644 --- a/ss/subscription_manager.py +++ b/ss/subscription_manager.py @@ -243,8 +243,12 @@ class SubscriptionManager: return content - def refresh_subscription(self, name: str) -> None: - """Refresh a subscription by downloading from URL.""" + def refresh_subscription(self, name: str, backup: bool = False) -> None: + """Refresh a subscription by downloading from URL. + Args: + name (str): The subscription name. + backup (bool, optional): Whether to backup the existing file. Defaults to False. + """ if name not in self.subscriptions_data.subscriptions: print(f"❌ Subscription '{name}' not found") return @@ -262,15 +266,15 @@ class SubscriptionManager: # File path without timestamp file_path = self.subscriptions_dir / f"{name}.yml" - # Handle existing file by renaming with creation date - if file_path.exists(): + # Handle existing file by renaming with creation date if backup is enabled + if backup and file_path.exists(): # Get creation time of existing file stat = file_path.stat() try: # Try st_birthtime first (macOS/Unix) creation_time = datetime.fromtimestamp(stat.st_birthtime) except AttributeError: - # Fallback to st_ctime (Windows) + # Fallback to st_ctime (Windows/Linux) creation_time = datetime.fromtimestamp(stat.st_ctime) backup_name = f"{name}_{creation_time.strftime('%Y%m%d_%H%M%S')}.yml"