feat: enhance subscription management with backup option and update CLI commands
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -129,7 +129,6 @@ dmypy.json
|
||||
.pyre/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
76
AGENTS.md
Normal file
76
AGENTS.md
Normal file
@ -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
|
||||
25
README.md
25
README.md
@ -29,8 +29,8 @@ pip install -r requirements.txt
|
||||
# add a subscription
|
||||
python -m ss subscription add <name> <clash-rss-subscription-url>
|
||||
|
||||
# refresh a subscription
|
||||
python -m ss subscription refresh <name>
|
||||
# refresh a subscription (with optional backup)
|
||||
python -m ss subscription refresh <name> [--backup]
|
||||
|
||||
# delete a subscription
|
||||
python -m ss subscription rm <name>
|
||||
@ -69,23 +69,30 @@ python -m ss hook rm <script-name>
|
||||
### Core Configuration Management
|
||||
```bash
|
||||
# import configuration from file
|
||||
python -m ss core config import <file-path>
|
||||
python -m ss core config import <file-path> [--config-file <config.yaml>]
|
||||
|
||||
# export configuration to file
|
||||
python -m ss core config export <file-path>
|
||||
python -m ss core config export <file-path> [--config-file <config.yaml>]
|
||||
|
||||
# edit configuration with system editor
|
||||
python -m ss core config edit
|
||||
python -m ss core config edit [--config-file <config.yaml>]
|
||||
|
||||
# reset configuration to default values
|
||||
python -m ss core config reset
|
||||
python -m ss core config reset [--config-file <config.yaml>]
|
||||
|
||||
# show current configuration
|
||||
python -m ss core config show
|
||||
python -m ss core config show [--config-file <config.yaml>]
|
||||
|
||||
# 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 <config.yaml>] \
|
||||
[--output-file <output.yaml>] \
|
||||
[--subscription <subscription-name>]
|
||||
```
|
||||
**Options:**
|
||||
- `--config-file <config.yaml>`: Use a custom config file instead of the default.
|
||||
- `--output-file <output.yaml>`: Specify the output path for the generated config file.
|
||||
- `--subscription <subscription-name>`: Use a specific subscription (not just the active one) for config generation.
|
||||
|
||||
### Core Management
|
||||
```bash
|
||||
|
||||
15
ss/cli.py
15
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)
|
||||
|
||||
|
||||
@ -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
|
||||
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()
|
||||
|
||||
if not 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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user