#!/usr/bin/env node import { spawn } from 'child_process'; import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs'; import { join, dirname } from 'path'; import { homedir } from 'os'; import * as readline from 'readline'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const TFCODE_DIR = join(homedir(), '.tfcode'); const TOOLS_FILE = join(TFCODE_DIR, 'tools.json'); const CREDENTIALS_FILE = join(TFCODE_DIR, 'credentials.json'); const CONFIG_FILE = join(TFCODE_DIR, 'config.json'); const COLORS = { reset: '\x1b[0m', bold: '\x1b[1m', green: '\x1b[32m', red: '\x1b[31m', cyan: '\x1b[36m', dim: '\x1b[90m', yellow: '\x1b[33m', magenta: '\x1b[35m' }; function log(msg) { console.log(msg); } function success(msg) { console.log(`${COLORS.green}✓${COLORS.reset} ${msg}`); } function error(msg) { console.error(`${COLORS.red}✗${COLORS.reset} ${msg}`); } function info(msg) { console.log(`${COLORS.cyan}ℹ${COLORS.reset} ${msg}`); } function ensureConfigDir() { if (!existsSync(TFCODE_DIR)) mkdirSync(TFCODE_DIR, { recursive: true }); } function loadConfig() { const envConfig = { workspace_id: process.env.TF_WORKSPACE_ID, api_key: process.env.TF_API_KEY, region: process.env.TF_REGION }; if (envConfig.workspace_id && envConfig.api_key) return envConfig; if (existsSync(CONFIG_FILE)) { try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')); } catch {} } return null; } function saveConfig(config) { ensureConfigDir(); writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); } function runPythonSync(method, config = null) { const wsId = config?.workspace_id || process.env.TF_WORKSPACE_ID || ''; const apiKey = config?.api_key || process.env.TF_API_KEY || ''; const region = config?.region || process.env.TF_REGION || 'au'; const pythonCode = ` import json, sys, os try: os.environ["TF_WORKSPACE_ID"] = "${wsId}" os.environ["TF_API_KEY"] = "${apiKey}" os.environ["TF_REGION"] = "${region}" from tf_sync.config import load_config, validate_credentials, Region from tf_sync.tools import sync_tools from tf_sync.config import get_region_urls method = "${method}" if method == "validate": config = load_config() result = validate_credentials(config) urls = get_region_urls(config.region) print(json.dumps({"success": result.success, "workspace_id": result.workspace_id, "workspace_name": result.workspace_name, "error": result.error, "base_url": urls["base_url"]})) elif method == "sync": config = load_config() result = sync_tools(config) tools_data = [{"id": t.id, "name": t.name, "description": t.description, "tool_type": t.tool_type.value, "request_type": t.request_type.value if t.request_type else None, "url": t.url, "auth_via": t.auth_via} for t in result.tools] print(json.dumps({"success": result.success, "tools": tools_data, "by_type": result.by_type, "error": result.error})) except Exception as e: print(json.dumps({"success": False, "error": str(e)})) `; return new Promise((resolve, reject) => { const proc = spawn(process.env.TFCODE_PYTHON_PATH || 'python3', ['-c', pythonCode], { env: { ...process.env } }); let stdout = '', stderr = ''; proc.stdout.on('data', (d) => stdout += d); proc.stderr.on('data', (d) => stderr += d); proc.on('close', (code) => { if (code !== 0 && !stdout) reject(new Error(`Python failed: ${stderr}`)); else try { resolve(JSON.parse(stdout.trim())); } catch (e) { reject(new Error(`Parse error: ${stdout}`)); } }); proc.on('error', reject); }); } function loadCachedTools() { if (!existsSync(TOOLS_FILE)) return null; try { return JSON.parse(readFileSync(TOOLS_FILE, 'utf-8')); } catch { return null; } } function saveToolsCache(tools) { ensureConfigDir(); writeFileSync(TOOLS_FILE, JSON.stringify(tools, null, 2)); } async function question(prompt) { return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question(prompt, (answer) => { rl.close(); resolve(answer.trim()); }); }); } async function select(prompt, options) { log(''); log(prompt); log(''); options.forEach((opt, i) => log(` ${COLORS.cyan}${i + 1}.${COLORS.reset} ${opt}`)); log(''); const answer = await question('Select (1-' + options.length + '): '); const idx = parseInt(answer) - 1; return idx >= 0 && idx < options.length ? idx : 0; } async function interactiveSetup() { log(''); log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`); log(`${COLORS.bold}${COLORS.magenta} tfcode Setup${COLORS.reset}`); log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`); log(''); log('This will guide you through setting up your ToothFairyAI credentials.'); log(''); log(`${COLORS.dim}You can find your credentials at:${COLORS.reset}`); log(`${COLORS.dim} https://app.toothfairyai.com → Settings → API Keys${COLORS.reset}`); log(''); log(`${COLORS.bold}Step 1: Workspace ID${COLORS.reset}`); log(`${COLORS.dim}This is your workspace UUID${COLORS.reset}`); log(''); const workspaceId = await question('Enter your Workspace ID: '); if (!workspaceId) { error('Workspace ID is required'); process.exit(1); } log(''); log(`${COLORS.bold}Step 2: API Key${COLORS.reset}`); log(`${COLORS.dim}Paste or type your API key${COLORS.reset}`); log(''); const apiKey = await question('Enter your API Key: '); if (!apiKey) { error('API Key is required'); process.exit(1); } log(''); log(`${COLORS.bold}Step 3: Region${COLORS.reset}`); const regions = ['dev (Development)', 'au (Australia)', 'eu (Europe)', 'us (United States)']; const regionIdx = await select('Select your region:', regions); const region = ['dev', 'au', 'eu', 'us'][regionIdx]; log(''); log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`); log(''); log(`${COLORS.bold}Summary:${COLORS.reset}`); log(` Workspace ID: ${workspaceId}`); log(` API Key: ***${apiKey.slice(-4)}`); log(` Region: ${region}`); log(''); const confirm = await question('Save these credentials? (Y/n): '); if (confirm.toLowerCase() === 'n' || confirm.toLowerCase() === 'no') { log('Setup cancelled.'); return; } const config = { workspace_id: workspaceId, api_key: apiKey, region }; saveConfig(config); success('Credentials saved to ~/.tfcode/config.json'); log(''); const testNow = await question('Validate credentials now? (Y/n): '); if (testNow.toLowerCase() === 'n' || testNow.toLowerCase() === 'no') return; log(''); info('Validating credentials...'); log(''); try { const result = await runPythonSync('validate', config); if (result.success) { success('Credentials valid!'); log(` API URL: ${result.base_url}`); log(` Workspace ID: ${result.workspace_id}`); log(''); const syncNow = await question('Sync tools now? (Y/n): '); if (syncNow.toLowerCase() === 'n' || syncNow.toLowerCase() === 'no') return; log(''); info('Syncing tools...'); log(''); const syncResult = await runPythonSync('sync', config); if (syncResult.success) { saveToolsCache(syncResult); success(`Synced ${syncResult.tools.length} tools`); if (syncResult.by_type && Object.keys(syncResult.by_type).length > 0) { log(''); log('By type:'); for (const [type, count] of Object.entries(syncResult.by_type)) log(` ${type}: ${count}`); } } else { error(`Sync failed: ${syncResult.error}`); } } else { error(`Validation failed: ${result.error}`); } } catch (e) { error(`Failed: ${e.message}`); } log(''); log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`); log(`${COLORS.bold}${COLORS.green} Setup Complete!${COLORS.reset}`); log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`); log(''); log('Commands:'); log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Check credentials`); log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools`); log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List tools`); log(''); } function showHelp() { log(''); log(`${COLORS.bold}tfcode${COLORS.reset} - ToothFairyAI's AI coding agent`); log(''); log('Commands:'); log(` ${COLORS.cyan}tfcode setup${COLORS.reset} Interactive credential setup`); log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Test credentials`); log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools from workspace`); log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List synced tools`); log(` ${COLORS.cyan}tfcode --help${COLORS.reset} Show this help`); log(` ${COLORS.cyan}tfcode --version${COLORS.reset} Show version`); log(''); log(`${COLORS.dim}For full TUI, run from source:${COLORS.reset}`); log(`${COLORS.dim} bun run packages/tfcode/src/index.ts${COLORS.reset}`); log(''); } const args = process.argv.slice(2); const command = args[0]; if (args.includes('--help') || args.includes('-h')) { showHelp(); } else if (args.includes('--version') || args.includes('-v')) { log('tfcode v1.0.0-beta.9'); } else if (command === 'setup') { interactiveSetup(); } else if (command === 'validate') { (async () => { const config = loadConfig(); if (!config) { error('No credentials. Run: tfcode setup'); process.exit(1); } info('Validating...'); try { const result = await runPythonSync('validate', config); if (result.success) { success('Credentials valid'); log(` API URL: ${result.base_url}`); } else { error(`Failed: ${result.error}`); process.exit(1); } } catch (e) { error(`Failed: ${e.message}`); process.exit(1); } })(); } else if (command === 'sync') { (async () => { const config = loadConfig(); if (!config) { error('No credentials. Run: tfcode setup'); process.exit(1); } info('Syncing tools...'); try { const result = await runPythonSync('sync', config); if (result.success) { saveToolsCache(result); success(`Synced ${result.tools.length} tools`); if (result.by_type) { log(''); log('By type:'); for (const [t, c] of Object.entries(result.by_type)) log(` ${t}: ${c}`); } } else { error(`Failed: ${result.error}`); process.exit(1); } } catch (e) { error(`Failed: ${e.message}`); process.exit(1); } })(); } else if (command === 'tools' && args[1] === 'list') { const cached = loadCachedTools(); if (!cached?.success) { error('No tools. Run: tfcode sync'); process.exit(1); } let tools = cached.tools; if (args[3] === '--type' && args[4]) tools = tools.filter(t => t.tool_type === args[4]); log(`\n${tools.length} tool(s):\n`); for (const t of tools) { log(` ${COLORS.cyan}${t.name}${COLORS.reset}`); log(` Type: ${t.tool_type}`); if (t.description) log(` ${COLORS.dim}${t.description.slice(0, 60)}${COLORS.reset}`); log(` Auth: ${t.auth_via}\n`); } } else if (!command) { // Show help instead of trying TUI (TUI requires full build) showHelp(); } else { error(`Unknown command: ${command}`); showHelp(); process.exit(1); }