create-hauis-app
Version:
Interactive CLI wizard for scaffolding modern frontend projects with Tailwind CSS v4
1,749 lines (1,669 loc) • 72.8 kB
JavaScript
#!/usr/bin/env node
// src/index.ts
import chalk2 from "chalk";
import { Command } from "commander";
// src/commands/create.ts
import { join as join7 } from "path";
import prompts from "prompts";
// src/core/project-builder.ts
import { existsSync as existsSync7 } from "fs";
import ora from "ora";
// src/features/code-quality/biome.ts
import { existsSync as existsSync2, writeFileSync } from "fs";
import { join } from "path";
// src/utils/logger.ts
import chalk from "chalk";
import { format } from "date-fns";
var LoggerImpl = class {
debugMode = false;
silent = false;
setDebugMode(enabled) {
this.debugMode = enabled;
}
setSilent(enabled) {
this.silent = enabled;
}
info(message) {
if (!this.silent) {
console.log(chalk.blue("\u2139"), message);
}
}
success(message) {
if (!this.silent) {
console.log(chalk.green("\u2714"), message);
}
}
warning(message) {
if (!this.silent) {
console.log(chalk.yellow("\u26A0"), message);
}
}
error(message) {
console.error(chalk.red("\u2716"), message);
}
debug(message) {
if (this.debugMode && !this.silent) {
const timestamp = format(/* @__PURE__ */ new Date(), "HH:mm:ss.SSS");
console.log(chalk.gray(`[${timestamp}]`), chalk.dim(message));
}
}
box(title, content) {
if (this.silent) return;
const maxLength = Math.max(
title.length,
...content.map((line) => line.length)
);
const boxWidth = maxLength + 4;
console.log(chalk.cyan(`\u250C${"\u2500".repeat(boxWidth - 2)}\u2510`));
console.log(
`${chalk.cyan("\u2502")}${chalk.bold(` ${title.padEnd(boxWidth - 4)} `)}${chalk.cyan("\u2502")}`
);
console.log(chalk.cyan(`\u251C${"\u2500".repeat(boxWidth - 2)}\u2524`));
for (const line of content) {
console.log(
`${chalk.cyan("\u2502")} ${line.padEnd(boxWidth - 4)} ${chalk.cyan("\u2502")}`
);
}
console.log(chalk.cyan(`\u2514${"\u2500".repeat(boxWidth - 2)}\u2518`));
}
table(data) {
if (this.silent) return;
const maxKeyLength = Math.max(...data.map(([key]) => key.length));
for (const [key, value] of data) {
console.log(
` ${chalk.gray(key.padEnd(maxKeyLength + 2))} ${chalk.white(value)}`
);
}
}
divider() {
if (!this.silent) {
console.log(chalk.gray("\u2500".repeat(50)));
}
}
newLine() {
if (!this.silent) {
console.log("");
}
}
};
var logger = new LoggerImpl();
// src/utils/package-manager.ts
import { execSync } from "child_process";
import { existsSync } from "fs";
// src/types/types.ts
var CLIError = class extends Error {
constructor(message, code, details) {
super(message);
this.code = code;
this.details = details;
this.name = "CLIError";
}
};
// src/utils/package-manager.ts
var packageManagers = {
bun: {
add: "bun add",
addDev: "bun add -D",
exec: "bunx",
init: "bun init -y",
install: "bun install",
run: "bun run"
},
npm: {
add: "npm install",
addDev: "npm install -D",
exec: "npx",
init: "npm init -y",
install: "npm install",
run: "npm run"
},
pnpm: {
add: "pnpm add",
addDev: "pnpm add -D",
exec: "pnpx",
init: "pnpm init",
install: "pnpm install",
run: "pnpm run"
},
yarn: {
add: "yarn add",
addDev: "yarn add -D",
exec: "yarn",
init: "yarn init -y",
install: "yarn install",
run: "yarn"
}
};
var PackageManagerAdapter = class {
commands;
manager;
constructor(manager) {
this.manager = manager;
this.commands = packageManagers[manager];
}
static detect(projectPath) {
const lockFiles = {
"bun.lockb": "bun",
"package-lock.json": "npm",
"pnpm-lock.yaml": "pnpm",
"yarn.lock": "yarn"
};
for (const [file, manager] of Object.entries(lockFiles)) {
if (existsSync(`${projectPath}/${file}`)) return manager;
}
return null;
}
static isInstalled(pm) {
try {
execSync(`${pm} --version`, { stdio: "ignore" });
return true;
} catch {
return false;
}
}
install(config) {
if (config.dryRun) {
logger.debug(`[DRY RUN] Would run: ${this.commands.install}`);
return;
}
logger.info(`Installing dependencies with ${this.manager}...`);
try {
execSync(this.commands.install, {
cwd: config.projectPath,
stdio: config.debug ? "inherit" : "ignore"
});
logger.success("Dependencies installed successfully");
} catch (error) {
throw new CLIError(
`Failed to install dependencies with ${this.manager}`,
"INSTALL_FAILED",
error
);
}
}
add(packages, dev, config) {
const command = dev ? this.commands.addDev : this.commands.add;
const fullCommand = `${command} ${packages.join(" ")}`;
if (config.dryRun) {
logger.debug(`[DRY RUN] Would run: ${fullCommand}`);
return;
}
logger.debug(`Running: ${fullCommand}`);
try {
execSync(fullCommand, {
cwd: config.projectPath,
stdio: config.debug ? "inherit" : "ignore"
});
} catch (error) {
throw new CLIError(
`Failed to add packages: ${packages.join(", ")}`,
"ADD_PACKAGES_FAILED",
error
);
}
}
run(script, args, config) {
const command = `${this.commands.run} ${script} ${args.join(" ")}`.trim();
if (config.dryRun) {
logger.debug(`[DRY RUN] Would run: ${command}`);
return;
}
logger.debug(`Running: ${command}`);
try {
execSync(command, {
cwd: config.projectPath,
stdio: config.debug ? "inherit" : "ignore"
});
} catch (error) {
throw new CLIError(
`Failed to run script: ${script}`,
"RUN_SCRIPT_FAILED",
error
);
}
}
exec(command, args, config) {
const fullCommand = `${this.commands.exec} ${command} ${args.join(" ")}`.trim();
if (config.dryRun) {
logger.debug(`[DRY RUN] Would run: ${fullCommand}`);
return;
}
logger.debug(`Running: ${fullCommand}`);
try {
execSync(fullCommand, {
cwd: config.projectPath,
stdio: config.debug ? "inherit" : "ignore"
});
} catch (error) {
throw new CLIError(
`Failed to execute command: ${command}`,
"EXEC_FAILED",
error
);
}
}
getInstallCommand() {
return this.commands.install;
}
getRunCommand(script) {
return `${this.commands.run} ${script}`;
}
getName() {
return this.manager;
}
};
// src/features/code-quality/biome.ts
var BiomeInstaller = class {
name = "Biome";
async install(config) {
logger.info("Setting up Biome for code quality...");
if (config.dryRun) {
logger.debug("[DRY RUN] Skipping Biome installation");
return;
}
const pm = new PackageManagerAdapter(config.packageManager);
pm.add(["@biomejs/biome"], true, config);
this.createBiomeConfig(config);
await this.updateScripts(config);
logger.success("Biome configured successfully");
}
createBiomeConfig(config) {
const biomeConfig = {
$schema: "https://biomejs.dev/schemas/2.0.4/schema.json",
assist: {
actions: {
source: {
organizeImports: "on",
useSortedAttributes: "on",
useSortedKeys: "on"
}
},
enabled: true
},
css: {
formatter: {
enabled: true
},
linter: {
enabled: true
}
},
files: {
includes: ["src/**", "!src/**/*.css", "*.ts"]
},
formatter: {
indentStyle: "space",
indentWidth: 2
},
javascript: {
formatter: {
quoteStyle: "double",
semicolons: "asNeeded"
}
},
linter: {
domains: {
next: "recommended",
react: "recommended",
test: "all"
},
rules: {
a11y: {
noSvgWithoutTitle: "off",
useAriaPropsSupportedByRole: "off"
},
complexity: {
noExcessiveCognitiveComplexity: {
level: "error",
options: {
maxAllowedComplexity: 22
}
},
noStaticOnlyClass: "off",
noUselessFragments: "off",
noUselessUndefinedInitialization: "error",
useDateNow: "error"
},
correctness: {
noNodejsModules: "off",
noUndeclaredDependencies: "off",
noUndeclaredVariables: "off",
noUnusedFunctionParameters: "error",
noUnusedImports: {
fix: "safe",
level: "error"
},
noUnusedVariables: "error",
useExhaustiveDependencies: "error",
useImportExtensions: "off"
},
nursery: {
noNestedComponentDefinitions: "error",
noSecrets: "off",
useExplicitType: "off",
useIterableCallbackReturn: "error",
useSortedClasses: {
fix: "safe",
level: "on",
options: {
attributes: ["classList"],
functions: ["clsx", "cva"]
}
}
},
performance: {
noBarrelFile: "off",
noDynamicNamespaceImportAccess: "error",
noNamespaceImport: "off",
useTopLevelRegex: "off"
},
security: {
noBlankTarget: "error",
noDangerouslySetInnerHtml: "off",
noGlobalEval: "error"
},
style: {
noDefaultExport: "off",
noInferrableTypes: "error",
noNamespace: "off",
noNonNullAssertion: "warn",
noParameterAssign: "error",
noProcessEnv: "off",
noRestrictedGlobals: {
level: "error",
options: {
deniedGlobals: {
$: "Don't use $ \u2014 unless you're wiring it to my bank account"
}
}
},
noRestrictedImports: {
level: "error",
options: {
paths: {
"next/router": "Use `next/navigation` instead"
}
}
},
noUnusedTemplateLiteral: "error",
noUselessElse: "error",
useAsConstAssertion: "error",
useBlockStatements: "off",
useComponentExportOnlyModules: "off",
useConsistentArrayType: {
level: "error",
options: {
syntax: "generic"
}
},
useConsistentCurlyBraces: "off",
useConst: "error",
useDefaultParameterLast: "error",
useDefaultSwitchClause: "off",
useEnumInitializers: "error",
useFilenamingConvention: {
level: "error",
options: {
filenameCases: ["kebab-case", "PascalCase"]
}
},
useNamingConvention: {
level: "error",
options: {
conventions: [
{
formats: ["camelCase", "PascalCase"],
selector: {
kind: "variable",
scope: "any"
}
},
{
formats: ["snake_case", "PascalCase"],
selector: {
kind: "classProperty",
modifiers: ["readonly"]
}
},
{
formats: ["camelCase", "PascalCase", "CONSTANT_CASE"],
selector: {
kind: "typeProperty",
modifiers: ["readonly"]
}
},
{
formats: ["camelCase", "PascalCase"],
selector: {
kind: "typeParameter"
}
},
{
formats: [
"camelCase",
"PascalCase",
"CONSTANT_CASE",
"snake_case"
],
selector: {
kind: "objectLiteralProperty",
scope: "any"
}
}
],
strictCase: false
}
},
useNodejsImportProtocol: "off",
useNumberNamespace: "error",
useSelfClosingElements: "error",
useSingleVarDeclarator: "error"
},
suspicious: {
noArrayIndexKey: "off",
noAssignInExpressions: "error",
noConsole: "off",
noDoubleEquals: "error",
noEvolvingTypes: "off",
noExplicitAny: "off",
noImplicitAnyLet: "off",
noMisplacedAssertion: "off",
noReactSpecificProps: "off",
noShadowRestrictedNames: "off",
noVar: "error",
useAwait: "error",
useStrictMode: "off"
}
}
},
overrides: [
{
includes: ["**/src/**/*.component.*", "**/src/**/*.stories.*"],
linter: {
rules: {
style: {
useFilenamingConvention: {
level: "error",
options: {
filenameCases: ["PascalCase"]
}
}
}
}
}
},
{
includes: ["**/src/**/*.spec.ts", "**/src/**/*.spec.tsx"],
linter: {
rules: {
style: {
useNamingConvention: "off"
}
}
}
},
{
includes: ["**/src/**/*.component.tsx"],
linter: {
rules: {
style: {
noDefaultExport: "on"
}
}
}
}
],
root: true
};
const configPath = join(config.projectPath, "biome.json");
if (config.dryRun) {
logger.debug("[DRY RUN] Would create Biome config");
return;
}
writeFileSync(configPath, JSON.stringify(biomeConfig, null, 2));
logger.debug("Created biome.json");
}
async updateScripts(config) {
if (config.dryRun) {
logger.debug("[DRY RUN] Would update package.json scripts");
return;
}
const packageJsonPath = join(config.projectPath, "package.json");
const { readFileSync: readFileSync3, writeFileSync: writeFileSync5 } = await import("fs");
const packageJson = existsSync2(packageJsonPath) ? JSON.parse(readFileSync3(packageJsonPath, "utf-8")) : { name: config.projectName, scripts: {} };
packageJson.scripts = {
...packageJson.scripts,
check: "pnpm check:biome && pnpm check:types",
"check:biome": "biome check --error-on-warnings",
"check:format": "biome check --write",
"check:types": "tsc"
};
writeFileSync5(packageJsonPath, JSON.stringify(packageJson, null, 2));
logger.debug(
`${existsSync2(packageJsonPath) ? "Updated" : "Created"} package.json scripts`
);
}
};
// src/features/code-quality/eslint-prettier.ts
import { existsSync as existsSync3, readFileSync, writeFileSync as writeFileSync2 } from "fs";
import { join as join2 } from "path";
var ESLintPrettierInstaller = class {
name = "ESLint + Prettier";
async install(config) {
logger.info("Setting up ESLint + Prettier...");
const pm = new PackageManagerAdapter(config.packageManager);
const deps = this.getDependencies(config);
await pm.add(deps, true, config);
this.createESLintConfig(config);
this.createPrettierConfig(config);
this.createIgnoreFiles(config);
this.updateScripts(config);
logger.success("ESLint + Prettier configured successfully");
}
getDependencies(config) {
const base = [
"eslint",
"prettier",
"eslint-config-prettier",
"eslint-plugin-prettier"
];
if (config.typescript) {
base.push("@typescript-eslint/parser", "@typescript-eslint/eslint-plugin");
}
switch (config.framework) {
case "react":
base.push("eslint-plugin-react", "eslint-plugin-react-hooks");
if (config.metaFramework === "next") {
base.push("eslint-config-next");
}
break;
case "vue":
base.push("eslint-plugin-vue");
break;
case "svelte":
base.push("eslint-plugin-svelte");
break;
}
return base;
}
createESLintConfig(config) {
const eslintConfig = {
env: {
browser: true,
es2022: true,
node: true
},
extends: this.getESLintExtends(config),
parser: config.typescript ? "@typescript-eslint/parser" : void 0,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
...config.framework === "react" && {
ecmaFeatures: {
jsx: true
}
}
},
plugins: this.getESLintPlugins(config),
root: true,
rules: {
"prettier/prettier": "error",
...this.getFrameworkRules(config)
},
settings: this.getESLintSettings(config)
};
for (const key of Object.keys(eslintConfig)) {
if (eslintConfig[key] === void 0) {
delete eslintConfig[key];
}
}
const configPath = join2(config.projectPath, ".eslintrc.json");
if (config.dryRun) {
logger.debug("[DRY RUN] Would create ESLint config");
return;
}
writeFileSync2(configPath, JSON.stringify(eslintConfig, null, 2));
logger.debug("Created .eslintrc.json");
}
getESLintExtends(config) {
const extendsList = ["eslint:recommended"];
if (config.typescript) {
extendsList.push("plugin:@typescript-eslint/recommended");
}
switch (config.framework) {
case "react":
extendsList.push("plugin:react/recommended");
extendsList.push("plugin:react-hooks/recommended");
if (config.metaFramework === "next") {
extendsList.push("next/core-web-vitals");
}
break;
case "vue":
extendsList.push("plugin:vue/vue3-recommended");
break;
case "svelte":
extendsList.push("plugin:svelte/recommended");
break;
}
extendsList.push("plugin:prettier/recommended");
return extendsList;
}
getESLintPlugins(config) {
const plugins = [];
if (config.typescript) {
plugins.push("@typescript-eslint");
}
switch (config.framework) {
case "react":
plugins.push("react", "react-hooks");
break;
case "vue":
plugins.push("vue");
break;
case "svelte":
plugins.push("svelte");
break;
}
return plugins;
}
getFrameworkRules(config) {
const rules = {};
if (config.framework === "react") {
rules["react/react-in-jsx-scope"] = "off";
rules["react/prop-types"] = "off";
}
if (config.typescript) {
rules["@typescript-eslint/explicit-module-boundary-types"] = "off";
rules["@typescript-eslint/no-explicit-any"] = "warn";
}
return rules;
}
getESLintSettings(config) {
if (config.framework === "react") {
return {
react: {
version: "detect"
}
};
}
return void 0;
}
createPrettierConfig(config) {
const prettierConfig = {
arrowParens: "always",
bracketSameLine: false,
bracketSpacing: true,
embeddedLanguageFormatting: "auto",
endOfLine: "lf",
htmlWhitespaceSensitivity: "css",
jsxSingleQuote: false,
printWidth: 100,
proseWrap: "preserve",
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: "es5",
useTabs: false
};
const configPath = join2(config.projectPath, ".prettierrc");
if (config.dryRun) {
logger.debug("[DRY RUN] Would create Prettier config");
return;
}
writeFileSync2(configPath, JSON.stringify(prettierConfig, null, 2));
logger.debug("Created .prettierrc");
}
createIgnoreFiles(config) {
const ignorePatterns = [
"node_modules",
"dist",
"build",
".next",
".nuxt",
".svelte-kit",
".astro",
"coverage",
"*.min.js",
"*.min.css",
"package-lock.json",
"pnpm-lock.yaml",
"yarn.lock",
"bun.lockb"
];
const eslintIgnorePath = join2(config.projectPath, ".eslintignore");
const prettierIgnorePath = join2(config.projectPath, ".prettierignore");
if (config.dryRun) {
logger.debug("[DRY RUN] Would create ignore files");
return;
}
const ignoreContent = ignorePatterns.join("\n");
writeFileSync2(eslintIgnorePath, ignoreContent);
writeFileSync2(prettierIgnorePath, ignoreContent);
logger.debug("Created .eslintignore and .prettierignore");
}
updateScripts(config) {
if (config.dryRun) {
logger.debug("[DRY RUN] Would update package.json scripts");
return;
}
const packageJsonPath = join2(config.projectPath, "package.json");
if (!existsSync3(packageJsonPath)) {
logger.warning("package.json not found, skipping script updates");
return;
}
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
packageJson.scripts = {
...packageJson.scripts,
format: "prettier --write .",
"format:check": "prettier --check .",
lint: "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
};
writeFileSync2(packageJsonPath, JSON.stringify(packageJson, null, 2));
logger.debug("Updated package.json scripts");
}
};
// src/features/git-hooks/lefthook.ts
import { execSync as execSync2 } from "child_process";
import { existsSync as existsSync4, writeFileSync as writeFileSync3 } from "fs";
import { join as join3 } from "path";
var LefthookInstaller = class {
name = "Lefthook";
async install(config) {
logger.info("Setting up Lefthook for Git hooks...");
const pm = new PackageManagerAdapter(config.packageManager);
await pm.add(["lefthook"], true, config);
this.createLefthookConfig(config);
this.installGitHooks(config);
logger.success("Lefthook configured successfully");
}
createLefthookConfig(config) {
const lefthookConfig = this.generateConfig(config);
const configPath = join3(config.projectPath, "lefthook.yml");
if (config.dryRun) {
logger.debug("[DRY RUN] Would create Lefthook config");
return;
}
writeFileSync3(configPath, lefthookConfig);
logger.debug("Created lefthook.yml");
}
generateConfig(config) {
const { packageManager, codeQuality } = config;
const pm = packageManager === "npm" ? "npm run" : packageManager;
const lockFile = this.getLockFileName(packageManager);
let configYaml = `pre-commit:
commands:`;
if (codeQuality === "biome") {
configYaml += `
${packageManager}-fmt:
run: |
# Skip confirmation prompt when Corepack is about to upgrade ${packageManager}.
CI=1 ${pm} check:format
stage_fixed: true`;
} else {
configYaml += `
${packageManager}-fmt:
run: |
# Skip confirmation prompt when Corepack is about to upgrade ${packageManager}.
CI=1 ${pm} format:check
stage_fixed: true`;
}
configYaml += `
skip:
- merge
- rebase
pre-push:
parallel: true
commands:
${packageManager}-check:
run: |
# Skip confirmation prompts when Corepack is about to upgrade ${packageManager}.
CI=1 ${pm} check
post-checkout:
commands:
${packageManager}-install:
# language=sh
run: |
OLD_HEAD_SHA="$(git rev-parse HEAD@{1})"
NEW_HEAD_SHA="$(git rev-parse HEAD)"
DIFF="$(git diff --name-only "$OLD_HEAD_SHA" "$NEW_HEAD_SHA" -- '${lockFile}')"
echo "Evaluating changes between the old HEAD commit '$OLD_HEAD_SHA' and the new HEAD commit '$NEW_HEAD_SHA':"
if echo "$DIFF" | grep -q '${lockFile}'; then
echo "- Changes detected in '${lockFile}'. Running '${packageManager} install'."
# Skip confirmation prompts when Corepack is about to upgrade ${packageManager}.
CI=1 ${packageManager} install
else
echo "- No changes detected in '${lockFile}'. Skipping '${packageManager} install'."
fi
post-rewrite:
commands:
${packageManager}-install:
use_stdin: true
# language=sh
run: |
while read OLD_SHA NEW_SHA EXTRA; do
if [ -z "$FIRST_OLD_SHA" ]; then
FIRST_OLD_SHA="$OLD_SHA"
fi
done
BASE_SHA="$(git rev-parse "$FIRST_OLD_SHA~1")"
REBASED_SHA="$(git rev-parse HEAD)"
DIFF="$(git diff --name-only "$BASE_SHA" "$REBASED_SHA" -- '${lockFile}')"
echo "Evaluating changes between the base commit '$BASE_SHA' and the rebased commit '$REBASED_SHA':"
if echo "$DIFF" | grep -q '${lockFile}'; then
echo "- Changes detected in '${lockFile}'. Running '${packageManager} install'."
# Skip confirmation prompts when Corepack is about to upgrade ${packageManager}.
CI=1 ${packageManager} install
else
echo "- No changes detected in '${lockFile}'. Skipping '${packageManager} install'."
fi
skip_output:
- meta
- summary`;
return configYaml;
}
getLockFileName(packageManager) {
switch (packageManager) {
case "npm":
return "package-lock.json";
case "pnpm":
return "pnpm-lock.yaml";
case "yarn":
return "yarn.lock";
case "bun":
return "bun.lockb";
default:
return "package-lock.json";
}
}
installGitHooks(config) {
if (config.dryRun) {
logger.debug("[DRY RUN] Would install Git hooks");
return;
}
try {
const gitDir = join3(config.projectPath, ".git");
if (!existsSync4(gitDir)) {
logger.warning(
"Git not initialized in project directory, skipping hook installation"
);
return;
}
const pm = config.packageManager;
const command = pm === "npm" ? "npx --prefix . lefthook install" : `${pm} exec lefthook install`;
execSync2(command, {
cwd: config.projectPath,
env: {
...process.env,
// Ensure lefthook only installs in the current directory
LEFTHOOK_EXCLUDE_TAGS: "meta,summary"
},
stdio: "ignore"
});
logger.debug("Git hooks installed");
} catch {
logger.warning("Could not install Git hooks (Git may not be initialized)");
}
}
};
// src/frameworks/base-framework.ts
import { execSync as execSync3 } from "child_process";
import { existsSync as existsSync5 } from "fs";
import { join as join4 } from "path";
var BaseFramework = class {
config;
constructor(config) {
this.config = config;
}
async scaffold(projectConfig) {
const command = this.getStarterCommand(projectConfig);
if (projectConfig.dryRun) {
logger.debug(`[DRY RUN] Would run: ${command}`);
return;
}
logger.info(`Creating ${this.config.displayName} project...`);
logger.debug(`Running: ${command}`);
try {
execSync3(command, {
cwd: process.cwd(),
stdio: projectConfig.debug ? "inherit" : "ignore"
});
logger.success(`${this.config.displayName} project created`);
process.chdir(projectConfig.projectPath);
logger.debug(`Changed working directory to: ${projectConfig.projectPath}`);
} catch (error) {
throw new CLIError(
`Failed to create ${this.config.displayName} project`,
"SCAFFOLD_FAILED",
error
);
}
const tasks = this.getPostInstallTasks(projectConfig);
for (const task of tasks) {
await task();
}
}
async updatePackageJson(projectConfig, updates) {
if (projectConfig.dryRun) {
logger.debug("[DRY RUN] Would update package.json");
return;
}
const packageJsonPath = join4(projectConfig.projectPath, "package.json");
try {
const { readFileSync: readFileSync3, writeFileSync: writeFileSync5 } = await import("fs");
if (!existsSync5(packageJsonPath)) {
logger.debug(
`package.json not found at ${packageJsonPath}, skipping update`
);
return;
}
const packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
for (const [key, value] of Object.entries(updates)) {
if (typeof value === "object" && !Array.isArray(value) && packageJson[key]) {
packageJson[key] = { ...packageJson[key], ...value };
} else {
packageJson[key] = value;
}
}
writeFileSync5(packageJsonPath, JSON.stringify(packageJson, null, 2));
logger.debug("Updated package.json");
} catch (error) {
throw new CLIError(
"Failed to update package.json",
"UPDATE_PACKAGE_JSON_FAILED",
error
);
}
}
getName() {
return this.config.name;
}
getDisplayName() {
return this.config.displayName;
}
getMetaFrameworks() {
return this.config.metaFrameworks;
}
hasMetaFramework(meta) {
return this.config.metaFrameworks.includes(meta);
}
};
// src/frameworks/astro/index.ts
var astroConfig = {
defaultMeta: "none",
displayName: "Astro",
metaFrameworks: ["none"],
name: "astro",
starter: {
args: (config) => {
const args = [];
args.push("--skip-install");
if (config.typescript) {
args.push("--typescript", "strict");
} else {
args.push("--no-typescript");
}
args.push("--template", "minimal");
args.push("--no-git");
return args;
},
command: "create-astro"
}
};
var AstroFramework = class extends BaseFramework {
constructor() {
super(astroConfig);
}
getStarterCommand(projectConfig) {
const { projectName, typescript } = projectConfig;
const args = [
projectName,
"--no-install",
typescript ? "--typescript strict" : "--no-typescript",
"--template minimal",
"--no-git",
"--skip-houston"
// Skip the Houston animation
];
return `npm create astro@latest ${args.join(" ")}`;
}
getPostInstallTasks(projectConfig) {
const tasks = [];
tasks.push(async () => {
await this.updatePackageJson(projectConfig, {
scripts: {
astro: "astro",
build: "astro build",
dev: "astro dev",
preview: "astro preview",
start: "astro dev"
}
});
if (projectConfig.typescript) {
await this.ensureAstroTypeScriptConfig(projectConfig);
}
});
return tasks;
}
async ensureAstroTypeScriptConfig(projectConfig) {
if (projectConfig.dryRun) {
return;
}
const tsConfigPath = `${projectConfig.projectPath}/tsconfig.json`;
try {
const { readFileSync: readFileSync3, writeFileSync: writeFileSync5 } = await import("fs");
const tsConfig = JSON.parse(readFileSync3(tsConfigPath, "utf-8"));
tsConfig.compilerOptions = {
...tsConfig.compilerOptions,
forceConsistentCasingInFileNames: true,
skipLibCheck: true,
strict: true
};
writeFileSync5(tsConfigPath, JSON.stringify(tsConfig, null, 2));
} catch (error) {
logger.info(`Error updating TypeScript config: ${error}`);
}
}
};
// src/frameworks/react/index.ts
var reactConfig = {
defaultMeta: "none",
displayName: "React",
metaFrameworks: ["next", "remix", "none"],
name: "react",
starter: {
args: (config) => {
const args = [];
if (config.typescript) {
args.push("--template", "typescript");
}
return args;
},
command: "create-react-app"
}
};
var ReactFramework = class extends BaseFramework {
constructor() {
super(reactConfig);
}
getStarterCommand(projectConfig) {
const { metaFramework } = projectConfig;
switch (metaFramework) {
case "next":
return this.getNextCommand(projectConfig);
case "remix":
return this.getRemixCommand(projectConfig);
case "none":
return this.getViteReactCommand(projectConfig);
}
}
getNextCommand(projectConfig) {
const { projectName, typescript } = projectConfig;
const args = [
`"${projectName}"`,
typescript ? "--ts" : "--js",
"--app",
"--no-git",
"--skip-install",
"--yes"
];
return `npx create-next-app@latest ${args.join(" ")}`;
}
getRemixCommand(projectConfig) {
const { projectName, typescript } = projectConfig;
const args = [
`"${projectName}"`,
"--skip-install",
typescript ? "--ts" : "--js",
"--yes"
];
return `npx create-remix@latest ${args.join(" ")}`;
}
getViteReactCommand(projectConfig) {
const { projectName, typescript } = projectConfig;
const template = typescript ? "react-ts" : "react";
return `npm create vite@latest ${projectName} -- --template ${template}`;
}
getPostInstallTasks(_projectConfig) {
const tasks = [];
return tasks;
}
};
// src/frameworks/svelte/index.ts
var svelteConfig = {
defaultMeta: "sveltekit",
displayName: "Svelte",
metaFrameworks: ["sveltekit", "none"],
name: "svelte",
starter: {
args: (config) => {
const args = [];
if (config.typescript) {
args.push("--types", "typescript");
} else {
args.push("--types", "null");
}
return args;
},
command: "create-svelte"
}
};
var SvelteFramework = class extends BaseFramework {
constructor() {
super(svelteConfig);
}
getStarterCommand(projectConfig) {
const { metaFramework } = projectConfig;
switch (metaFramework) {
case "sveltekit":
return this.getSvelteKitCommand(projectConfig);
case "none":
return this.getViteSvelteCommand(projectConfig);
default:
return this.getViteSvelteCommand(projectConfig);
}
}
getSvelteKitCommand(projectConfig) {
const { projectName, typescript } = projectConfig;
const args = [
projectName,
"--no-install",
typescript ? "--types typescript" : "--types null",
"--template skeleton",
"--no-prettier",
"--no-eslint",
"--no-playwright",
"--no-vitest"
];
return `npm create svelte@latest ${args.join(" ")}`;
}
getViteSvelteCommand(projectConfig) {
const { projectName, typescript } = projectConfig;
const template = typescript ? "svelte-ts" : "svelte";
return `npm create vite@latest ${projectName} -- --template ${template}`;
}
getPostInstallTasks(projectConfig) {
const tasks = [];
if (projectConfig.metaFramework === "sveltekit") {
tasks.push(async () => {
await this.updatePackageJson(projectConfig, {
scripts: {
build: "vite build",
check: "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
dev: "vite dev",
preview: "vite preview"
}
});
});
}
return tasks;
}
};
// src/frameworks/tailwind/installer.ts
import { existsSync as existsSync6, mkdirSync, writeFileSync as writeFileSync4 } from "fs";
import { join as join5 } from "path";
// src/frameworks/tailwind/templates.ts
function globalsCssTemplate(_data) {
return `@import 'tailwindcss';
/*
* Tailwind CSS v4 Configuration
* Using CSS variables and @theme directive
*/
@theme {
/* Typography */
--font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
--font-serif: Georgia, Cambria, 'Times New Roman', Times, serif;
--font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
/* Colors - Modern palette */
--color-background: #ffffff;
--color-foreground: #0a0a0a;
--color-muted: #6b7280;
--color-border: #e5e7eb;
/* Primary colors */
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-200: #bfdbfe;
--color-primary-300: #93c5fd;
--color-primary-400: #60a5fa;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
--color-primary-800: #1e40af;
--color-primary-900: #1e3a8a;
--color-primary-950: #172554;
/* Secondary colors */
--color-secondary-50: #f8fafc;
--color-secondary-100: #f1f5f9;
--color-secondary-200: #e2e8f0;
--color-secondary-300: #cbd5e1;
--color-secondary-400: #94a3b8;
--color-secondary-500: #64748b;
--color-secondary-600: #475569;
--color-secondary-700: #334155;
--color-secondary-800: #1e293b;
--color-secondary-900: #0f172a;
--color-secondary-950: #020617;
/* Accent colors */
--color-accent: #8b5cf6;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-info: #3b82f6;
/* Spacing */
--spacing-xs: 0.5rem;
--spacing-sm: 0.75rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
--spacing-3xl: 4rem;
/* Border radius */
--radius-sm: 0.125rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
/* Animation */
--animate-fast: 150ms;
--animate-normal: 300ms;
--animate-slow: 500ms;
}
/* Dark mode theme */
@media (prefers-color-scheme: dark) {
@theme {
--color-background: #0a0a0a;
--color-foreground: #fafafa;
--color-muted: #a1a1aa;
--color-border: #27272a;
}
}
/* Base styles */
@layer base {
:root {
color-scheme: light dark;
}
* {
@apply border-border;
}
html {
@apply antialiased;
font-family: var(--font-sans);
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
/* Headings */
h1, h2, h3, h4, h5, h6 {
@apply font-bold tracking-tight;
}
h1 {
@apply text-4xl md:text-5xl;
}
h2 {
@apply text-3xl md:text-4xl;
}
h3 {
@apply text-2xl md:text-3xl;
}
h4 {
@apply text-xl md:text-2xl;
}
h5 {
@apply text-lg md:text-xl;
}
h6 {
@apply text-base md:text-lg;
}
/* Links */
a {
@apply text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300;
@apply transition-colors duration-[var(--animate-fast)];
}
/* Code blocks */
pre, code {
@apply font-mono text-sm;
}
pre {
@apply bg-secondary-100 dark:bg-secondary-900 p-4 rounded-lg overflow-x-auto;
}
code {
@apply bg-secondary-100 dark:bg-secondary-900 px-1 py-0.5 rounded;
}
pre code {
@apply bg-transparent p-0;
}
/* Forms */
input, textarea, select {
@apply w-full px-3 py-2 border rounded-md;
@apply bg-background;
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent;
@apply transition-shadow duration-[var(--animate-fast)];
}
button {
@apply px-4 py-2 font-medium rounded-md;
@apply transition-all duration-[var(--animate-fast)];
@apply focus:outline-none focus:ring-2 focus:ring-offset-2;
}
}
/* Utility classes */
@layer utilities {
/* Hide scrollbar but keep functionality */
.scrollbar-none {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-none::-webkit-scrollbar {
display: none;
}
/* Gradient text */
.gradient-text {
@apply bg-gradient-to-r from-primary-600 to-accent bg-clip-text text-transparent;
}
/* Glass effect */
.glass {
@apply bg-white/80 dark:bg-black/80 backdrop-blur-md;
}
/* Animation utilities */
.animate-in {
animation: animateIn var(--animate-normal) ease-out;
}
@keyframes animateIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
/* Components */
@layer components {
/* Base button styles */
.btn {
@apply inline-flex items-center justify-center font-medium rounded-md;
@apply px-4 py-2 text-sm;
@apply transition-all duration-[var(--animate-fast)];
@apply focus:outline-none focus:ring-2 focus:ring-offset-2;
@apply disabled:opacity-50 disabled:cursor-not-allowed;
}
/* Button variants - no longer using @apply btn */
.btn-primary {
@apply inline-flex items-center justify-center font-medium rounded-md;
@apply px-4 py-2 text-sm;
@apply transition-all duration-[var(--animate-fast)];
@apply focus:outline-none focus:ring-2 focus:ring-offset-2;
@apply disabled:opacity-50 disabled:cursor-not-allowed;
@apply bg-primary-600 text-white hover:bg-primary-700;
@apply focus:ring-primary-500;
}
.btn-secondary {
@apply inline-flex items-center justify-center font-medium rounded-md;
@apply px-4 py-2 text-sm;
@apply transition-all duration-[var(--animate-fast)];
@apply focus:outline-none focus:ring-2 focus:ring-offset-2;
@apply disabled:opacity-50 disabled:cursor-not-allowed;
@apply bg-secondary-100 text-secondary-900 hover:bg-secondary-200;
@apply dark:bg-secondary-800 dark:text-secondary-100 dark:hover:bg-secondary-700;
@apply focus:ring-secondary-500;
}
.btn-ghost {
@apply inline-flex items-center justify-center font-medium rounded-md;
@apply px-4 py-2 text-sm;
@apply transition-all duration-[var(--animate-fast)];
@apply focus:outline-none focus:ring-2 focus:ring-offset-2;
@apply disabled:opacity-50 disabled:cursor-not-allowed;
@apply hover:bg-secondary-100 dark:hover:bg-secondary-800;
@apply focus:ring-secondary-500;
}
/* Card component */
.card {
@apply bg-white dark:bg-secondary-900;
@apply border border-border;
@apply rounded-lg shadow-sm;
@apply p-6;
}
/* Input component */
.input {
@apply w-full px-3 py-2;
@apply bg-background border border-border rounded-md;
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent;
@apply placeholder:text-muted;
@apply transition-shadow duration-[var(--animate-fast)];
}
/* Container */
.container {
@apply w-full mx-auto px-4 sm:px-6 lg:px-8;
@apply max-w-7xl;
}
}`;
}
function postcssConfigTemplate() {
return `/** @type {import('postcss').Config} */
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
};`;
}
// src/frameworks/tailwind/installer.ts
var TailwindInstaller = class {
name = "Tailwind CSS v4";
async install(config) {
logger.info("Setting up Tailwind CSS v4...");
if (config.dryRun) {
logger.debug(
"[DRY RUN] Would install Tailwind, write globals.css and postcss.config.js"
);
return;
}
const pm = new PackageManagerAdapter(config.packageManager);
await pm.add(
["tailwindcss@latest", "@tailwindcss/postcss@latest"],
true,
config
);
this.createGlobalsCss(config);
await this.createPostCSSConfig(config);
this.updateMainCssImport(config);
logger.success("Tailwind CSS v4 configured successfully");
}
createGlobalsCss(config) {
const cssPath = this.getCssPath(config);
const cssDir = cssPath.substring(0, cssPath.lastIndexOf("/"));
if (config.dryRun) {
logger.debug(`[DRY RUN] Would create CSS at: ${cssPath}`);
return;
}
try {
if (cssDir && !existsSync6(join5(config.projectPath, cssDir))) {
mkdirSync(join5(config.projectPath, cssDir), { recursive: true });
}
const content = globalsCssTemplate({
framework: config.framework,
metaFramework: config.metaFramework
});
writeFileSync4(join5(config.projectPath, cssPath), content);
logger.debug(`Created Tailwind CSS file at: ${cssPath}`);
} catch (error) {
logger.warning(`Could not create Tailwind CSS file: ${error.message}`);
}
}
async createPostCSSConfig(config) {
const configPath = join5(config.projectPath, "postcss.config.js");
const mjsConfigPath = join5(config.projectPath, "postcss.config.mjs");
if (config.dryRun) {
logger.debug("[DRY RUN] Would create PostCSS config");
return;
}
try {
if (existsSync6(mjsConfigPath)) {
const { unlinkSync } = await import("fs");
unlinkSync(mjsConfigPath);
logger.debug("Removed existing postcss.config.mjs");
}
writeFileSync4(configPath, postcssConfigTemplate());
logger.debug("Created PostCSS config");
} catch (error) {
logger.warning(`Could not create PostCSS config: ${error.message}`);
}
}
getCssPath(config) {
const { framework, metaFramework } = config;
const paths = {
astro: "src/styles/global.css",
default: "src/styles/globals.css",
next: "src/app/globals.css",
nuxt: "assets/css/main.css",
remix: "app/styles/globals.css",
sveltekit: "src/app.css"
};
return paths[metaFramework] || paths[framework] || paths.default;
}
updateMainCssImport(_config) {
logger.debug("CSS import paths updated");
}
};
// src/frameworks/vanilla/index.ts
var vanillaConfig = {
defaultMeta: "none",
displayName: "Vanilla JavaScript",
metaFrameworks: ["none"],
name: "vanilla",
starter: {
args: (config) => {
const args = [];
args.push("--template", config.typescript ? "vanilla-ts" : "vanilla");
return args;
},
command: "create-vite"
}
};
var VanillaFramework = class extends BaseFramework {
constructor() {
super(vanillaConfig);
}
getStarterCommand(projectConfig) {
const { projectName, typescript } = projectConfig;
const template = typescript ? "vanilla-ts" : "vanilla";
return `npm create vite@latest ${projectName} -- --template ${template}`;
}
getPostInstallTasks(projectConfig) {
const tasks = [];
tasks.push(async () => {
await this.updatePackageJson(projectConfig, {
scripts: {
build: "vite build",
dev: "vite",
preview: "vite preview"
}
});
await this.createProjectStructure(projectConfig);
});
return tasks;
}
async createProjectStructure(projectConfig) {
if (projectConfig.dryRun) {
return;
}
const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync5, existsSync: existsSync9 } = await import("fs");
const { join: join9 } = await import("path");
const dirs = ["src/js", "src/css", "src/assets", "src/components"];
for (const dir of dirs) {
const dirPath = join9(projectConfig.projectPath, dir);
if (!existsSync9(dirPath)) {
mkdirSync2(dirPath, { recursive: true });
}
}
const componentExample = projectConfig.typescript ? `// src/components/counter.ts
export class Counter {
private count = 0;
private element: HTMLElement;
private displayElement: HTMLElement;
constructor(element: HTMLElement) {
this.element = element;
this.render();
this.attachEventListeners();
}
private render(): void {
this.element.innerHTML = \`
<div class="counter">
<h2>Counter Component</h2>
<div class="counter-display">Count: <span id="count-display">0</span></div>
<div class="counter-buttons">
<button id="decrement">-</button>
<button id="increment">+</button>
<button id="reset">Reset</button>
</div>
</div>
\`;
this.displayElement = this.element.querySelector('#count-display')!;
}
private attachEventListeners(): void {
const incrementBtn = this.element.querySelector('#increment');
const decrementBtn = this.element.querySelector('#decrement');
const resetBtn = this.element.querySelector('#reset');
incrementBtn?.addEventListener('click', () => this.increment());
decrementBtn?.addEventListener('click', () => this.decrement());
resetBtn?.addEventListener('click', () => this.reset());
}
private increment(): void {
this.count++;
this.updateDisplay();
}
private decrement(): void {
this.count--;
this.updateDisplay();
}
private reset(): void {
this.count = 0;
this.updateDisplay();
}
private updateDisplay(): void {
this.displayElement.textContent = this.count.toString();
}
}` : `// src/components/counter.js
export class Counter {
constructor(element) {
this.count = 0;
this.element = element;
this.render();
this.attachEventListeners();
}
render() {
this.element.innerHTML = \`
<div class="counter">
<h2>Counter Component</h2>
<div class="counter-display">Count: <span id="count-display">0</span></div>
<div class="counter-buttons">
<button id="decrement">-</button>
<button id="increment">+</button>
<button id="reset">Reset</button>
</div>
</div>
\`;
this.displayElement = this.element.querySelector('#count-display');
}
attachEventListeners() {
const incrementBtn = this.element.querySelector('#increment');
const decrementBtn = this.element.querySelector('#decrement');
const resetBtn = this.element.querySelector('#reset');
incrementBtn?.addEventListener('click', () => this.increment());
decrementBtn?.addEventListener('click', () => this.decrement());
resetBtn?.addEventListener('click', () => this.reset());
}
increment() {
this.count++;
this.updateDisplay();
}
decrement() {
this.count--;
this.