fix: tfcode web fails after npm install — copy app dist in postinstall

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) <noreply@anthropic.com>
This commit is contained in:
Gab
2026-04-15 13:57:44 +10:00
parent c3e504f036
commit 3639218879
15 changed files with 251 additions and 175 deletions

View File

@@ -1,11 +1,12 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.33",
"version": "1.0.36",
"name": "@toothfairyai/tfcode",
"type": "module",
"license": "MIT",
"files": [
"bin",
"app",
"python",
"postinstall.mjs",
"LICENSE"

View File

@@ -164,6 +164,10 @@ for (const item of targets) {
}
await $`rm -rf ./dist/${name}/bin/tui`
const appDistDir = path.resolve(dir, "../app/dist")
if (fs.existsSync(appDistDir)) {
fs.cpSync(appDistDir, path.join(dir, "dist", name, "bin", "app", "dist"), { recursive: true })
}
await Bun.file(`dist/${name}/package.json`).write(
JSON.stringify(
{

View File

@@ -181,6 +181,15 @@ async function downloadBinary() {
console.log(`Installed tfcode to ${targetBinary}`)
}
// Move app directory
const extractedAppDir = path.join(tmpDir, "app")
const targetAppDir = path.join(binDir, "app")
if (fs.existsSync(path.join(extractedAppDir, "dist", "index.html"))) {
if (fs.existsSync(targetAppDir)) fs.rmSync(targetAppDir, { recursive: true, force: true })
fs.cpSync(extractedAppDir, targetAppDir, { recursive: true })
console.log("Installed web app")
}
// Cleanup
fs.rmSync(tmpDir, { recursive: true, force: true })
}
@@ -202,6 +211,16 @@ function setupBinary(sourcePath, platform) {
fs.chmodSync(targetBinary, 0o755)
console.log(`tfcode installed to ${targetBinary}`)
// Also copy app dir from platform package
const sourceDir = path.dirname(sourcePath)
const srcAppDir = path.join(sourceDir, "app")
const targetAppDir = path.join(binDir, "app")
if (fs.existsSync(path.join(srcAppDir, "dist", "index.html"))) {
if (fs.existsSync(targetAppDir)) fs.rmSync(targetAppDir, { recursive: true, force: true })
fs.cpSync(srcAppDir, targetAppDir, { recursive: true })
console.log("Web app copied from platform package")
}
}
async function main() {

View File

@@ -1,171 +1,225 @@
#!/usr/bin/env node
const { spawn, execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const { spawn, execSync } = require("child_process")
const fs = require("fs")
const path = require("path")
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
const GREEN = '\x1b[32m';
const YELLOW = '\x1b[33m';
const RED = '\x1b[31m';
const CYAN = '\x1b[36m';
const DIM = '\x1b[90m';
const RESET = "\x1b[0m"
const BOLD = "\x1b[1m"
const GREEN = "\x1b[32m"
const YELLOW = "\x1b[33m"
const RED = "\x1b[31m"
const CYAN = "\x1b[36m"
const DIM = "\x1b[90m"
function log(msg) {
console.log(msg);
console.log(msg)
}
function logSuccess(msg) {
console.log(`${GREEN}${RESET} ${msg}`);
console.log(`${GREEN}${RESET} ${msg}`)
}
function logError(msg) {
console.error(`${RED}${RESET} ${msg}`);
console.error(`${RED}${RESET} ${msg}`)
}
function logInfo(msg) {
console.log(`${CYAN}${RESET} ${msg}`);
console.log(`${CYAN}${RESET} ${msg}`)
}
function logWarning(msg) {
console.log(`${YELLOW}!${RESET} ${msg}`);
console.log(`${YELLOW}!${RESET} ${msg}`)
}
function checkPython() {
const commands = ['python3', 'python'];
const commands = ["python3", "python"]
for (const cmd of commands) {
try {
const result = execSync(`${cmd} --version`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
const match = result.match(/Python (\d+)\.(\d+)/);
const result = execSync(`${cmd} --version`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
const match = result.match(/Python (\d+)\.(\d+)/)
if (match) {
const major = parseInt(match[1]);
const minor = parseInt(match[2]);
const major = parseInt(match[1])
const minor = parseInt(match[2])
if (major >= 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);
});
logError(`Installation failed: ${err.message}`)
process.exit(1)
})

View File

@@ -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}`)

View File

@@ -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)

View File

@@ -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)