#!/usr/bin/env python3 """ CODITECT License Activation Script
Activates a CODITECT license key by:
- Validating the license with the server
- Binding the license to this machine
- Saving the license file locally
Usage: python3 license-activate.py PILOT-XXXX-XXXX-XXXX-XXXX python3 license-activate.py --json PILOT-XXXX-XXXX-XXXX-XXXX
Per ADR-067: Time-Controlled Licensing System """
import argparse import json import os import re import sys import urllib.request import urllib.error from datetime import datetime from pathlib import Path from typing import Dict, Any, Optional
============================================================================
Constants
============================================================================
ADR-114: Framework install vs user data
CODITECT_DIR = Path.home() / ".coditect" USER_DATA_DIR = Path.home() / "PROJECTS" / ".coditect-data" LICENSING_DIR = CODITECT_DIR / "licensing" LICENSE_FILE = LICENSING_DIR / "license.json"
ADR-114: machine-id.json is in user data (portable identity)
MACHINE_ID_FILE = USER_DATA_DIR / "machine-id.json" if (USER_DATA_DIR / "machine-id.json").exists() else CODITECT_DIR / "machine-id.json" CONFIG_FILE = CODITECT_DIR / "config" / "config.json"
License key pattern: PREFIX-XXXX-XXXX-XXXX-XXXX
LICENSE_KEY_PATTERN = r'^(PILOT|PRO|TEAM|ENT)-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
Default API URL
DEFAULT_API_URL = "https://api.coditect.ai"
============================================================================
Utility Functions
============================================================================
def get_api_url() -> str: """Get API URL from config or use default.""" if CONFIG_FILE.exists(): try: config = json.loads(CONFIG_FILE.read_text()) return config.get("cloud_sync", {}).get("api_url", DEFAULT_API_URL) except Exception: pass return DEFAULT_API_URL
def get_machine_id() -> Optional[Dict[str, Any]]: """Load machine identification.""" if MACHINE_ID_FILE.exists(): try: return json.loads(MACHINE_ID_FILE.read_text()) except Exception: pass return None
def validate_key_format(license_key: str) -> bool: """Validate license key format.""" return bool(re.match(LICENSE_KEY_PATTERN, license_key.upper()))
============================================================================
Activation Functions
============================================================================
def activate_license(license_key: str, dry_run: bool = False) -> Dict[str, Any]: """ Activate a license key with the server.
Args:
license_key: The license key to activate
dry_run: If True, don't save the license file
Returns:
Dict with activation result
"""
# Normalize key format
license_key = license_key.upper().strip()
# Validate format
if not validate_key_format(license_key):
return {
"success": False,
"error": f"Invalid license key format. Expected: PILOT-XXXX-XXXX-XXXX-XXXX, got: {license_key}"
}
# Get machine ID
machine_id = get_machine_id()
if not machine_id:
return {
"success": False,
"error": "Machine ID not found. Run CODITECT-CORE-INITIAL-SETUP.py first."
}
machine_uuid = machine_id.get("machine_uuid")
device_name = machine_id.get("hostname", "Unknown Device")
if not machine_uuid:
return {
"success": False,
"error": "Invalid machine ID file. Re-run CODITECT-CORE-INITIAL-SETUP.py"
}
# Build activation request
payload = {
"license_key": license_key,
"machine_uuid": machine_uuid,
"device_name": device_name
}
if dry_run:
return {
"success": True,
"dry_run": True,
"would_send": payload,
"message": "Dry run - no activation performed"
}
# Call activation API
api_url = get_api_url()
activate_endpoint = f"{api_url}/api/v1/licenses/activate/"
try:
req = urllib.request.Request(
activate_endpoint,
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
method="POST"
)
with urllib.request.urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode())
if result.get("valid"):
# Build license data from response
license_data = {
"license_id": result.get("license_id"),
"license_key": license_key,
"tier": result.get("tier"),
"licensee": {
"email": result.get("email"),
"organization": result.get("organization")
},
"binding": {
"machine_uuid": machine_uuid,
"device_name": device_name,
"bound_at": datetime.now().isoformat()
},
"validity": {
"issued_at": result.get("issued_at"),
"expires_at": result.get("expires_at"),
"grace_period_days": result.get("grace_period_days", 7)
},
"offline": {
"max_offline_days": result.get("offline_tolerance_days", 7),
"last_server_check": datetime.now().isoformat()
},
"features": result.get("features", {}),
"signature": result.get("signature", {}),
"activated_at": datetime.now().isoformat()
}
# Save license file
LICENSING_DIR.mkdir(parents=True, exist_ok=True)
LICENSE_FILE.write_text(json.dumps(license_data, indent=2))
# Calculate days remaining
days_remaining = None
if result.get("expires_at"):
try:
expires = datetime.fromisoformat(result["expires_at"].replace("Z", "+00:00"))
days_remaining = (expires - datetime.now(expires.tzinfo)).days
except Exception:
pass
return {
"success": True,
"license_id": license_data["license_id"],
"tier": license_data["tier"],
"email": license_data["licensee"]["email"],
"organization": license_data["licensee"]["organization"],
"expires_at": license_data["validity"]["expires_at"],
"days_remaining": days_remaining,
"device_name": device_name,
"message": f"License activated successfully!"
}
else:
return {
"success": False,
"error": result.get("error", result.get("detail", "Activation failed"))
}
except urllib.error.HTTPError as e:
error_body = ""
try:
error_body = e.read().decode() if e.fp else ""
error_data = json.loads(error_body)
error_msg = error_data.get("error", error_data.get("detail", str(e)))
except Exception:
error_msg = str(e)
# Handle specific HTTP errors
if e.code == 404:
return {"success": False, "error": "Invalid license key. Please check and try again."}
elif e.code == 409:
return {"success": False, "error": "License already activated on maximum allowed devices. Contact support."}
elif e.code == 410:
return {"success": False, "error": "License has been revoked. Contact support."}
elif e.code == 401:
return {"success": False, "error": "Authentication required. Please check your credentials."}
else:
return {"success": False, "error": f"Activation failed: {error_msg}"}
except urllib.error.URLError as e:
return {
"success": False,
"error": f"Network error: Unable to connect to activation server. Check internet connection."
}
except Exception as e:
return {
"success": False,
"error": f"Unexpected error: {str(e)}"
}
def deactivate_license() -> Dict[str, Any]: """ Deactivate the current license (unbind from this machine).
Returns:
Dict with deactivation result
"""
if not LICENSE_FILE.exists():
return {
"success": False,
"error": "No license file found. Nothing to deactivate."
}
try:
license_data = json.loads(LICENSE_FILE.read_text())
license_id = license_data.get("license_id")
# Get machine ID for unbinding
machine_id = get_machine_id()
if not machine_id:
# Just delete local file
LICENSE_FILE.unlink()
return {
"success": True,
"message": "Local license file removed (server unbinding skipped - no machine ID)"
}
machine_uuid = machine_id.get("machine_uuid")
# Call deactivation API
api_url = get_api_url()
# Note: This endpoint would need to be implemented on the backend
# For now, just remove the local file
LICENSE_FILE.unlink()
return {
"success": True,
"license_id": license_id,
"message": "License deactivated from this machine"
}
except Exception as e:
return {
"success": False,
"error": f"Deactivation failed: {str(e)}"
}
============================================================================
CLI Interface
============================================================================
def print_result(result: Dict[str, Any]) -> None: """Print activation result in human-readable format.""" if result.get("success"): print() print("=" * 50) print(" CODITECT License Activated!") print("=" * 50) print() if result.get("license_id"): print(f" License ID: {result['license_id'][:16]}...") if result.get("tier"): print(f" Tier: {result['tier'].title()}") if result.get("organization"): print(f" Organization: {result['organization']}") if result.get("email"): print(f" Email: {result['email']}") if result.get("expires_at"): print(f" Expires: {result['expires_at'][:10]}") if result.get("days_remaining") is not None: print(f" Days Left: {result['days_remaining']}") if result.get("device_name"): print(f" Device: {result['device_name']}") print() print(" Your CODITECT installation is now fully licensed.") print(" Run /license-status to view details.") print() print("=" * 50) else: print() print("=" * 50) print(" License Activation Failed") print("=" * 50) print() print(f" Error: {result.get('error', 'Unknown error')}") print() print(" Common issues:") print(" - Invalid license key format") print(" - License key already used on max devices") print(" - License expired or revoked") print(" - Network connectivity issues") print() print(" Contact support@coditect.ai for help.") print() print("=" * 50)
def main(): """Main CLI entry point.""" parser = argparse.ArgumentParser( description="Activate a CODITECT license key (ADR-067)" ) parser.add_argument( "license_key", nargs="?", help="License key to activate (e.g., PILOT-XXXX-XXXX-XXXX-XXXX)" ) parser.add_argument( "--json", action="store_true", help="Output as JSON" ) parser.add_argument( "--dry-run", action="store_true", help="Validate without activating" ) parser.add_argument( "--deactivate", action="store_true", help="Deactivate current license" )
args = parser.parse_args()
if args.deactivate:
result = deactivate_license()
if args.json:
print(json.dumps(result, indent=2))
else:
if result.get("success"):
print(f"\n✓ {result.get('message', 'License deactivated')}\n")
else:
print(f"\n✗ {result.get('error', 'Deactivation failed')}\n")
sys.exit(0 if result.get("success") else 1)
if not args.license_key:
print("\nUsage: python3 license-activate.py PILOT-XXXX-XXXX-XXXX-XXXX")
print("\nOr run /license-activate in Claude Code for interactive activation.\n")
sys.exit(1)
result = activate_license(args.license_key, dry_run=args.dry_run)
if args.json:
print(json.dumps(result, indent=2))
else:
print_result(result)
sys.exit(0 if result.get("success") else 1)
if name == "main": main()