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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user