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.
- package/.editorconfig +18 -0
- package/.env.example +11 -0
- package/.eslintrc.json +30 -0
- package/.github/workflows/ci.yml +36 -0
- package/.prettierignore +11 -0
- package/.prettierrc.json +10 -0
- package/CONTRIBUTING.md +272 -0
- package/Dockerfile +36 -0
- package/LICENSE +22 -0
- package/README.md +202 -0
- package/dist/banner.d.ts +2 -0
- package/dist/banner.d.ts.map +1 -0
- package/dist/banner.js +16 -0
- package/dist/banner.js.map +1 -0
- package/dist/ci.d.ts +3 -0
- package/dist/ci.d.ts.map +1 -0
- package/dist/ci.js +83 -0
- package/dist/ci.js.map +1 -0
- package/dist/dependencies.d.ts +3 -0
- package/dist/dependencies.d.ts.map +1 -0
- package/dist/dependencies.js +102 -0
- package/dist/dependencies.js.map +1 -0
- package/dist/detect.d.ts +3 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +125 -0
- package/dist/detect.js.map +1 -0
- package/dist/docker.d.ts +3 -0
- package/dist/docker.d.ts.map +1 -0
- package/dist/docker.js +100 -0
- package/dist/docker.js.map +1 -0
- package/dist/generate.d.ts +3 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +71 -0
- package/dist/generate.js.map +1 -0
- package/dist/husky.d.ts +3 -0
- package/dist/husky.d.ts.map +1 -0
- package/dist/husky.js +92 -0
- package/dist/husky.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +128 -0
- package/dist/index.js.map +1 -0
- package/dist/templates.d.ts +11 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +209 -0
- package/dist/templates.js.map +1 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docker-compose.yml +15 -0
- package/package.json +60 -0
- package/src/banner.ts +15 -0
- package/src/ci.ts +94 -0
- package/src/dependencies.ts +120 -0
- package/src/detect.ts +132 -0
- package/src/docker.ts +107 -0
- package/src/generate.ts +81 -0
- package/src/husky.ts +107 -0
- package/src/index.ts +145 -0
- package/src/templates.ts +244 -0
- package/src/types.ts +18 -0
- 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
|
+
|
package/src/generate.ts
ADDED
|
@@ -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
|
+
|