UNPKG

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
#!/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.