#!/usr/bin/env node import chalk from "chalk"; import chokidar from "chokidar"; import confirm from "@inquirer/confirm"; import { argv } from "node:process"; import { getPortPromise } from "portfinder"; import open from "open"; import type { Stats } from "node:fs"; import { findEntryPoints, loadConfig, optimizeImages, clearLogFile } from "./utils.js"; import { writeAssets } from "./builder.js"; import { initWs, printCompilationDetails } from "./server.js"; import { makeSkeleton, runNpmInstall, parseGhostCliOutput, checkTheme, zipTheme, symLinkTheme, lintTheme, deployTheme, runDoctor, } from "./cli.js"; function ignored(path: string, stats?: Stats): boolean { if (path !== "." && /(^|[/\\])\../.test(path)) return true; const lowerPath = path.toLowerCase(); if ( lowerPath.includes("node_modules") || lowerPath.includes("dist") || lowerPath.includes("assets/built") || lowerPath.includes("assets\\built") || lowerPath.includes("postcss.config.js") ) { return true; } if (stats?.isFile()) { return !/\.(hbs|css|js|ts|jpg|jpeg|png|webp|avif|svg|woff|woff2|ttf|otf)$/i.test(path); } if (/\.[^/\\]+$/.test(path)) { return !/\.(hbs|css|js|ts)$/i.test(path); } return false; } async function init() { clearLogFile(); const handleExit = () => { clearLogFile(); process.exit(0); }; process.on("SIGINT", handleExit); process.on("SIGTERM", handleExit); const config = await loadConfig(); const command = argv[2]; if (argv.includes("--help") || argv.includes("-h") || !command) { const { readFile } = await import("node:fs/promises"); const pkg = JSON.parse( await readFile(new URL("../package.json", import.meta.url), "utf-8"), ); console.log(chalk.bold(`\n gtb - Ghost Theme Builder v${pkg.version}`)); console.log(`\n ${chalk.bold("USAGE")}`); console.log(` gtb [options]`); console.log(`\n ${chalk.bold("COMMANDS")}`); console.log( ` dev Start development server with live reload (default)`, ); console.log( ` init Scaffold a new theme and link to local Ghost`, ); console.log(` build Build theme for production`); console.log(` check Run theme validation using gscan`); console.log(` lint Run ESLint and Stylelint`); console.log(` zip Package theme into a zip file for upload`); console.log(` doctor Check system for common Ghost/gtb issues`); console.log(`\n ${chalk.bold("OPTIONS")}`); console.log(` --watch, -w Watch for changes (implied in 'dev')`); console.log(` --open, -o Open local Ghost in browser on start`); console.log(` --help, -h Show this help message`); console.log(""); return; } if (command === "init" || argv.includes("--init")) { await makeSkeleton(); const linked = await symLinkTheme(); console.log(chalk.green("⬥"), " Theme skeleton created successfully."); const installDeps = await confirm({ message: "Would you like to install dependencies now?", default: true, }); if (installDeps) { await runNpmInstall(); } if (linked) { console.log(chalk.green("⬥"), " Theme linked successfully."); } else { console.log(chalk.yellow("⬥"), " Skipping theme linking."); } return; } if (command === "check") { console.log(chalk.blue("⬥"), " Running theme validation (gscan)..."); await checkTheme(); return; } if (command === "lint") { await lintTheme(); return; } if (command === "zip") { console.log(chalk.blue("⬥"), " Packaging theme for production..."); await zipTheme(); return; } if (command === "deploy") { await deployTheme(); return; } if (command === "doctor") { await runDoctor(); return; } const isWatch = argv.includes("--watch") || command === "dev"; const jsEntryPoints = await findEntryPoints("src/js", [".ts", ".js"]); const cssEntryPoints = await findEntryPoints("src/css", [".css"]); const port = isWatch ? await getPortPromise({ port: 3000 }) : 0; const res = await writeAssets( [...jsEntryPoints, ...cssEntryPoints], port, isWatch, config, ); if (isWatch) { console.log(chalk.blue("⬥"), " Optimizing images..."); try { await optimizeImages(process.cwd()); } catch (e) { console.log(chalk.yellow("┃"), " Image optimization skipped or failed."); } const url = await parseGhostCliOutput(); const watcher = chokidar.watch(".", { ignored, ignoreInitial: true, }); await initWs(res, port, url, watcher, config); if (argv.includes("--open")) { await open(url); } } else { printCompilationDetails(res); console.log(chalk.blue("⬥"), " Optimizing images..."); try { await optimizeImages(process.cwd()); } catch (e) { console.log(chalk.yellow("┃"), " Image optimization skipped or failed."); } console.log(chalk.green("⬥"), " Files built successfully. Exiting..."); process.exit(0); } } init().catch((err) => { console.error(err); process.exit(1); });