Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 21 additions & 20 deletions exec/cli.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ bunx carllosnc/dashkit add sidebar

## CLI Reference

- `dashkit init [-y]`: Configures dashkit.json and injects design token stylesheet (dashkit.css).
- `dashkit init [-y] [-c <dir>] [--css-dir <dir>] [--ai-rules [file]] [--no-ai-rules] [-f] [--no-install] [--json]`: Configures `dashkit.json`, installs `dashkit.css` and `utils/`, injects `@import` into the main stylesheet, and generates AI assistant instructions (`AGENTS.md`).
- `dashkit add <components...> [--all] [-o <dir>] [-t] [-f] [--no-install]`: Scaffolds one or more components, resolves dependencies in a single batch, and updates the root `index.ts` barrel file.
- `dashkit info <component> [--json]`: Outputs component exports, dependencies, and TypeScript Props definitions.
- `dashkit list [--installed] [--json]`: Lists all registry components with status.
Expand Down
10 changes: 9 additions & 1 deletion scripts/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,16 @@ program

program
.command('init')
.description('Initialize Dashkit in your project (installs dashkit.css)')
.description('Initialize Dashkit in your project (installs dashkit.css, utils, dashkit.json, and AI rules)')
.option('-y, --yes', 'Accept all defaults without prompting')
.option('-c, --components-dir <dir>', 'Directory to store Dashkit components')
.option('-o, --output <dir>', 'Alias for --components-dir')
.option('--css-dir <dir>', 'Directory to store dashkit.css')
.option('--ai-rules [file]', 'Generate AI assistant instructions (defaults to AGENTS.md)')
.option('--no-ai-rules', 'Skip generating AI assistant instructions')
.option('-f, --force', 'Overwrite existing dashkit.css and utility files')
.option('--no-install', 'Do not automatically install base dependencies')
.option('--json', 'Output initialization result as JSON for LLMs and scripts')
.action(handleInit);

program
Expand Down
229 changes: 181 additions & 48 deletions scripts/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { runDoctor, getPackageManager, getMissingDependencies } from './doctor';
import {
getProjectInfo,
getFolderStructure,
normalizeDirPath,
normalizeComponentsDir,
saveConfig,
loadConfig,
findMainCssFile,
Expand Down Expand Up @@ -61,19 +63,22 @@ export interface CliOptions {
all?: boolean;
}

async function installDashkitCss(targetPath: string, sourcePath: string) {
if (await fs.pathExists(targetPath)) return true;
async function installDashkitCss(
targetPath: string,
sourcePath: string,
force = false
): Promise<'installed' | 'kept'> {
if (!(await fs.pathExists(sourcePath))) {
throw new Error(`Source file not found: ${sourcePath}`);
}

try {
if (await fs.pathExists(sourcePath)) {
await fs.copy(sourcePath, targetPath);
logger.success('dashkit.css installed.');
return true;
}
} catch {
return false;
if (!force && (await fs.pathExists(targetPath))) {
return 'kept';
}
return false;

await fs.ensureDir(path.dirname(targetPath));
await fs.copy(sourcePath, targetPath);
return 'installed';
}

// Components import shared helpers from ../utils (cn.ts, scrollLock.ts, ...).
Expand Down Expand Up @@ -170,23 +175,28 @@ async function copyComponentFiles(
return result;
}

async function installDependencies(deps: string[]) {
async function installDependencies(deps: string[], silent = false) {
const missingDeps = await getMissingDependencies(deps);
if (missingDeps.length === 0) return;

const pm = getPackageManager();
const installCmd = pm === 'npm' ? 'install' : 'add';
const command = `${pm} ${installCmd} ${missingDeps.join(' ')}`;

const spinner = ora(`Installing dependencies...`).start();
const spinner = ora({
text: 'Installing dependencies...',
isSilent: silent || !process.stdout.isTTY
}).start();
try {
execSync(command, { stdio: 'inherit', cwd: process.cwd() });
execSync(command, { stdio: silent ? 'pipe' : 'inherit', cwd: process.cwd() });
spinner.stop();
logger.success('Dependencies installed.');
if (!silent) logger.success('Dependencies installed.');
} catch {
spinner.stop();
logger.error('Failed to install dependencies. Please run:');
logger.bold(` ${command}`);
if (!silent) {
logger.error('Failed to install dependencies. Please run:');
logger.bold(` ${command}`);
}
process.exitCode = 1;
}
}
Expand Down Expand Up @@ -444,39 +454,97 @@ export async function handleList(options: ListOptions = {}) {

export interface InitOptions {
yes?: boolean;
componentsDir?: string;
output?: string;
cssDir?: string;
aiRules?: boolean | string;
force?: boolean;
install?: boolean;
json?: boolean;
}

export interface InitResultPayload {
initialized: boolean;
componentsDir: string;
cssDir: string;
cssFile: string;
cssStatus: 'installed' | 'kept';
mainCssFile: string | null;
cssImportStatus: 'imported' | 'already_imported' | 'skipped';
utilsInstalled: string[];
configFile: string | null;
aiRulesFile: string | null;
missingDependencies: string[];
}

export async function handleInit(options: InitOptions = {}) {
const info = await getProjectInfo();
const defaults = getFolderStructure(info);
const existingConfig = await loadConfig();
const initialComponentsDir = normalizeDirPath(existingConfig?.componentsDir ?? defaults.components) || defaults.components;
const initialCssDir = normalizeDirPath(existingConfig?.cssDir ?? defaults.css);
const mainCssFile = await findMainCssFile();
const silent = Boolean(options.json);
let spinner: ReturnType<typeof ora> | undefined;

try {
let componentsDir: string;
const explicitComponentsDir = options.componentsDir ?? options.output;
const hasExplicitFlags = Boolean(
options.yes ||
options.json ||
explicitComponentsDir !== undefined ||
options.cssDir !== undefined
);
const isAgentOrCiEnv = Boolean(
process.env.CI ||
process.env.CLAUDECODE ||
process.env.CURSOR_AGENT ||
process.env.CODEX_SANDBOX ||
process.env.ANTIGRAVITY_AGENT
);
const isNonInteractive =
hasExplicitFlags ||
isAgentOrCiEnv ||
!process.stdin.isTTY ||
!process.stdout.isTTY;

let componentsDirRaw: string;
let cssDirRaw: string;
let shouldSave: boolean;
let shouldInjectCss: boolean;

const isNonInteractive = Boolean(options.yes) || !process.stdin.isTTY || !process.stdout.isTTY;
let shouldGenerateAiRules: boolean;
const aiRulesTarget = typeof options.aiRules === 'string' ? options.aiRules : 'AGENTS.md';

if (isNonInteractive) {
componentsDir = defaults.components;
cssDirRaw = defaults.css || '.';
componentsDirRaw = explicitComponentsDir ?? initialComponentsDir;
const resolvedComponentsPreview = normalizeComponentsDir(componentsDirRaw, initialComponentsDir);
if (options.cssDir !== undefined) {
cssDirRaw = options.cssDir;
} else if (explicitComponentsDir !== undefined && existingConfig?.cssDir === undefined) {
cssDirRaw = resolvedComponentsPreview;
} else {
cssDirRaw = initialCssDir || '.';
}
shouldSave = true;
shouldInjectCss = Boolean(mainCssFile);
shouldGenerateAiRules = options.aiRules !== false;
} else {
const componentsDirPrompt = new Input({
name: 'componentsDir',
message: 'Where would you like to store your components?',
initial: defaults.components
initial: initialComponentsDir
});
componentsDir = await componentsDirPrompt.run();
componentsDirRaw = await componentsDirPrompt.run();

const normalizedPreview = normalizeComponentsDir(componentsDirRaw, initialComponentsDir);
const suggestedCssDir = existingConfig?.cssDir !== undefined
? (initialCssDir || '.')
: normalizedPreview;

const cssDirPrompt = new Input({
name: 'cssDir',
message: 'Where would you like to store your dashkit.css?',
initial: defaults.css || '.'
initial: suggestedCssDir
});
cssDirRaw = await cssDirPrompt.run();

Expand All @@ -492,6 +560,19 @@ export async function handleInit(options: InitOptions = {}) {
shouldInjectCss = false;
}

if (options.aiRules === false) {
shouldGenerateAiRules = false;
} else if (typeof options.aiRules === 'string') {
shouldGenerateAiRules = true;
} else {
const confirmAiRules = new Confirm({
name: 'aiRules',
message: `Generate AI assistant instructions (${aiRulesTarget})?`,
initial: true
});
shouldGenerateAiRules = await confirmAiRules.run();
}

const confirmPrompt = new Confirm({
name: 'save',
message: 'Save these settings to dashkit.json?',
Expand All @@ -500,17 +581,19 @@ export async function handleInit(options: InitOptions = {}) {
shouldSave = await confirmPrompt.run();
}

if (!componentsDir.replace(/\\/g, '/').split('/').pop()?.includes('dashkit')) {
componentsDir = path.join(componentsDir, 'dashkit').replace(/\\/g, '/');
}
const cssDir = cssDirRaw === '.' ? '' : cssDirRaw;
const componentsDir = normalizeComponentsDir(componentsDirRaw, initialComponentsDir);
const cssDir = normalizeDirPath(cssDirRaw);
const cssFile = cssDir ? `${cssDir}/dashkit.css` : 'dashkit.css';

spinner = ora('Initializing Dashkit...').start();
spinner = ora({
text: 'Initializing Dashkit...',
isSilent: silent || !process.stdout.isTTY
}).start();

// Ensure CSS installation
const targetPath = path.resolve(process.cwd(), cssDir, 'dashkit.css');
const sourcePath = path.resolve(projectRoot, 'src', 'dashkit', 'dashkit.css');
await installDashkitCss(targetPath, sourcePath);
const cssStatus = await installDashkitCss(targetPath, sourcePath, options.force);

// Create dashkit and utils folders
const fullComponentsDir = path.resolve(process.cwd(), componentsDir);
Expand All @@ -519,36 +602,77 @@ export async function handleInit(options: InitOptions = {}) {

// Copy shared utils (cn.ts, scrollLock.ts, ...) so every component's
// ../utils imports resolve in the user's project.
await copyUtilsFiles(utilsDir);
const copiedUtils = await copyUtilsFiles(utilsDir, options.force);

let injectedCssFile: string | null = null;
let cssImportStatus: 'imported' | 'already_imported' | 'skipped' = 'skipped';
if (shouldInjectCss && mainCssFile) {
const injectResult = await injectDashkitCss(mainCssFile, targetPath);
if (injectResult.updated) {
injectedCssFile = path.relative(process.cwd(), mainCssFile).replace(/\\/g, '/');
cssImportStatus = 'imported';
} else if (injectResult.reason === 'already_imported') {
cssImportStatus = 'already_imported';
}
}

if (shouldSave) {
await saveConfig({ componentsDir, cssDir });
}

let writtenAiRulesFile: string | null = null;
if (shouldGenerateAiRules) {
writtenAiRulesFile = await writeAiRulesFile(aiRulesTarget, { componentsDir, cssDir });
}

spinner.stop();

const baseDeps = ['clsx', 'tailwind-merge'];
let missingBaseDeps = await getMissingDependencies(baseDeps);
if (missingBaseDeps.length > 0) {
const hasPkgJson = await fs.pathExists(path.resolve(process.cwd(), 'package.json'));
if (options.install !== false && hasPkgJson) {
await installDependencies(missingBaseDeps, silent);
missingBaseDeps = await getMissingDependencies(baseDeps);
}
}

const relMainCss = mainCssFile
? path.relative(process.cwd(), mainCssFile).replace(/\\/g, '/')
: null;

if (silent) {
const payload: InitResultPayload = {
initialized: true,
componentsDir,
cssDir,
cssFile,
cssStatus,
mainCssFile: relMainCss,
cssImportStatus,
utilsInstalled: copiedUtils,
configFile: shouldSave ? 'dashkit.json' : null,
aiRulesFile: writtenAiRulesFile,
missingDependencies: missingBaseDeps,
};
console.log(JSON.stringify(payload, null, 2));
return;
}

logger.success('Dashkit initialized successfully.');

logger.bold('\nInitialization Summary:');
logger.info(` Components Dir: ${chalk.bold(componentsDir)}`);
logger.info(` CSS Dir: ${chalk.bold(cssDir || 'root')}`);
if (injectedCssFile) {
logger.info(` Main CSS: ${chalk.bold(injectedCssFile)} (imported dashkit.css)`);
} else if (mainCssFile) {
const relMain = path.relative(process.cwd(), mainCssFile).replace(/\\/g, '/');
const relImport = getRelativeCssImport(mainCssFile, targetPath);
logger.info(` Main CSS: ${chalk.bold(relMain)} (add: @import "${relImport}";)`);
if (relMainCss && mainCssFile) {
if (cssImportStatus === 'imported' || cssImportStatus === 'already_imported') {
logger.info(` Main CSS: ${chalk.bold(relMainCss)} (imported dashkit.css)`);
} else {
const relImport = getRelativeCssImport(mainCssFile, targetPath);
logger.info(` Main CSS: ${chalk.bold(relMainCss)} (add: @import "${relImport}";)`);
}
}
if (shouldSave) logger.info(' Configuration: dashkit.json created');
if (shouldSave) logger.info(' Configuration: dashkit.json saved');
if (writtenAiRulesFile) logger.info(` AI Rules: ${chalk.bold(writtenAiRulesFile)} (updated)`);

const missingBaseDeps = await getMissingDependencies(['clsx', 'tailwind-merge']);
if (missingBaseDeps.length > 0) {
const pm = getPackageManager();
const installCmd = pm === 'npm' ? 'install' : 'add';
Expand All @@ -559,6 +683,7 @@ export async function handleInit(options: InitOptions = {}) {
if (error instanceof Error && error.message === 'cancelled') {
process.exit(0);
}
spinner?.stop();
logger.error('Failed to initialize Dashkit.');
if (error instanceof Error) logger.error(error.message);
process.exitCode = 1;
Expand Down Expand Up @@ -650,14 +775,16 @@ export interface AiRulesOptions {
output?: string;
}

export async function handleAiRules(options: AiRulesOptions = {}) {
async function writeAiRulesFile(
targetFile = 'AGENTS.md',
configOverride?: { componentsDir: string; cssDir: string }
): Promise<string> {
const info = await getProjectInfo();
const defaults = getFolderStructure(info);
const config = await loadConfig();
const componentsDir = (config?.componentsDir ?? defaults.components).replace(/\\/g, '/');
const cssDir = (config?.cssDir ?? (defaults.css || '.')).replace(/\\/g, '/');
const cssFile = cssDir === '.' || cssDir === '' ? 'dashkit.css' : `${cssDir}/dashkit.css`;
const targetFile = options.output || 'AGENTS.md';
const config = configOverride ?? (await loadConfig());
const componentsDir = normalizeDirPath(config?.componentsDir ?? defaults.components) || defaults.components;
const cssDir = normalizeDirPath(config?.cssDir ?? defaults.css);
const cssFile = cssDir ? `${cssDir}/dashkit.css` : 'dashkit.css';
const targetPath = path.resolve(process.cwd(), targetFile);

const allSlugs = Object.keys(registry).sort((a, b) => a.localeCompare(b));
Expand Down Expand Up @@ -695,6 +822,12 @@ export async function handleAiRules(options: AiRulesOptions = {}) {
await fs.writeFile(targetPath, `# Project AI Instructions\n\n${rulesBlock}\n`, 'utf8');
}

return targetFile.replace(/\\/g, '/');
}

export async function handleAiRules(options: AiRulesOptions = {}) {
const targetFile = options.output || 'AGENTS.md';
await writeAiRulesFile(targetFile);
logger.success(`AI rules written to ${targetFile}.`);
}

Expand Down
2 changes: 1 addition & 1 deletion scripts/generate-llms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ export function generateLlmsTxt(): string {
lines.push('```\n');

lines.push('## CLI Reference\n');
lines.push('- `dashkit init [-y]`: Configures dashkit.json and injects design token stylesheet (dashkit.css).');
lines.push('- `dashkit init [-y] [-c <dir>] [--css-dir <dir>] [--ai-rules [file]] [--no-ai-rules] [-f] [--no-install] [--json]`: Configures `dashkit.json`, installs `dashkit.css` and `utils/`, injects `@import` into the main stylesheet, and generates AI assistant instructions (`AGENTS.md`).');
lines.push('- `dashkit add <components...> [--all] [-o <dir>] [-t] [-f] [--no-install]`: Scaffolds one or more components, resolves dependencies in a single batch, and updates the root `index.ts` barrel file.');
lines.push('- `dashkit info <component> [--json]`: Outputs component exports, dependencies, and TypeScript Props definitions.');
lines.push('- `dashkit list [--installed] [--json]`: Lists all registry components with status.');
Expand Down
Loading
Loading