diff --git a/.gitignore b/.gitignore index f45e8b0..8842f02 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ build/ .venv/ .pytest_cache/ .mypy_cache/ + +# Config contains sensitive data (API keys) +mycli/config.json diff --git a/README.md b/README.md index 649eba0..757bc23 100644 --- a/README.md +++ b/README.md @@ -64,13 +64,31 @@ mycli self update # Update mycli mycli self uninstall # Uninstall mycli mycli self info # Show installation info -# Configuration (config.json) -mycli config show # Show full config -mycli config get paths # Get paths section -mycli config get paths.apaam # Get specific path -mycli config paths # List all paths with existence check -mycli config where # Show config file location -mycli config edit # Edit config in $EDITOR +# Configuration (config.json) - 增删改查 +mycli config show # Show full config +mycli config list # List top-level sections +mycli config get paths # Get paths section +mycli config get paths.apaam # Get specific path +mycli config get api_keys.deepseek # Get API key +mycli config set paths.new ~/Documents/new # Set simple value +mycli config set api_keys.newkey abc123 # Set new API key +mycli config set --json ssh_hosts.new '{"host":"user@host","port":22}' +mycli config delete paths.old # Delete a key +mycli config delete api_keys.old -y # Delete without confirmation +mycli config paths # List all paths with existence check +mycli config where # Show config file location +mycli config edit # Edit config in $EDITOR +mycli config init # Create config from template + +# Environment Variables (API Keys) +mycli env status # Show paths, API keys, Feishu status +mycli env export # Print all export statements +mycli env export deepseek # Export specific API key +mycli env export qwen +mycli env export feishu # Export all Feishu apps +mycli env export claude # Export specific Feishu app +eval "$(mycli env export)" # Load all in current shell +mycli env init # Print shell init snippet ``` ## Configuration @@ -80,6 +98,30 @@ All settings are stored in `mycli/config.json`: - `ssh_hosts`: SSH connection settings - `git_proxies`: Git proxy URLs - `openfoam`: OpenFOAM mount settings +- `api_keys`: API keys (deepseek, qwen) +- `feishu_apps`: Feishu app credentials + +### Security Note + +`config.json` contains sensitive information (API keys) and is **gitignored**. +The repository includes `config.json.example` as a template. + +### Setup + +1. Copy the example config: + ```bash + cp mycli/config.json.example mycli/config.json + ``` + +2. Edit with your actual API keys: + ```bash + mycli config edit + ``` + +3. Add to your `~/.zshrc` to auto-load: + ```bash + eval "$(mycli env export 2>/dev/null)" + ``` ## Uninstall diff --git a/mycli/commands/__pycache__/config_cmd.cpython-310.pyc b/mycli/commands/__pycache__/config_cmd.cpython-310.pyc index 0315113..44e291f 100644 Binary files a/mycli/commands/__pycache__/config_cmd.cpython-310.pyc and b/mycli/commands/__pycache__/config_cmd.cpython-310.pyc differ diff --git a/mycli/commands/__pycache__/env.cpython-310.pyc b/mycli/commands/__pycache__/env.cpython-310.pyc index 44ee59e..07069eb 100644 Binary files a/mycli/commands/__pycache__/env.cpython-310.pyc and b/mycli/commands/__pycache__/env.cpython-310.pyc differ diff --git a/mycli/commands/config_cmd.py b/mycli/commands/config_cmd.py index adb3a7f..199a4f1 100644 --- a/mycli/commands/config_cmd.py +++ b/mycli/commands/config_cmd.py @@ -2,10 +2,12 @@ import json import os +from pathlib import Path import click from mycli.config import config +from mycli.utils import handle_errors @click.group(name="config") @@ -23,7 +25,13 @@ def show(): @config_cmd.command("get") @click.argument("key") def get_value(key): - """Get a configuration value by key (dot notation).""" + """Get a configuration value by key (dot notation). + + Examples: + mycli config get paths.research + mycli config get ssh_hosts.tianhe.port + mycli config get api_keys.deepseek + """ value = config.get(key) if value is not None: if isinstance(value, dict): @@ -35,12 +43,123 @@ def get_value(key): raise click.Exit(1) +@config_cmd.command("set") +@click.argument("key") +@click.argument("value") +@click.option("--json", "is_json", is_flag=True, help="Parse value as JSON") +def set_value(key, value, is_json): + """Set a configuration value. + + Examples: + mycli config set paths.newpath ~/Documents/new + mycli config set api_keys.newkey abc123 + mycli config set --json ssh_hosts.newhost '{"host":"user@host","port":22}' + """ + # Load current config + config_path = config._config_path + with open(config_path) as f: + data = json.load(f) + + # Parse value + if is_json: + try: + parsed_value = json.loads(value) + except json.JSONDecodeError as e: + click.echo(f"Invalid JSON: {e}", err=True) + raise click.Exit(1) + else: + parsed_value = value + + # Navigate to nested key + keys = key.split(".") + target = data + for k in keys[:-1]: + if k not in target: + target[k] = {} + target = target[k] + + # Set value + target[keys[-1]] = parsed_value + + # Save + with open(config_path, "w") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write("\n") + + click.secho(f"Set {key} = {parsed_value}", fg="green") + + +@config_cmd.command("delete") +@click.argument("key") +@click.option("--yes", "-y", is_flag=True, help="Skip confirmation") +def delete_value(key, yes): + """Delete a configuration key. + + Examples: + mycli config delete api_keys.oldkey + mycli config delete paths.oldpath + """ + # Check if exists + if config.get(key) is None: + click.echo(f"Key not found: {key}", err=True) + raise click.Exit(1) + + if not yes: + click.confirm(f"Delete '{key}'?", abort=True) + + # Load current config + config_path = config._config_path + with open(config_path) as f: + data = json.load(f) + + # Navigate and delete + keys = key.split(".") + target = data + for k in keys[:-1]: + if k not in target: + click.echo(f"Key not found: {key}", err=True) + raise click.Exit(1) + target = target[k] + + if keys[-1] in target: + del target[keys[-1]] + else: + click.echo(f"Key not found: {key}", err=True) + raise click.Exit(1) + + # Save + with open(config_path, "w") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write("\n") + + click.secho(f"Deleted {key}", fg="green") + + +@config_cmd.command("list") +def list_keys(): + """List all top-level configuration keys.""" + config_path = config._config_path + with open(config_path) as f: + data = json.load(f) + + click.secho("Configuration sections:", fg="cyan") + for key in sorted(data.keys()): + value = data[key] + type_str = type(value).__name__ + if isinstance(value, dict): + count = len(value) + click.echo(f" {key:20} ({type_str}, {count} items)") + elif isinstance(value, list): + click.echo(f" {key:20} ({type_str}, {len(value)} items)") + else: + click.echo(f" {key:20} ({type_str})") + + @config_cmd.command("paths") def list_paths(): """List all configured paths.""" paths = config.get_all_paths() max_len = max(len(name) for name in paths.keys()) - from pathlib import Path for name, path in sorted(paths.items()): exists = "✓" if path.exists() else "✗" click.echo(f"{name:{max_len}} [{exists}] {path}") @@ -56,3 +175,24 @@ def edit(): def where(): """Show configuration file location.""" click.echo(config._config_path) + + +@config_cmd.command("init") +def init_config(): + """Create initial config from example template.""" + config_path = config._config_path + example_path = config_path.parent / "config.json.example" + + if config_path.exists(): + click.echo(f"Config already exists: {config_path}", err=True) + click.echo("Use 'mycli config edit' to modify it") + raise click.Exit(1) + + if not example_path.exists(): + click.echo(f"Example config not found: {example_path}", err=True) + raise click.Exit(1) + + import shutil + shutil.copy(example_path, config_path) + click.secho(f"Created config from template: {config_path}", fg="green") + click.echo("Edit it to add your API keys and customize paths") diff --git a/mycli/commands/env.py b/mycli/commands/env.py index 2d67add..79c3f00 100644 --- a/mycli/commands/env.py +++ b/mycli/commands/env.py @@ -83,3 +83,100 @@ def status(): click.secho(f" Mounted: {openfoam_path}", fg="green") else: click.secho(" Not mounted", fg="red") + + # API Keys status (without showing actual values) + click.secho("\n=== API Keys ===", fg="cyan") + api_keys = config.get("api_keys", {}) + for name in api_keys.keys(): + env_var = f"{name.upper()}_API_KEY" + is_set = "✓" if os.environ.get(env_var) else "✗" + click.echo(f" {name:12} [{is_set}] {env_var}") + + # Feishu Apps status + click.secho("\n=== Feishu Apps ===", fg="cyan") + feishu_apps = config.get("feishu_apps", {}) + for app_name in feishu_apps.keys(): + app_id_var = f"{app_name.upper()}_APP_ID" + is_set = "✓" if os.environ.get(app_id_var) else "✗" + click.echo(f" {app_name:12} [{is_set}] {app_id_var}") + + +@env_cmd.command("export") +@click.argument("name", required=False) +def export_env(name): + """Print export statements for environment variables. + + Examples: + mycli env export # Export all + mycli env export deepseek # Export specific API key + mycli env export feishu # Export all Feishu apps + eval "$(mycli env export)" # Load in current shell + """ + if name is None: + # Export all + _export_api_keys() + _export_feishu_apps() + elif name == "feishu": + _export_feishu_apps() + elif name in config.get("api_keys", {}): + _export_api_key(name) + elif name in config.get("feishu_apps", {}): + _export_feishu_app(name) + else: + click.echo(f"Unknown export target: {name}", err=True) + click.echo(f"Available: api_keys ({', '.join(config.get('api_keys', {}).keys())}), " + f"feishu_apps ({', '.join(config.get('feishu_apps', {}).keys())})", err=True) + raise click.Exit(1) + + +def _export_api_key(name: str): + """Export a single API key.""" + api_keys = config.get("api_keys", {}) + if name not in api_keys: + return + env_var = f"{name.upper()}_API_KEY" + value = api_keys[name] + click.echo(f"export {env_var}={value}") + + +def _export_api_keys(): + """Export all API keys.""" + api_keys = config.get("api_keys", {}) + for name in api_keys.keys(): + _export_api_key(name) + + +def _export_feishu_app(name: str): + """Export a single Feishu app.""" + feishu_apps = config.get("feishu_apps", {}) + if name not in feishu_apps: + return + app = feishu_apps[name] + prefix = name.upper() + click.echo(f"export {prefix}_APP_ID={app['app_id']}") + click.echo(f"export {prefix}_APP_SECRET={app['app_secret']}") + + +def _export_feishu_apps(): + """Export all Feishu apps.""" + feishu_apps = config.get("feishu_apps", {}) + for name in feishu_apps.keys(): + _export_feishu_app(name) + + +@env_cmd.command("init") +@click.option("--shell", "-s", "shell_type", type=click.Choice(["bash", "zsh", "fish"]), default="zsh") +def init_shell(shell_type): + """Print shell initialization snippet. + + Add this to your ~/.zshrc or ~/.bashrc: + eval "$(mycli env init)" + """ + snippet = f"""# MyCLI environment initialization +# Add this line to your ~/.{shell_type}rc: +# eval "$(mycli env init)" + +# Export API keys and app credentials +eval "$(mycli env export 2>/dev/null)" +""" + click.echo(snippet) diff --git a/mycli/config.json b/mycli/config.json index 3cdff66..14d40f7 100644 --- a/mycli/config.json +++ b/mycli/config.json @@ -30,5 +30,19 @@ "mount_point": "/Volumes/OpenFOAM", "build_path": "/Volumes/OpenFOAM/openfoam/build" }, - "cv": "~/Documents/Personal/resume_cv/cv_lai_maintaining_latex/cv_lai_maintaining" + "cv": "~/Documents/Personal/resume_cv/cv_lai_maintaining_latex/cv_lai_maintaining", + "api_keys": { + "deepseek": "sk-5c2f22ad351a485380e29385c710c2b2", + "qwen": "sk-dcbbcce8cce94ecebb4c19417757d32f" + }, + "feishu_apps": { + "claude": { + "app_id": "cli_a9452b9024781bd2", + "app_secret": "YIyWNMOUhuPxrs1emA6H7mOvbZajJHBw" + }, + "myagent": { + "app_id": "cli_a916d9fef8b89bc7", + "app_secret": "WxhZe8N8MuMuzCjYduDe8btitLeiFUFj" + } + } } diff --git a/mycli/config.json.example b/mycli/config.json.example new file mode 100644 index 0000000..f924c30 --- /dev/null +++ b/mycli/config.json.example @@ -0,0 +1,48 @@ +{ + "paths": { + "study": "~/Documents/myStudy/self_learning", + "research": "~/Documents/myResearch", + "apaam": "~/Documents/myResearch/myProjects/apaam", + "tianhe": "~/Documents/myWork/sysu/0_中山大学/hpc_tianhe", + "academia": "~/Library/Mobile Documents/iCloud~md~obsidian/Documents/myacademia", + "webpage": "~/Documents/myWork/sysu/4_成果归档/mywebpage", + "slides": "~/Documents/myWork/sysu/4_成果归档/myslides", + "myclaude": "~/Documents/myResearch/myProjects/apaam/repo/myclaude", + "myagent": "~/Documents/myResearch/myProjects/apaam/repo/myagent" + }, + "ssh_hosts": { + "tianhe": { + "host": "sysu_lchuangxy_1@172.16.31.31", + "port": 6666, + "key": "~/Documents/myWork/sysu/0_中山大学/hpc_tianhe/sysu_lchuangxy_1.id" + }, + "starlight": { + "host": "sysu_lchuang_1@proxy.nscc-gz.cn", + "port": 23 + } + }, + "git_proxies": { + "fastgithub": "http://127.0.0.1:38457", + "pandafan": "http://127.0.0.1:10080" + }, + "openfoam": { + "dmg_path": "~/Documents/myResearch/myProjects/apaam/repo/OpenFOAM.dmg", + "mount_point": "/Volumes/OpenFOAM", + "build_path": "/Volumes/OpenFOAM/openfoam/build" + }, + "cv": "~/Documents/Personal/resume_cv/cv_lai_maintaining_latex/cv_lai_maintaining", + "api_keys": { + "deepseek": "YOUR_DEEPSEEK_API_KEY_HERE", + "qwen": "YOUR_QWEN_API_KEY_HERE" + }, + "feishu_apps": { + "claude": { + "app_id": "YOUR_CLAUDE_APP_ID", + "app_secret": "YOUR_CLAUDE_APP_SECRET" + }, + "myagent": { + "app_id": "YOUR_MYAGENT_APP_ID", + "app_secret": "YOUR_MYAGENT_APP_SECRET" + } + } +}