feat: add API keys management and config CRUD operations
- Add api_keys and feishu_apps to config.json - Add env export/init commands for shell integration - Add config CRUD: list, set, delete, init - Add config.json.example template - Add config.json to .gitignore for security - Update README with new commands Usage: mycli env export # Export all env vars mycli config set key value # Set config value mycli config delete key # Delete config key
This commit is contained in:
@@ -11,3 +11,6 @@ build/
|
|||||||
.venv/
|
.venv/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|
||||||
|
# Config contains sensitive data (API keys)
|
||||||
|
mycli/config.json
|
||||||
|
|||||||
@@ -64,13 +64,31 @@ mycli self update # Update mycli
|
|||||||
mycli self uninstall # Uninstall mycli
|
mycli self uninstall # Uninstall mycli
|
||||||
mycli self info # Show installation info
|
mycli self info # Show installation info
|
||||||
|
|
||||||
# Configuration (config.json)
|
# Configuration (config.json) - 增删改查
|
||||||
mycli config show # Show full config
|
mycli config show # Show full config
|
||||||
mycli config get paths # Get paths section
|
mycli config list # List top-level sections
|
||||||
mycli config get paths.apaam # Get specific path
|
mycli config get paths # Get paths section
|
||||||
mycli config paths # List all paths with existence check
|
mycli config get paths.apaam # Get specific path
|
||||||
mycli config where # Show config file location
|
mycli config get api_keys.deepseek # Get API key
|
||||||
mycli config edit # Edit config in $EDITOR
|
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
|
## Configuration
|
||||||
@@ -80,6 +98,30 @@ All settings are stored in `mycli/config.json`:
|
|||||||
- `ssh_hosts`: SSH connection settings
|
- `ssh_hosts`: SSH connection settings
|
||||||
- `git_proxies`: Git proxy URLs
|
- `git_proxies`: Git proxy URLs
|
||||||
- `openfoam`: OpenFOAM mount settings
|
- `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
|
## Uninstall
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from mycli.config import config
|
from mycli.config import config
|
||||||
|
from mycli.utils import handle_errors
|
||||||
|
|
||||||
|
|
||||||
@click.group(name="config")
|
@click.group(name="config")
|
||||||
@@ -23,7 +25,13 @@ def show():
|
|||||||
@config_cmd.command("get")
|
@config_cmd.command("get")
|
||||||
@click.argument("key")
|
@click.argument("key")
|
||||||
def get_value(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)
|
value = config.get(key)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
@@ -35,12 +43,123 @@ def get_value(key):
|
|||||||
raise click.Exit(1)
|
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")
|
@config_cmd.command("paths")
|
||||||
def list_paths():
|
def list_paths():
|
||||||
"""List all configured paths."""
|
"""List all configured paths."""
|
||||||
paths = config.get_all_paths()
|
paths = config.get_all_paths()
|
||||||
max_len = max(len(name) for name in paths.keys())
|
max_len = max(len(name) for name in paths.keys())
|
||||||
from pathlib import Path
|
|
||||||
for name, path in sorted(paths.items()):
|
for name, path in sorted(paths.items()):
|
||||||
exists = "✓" if path.exists() else "✗"
|
exists = "✓" if path.exists() else "✗"
|
||||||
click.echo(f"{name:{max_len}} [{exists}] {path}")
|
click.echo(f"{name:{max_len}} [{exists}] {path}")
|
||||||
@@ -56,3 +175,24 @@ def edit():
|
|||||||
def where():
|
def where():
|
||||||
"""Show configuration file location."""
|
"""Show configuration file location."""
|
||||||
click.echo(config._config_path)
|
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")
|
||||||
|
|||||||
@@ -83,3 +83,100 @@ def status():
|
|||||||
click.secho(f" Mounted: {openfoam_path}", fg="green")
|
click.secho(f" Mounted: {openfoam_path}", fg="green")
|
||||||
else:
|
else:
|
||||||
click.secho(" Not mounted", fg="red")
|
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)
|
||||||
|
|||||||
+15
-1
@@ -30,5 +30,19 @@
|
|||||||
"mount_point": "/Volumes/OpenFOAM",
|
"mount_point": "/Volumes/OpenFOAM",
|
||||||
"build_path": "/Volumes/OpenFOAM/openfoam/build"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user