stackinit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.editorconfig +18 -0
  2. package/.env.example +11 -0
  3. package/.eslintrc.json +30 -0
  4. package/.github/workflows/ci.yml +36 -0
  5. package/.prettierignore +11 -0
  6. package/.prettierrc.json +10 -0
  7. package/CONTRIBUTING.md +272 -0
  8. package/Dockerfile +36 -0
  9. package/LICENSE +22 -0
  10. package/README.md +202 -0
  11. package/dist/banner.d.ts +2 -0
  12. package/dist/banner.d.ts.map +1 -0
  13. package/dist/banner.js +16 -0
  14. package/dist/banner.js.map +1 -0
  15. package/dist/ci.d.ts +3 -0
  16. package/dist/ci.d.ts.map +1 -0
  17. package/dist/ci.js +83 -0
  18. package/dist/ci.js.map +1 -0
  19. package/dist/dependencies.d.ts +3 -0
  20. package/dist/dependencies.d.ts.map +1 -0
  21. package/dist/dependencies.js +102 -0
  22. package/dist/dependencies.js.map +1 -0
  23. package/dist/detect.d.ts +3 -0
  24. package/dist/detect.d.ts.map +1 -0
  25. package/dist/detect.js +125 -0
  26. package/dist/detect.js.map +1 -0
  27. package/dist/docker.d.ts +3 -0
  28. package/dist/docker.d.ts.map +1 -0
  29. package/dist/docker.js +100 -0
  30. package/dist/docker.js.map +1 -0
  31. package/dist/generate.d.ts +3 -0
  32. package/dist/generate.d.ts.map +1 -0
  33. package/dist/generate.js +71 -0
  34. package/dist/generate.js.map +1 -0
  35. package/dist/husky.d.ts +3 -0
  36. package/dist/husky.d.ts.map +1 -0
  37. package/dist/husky.js +92 -0
  38. package/dist/husky.js.map +1 -0
  39. package/dist/index.d.ts +3 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +128 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/templates.d.ts +11 -0
  44. package/dist/templates.d.ts.map +1 -0
  45. package/dist/templates.js +209 -0
  46. package/dist/templates.js.map +1 -0
  47. package/dist/types.d.ts +16 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +2 -0
  50. package/dist/types.js.map +1 -0
  51. package/docker-compose.yml +15 -0
  52. package/package.json +60 -0
  53. package/src/banner.ts +15 -0
  54. package/src/ci.ts +94 -0
  55. package/src/dependencies.ts +120 -0
  56. package/src/detect.ts +132 -0
  57. package/src/docker.ts +107 -0
  58. package/src/generate.ts +81 -0
  59. package/src/husky.ts +107 -0
  60. package/src/index.ts +145 -0
  61. package/src/templates.ts +244 -0
  62. package/src/types.ts +18 -0
  63. package/tsconfig.json +26 -0
@@ -0,0 +1,120 @@
1
+ import { readFile, writeFile } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { exec } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import type { ProjectInfo, Options } from './types.js';
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ export async function addDependencies(projectInfo: ProjectInfo, options: Options): Promise<void> {
10
+ const packageJsonPath = join(projectInfo.rootPath, 'package.json');
11
+
12
+ try {
13
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
14
+
15
+ // Initialize devDependencies if not present
16
+ if (!packageJson.devDependencies) {
17
+ packageJson.devDependencies = {};
18
+ }
19
+
20
+ // Core dependencies (always needed)
21
+ const coreDeps: Record<string, string> = {
22
+ eslint: '^8.57.0',
23
+ prettier: '^3.2.0',
24
+ husky: '^9.0.0',
25
+ 'lint-staged': '^15.2.0',
26
+ };
27
+
28
+ // TypeScript dependencies
29
+ if (projectInfo.hasTypeScript) {
30
+ coreDeps['@typescript-eslint/parser'] = '^6.19.0';
31
+ coreDeps['@typescript-eslint/eslint-plugin'] = '^6.19.0';
32
+ }
33
+
34
+ // React dependencies
35
+ const isReact = projectInfo.type === 'react' ||
36
+ projectInfo.type === 'nextjs' ||
37
+ projectInfo.type === 'vite';
38
+
39
+ if (isReact) {
40
+ coreDeps['eslint-plugin-react'] = '^7.33.0';
41
+ coreDeps['eslint-plugin-react-hooks'] = '^4.6.0';
42
+ }
43
+
44
+ // Next.js specific
45
+ if (projectInfo.type === 'nextjs') {
46
+ coreDeps['eslint-config-next'] = 'latest';
47
+ }
48
+
49
+ // Commitlint (strict mode)
50
+ if (options.strict) {
51
+ coreDeps['@commitlint/cli'] = '^18.6.0';
52
+ coreDeps['@commitlint/config-conventional'] = '^18.6.0';
53
+ }
54
+
55
+ // Add dependencies only if they don't already exist
56
+ let hasNewDeps = false;
57
+ for (const [dep, version] of Object.entries(coreDeps)) {
58
+ if (!packageJson.devDependencies[dep] && !packageJson.dependencies?.[dep]) {
59
+ packageJson.devDependencies[dep] = version;
60
+ hasNewDeps = true;
61
+ }
62
+ }
63
+
64
+ if (hasNewDeps) {
65
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
66
+ console.log(' Updated package.json with dependencies');
67
+
68
+ // Install dependencies
69
+ await installDependencies(projectInfo);
70
+ } else {
71
+ console.log(' All required dependencies already present');
72
+ }
73
+ } catch (error) {
74
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
75
+ console.warn(' Warning: package.json not found, skipping dependency installation');
76
+ } else {
77
+ throw error;
78
+ }
79
+ }
80
+ }
81
+
82
+ async function installDependencies(projectInfo: ProjectInfo): Promise<void> {
83
+ const { packageManager } = projectInfo;
84
+ const rootPath = projectInfo.rootPath;
85
+
86
+ console.log(`\n Installing dependencies with ${packageManager}...`);
87
+
88
+ try {
89
+ let installCommand: string;
90
+
91
+ switch (packageManager) {
92
+ case 'pnpm':
93
+ installCommand = 'pnpm install';
94
+ break;
95
+ case 'yarn':
96
+ installCommand = 'yarn install';
97
+ break;
98
+ case 'npm':
99
+ default:
100
+ installCommand = 'npm install';
101
+ break;
102
+ }
103
+
104
+ await execAsync(installCommand, {
105
+ cwd: rootPath,
106
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer
107
+ });
108
+
109
+ console.log(' Dependencies installed successfully');
110
+ } catch {
111
+ console.error(' Failed to install dependencies automatically');
112
+ console.error(' Please run manually:',
113
+ packageManager === 'pnpm' ? 'pnpm install' :
114
+ packageManager === 'yarn' ? 'yarn install' :
115
+ 'npm install'
116
+ );
117
+ // Don't throw - let the user install manually if needed
118
+ }
119
+ }
120
+
package/src/detect.ts ADDED
@@ -0,0 +1,132 @@
1
+ import { readFile, access } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import type { ProjectInfo, ProjectType, PackageManager } from './types.js';
4
+
5
+ export async function detectProject(): Promise<ProjectInfo> {
6
+ const rootPath = process.cwd();
7
+
8
+ const [type, packageManager, hasTypeScript, isMonorepo] = await Promise.all([
9
+ detectProjectType(rootPath),
10
+ detectPackageManager(rootPath),
11
+ detectTypeScript(rootPath),
12
+ detectMonorepo(rootPath),
13
+ ]);
14
+
15
+ return {
16
+ type,
17
+ packageManager,
18
+ hasTypeScript,
19
+ isMonorepo,
20
+ rootPath,
21
+ };
22
+ }
23
+
24
+ async function detectProjectType(rootPath: string): Promise<ProjectType> {
25
+ try {
26
+ const packageJsonPath = join(rootPath, 'package.json');
27
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
28
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
29
+
30
+ // Check for Next.js
31
+ if (deps.next || deps['nextjs']) {
32
+ return 'nextjs';
33
+ }
34
+
35
+ // Check for Vite
36
+ if (deps.vite || deps['@vitejs/plugin-react']) {
37
+ return 'vite';
38
+ }
39
+
40
+ // Check for React
41
+ if (deps.react || deps['react-dom']) {
42
+ return 'react';
43
+ }
44
+
45
+ // Default to Node backend
46
+ return 'node';
47
+ } catch {
48
+ return 'unknown';
49
+ }
50
+ }
51
+
52
+ async function detectPackageManager(rootPath: string): Promise<PackageManager> {
53
+ // Check for lock files
54
+ const lockFiles = [
55
+ { file: 'pnpm-lock.yaml', manager: 'pnpm' as PackageManager },
56
+ { file: 'yarn.lock', manager: 'yarn' as PackageManager },
57
+ { file: 'package-lock.json', manager: 'npm' as PackageManager },
58
+ ];
59
+
60
+ for (const { file, manager } of lockFiles) {
61
+ try {
62
+ await access(join(rootPath, file));
63
+ return manager;
64
+ } catch {
65
+ // Continue checking
66
+ }
67
+ }
68
+
69
+ // Check for packageManager field in package.json
70
+ try {
71
+ const packageJsonPath = join(rootPath, 'package.json');
72
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
73
+ if (packageJson.packageManager) {
74
+ if (packageJson.packageManager.startsWith('pnpm')) return 'pnpm';
75
+ if (packageJson.packageManager.startsWith('yarn')) return 'yarn';
76
+ if (packageJson.packageManager.startsWith('npm')) return 'npm';
77
+ }
78
+ } catch {
79
+ // Fall through
80
+ }
81
+
82
+ // Default to npm
83
+ return 'npm';
84
+ }
85
+
86
+ async function detectTypeScript(rootPath: string): Promise<boolean> {
87
+ try {
88
+ const tsConfigPath = join(rootPath, 'tsconfig.json');
89
+ await access(tsConfigPath);
90
+ return true;
91
+ } catch {
92
+ // Check for TypeScript in dependencies
93
+ try {
94
+ const packageJsonPath = join(rootPath, 'package.json');
95
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
96
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
97
+ return 'typescript' in deps;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+ }
103
+
104
+ async function detectMonorepo(rootPath: string): Promise<boolean> {
105
+ // Check for common monorepo indicators
106
+ const indicators = [
107
+ 'pnpm-workspace.yaml',
108
+ 'lerna.json',
109
+ 'nx.json',
110
+ 'turbo.json',
111
+ 'rush.json',
112
+ ];
113
+
114
+ for (const indicator of indicators) {
115
+ try {
116
+ await access(join(rootPath, indicator));
117
+ return true;
118
+ } catch {
119
+ // Continue checking
120
+ }
121
+ }
122
+
123
+ // Check package.json for workspaces
124
+ try {
125
+ const packageJsonPath = join(rootPath, 'package.json');
126
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
127
+ return !!(packageJson.workspaces || packageJson.workspace);
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+
package/src/docker.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { writeFile, access } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import type { ProjectInfo } from './types.js';
4
+
5
+ export async function generateDocker(projectInfo: ProjectInfo): Promise<void> {
6
+ const dockerfile = generateDockerfile(projectInfo);
7
+ const dockerCompose = generateDockerCompose();
8
+
9
+ const dockerfilePath = join(projectInfo.rootPath, 'Dockerfile');
10
+ const dockerComposePath = join(projectInfo.rootPath, 'docker-compose.yml');
11
+
12
+ // Check if files exist
13
+ try {
14
+ await access(dockerfilePath);
15
+ console.log(' Skipping Dockerfile (already exists)');
16
+ } catch {
17
+ await writeFile(dockerfilePath, dockerfile, 'utf-8');
18
+ console.log(' Generated Dockerfile');
19
+ }
20
+
21
+ try {
22
+ await access(dockerComposePath);
23
+ console.log(' Skipping docker-compose.yml (already exists)');
24
+ } catch {
25
+ await writeFile(dockerComposePath, dockerCompose, 'utf-8');
26
+ console.log(' Generated docker-compose.yml');
27
+ }
28
+ }
29
+
30
+ function generateDockerfile(projectInfo: ProjectInfo): string {
31
+ const packageManager = projectInfo.packageManager;
32
+ const installCmd = packageManager === 'pnpm'
33
+ ? 'RUN pnpm install --frozen-lockfile'
34
+ : packageManager === 'yarn'
35
+ ? 'RUN yarn install --frozen-lockfile'
36
+ : 'RUN npm ci';
37
+
38
+ const lockFileName = packageManager === 'pnpm'
39
+ ? 'pnpm-lock.yaml'
40
+ : packageManager === 'yarn'
41
+ ? 'yarn.lock'
42
+ : 'package-lock.json';
43
+
44
+ return `# Build stage
45
+ FROM node:20-alpine AS builder
46
+
47
+ WORKDIR /app
48
+
49
+ # Copy package files
50
+ COPY package.json .
51
+ COPY ${lockFileName} .
52
+
53
+ ${installCmd}
54
+
55
+ # Copy source files
56
+ COPY . .
57
+
58
+ # Build the application (adjust as needed)
59
+ ${projectInfo.hasTypeScript ? 'RUN npm run build' : '# Add build command if needed'}
60
+
61
+ # Production stage
62
+ FROM node:20-alpine AS production
63
+
64
+ WORKDIR /app
65
+
66
+ # Copy package files
67
+ COPY package.json .
68
+ COPY ${lockFileName} .
69
+
70
+ # Install production dependencies only
71
+ ${packageManager === 'pnpm'
72
+ ? 'RUN pnpm install --frozen-lockfile --prod'
73
+ : packageManager === 'yarn'
74
+ ? 'RUN yarn install --frozen-lockfile --production'
75
+ : 'RUN npm ci --only=production'}
76
+
77
+ # Copy built application from builder
78
+ COPY --from=builder /app/dist ./dist
79
+ COPY --from=builder /app/public ./public 2>/dev/null || true
80
+
81
+ # Expose port (adjust as needed)
82
+ EXPOSE 3000
83
+
84
+ # Start the application
85
+ CMD ["node", "dist/index.js"]
86
+ `;
87
+ }
88
+
89
+ function generateDockerCompose(): string {
90
+ return `version: '3.8'
91
+
92
+ services:
93
+ app:
94
+ build:
95
+ context: .
96
+ dockerfile: Dockerfile
97
+ target: production
98
+ ports:
99
+ - "3000:3000"
100
+ environment:
101
+ - NODE_ENV=production
102
+ restart: unless-stopped
103
+ # volumes:
104
+ # - ./data:/app/data
105
+ `;
106
+ }
107
+
@@ -0,0 +1,81 @@
1
+ import { writeFile, readFile, access } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import type { ProjectInfo, Options } from './types.js';
4
+ import { getTemplates } from './templates.js';
5
+
6
+ export async function generateFiles(projectInfo: ProjectInfo, options: Options): Promise<void> {
7
+ const templates = getTemplates(projectInfo, options);
8
+ const filesToGenerate = [
9
+ { name: '.eslintrc.json', content: templates.eslint },
10
+ { name: '.prettierrc.json', content: templates.prettier },
11
+ { name: '.prettierignore', content: templates.prettierIgnore },
12
+ { name: '.gitignore', content: templates.gitignore },
13
+ { name: '.editorconfig', content: templates.editorconfig },
14
+ { name: '.env.example', content: templates.envExample },
15
+ ];
16
+
17
+ for (const file of filesToGenerate) {
18
+ const filePath = join(projectInfo.rootPath, file.name);
19
+
20
+ // Skip if file exists (don't overwrite)
21
+ try {
22
+ await access(filePath);
23
+ console.log(` Skipping ${file.name} (already exists)`);
24
+ continue;
25
+ } catch {
26
+ // File doesn't exist, proceed
27
+ }
28
+
29
+ await writeFile(filePath, file.content, 'utf-8');
30
+ console.log(` Generated ${file.name}`);
31
+ }
32
+
33
+ // Update package.json scripts
34
+ await updatePackageJson(projectInfo);
35
+ }
36
+
37
+ async function updatePackageJson(projectInfo: ProjectInfo): Promise<void> {
38
+ const packageJsonPath = join(projectInfo.rootPath, 'package.json');
39
+
40
+ try {
41
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
42
+
43
+ // Initialize scripts if not present
44
+ if (!packageJson.scripts) {
45
+ packageJson.scripts = {};
46
+ }
47
+
48
+ // Add scripts only if they don't exist
49
+ const scriptsToAdd: Record<string, string> = {
50
+ lint: projectInfo.hasTypeScript
51
+ ? 'eslint . --ext .js,.jsx,.ts,.tsx'
52
+ : 'eslint . --ext .js,.jsx',
53
+ 'lint:fix': projectInfo.hasTypeScript
54
+ ? 'eslint . --ext .js,.jsx,.ts,.tsx --fix'
55
+ : 'eslint . --ext .js,.jsx --fix',
56
+ format: 'prettier --write "**/*.{js,jsx,ts,tsx,json,md}"',
57
+ 'format:check': 'prettier --check "**/*.{js,jsx,ts,tsx,json,md}"',
58
+ };
59
+
60
+ if (projectInfo.hasTypeScript) {
61
+ scriptsToAdd['type-check'] = 'tsc --noEmit';
62
+ }
63
+
64
+ // Only add scripts that don't already exist
65
+ for (const [key, value] of Object.entries(scriptsToAdd)) {
66
+ if (!packageJson.scripts[key]) {
67
+ packageJson.scripts[key] = value;
68
+ }
69
+ }
70
+
71
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
72
+ console.log(' Updated package.json scripts');
73
+ } catch (error) {
74
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
75
+ console.warn(' Warning: package.json not found, skipping script updates');
76
+ } else {
77
+ throw error;
78
+ }
79
+ }
80
+ }
81
+
package/src/husky.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { writeFile, mkdir, access, readFile } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import type { ProjectInfo, Options } from './types.js';
4
+
5
+ export async function setupHusky(projectInfo: ProjectInfo, options: Options): Promise<void> {
6
+ const huskyDir = join(projectInfo.rootPath, '.husky');
7
+
8
+ // Check if .git exists
9
+ try {
10
+ await access(join(projectInfo.rootPath, '.git'));
11
+ } catch {
12
+ console.warn(' Warning: .git directory not found, skipping Husky setup');
13
+ return;
14
+ }
15
+
16
+ // Create .husky directory
17
+ try {
18
+ await mkdir(huskyDir, { recursive: true });
19
+ } catch {
20
+ // Directory might already exist
21
+ }
22
+
23
+ // Create pre-commit hook
24
+ const preCommitHook = `#!/usr/bin/env sh
25
+ . "$(dirname -- "$0")/_/husky.sh"
26
+
27
+ ${projectInfo.packageManager === 'pnpm' ? 'pnpm' : 'npx'} lint-staged
28
+ `;
29
+
30
+ const preCommitPath = join(huskyDir, 'pre-commit');
31
+ await writeFile(preCommitPath, preCommitHook, 'utf-8');
32
+
33
+ // Make it executable (Unix-like systems)
34
+ if (process.platform !== 'win32') {
35
+ const { chmod } = await import('fs/promises');
36
+ await chmod(preCommitPath, 0o755);
37
+ }
38
+
39
+ // Create commit-msg hook if commitlint is enabled
40
+ if (options.strict) {
41
+ const commitMsgHook = `#!/usr/bin/env sh
42
+ . "$(dirname -- "$0")/_/husky.sh"
43
+
44
+ ${projectInfo.packageManager === 'pnpm' ? 'pnpm' : 'npx'} commitlint --edit "$1"
45
+ `;
46
+
47
+ const commitMsgPath = join(huskyDir, 'commit-msg');
48
+ await writeFile(commitMsgPath, commitMsgHook, 'utf-8');
49
+
50
+ if (process.platform !== 'win32') {
51
+ const { chmod } = await import('fs/promises');
52
+ await chmod(commitMsgPath, 0o755);
53
+ }
54
+ }
55
+
56
+ // Update package.json with husky and lint-staged config
57
+ await updatePackageJsonForHusky(projectInfo, options);
58
+ }
59
+
60
+ async function updatePackageJsonForHusky(projectInfo: ProjectInfo, options: Options): Promise<void> {
61
+ const packageJsonPath = join(projectInfo.rootPath, 'package.json');
62
+
63
+ try {
64
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
65
+
66
+ // Add prepare script for husky
67
+ if (!packageJson.scripts) {
68
+ packageJson.scripts = {};
69
+ }
70
+
71
+ if (!packageJson.scripts.prepare) {
72
+ packageJson.scripts.prepare = 'husky install';
73
+ }
74
+
75
+ // Add lint-staged configuration
76
+ if (!packageJson['lint-staged']) {
77
+ const lintStagedConfig: Record<string, string | string[]> = {
78
+ '*.{js,jsx,ts,tsx}': [
79
+ 'eslint --fix',
80
+ 'prettier --write',
81
+ ],
82
+ '*.{json,md,yml,yaml}': [
83
+ 'prettier --write',
84
+ ],
85
+ };
86
+
87
+ packageJson['lint-staged'] = lintStagedConfig;
88
+ }
89
+
90
+ // Add commitlint config if strict mode
91
+ if (options.strict && !packageJson.commitlint) {
92
+ packageJson.commitlint = {
93
+ extends: ['@commitlint/config-conventional'],
94
+ };
95
+ }
96
+
97
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
98
+ console.log(' Configured Husky git hooks');
99
+ } catch (error) {
100
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
101
+ console.warn(' Warning: package.json not found, skipping Husky configuration');
102
+ } else {
103
+ throw error;
104
+ }
105
+ }
106
+ }
107
+
package/src/index.ts ADDED
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { detectProject } from './detect.js';
5
+ import { generateFiles } from './generate.js';
6
+ import { setupHusky } from './husky.js';
7
+ import { generateCI } from './ci.js';
8
+ import { generateDocker } from './docker.js';
9
+ import { addDependencies } from './dependencies.js';
10
+ import { printBanner } from './banner.js';
11
+ import { exec } from 'child_process';
12
+ import { promisify } from 'util';
13
+ import type { Options, ProjectInfo } from './types.js';
14
+
15
+ const execAsync = promisify(exec);
16
+
17
+ const program = new Command();
18
+
19
+ program
20
+ .name('stackinit')
21
+ .description('Initialize a consistent development environment for Node-based projects')
22
+ .version('0.1.0')
23
+ .option('--strict', 'Enable stricter lint rules and CI failure on warnings')
24
+ .option('--docker', 'Generate Dockerfile and docker-compose.yml')
25
+ .option('--ci-only', 'Generate only GitHub Actions')
26
+ .option('--dry-run', 'Show what files would be created without writing')
27
+ .action(async (options: Options) => {
28
+ try {
29
+ if (!options.dryRun && !options.ciOnly) {
30
+ printBanner();
31
+ }
32
+
33
+ const projectInfo = await detectProject();
34
+
35
+ if (options.dryRun) {
36
+ console.log('\nDRY RUN MODE - No files will be written\n');
37
+ console.log('Detected project:');
38
+ console.log(` Type: ${projectInfo.type}`);
39
+ console.log(` Package Manager: ${projectInfo.packageManager}`);
40
+ console.log(` TypeScript: ${projectInfo.hasTypeScript ? 'Yes' : 'No'}`);
41
+ console.log(` Monorepo: ${projectInfo.isMonorepo ? 'Yes' : 'No'}\n`);
42
+ console.log('Files that would be generated:');
43
+ console.log(' - .eslintrc.json');
44
+ console.log(' - .prettierrc.json');
45
+ console.log(' - .prettierignore');
46
+ console.log(' - .gitignore');
47
+ console.log(' - .editorconfig');
48
+ console.log(' - .env.example');
49
+ console.log(' - .husky/pre-commit');
50
+ if (options.strict) {
51
+ console.log(' - .husky/commit-msg');
52
+ }
53
+ console.log(' - .github/workflows/ci.yml');
54
+ if (options.docker) {
55
+ console.log(' - Dockerfile');
56
+ console.log(' - docker-compose.yml');
57
+ }
58
+ console.log('\nPackage.json scripts that would be added:');
59
+ console.log(' - lint');
60
+ console.log(' - lint:fix');
61
+ console.log(' - format');
62
+ console.log(' - format:check');
63
+ if (projectInfo.hasTypeScript) {
64
+ console.log(' - type-check');
65
+ }
66
+ console.log(' - prepare (for Husky)');
67
+ console.log('\nDependencies that would be installed:');
68
+ console.log(' - eslint, prettier, husky, lint-staged');
69
+ if (projectInfo.hasTypeScript) {
70
+ console.log(' - @typescript-eslint/parser, @typescript-eslint/eslint-plugin');
71
+ }
72
+ if (projectInfo.type === 'react' || projectInfo.type === 'nextjs' || projectInfo.type === 'vite') {
73
+ console.log(' - eslint-plugin-react, eslint-plugin-react-hooks');
74
+ }
75
+ if (projectInfo.type === 'nextjs') {
76
+ console.log(' - eslint-config-next');
77
+ }
78
+ if (options.strict) {
79
+ console.log(' - @commitlint/cli, @commitlint/config-conventional');
80
+ }
81
+ return;
82
+ }
83
+
84
+ if (options.ciOnly) {
85
+ await generateCI(projectInfo, options);
86
+ console.log('\n✓ GitHub Actions CI generated successfully');
87
+ return;
88
+ }
89
+
90
+ console.log('\nInitializing development environment...\n');
91
+
92
+ // Generate core config files
93
+ await generateFiles(projectInfo, options);
94
+
95
+ // Add and install dependencies
96
+ await addDependencies(projectInfo, options);
97
+
98
+ // Setup Husky and lint-staged
99
+ await setupHusky(projectInfo, options);
100
+
101
+ // Initialize Husky (if git repo exists)
102
+ await initializeHusky(projectInfo);
103
+
104
+ // Generate CI
105
+ await generateCI(projectInfo, options);
106
+
107
+ // Generate Docker files if requested
108
+ if (options.docker) {
109
+ await generateDocker(projectInfo);
110
+ }
111
+
112
+ console.log('\n✓ Development environment initialized successfully\n');
113
+ console.log('Next steps:');
114
+ console.log(' 1. Review the generated configuration files');
115
+ console.log(' 2. Start developing!\n');
116
+ } catch (error) {
117
+ console.error('\nERROR:', error instanceof Error ? error.message : String(error));
118
+ process.exit(1);
119
+ }
120
+ });
121
+
122
+ async function initializeHusky(projectInfo: ProjectInfo): Promise<void> {
123
+ try {
124
+ const { access } = await import('fs/promises');
125
+ await access(projectInfo.rootPath + '/.git');
126
+
127
+ // Husky install
128
+ const huskyCommand = projectInfo.packageManager === 'pnpm'
129
+ ? 'pnpm exec husky install'
130
+ : 'npx husky install';
131
+
132
+ try {
133
+ await execAsync(huskyCommand, { cwd: projectInfo.rootPath });
134
+ console.log('✓ Husky initialized');
135
+ } catch {
136
+ // Husky install might fail if not in git repo or already installed
137
+ // This is okay, user can run manually if needed
138
+ }
139
+ } catch {
140
+ // Not a git repo, skip husky init
141
+ }
142
+ }
143
+
144
+ program.parse();
145
+