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/
|
.pyre/
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.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
|
# add a subscription
|
||||||
python -m ss subscription add <name> <clash-rss-subscription-url>
|
python -m ss subscription add <name> <clash-rss-subscription-url>
|
||||||
|
|
||||||
# refresh a subscription
|
# refresh a subscription (with optional backup)
|
||||||
python -m ss subscription refresh <name>
|
python -m ss subscription refresh <name> [--backup]
|
||||||
|
|
||||||
# delete a subscription
|
# delete a subscription
|
||||||
python -m ss subscription rm <name>
|
python -m ss subscription rm <name>
|
||||||
@ -69,23 +69,30 @@ python -m ss hook rm <script-name>
|
|||||||
### Core Configuration Management
|
### Core Configuration Management
|
||||||
```bash
|
```bash
|
||||||
# import configuration from file
|
# 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
|
# 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
|
# 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
|
# reset configuration to default values
|
||||||
python -m ss core config reset
|
python -m ss core config reset [--config-file <config.yaml>]
|
||||||
|
|
||||||
# show current configuration
|
# 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
|
# apply subscription to generate final config (with advanced options)
|
||||||
python -m ss core config apply
|
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
|
### Core Management
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
15
ss/cli.py
15
ss/cli.py
@ -30,6 +30,7 @@ def create_parser() -> argparse.ArgumentParser:
|
|||||||
# Refresh subscription command
|
# Refresh subscription command
|
||||||
refresh_parser = subscription_subparsers.add_parser('refresh', help='Refresh a subscription')
|
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('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 subscription command (rm)
|
||||||
delete_parser = subscription_subparsers.add_parser('rm', help='Delete a subscription')
|
delete_parser = subscription_subparsers.add_parser('rm', help='Delete a subscription')
|
||||||
@ -67,6 +68,9 @@ def create_parser() -> argparse.ArgumentParser:
|
|||||||
# Config commands
|
# Config commands
|
||||||
config_parser = subparsers.add_parser('config', help='Manage core configuration')
|
config_parser = subparsers.add_parser('config', help='Manage core configuration')
|
||||||
config_subparsers = config_parser.add_subparsers(dest='config_command', help='Configuration operations')
|
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 config
|
||||||
import_parser = config_subparsers.add_parser('import', help='Import configuration from file')
|
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':
|
if args.subcommand == 'add':
|
||||||
subscription_manager.add_subscription(args.name, args.url)
|
subscription_manager.add_subscription(args.name, args.url)
|
||||||
elif args.subcommand == 'refresh':
|
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':
|
elif args.subcommand == 'rm':
|
||||||
subscription_manager.delete_subscription(args.name)
|
subscription_manager.delete_subscription(args.name)
|
||||||
elif args.subcommand == 'rename':
|
elif args.subcommand == 'rename':
|
||||||
@ -274,7 +279,13 @@ def main() -> None:
|
|||||||
|
|
||||||
storage = StorageManager()
|
storage = StorageManager()
|
||||||
subscription_manager = SubscriptionManager(storage)
|
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)
|
core_manager = CoreManager(core_config_manager)
|
||||||
hook_manager = HookManager(storage)
|
hook_manager = HookManager(storage)
|
||||||
|
|
||||||
|
|||||||
@ -19,10 +19,18 @@ from .utils import open_file_in_editor
|
|||||||
class CoreConfigManager:
|
class CoreConfigManager:
|
||||||
"""Manages user configuration with import, export, and edit operations."""
|
"""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.subscription_manager = subscription_manager
|
||||||
self.storage = subscription_manager.storage
|
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"
|
self.default_config_path = Path(__file__).parent / "templates" / "default-core-config.yaml"
|
||||||
|
|
||||||
def _ensure_config_exists(self) -> bool:
|
def _ensure_config_exists(self) -> bool:
|
||||||
@ -258,17 +266,22 @@ class CoreConfigManager:
|
|||||||
# Load current configuration
|
# Load current configuration
|
||||||
config = self.load_config()
|
config = self.load_config()
|
||||||
|
|
||||||
# Load subscriptions to get active subscription
|
# Load subscriptions to get the selected or active subscription
|
||||||
active_subscription = self.subscription_manager.subscriptions_data.get_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:
|
file_path = subscription.get_file_path(self.storage.config_dir)
|
||||||
print("❌ No active subscription found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
file_path = active_subscription.get_file_path(self.storage.config_dir)
|
|
||||||
|
|
||||||
if not file_path or not Path(file_path).exists():
|
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
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -322,16 +335,14 @@ class CoreConfigManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Generate final config file
|
# Generate final config file
|
||||||
generated_path = self.storage.config_dir / "generated_config.yaml"
|
with open(self.generated_path, 'w', encoding='utf-8') as f:
|
||||||
|
|
||||||
with open(generated_path, 'w', encoding='utf-8') as f:
|
|
||||||
yaml.dump(final_config, f, default_flow_style=False, allow_unicode=True)
|
yaml.dump(final_config, f, default_flow_style=False, allow_unicode=True)
|
||||||
|
|
||||||
print(f"✅ Generated final configuration: {generated_path}")
|
print(f"✅ Generated final configuration: {self.generated_path}")
|
||||||
print(f" Active subscription: {active_subscription.name}")
|
print(f" Active subscription: {subscription.name}")
|
||||||
|
|
||||||
# Execute hooks after successful config generation
|
# Execute hooks after successful config generation
|
||||||
self._execute_hooks(generated_path)
|
self._execute_hooks(self.generated_path)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@ -243,8 +243,12 @@ class SubscriptionManager:
|
|||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def refresh_subscription(self, name: str) -> None:
|
def refresh_subscription(self, name: str, backup: bool = False) -> None:
|
||||||
"""Refresh a subscription by downloading from URL."""
|
"""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:
|
if name not in self.subscriptions_data.subscriptions:
|
||||||
print(f"❌ Subscription '{name}' not found")
|
print(f"❌ Subscription '{name}' not found")
|
||||||
return
|
return
|
||||||
@ -262,15 +266,15 @@ class SubscriptionManager:
|
|||||||
# File path without timestamp
|
# File path without timestamp
|
||||||
file_path = self.subscriptions_dir / f"{name}.yml"
|
file_path = self.subscriptions_dir / f"{name}.yml"
|
||||||
|
|
||||||
# Handle existing file by renaming with creation date
|
# Handle existing file by renaming with creation date if backup is enabled
|
||||||
if file_path.exists():
|
if backup and file_path.exists():
|
||||||
# Get creation time of existing file
|
# Get creation time of existing file
|
||||||
stat = file_path.stat()
|
stat = file_path.stat()
|
||||||
try:
|
try:
|
||||||
# Try st_birthtime first (macOS/Unix)
|
# Try st_birthtime first (macOS/Unix)
|
||||||
creation_time = datetime.fromtimestamp(stat.st_birthtime)
|
creation_time = datetime.fromtimestamp(stat.st_birthtime)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Fallback to st_ctime (Windows)
|
# Fallback to st_ctime (Windows/Linux)
|
||||||
creation_time = datetime.fromtimestamp(stat.st_ctime)
|
creation_time = datetime.fromtimestamp(stat.st_ctime)
|
||||||
|
|
||||||
backup_name = f"{name}_{creation_time.strftime('%Y%m%d_%H%M%S')}.yml"
|
backup_name = f"{name}_{creation_time.strftime('%Y%m%d_%H%M%S')}.yml"
|
||||||
|
|||||||
Reference in New Issue
Block a user