mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-09 10:18:57 +00:00
717 lines
20 KiB
TypeScript
717 lines
20 KiB
TypeScript
import { cmd } from "@/cli/cmd/cmd"
|
|
import { UI } from "@/cli/ui"
|
|
import { Log } from "@/util/log"
|
|
import { spawn } from "child_process"
|
|
import { Filesystem } from "@/util/filesystem"
|
|
import { mkdir } from "fs/promises"
|
|
import { existsSync } from "fs"
|
|
import path from "path"
|
|
import { Global } from "@/global"
|
|
import * as prompts from "@clack/prompts"
|
|
|
|
const println = (msg: string) => UI.println(msg)
|
|
const printError = (msg: string) => UI.error(msg)
|
|
const success = (msg: string) => UI.println(UI.Style.TEXT_SUCCESS_BOLD + msg + UI.Style.TEXT_NORMAL)
|
|
const info = (msg: string) => UI.println(UI.Style.TEXT_NORMAL + msg)
|
|
|
|
type ToolType = "mcp_server" | "agent_skill" | "database_script" | "api_function" | "coder_agent" | "prompt"
|
|
|
|
interface SyncedTool {
|
|
id: string
|
|
name: string
|
|
description?: string
|
|
tool_type: ToolType
|
|
is_mcp_server: boolean
|
|
is_agent_skill: boolean
|
|
is_database_script: boolean
|
|
request_type?: string
|
|
url?: string
|
|
tools: string[]
|
|
auth_via: string
|
|
interpolation_string?: string
|
|
goals?: string
|
|
temperature?: number
|
|
max_tokens?: number
|
|
llm_base_model?: string
|
|
llm_provider?: string
|
|
}
|
|
|
|
interface SyncedPrompt {
|
|
id: string
|
|
label: string
|
|
interpolation_string: string
|
|
prompt_type?: string
|
|
available_to_agents?: string[]
|
|
description?: string
|
|
}
|
|
|
|
interface ToolSyncResult {
|
|
success: boolean
|
|
tools: SyncedTool[]
|
|
prompts: SyncedPrompt[]
|
|
by_type: Record<string, number>
|
|
error?: string
|
|
}
|
|
|
|
interface CredentialValidationResult {
|
|
success: boolean
|
|
workspace_id?: string
|
|
workspace_name?: string
|
|
error?: string
|
|
}
|
|
|
|
const TFCODE_CONFIG_DIR = ".tfcode"
|
|
const TFCODE_TOOLS_FILE = "tools.json"
|
|
|
|
function getPythonSyncPath(): string {
|
|
// Check embedded python path first (for npm distribution)
|
|
const embedded = [
|
|
path.join(__dirname, "..", "..", "..", "..", "python"), // packages/tfcode/python
|
|
path.join(__dirname, "..", "..", "..", "python"), // dist/python
|
|
]
|
|
for (const p of embedded) {
|
|
if (existsSync(p)) return p
|
|
}
|
|
// Fallback to development paths
|
|
const dev = [
|
|
path.join(__dirname, "..", "..", "..", "..", "tf-sync", "src", "tf_sync"),
|
|
path.join(process.cwd(), "packages", "tf-sync", "src", "tf_sync"),
|
|
]
|
|
for (const p of dev) {
|
|
if (existsSync(p)) return p
|
|
}
|
|
return "tf_sync"
|
|
}
|
|
|
|
async function runPythonSync(method: string, args: Record<string, unknown> = {}): Promise<unknown> {
|
|
const credentials = await loadCredentials()
|
|
const pythonPath = getPythonSyncPath()
|
|
|
|
const pythonCode = `
|
|
import json
|
|
import sys
|
|
import os
|
|
|
|
try:
|
|
# Add embedded tf_sync module path
|
|
sys.path.insert(0, "${pythonPath.replace(/\\/g, "/")}")
|
|
from tf_sync.config import load_config, validate_credentials
|
|
from tf_sync.tools import sync_tools, sync_tools_by_type, ToolType
|
|
from tf_sync.mcp import sync_mcp_servers
|
|
|
|
method = ${JSON.stringify(method)}
|
|
args = ${JSON.stringify(args)}
|
|
|
|
if method == "validate":
|
|
config = load_config()
|
|
result = validate_credentials(config)
|
|
print(json.dumps({
|
|
"success": result.success,
|
|
"workspace_id": result.workspace_id,
|
|
"workspace_name": result.workspace_name,
|
|
"error": result.error
|
|
}))
|
|
|
|
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,
|
|
"is_mcp_server": tool.is_mcp_server,
|
|
"is_agent_skill": tool.is_agent_skill,
|
|
"is_database_script": tool.is_database_script,
|
|
"request_type": tool.request_type.value if tool.request_type else None,
|
|
"url": tool.url,
|
|
"tools": tool.tools,
|
|
"auth_via": tool.auth_via,
|
|
"interpolation_string": tool.interpolation_string,
|
|
"goals": tool.goals,
|
|
"temperature": tool.temperature,
|
|
"max_tokens": tool.max_tokens,
|
|
"llm_base_model": tool.llm_base_model,
|
|
"llm_provider": tool.llm_provider
|
|
})
|
|
|
|
prompts_data = []
|
|
for prompt in result.prompts:
|
|
prompts_data.append({
|
|
"id": prompt.id,
|
|
"label": prompt.label,
|
|
"interpolation_string": prompt.interpolation_string,
|
|
"prompt_type": prompt.prompt_type,
|
|
"available_to_agents": prompt.available_to_agents,
|
|
"description": prompt.description
|
|
})
|
|
|
|
print(json.dumps({
|
|
"success": result.success,
|
|
"tools": tools_data,
|
|
"prompts": prompts_data,
|
|
"by_type": result.by_type,
|
|
"error": result.error
|
|
}))
|
|
|
|
elif method == "sync_type":
|
|
tool_type_str = args.get("tool_type")
|
|
if tool_type_str:
|
|
tool_type_map = {
|
|
"mcp_server": ToolType.MCP_SERVER,
|
|
"agent_skill": ToolType.AGENT_SKILL,
|
|
"database_script": ToolType.DATABASE_SCRIPT,
|
|
"api_function": ToolType.API_FUNCTION
|
|
}
|
|
tool_type = tool_type_map.get(tool_type_str)
|
|
if tool_type:
|
|
config = load_config()
|
|
result = sync_tools_by_type(config, [tool_type])
|
|
|
|
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,
|
|
"is_mcp_server": tool.is_mcp_server,
|
|
"is_agent_skill": tool.is_agent_skill,
|
|
"is_database_script": tool.is_database_script,
|
|
"request_type": tool.request_type.value if tool.request_type else None,
|
|
"url": tool.url,
|
|
"tools": tool.tools,
|
|
"auth_via": tool.auth_via,
|
|
"interpolation_string": tool.interpolation_string,
|
|
"goals": tool.goals,
|
|
"temperature": tool.temperature,
|
|
"max_tokens": tool.max_tokens,
|
|
"llm_base_model": tool.llm_base_model,
|
|
"llm_provider": tool.llm_provider
|
|
})
|
|
|
|
print(json.dumps({
|
|
"success": result.success,
|
|
"tools": tools_data,
|
|
"by_type": result.by_type,
|
|
"error": result.error
|
|
}))
|
|
else:
|
|
print(json.dumps({"success": False, "error": "Missing tool_type argument"}))
|
|
|
|
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: getPythonSyncPath(),
|
|
TF_WORKSPACE_ID: credentials?.workspace_id || "",
|
|
TF_API_KEY: credentials?.api_key || "",
|
|
TF_REGION: credentials?.region || "au",
|
|
},
|
|
})
|
|
|
|
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 getConfigPath(): string {
|
|
return path.join(Global.Path.data, TFCODE_CONFIG_DIR)
|
|
}
|
|
|
|
function getToolsFilePath(): string {
|
|
return path.join(getConfigPath(), TFCODE_TOOLS_FILE)
|
|
}
|
|
|
|
function getCredentialsFilePath(): string {
|
|
return path.join(getConfigPath(), "credentials.json")
|
|
}
|
|
|
|
async function loadCredentials(): Promise<{ workspace_id: string; api_key: string; region: string } | null> {
|
|
const credFile = getCredentialsFilePath()
|
|
if (!(await Filesystem.exists(credFile))) {
|
|
return null
|
|
}
|
|
try {
|
|
const content = await Bun.file(credFile).text()
|
|
return JSON.parse(content)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
async function loadCachedTools(): Promise<ToolSyncResult | null> {
|
|
const toolsFile = getToolsFilePath()
|
|
if (!(await Filesystem.exists(toolsFile))) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
const content = await Bun.file(toolsFile).text()
|
|
return JSON.parse(content)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
async function saveToolsCache(result: ToolSyncResult): Promise<void> {
|
|
const configPath = getConfigPath()
|
|
await mkdir(configPath, { recursive: true })
|
|
await Bun.write(getToolsFilePath(), JSON.stringify(result, null, 2))
|
|
}
|
|
|
|
const ValidateCommand = cmd({
|
|
command: "validate",
|
|
describe: "validate ToothFairyAI credentials",
|
|
handler: async () => {
|
|
info("Validating ToothFairyAI credentials...")
|
|
|
|
try {
|
|
const result = (await runPythonSync("validate")) as CredentialValidationResult
|
|
|
|
if (result.success) {
|
|
success("✓ Credentials valid")
|
|
if (result.workspace_name) {
|
|
info(` Workspace: ${result.workspace_name}`)
|
|
}
|
|
if (result.workspace_id) {
|
|
info(` ID: ${result.workspace_id}`)
|
|
}
|
|
} else {
|
|
printError(`✗ Validation failed: ${result.error || "Unknown error"}`)
|
|
process.exitCode = 1
|
|
}
|
|
} catch (e) {
|
|
printError(`Failed to validate: ${e instanceof Error ? e.message : String(e)}`)
|
|
process.exitCode = 1
|
|
}
|
|
},
|
|
})
|
|
|
|
const SyncCommand = cmd({
|
|
command: "sync",
|
|
describe: "sync tools from ToothFairyAI workspace",
|
|
builder: (yargs) =>
|
|
yargs.option("force", {
|
|
alias: "f",
|
|
type: "boolean",
|
|
describe: "force re-sync",
|
|
default: false,
|
|
}),
|
|
handler: async (args) => {
|
|
info("Syncing tools from ToothFairyAI workspace...")
|
|
|
|
try {
|
|
const result = (await runPythonSync("sync")) as ToolSyncResult
|
|
|
|
if (result.success) {
|
|
await saveToolsCache(result)
|
|
|
|
success(`✓ Synced ${result.tools.length} tools`)
|
|
|
|
if (result.by_type && Object.keys(result.by_type).length > 0) {
|
|
info("\nBy type:")
|
|
for (const [type, count] of Object.entries(result.by_type)) {
|
|
info(` ${type}: ${count}`)
|
|
}
|
|
}
|
|
} else {
|
|
printError(`✗ Sync failed: ${result.error || "Unknown error"}`)
|
|
process.exitCode = 1
|
|
}
|
|
} catch (e) {
|
|
printError(`Failed to sync: ${e instanceof Error ? e.message : String(e)}`)
|
|
process.exitCode = 1
|
|
}
|
|
},
|
|
})
|
|
|
|
const ToolsListCommand = cmd({
|
|
command: "list",
|
|
describe: "list synced tools",
|
|
builder: (yargs) =>
|
|
yargs.option("type", {
|
|
type: "string",
|
|
choices: ["mcp", "skill", "database", "function"] as const,
|
|
describe: "filter by tool type",
|
|
}),
|
|
handler: async (args) => {
|
|
const cached = await loadCachedTools()
|
|
|
|
if (!cached || !cached.success) {
|
|
printError("No tools synced. Run 'tfcode sync' first.")
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
let tools = cached.tools
|
|
|
|
if (args.type) {
|
|
const typeMap: Record<string, ToolType> = {
|
|
mcp: "mcp_server",
|
|
skill: "agent_skill",
|
|
database: "database_script",
|
|
function: "api_function",
|
|
}
|
|
const targetType = typeMap[args.type]
|
|
tools = tools.filter((t) => t.tool_type === targetType)
|
|
}
|
|
|
|
if (tools.length === 0) {
|
|
info("No tools found.")
|
|
return
|
|
}
|
|
|
|
info(`\n${tools.length} tool(s):\n`)
|
|
|
|
for (const tool of tools) {
|
|
const typeLabel = {
|
|
mcp_server: "MCP",
|
|
agent_skill: "Skill",
|
|
database_script: "DB",
|
|
api_function: "API",
|
|
coder_agent: "Coder Agent",
|
|
prompt: "Prompt",
|
|
}[tool.tool_type]
|
|
|
|
info(` ${tool.name}`)
|
|
info(` Type: ${typeLabel}`)
|
|
if (tool.description) {
|
|
info(` Description: ${tool.description}`)
|
|
}
|
|
if (tool.url) {
|
|
info(` URL: ${tool.url}`)
|
|
}
|
|
info(` Auth: ${tool.auth_via}`)
|
|
info("")
|
|
}
|
|
},
|
|
})
|
|
|
|
const ToolsCredentialsSetCommand = cmd({
|
|
command: "credentials <name>",
|
|
describe: "manage tool credentials",
|
|
builder: (yargs) =>
|
|
yargs
|
|
.positional("name", {
|
|
type: "string",
|
|
describe: "tool name",
|
|
demandOption: true,
|
|
})
|
|
.option("set", {
|
|
type: "boolean",
|
|
describe: "set credential",
|
|
})
|
|
.option("show", {
|
|
type: "boolean",
|
|
describe: "show stored credential",
|
|
}),
|
|
handler: async (args) => {
|
|
const toolName = args.name as string
|
|
|
|
const cached = await loadCachedTools()
|
|
if (!cached || !cached.success) {
|
|
printError("No tools synced. Run 'tfcode sync' first.")
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
const tool = cached.tools.find((t) => t.name === toolName)
|
|
if (!tool) {
|
|
printError(`Tool '${toolName}' not found.`)
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
if (tool.auth_via !== "user_provided") {
|
|
printError(`Tool '${toolName}' uses tf_proxy authentication. Credentials are managed by ToothFairyAI.`)
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
const credentialsFile = path.join(getConfigPath(), "credentials.json")
|
|
let credentials: Record<string, string> = {}
|
|
|
|
if (await Filesystem.exists(credentialsFile)) {
|
|
try {
|
|
credentials = await Bun.file(credentialsFile).json()
|
|
} catch {}
|
|
}
|
|
|
|
if (args.show) {
|
|
const cred = credentials[toolName]
|
|
if (cred) {
|
|
info(`${toolName}: ${cred.substring(0, 8)}...${cred.substring(cred.length - 4)}`)
|
|
} else {
|
|
info(`No credential stored for '${toolName}'`)
|
|
}
|
|
return
|
|
}
|
|
|
|
if (args.set) {
|
|
const value = await prompts.password({
|
|
message: `Enter API key for '${toolName}'`,
|
|
})
|
|
|
|
if (prompts.isCancel(value)) {
|
|
printError("Cancelled")
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
credentials[toolName] = value as string
|
|
await mkdir(getConfigPath(), { recursive: true })
|
|
await Bun.write(credentialsFile, JSON.stringify(credentials, null, 2))
|
|
success(`✓ Credential saved for '${toolName}'`)
|
|
return
|
|
}
|
|
|
|
printError("Use --set or --show")
|
|
process.exitCode = 1
|
|
},
|
|
})
|
|
|
|
const ToolsCommand = cmd({
|
|
command: "tools",
|
|
describe: "manage ToothFairyAI tools",
|
|
builder: (yargs) => yargs.command(ToolsListCommand).command(ToolsCredentialsSetCommand).demandCommand(),
|
|
async handler() {},
|
|
})
|
|
|
|
const ToolsDebugCommand = cmd({
|
|
command: "debug <name>",
|
|
describe: "debug tool connection",
|
|
builder: (yargs) =>
|
|
yargs.positional("name", {
|
|
type: "string",
|
|
describe: "tool name",
|
|
demandOption: true,
|
|
}),
|
|
handler: async (args) => {
|
|
const toolName = args.name as string
|
|
|
|
const cached = await loadCachedTools()
|
|
if (!cached || !cached.success) {
|
|
printError("No tools synced. Run 'tfcode sync' first.")
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
const tool = cached.tools.find((t) => t.name === toolName)
|
|
if (!tool) {
|
|
printError(`Tool '${toolName}' not found.`)
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
info(`\nTool: ${tool.name}`)
|
|
info(`Type: ${tool.tool_type}`)
|
|
info(`Auth: ${tool.auth_via}`)
|
|
|
|
if (tool.url) {
|
|
info(`URL: ${tool.url}`)
|
|
}
|
|
|
|
if (tool.request_type) {
|
|
info(`Request Type: ${tool.request_type}`)
|
|
}
|
|
|
|
info("\nChecking configuration...")
|
|
|
|
const configPath = getConfigPath()
|
|
info(`Config dir: ${configPath}`)
|
|
|
|
const toolsFile = getToolsFilePath()
|
|
info(`Tools cache: ${toolsFile}`)
|
|
info(` Exists: ${await Filesystem.exists(toolsFile)}`)
|
|
|
|
if (tool.auth_via === "user_provided") {
|
|
const credentialsFile = path.join(configPath, "credentials.json")
|
|
info(`Credentials file: ${credentialsFile}`)
|
|
info(` Exists: ${await Filesystem.exists(credentialsFile)}`)
|
|
|
|
if (await Filesystem.exists(credentialsFile)) {
|
|
const credentials = await Bun.file(credentialsFile).json()
|
|
info(` Has credential for '${toolName}': ${!!credentials[toolName]}`)
|
|
}
|
|
}
|
|
},
|
|
})
|
|
|
|
const ToolsTestCommand = cmd({
|
|
command: "test <name>",
|
|
describe: "test tool call",
|
|
builder: (yargs) =>
|
|
yargs.positional("name", {
|
|
type: "string",
|
|
describe: "tool name",
|
|
demandOption: true,
|
|
}),
|
|
handler: async (args) => {
|
|
const toolName = args.name as string
|
|
|
|
const cached = await loadCachedTools()
|
|
if (!cached || !cached.success) {
|
|
printError("No tools synced. Run 'tfcode sync' first.")
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
const tool = cached.tools.find((t) => t.name === toolName)
|
|
if (!tool) {
|
|
printError(`Tool '${toolName}' not found.`)
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
info(`Testing tool '${toolName}'...`)
|
|
info(`This feature is not yet implemented.`)
|
|
info(`Tool type: ${tool.tool_type}`)
|
|
info(`Authentication: ${tool.auth_via}`)
|
|
process.exitCode = 1
|
|
},
|
|
})
|
|
|
|
export const ToolsMainCommand = cmd({
|
|
command: "tools",
|
|
describe: "manage ToothFairyAI tools",
|
|
builder: (yargs) =>
|
|
yargs
|
|
.command(ToolsListCommand)
|
|
.command(ToolsCredentialsSetCommand)
|
|
.command(ToolsDebugCommand)
|
|
.command(ToolsTestCommand)
|
|
.demandCommand(),
|
|
async handler() {},
|
|
})
|
|
|
|
export const SetupCommand = cmd({
|
|
command: "setup",
|
|
describe: "configure ToothFairyAI credentials",
|
|
builder: (yargs) =>
|
|
yargs
|
|
.option("api-key", {
|
|
type: "string",
|
|
describe: "API key",
|
|
})
|
|
.option("workspace-id", {
|
|
type: "string",
|
|
describe: "Workspace ID",
|
|
})
|
|
.option("region", {
|
|
type: "string",
|
|
describe: "Region (dev, au, eu, us)",
|
|
default: "au",
|
|
}),
|
|
handler: async (args) => {
|
|
const configPath = getConfigPath()
|
|
const credFile = getCredentialsFilePath()
|
|
|
|
// Load existing credentials
|
|
let existing: { api_key?: string; workspace_id?: string; region?: string } = {}
|
|
if (await Filesystem.exists(credFile)) {
|
|
try {
|
|
existing = await Bun.file(credFile).json()
|
|
} catch {}
|
|
}
|
|
|
|
// Get credentials from args or prompt
|
|
let apiKey = args["api-key"]
|
|
let workspaceId = args["workspace-id"]
|
|
let region = args.region || existing.region || "au"
|
|
|
|
if (!apiKey || !workspaceId) {
|
|
info("")
|
|
info("ToothFairyAI Credentials Setup")
|
|
info("━".repeat(40))
|
|
info("")
|
|
|
|
if (!workspaceId) {
|
|
process.stdout.write(`Workspace ID [${existing.workspace_id || ""}]: `)
|
|
const input = await new Promise<string>((resolve) => {
|
|
process.stdin.once("data", (data) => resolve(data.toString().trim()))
|
|
})
|
|
workspaceId = input || existing.workspace_id
|
|
}
|
|
|
|
if (!apiKey) {
|
|
process.stdout.write(`API Key [${existing.api_key ? "****" + existing.api_key.slice(-4) : ""}]: `)
|
|
const input = await new Promise<string>((resolve) => {
|
|
process.stdin.once("data", (data) => resolve(data.toString().trim()))
|
|
})
|
|
apiKey = input || existing.api_key
|
|
}
|
|
|
|
process.stdout.write(`Region [${region}]: `)
|
|
const regionInput = await new Promise<string>((resolve) => {
|
|
process.stdin.once("data", (data) => resolve(data.toString().trim()))
|
|
})
|
|
if (regionInput) region = regionInput
|
|
}
|
|
|
|
if (!apiKey || !workspaceId) {
|
|
printError("API key and workspace ID are required")
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
// Save credentials
|
|
await mkdir(configPath, { recursive: true })
|
|
await Bun.write(
|
|
credFile,
|
|
JSON.stringify(
|
|
{
|
|
api_key: apiKey,
|
|
workspace_id: workspaceId,
|
|
region,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
)
|
|
|
|
success(`✓ Credentials saved to ${credFile}`)
|
|
info(` Workspace: ${workspaceId}`)
|
|
info(` Region: ${region}`)
|
|
info("")
|
|
info("Run 'tfcode validate' to test your credentials")
|
|
},
|
|
})
|
|
|
|
export { ValidateCommand, SyncCommand, ToolsCommand }
|