UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

209 lines (188 loc) 6.07 kB
import { existsSync, readFileSync } from "node:fs"; import { createRequire } from "node:module"; import { join } from "node:path"; import { $inject, Alepha, AlephaError } from "alepha"; import { EnvUtils } from "alepha/command"; import { $logger } from "alepha/logger"; import { FileSystemProvider, ShellProvider } from "alepha/system"; import { type AppEntry, AppEntryProvider, } from "../providers/AppEntryProvider.ts"; import { ViteUtils } from "./ViteUtils.ts"; /** * Core utility service for CLI commands. * * Provides: * - Command execution * - File editing helpers * - Drizzle/ORM utilities * - Environment loading */ export class AlephaCliUtils { protected readonly log = $logger(); protected readonly fs = $inject(FileSystemProvider); protected readonly envUtils = $inject(EnvUtils); protected readonly boot = $inject(AppEntryProvider); protected readonly shell = $inject(ShellProvider); protected readonly viteUtils = $inject(ViteUtils); protected readonly alepha = $inject(Alepha); // =========================================== // Command Execution // =========================================== /** * Execute a command with inherited stdio. */ public async exec( command: string, options: { root?: string; env?: Record<string, string>; global?: boolean; capture?: boolean; } = {}, ): Promise<void> { await this.shell.run(command, { root: options.root, env: options.env, resolve: !options.global, capture: options.capture, }); } /** * Resolve the absolute path to a toolchain binary that ships embedded in * `alepha`'s own `dependencies` (typescript, vite, vitest, @biomejs/biome, * drizzle-kit). * * The CLI runs the result via `node <path>` so the toolchain works under * every package manager — including pnpm with a strict node-linker, where * a transitive dependency's bin is NOT hoisted into the project's * `node_modules/.bin`. Resolution starts from `alepha`'s own location, so * the version is whatever `alepha` shipped — the project never pins it. * * @param pkg - npm package name (e.g. `"typescript"`) * @param binName - which `bin` entry to use (e.g. `"tsc"`); defaults to the * package's only/first bin */ public resolveBin(pkg: string, binName?: string): string { const require = createRequire(import.meta.url); // Locate the package root by scanning the `node_modules` directories // Node would search, then reading `package.json` from disk directly. // We deliberately avoid `require.resolve("<pkg>/package.json")` — a // strict `exports` map (e.g. drizzle-kit) blocks that subpath. let pkgDir: string | undefined; for (const nm of require.resolve.paths(pkg) ?? []) { const candidate = join(nm, pkg); if (existsSync(join(candidate, "package.json"))) { pkgDir = candidate; break; } } if (!pkgDir) { throw new AlephaError( `Cannot locate package '${pkg}' — is it installed alongside alepha?`, ); } const meta = JSON.parse( readFileSync(join(pkgDir, "package.json"), "utf8"), ) as { bin?: string | Record<string, string> }; const bin = meta.bin; if (!bin) { throw new AlephaError(`Package '${pkg}' declares no 'bin' entry`); } const rel = typeof bin === "string" ? bin : (bin[binName ?? pkg] ?? Object.values(bin)[0]); if (!rel) { throw new AlephaError(`Package '${pkg}' has no bin named '${binName}'`); } return join(pkgDir, rel); } /** * Write a configuration file to node_modules/.alepha directory. */ public async writeConfigFile( name: string, content: string, root = process.cwd(), ): Promise<string> { const dir = this.fs.join(root, "node_modules", ".alepha"); await this.fs.mkdir(dir, { recursive: true }).catch(() => null); const path = this.fs.join(dir, name); await this.fs.writeFile(path, content); this.log.debug(`Config file written: ${path}`); return path; } public async loadAlephaFromServerEntryFile( opts: { mode: "production" | "development"; } & ({ entry: AppEntry } | { root: string }), ): Promise<Alepha> { let entry: AppEntry; if ("root" in opts) { entry = await this.boot.getAppEntry(opts.root); } else { entry = opts.entry; } return await this.viteUtils.runAlepha({ entry, mode: opts.mode, }); } // =========================================== // Environment // =========================================== /** * Load environment variables from a .env file. */ public async loadEnv( root: string, files: string[] = [".env"], ): Promise<void> { await this.envUtils.loadEnv(root, files); } // =========================================== // Helpers // =========================================== public async exists(root: string, path: string): Promise<boolean> { return this.fs.exists(this.fs.join(root, path)); } /** * Check if a command is installed and available in the system PATH. */ public isInstalledAsync(cmd: string): Promise<boolean> { return this.shell.isInstalled(cmd); } /** * Get the current git revision (commit SHA). * * @returns The short commit SHA or "unknown" if not in a git repo */ public async getGitRevision(): Promise<string> { try { const result = await this.shell.run("git rev-parse --short HEAD", { capture: true, }); return result.trim(); } catch { return "unknown"; } } /** * Get the user's email from git config. * * @returns The git user email or undefined if not configured */ public async getGitEmail(): Promise<string | undefined> { try { const result = await this.shell.run("git config user.email", { capture: true, }); const email = result.trim(); return email || undefined; } catch { return undefined; } } }