mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-08 01:39:12 +00:00
feat: tfcode beta
This commit is contained in:
@@ -1,179 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const childProcess = require("child_process")
|
||||
const fs = require("fs")
|
||||
const { spawn } = require("child_process")
|
||||
const path = require("path")
|
||||
const os = require("os")
|
||||
|
||||
function run(target) {
|
||||
const result = childProcess.spawnSync(target, process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
})
|
||||
if (result.error) {
|
||||
console.error(result.error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
const code = typeof result.status === "number" ? result.status : 0
|
||||
process.exit(code)
|
||||
}
|
||||
const scriptDir = __dirname
|
||||
const srcPath = path.join(scriptDir, "..", "src", "index.ts")
|
||||
|
||||
const envPath = process.env.OPENCODE_BIN_PATH
|
||||
if (envPath) {
|
||||
run(envPath)
|
||||
}
|
||||
const child = spawn("npx", ["tsx", srcPath, ...process.argv.slice(2)], {
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
cwd: scriptDir
|
||||
})
|
||||
|
||||
const scriptPath = fs.realpathSync(__filename)
|
||||
const scriptDir = path.dirname(scriptPath)
|
||||
|
||||
//
|
||||
const cached = path.join(scriptDir, ".tfcode")
|
||||
if (fs.existsSync(cached)) {
|
||||
run(cached)
|
||||
}
|
||||
|
||||
const platformMap = {
|
||||
darwin: "darwin",
|
||||
linux: "linux",
|
||||
win32: "windows",
|
||||
}
|
||||
const archMap = {
|
||||
x64: "x64",
|
||||
arm64: "arm64",
|
||||
arm: "arm",
|
||||
}
|
||||
|
||||
let platform = platformMap[os.platform()]
|
||||
if (!platform) {
|
||||
platform = os.platform()
|
||||
}
|
||||
let arch = archMap[os.arch()]
|
||||
if (!arch) {
|
||||
arch = os.arch()
|
||||
}
|
||||
const base = "opencode-" + platform + "-" + arch
|
||||
const binary = platform === "windows" ? "opencode.exe" : "opencode"
|
||||
|
||||
function supportsAvx2() {
|
||||
if (arch !== "x64") return false
|
||||
|
||||
if (platform === "linux") {
|
||||
try {
|
||||
return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === "darwin") {
|
||||
try {
|
||||
const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
|
||||
encoding: "utf8",
|
||||
timeout: 1500,
|
||||
})
|
||||
if (result.status !== 0) return false
|
||||
return (result.stdout || "").trim() === "1"
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === "windows") {
|
||||
const cmd =
|
||||
'(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
|
||||
|
||||
for (const exe of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
|
||||
try {
|
||||
const result = childProcess.spawnSync(exe, ["-NoProfile", "-NonInteractive", "-Command", cmd], {
|
||||
encoding: "utf8",
|
||||
timeout: 3000,
|
||||
windowsHide: true,
|
||||
})
|
||||
if (result.status !== 0) continue
|
||||
const out = (result.stdout || "").trim().toLowerCase()
|
||||
if (out === "true" || out === "1") return true
|
||||
if (out === "false" || out === "0") return false
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const names = (() => {
|
||||
const avx2 = supportsAvx2()
|
||||
const baseline = arch === "x64" && !avx2
|
||||
|
||||
if (platform === "linux") {
|
||||
const musl = (() => {
|
||||
try {
|
||||
if (fs.existsSync("/etc/alpine-release")) return true
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
|
||||
const text = ((result.stdout || "") + (result.stderr || "")).toLowerCase()
|
||||
if (text.includes("musl")) return true
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return false
|
||||
})()
|
||||
|
||||
if (musl) {
|
||||
if (arch === "x64") {
|
||||
if (baseline) return [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
|
||||
return [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
|
||||
}
|
||||
return [`${base}-musl`, base]
|
||||
}
|
||||
|
||||
if (arch === "x64") {
|
||||
if (baseline) return [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
|
||||
return [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
|
||||
}
|
||||
return [base, `${base}-musl`]
|
||||
}
|
||||
|
||||
if (arch === "x64") {
|
||||
if (baseline) return [`${base}-baseline`, base]
|
||||
return [base, `${base}-baseline`]
|
||||
}
|
||||
return [base]
|
||||
})()
|
||||
|
||||
function findBinary(startDir) {
|
||||
let current = startDir
|
||||
for (;;) {
|
||||
const modules = path.join(current, "node_modules")
|
||||
if (fs.existsSync(modules)) {
|
||||
for (const name of names) {
|
||||
const candidate = path.join(modules, name, "bin", binary)
|
||||
if (fs.existsSync(candidate)) return candidate
|
||||
}
|
||||
}
|
||||
const parent = path.dirname(current)
|
||||
if (parent === current) {
|
||||
return
|
||||
}
|
||||
current = parent
|
||||
}
|
||||
}
|
||||
|
||||
const resolved = findBinary(scriptDir)
|
||||
if (!resolved) {
|
||||
console.error(
|
||||
"It seems that your package manager failed to install the right version of the opencode CLI for your platform. You can try manually installing " +
|
||||
names.map((n) => `\"${n}\"`).join(" or ") +
|
||||
" package",
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
run(resolved)
|
||||
child.on("exit", (code) => {
|
||||
process.exit(code || 0)
|
||||
})
|
||||
|
||||
333
packages/tfcode/bin/tfcode.js
Normal file
333
packages/tfcode/bin/tfcode.js
Normal file
@@ -0,0 +1,333 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user