From 363921887998ff87706787966447e183256a9f7c Mon Sep 17 00:00:00 2001 From: Gab Date: Wed, 15 Apr 2026 13:57:44 +1000 Subject: [PATCH] =?UTF-8?q?fix:=20tfcode=20web=20fails=20after=20npm=20ins?= =?UTF-8?q?tall=20=E2=80=94=20copy=20app=20dist=20in=20postinstall?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web command crashed because postinstall scripts never copied the app/dist directory from the platform package. Added copyAppDir() to both postinstall scripts, multi-path resolution in findAppDir() and server static serving, and updated branding to use local favicon. Co-Authored-By: Claude Opus 4.6 (1M context) --- bun.lock | 2 +- packages/app/index.html | 5 +- packages/app/public/favicon.png | Bin 0 -> 3097 bytes packages/app/src/entry.tsx | 2 +- .../app/src/pages/layout/sidebar-items.tsx | 4 +- packages/tfcode/package.json | 3 +- packages/tfcode/script/build-tfcode.ts | 4 + packages/tfcode/script/postinstall-tfcode.mjs | 19 ++ packages/tfcode/scripts/postinstall.cjs | 284 +++++++++++------- packages/tfcode/src/cli/cmd/serve.ts | 3 - packages/tfcode/src/cli/cmd/web.ts | 20 +- packages/tfcode/src/server/server.ts | 12 +- packages/ui/src/assets/favicon.png | Bin 0 -> 3097 bytes packages/ui/src/components/logo.css | 11 +- packages/ui/src/components/logo.tsx | 57 ++-- 15 files changed, 251 insertions(+), 175 deletions(-) create mode 100755 packages/app/public/favicon.png create mode 100755 packages/ui/src/assets/favicon.png 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 0000000000000000000000000000000000000000..c640e54042fde2da3337a368ad754c65923f2957 GIT binary patch literal 3097 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 { 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 0000000000000000000000000000000000000000..c640e54042fde2da3337a368ad754c65923f2957 GIT binary patch literal 3097 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 { 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} + /> ) }