feat: initial agos-cli standalone repo
Extracted from the agos monorepo. Contains the full AGOS CLI: - cli/agos.py — main entry point with login, logout, whoami, ask, db commands - cli/command_surface.py — API-backed commands (agent, task, workflow, plugin, policy, integration) - cli/manual.py — rich interactive manual and help system Uses clean package imports (no sys.path hacks). Packaged as agos-cli with `agos` console script entry point. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.egg
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.eggs/
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
.env
|
||||
.env.local
|
||||
*.log
|
||||
.DS_Store
|
||||
78
README.md
Normal file
78
README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# agos-cli
|
||||
|
||||
The official command-line interface for [AGOS](https://agos.armco.dev) — an agent orchestrator and runtime platform.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
This registers the `agos` console script globally.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Authenticate with Armco IAM
|
||||
agos login
|
||||
|
||||
# Check who you are
|
||||
agos whoami
|
||||
|
||||
# Ask the platform a question
|
||||
agos ask "What agents are currently running?"
|
||||
|
||||
# List agents
|
||||
agos agent list
|
||||
|
||||
# Run a workflow
|
||||
agos workflow run wf_123 --agent-id agent_123
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `agos login` | Authenticate via Armco IAM (PKCE browser flow) |
|
||||
| `agos logout` | Clear the local session |
|
||||
| `agos whoami` | Show current identity and tenant |
|
||||
| `agos ask <prompt>` | Send a natural-language prompt to AGOS chat |
|
||||
| `agos agent <subcommand>` | Manage agents (list, get, create, run, stop, delete) |
|
||||
| `agos task <subcommand>` | Manage tasks (list, get, create, cancel, delete) |
|
||||
| `agos workflow <subcommand>` | Manage workflows (list, get, create, run, status) |
|
||||
| `agos plugin <subcommand>` | Manage plugins (list, search, install, enable, disable, uninstall) |
|
||||
| `agos policy <subcommand>` | Manage policies (list, create, delete) |
|
||||
| `agos integration <subcommand>` | Manage integrations (catalog, providers, instances, create, test) |
|
||||
| `agos db <subcommand>` | Local database ops (bootstrap, seed, status) |
|
||||
| `agos system <subcommand>` | System health and logs |
|
||||
| `agos help [topic]` | Focused help for any command |
|
||||
| `agos man [topic]` | Full interactive manual |
|
||||
|
||||
## Configuration
|
||||
|
||||
The CLI reads API URL from the following env vars in order:
|
||||
|
||||
- `AGOS_CLI_API_URL`
|
||||
- `AGOS_API_URL`
|
||||
- `VITE_API_URL`
|
||||
- `AGOS_BASE_URL`
|
||||
- Default: `http://localhost:2000`
|
||||
|
||||
Auth tokens are cached at `~/.agos/auth.json` (mode `0600`).
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Clone and install in editable mode
|
||||
git clone <repo>
|
||||
cd agos-cli
|
||||
pip install -e .
|
||||
|
||||
# Run directly
|
||||
agos --help
|
||||
```
|
||||
|
||||
## Related Repos
|
||||
|
||||
- [agos](https://gitea.armco.dev/agos/agos) — backend runtime
|
||||
- [agos-client](https://gitea.armco.dev/agos/agos-client) — React frontend
|
||||
0
cli/__init__.py
Normal file
0
cli/__init__.py
Normal file
772
cli/agos.py
Normal file
772
cli/agos.py
Normal file
@@ -0,0 +1,772 @@
|
||||
#!/usr/bin/env python3
|
||||
"""AGOS CLI - Main entry point."""
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
import sys
|
||||
import os
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import secrets
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
import webbrowser
|
||||
from datetime import datetime, timezone
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlencode, urlparse, parse_qs
|
||||
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from cli.command_surface import install_api_commands
|
||||
from cli.manual import install_manual_commands
|
||||
|
||||
# `agos` is installed as a first-class console script (see pyproject.toml
|
||||
# project.scripts). A non-editable `pip install` copies cli/agos.py into
|
||||
# site-packages, so __file__-relative paths never reach the actual AGOS
|
||||
# checkout. Resolve .env files from the canonical checkout locations that
|
||||
# install.sh itself creates (AGOS_DIR / AGOS_CLIENT_DIR), plus the
|
||||
# in-repo dev path (this file's own repo, for `python -m cli.agos` usage)
|
||||
# and cwd (for running the CLI from inside either checkout).
|
||||
_PACKAGE_REPO_DIR = Path(__file__).resolve().parent.parent
|
||||
_AGOS_DIR = Path(os.environ.get('AGOS_DIR', Path.home() / '.agos'))
|
||||
_AGOS_CLIENT_DIR = Path(os.environ.get(
|
||||
'AGOS_CLIENT_DIR', _AGOS_DIR.parent / 'agos-client',
|
||||
))
|
||||
_CWD = Path.cwd()
|
||||
|
||||
_ENV_CANDIDATE_DIRS = [
|
||||
_AGOS_DIR,
|
||||
_AGOS_CLIENT_DIR,
|
||||
_PACKAGE_REPO_DIR,
|
||||
_PACKAGE_REPO_DIR.parent / 'agos-client',
|
||||
_CWD,
|
||||
_CWD.parent / 'agos-client',
|
||||
]
|
||||
|
||||
for _env_dir in _ENV_CANDIDATE_DIRS:
|
||||
load_dotenv(_env_dir / '.env', override=False)
|
||||
for _env_dir in _ENV_CANDIDATE_DIRS:
|
||||
load_dotenv(_env_dir / '.env.local', override=True)
|
||||
|
||||
console = Console()
|
||||
_AUTH_DIR = Path.home() / '.agos'
|
||||
_AUTH_FILE = _AUTH_DIR / 'auth.json'
|
||||
|
||||
|
||||
def _utc_now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def _emit_log(
|
||||
level,
|
||||
operation,
|
||||
correlation_id,
|
||||
entity_id='agos_cli',
|
||||
metadata=None,
|
||||
component='agos.cli.auth',
|
||||
):
|
||||
payload = {
|
||||
'timestamp': _utc_now_iso(),
|
||||
'level': level,
|
||||
'component': component,
|
||||
'operation': operation,
|
||||
'entity_id': entity_id,
|
||||
'correlation_id': correlation_id,
|
||||
'metadata': metadata or {},
|
||||
}
|
||||
click.echo(json.dumps(payload), err=True)
|
||||
|
||||
|
||||
def _first_env(*keys):
|
||||
for key in keys:
|
||||
value = os.getenv(key, '').strip()
|
||||
if value:
|
||||
return value
|
||||
return ''
|
||||
|
||||
|
||||
def _parse_scopes(raw_scopes):
|
||||
normalized = (raw_scopes or '').replace(',', ' ')
|
||||
scopes = [scope.strip() for scope in normalized.split() if scope.strip()]
|
||||
return scopes or ['openid', 'profile', 'email', 'offline_access']
|
||||
|
||||
|
||||
def _pkce_verifier():
|
||||
return secrets.token_urlsafe(64)
|
||||
|
||||
|
||||
def _pkce_challenge(verifier):
|
||||
digest = hashlib.sha256(verifier.encode('utf-8')).digest()
|
||||
return base64.urlsafe_b64encode(digest).rstrip(b'=').decode('utf-8')
|
||||
|
||||
|
||||
def _fetch_discovery(issuer, correlation_id):
|
||||
issuer_base = issuer.rstrip('/')
|
||||
discovery_url = f'{issuer_base}/.well-known/openid-configuration'
|
||||
_emit_log('INFO', 'fetch_discovery_start', correlation_id, metadata={'issuer': issuer_base})
|
||||
response = requests.get(discovery_url, timeout=15)
|
||||
response.raise_for_status()
|
||||
discovery = response.json()
|
||||
_emit_log(
|
||||
'INFO',
|
||||
'fetch_discovery_ok',
|
||||
correlation_id,
|
||||
metadata={
|
||||
'issuer': issuer_base,
|
||||
'authorization_endpoint': discovery.get('authorization_endpoint'),
|
||||
'token_endpoint': discovery.get('token_endpoint'),
|
||||
},
|
||||
)
|
||||
return discovery
|
||||
|
||||
|
||||
def _write_auth_state(payload):
|
||||
_AUTH_DIR.mkdir(parents=True, exist_ok=True)
|
||||
_AUTH_FILE.write_text(json.dumps(payload, indent=2), encoding='utf-8')
|
||||
os.chmod(_AUTH_FILE, 0o600)
|
||||
|
||||
|
||||
def _start_callback_server(host, port, expected_state, timeout_seconds, correlation_id):
|
||||
callback_event = threading.Event()
|
||||
callback_payload = {}
|
||||
|
||||
class CallbackHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
parsed = urlparse(self.path)
|
||||
params = parse_qs(parsed.query)
|
||||
callback_payload['path'] = parsed.path
|
||||
callback_payload['code'] = params.get('code', [None])[0]
|
||||
callback_payload['state'] = params.get('state', [None])[0]
|
||||
callback_payload['error'] = params.get('error', [None])[0]
|
||||
callback_payload['error_description'] = params.get('error_description', [None])[0]
|
||||
callback_payload['received_at'] = _utc_now_iso()
|
||||
callback_payload['valid_state'] = callback_payload.get('state') == expected_state
|
||||
body = (
|
||||
'<html><body><h2>Agos CLI login complete.</h2>'
|
||||
'<p>You can return to the terminal.</p></body></html>'
|
||||
if callback_payload['valid_state'] and not callback_payload.get('error')
|
||||
else '<html><body><h2>Agos CLI login failed.</h2>'
|
||||
'<p>Return to the terminal for details.</p></body></html>'
|
||||
)
|
||||
status = 200 if callback_payload['valid_state'] else 400
|
||||
self.send_response(status)
|
||||
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
||||
self.end_headers()
|
||||
self.wfile.write(body.encode('utf-8'))
|
||||
callback_event.set()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
return
|
||||
|
||||
server = HTTPServer((host, port), CallbackHandler)
|
||||
server.timeout = 0.5
|
||||
|
||||
def serve():
|
||||
deadline = time.time() + timeout_seconds
|
||||
while time.time() < deadline and not callback_event.is_set():
|
||||
server.handle_request()
|
||||
server.server_close()
|
||||
|
||||
thread = threading.Thread(target=serve, daemon=True)
|
||||
thread.start()
|
||||
_emit_log(
|
||||
'INFO',
|
||||
'callback_server_started',
|
||||
correlation_id,
|
||||
metadata={'host': host, 'port': port, 'timeout_seconds': timeout_seconds},
|
||||
)
|
||||
return callback_event, callback_payload, thread
|
||||
|
||||
|
||||
def _exchange_code_for_tokens(discovery, client_id, code, redirect_uri, code_verifier, correlation_id):
|
||||
payload = {
|
||||
'grant_type': 'authorization_code',
|
||||
'client_id': client_id,
|
||||
'code': code,
|
||||
'redirect_uri': redirect_uri,
|
||||
'code_verifier': code_verifier,
|
||||
}
|
||||
_emit_log(
|
||||
'INFO',
|
||||
'token_exchange_start',
|
||||
correlation_id,
|
||||
metadata={'token_endpoint': discovery.get('token_endpoint'), 'client_id': client_id},
|
||||
)
|
||||
response = requests.post(
|
||||
discovery['token_endpoint'],
|
||||
data=payload,
|
||||
headers={'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
timeout=15,
|
||||
)
|
||||
if not response.ok:
|
||||
try:
|
||||
error_payload = response.json()
|
||||
except Exception:
|
||||
error_payload = {'error': response.text[:200]}
|
||||
raise click.ClickException(
|
||||
error_payload.get('error_description')
|
||||
or error_payload.get('error')
|
||||
or f'Token exchange failed with status {response.status_code}'
|
||||
)
|
||||
tokens = response.json()
|
||||
_emit_log(
|
||||
'INFO',
|
||||
'token_exchange_ok',
|
||||
correlation_id,
|
||||
metadata={'expires_in': tokens.get('expires_in'), 'has_refresh_token': bool(tokens.get('refresh_token'))},
|
||||
)
|
||||
return tokens
|
||||
|
||||
|
||||
def _fetch_userinfo(discovery, access_token, correlation_id):
|
||||
userinfo_endpoint = discovery.get('userinfo_endpoint')
|
||||
if not userinfo_endpoint:
|
||||
return None
|
||||
_emit_log('INFO', 'userinfo_fetch_start', correlation_id, metadata={'userinfo_endpoint': userinfo_endpoint})
|
||||
response = requests.get(
|
||||
userinfo_endpoint,
|
||||
headers={'Authorization': f'Bearer {access_token}'},
|
||||
timeout=15,
|
||||
)
|
||||
if not response.ok:
|
||||
_emit_log('WARN', 'userinfo_fetch_failed', correlation_id, metadata={'status_code': response.status_code})
|
||||
return None
|
||||
userinfo = response.json()
|
||||
_emit_log(
|
||||
'INFO',
|
||||
'userinfo_fetch_ok',
|
||||
correlation_id,
|
||||
metadata={'subject': userinfo.get('sub'), 'email': userinfo.get('email')},
|
||||
)
|
||||
return userinfo
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version='1.0.0', prog_name='AGOS')
|
||||
def cli():
|
||||
"""AGOS - Agent Runtime Platform CLI"""
|
||||
pass
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--issuer', default=None, help='Armco IAM issuer URL')
|
||||
@click.option('--client-id', default=None, help='IAM OAuth client ID for the CLI/native app')
|
||||
@click.option(
|
||||
'--host',
|
||||
default='127.0.0.1',
|
||||
show_default=True,
|
||||
help='Loopback host for the local callback listener',
|
||||
)
|
||||
@click.option(
|
||||
'--port',
|
||||
default=8976,
|
||||
show_default=True,
|
||||
type=int,
|
||||
help='Loopback port for the local callback listener',
|
||||
)
|
||||
@click.option(
|
||||
'--timeout',
|
||||
'timeout_seconds',
|
||||
default=180,
|
||||
show_default=True,
|
||||
type=int,
|
||||
help='Seconds to wait for the browser login callback',
|
||||
)
|
||||
@click.option('--scopes', default=None, help='Space or comma separated OAuth scopes')
|
||||
def login(issuer, client_id, host, port, timeout_seconds, scopes):
|
||||
"""Login with Armco IAM for Agos CLI."""
|
||||
correlation_id = f'cli_login_{uuid.uuid4().hex[:12]}'
|
||||
resolved_issuer = issuer or _first_env('AGOS_CLI_IAM_ISSUER', 'VITE_IAM_ISSUER', 'IAM_ISSUER')
|
||||
resolved_client_id = client_id or _first_env('AGOS_CLI_IAM_CLIENT_ID', 'VITE_IAM_CLIENT_ID')
|
||||
resolved_scopes = _parse_scopes(scopes or _first_env('AGOS_CLI_IAM_SCOPES'))
|
||||
redirect_uri = f'http://{host}:{port}/callback'
|
||||
|
||||
if not resolved_issuer:
|
||||
raise click.ClickException('Missing IAM issuer. Set AGOS_CLI_IAM_ISSUER, VITE_IAM_ISSUER, or pass --issuer.')
|
||||
|
||||
if not resolved_client_id:
|
||||
raise click.ClickException('Missing CLI IAM client ID. Set AGOS_CLI_IAM_CLIENT_ID or pass --client-id.')
|
||||
|
||||
if os.getenv('AGOS_CLI_IAM_CLIENT_ID', '').strip() == '' and os.getenv('VITE_IAM_CLIENT_ID', '').strip():
|
||||
console.print(
|
||||
'[yellow]Using VITE_IAM_CLIENT_ID for CLI login. '
|
||||
'Ensure IAM allows the loopback redirect URI for this client.[/yellow]'
|
||||
)
|
||||
|
||||
_emit_log(
|
||||
'INFO',
|
||||
'login_start',
|
||||
correlation_id,
|
||||
metadata={
|
||||
'issuer': resolved_issuer,
|
||||
'client_id': resolved_client_id,
|
||||
'redirect_uri': redirect_uri,
|
||||
'scopes': resolved_scopes,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
discovery = _fetch_discovery(resolved_issuer, correlation_id)
|
||||
state = secrets.token_urlsafe(24)
|
||||
nonce = secrets.token_urlsafe(24)
|
||||
code_verifier = _pkce_verifier()
|
||||
code_challenge = _pkce_challenge(code_verifier)
|
||||
callback_event, callback_payload, thread = _start_callback_server(
|
||||
host=host,
|
||||
port=port,
|
||||
expected_state=state,
|
||||
timeout_seconds=timeout_seconds,
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
|
||||
authorize_params = {
|
||||
'client_id': resolved_client_id,
|
||||
'redirect_uri': redirect_uri,
|
||||
'response_type': 'code',
|
||||
'scope': ' '.join(resolved_scopes),
|
||||
'state': state,
|
||||
'nonce': nonce,
|
||||
'code_challenge': code_challenge,
|
||||
'code_challenge_method': 'S256',
|
||||
}
|
||||
authorize_url = f"{discovery['authorization_endpoint']}?{urlencode(authorize_params)}"
|
||||
browser_opened = webbrowser.open(authorize_url)
|
||||
_emit_log(
|
||||
'INFO',
|
||||
'browser_opened',
|
||||
correlation_id,
|
||||
metadata={'browser_opened': browser_opened, 'authorization_endpoint': discovery['authorization_endpoint']},
|
||||
)
|
||||
|
||||
console.print('[bold blue]Opening Armco IAM login in your browser...[/bold blue]')
|
||||
console.print(f'[dim]If the browser does not open, use this URL:[/dim]\n{authorize_url}')
|
||||
|
||||
if not callback_event.wait(timeout=timeout_seconds):
|
||||
raise click.ClickException('Timed out waiting for the IAM login callback.')
|
||||
|
||||
thread.join(timeout=1)
|
||||
|
||||
if callback_payload.get('error'):
|
||||
raise click.ClickException(
|
||||
callback_payload.get('error_description') or callback_payload['error']
|
||||
)
|
||||
|
||||
if not callback_payload.get('valid_state'):
|
||||
raise click.ClickException('IAM callback state validation failed.')
|
||||
|
||||
code = callback_payload.get('code')
|
||||
if not code:
|
||||
raise click.ClickException('IAM callback did not include an authorization code.')
|
||||
|
||||
tokens = _exchange_code_for_tokens(
|
||||
discovery=discovery,
|
||||
client_id=resolved_client_id,
|
||||
code=code,
|
||||
redirect_uri=redirect_uri,
|
||||
code_verifier=code_verifier,
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
userinfo = _fetch_userinfo(discovery, tokens['access_token'], correlation_id)
|
||||
auth_state = {
|
||||
'timestamp': _utc_now_iso(),
|
||||
'issuer': resolved_issuer.rstrip('/'),
|
||||
'client_id': resolved_client_id,
|
||||
'redirect_uri': redirect_uri,
|
||||
'scopes': resolved_scopes,
|
||||
'access_token': tokens.get('access_token'),
|
||||
'refresh_token': tokens.get('refresh_token'),
|
||||
'id_token': tokens.get('id_token'),
|
||||
'token_type': tokens.get('token_type'),
|
||||
'expires_in': tokens.get('expires_in'),
|
||||
'expires_at': int(time.time()) + int(tokens.get('expires_in', 0) or 0),
|
||||
'user': userinfo,
|
||||
}
|
||||
_write_auth_state(auth_state)
|
||||
_emit_log(
|
||||
'INFO',
|
||||
'login_completed',
|
||||
correlation_id,
|
||||
entity_id=(userinfo or {}).get('sub', 'agos_cli'),
|
||||
metadata={
|
||||
'auth_file': str(_AUTH_FILE),
|
||||
'email': (userinfo or {}).get('email'),
|
||||
'has_refresh_token': bool(tokens.get('refresh_token')),
|
||||
},
|
||||
)
|
||||
console.print('[bold green]✓ Login successful[/bold green]')
|
||||
if userinfo:
|
||||
identity_label = userinfo.get('email') or userinfo.get('username') or userinfo.get('sub')
|
||||
console.print(f'Authenticated as: {identity_label}')
|
||||
console.print(f'Token cache: {_AUTH_FILE}')
|
||||
except Exception as exc:
|
||||
_emit_log('ERROR', 'login_failed', correlation_id, metadata={'error': str(exc)})
|
||||
if isinstance(exc, click.ClickException):
|
||||
raise
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
|
||||
|
||||
@cli.group()
|
||||
def db():
|
||||
"""Database management commands"""
|
||||
pass
|
||||
|
||||
|
||||
@db.command()
|
||||
@click.option('--host', default='localhost', help='PostgreSQL host')
|
||||
@click.option('--port', default=5432, help='PostgreSQL port')
|
||||
@click.option('--database', default='agos', help='Database name')
|
||||
@click.option('--user', default='postgres', help='Database user')
|
||||
@click.password_option('--password', help='Database password')
|
||||
def db_bootstrap_command(host, port, database, user, password):
|
||||
"""Bootstrap database schema (idempotent)"""
|
||||
import asyncpg
|
||||
import asyncio
|
||||
|
||||
async def run_bootstrap():
|
||||
try:
|
||||
console.print("[bold blue]Bootstrapping AGOS database...[/bold blue]")
|
||||
|
||||
conn = await asyncpg.connect(
|
||||
host=host,
|
||||
port=port,
|
||||
database=database,
|
||||
user=user,
|
||||
password=password
|
||||
)
|
||||
|
||||
bootstrap_path = os.path.join(
|
||||
os.path.dirname(__file__), '..', 'seeders', 'bootstrap.sql'
|
||||
)
|
||||
|
||||
with open(bootstrap_path, 'r') as f:
|
||||
sql = f.read()
|
||||
|
||||
await conn.execute(sql)
|
||||
await conn.close()
|
||||
|
||||
console.print("[bold green]✓ Database bootstrap completed successfully![/bold green]")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]✗ Bootstrap failed: {e}[/bold red]")
|
||||
sys.exit(1)
|
||||
|
||||
asyncio.run(run_bootstrap())
|
||||
|
||||
|
||||
@db.command()
|
||||
@click.option('--host', default='localhost', help='PostgreSQL host')
|
||||
@click.option('--port', default=5432, help='PostgreSQL port')
|
||||
@click.option('--database', default='agos', help='Database name')
|
||||
@click.option('--user', default='postgres', help='Database user')
|
||||
@click.password_option('--password', help='Database password')
|
||||
def db_seed_command(host, port, database, user, password):
|
||||
"""Seed database with test data"""
|
||||
import asyncpg
|
||||
import asyncio
|
||||
|
||||
async def run_seed():
|
||||
try:
|
||||
console.print("[bold blue]Seeding AGOS database...[/bold blue]")
|
||||
|
||||
conn = await asyncpg.connect(
|
||||
host=host,
|
||||
port=port,
|
||||
database=database,
|
||||
user=user,
|
||||
password=password
|
||||
)
|
||||
|
||||
seed_path = os.path.join(
|
||||
os.path.dirname(__file__), '..', 'seeders', 'seed.sql'
|
||||
)
|
||||
|
||||
with open(seed_path, 'r') as f:
|
||||
sql = f.read()
|
||||
|
||||
await conn.execute(sql)
|
||||
await conn.close()
|
||||
|
||||
console.print("[bold green]✓ Database seeded successfully![/bold green]")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]✗ Seed failed: {e}[/bold red]")
|
||||
sys.exit(1)
|
||||
|
||||
asyncio.run(run_seed())
|
||||
|
||||
|
||||
@db.command()
|
||||
@click.option('--host', default='localhost', help='PostgreSQL host')
|
||||
@click.option('--port', default=5432, help='PostgreSQL port')
|
||||
@click.option('--database', default='agos', help='Database name')
|
||||
@click.option('--user', default='postgres', help='Database user')
|
||||
@click.password_option('--password', help='Database password')
|
||||
def db_status_command(host, port, database, user, password):
|
||||
"""Check database status"""
|
||||
import asyncpg
|
||||
import asyncio
|
||||
|
||||
async def check_status():
|
||||
try:
|
||||
console.print("[bold blue]Checking database status...[/bold blue]")
|
||||
|
||||
conn = await asyncpg.connect(
|
||||
host=host,
|
||||
port=port,
|
||||
database=database,
|
||||
user=user,
|
||||
password=password
|
||||
)
|
||||
|
||||
# Check tables
|
||||
tables = await conn.fetch("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name
|
||||
""")
|
||||
|
||||
# Check row counts
|
||||
agent_count = await conn.fetchval("SELECT COUNT(*) FROM agents")
|
||||
task_count = await conn.fetchval("SELECT COUNT(*) FROM tasks")
|
||||
plugin_count = await conn.fetchval("SELECT COUNT(*) FROM plugins")
|
||||
|
||||
await conn.close()
|
||||
|
||||
table = Table(title="Database Status")
|
||||
table.add_column("Metric", style="cyan")
|
||||
table.add_column("Value", style="green")
|
||||
|
||||
table.add_row("Tables", str(len(tables)))
|
||||
table.add_row("Agents", str(agent_count))
|
||||
table.add_row("Tasks", str(task_count))
|
||||
table.add_row("Plugins", str(plugin_count))
|
||||
|
||||
console.print(table)
|
||||
console.print("[bold green]✓ Database is healthy[/bold green]")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]✗ Status check failed: {e}[/bold red]")
|
||||
sys.exit(1)
|
||||
|
||||
asyncio.run(check_status())
|
||||
|
||||
|
||||
@cli.group()
|
||||
def agent():
|
||||
"""Agent management commands"""
|
||||
pass
|
||||
|
||||
|
||||
@agent.command()
|
||||
@click.option('--name', required=True, help='Agent name')
|
||||
@click.option('--description', help='Agent description')
|
||||
@click.option('--model', default='gpt-4', help='LLM model')
|
||||
def agent_create_stub(name, description, model):
|
||||
"""Create a new agent"""
|
||||
console.print(f"[bold blue]Creating agent: {name}[/bold blue]")
|
||||
console.print("[yellow]Note: Full agent creation requires API implementation[/yellow]")
|
||||
console.print(f" Name: {name}")
|
||||
console.print(f" Description: {description or 'N/A'}")
|
||||
console.print(f" Model: {model}")
|
||||
|
||||
|
||||
@agent.command()
|
||||
def agent_list_stub():
|
||||
"""List all agents"""
|
||||
console.print("[bold blue]Listing agents...[/bold blue]")
|
||||
console.print("[yellow]Note: Full agent listing requires API implementation[/yellow]")
|
||||
|
||||
|
||||
@agent.command()
|
||||
@click.argument('agent_id')
|
||||
def agent_start_stub(agent_id):
|
||||
"""Start an agent"""
|
||||
console.print(f"[bold blue]Starting agent: {agent_id}[/bold blue]")
|
||||
console.print("[yellow]Note: Full agent start requires runtime implementation[/yellow]")
|
||||
|
||||
|
||||
@agent.command()
|
||||
@click.argument('agent_id')
|
||||
def agent_stop_stub(agent_id):
|
||||
"""Stop an agent"""
|
||||
console.print(f"[bold blue]Stopping agent: {agent_id}[/bold blue]")
|
||||
console.print("[yellow]Note: Full agent stop requires runtime implementation[/yellow]")
|
||||
|
||||
|
||||
@agent.command()
|
||||
@click.argument('agent_id')
|
||||
@click.confirmation_option(prompt='Are you sure you want to delete this agent?')
|
||||
def agent_delete_stub(agent_id):
|
||||
"""Delete an agent"""
|
||||
console.print(f"[bold red]Deleting agent: {agent_id}[/bold red]")
|
||||
console.print("[yellow]Note: Full agent deletion requires API implementation[/yellow]")
|
||||
|
||||
|
||||
@cli.group()
|
||||
def plugin():
|
||||
"""Plugin management commands"""
|
||||
pass
|
||||
|
||||
|
||||
@plugin.command()
|
||||
@click.argument('plugin_name')
|
||||
def plugin_install_stub(plugin_name):
|
||||
"""Install a plugin"""
|
||||
console.print(f"[bold blue]Installing plugin: {plugin_name}[/bold blue]")
|
||||
console.print("[yellow]Note: Full plugin install requires implementation[/yellow]")
|
||||
|
||||
|
||||
@plugin.command()
|
||||
def plugin_list_stub():
|
||||
"""List installed plugins"""
|
||||
console.print("[bold blue]Listing plugins...[/bold blue]")
|
||||
console.print("[yellow]Note: Full plugin listing requires API implementation[/yellow]")
|
||||
|
||||
|
||||
@plugin.command()
|
||||
@click.argument('query')
|
||||
def plugin_search_stub(query):
|
||||
"""Search marketplace for plugins"""
|
||||
console.print(f"[bold blue]Searching for: {query}[/bold blue]")
|
||||
console.print("[yellow]Note: Full search requires marketplace API[/yellow]")
|
||||
|
||||
|
||||
@plugin.command()
|
||||
@click.argument('plugin_id')
|
||||
@click.confirmation_option(prompt='Are you sure you want to uninstall this plugin?')
|
||||
def plugin_uninstall_stub(plugin_id):
|
||||
"""Uninstall a plugin"""
|
||||
console.print(f"[bold red]Uninstalling plugin: {plugin_id}[/bold red]")
|
||||
console.print("[yellow]Note: Full uninstall requires implementation[/yellow]")
|
||||
|
||||
|
||||
@cli.group()
|
||||
def workflow():
|
||||
"""Workflow management commands"""
|
||||
pass
|
||||
|
||||
|
||||
@workflow.command()
|
||||
@click.option('--name', required=True, help='Workflow name')
|
||||
@click.option('--file', type=click.Path(exists=True), help='Workflow definition file')
|
||||
def workflow_create_stub(name, file):
|
||||
"""Create a new workflow"""
|
||||
console.print(f"[bold blue]Creating workflow: {name}[/bold blue]")
|
||||
if file:
|
||||
console.print(f" From file: {file}")
|
||||
console.print("[yellow]Note: Full workflow creation requires API implementation[/yellow]")
|
||||
|
||||
|
||||
@workflow.command()
|
||||
@click.argument('workflow_id')
|
||||
def workflow_run_stub(workflow_id):
|
||||
"""Execute a workflow"""
|
||||
console.print(f"[bold blue]Running workflow: {workflow_id}[/bold blue]")
|
||||
console.print("[yellow]Note: Full workflow execution requires runtime[/yellow]")
|
||||
|
||||
|
||||
@workflow.command()
|
||||
@click.argument('workflow_id')
|
||||
def workflow_status_stub(workflow_id):
|
||||
"""Check workflow status"""
|
||||
console.print(f"[bold blue]Checking status of workflow: {workflow_id}[/bold blue]")
|
||||
console.print("[yellow]Note: Full status check requires API implementation[/yellow]")
|
||||
|
||||
|
||||
@cli.group()
|
||||
def policy():
|
||||
"""Policy management commands"""
|
||||
pass
|
||||
|
||||
|
||||
@policy.command()
|
||||
@click.option('--name', required=True, help='Policy name')
|
||||
@click.option('--type', 'policy_type', required=True,
|
||||
type=click.Choice(['filesystem', 'network', 'resource']))
|
||||
@click.option('--effect', required=True,
|
||||
type=click.Choice(['allow', 'deny', 'require_approval']))
|
||||
def policy_create_stub(name, policy_type, effect):
|
||||
"""Create a new policy"""
|
||||
console.print(f"[bold blue]Creating policy: {name}[/bold blue]")
|
||||
console.print(f" Type: {policy_type}")
|
||||
console.print(f" Effect: {effect}")
|
||||
console.print("[yellow]Note: Full policy creation requires API implementation[/yellow]")
|
||||
|
||||
|
||||
@policy.command()
|
||||
def policy_list_stub():
|
||||
"""List all policies"""
|
||||
console.print("[bold blue]Listing policies...[/bold blue]")
|
||||
console.print("[yellow]Note: Full policy listing requires API implementation[/yellow]")
|
||||
|
||||
|
||||
@policy.command()
|
||||
@click.argument('policy_id')
|
||||
@click.confirmation_option(prompt='Are you sure you want to delete this policy?')
|
||||
def policy_delete_stub(policy_id):
|
||||
"""Delete a policy"""
|
||||
console.print(f"[bold red]Deleting policy: {policy_id}[/bold red]")
|
||||
console.print("[yellow]Note: Full policy deletion requires API implementation[/yellow]")
|
||||
|
||||
|
||||
@cli.group()
|
||||
def system():
|
||||
"""System operations"""
|
||||
pass
|
||||
|
||||
|
||||
@system.command()
|
||||
def system_status_stub():
|
||||
"""Show system status"""
|
||||
console.print("[bold blue]System Status[/bold blue]")
|
||||
|
||||
table = Table(title="AGOS Status")
|
||||
table.add_column("Component", style="cyan")
|
||||
table.add_column("Status", style="green")
|
||||
|
||||
table.add_row("Database", "✓ Connected")
|
||||
table.add_row("Runtime", "⚠ Not Started")
|
||||
table.add_row("API Server", "⚠ Not Started")
|
||||
table.add_row("Plugin System", "✓ Ready")
|
||||
|
||||
console.print(table)
|
||||
|
||||
|
||||
@system.command()
|
||||
@click.option('--lines', default=50, help='Number of log lines to show')
|
||||
def system_logs_stub(lines):
|
||||
"""Show system logs"""
|
||||
console.print(f"[bold blue]Showing last {lines} log lines...[/bold blue]")
|
||||
console.print("[yellow]Note: Full log viewing requires implementation[/yellow]")
|
||||
|
||||
|
||||
@system.command()
|
||||
def system_metrics_stub():
|
||||
"""Show system metrics"""
|
||||
console.print("[bold blue]System Metrics[/bold blue]")
|
||||
console.print("[yellow]Note: Full metrics require observability implementation[/yellow]")
|
||||
|
||||
|
||||
install_api_commands(
|
||||
cli=cli,
|
||||
agent_group=agent,
|
||||
plugin_group=plugin,
|
||||
workflow_group=workflow,
|
||||
policy_group=policy,
|
||||
system_group=system,
|
||||
console=console,
|
||||
emit_log=_emit_log,
|
||||
auth_file=_AUTH_FILE,
|
||||
)
|
||||
install_manual_commands(cli=cli, console=console)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
|
||||
1367
cli/command_surface.py
Normal file
1367
cli/command_surface.py
Normal file
File diff suppressed because it is too large
Load Diff
562
cli/manual.py
Normal file
562
cli/manual.py
Normal file
@@ -0,0 +1,562 @@
|
||||
from typing import Dict, Iterable, Sequence
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
|
||||
|
||||
_MANUAL: Dict[str, Dict[str, object]] = {
|
||||
'agos': {
|
||||
'summary': 'AGOS CLI for authentication, runtime operations, and API-backed platform management.',
|
||||
'examples': [
|
||||
'agos login',
|
||||
'agos ask Hi',
|
||||
'agos ask "Summarize today\'s failed tasks"',
|
||||
'agos whoami',
|
||||
'agos agent list',
|
||||
'agos task list --status running',
|
||||
'agos workflow run wf_autoblogger_v1 --agent-id agent_123',
|
||||
'agos help agent',
|
||||
'agos man workflow run',
|
||||
],
|
||||
'notes': [
|
||||
'Most API-backed commands honor `--api-url` and default to `http://localhost:2000`.',
|
||||
'Many read commands also accept `--json-output` for scripting.',
|
||||
'Run `agos help <topic>` for focused usage or `agos man` for the full manual.',
|
||||
],
|
||||
},
|
||||
'login': {
|
||||
'summary': 'Authenticate the CLI with Armco IAM and cache tokens in `~/.agos/auth.json`.',
|
||||
'examples': [
|
||||
'agos login',
|
||||
'agos login --issuer https://iam.armco.dev --client-id client_abc123',
|
||||
'agos login --port 8976 --timeout 240',
|
||||
],
|
||||
},
|
||||
'whoami': {
|
||||
'summary': 'Show the identity, tenant, scopes, and admin status for the current CLI session.',
|
||||
'examples': [
|
||||
'agos whoami',
|
||||
'agos whoami --json-output',
|
||||
'agos whoami --api-url https://agos.armco.dev',
|
||||
],
|
||||
},
|
||||
'quota': {
|
||||
'summary': 'Inspect your current quota status or recent quota consumption history.',
|
||||
'examples': [
|
||||
'agos quota',
|
||||
'agos quota --history',
|
||||
'agos quota --json-output',
|
||||
],
|
||||
},
|
||||
'logout': {
|
||||
'summary': 'Clear the locally cached Agos session from disk.',
|
||||
'examples': ['agos logout'],
|
||||
},
|
||||
'help': {
|
||||
'summary': 'Show focused help for a top-level command or nested action.',
|
||||
'examples': [
|
||||
'agos help',
|
||||
'agos help agent',
|
||||
'agos help workflow run',
|
||||
],
|
||||
},
|
||||
'clear': {
|
||||
'summary': 'Clear the visible terminal screen for the current CLI session.',
|
||||
'examples': ['agos clear'],
|
||||
},
|
||||
'ask': {
|
||||
'summary': 'Send a natural-language prompt to Agos chat using the authenticated CLI session.',
|
||||
'examples': [
|
||||
'agos ask Hi',
|
||||
'agos ask "Plan a launch checklist for AGOS"',
|
||||
'agos ask --json-output "Summarize current quota usage"',
|
||||
],
|
||||
},
|
||||
'man': {
|
||||
'summary': 'Show the expanded Agos manual with detailed actions and examples.',
|
||||
'examples': [
|
||||
'agos man',
|
||||
'agos man task create',
|
||||
'agos man installable',
|
||||
],
|
||||
},
|
||||
'db': {
|
||||
'summary': 'Run local database bootstrap, seed, and status operations.',
|
||||
'examples': [
|
||||
'agos db status --host localhost --database agos',
|
||||
'agos db bootstrap --database agos --user postgres',
|
||||
],
|
||||
},
|
||||
'db bootstrap': {
|
||||
'summary': 'Apply the Agos bootstrap schema to a PostgreSQL database.',
|
||||
'examples': [
|
||||
'agos db bootstrap --database agos --user postgres',
|
||||
],
|
||||
},
|
||||
'db seed': {
|
||||
'summary': 'Seed the Agos database with local development data.',
|
||||
'examples': [
|
||||
'agos db seed --database agos --user postgres',
|
||||
],
|
||||
},
|
||||
'db status': {
|
||||
'summary': 'Check database connectivity and basic table counts.',
|
||||
'examples': [
|
||||
'agos db status --database agos --user postgres',
|
||||
],
|
||||
},
|
||||
'installable': {
|
||||
'summary': 'Inspect and manage installable Agos features for the current instance.',
|
||||
'examples': [
|
||||
'agos installable list',
|
||||
'agos installable install integrations',
|
||||
],
|
||||
},
|
||||
'installable list': {
|
||||
'summary': 'List installable features and whether they are enabled.',
|
||||
'examples': [
|
||||
'agos installable list',
|
||||
'agos installable list --json-output',
|
||||
],
|
||||
},
|
||||
'installable install': {
|
||||
'summary': 'Enable a named installable feature.',
|
||||
'examples': ['agos installable install integrations'],
|
||||
},
|
||||
'installable uninstall': {
|
||||
'summary': 'Disable a named installable feature.',
|
||||
'examples': ['agos installable uninstall integrations'],
|
||||
},
|
||||
'agent': {
|
||||
'summary': 'Create, inspect, run, and control Agos agents.',
|
||||
'examples': [
|
||||
'agos agent list',
|
||||
'agos agent get agent_123',
|
||||
'agos agent run agent_123 --goal "Summarize today\'s issues"',
|
||||
],
|
||||
},
|
||||
'agent list': {
|
||||
'summary': 'List agents with optional filtering by page, status, or ephemeral flag.',
|
||||
'examples': [
|
||||
'agos agent list',
|
||||
'agos agent list --status running',
|
||||
'agos agent list --page 2 --page-size 50',
|
||||
],
|
||||
},
|
||||
'agent get': {
|
||||
'summary': 'Fetch the full details for a single agent.',
|
||||
'examples': ['agos agent get agent_123'],
|
||||
},
|
||||
'agent create': {
|
||||
'summary': 'Create a new agent with model, capabilities, and optional JSON configuration.',
|
||||
'examples': [
|
||||
'agos agent create --name "Ops Agent" --model gpt-4.1',
|
||||
(
|
||||
'agos agent create --name "RAG Agent" --model gpt-4.1 '
|
||||
'--capability search --config-json "{\"temperature\":0.2}"'
|
||||
),
|
||||
],
|
||||
},
|
||||
'agent start': {
|
||||
'summary': 'Resume a paused agent.',
|
||||
'examples': ['agos agent start agent_123'],
|
||||
},
|
||||
'agent stop': {
|
||||
'summary': 'Pause a running agent.',
|
||||
'examples': ['agos agent stop agent_123'],
|
||||
},
|
||||
'agent delete': {
|
||||
'summary': 'Delete an agent after interactive confirmation.',
|
||||
'examples': ['agos agent delete agent_123'],
|
||||
},
|
||||
'agent run': {
|
||||
'summary': 'Queue a goal-driven run for an agent.',
|
||||
'examples': [
|
||||
'agos agent run agent_123 --goal "Write the daily operations brief"',
|
||||
'agos agent run agent_123 --goal "Triage open incidents" --max-reasoning-turns 6',
|
||||
],
|
||||
},
|
||||
'task': {
|
||||
'summary': 'List, create, inspect, cancel, and delete tasks.',
|
||||
'examples': [
|
||||
'agos task list',
|
||||
'agos task get task_123',
|
||||
'agos task create --agent-id agent_123 --description "Run daily sync"',
|
||||
],
|
||||
},
|
||||
'task list': {
|
||||
'summary': 'List tasks with optional agent and status filters.',
|
||||
'examples': [
|
||||
'agos task list',
|
||||
'agos task list --status failed',
|
||||
'agos task list --agent-id agent_123',
|
||||
],
|
||||
},
|
||||
'task get': {
|
||||
'summary': 'Fetch the full details for a single task.',
|
||||
'examples': ['agos task get task_123'],
|
||||
},
|
||||
'task create': {
|
||||
'summary': 'Create a task for an agent with structured input and retry controls.',
|
||||
'examples': [
|
||||
'agos task create --agent-id agent_123 --description "Analyze churn risk"',
|
||||
(
|
||||
'agos task create --agent-id agent_123 --description '
|
||||
'"Import backlog" --input-json "{\"board_id\":\"b1\"}"'
|
||||
),
|
||||
],
|
||||
},
|
||||
'task cancel': {
|
||||
'summary': 'Cancel an in-flight task.',
|
||||
'examples': ['agos task cancel task_123'],
|
||||
},
|
||||
'task delete': {
|
||||
'summary': 'Delete a task after interactive confirmation.',
|
||||
'examples': ['agos task delete task_123'],
|
||||
},
|
||||
'workflow': {
|
||||
'summary': 'Inspect, create, run, and monitor workflows.',
|
||||
'examples': [
|
||||
'agos workflow list',
|
||||
'agos workflow create --name "Daily Brief" --definition-file workflow.json',
|
||||
'agos workflow run wf_123 --agent-id agent_123',
|
||||
],
|
||||
},
|
||||
'workflow list': {
|
||||
'summary': 'List workflows with optional status and text search filters.',
|
||||
'examples': [
|
||||
'agos workflow list',
|
||||
'agos workflow list --status active',
|
||||
'agos workflow list --search autoblogger',
|
||||
],
|
||||
},
|
||||
'workflow get': {
|
||||
'summary': 'Fetch the full details for a workflow.',
|
||||
'examples': ['agos workflow get wf_123'],
|
||||
},
|
||||
'workflow create': {
|
||||
'summary': 'Create a workflow from inline JSON or a definition file.',
|
||||
'examples': [
|
||||
'agos workflow create --name "Daily Brief" --definition-file workflow.json',
|
||||
'agos workflow create --name "Quick Flow" --definition-json "{\"nodes\":[]}"',
|
||||
],
|
||||
},
|
||||
'workflow run': {
|
||||
'summary': 'Start a workflow execution with an agent and optional runtime parameters.',
|
||||
'examples': [
|
||||
'agos workflow run wf_123 --agent-id agent_123',
|
||||
'agos workflow run wf_123 --agent-id agent_123 --parameters-json "{\"region\":\"us\"}"',
|
||||
],
|
||||
},
|
||||
'workflow status': {
|
||||
'summary': 'List recent executions for a workflow.',
|
||||
'examples': [
|
||||
'agos workflow status wf_123',
|
||||
'agos workflow status wf_123 --limit 50',
|
||||
],
|
||||
},
|
||||
'plugin': {
|
||||
'summary': 'List, search, install, inspect, enable, disable, and uninstall plugins.',
|
||||
'examples': [
|
||||
'agos plugin list',
|
||||
'agos plugin search slack',
|
||||
'agos plugin enable plugin_123',
|
||||
],
|
||||
},
|
||||
'plugin list': {
|
||||
'summary': 'List installed plugins with trust and verification filters.',
|
||||
'examples': [
|
||||
'agos plugin list',
|
||||
'agos plugin list --verified-only',
|
||||
'agos plugin list --trust-level official',
|
||||
],
|
||||
},
|
||||
'plugin get': {
|
||||
'summary': 'Fetch the full details for a plugin.',
|
||||
'examples': ['agos plugin get plugin_123'],
|
||||
},
|
||||
'plugin search': {
|
||||
'summary': 'Search the marketplace plugin catalog.',
|
||||
'examples': [
|
||||
'agos plugin search slack',
|
||||
'agos plugin search rag',
|
||||
],
|
||||
},
|
||||
'plugin install': {
|
||||
'summary': 'Install a plugin from manifest metadata and local manifest JSON.',
|
||||
'examples': [
|
||||
'agos plugin install agos-chat-launcher --version 1.0.0 --author armco --manifest-file manifest.json',
|
||||
],
|
||||
},
|
||||
'plugin enable': {
|
||||
'summary': 'Enable a previously installed plugin.',
|
||||
'examples': ['agos plugin enable plugin_123'],
|
||||
},
|
||||
'plugin disable': {
|
||||
'summary': 'Disable an installed plugin.',
|
||||
'examples': ['agos plugin disable plugin_123'],
|
||||
},
|
||||
'plugin uninstall': {
|
||||
'summary': 'Uninstall a plugin after interactive confirmation.',
|
||||
'examples': ['agos plugin uninstall plugin_123'],
|
||||
},
|
||||
'policy': {
|
||||
'summary': 'List, create, and delete platform policies.',
|
||||
'examples': [
|
||||
'agos policy list',
|
||||
'agos policy create --name "No Public Internet" --type network --conditions-json "{\"egress\":\"deny\"}"',
|
||||
],
|
||||
},
|
||||
'policy list': {
|
||||
'summary': 'List policies with optional type, agent, and enabled filters.',
|
||||
'examples': [
|
||||
'agos policy list',
|
||||
'agos policy list --enabled-only',
|
||||
'agos policy list --policy-type network',
|
||||
],
|
||||
},
|
||||
'policy create': {
|
||||
'summary': 'Create a new policy with structured conditions.',
|
||||
'examples': [
|
||||
(
|
||||
'agos policy create --name "Filesystem Guard" --type '
|
||||
'filesystem --conditions-json "{\"paths\":[\"/tmp\"]}"'
|
||||
),
|
||||
],
|
||||
},
|
||||
'policy delete': {
|
||||
'summary': 'Delete a policy after interactive confirmation.',
|
||||
'examples': ['agos policy delete policy_123'],
|
||||
},
|
||||
'integration': {
|
||||
'summary': 'Inspect the integration catalog, providers, instances, and connectivity.',
|
||||
'examples': [
|
||||
'agos integration catalog --search slack',
|
||||
'agos integration providers',
|
||||
'agos integration create --provider-id slack --name "Ops Slack"',
|
||||
],
|
||||
},
|
||||
'integration catalog': {
|
||||
'summary': 'List catalog entries for supported integrations.',
|
||||
'examples': [
|
||||
'agos integration catalog',
|
||||
'agos integration catalog --search slack',
|
||||
'agos integration catalog --category communication',
|
||||
],
|
||||
},
|
||||
'integration providers': {
|
||||
'summary': 'List configured integration providers.',
|
||||
'examples': [
|
||||
'agos integration providers',
|
||||
'agos integration providers --category communication',
|
||||
],
|
||||
},
|
||||
'integration instances': {
|
||||
'summary': 'List created integration instances.',
|
||||
'examples': [
|
||||
'agos integration instances',
|
||||
'agos integration instances --category communication',
|
||||
],
|
||||
},
|
||||
'integration create': {
|
||||
'summary': 'Create a new integration instance with JSON config.',
|
||||
'examples': [
|
||||
(
|
||||
'agos integration create --provider-id slack --name '
|
||||
'"Ops Slack" --config-json "{\"workspace\":\"armco\"}"'
|
||||
),
|
||||
],
|
||||
},
|
||||
'integration test': {
|
||||
'summary': 'Run a test against an integration instance.',
|
||||
'examples': ['agos integration test instance_123'],
|
||||
},
|
||||
'system': {
|
||||
'summary': 'Inspect high-level system health and local runtime views.',
|
||||
'examples': [
|
||||
'agos system status',
|
||||
'agos system logs --lines 100',
|
||||
],
|
||||
},
|
||||
'system status': {
|
||||
'summary': 'Show a system summary using the authenticated API surface.',
|
||||
'examples': [
|
||||
'agos system status',
|
||||
'agos system status --json-output',
|
||||
],
|
||||
},
|
||||
'system logs': {
|
||||
'summary': 'Show the legacy local log view.',
|
||||
'examples': ['agos system logs --lines 100'],
|
||||
},
|
||||
'system metrics': {
|
||||
'summary': 'Show the legacy local metrics view.',
|
||||
'examples': ['agos system metrics'],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _visible_commands(command: click.MultiCommand) -> Iterable[tuple[str, click.Command]]:
|
||||
for name in command.list_commands(click.Context(command)):
|
||||
if name.endswith('-stub') or name.endswith('-command'):
|
||||
continue
|
||||
yield name, command.get_command(click.Context(command), name)
|
||||
|
||||
|
||||
def _find_command(root: click.MultiCommand, topic: str) -> tuple[str, click.Command]:
|
||||
normalized = ' '.join(topic.strip().split())
|
||||
if normalized in {'', 'agos'}:
|
||||
return 'agos', root
|
||||
if normalized.startswith('agos '):
|
||||
normalized = normalized[len('agos '):]
|
||||
parts = normalized.split()
|
||||
current: click.Command = root
|
||||
path_parts = ['agos']
|
||||
for part in parts:
|
||||
if not isinstance(current, click.MultiCommand):
|
||||
raise click.ClickException(f'`{normalized}` is not a valid Agos command path.')
|
||||
next_command = current.get_command(click.Context(current), part)
|
||||
if next_command is None:
|
||||
raise click.ClickException(f'Unknown Agos help topic: `{normalized}`.')
|
||||
current = next_command
|
||||
path_parts.append(part)
|
||||
return ' '.join(path_parts), current
|
||||
|
||||
|
||||
def _entry_for(path: str) -> Dict[str, object]:
|
||||
normalized = path.replace('agos ', '').strip()
|
||||
if path == 'agos':
|
||||
return _MANUAL.get('agos', {})
|
||||
return _MANUAL.get(normalized, {})
|
||||
|
||||
|
||||
def _usage_for(path: str, command: click.Command) -> str:
|
||||
ctx = click.Context(command, info_name=path)
|
||||
return command.get_usage(ctx).strip()
|
||||
|
||||
|
||||
def _examples_for(path: str) -> Sequence[str]:
|
||||
return _entry_for(path).get('examples', []) # type: ignore[return-value]
|
||||
|
||||
|
||||
def _notes_for(path: str) -> Sequence[str]:
|
||||
return _entry_for(path).get('notes', []) # type: ignore[return-value]
|
||||
|
||||
|
||||
def _summary_for(path: str, command: click.Command) -> str:
|
||||
entry = _entry_for(path)
|
||||
if entry.get('summary'):
|
||||
return str(entry['summary'])
|
||||
return command.help or command.short_help or 'No description available.'
|
||||
|
||||
|
||||
def _render_subcommands(console: Console, path: str, command: click.Command) -> None:
|
||||
if not isinstance(command, click.MultiCommand):
|
||||
return
|
||||
rows = list(_visible_commands(command))
|
||||
if not rows:
|
||||
return
|
||||
table = Table(title=f'{path} actions')
|
||||
table.add_column('Action', style='cyan')
|
||||
table.add_column('Description', style='green')
|
||||
for name, subcommand in rows:
|
||||
sub_path = f'{path} {name}'
|
||||
table.add_row(sub_path, _summary_for(sub_path, subcommand))
|
||||
console.print(table)
|
||||
|
||||
|
||||
def _render_params(console: Console, command: click.Command) -> None:
|
||||
if not command.params:
|
||||
return
|
||||
table = Table(title='Arguments and Options')
|
||||
table.add_column('Name', style='cyan')
|
||||
table.add_column('Required', style='magenta')
|
||||
table.add_column('Type', style='yellow')
|
||||
table.add_column('Details', style='green')
|
||||
for param in command.params:
|
||||
if isinstance(param, click.Option):
|
||||
name = ', '.join([*param.opts, *param.secondary_opts])
|
||||
details = param.help or ''
|
||||
if param.default not in (None, (), []):
|
||||
details = f'{details} Default: {param.default}'.strip()
|
||||
table.add_row(
|
||||
name,
|
||||
'yes' if param.required else 'no',
|
||||
'option',
|
||||
details or '—',
|
||||
)
|
||||
else:
|
||||
table.add_row(
|
||||
param.human_readable_name,
|
||||
'yes',
|
||||
'argument',
|
||||
'Positional argument.',
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
|
||||
def _render_examples(console: Console, path: str) -> None:
|
||||
examples = list(_examples_for(path))
|
||||
if not examples:
|
||||
return
|
||||
body = '\n'.join(f'$ {example}' for example in examples)
|
||||
console.print(Panel.fit(body, title='Examples'))
|
||||
|
||||
|
||||
def _render_notes(console: Console, path: str) -> None:
|
||||
notes = list(_notes_for(path))
|
||||
if not notes:
|
||||
return
|
||||
body = '\n'.join(f'- {note}' for note in notes)
|
||||
console.print(Panel.fit(body, title='Notes'))
|
||||
|
||||
|
||||
def _render_topic(console: Console, root: click.MultiCommand, topic: str) -> None:
|
||||
path, command = _find_command(root, topic)
|
||||
console.rule(f'[bold blue]{path}[/bold blue]')
|
||||
console.print(Panel.fit(_summary_for(path, command), title='Summary'))
|
||||
console.print(Panel.fit(_usage_for(path, command), title='Usage'))
|
||||
_render_subcommands(console, path, command)
|
||||
_render_params(console, command)
|
||||
_render_examples(console, path)
|
||||
_render_notes(console, path)
|
||||
|
||||
|
||||
def _render_root_help(console: Console, root: click.MultiCommand) -> None:
|
||||
console.print(Panel.fit(_summary_for('agos', root), title='Agos Help'))
|
||||
_render_subcommands(console, 'agos', root)
|
||||
_render_examples(console, 'agos')
|
||||
_render_notes(console, 'agos')
|
||||
|
||||
|
||||
def _all_topics(root: click.MultiCommand) -> Sequence[str]:
|
||||
topics = ['agos']
|
||||
for name, command in _visible_commands(root):
|
||||
path = f'agos {name}'
|
||||
topics.append(path)
|
||||
if isinstance(command, click.MultiCommand):
|
||||
for sub_name, _ in _visible_commands(command):
|
||||
topics.append(f'{path} {sub_name}')
|
||||
return topics
|
||||
|
||||
|
||||
def install_manual_commands(cli: click.MultiCommand, console: Console) -> None:
|
||||
@cli.command(name='help')
|
||||
@click.argument('topic', nargs=-1)
|
||||
def help_command(topic: Sequence[str]) -> None:
|
||||
if not topic:
|
||||
_render_root_help(console, cli)
|
||||
return
|
||||
_render_topic(console, cli, ' '.join(topic))
|
||||
|
||||
@cli.command(name='man')
|
||||
@click.argument('topic', nargs=-1)
|
||||
def man_command(topic: Sequence[str]) -> None:
|
||||
if topic:
|
||||
_render_topic(console, cli, ' '.join(topic))
|
||||
return
|
||||
console.print(Panel.fit(_summary_for('agos', cli), title='Agos Manual'))
|
||||
for manual_topic in _all_topics(cli):
|
||||
_render_topic(console, cli, manual_topic)
|
||||
21
pyproject.toml
Normal file
21
pyproject.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=68", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "agos-cli"
|
||||
version = "0.1.0"
|
||||
description = "AGOS command line interface"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"click==8.1.7",
|
||||
"rich==13.7.0",
|
||||
"requests==2.32.3",
|
||||
"python-dotenv==1.0.1",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
agos = "cli.agos:cli"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["cli"]
|
||||
Reference in New Issue
Block a user