diff --git a/.gitignore b/.gitignore index 553e817..a3c399d 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,4 @@ pnpm-lock.yaml.js **/package-lock.json # brandcn specific -**/components \ No newline at end of file +**/packages/brandcn/components \ No newline at end of file diff --git a/packages/brandcn/package.json b/packages/brandcn/package.json index a27eba3..dff302c 100644 --- a/packages/brandcn/package.json +++ b/packages/brandcn/package.json @@ -1,7 +1,7 @@ { "name": "brandcn", "description": "brandcn is a CLI tool that lets you quickly pull and add high-quality brand logos to your project — just like adding components with shadcn/ui.", - "version": "0.1.14", + "version": "0.1.16", "author": "yonaries", "bin": { "brandcn": "./bin/run.js" diff --git a/packages/brandcn/src/commands/add.ts b/packages/brandcn/src/commands/add.ts index a016cc1..2089ffc 100644 --- a/packages/brandcn/src/commands/add.ts +++ b/packages/brandcn/src/commands/add.ts @@ -1,7 +1,9 @@ import * as p from '@clack/prompts' import {Command, Flags} from '@oclif/core' -import {LogoOperationResult, processLogos, setCustomTargetDirectory, targetDirectoryExists} from '../utils/fs.js' +import type {LogoOperationResult} from '../types/logos.js' + +import {getDefaultDirectoryPath, processLogos, setCustomTargetDirectory, targetDirectoryExists} from '../utils/fs.js' import {displayError, displayUsage, LogoSpinner} from '../utils/log.js' import {validateLogoNames} from '../utils/validate.js' @@ -64,9 +66,10 @@ export default class Add extends Command { if (!directoryExists) { p.intro('🎨 brandcn') + const defaultPath = getDefaultDirectoryPath() const directory = await p.text({ message: 'Would you like to specify a custom directory?', - placeholder: 'components/logos', + placeholder: defaultPath, }) if (p.isCancel(directory)) { @@ -74,9 +77,11 @@ export default class Add extends Command { this.exit(0) } - if (directory !== 'components/logos') { - setCustomTargetDirectory(directory) - } + const chosenDir = (directory && directory.toString().trim().length > 0) + ? directory.toString().trim() + : defaultPath + + setCustomTargetDirectory(chosenDir) } const spinner = new LogoSpinner(`Processing ${validation.validNames.length} logo(s)...`) diff --git a/packages/brandcn/src/types/logos.ts b/packages/brandcn/src/types/logos.ts new file mode 100644 index 0000000..6b672b8 --- /dev/null +++ b/packages/brandcn/src/types/logos.ts @@ -0,0 +1,17 @@ +export type VariantType = 'dark' | 'light' | 'wordmark' + +export interface ProcessLogosOptions { + dark?: boolean + light?: boolean + wordmark?: boolean +} + +export interface LogoOperationResult { + error?: string + logoName: string + reason?: string + skipped?: boolean + success: boolean +} + + diff --git a/packages/brandcn/src/utils/fs.ts b/packages/brandcn/src/utils/fs.ts index 6a54587..a4e9fed 100644 --- a/packages/brandcn/src/utils/fs.ts +++ b/packages/brandcn/src/utils/fs.ts @@ -4,6 +4,8 @@ import {constants} from 'node:fs' import path from 'node:path' import {fileURLToPath} from 'node:url' +import type {LogoOperationResult, ProcessLogosOptions} from '../types/logos.js' + // Global variable to store custom target directory let customTargetDirectory: null | string = null @@ -17,11 +19,36 @@ export function getTargetLogosPath(): string { return path.resolve(process.cwd(), customTargetDirectory) } - return path.resolve(process.cwd(), 'components/logos') + // Try reading configured outputDir from package.json + const configured = getConfiguredOutputDir() + if (configured) { + return configured + } + + // Check if src folder exists first + const defaultPath = getDefaultDirectoryPath() + + return path.resolve(process.cwd(), defaultPath) } export function setCustomTargetDirectory(directory: string): void { - customTargetDirectory = directory + const normalized = directory?.toString().trim() || 'components/logos' + customTargetDirectory = normalized + persistConfiguredOutputDir(normalized) +} + +export function getDefaultDirectoryPath(): string { + // Check if src folder exists first + const srcPath = path.resolve(process.cwd(), 'src') + try { + if (fs.pathExistsSync(srcPath) && fs.statSync(srcPath).isDirectory()) { + return 'src/components/logos' + } + } catch { + // If we can't access src folder, fall back to default + } + + return 'components/logos' } export async function targetDirectoryExists(): Promise { @@ -79,14 +106,6 @@ export async function getAvailableLogos(): Promise { } } -export type VariantType = 'dark' | 'light' | 'wordmark' - -export interface ProcessLogosOptions { - dark?: boolean - light?: boolean - wordmark?: boolean -} - export function findLogoVariants(brandName: string, availableLogos: string[]): string[] { const normalizedBrand = brandName.toLowerCase() @@ -134,13 +153,6 @@ export function filterByVariants(logoNames: string[], options: ProcessLogosOptio }) } -export interface LogoOperationResult { - error?: string - logoName: string - reason?: string - skipped?: boolean - success: boolean -} export async function processLogos( logoNames: string[], @@ -224,3 +236,102 @@ export function getVariantType(logoName: string, baseName: string): null | strin return null } + +// Configuration helpers +function findNearestPackageJson(startDir: string = process.cwd()): null | string { + let currentDir = startDir + while (true) { + const candidate = path.join(currentDir, 'package.json') + if (fs.pathExistsSync(candidate)) return candidate + const parent = path.dirname(currentDir) + if (parent === currentDir) break + currentDir = parent + } + + return null +} + +function readBrandcnOutputDirFrom(pkgPath: string): null | string { + try { + const pkg = fs.readJSONSync(pkgPath) + const output = pkg?.brandcn?.outputDir + if (typeof output === 'string' && output.trim().length > 0) { + return path.resolve(path.dirname(pkgPath), output.trim()) + } + + return null + } catch { + return null + } +} + +function listWorkspacePackageJsonCandidates(): string[] { + const roots = ['apps', 'packages'] + const results: string[] = [] + for (const root of roots) { + const rootPath = path.resolve(process.cwd(), root) + if (!fs.pathExistsSync(rootPath) || !fs.statSync(rootPath).isDirectory()) continue + const entries = fs.readdirSync(rootPath) + for (const entry of entries) { + const pkgJson = path.join(rootPath, entry, 'package.json') + if (fs.pathExistsSync(pkgJson)) results.push(pkgJson) + } + } + + return results +} + +function getConfiguredOutputDir(): null | string { + try { + const nearest = findNearestPackageJson() + const absFromNearest = nearest ? readBrandcnOutputDirFrom(nearest) : null + if (absFromNearest) return absFromNearest + + const workspacePkgs = listWorkspacePackageJsonCandidates() + const absCandidates = workspacePkgs + .map((pkgPath) => readBrandcnOutputDirFrom(pkgPath)) + .filter(Boolean) as string[] + if (absCandidates.length === 1) return absCandidates[0] + + return null + } catch { + return null + } +} + +interface PackageJsonLike { + [key: string]: unknown + brandcn?: { + [key: string]: unknown + outputDir?: string + } +} + +function persistConfiguredOutputDir(directory: string): void { + try { + const normalized = directory?.toString().trim() || 'components/logos' + const absTarget = path.resolve(process.cwd(), normalized) + const nearestFromTarget = findNearestPackageJson(path.dirname(absTarget)) + const nearestFromCwd = findNearestPackageJson() + const pkgPath = nearestFromTarget || nearestFromCwd + if (!pkgPath) return + let pkg: PackageJsonLike = {} + try { + pkg = fs.readJSONSync(pkgPath) + } catch { + pkg = {} + } + + const nextPkg = { + ...pkg, + brandcn: { + ...pkg.brandcn, + outputDir: normalized, + }, + } + + fs.writeJSONSync(pkgPath, nextPkg, {spaces: 2}) + } catch { + // Ignore persistence errors; CLI can still operate with in-memory value + } +} diff --git a/packages/brandcn/test/utils/fs.test.ts b/packages/brandcn/test/utils/fs.test.ts index d608835..709134a 100644 --- a/packages/brandcn/test/utils/fs.test.ts +++ b/packages/brandcn/test/utils/fs.test.ts @@ -47,10 +47,18 @@ describe('fs utilities', () => { }) describe('getTargetLogosPath', () => { - it('should return path to logos directory in current working directory', () => { + it('should return path to logos directory in current working directory when no src folder exists', () => { const targetPath = getTargetLogosPath() expect(targetPath).toBe(path.resolve(process.cwd(), 'components/logos')) }) + + it('should return path to logos directory in src folder when src folder exists', () => { + // Create an src directory + mkdirSync('./src', {recursive: true}) + + const targetPath = getTargetLogosPath() + expect(targetPath).toBe(path.resolve(process.cwd(), 'src/components/logos')) + }) }) describe('logoExistsInLibrary', () => {