Add app pairing feature for simplified registry connection
- Add pairing_requests table and hostname column to api_tokens - Add pairing API endpoints: initiate, check, status, connected-apps - Add cmdforge config connect <username> CLI command - Rewrite tokens.html as Connected Apps with pairing flow - Update TUI: Connect button when not authenticated, Publish when connected - Add private sync option after save in TUI when connected - Add visibility parameter to publish_tool in registry_client Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -240,6 +240,11 @@ def main():
|
||||
p_cfg_set.add_argument("value", help="Config value")
|
||||
p_cfg_set.set_defaults(func=cmd_config)
|
||||
|
||||
# config connect
|
||||
p_cfg_connect = config_sub.add_parser("connect", help="Connect this app to your CmdForge account")
|
||||
p_cfg_connect.add_argument("username", help="Your CmdForge username")
|
||||
p_cfg_connect.set_defaults(func=cmd_config)
|
||||
|
||||
# Default for config with no subcommand
|
||||
p_config.set_defaults(func=lambda args: cmd_config(args) if args.config_cmd else (setattr(args, 'config_cmd', 'show') or cmd_config(args)))
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"""Configuration commands."""
|
||||
|
||||
from ..config import load_config, save_config, set_registry_token
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
from ..config import load_config, save_config, set_registry_token, get_registry_url
|
||||
|
||||
|
||||
def cmd_config(args):
|
||||
@@ -11,9 +15,12 @@ def cmd_config(args):
|
||||
return _cmd_config_set_token(args)
|
||||
elif args.config_cmd == "set":
|
||||
return _cmd_config_set(args)
|
||||
elif args.config_cmd == "connect":
|
||||
return _cmd_config_connect(args)
|
||||
else:
|
||||
print("Config commands:")
|
||||
print(" show Show current configuration")
|
||||
print(" connect <username> Connect this app to your CmdForge account")
|
||||
print(" set-token <token> Set registry authentication token")
|
||||
print(" set <key> <value> Set a configuration value")
|
||||
return 0
|
||||
@@ -60,3 +67,101 @@ def _cmd_config_set(args):
|
||||
save_config(config)
|
||||
print(f"Set {key} = {value}")
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_config_connect(args):
|
||||
"""Connect this app to a CmdForge account via web pairing."""
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("Error: requests library required. Install with: pip install requests")
|
||||
return 1
|
||||
|
||||
username = args.username
|
||||
hostname = socket.gethostname()
|
||||
registry_url = get_registry_url()
|
||||
|
||||
# Remove trailing /api/v1 if present to get base URL
|
||||
base_url = registry_url.rstrip("/")
|
||||
if base_url.endswith("/api/v1"):
|
||||
base_url = base_url[:-7]
|
||||
|
||||
pairing_url = f"{base_url}/api/v1/pairing/check/{username}"
|
||||
|
||||
print(f"Connecting to CmdForge as @{username}...")
|
||||
print(f"Device: {hostname}")
|
||||
print()
|
||||
print("Waiting for approval from the web interface...")
|
||||
print("Go to https://cmdforge.brrd.tech/dashboard/connected-apps")
|
||||
print("and click 'Connect New App', then 'I've Run the Command'")
|
||||
print()
|
||||
print("Press Ctrl+C to cancel")
|
||||
print()
|
||||
|
||||
# Poll for pairing status
|
||||
max_attempts = 150 # 5 minutes at 2-second intervals
|
||||
attempt = 0
|
||||
|
||||
try:
|
||||
while attempt < max_attempts:
|
||||
attempt += 1
|
||||
try:
|
||||
response = requests.get(
|
||||
pairing_url,
|
||||
params={"hostname": hostname},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
status = data.get("data", {}).get("status")
|
||||
|
||||
if status == "connected":
|
||||
token = data.get("data", {}).get("token")
|
||||
if token:
|
||||
set_registry_token(token)
|
||||
print("\nConnected successfully!")
|
||||
print(f"Your device '{hostname}' is now linked to @{username}")
|
||||
print("\nYou can now publish tools with: cmdforge registry publish")
|
||||
return 0
|
||||
else:
|
||||
print("\nError: Pairing completed but no token received")
|
||||
return 1
|
||||
elif status == "pending":
|
||||
# Still waiting, continue polling
|
||||
pass
|
||||
elif status == "expired":
|
||||
print("\nPairing request expired. Please try again.")
|
||||
return 1
|
||||
elif status == "not_found":
|
||||
# No pending pairing yet, continue waiting
|
||||
pass
|
||||
else:
|
||||
# Unknown status, keep waiting
|
||||
pass
|
||||
|
||||
elif response.status_code == 404:
|
||||
# No pairing found yet, keep waiting
|
||||
pass
|
||||
else:
|
||||
# Server error, but keep trying
|
||||
pass
|
||||
|
||||
except requests.exceptions.RequestException:
|
||||
# Network error, keep trying
|
||||
pass
|
||||
|
||||
# Show progress indicator
|
||||
dots = "." * ((attempt % 3) + 1)
|
||||
sys.stdout.write(f"\rWaiting{dots} ")
|
||||
sys.stdout.flush()
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
print("\n\nTimed out waiting for approval.")
|
||||
print("Please initiate the connection from the web interface first.")
|
||||
return 1
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nConnection cancelled.")
|
||||
return 1
|
||||
|
||||
Reference in New Issue
Block a user