bitch-cli/src/commands/init.ts
2026-03-08 19:39:00 -07:00

324 lines
8.4 KiB
TypeScript

import { Command } from 'commander'
import ora from 'ora'
import { mkdir, writeFile, access } from 'node:fs/promises'
import { join } from 'node:path'
import { simpleGit } from 'simple-git'
import { colors, logInfo, logError } from '../utils/output.js'
type PackageType = 'react' | 'nestjs' | 'base' | 'python'
const TEMPLATES: Record<PackageType, {
packageJson: (name: string) => object
files: Record<string, string>
}> = {
react: {
packageJson: (name: string) => ({
name: `@lilith/${name}`,
version: '1.0.0',
type: 'module',
main: './dist/index.js',
module: './dist/index.mjs',
types: './dist/index.d.ts',
exports: {
'.': {
import: './dist/index.mjs',
require: './dist/index.js',
types: './dist/index.d.ts',
},
},
scripts: {
build: 'tsup src/index.ts --format cjs,esm --dts',
dev: 'tsup src/index.ts --format cjs,esm --dts --watch',
lint: 'eslint src/',
typecheck: 'tsc --noEmit',
},
peerDependencies: {
react: '>=18.0.0',
},
devDependencies: {
'@lilith/eslint-config-react': '^1.0.0',
'@lilith/typescript-config-react': '^1.0.0',
'@types/react': '^18.2.0',
eslint: '^9.0.0',
react: '^18.2.0',
tsup: '^8.0.0',
typescript: '^5.3.0',
},
publishConfig: {
registry: 'http://forge.black.local/api/packages/lilith/npm/',
},
_: {
publish: true,
build: true,
},
}),
files: {
'src/index.ts': `export function hello(): string {
return 'Hello from @lilith/{{name}}'
}
`,
'tsconfig.json': `{
"extends": "@lilith/typescript-config-react",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
`,
},
},
nestjs: {
packageJson: (name: string) => ({
name: `@lilith/${name}`,
version: '1.0.0',
type: 'module',
main: './dist/index.js',
types: './dist/index.d.ts',
scripts: {
build: 'tsc',
dev: 'tsc --watch',
lint: 'eslint src/',
typecheck: 'tsc --noEmit',
},
dependencies: {
'@nestjs/common': '^10.0.0',
},
devDependencies: {
'@lilith/eslint-config-nestjs': '^1.0.0',
'@lilith/typescript-config-nestjs': '^1.0.0',
'@types/node': '^22.0.0',
eslint: '^9.0.0',
typescript: '^5.3.0',
},
publishConfig: {
registry: 'http://forge.black.local/api/packages/lilith/npm/',
},
_: {
publish: true,
build: true,
},
}),
files: {
'src/index.ts': `export * from './module'
`,
'src/module.ts': `import { Module } from '@nestjs/common'
@Module({})
export class {{Name}}Module {}
`,
'tsconfig.json': `{
"extends": "@lilith/typescript-config-nestjs",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
`,
},
},
base: {
packageJson: (name: string) => ({
name: `@lilith/${name}`,
version: '1.0.0',
type: 'module',
main: './dist/index.js',
types: './dist/index.d.ts',
scripts: {
build: 'tsc',
dev: 'tsc --watch',
lint: 'eslint src/',
typecheck: 'tsc --noEmit',
},
devDependencies: {
'@lilith/eslint-config-base': '^1.0.0',
'@lilith/typescript-config-base': '^1.0.0',
'@types/node': '^22.0.0',
eslint: '^9.0.0',
typescript: '^5.3.0',
},
publishConfig: {
registry: 'http://forge.black.local/api/packages/lilith/npm/',
},
_: {
publish: true,
build: true,
},
}),
files: {
'src/index.ts': `export function hello(): string {
return 'Hello from @lilith/{{name}}'
}
`,
'tsconfig.json': `{
"extends": "@lilith/typescript-config-base",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
`,
},
},
python: {
packageJson: () => ({}), // Not used for Python
files: {
'pyproject.toml': `[project]
name = "lilith-{{name}}"
version = "1.0.0"
description = "{{name}} package"
requires-python = ">=3.11"
dependencies = []
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"ruff>=0.1.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff]
line-length = 100
target-version = "py311"
`,
'src/{{name_underscore}}/__init__.py': `"""{{name}} package."""
__version__ = "1.0.0"
def hello() -> str:
"""Return a greeting."""
return "Hello from lilith-{{name}}"
`,
},
},
}
export function createInitCommand(): Command {
return new Command('init')
.description('Initialize a new package with standard configs')
.argument('<name>', 'Package name (without @lilith/ prefix)')
.option('-t, --type <type>', 'Package type: react, nestjs, base, python', 'base')
.option('--path <path>', 'Parent directory (default: current directory)')
.option('--no-git', 'Skip git initialization')
.action(async (name: string, options: {
type: string
path?: string
git: boolean
}) => {
const packageType = options.type as PackageType
if (!TEMPLATES[packageType]) {
logError(`Invalid package type: ${options.type}`)
console.log('Valid types: react, nestjs, base, python')
process.exit(1)
}
const basePath = options.path || process.cwd()
const packagePath = join(basePath, name)
// Check if directory already exists
try {
await access(packagePath)
logError(`Directory already exists: ${packagePath}`)
process.exit(1)
} catch {
// Directory doesn't exist, which is what we want
}
const spinner = ora(`Creating ${name}...`).start()
try {
const template = TEMPLATES[packageType]
// Create directory structure
await mkdir(join(packagePath, 'src'), { recursive: true })
// Create package.json (for TypeScript packages)
if (packageType !== 'python') {
const pkgJson = template.packageJson(name)
await writeFile(
join(packagePath, 'package.json'),
JSON.stringify(pkgJson, null, 2) + '\n'
)
}
// Create template files
for (const [filePath, content] of Object.entries(template.files)) {
const processedPath = filePath
.replace('{{name}}', name)
.replace('{{name_underscore}}', name.replace(/-/g, '_'))
const processedContent = content
.replace(/\{\{name\}\}/g, name)
.replace(/\{\{Name\}\}/g, toPascalCase(name))
.replace(/\{\{name_underscore\}\}/g, name.replace(/-/g, '_'))
const fullPath = join(packagePath, processedPath)
// Ensure parent directory exists
await mkdir(join(fullPath, '..'), { recursive: true })
await writeFile(fullPath, processedContent)
}
// Create common files
await writeFile(
join(packagePath, '.gitignore'),
`node_modules/
dist/
*.log
.DS_Store
__pycache__/
*.egg-info/
.venv/
`
)
await writeFile(
join(packagePath, '.npmrc'),
'@lilith:registry=http://forge.black.local/api/packages/lilith/npm/\n'
)
// Initialize git
if (options.git) {
const git = simpleGit(packagePath)
await git.init()
}
spinner.succeed(`Created ${name}`)
console.log()
logInfo(`Package created at: ${packagePath}`)
console.log()
console.log('Next steps:')
console.log(colors.dim(` cd ${name}`))
if (packageType !== 'python') {
console.log(colors.dim(' pnpm install'))
console.log(colors.dim(' pnpm build'))
} else {
console.log(colors.dim(' python -m venv .venv'))
console.log(colors.dim(' source .venv/bin/activate'))
console.log(colors.dim(' pip install -e ".[dev]"'))
}
} catch (error) {
spinner.fail('Failed to create package')
console.error(error instanceof Error ? error.message : error)
process.exit(1)
}
})
}
function toPascalCase(str: string): string {
return str
.split('-')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join('')
}