Last active
June 18, 2025 04:43
-
-
Save jalateras/2ce3c1345c98f115ed76478248507a85 to your computer and use it in GitHub Desktop.
This shell script bootstraps a modern TypeScript-based Command Line Interface (CLI) application with a full development and testing toolchain. It sets up a ready-to-use project that supports ESM modules, includes robust linting, testing, coverage reporting, and build tooling, and integrates Git hooks for quality enforcement.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ------------------------------------------------------------------ | |
| # bootstrap-ts-cli.sh — create a TypeScript CLI scaffold | |
| # stack: npm · TypeScript · tsx · tsup · Vitest · Commander · Husky | |
| # ------------------------------------------------------------------ | |
| set -euo pipefail | |
| # Error handling | |
| trap 'error "❌ An error occurred. Exiting..."' ERR | |
| # Colored output functions | |
| info() { echo -e "\033[0;34m$1\033[0m"; } | |
| success() { echo -e "\033[0;32m$1\033[0m"; } | |
| error() { echo -e "\033[0;31m$1\033[0m" >&2; } | |
| # ---- 1. basic argument handling ---------------------------------- | |
| if [[ $# -ne 1 ]]; then | |
| error "Usage: $0 <project-name>" | |
| exit 1 | |
| fi | |
| APP="$1" | |
| # Validate project name | |
| if [[ ! "$APP" =~ ^[a-z0-9-]+$ ]]; then | |
| error "❌ Project name should only contain lowercase letters, numbers, and hyphens" | |
| exit 1 | |
| fi | |
| # Prevent accidental overwrite | |
| if [[ -e "$APP" ]]; then | |
| error "❌ Directory '$APP' already exists. Please choose another name." | |
| exit 1 | |
| fi | |
| info "🚀 Creating TypeScript CLI project: $APP" | |
| # ---- 2. create repo skeleton ------------------------------------- | |
| mkdir -p "$APP"/{src/{commands/__tests__,lib,types},.husky,.github/workflows} | |
| cd "$APP" | |
| git init --initial-branch=main -q | |
| # ---- 3. initialise package --------------------------------------- | |
| info "📦 Initializing package manager..." | |
| npm init -y | |
| # mark project as ESM | |
| npm pkg set type=module | |
| # add bin field for CLI | |
| npm pkg set bin.${APP}="./dist/cli.js" | |
| # ---- 4. install runtime + dev deps -------------------------------- | |
| info "📥 Installing dependencies..." | |
| npm install commander chalk read-pkg | |
| npm install --save-dev \ | |
| typescript tsx tsup @types/node \ | |
| vitest @vitest/coverage-v8 \ | |
| eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin \ | |
| prettier eslint-config-prettier \ | |
| husky lint-staged \ | |
| @commitlint/cli @commitlint/config-conventional \ | |
| rimraf | |
| # ---- 5. configure package.json scripts & hooks -------------------- | |
| npm pkg set scripts.dev="tsx watch src/cli.ts" | |
| npm pkg set scripts.typecheck="tsc --noEmit" | |
| npm pkg set scripts.lint="eslint src --ext .ts" | |
| npm pkg set scripts.format="prettier --write 'src/**/*.{ts,json}'" | |
| npm pkg set scripts.test="vitest" | |
| npm pkg set scripts.test:coverage="vitest --coverage" | |
| npm pkg set scripts.clean="rimraf dist coverage" | |
| npm pkg set scripts.build="tsup" | |
| npm pkg set scripts.start="node dist/cli.js" | |
| npm pkg set scripts.prepare="husky" | |
| # lint-staged runs ESLint and Prettier on staged files | |
| # Use Node.js to modify package.json directly | |
| node -e ' | |
| const fs = require("fs"); | |
| const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); | |
| pkg["lint-staged"] = { | |
| "*.ts": ["eslint --fix", "prettier --write"], | |
| "*.json": "prettier --write" | |
| }; | |
| fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n"); | |
| ' | |
| # ---- 6. baseline config files ------------------------------------ | |
| info "📝 Creating configuration files..." | |
| # Enhanced TypeScript configuration | |
| cat > tsconfig.json <<'JSON' | |
| { | |
| "compilerOptions": { | |
| "target": "ES2022", | |
| "module": "NodeNext", | |
| "moduleResolution": "NodeNext", | |
| "outDir": "dist", | |
| "rootDir": "src", | |
| "strict": true, | |
| "noUncheckedIndexedAccess": true, | |
| "noImplicitOverride": true, | |
| "esModuleInterop": true, | |
| "allowSyntheticDefaultImports": true, | |
| "resolveJsonModule": true, | |
| "sourceMap": true, | |
| "declaration": true, | |
| "declarationMap": true, | |
| "skipLibCheck": true, | |
| "forceConsistentCasingInFileNames": true, | |
| "verbatimModuleSyntax": true | |
| }, | |
| "include": ["src/**/*"], | |
| "exclude": ["**/*.test.ts", "**/*.spec.ts", "dist", "node_modules"] | |
| } | |
| JSON | |
| # tsup configuration | |
| cat > tsup.config.ts <<'TS' | |
| import { defineConfig } from 'tsup'; | |
| export default defineConfig({ | |
| entry: ['src/cli.ts'], | |
| format: ['esm', 'cjs'], | |
| dts: true, | |
| sourcemap: true, | |
| clean: true, | |
| minify: true, | |
| splitting: false, | |
| shims: true, | |
| banner: { | |
| js: '#!/usr/bin/env node', | |
| }, | |
| onSuccess: async () => { | |
| await import('fs').then(fs => | |
| fs.promises.copyFile('package.json', 'dist/package.json') | |
| ); | |
| }, | |
| }); | |
| TS | |
| # Vitest configuration | |
| cat > vitest.config.ts <<'TS' | |
| import { defineConfig } from 'vitest/config'; | |
| export default defineConfig({ | |
| test: { | |
| globals: true, | |
| environment: 'node', | |
| coverage: { | |
| reporter: ['text', 'json', 'html'], | |
| exclude: ['node_modules/', 'dist/', '**/*.config.ts'] | |
| } | |
| } | |
| }); | |
| TS | |
| # ESLint configuration | |
| cat > eslint.config.js <<'JS' | |
| import tseslint from '@typescript-eslint/eslint-plugin'; | |
| import tsParser from '@typescript-eslint/parser'; | |
| import eslintConfigPrettier from 'eslint-config-prettier'; | |
| export default [ | |
| { | |
| ignores: ['dist/**', '*.cjs', '*.config.ts', 'eslint.config.js'], | |
| }, | |
| { | |
| files: ['src/**/*.ts'], | |
| ignores: ['src/**/*.test.ts', 'src/**/*.spec.ts'], | |
| languageOptions: { | |
| parser: tsParser, | |
| parserOptions: { | |
| project: './tsconfig.json', | |
| tsconfigRootDir: import.meta.dirname, | |
| }, | |
| ecmaVersion: 2022, | |
| sourceType: 'module', | |
| }, | |
| plugins: { | |
| '@typescript-eslint': tseslint, | |
| }, | |
| rules: { | |
| ...tseslint.configs.recommended.rules, | |
| ...tseslint.configs['recommended-requiring-type-checking'].rules, | |
| }, | |
| }, | |
| { | |
| files: ['src/**/*.test.ts', 'src/**/*.spec.ts'], | |
| languageOptions: { | |
| parser: tsParser, | |
| ecmaVersion: 2022, | |
| sourceType: 'module', | |
| }, | |
| plugins: { | |
| '@typescript-eslint': tseslint, | |
| }, | |
| rules: { | |
| ...tseslint.configs.recommended.rules, | |
| }, | |
| }, | |
| eslintConfigPrettier, | |
| ]; | |
| JS | |
| # Prettier configuration | |
| cat > .prettierrc.json <<'JSON' | |
| { | |
| "singleQuote": true, | |
| "semi": true, | |
| "trailingComma": "all", | |
| "printWidth": 100, | |
| "tabWidth": 2 | |
| } | |
| JSON | |
| # Enhanced .gitignore | |
| cat > .gitignore <<'EOF' | |
| node_modules | |
| dist | |
| coverage | |
| .DS_Store | |
| .env* | |
| *.log | |
| .vscode | |
| .idea | |
| *.swp | |
| *.swo | |
| .nyc_output | |
| tmp | |
| *.tsbuildinfo | |
| EOF | |
| # Commitlint configuration | |
| cat > commitlint.config.cjs <<'CJS' | |
| module.exports = { extends: ['@commitlint/config-conventional'] }; | |
| CJS | |
| # Node version file | |
| cat > .nvmrc <<'EOF' | |
| 22 | |
| EOF | |
| # ---- 7. README.md ------------------------------------------------ | |
| cat > README.md <<EOF | |
| # ${APP} | |
| A TypeScript CLI application bootstrapped with modern tooling. | |
| ## 🚀 Features | |
| - **TypeScript** - Type-safe development | |
| - **Commander.js** - Robust CLI framework | |
| - **tsup** - Lightning-fast bundling | |
| - **Vitest** - Modern testing framework | |
| - **ESLint & Prettier** - Code quality and formatting | |
| - **Husky & lint-staged** - Git hooks for quality control | |
| ## 📦 Installation | |
| \`\`\`bash | |
| # Install dependencies | |
| npm install | |
| # Build the CLI | |
| npm run build | |
| # Link for local development | |
| npm link | |
| \`\`\` | |
| ## 🛠️ Development | |
| \`\`\`bash | |
| npm run dev # Watch mode with tsx | |
| npm test # Run tests with Vitest | |
| npm run test:coverage # Run tests with coverage | |
| npm run lint # Lint with ESLint | |
| npm run format # Format with Prettier | |
| npm run typecheck # Type checking | |
| npm run build # Build for production | |
| \`\`\` | |
| ## 📖 Usage | |
| After building: | |
| \`\`\`bash | |
| # Check version | |
| node dist/cli.js version | |
| node dist/cli.js version --json | |
| # Built-in help | |
| node dist/cli.js --help | |
| \`\`\` | |
| After linking: | |
| \`\`\`bash | |
| ${APP} version --json | |
| ${APP} --help | |
| \`\`\` | |
| ## 🏗️ Project Structure | |
| \`\`\` | |
| ${APP}/ | |
| ├── src/ | |
| │ ├── commands/ # CLI commands | |
| │ │ └── __tests__/ # Command tests | |
| │ ├── lib/ # Shared utilities | |
| │ ├── types/ # TypeScript types | |
| │ └── cli.ts # Entry point | |
| ├── dist/ # Compiled output | |
| ├── coverage/ # Test coverage reports | |
| └── package.json | |
| \`\`\` | |
| ## 📄 License | |
| MIT | |
| EOF | |
| # ---- 8. GitHub Actions CI workflow ------------------------------- | |
| cat > .github/workflows/ci.yml <<'YAML' | |
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| jobs: | |
| test: | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| node-version: [22.x] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Use Node.js ${{ matrix.node-version }} | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Lint | |
| run: npm run lint | |
| - name: Type check | |
| run: npm run typecheck | |
| - name: Test | |
| run: npm run test:coverage | |
| - name: Build | |
| run: npm run build | |
| - name: Test CLI | |
| run: node dist/cli.js version | |
| YAML | |
| # ---- 9. Makefile ------------------------------------------------- | |
| cat > Makefile <<'MAKE' | |
| SHELL := bash | |
| .ONESHELL: | |
| .SHELLFLAGS := -eu -o pipefail -c | |
| PKG_MGR ?= npm | |
| .PHONY: help install dev lint typecheck test build clean format start ci | |
| help: ## Show this help | |
| @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' | |
| install: ## Install dependencies | |
| $(PKG_MGR) ci | |
| lint: ## Run ESLint | |
| $(PKG_MGR) run lint | |
| typecheck: ## Run TypeScript type checking | |
| $(PKG_MGR) run typecheck | |
| test: ## Run tests | |
| $(PKG_MGR) test | |
| test-coverage: ## Run tests with coverage | |
| $(PKG_MGR) run test:coverage | |
| format: ## Format code with Prettier | |
| $(PKG_MGR) run format | |
| clean: ## Clean build artifacts | |
| $(PKG_MGR) run clean | |
| build: clean ## Build for production | |
| $(PKG_MGR) run build | |
| chmod +x dist/cli.js | |
| ci: ## Run CI pipeline locally | |
| @echo "Running CI pipeline..." | |
| make lint | |
| make typecheck | |
| make test-coverage | |
| make build | |
| @echo "✅ CI pipeline passed!" | |
| # Wrapper to exec any sub-command: e.g., make run version | |
| run: build ## Run CLI commands (usage: make run <command>) | |
| node dist/cli.js $(filter-out $@,$(MAKECMDGOALS)) | |
| # Prevent make from interpreting extra args as targets | |
| %: | |
| @: | |
| MAKE | |
| # ---- 10. Husky & Commitlint hooks -------------------------------- | |
| info "🪝 Setting up Git hooks..." | |
| # Initialize Husky (modern way) | |
| npx husky init | |
| # Create pre-commit hook | |
| cat > .husky/pre-commit <<'HOOK' | |
| npx lint-staged | |
| HOOK | |
| # Create commit-msg hook | |
| cat > .husky/commit-msg <<'HOOK' | |
| npx commitlint --edit $1 | |
| HOOK | |
| # Make hooks executable | |
| chmod +x .husky/pre-commit | |
| chmod +x .husky/commit-msg | |
| # ---- 11. source files -------------------------------------------- | |
| info "📄 Creating source files..." | |
| # Simple CLI entry point with version command | |
| cat > src/cli.ts <<'TS' | |
| import { Command } from 'commander'; | |
| import chalk from 'chalk'; | |
| import { readFileSync } from 'node:fs'; | |
| import { dirname, join } from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { versionCommand } from './commands/version.js'; | |
| interface PackageJson { | |
| name: string; | |
| version: string; | |
| description?: string; | |
| } | |
| const __dirname = dirname(fileURLToPath(import.meta.url)); | |
| const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8')) as PackageJson; | |
| const program = new Command() | |
| .name(pkg.name) | |
| .description(pkg.description ?? 'A TypeScript CLI application') | |
| .version(pkg.version) | |
| .addHelpText( | |
| 'after', | |
| `\n${chalk.gray('For more information, visit: https://github.com/your-username/' + pkg.name)}`, | |
| ) | |
| .addCommand(versionCommand); // Version command | |
| // Global error handling | |
| process.on('unhandledRejection', (reason: Error) => { | |
| console.error(chalk.red('Unhandled rejection:'), reason.message); | |
| process.exit(1); | |
| }); | |
| process.on('uncaughtException', (error: Error) => { | |
| console.error(chalk.red('Uncaught exception:'), error.message); | |
| process.exit(1); | |
| }); | |
| // Parse CLI arguments | |
| program.parseAsync(process.argv).catch((err: Error) => { | |
| console.error(chalk.red('Error:'), err.message); | |
| process.exit(1); | |
| }); | |
| TS | |
| # Enhanced version command (sync) | |
| cat > src/commands/version.ts <<'TS' | |
| import { Command } from 'commander'; | |
| import chalk from 'chalk'; | |
| import { readPackage } from 'read-pkg'; | |
| import { readFileSync } from 'fs'; | |
| import { fileURLToPath } from 'url'; | |
| import { dirname, resolve } from 'path'; | |
| interface PackageJson { | |
| name: string; | |
| version: string; | |
| description?: string; | |
| author?: string | { name: string; email?: string }; | |
| license?: string; | |
| } | |
| // APPROACH 1: Use read-pkg's built-in search (RECOMMENDED) | |
| async function getPackageAutoSearch(): Promise<PackageJson> { | |
| try { | |
| // read-pkg automatically searches up the directory tree from cwd | |
| const packageJson = await readPackage(); | |
| return packageJson; | |
| } catch (error) { | |
| throw new Error(`Could not find package.json: ${error instanceof Error ? error.message : String(error)}`); | |
| } | |
| } | |
| // APPROACH 2: Read from a specific, known location | |
| function getPackageFromBundled(): PackageJson { | |
| try { | |
| const __dirname = dirname(fileURLToPath(import.meta.url)); | |
| // If you bundle package.json with your CLI distribution | |
| const packagePath = resolve(__dirname, 'package.json'); | |
| const packageJson = JSON.parse(readFileSync(packagePath, 'utf8')) as PackageJson; | |
| return packageJson; | |
| } catch (error) { | |
| throw new Error(`Could not read bundled package.json: ${error instanceof Error ? error.message : String(error)}`); | |
| } | |
| } | |
| // APPROACH 3: Embed version info at build time (BEST for distributed CLIs) | |
| const EMBEDDED_VERSION = process.env.CLI_VERSION || '0.0.0'; | |
| const EMBEDDED_NAME = process.env.CLI_NAME || 'unknown-cli'; | |
| function getEmbeddedPackageInfo(): PackageJson { | |
| return { | |
| name: EMBEDDED_NAME, | |
| version: EMBEDDED_VERSION, | |
| description: process.env.CLI_DESCRIPTION, | |
| author: process.env.CLI_AUTHOR, | |
| license: process.env.CLI_LICENSE, | |
| }; | |
| } | |
| // APPROACH 4: Use import.meta.resolve (Node 20.6+) | |
| function getPackageViaImportMeta(): PackageJson { | |
| try { | |
| // This resolves relative to the current module | |
| const packageUrl = import.meta.resolve('./package.json'); | |
| const packagePath = fileURLToPath(packageUrl); | |
| const packageJson = JSON.parse(readFileSync(packagePath, 'utf8')) as PackageJson; | |
| return packageJson; | |
| } catch (error) { | |
| throw new Error(`Could not resolve package.json via import.meta: ${error instanceof Error ? error.message : String(error)}`); | |
| } | |
| } | |
| function formatAuthor(author?: string | { name: string; email?: string }): string | undefined { | |
| if (!author) return undefined; | |
| if (typeof author === 'string') { | |
| return author; | |
| } | |
| return author.email ? `${author.name} <${author.email}>` : author.name; | |
| } | |
| export const versionCommand = new Command('version') | |
| .description('Show CLI version and metadata') | |
| .option('-j, --json', 'Output in JSON format') | |
| .option('--method <method>', 'Method to use: auto, bundled, embedded, import-meta', 'auto') | |
| .action(async (options: { json?: boolean; method?: string }) => { | |
| try { | |
| let pkg: PackageJson; | |
| switch (options.method) { | |
| case 'bundled': | |
| pkg = getPackageFromBundled(); | |
| break; | |
| case 'embedded': | |
| pkg = getEmbeddedPackageInfo(); | |
| break; | |
| case 'import-meta': | |
| pkg = getPackageViaImportMeta(); | |
| break; | |
| case 'auto': | |
| default: | |
| pkg = await getPackageAutoSearch(); | |
| break; | |
| } | |
| if (options.json) { | |
| console.log(JSON.stringify(pkg, null, 2)); | |
| return; | |
| } | |
| console.log(chalk.bold.blue(`${pkg.name} v${pkg.version}`)); | |
| const metadata: Array<[string, string | undefined]> = [ | |
| ['Description', pkg.description], | |
| ['Author', formatAuthor(pkg.author)], | |
| ['License', pkg.license], | |
| ]; | |
| metadata.forEach(([key, value]) => { | |
| if (value) { | |
| console.log(`${chalk.gray(key + ':')} ${value}`); | |
| } | |
| }); | |
| } catch (error) { | |
| console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error'); | |
| process.exit(1); | |
| } | |
| }); | |
| TS | |
| # Sample test file | |
| cat > src/commands/__tests__/version.test.ts <<'TS' | |
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; | |
| import { versionCommand } from '../version.js'; | |
| describe('version command', () => { | |
| let consoleLogSpy: ReturnType<typeof vi.spyOn>; | |
| beforeEach(() => { | |
| consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); | |
| }); | |
| afterEach(() => { | |
| consoleLogSpy.mockRestore(); | |
| }); | |
| it('should have correct command name', () => { | |
| expect(versionCommand.name()).toBe('version'); | |
| }); | |
| it('should have a description', () => { | |
| expect(versionCommand.description()).toBeTruthy(); | |
| }); | |
| it('should display version information', async () => { | |
| await versionCommand.parseAsync(['node', 'test']); | |
| expect(consoleLogSpy).toHaveBeenCalled(); | |
| }); | |
| it('should output JSON when --json flag is used', async () => { | |
| await versionCommand.parseAsync(['node', 'test', '--json']); | |
| expect(consoleLogSpy).toHaveBeenCalled(); | |
| const output = consoleLogSpy.mock.calls[0][0]; | |
| expect(() => JSON.parse(output)).not.toThrow(); | |
| }); | |
| }); | |
| TS | |
| # Basic type definitions | |
| cat > src/types/index.ts <<'TS' | |
| // CLI configuration types | |
| export interface CliConfig { | |
| verbose?: boolean; | |
| quiet?: boolean; | |
| debug?: boolean; | |
| } | |
| // Command result types | |
| export interface CommandResult<T = unknown> { | |
| success: boolean; | |
| message?: string; | |
| data?: T; | |
| error?: Error; | |
| } | |
| // Command options base interface | |
| export interface BaseCommandOptions { | |
| verbose?: boolean; | |
| quiet?: boolean; | |
| } | |
| TS | |
| # Simple logger utility | |
| cat > src/lib/logger.ts <<'TS' | |
| import chalk from 'chalk'; | |
| export interface LoggerOptions { | |
| silent?: boolean; | |
| timestamps?: boolean; | |
| } | |
| export class Logger { | |
| private silent: boolean; | |
| private timestamps: boolean; | |
| constructor(options: LoggerOptions = {}) { | |
| this.silent = options.silent ?? false; | |
| this.timestamps = options.timestamps ?? false; | |
| } | |
| private formatMessage(message: string): string { | |
| if (this.timestamps) { | |
| const timestamp = new Date().toISOString(); | |
| return `[${timestamp}] ${message}`; | |
| } | |
| return message; | |
| } | |
| info(message: string): void { | |
| if (!this.silent) { | |
| console.log(chalk.blue('ℹ'), this.formatMessage(message)); | |
| } | |
| } | |
| success(message: string): void { | |
| if (!this.silent) { | |
| console.log(chalk.green('✔'), this.formatMessage(message)); | |
| } | |
| } | |
| warning(message: string): void { | |
| if (!this.silent) { | |
| console.warn(chalk.yellow('⚠'), this.formatMessage(message)); | |
| } | |
| } | |
| error(message: string): void { | |
| console.error(chalk.red('✖'), this.formatMessage(message)); | |
| } | |
| debug(message: string): void { | |
| if (process.env.DEBUG) { | |
| console.log(chalk.gray('●'), this.formatMessage(message)); | |
| } | |
| } | |
| } | |
| export const logger = new Logger(); | |
| TS | |
| # Create command base classes for better organization | |
| cat > src/lib/command-base.ts <<'TS' | |
| import { Command } from 'commander'; | |
| import { logger } from './logger.js'; | |
| import type { BaseCommandOptions } from '../types/index.js'; | |
| export abstract class BaseCommand { | |
| protected command: Command; | |
| constructor(name: string, description: string) { | |
| this.command = new Command(name).description(description); | |
| this.setupOptions(); | |
| this.setupAction(); | |
| } | |
| protected setupOptions(): void { | |
| // Base options that all commands share | |
| this.command | |
| .option('-v, --verbose', 'Enable verbose output') | |
| .option('-q, --quiet', 'Suppress non-error output'); | |
| } | |
| protected abstract setupAction(): void; | |
| protected createLogger(): typeof logger { | |
| return logger; | |
| } | |
| public getCommand(): Command { | |
| return this.command; | |
| } | |
| } | |
| // For synchronous commands | |
| export abstract class SyncCommand< | |
| T extends BaseCommandOptions = BaseCommandOptions, | |
| > extends BaseCommand { | |
| protected abstract execute(args: unknown, options: T): void; | |
| protected setupAction(): void { | |
| this.command.action((args: unknown, options: T) => { | |
| try { | |
| this.execute(args, options); | |
| } catch (error) { | |
| logger.error(error instanceof Error ? error.message : 'Unknown error'); | |
| process.exit(1); | |
| } | |
| }); | |
| } | |
| } | |
| // For asynchronous commands | |
| export abstract class AsyncCommand< | |
| T extends BaseCommandOptions = BaseCommandOptions, | |
| > extends BaseCommand { | |
| protected abstract execute(args: unknown, options: T): Promise<void>; | |
| protected setupAction(): void { | |
| this.command.action(async (args: unknown, options: T) => { | |
| try { | |
| await this.execute(args, options); | |
| } catch (error) { | |
| logger.error(error instanceof Error ? error.message : 'Unknown error'); | |
| process.exit(1); | |
| } | |
| }); | |
| } | |
| } | |
| TS | |
| chmod +x src/cli.ts | |
| # ---- 12. first commit -------------------------------------------- | |
| info "📸 Creating initial commit..." | |
| git add . | |
| git commit -qm "chore: bootstrap TypeScript CLI scaffold with modern tooling" | |
| # ---- 13. final instructions -------------------------------------- | |
| success " | |
| ✅ Project '$APP' successfully created! | |
| 📂 Project structure: | |
| $APP/ | |
| ├── src/ # Source files | |
| ├── dist/ # Build output | |
| ├── coverage/ # Test coverage | |
| └── .github/ # CI workflows | |
| 🚀 Quick start: | |
| cd $APP | |
| make help # Show all available commands | |
| npm run dev # Start development mode | |
| npm test # Run tests" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment