#!/usr/bin/env node import { accessSync } from "node:fs" import { access, copyFile, mkdir, readFile, writeFile } from "node:fs/promises" import path from "node:path" import process from "node:process" import { fileURLToPath } from "node:url" const command = process.argv[2] const args = process.argv.slice(3) const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") const bundledManifestPath = path.join(packageRoot, "manifest.json") const configFileName = "base-ui-tailwind.json" main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)) process.exit(1) }) async function main() { if (!command || command === "-h" || command === "--help") { printHelp() return } if (command === "init") { await initProject(parseOptions(args)) return } if (command === "add") { await addItems(parseOptions(args)) return } throw new Error(`Unknown command: ${command}`) } async function initProject(options) { const cwd = path.resolve(options.cwd ?? process.cwd()) const configPath = path.join(cwd, configFileName) const localManifestPath = path.join(cwd, "manifest.json") const config = { manifest: options.manifest ?? ((await exists(localManifestPath)) ? "manifest.json" : bundledManifestPath), aliases: { ui: "components/ui", lib: "lib", }, } if (!options.force && (await exists(configPath))) { console.log(`${configFileName} already exists. Use --force to overwrite it.`) return } await mkdir(path.join(cwd, config.aliases.ui), { recursive: true }) await mkdir(path.join(cwd, config.aliases.lib), { recursive: true }) await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`) console.log(`Created ${path.relative(process.cwd(), configPath) || configFileName}.`) } async function addItems(options) { const names = options._.filter((item) => !item.startsWith("-")) if (names.length === 0) { throw new Error("Usage: base-ui-tailwind add ") } const cwd = path.resolve(options.cwd ?? process.cwd()) const config = await loadConfig(cwd) const manifestRef = options.manifest ?? config.manifest ?? bundledManifestPath const manifest = await loadManifest(manifestRef, cwd) const selected = resolveItems(manifest, names) const files = collectFiles(manifest, selected) const written = [] const skipped = [] const packageDependencies = collectPackageDependencies(manifest, selected) for (const file of files) { const targetPath = path.join(cwd, resolveTargetPath(file.target, config.aliases)) if (!options.overwrite && (await exists(targetPath))) { skipped.push(path.relative(cwd, targetPath)) continue } if (!options.dryRun) { await mkdir(path.dirname(targetPath), { recursive: true }) const source = await readSourceFile(file.path, manifestRef, cwd) if (source.kind === "local") { await copyFile(source.path, targetPath) } else { await writeFile(targetPath, source.contents) } } written.push(path.relative(cwd, targetPath)) } if (written.length > 0) { console.log(`${options.dryRun ? "Would add" : "Added"}:`) for (const file of written) { console.log(` ${file}`) } } if (skipped.length > 0) { console.log("Skipped existing files:") for (const file of skipped) { console.log(` ${file}`) } console.log("Use --overwrite to replace existing files.") } if (packageDependencies.length > 0) { console.log("Required packages:") console.log(` ${packageDependencies.join(" ")}`) } } function resolveItems(manifest, names) { const resolved = new Set() const queue = [...names] while (queue.length > 0) { const name = queue.shift() const item = manifest.items?.[name] if (!item) { throw new Error(`Manifest item not found: ${name}`) } if (resolved.has(name)) { continue } resolved.add(name) for (const dependency of item.localDependencies ?? []) { queue.push(dependency) } } return [...resolved] } function collectFiles(manifest, names) { const files = [] const seen = new Set() for (const name of names) { for (const file of manifest.items[name].files ?? []) { const key = file.target if (!seen.has(key)) { seen.add(key) files.push(file) } } } return files } function collectPackageDependencies(manifest, names) { const dependencies = new Set() for (const name of names) { for (const dependency of manifest.items[name].dependencies ?? []) { dependencies.add(dependency) } } return [...dependencies].sort((a, b) => a.localeCompare(b)) } async function loadConfig(cwd) { const configPath = path.join(cwd, configFileName) if (!(await exists(configPath))) { return { manifest: undefined, aliases: { ui: "components/ui", lib: "lib", }, } } const config = JSON.parse(await readFile(configPath, "utf8")) return { manifest: config.manifest, aliases: { ui: config.aliases?.ui ?? "components/ui", lib: config.aliases?.lib ?? "lib", }, } } async function loadManifest(manifestRef, cwd) { const contents = isUrl(manifestRef) ? await fetchText(manifestRef) : await readFile(resolveLocalPath(manifestRef, cwd), "utf8") const manifest = JSON.parse(contents) if (!manifest.items || typeof manifest.items !== "object") { throw new Error("Invalid manifest: missing items.") } return manifest } async function readSourceFile(filePath, manifestRef, cwd) { if (isUrl(manifestRef)) { return { kind: "remote", contents: await fetchText(new URL(filePath, manifestRef).toString()), } } return { kind: "local", path: path.join(path.dirname(resolveLocalPath(manifestRef, cwd)), filePath), } } function resolveTargetPath(target, aliases) { if (target.startsWith("src/components/ui/")) { return path.join(aliases.ui, target.slice("src/components/ui/".length)) } if (target.startsWith("src/lib/")) { return path.join(aliases.lib, target.slice("src/lib/".length)) } if (target.startsWith("components/ui/")) { return path.join(aliases.ui, target.slice("components/ui/".length)) } if (target.startsWith("lib/")) { return path.join(aliases.lib, target.slice("lib/".length)) } return target } function resolveLocalPath(value, cwd) { if (path.isAbsolute(value)) { return value } const projectPath = path.resolve(cwd, value) return value === "manifest.json" && !existsSync(projectPath) ? bundledManifestPath : projectPath } function parseOptions(values) { const options = { _: [] } for (let index = 0; index < values.length; index += 1) { const value = values[index] if (value === "--cwd") { options.cwd = values[++index] } else if (value === "--manifest") { options.manifest = values[++index] } else if (value === "--force") { options.force = true } else if (value === "--overwrite") { options.overwrite = true } else if (value === "--dry-run") { options.dryRun = true } else { options._.push(value) } } return options } async function fetchText(url) { const response = await fetch(url) if (!response.ok) { throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`) } return response.text() } function existsSync(filePath) { try { accessSync(filePath) return true } catch { return false } } async function exists(filePath) { try { await access(filePath) return true } catch { return false } } function isUrl(value) { return /^https?:\/\//.test(value) } function printHelp() { console.log(`base-ui-tailwind Usage: base-ui-tailwind init [--manifest ] [--cwd ] [--force] base-ui-tailwind add [--manifest ] [--cwd ] [--overwrite] [--dry-run] `) }