diff --git a/bun.lock b/bun.lock index a93a90f1a..71376a901 100644 --- a/bun.lock +++ b/bun.lock @@ -381,7 +381,7 @@ }, "packages/tfcode": { "name": "@toothfairyai/tfcode", - "version": "1.0.33", + "version": "1.0.36", "bin": { "tfcode": "./bin/tfcode", }, diff --git a/packages/app/index.html b/packages/app/index.html index 8a79aed2d..70d9f1f18 100644 --- a/packages/app/index.html +++ b/packages/app/index.html @@ -4,10 +4,7 @@ TF Code - - - - + diff --git a/packages/app/public/favicon.png b/packages/app/public/favicon.png new file mode 100755 index 000000000..c640e5404 Binary files /dev/null and b/packages/app/public/favicon.png differ diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index b5cbed6e7..c4abdb33b 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -67,7 +67,7 @@ const notify: Platform["notify"] = async (title, description, href) => { const notification = new Notification(title, { body: description ?? "", - icon: "https://opencode.ai/favicon-96x96-v3.png", + icon: "/favicon.png", }) notification.onclick = () => { diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index a9627c5db..9226a36ab 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -43,9 +43,7 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
= 3 && minor >= 10) { - return { cmd, version: result.trim() }; + return { cmd, version: result.trim() } } } } catch {} } - - return null; + + return null } function installPythonDeps(pythonCmd) { return new Promise((resolve, reject) => { - const packages = ['toothfairyai', 'pydantic', 'httpx', 'rich']; - - log(`${DIM}Installing Python packages: ${packages.join(', ')}...${RESET}`); - + const packages = ["toothfairyai", "pydantic", "httpx", "rich"] + + log(`${DIM}Installing Python packages: ${packages.join(", ")}...${RESET}`) + // Try with --user first, then --break-system-packages if needed - const args = ['-m', 'pip', 'install', '--user', ...packages]; - + const args = ["-m", "pip", "install", "--user", ...packages] + const proc = spawn(pythonCmd, args, { - stdio: 'inherit', - shell: process.platform === 'win32' - }); - - proc.on('close', (code) => { + stdio: "inherit", + shell: process.platform === "win32", + }) + + proc.on("close", (code) => { if (code === 0) { - resolve(); + resolve() } else { // Try with --break-system-packages flag - log(`${DIM}Retrying with --break-system-packages...${RESET}`); - const retryArgs = ['-m', 'pip', 'install', '--user', '--break-system-packages', ...packages]; + log(`${DIM}Retrying with --break-system-packages...${RESET}`) + const retryArgs = ["-m", "pip", "install", "--user", "--break-system-packages", ...packages] const retry = spawn(pythonCmd, retryArgs, { - stdio: 'inherit', - shell: process.platform === 'win32' - }); - - retry.on('close', (retryCode) => { + stdio: "inherit", + shell: process.platform === "win32", + }) + + retry.on("close", (retryCode) => { if (retryCode === 0) { - resolve(); + resolve() } else { - reject(new Error(`pip install exited with code ${retryCode}`)); + reject(new Error(`pip install exited with code ${retryCode}`)) } - }); - - retry.on('error', (err) => { - reject(err); - }); + }) + + retry.on("error", (err) => { + reject(err) + }) } - }); - - proc.on('error', (err) => { - reject(err); - }); - }); + }) + + proc.on("error", (err) => { + reject(err) + }) + }) +} + +function copyAppDir() { + let platform + switch (process.platform) { + case "darwin": + platform = "darwin" + break + case "linux": + platform = "linux" + break + case "win32": + platform = "windows" + break + default: + platform = process.platform + } + + let arch + switch (process.arch) { + case "x64": + arch = "x64" + break + case "arm64": + arch = "arm64" + break + default: + arch = process.arch + } + + const pkgName = `@toothfairyai/tfcode-${platform}-${arch}` + const appDir = path.join(__dirname, "app") + + if (fs.existsSync(path.join(appDir, "dist", "index.html"))) { + logSuccess("Web app already present") + return + } + + try { + const pkgDir = path.dirname(require.resolve(`${pkgName}/package.json`)) + const srcAppDir = path.join(pkgDir, "bin", "app") + + if (!fs.existsSync(path.join(srcAppDir, "dist", "index.html"))) { + logWarning("Web app dist not found in platform package") + return + } + + fs.cpSync(srcAppDir, appDir, { recursive: true }) + logSuccess("Web app copied from platform package") + } catch (e) { + logWarning(`Could not copy web app: ${e.message}`) + } } async function main() { - log(''); - log(`${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`); - log(`${BOLD} tfcode - ToothFairyAI's official coding agent${RESET}`); - log(`${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`); - log(''); - + log("") + log(`${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`) + log(`${BOLD} tfcode - ToothFairyAI's official coding agent${RESET}`) + log(`${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`) + log("") + + copyAppDir() + // Check for Python - logInfo('Checking Python installation...'); - const python = checkPython(); - + logInfo("Checking Python installation...") + const python = checkPython() + if (!python) { - log(''); - logError('Python 3.10+ is required but not found.'); - log(''); - log(`${BOLD}Please install Python 3.10 or later:${RESET}`); - log(''); - log(` ${CYAN}macOS:${RESET} brew install python@3.12`); - log(` ${CYAN}Ubuntu:${RESET} sudo apt-get install python3.12`); - log(` ${CYAN}Windows:${RESET} Download from https://python.org/downloads`); - log(''); - log(`${DIM}After installing Python, run: npm rebuild tfcode${RESET}`); - log(''); - process.exit(1); + log("") + logError("Python 3.10+ is required but not found.") + log("") + log(`${BOLD}Please install Python 3.10 or later:${RESET}`) + log("") + log(` ${CYAN}macOS:${RESET} brew install python@3.12`) + log(` ${CYAN}Ubuntu:${RESET} sudo apt-get install python3.12`) + log(` ${CYAN}Windows:${RESET} Download from https://python.org/downloads`) + log("") + log(`${DIM}After installing Python, run: npm rebuild tfcode${RESET}`) + log("") + process.exit(1) } - - logSuccess(`Found ${python.version} (${python.cmd})`); - log(''); - + + logSuccess(`Found ${python.version} (${python.cmd})`) + log("") + // Install Python dependencies - logInfo('Installing ToothFairyAI Python SDK...'); + logInfo("Installing ToothFairyAI Python SDK...") try { - await installPythonDeps(python.cmd); - logSuccess('Python dependencies installed'); + await installPythonDeps(python.cmd) + logSuccess("Python dependencies installed") } catch (err) { - logWarning(`Failed to install Python dependencies: ${err.message}`); - log(''); - log(`${DIM}You can install them manually with:${RESET}`); - log(` ${CYAN}${python.cmd} -m pip install toothfairyai pydantic httpx rich${RESET}`); - log(''); + logWarning(`Failed to install Python dependencies: ${err.message}`) + log("") + log(`${DIM}You can install them manually with:${RESET}`) + log(` ${CYAN}${python.cmd} -m pip install toothfairyai pydantic httpx rich${RESET}`) + log("") // Don't exit - user might install manually later } - - log(''); - log(`${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`); - log(`${GREEN}✓ tfcode installed successfully!${RESET}`); - log(`${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`); - log(''); - log(`${BOLD}Quick Start:${RESET}`); - log(''); - log(` ${CYAN}1.${RESET} Set your ToothFairyAI credentials:`); - log(` ${DIM}export TF_WORKSPACE_ID="your-workspace-id"${RESET}`); - log(` ${DIM}export TF_API_KEY="your-api-key"${RESET}`); - log(''); - log(` ${CYAN}2.${RESET} Validate your credentials:`); - log(` ${DIM}tfcode validate${RESET}`); - log(''); - log(` ${CYAN}3.${RESET} Sync tools from your workspace:`); - log(` ${DIM}tfcode sync${RESET}`); - log(''); - log(` ${CYAN}4.${RESET} Start coding:`); - log(` ${DIM}tfcode${RESET}`); - log(''); - log(`${DIM}Documentation: https://toothfairyai.com/developers/tfcode${RESET}`); - log(''); + + log("") + log(`${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`) + log(`${GREEN}✓ tfcode installed successfully!${RESET}`) + log(`${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`) + log("") + log(`${BOLD}Quick Start:${RESET}`) + log("") + log(` ${CYAN}1.${RESET} Set your ToothFairyAI credentials:`) + log(` ${DIM}export TF_WORKSPACE_ID="your-workspace-id"${RESET}`) + log(` ${DIM}export TF_API_KEY="your-api-key"${RESET}`) + log("") + log(` ${CYAN}2.${RESET} Validate your credentials:`) + log(` ${DIM}tfcode validate${RESET}`) + log("") + log(` ${CYAN}3.${RESET} Sync tools from your workspace:`) + log(` ${DIM}tfcode sync${RESET}`) + log("") + log(` ${CYAN}4.${RESET} Start coding:`) + log(` ${DIM}tfcode${RESET}`) + log("") + log(`${DIM}Documentation: https://toothfairyai.com/developers/tfcode${RESET}`) + log("") } main().catch((err) => { - logError(`Installation failed: ${err.message}`); - process.exit(1); -}); \ No newline at end of file + logError(`Installation failed: ${err.message}`) + process.exit(1) +}) diff --git a/packages/tfcode/src/cli/cmd/serve.ts b/packages/tfcode/src/cli/cmd/serve.ts index 13a10d664..7568d46a5 100644 --- a/packages/tfcode/src/cli/cmd/serve.ts +++ b/packages/tfcode/src/cli/cmd/serve.ts @@ -11,9 +11,6 @@ export const ServeCommand = cmd({ builder: (yargs) => withNetworkOptions(yargs), describe: "starts a headless tfcode server", handler: async (args) => { - if (!Flag.OPENCODE_SERVER_PASSWORD) { - console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") - } const opts = await resolveNetworkOptions(args) const server = Server.listen(opts) console.log(`tfcode server listening on http://${server.hostname}:${server.port}`) diff --git a/packages/tfcode/src/cli/cmd/web.ts b/packages/tfcode/src/cli/cmd/web.ts index 68cf569cb..a78c87365 100644 --- a/packages/tfcode/src/cli/cmd/web.ts +++ b/packages/tfcode/src/cli/cmd/web.ts @@ -27,8 +27,22 @@ function getNetworkIPs() { return results } +function findAppDir(): string { + const binDir = path.dirname(process.execPath) + const candidates = [ + path.join(binDir, "app"), + path.join(binDir, "..", "app"), + path.join(import.meta.dirname, "..", "app"), + path.join(import.meta.dirname, "..", "..", "..", "..", "app"), + ] + for (const dir of candidates) { + if (existsSync(path.join(dir, "dist", "index.html"))) return dir + } + return candidates[0] +} + async function ensureAppBuilt() { - const appDir = path.join(import.meta.dirname, "..", "..", "..", "..", "app") + const appDir = findAppDir() const distDir = path.join(appDir, "dist") if (!existsSync(distDir) || !existsSync(path.join(distDir, "index.html"))) { @@ -44,10 +58,6 @@ export const WebCommand = cmd({ builder: (yargs) => withNetworkOptions(yargs), describe: "start tfcode server and open web interface", handler: async (args) => { - if (!Flag.OPENCODE_SERVER_PASSWORD) { - UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") - } - await ensureAppBuilt() const opts = await resolveNetworkOptions(args) diff --git a/packages/tfcode/src/server/server.ts b/packages/tfcode/src/server/server.ts index 8cd43dbca..82ca4c3ce 100644 --- a/packages/tfcode/src/server/server.ts +++ b/packages/tfcode/src/server/server.ts @@ -5,6 +5,7 @@ import { cors } from "hono/cors" import { basicAuth } from "hono/basic-auth" import z from "zod" import path from "path" +import { existsSync } from "fs" import { Provider } from "../provider/provider" import { NamedError } from "@opencode-ai/util/error" import { LSP } from "../lsp" @@ -530,8 +531,15 @@ export namespace Server { ) .all("/*", async (c) => { const reqPath = c.req.path === "/" ? "/index.html" : c.req.path - const appDist = path.join(import.meta.dirname, "..", "..", "..", "app", "dist") - const publicDir = path.join(import.meta.dirname, "..", "..", "..", "app", "public") + const binDir = path.dirname(process.execPath) + const appDistCandidates = [ + path.join(binDir, "app", "dist"), + path.join(binDir, "..", "app", "dist"), + path.join(import.meta.dirname, "..", "app", "dist"), + path.join(import.meta.dirname, "..", "..", "..", "app", "dist"), + ] + const appDist = appDistCandidates.find((d) => existsSync(path.join(d, "index.html"))) ?? appDistCandidates[0] + const publicDir = path.join(appDist, "..", "public") let filePath = path.join(appDist, reqPath) let file = Bun.file(filePath) diff --git a/packages/ui/src/assets/favicon.png b/packages/ui/src/assets/favicon.png new file mode 100755 index 000000000..c640e5404 Binary files /dev/null and b/packages/ui/src/assets/favicon.png differ diff --git a/packages/ui/src/components/logo.css b/packages/ui/src/components/logo.css index a909782b7..fa5428c8e 100644 --- a/packages/ui/src/components/logo.css +++ b/packages/ui/src/components/logo.css @@ -1,4 +1,13 @@ [data-component="logo-mark"] { width: 16px; - aspect-ratio: 4/5; + aspect-ratio: 137/120; + object-fit: contain; +} + +[data-component="logo-splash"] { + object-fit: contain; +} + +[data-component="logo-full"] { + object-fit: contain; } diff --git a/packages/ui/src/components/logo.tsx b/packages/ui/src/components/logo.tsx index 14b437a5e..a85976f2d 100644 --- a/packages/ui/src/components/logo.tsx +++ b/packages/ui/src/components/logo.tsx @@ -1,60 +1,39 @@ import { ComponentProps } from "solid-js" +import tfIcon from "../assets/favicon.png" export const Mark = (props: { class?: string }) => { return ( - - - - + src={tfIcon} + alt="ToothFairyAI" + draggable={false} + /> ) } -export const Splash = (props: Pick, "ref" | "class">) => { +export const Splash = (props: Pick, "ref" | "class">) => { return ( - - - - + src={tfIcon} + alt="ToothFairyAI" + draggable={false} + /> ) } export const Logo = (props: { class?: string }) => { return ( - - - - - - - - Code - - - + src={tfIcon} + alt="ToothFairyAI" + draggable={false} + /> ) }