2026-03-24 14:01:32 +11:00

333 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { spawn } from 'child_process';
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
const TFCODE_DIR = join(homedir(), '.tfcode');
const TOOLS_FILE = join(TFCODE_DIR, 'tools.json');
const CREDENTIALS_FILE = join(TFCODE_DIR, 'credentials.json');
const COLORS = {
reset: '\x1b[0m',
bold: '\x1b[1m',
green: '\x1b[32m',
red: '\x1b[31m',
cyan: '\x1b[36m',
dim: '\x1b[90m',
yellow: '\x1b[33m'
};
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 runPythonSync(method) {
const pythonCode = `
import json
import sys
import os
try:
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 = []
for tool in result.tools:
tools_data.append({
"id": tool.id,
"name": tool.name,
"description": tool.description,
"tool_type": tool.tool_type.value,
"request_type": tool.request_type.value if tool.request_type else None,
"url": tool.url,
"auth_via": tool.auth_via
})
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)}))
sys.exit(0)
`;
return new Promise((resolve, reject) => {
const pythonPath = process.env.TFCODE_PYTHON_PATH || 'python3';
const proc = spawn(pythonPath, ['-c', pythonCode], {
env: {
...process.env,
PYTHONPATH: process.env.TFCODE_PYTHONPATH || ''
}
});
let stdout = '';
let stderr = '';
proc.stdout.on('data', (data) => {
stdout += data.toString();
});
proc.stderr.on('data', (data) => {
stderr += data.toString();
});
proc.on('close', (code) => {
if (code !== 0 && !stdout) {
reject(new Error(`Python sync failed: ${stderr}`));
return;
}
try {
const result = JSON.parse(stdout.trim());
resolve(result);
} catch (e) {
reject(new Error(`Failed to parse Python output: ${stdout}\nstderr: ${stderr}`));
}
});
proc.on('error', (err) => {
reject(err);
});
});
}
function ensureConfigDir() {
if (!existsSync(TFCODE_DIR)) {
mkdirSync(TFCODE_DIR, { recursive: true });
}
}
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));
}
const cli = yargs(hideBin(process.argv))
.scriptName('tfcode')
.wrap(100)
.help()
.alias('help', 'h')
.command({
command: 'quickstart',
describe: 'show quick start guide',
handler: () => {
log('');
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
log(`${COLORS.bold} tfcode - Quick Start Guide${COLORS.reset}`);
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
log('');
log('Welcome to tfcode! Follow these steps to get started:');
log('');
log(`${COLORS.cyan}STEP 1: Set Your Credentials${COLORS.reset}`);
log(`${COLORS.dim} export TF_WORKSPACE_ID="your-workspace-id"${COLORS.reset}`);
log(`${COLORS.dim} export TF_API_KEY="your-api-key"${COLORS.reset}`);
log(`${COLORS.dim} export TF_REGION="au"${COLORS.reset}`);
log(`${COLORS.dim} Regions: dev, au, eu, us${COLORS.reset}`);
log('');
log(`${COLORS.cyan}STEP 2: Validate Connection${COLORS.reset}`);
log(`${COLORS.dim} tfcode validate${COLORS.reset}`);
log('');
log(`${COLORS.cyan}STEP 3: Sync Your Tools${COLORS.reset}`);
log(`${COLORS.dim} tfcode sync${COLORS.reset}`);
log('');
log(`${COLORS.cyan}STEP 4: Start Coding!${COLORS.reset}`);
log(`${COLORS.dim} tfcode${COLORS.reset}`);
log('');
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
log('');
log(' Useful Commands:');
log('');
log(' tfcode validate Test your credentials');
log(' tfcode sync Sync tools from workspace');
log(' tfcode tools list Show all your tools');
log('');
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
log('');
log(' Need help? https://toothfairyai.com/developers/tfcode');
log('');
}
})
.command({
command: 'validate',
describe: 'validate ToothFairyAI credentials',
handler: async () => {
info('Validating ToothFairyAI credentials...');
log('');
try {
const result = await runPythonSync('validate');
if (result.success) {
success('Credentials valid');
if (result.base_url) {
log(`${COLORS.dim} API URL: ${result.base_url}${COLORS.reset}`);
}
if (result.workspace_id) {
log(`${COLORS.dim} Workspace ID: ${result.workspace_id}${COLORS.reset}`);
}
} else {
error(`Validation failed: ${result.error || 'Unknown error'}`);
log('');
log(`${COLORS.dim}Check your credentials:${COLORS.reset}`);
log(`${COLORS.dim} TF_WORKSPACE_ID: ${process.env.TF_WORKSPACE_ID || 'not set'}${COLORS.reset}`);
log(`${COLORS.dim} TF_API_KEY: ${process.env.TF_API_KEY ? '***' + process.env.TF_API_KEY.slice(-4) : 'not set'}${COLORS.reset}`);
log(`${COLORS.dim} TF_REGION: ${process.env.TF_REGION || 'au (default)'}${COLORS.reset}`);
process.exit(1);
}
} catch (e) {
error(`Failed to validate: ${e.message}`);
log('');
log(`${COLORS.dim}Make sure Python 3.10+ and the ToothFairyAI SDK are installed:${COLORS.reset}`);
log(`${COLORS.dim} pip install toothfairyai pydantic httpx rich${COLORS.reset}`);
process.exit(1);
}
}
})
.command({
command: 'sync',
describe: 'sync tools from ToothFairyAI workspace',
handler: async () => {
info('Syncing tools from ToothFairyAI workspace...');
log('');
try {
const result = await runPythonSync('sync');
if (result.success) {
saveToolsCache(result);
success(`Synced ${result.tools.length} tools`);
log('');
if (result.by_type && Object.keys(result.by_type).length > 0) {
log('By type:');
for (const [type, count] of Object.entries(result.by_type)) {
log(` ${type}: ${count}`);
}
log('');
}
} else {
error(`Sync failed: ${result.error || 'Unknown error'}`);
process.exit(1);
}
} catch (e) {
error(`Failed to sync: ${e.message}`);
process.exit(1);
}
}
})
.command({
command: 'tools',
describe: 'manage tools',
builder: (yargs) => {
return yargs
.command({
command: 'list',
describe: 'list synced tools',
builder: (yargs) => {
return yargs.option('type', {
type: 'string',
describe: 'filter by type (api_function)'
});
},
handler: (args) => {
const cached = loadCachedTools();
if (!cached || !cached.success) {
error('No tools synced. Run \'tfcode sync\' first.');
process.exit(1);
return;
}
let tools = cached.tools;
if (args.type) {
tools = tools.filter(t => t.tool_type === args.type);
}
if (tools.length === 0) {
log('No tools found.');
return;
}
log('');
log(`${tools.length} tool(s):`);
log('');
for (const tool of tools) {
log(` ${COLORS.cyan}${tool.name}${COLORS.reset}`);
log(` Type: ${tool.tool_type}`);
if (tool.description) {
log(` ${COLORS.dim}${tool.description.slice(0, 60)}${tool.description.length > 60 ? '...' : ''}${COLORS.reset}`);
}
log(` Auth: ${tool.auth_via}`);
log('');
}
}
})
.demandCommand();
},
handler: () => {}
})
.demandCommand()
.strict()
.fail((msg, err) => {
if (msg) {
error(msg);
}
if (err) {
error(err.message);
}
process.exit(1);
});
cli.parse();