devstarter-tool 0.2.2 → 0.3.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/README.md +69 -25
- package/dist/cli.js +8 -0
- package/dist/commands/add/collector.js +47 -0
- package/dist/commands/add/registry.js +24 -0
- package/dist/commands/add/resolvers.js +8 -0
- package/dist/commands/add.js +65 -0
- package/dist/features/ci.js +158 -0
- package/dist/features/docker.js +97 -0
- package/dist/features/eslint.js +74 -0
- package/dist/features/prettier.js +55 -0
- package/dist/features/tailwind.js +72 -0
- package/dist/features/vitest.js +31 -0
- package/dist/generators/createMonorepo.js +0 -1
- package/dist/prompts/addPrompts.js +20 -0
- package/dist/types/feature.js +1 -0
- package/dist/utils/detectProjectContext.js +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,12 +4,12 @@ CLI to generate projects with best practices and predefined configurations.
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Project
|
|
7
|
+
- **Project scaffolding**: Create frontend, backend, or monorepo projects in seconds
|
|
8
|
+
- **`add` command**: Add features like ESLint, Vitest, Prettier, CI, Docker, and Tailwind CSS to existing projects
|
|
9
|
+
- **Interactive prompts**: Guided project creation or flag-based configuration
|
|
8
10
|
- **Automatic dependency installation**: No need to run `npm install` manually
|
|
9
|
-
- **Vitest integration**: Optional testing setup with one flag
|
|
10
11
|
- **Package manager detection**: Automatically uses npm, pnpm, or yarn
|
|
11
12
|
- **Git initialization**: Optional repository setup
|
|
12
|
-
- **Interactive prompts**: Guided project creation
|
|
13
13
|
- **Dry-run mode**: Preview changes before creating files
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
@@ -36,18 +36,27 @@ devstarter init my-app -y
|
|
|
36
36
|
# Frontend project with Vitest
|
|
37
37
|
devstarter init my-app --type frontend --vitest
|
|
38
38
|
|
|
39
|
+
# Add features to an existing project
|
|
40
|
+
devstarter add prettier
|
|
41
|
+
devstarter add eslint
|
|
42
|
+
devstarter add tailwind
|
|
43
|
+
devstarter add docker
|
|
44
|
+
devstarter add ci
|
|
45
|
+
|
|
39
46
|
# Preview without creating files
|
|
40
47
|
devstarter init my-app --dry-run
|
|
41
48
|
```
|
|
42
49
|
|
|
43
|
-
##
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
### `devstarter init`
|
|
53
|
+
|
|
54
|
+
Scaffolds a new project from a template.
|
|
44
55
|
|
|
45
56
|
```bash
|
|
46
57
|
devstarter init [project-name] [options]
|
|
47
58
|
```
|
|
48
59
|
|
|
49
|
-
### Options
|
|
50
|
-
|
|
51
60
|
| Option | Description |
|
|
52
61
|
|--------|-------------|
|
|
53
62
|
| `-y, --yes` | Use default values without prompting |
|
|
@@ -55,9 +64,10 @@ devstarter init [project-name] [options]
|
|
|
55
64
|
| `--template <name>` | Template to use (e.g., `basic`, `react`) |
|
|
56
65
|
| `--vitest` | Add Vitest for testing |
|
|
57
66
|
| `--no-git` | Skip Git repository initialization |
|
|
67
|
+
| `--no-vitest` | Skip Vitest testing framework setup |
|
|
58
68
|
| `--dry-run` | Preview changes without creating files |
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
#### Examples
|
|
61
71
|
|
|
62
72
|
```bash
|
|
63
73
|
# Full interactive mode
|
|
@@ -79,6 +89,55 @@ devstarter init my-app --type frontend -y
|
|
|
79
89
|
devstarter init my-app --no-git
|
|
80
90
|
```
|
|
81
91
|
|
|
92
|
+
### `devstarter add`
|
|
93
|
+
|
|
94
|
+
Adds features to an existing project. Automatically detects features already configured and skips them.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
devstarter add [feature] [options]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
| Option | Description |
|
|
101
|
+
|--------|-------------|
|
|
102
|
+
| `--list` | List all available features |
|
|
103
|
+
| `-y, --yes` | Add all available features without prompting |
|
|
104
|
+
| `--dry-run` | Show what would be added without making changes |
|
|
105
|
+
|
|
106
|
+
#### Available Features
|
|
107
|
+
|
|
108
|
+
| Feature | Description | What it adds |
|
|
109
|
+
|---------|-------------|--------------|
|
|
110
|
+
| `eslint` | Linter for JavaScript and TypeScript | `eslint.config.js`, lint script, ESLint + typescript-eslint deps |
|
|
111
|
+
| `vitest` | Unit testing framework | `vitest.config.ts`, test scripts, Vitest dep |
|
|
112
|
+
| `prettier` | Code formatter | `.prettierrc`, format script, Prettier dep |
|
|
113
|
+
| `ci` | CI/CD pipeline | GitHub Actions workflow (`.github/workflows/ci.yml`) or GitLab CI (`.gitlab-ci.yml`) with lint, test, and build steps |
|
|
114
|
+
| `docker` | Docker containerization | `Dockerfile` (multi-stage build), `docker-compose.yml`, `.dockerignore`. Auto-detects frontend (nginx) vs backend (node) |
|
|
115
|
+
| `tailwind` | Tailwind CSS v4 | `tailwindcss` + `@tailwindcss/vite` deps, Vite plugin config, `@import "tailwindcss"` in CSS |
|
|
116
|
+
|
|
117
|
+
#### Examples
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Add a specific feature
|
|
121
|
+
devstarter add prettier
|
|
122
|
+
devstarter add eslint
|
|
123
|
+
devstarter add vitest
|
|
124
|
+
devstarter add tailwind
|
|
125
|
+
devstarter add docker
|
|
126
|
+
devstarter add ci
|
|
127
|
+
|
|
128
|
+
# Interactive mode - choose from available features
|
|
129
|
+
devstarter add
|
|
130
|
+
|
|
131
|
+
# Add all available features at once
|
|
132
|
+
devstarter add -y
|
|
133
|
+
|
|
134
|
+
# List available features
|
|
135
|
+
devstarter add --list
|
|
136
|
+
|
|
137
|
+
# Preview changes
|
|
138
|
+
devstarter add prettier --dry-run
|
|
139
|
+
```
|
|
140
|
+
|
|
82
141
|
## Project Structures
|
|
83
142
|
|
|
84
143
|
### Basic (single project)
|
|
@@ -130,23 +189,6 @@ my-app/
|
|
|
130
189
|
|----------|-------------|
|
|
131
190
|
| `basic` | Express + TypeScript |
|
|
132
191
|
|
|
133
|
-
## Vitest Integration
|
|
134
|
-
|
|
135
|
-
When using `--vitest`, the CLI adds:
|
|
136
|
-
|
|
137
|
-
- `vitest` as a dev dependency
|
|
138
|
-
- `vitest.config.ts` with basic configuration
|
|
139
|
-
- `test` and `test:run` scripts in package.json
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
# Create project with Vitest
|
|
143
|
-
devstarter init my-app --vitest
|
|
144
|
-
|
|
145
|
-
# Then run tests
|
|
146
|
-
cd my-app
|
|
147
|
-
npm test
|
|
148
|
-
```
|
|
149
|
-
|
|
150
192
|
## Requirements
|
|
151
193
|
|
|
152
194
|
- Node.js 18+
|
|
@@ -175,7 +217,9 @@ node dist/cli.js init test-app --dry-run
|
|
|
175
217
|
|--------|-------------|
|
|
176
218
|
| `npm run build` | Compile TypeScript and copy templates |
|
|
177
219
|
| `npm run dev` | Watch mode for development |
|
|
178
|
-
| `npm run test` | Run tests |
|
|
220
|
+
| `npm run test` | Run tests in watch mode |
|
|
221
|
+
| `npm run test:run` | Single test run |
|
|
222
|
+
| `npm run test:coverage` | Coverage with HTML + text reports |
|
|
179
223
|
| `npm run lint` | Run ESLint |
|
|
180
224
|
| `npm run format` | Format code with Prettier |
|
|
181
225
|
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { initCommand } from './commands/init.js';
|
|
4
|
+
import { addCommand } from './commands/add.js';
|
|
4
5
|
const program = new Command();
|
|
5
6
|
program
|
|
6
7
|
.name('devstarter')
|
|
@@ -16,4 +17,11 @@ program
|
|
|
16
17
|
.option('--no-git', 'Skip git repository initialization')
|
|
17
18
|
.option('--vitest', 'Add Vitest for testing')
|
|
18
19
|
.action(initCommand);
|
|
20
|
+
program
|
|
21
|
+
.command('add [feature]')
|
|
22
|
+
.description('Add a feature to an existing project')
|
|
23
|
+
.option('--dry-run', 'Show what would be added without making changes')
|
|
24
|
+
.option('--list', 'List available features')
|
|
25
|
+
.option('-y, --yes', 'Add all available features without prompting')
|
|
26
|
+
.action(addCommand);
|
|
19
27
|
program.parse(process.argv);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { detectProjectContext } from '../../utils/detectProjectContext.js';
|
|
2
|
+
import { PromptCancelledError } from '../../prompts/initPrompts.js';
|
|
3
|
+
import { getAllFeatures } from './registry.js';
|
|
4
|
+
import { resolveFeatureArg } from './resolvers.js';
|
|
5
|
+
import { askFeatures } from '../../prompts/addPrompts.js';
|
|
6
|
+
export async function collectAddContext(featureArg, options) {
|
|
7
|
+
const projectContext = await detectProjectContext();
|
|
8
|
+
if (featureArg) {
|
|
9
|
+
const featureId = resolveFeatureArg(featureArg);
|
|
10
|
+
return {
|
|
11
|
+
projectRoot: projectContext.root,
|
|
12
|
+
features: [featureId],
|
|
13
|
+
isDryRun: Boolean(options.dryRun),
|
|
14
|
+
packageManager: projectContext.packageManager,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// Filter out already-detected features
|
|
18
|
+
const allFeatures = getAllFeatures();
|
|
19
|
+
const available = [];
|
|
20
|
+
for (const feature of allFeatures) {
|
|
21
|
+
const detected = await feature.detect(projectContext);
|
|
22
|
+
if (!detected) {
|
|
23
|
+
available.push(feature);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (available.length === 0) {
|
|
27
|
+
throw new Error('All available features are already configured.');
|
|
28
|
+
}
|
|
29
|
+
if (options.yes) {
|
|
30
|
+
return {
|
|
31
|
+
projectRoot: projectContext.root,
|
|
32
|
+
features: available.map((f) => f.id),
|
|
33
|
+
isDryRun: Boolean(options.dryRun),
|
|
34
|
+
packageManager: projectContext.packageManager,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const answer = await askFeatures(available);
|
|
38
|
+
if (answer.features.length === 0) {
|
|
39
|
+
throw new PromptCancelledError();
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
projectRoot: projectContext.root,
|
|
43
|
+
features: answer.features,
|
|
44
|
+
isDryRun: Boolean(options.dryRun),
|
|
45
|
+
packageManager: projectContext.packageManager,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { eslintFeature } from '../../features/eslint.js';
|
|
2
|
+
import { vitestFeature } from '../../features/vitest.js';
|
|
3
|
+
import { prettierFeature } from '../../features/prettier.js';
|
|
4
|
+
import { ciFeature } from '../../features/ci.js';
|
|
5
|
+
import { dockerFeature } from '../../features/docker.js';
|
|
6
|
+
import { tailwindFeature } from '../../features/tailwind.js';
|
|
7
|
+
const features = [
|
|
8
|
+
eslintFeature,
|
|
9
|
+
vitestFeature,
|
|
10
|
+
prettierFeature,
|
|
11
|
+
ciFeature,
|
|
12
|
+
dockerFeature,
|
|
13
|
+
tailwindFeature,
|
|
14
|
+
];
|
|
15
|
+
const featureMap = new Map(features.map((f) => [f.id, f]));
|
|
16
|
+
export function getAvailableFeatureIds() {
|
|
17
|
+
return features.map((f) => f.id);
|
|
18
|
+
}
|
|
19
|
+
export function getFeature(id) {
|
|
20
|
+
return featureMap.get(id);
|
|
21
|
+
}
|
|
22
|
+
export function getAllFeatures() {
|
|
23
|
+
return features;
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getAvailableFeatureIds } from './registry.js';
|
|
2
|
+
export function resolveFeatureArg(arg) {
|
|
3
|
+
const available = getAvailableFeatureIds();
|
|
4
|
+
if (!available.includes(arg)) {
|
|
5
|
+
throw new Error(`Unknown feature "${arg}". Available: ${available.join(', ')}`);
|
|
6
|
+
}
|
|
7
|
+
return arg;
|
|
8
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { PromptCancelledError } from '../prompts/initPrompts.js';
|
|
2
|
+
import { styles } from '../utils/styles.js';
|
|
3
|
+
import { detectProjectContext } from '../utils/detectProjectContext.js';
|
|
4
|
+
import { installDependencies } from '../utils/installDependencies.js';
|
|
5
|
+
import { getAllFeatures, getFeature } from './add/registry.js';
|
|
6
|
+
import { collectAddContext } from './add/collector.js';
|
|
7
|
+
export async function addCommand(featureArg, options) {
|
|
8
|
+
try {
|
|
9
|
+
if (options.list) {
|
|
10
|
+
printFeatureList();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const context = await collectAddContext(featureArg, options);
|
|
14
|
+
if (context.isDryRun) {
|
|
15
|
+
printDryRun(context.features);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const projectContext = await detectProjectContext(context.projectRoot);
|
|
19
|
+
for (const featureId of context.features) {
|
|
20
|
+
const feature = getFeature(featureId);
|
|
21
|
+
console.log(`${styles.info('Adding')} ${feature.name}...`);
|
|
22
|
+
await feature.apply(projectContext);
|
|
23
|
+
console.log(`${styles.success('Added')} ${feature.name}`);
|
|
24
|
+
}
|
|
25
|
+
console.log(`\n${styles.info('Installing dependencies...')}`);
|
|
26
|
+
installDependencies(context.projectRoot, context.packageManager);
|
|
27
|
+
printAddSummary(context.features);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
handleError(error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function printFeatureList() {
|
|
34
|
+
const features = getAllFeatures();
|
|
35
|
+
console.log(`\n${styles.title('Available features')}\n`);
|
|
36
|
+
for (const feature of features) {
|
|
37
|
+
console.log(` ${styles.highlight(feature.id)} - ${feature.description}`);
|
|
38
|
+
}
|
|
39
|
+
console.log('');
|
|
40
|
+
}
|
|
41
|
+
function printDryRun(featureIds) {
|
|
42
|
+
console.log(`\n${styles.warning('Dry run – no changes will be made')}\n`);
|
|
43
|
+
console.log(styles.title('Features to add'));
|
|
44
|
+
for (const id of featureIds) {
|
|
45
|
+
const feature = getFeature(id);
|
|
46
|
+
console.log(` ${styles.info('-')} ${feature.name}: ${feature.description}`);
|
|
47
|
+
}
|
|
48
|
+
console.log('');
|
|
49
|
+
}
|
|
50
|
+
function printAddSummary(featureIds) {
|
|
51
|
+
console.log(`\n${styles.success('Done!')}\n`);
|
|
52
|
+
console.log(styles.title('Added features'));
|
|
53
|
+
for (const id of featureIds) {
|
|
54
|
+
const feature = getFeature(id);
|
|
55
|
+
console.log(` ${styles.success('-')} ${feature.name}`);
|
|
56
|
+
}
|
|
57
|
+
console.log('');
|
|
58
|
+
}
|
|
59
|
+
function handleError(error) {
|
|
60
|
+
if (error instanceof PromptCancelledError) {
|
|
61
|
+
console.log(`\n${styles.muted('Operation cancelled')}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
console.error(`\n${styles.error('Error:')} ${error.message}`);
|
|
65
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
async function detect(context) {
|
|
5
|
+
// Check for GitLab CI
|
|
6
|
+
if (await fs.pathExists(path.join(context.root, '.gitlab-ci.yml'))) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
// Check for GitHub Actions (directory must exist and contain at least one .yml)
|
|
10
|
+
const workflowsDir = path.join(context.root, '.github', 'workflows');
|
|
11
|
+
if (await fs.pathExists(workflowsDir)) {
|
|
12
|
+
const files = await fs.readdir(workflowsDir);
|
|
13
|
+
if (files.some((f) => f.endsWith('.yml') || f.endsWith('.yaml'))) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
function getInstallCommand(context) {
|
|
20
|
+
switch (context.packageManager) {
|
|
21
|
+
case 'pnpm':
|
|
22
|
+
return 'pnpm install --frozen-lockfile';
|
|
23
|
+
case 'yarn':
|
|
24
|
+
return 'yarn install --frozen-lockfile';
|
|
25
|
+
default:
|
|
26
|
+
return 'npm ci';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function getRunCommand(context, script) {
|
|
30
|
+
switch (context.packageManager) {
|
|
31
|
+
case 'pnpm':
|
|
32
|
+
return `pnpm run ${script}`;
|
|
33
|
+
case 'yarn':
|
|
34
|
+
return `yarn ${script}`;
|
|
35
|
+
default:
|
|
36
|
+
return `npm run ${script}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function getScripts(packageJson) {
|
|
40
|
+
return packageJson.scripts ?? {};
|
|
41
|
+
}
|
|
42
|
+
function generateGitHubWorkflow(context) {
|
|
43
|
+
const scripts = getScripts(context.packageJson);
|
|
44
|
+
const installCmd = getInstallCommand(context);
|
|
45
|
+
const steps = [
|
|
46
|
+
' - uses: actions/checkout@v4',
|
|
47
|
+
' - uses: actions/setup-node@v4',
|
|
48
|
+
' with:',
|
|
49
|
+
' node-version: 20',
|
|
50
|
+
` - run: ${installCmd}`,
|
|
51
|
+
];
|
|
52
|
+
if (scripts.lint) {
|
|
53
|
+
steps.push(` - run: ${getRunCommand(context, 'lint')}`);
|
|
54
|
+
}
|
|
55
|
+
if (scripts.test) {
|
|
56
|
+
steps.push(` - run: ${getRunCommand(context, 'test')}`);
|
|
57
|
+
}
|
|
58
|
+
if (scripts.build) {
|
|
59
|
+
steps.push(` - run: ${getRunCommand(context, 'build')}`);
|
|
60
|
+
}
|
|
61
|
+
return `name: CI
|
|
62
|
+
on:
|
|
63
|
+
push:
|
|
64
|
+
branches: [main]
|
|
65
|
+
pull_request:
|
|
66
|
+
branches: [main]
|
|
67
|
+
jobs:
|
|
68
|
+
ci:
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
steps:
|
|
71
|
+
${steps.join('\n')}
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
function generateGitLabCI(context) {
|
|
75
|
+
const scripts = getScripts(context.packageJson);
|
|
76
|
+
const installCmd = getInstallCommand(context);
|
|
77
|
+
let config = `stages:
|
|
78
|
+
- install
|
|
79
|
+
- lint
|
|
80
|
+
- test
|
|
81
|
+
- build
|
|
82
|
+
|
|
83
|
+
install:
|
|
84
|
+
stage: install
|
|
85
|
+
image: node:20-alpine
|
|
86
|
+
script:
|
|
87
|
+
- ${installCmd}
|
|
88
|
+
cache:
|
|
89
|
+
paths:
|
|
90
|
+
- node_modules/
|
|
91
|
+
`;
|
|
92
|
+
if (scripts.lint) {
|
|
93
|
+
config += `
|
|
94
|
+
lint:
|
|
95
|
+
stage: lint
|
|
96
|
+
image: node:20-alpine
|
|
97
|
+
script:
|
|
98
|
+
- ${getRunCommand(context, 'lint')}
|
|
99
|
+
cache:
|
|
100
|
+
paths:
|
|
101
|
+
- node_modules/
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
if (scripts.test) {
|
|
105
|
+
config += `
|
|
106
|
+
test:
|
|
107
|
+
stage: test
|
|
108
|
+
image: node:20-alpine
|
|
109
|
+
script:
|
|
110
|
+
- ${getRunCommand(context, 'test')}
|
|
111
|
+
cache:
|
|
112
|
+
paths:
|
|
113
|
+
- node_modules/
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
if (scripts.build) {
|
|
117
|
+
config += `
|
|
118
|
+
build:
|
|
119
|
+
stage: build
|
|
120
|
+
image: node:20-alpine
|
|
121
|
+
script:
|
|
122
|
+
- ${getRunCommand(context, 'build')}
|
|
123
|
+
cache:
|
|
124
|
+
paths:
|
|
125
|
+
- node_modules/
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
return config;
|
|
129
|
+
}
|
|
130
|
+
async function apply(context) {
|
|
131
|
+
const { provider } = await prompts({
|
|
132
|
+
type: 'select',
|
|
133
|
+
name: 'provider',
|
|
134
|
+
message: 'Which CI provider?',
|
|
135
|
+
choices: [
|
|
136
|
+
{ title: 'GitHub Actions', value: 'github' },
|
|
137
|
+
{ title: 'GitLab CI', value: 'gitlab' },
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
if (!provider) {
|
|
141
|
+
throw new Error('CI provider selection cancelled');
|
|
142
|
+
}
|
|
143
|
+
if (provider === 'github') {
|
|
144
|
+
const workflowDir = path.join(context.root, '.github', 'workflows');
|
|
145
|
+
await fs.ensureDir(workflowDir);
|
|
146
|
+
await fs.writeFile(path.join(workflowDir, 'ci.yml'), generateGitHubWorkflow(context));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
await fs.writeFile(path.join(context.root, '.gitlab-ci.yml'), generateGitLabCI(context));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export const ciFeature = {
|
|
153
|
+
id: 'ci',
|
|
154
|
+
name: 'CI',
|
|
155
|
+
description: 'Continuous integration with GitHub Actions or GitLab CI',
|
|
156
|
+
detect,
|
|
157
|
+
apply,
|
|
158
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
async function detect(context) {
|
|
4
|
+
const configFiles = [
|
|
5
|
+
'Dockerfile',
|
|
6
|
+
'docker-compose.yml',
|
|
7
|
+
'docker-compose.yaml',
|
|
8
|
+
'compose.yml',
|
|
9
|
+
'compose.yaml',
|
|
10
|
+
];
|
|
11
|
+
for (const file of configFiles) {
|
|
12
|
+
if (await fs.pathExists(path.join(context.root, file))) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
function isFrontend(packageJson) {
|
|
19
|
+
const deps = {
|
|
20
|
+
...(packageJson.dependencies ?? {}),
|
|
21
|
+
...(packageJson.devDependencies ?? {}),
|
|
22
|
+
};
|
|
23
|
+
return 'vite' in deps;
|
|
24
|
+
}
|
|
25
|
+
function getInstallCommand(context) {
|
|
26
|
+
switch (context.packageManager) {
|
|
27
|
+
case 'pnpm':
|
|
28
|
+
return 'pnpm install --frozen-lockfile';
|
|
29
|
+
case 'yarn':
|
|
30
|
+
return 'yarn install --frozen-lockfile';
|
|
31
|
+
default:
|
|
32
|
+
return 'npm ci';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function generateDockerfile(context) {
|
|
36
|
+
const installCmd = getInstallCommand(context);
|
|
37
|
+
const frontend = isFrontend(context.packageJson);
|
|
38
|
+
if (frontend) {
|
|
39
|
+
return `FROM node:20-alpine AS build
|
|
40
|
+
WORKDIR /app
|
|
41
|
+
COPY package*.json ./
|
|
42
|
+
RUN ${installCmd}
|
|
43
|
+
COPY . .
|
|
44
|
+
RUN npm run build
|
|
45
|
+
|
|
46
|
+
FROM nginx:alpine
|
|
47
|
+
COPY --from=build /app/dist /usr/share/nginx/html
|
|
48
|
+
EXPOSE 80
|
|
49
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
return `FROM node:20-alpine AS build
|
|
53
|
+
WORKDIR /app
|
|
54
|
+
COPY package*.json ./
|
|
55
|
+
RUN ${installCmd}
|
|
56
|
+
COPY . .
|
|
57
|
+
RUN npm run build
|
|
58
|
+
|
|
59
|
+
FROM node:20-alpine
|
|
60
|
+
WORKDIR /app
|
|
61
|
+
COPY --from=build /app/dist ./dist
|
|
62
|
+
COPY --from=build /app/package*.json ./
|
|
63
|
+
RUN ${installCmd} --omit=dev
|
|
64
|
+
EXPOSE 3000
|
|
65
|
+
CMD ["node", "dist/index.js"]
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
function generateDockerCompose(context) {
|
|
69
|
+
const frontend = isFrontend(context.packageJson);
|
|
70
|
+
const port = frontend ? '8080:80' : '3000:3000';
|
|
71
|
+
return `services:
|
|
72
|
+
app:
|
|
73
|
+
build: .
|
|
74
|
+
ports:
|
|
75
|
+
- "${port}"
|
|
76
|
+
environment:
|
|
77
|
+
- NODE_ENV=production
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
const DOCKERIGNORE = `node_modules
|
|
81
|
+
dist
|
|
82
|
+
.git
|
|
83
|
+
*.log
|
|
84
|
+
.env
|
|
85
|
+
`;
|
|
86
|
+
async function apply(context) {
|
|
87
|
+
await fs.writeFile(path.join(context.root, 'Dockerfile'), generateDockerfile(context));
|
|
88
|
+
await fs.writeFile(path.join(context.root, 'docker-compose.yml'), generateDockerCompose(context));
|
|
89
|
+
await fs.writeFile(path.join(context.root, '.dockerignore'), DOCKERIGNORE);
|
|
90
|
+
}
|
|
91
|
+
export const dockerFeature = {
|
|
92
|
+
id: 'docker',
|
|
93
|
+
name: 'Docker',
|
|
94
|
+
description: 'Dockerfile, docker-compose, and .dockerignore',
|
|
95
|
+
detect,
|
|
96
|
+
apply,
|
|
97
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const ESLINT_CONFIG_TS = `import eslint from '@eslint/js';
|
|
4
|
+
import tseslint from 'typescript-eslint';
|
|
5
|
+
|
|
6
|
+
export default tseslint.config(
|
|
7
|
+
eslint.configs.recommended,
|
|
8
|
+
...tseslint.configs.recommended,
|
|
9
|
+
{
|
|
10
|
+
ignores: ['dist/'],
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
`;
|
|
14
|
+
const ESLINT_CONFIG_JS = `import eslint from '@eslint/js';
|
|
15
|
+
|
|
16
|
+
export default [
|
|
17
|
+
eslint.configs.recommended,
|
|
18
|
+
{
|
|
19
|
+
ignores: ['dist/'],
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
`;
|
|
23
|
+
async function detect(context) {
|
|
24
|
+
const configFiles = [
|
|
25
|
+
'eslint.config.js',
|
|
26
|
+
'eslint.config.mjs',
|
|
27
|
+
'eslint.config.cjs',
|
|
28
|
+
'.eslintrc.js',
|
|
29
|
+
'.eslintrc.json',
|
|
30
|
+
'.eslintrc.yml',
|
|
31
|
+
'.eslintrc',
|
|
32
|
+
];
|
|
33
|
+
for (const file of configFiles) {
|
|
34
|
+
if (await fs.pathExists(path.join(context.root, file))) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const deps = {
|
|
39
|
+
...(context.packageJson.dependencies ?? {}),
|
|
40
|
+
...(context.packageJson.devDependencies ?? {}),
|
|
41
|
+
};
|
|
42
|
+
return 'eslint' in deps;
|
|
43
|
+
}
|
|
44
|
+
async function apply(context) {
|
|
45
|
+
const packageJsonPath = path.join(context.root, 'package.json');
|
|
46
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
47
|
+
const devDeps = {
|
|
48
|
+
eslint: '^9.0.0',
|
|
49
|
+
'@eslint/js': '^9.0.0',
|
|
50
|
+
};
|
|
51
|
+
if (context.hasTypescript) {
|
|
52
|
+
devDeps['typescript-eslint'] = '^8.0.0';
|
|
53
|
+
}
|
|
54
|
+
packageJson.devDependencies = {
|
|
55
|
+
...packageJson.devDependencies,
|
|
56
|
+
...devDeps,
|
|
57
|
+
};
|
|
58
|
+
packageJson.scripts = {
|
|
59
|
+
...packageJson.scripts,
|
|
60
|
+
lint: 'eslint .',
|
|
61
|
+
};
|
|
62
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
63
|
+
const configContent = context.hasTypescript
|
|
64
|
+
? ESLINT_CONFIG_TS
|
|
65
|
+
: ESLINT_CONFIG_JS;
|
|
66
|
+
await fs.writeFile(path.join(context.root, 'eslint.config.js'), configContent);
|
|
67
|
+
}
|
|
68
|
+
export const eslintFeature = {
|
|
69
|
+
id: 'eslint',
|
|
70
|
+
name: 'ESLint',
|
|
71
|
+
description: 'Linter for JavaScript and TypeScript',
|
|
72
|
+
detect,
|
|
73
|
+
apply,
|
|
74
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const PRETTIER_CONFIG = `{
|
|
4
|
+
"semi": true,
|
|
5
|
+
"singleQuote": true,
|
|
6
|
+
"trailingComma": "all",
|
|
7
|
+
"printWidth": 80,
|
|
8
|
+
"tabWidth": 2
|
|
9
|
+
}
|
|
10
|
+
`;
|
|
11
|
+
async function detect(context) {
|
|
12
|
+
const configFiles = [
|
|
13
|
+
'.prettierrc',
|
|
14
|
+
'.prettierrc.json',
|
|
15
|
+
'.prettierrc.yaml',
|
|
16
|
+
'.prettierrc.yml',
|
|
17
|
+
'.prettierrc.js',
|
|
18
|
+
'.prettierrc.cjs',
|
|
19
|
+
'.prettierrc.mjs',
|
|
20
|
+
'prettier.config.js',
|
|
21
|
+
'prettier.config.cjs',
|
|
22
|
+
'prettier.config.mjs',
|
|
23
|
+
];
|
|
24
|
+
for (const file of configFiles) {
|
|
25
|
+
if (await fs.pathExists(path.join(context.root, file))) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const deps = {
|
|
30
|
+
...(context.packageJson.dependencies ?? {}),
|
|
31
|
+
...(context.packageJson.devDependencies ?? {}),
|
|
32
|
+
};
|
|
33
|
+
return 'prettier' in deps;
|
|
34
|
+
}
|
|
35
|
+
async function apply(context) {
|
|
36
|
+
const packageJsonPath = path.join(context.root, 'package.json');
|
|
37
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
38
|
+
packageJson.devDependencies = {
|
|
39
|
+
...packageJson.devDependencies,
|
|
40
|
+
prettier: '^3.0.0',
|
|
41
|
+
};
|
|
42
|
+
packageJson.scripts = {
|
|
43
|
+
...packageJson.scripts,
|
|
44
|
+
format: 'prettier --write .',
|
|
45
|
+
};
|
|
46
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
47
|
+
await fs.writeFile(path.join(context.root, '.prettierrc'), PRETTIER_CONFIG);
|
|
48
|
+
}
|
|
49
|
+
export const prettierFeature = {
|
|
50
|
+
id: 'prettier',
|
|
51
|
+
name: 'Prettier',
|
|
52
|
+
description: 'Code formatter for JavaScript and TypeScript',
|
|
53
|
+
detect,
|
|
54
|
+
apply,
|
|
55
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
async function getWebAppDir(root) {
|
|
4
|
+
const webDir = path.join(root, 'apps', 'web');
|
|
5
|
+
if (await fs.pathExists(path.join(webDir, 'package.json'))) {
|
|
6
|
+
return webDir;
|
|
7
|
+
}
|
|
8
|
+
return root;
|
|
9
|
+
}
|
|
10
|
+
async function detect(context) {
|
|
11
|
+
const targetDir = await getWebAppDir(context.root);
|
|
12
|
+
const configFiles = [
|
|
13
|
+
'tailwind.config.js',
|
|
14
|
+
'tailwind.config.ts',
|
|
15
|
+
'tailwind.config.mjs',
|
|
16
|
+
'tailwind.config.cjs',
|
|
17
|
+
];
|
|
18
|
+
for (const file of configFiles) {
|
|
19
|
+
if (await fs.pathExists(path.join(targetDir, file))) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Check deps in the target package.json
|
|
24
|
+
const pkgJson = targetDir === context.root
|
|
25
|
+
? context.packageJson
|
|
26
|
+
: await fs.readJson(path.join(targetDir, 'package.json'));
|
|
27
|
+
const deps = {
|
|
28
|
+
...(pkgJson.dependencies ?? {}),
|
|
29
|
+
...(pkgJson.devDependencies ?? {}),
|
|
30
|
+
};
|
|
31
|
+
return 'tailwindcss' in deps;
|
|
32
|
+
}
|
|
33
|
+
async function apply(context) {
|
|
34
|
+
const targetDir = await getWebAppDir(context.root);
|
|
35
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
36
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
37
|
+
packageJson.devDependencies = {
|
|
38
|
+
...packageJson.devDependencies,
|
|
39
|
+
tailwindcss: '^4.0.0',
|
|
40
|
+
'@tailwindcss/vite': '^4.0.0',
|
|
41
|
+
};
|
|
42
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
43
|
+
// Update vite.config.ts to add Tailwind plugin if it exists
|
|
44
|
+
const viteConfigPath = path.join(targetDir, 'vite.config.ts');
|
|
45
|
+
if (await fs.pathExists(viteConfigPath)) {
|
|
46
|
+
const viteConfig = await fs.readFile(viteConfigPath, 'utf-8');
|
|
47
|
+
if (!viteConfig.includes('@tailwindcss/vite')) {
|
|
48
|
+
const updated = `import tailwindcss from '@tailwindcss/vite';\n` +
|
|
49
|
+
viteConfig.replace(/plugins:\s*\[/, 'plugins: [\n tailwindcss(),');
|
|
50
|
+
await fs.writeFile(viteConfigPath, updated);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Create/update src/index.css with Tailwind import
|
|
54
|
+
const cssPath = path.join(targetDir, 'src', 'index.css');
|
|
55
|
+
await fs.ensureDir(path.join(targetDir, 'src'));
|
|
56
|
+
if (await fs.pathExists(cssPath)) {
|
|
57
|
+
const existing = await fs.readFile(cssPath, 'utf-8');
|
|
58
|
+
if (!existing.includes('@import "tailwindcss"')) {
|
|
59
|
+
await fs.writeFile(cssPath, `@import "tailwindcss";\n\n${existing}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
await fs.writeFile(cssPath, '@import "tailwindcss";\n');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export const tailwindFeature = {
|
|
67
|
+
id: 'tailwind',
|
|
68
|
+
name: 'Tailwind CSS',
|
|
69
|
+
description: 'Utility-first CSS framework',
|
|
70
|
+
detect,
|
|
71
|
+
apply,
|
|
72
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { setupVitest } from '../utils/setupVitest.js';
|
|
4
|
+
async function detect(context) {
|
|
5
|
+
const configFiles = [
|
|
6
|
+
'vitest.config.ts',
|
|
7
|
+
'vitest.config.js',
|
|
8
|
+
'vitest.config.mts',
|
|
9
|
+
'vitest.config.mjs',
|
|
10
|
+
];
|
|
11
|
+
for (const file of configFiles) {
|
|
12
|
+
if (await fs.pathExists(path.join(context.root, file))) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const deps = {
|
|
17
|
+
...(context.packageJson.dependencies ?? {}),
|
|
18
|
+
...(context.packageJson.devDependencies ?? {}),
|
|
19
|
+
};
|
|
20
|
+
return 'vitest' in deps;
|
|
21
|
+
}
|
|
22
|
+
async function apply(context) {
|
|
23
|
+
await setupVitest(context.root);
|
|
24
|
+
}
|
|
25
|
+
export const vitestFeature = {
|
|
26
|
+
id: 'vitest',
|
|
27
|
+
name: 'Vitest',
|
|
28
|
+
description: 'Unit testing framework for JavaScript and TypeScript',
|
|
29
|
+
detect,
|
|
30
|
+
apply,
|
|
31
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import { PromptCancelledError } from './initPrompts.js';
|
|
3
|
+
const onCancel = () => {
|
|
4
|
+
throw new PromptCancelledError();
|
|
5
|
+
};
|
|
6
|
+
export async function askFeatures(available) {
|
|
7
|
+
return prompts({
|
|
8
|
+
type: 'multiselect',
|
|
9
|
+
name: 'features',
|
|
10
|
+
message: 'Select features to add:',
|
|
11
|
+
choices: available.map((f) => ({
|
|
12
|
+
title: f.name,
|
|
13
|
+
value: f.id,
|
|
14
|
+
description: f.description,
|
|
15
|
+
})),
|
|
16
|
+
instructions: false,
|
|
17
|
+
hint: '- Space to select. Return to submit',
|
|
18
|
+
min: 1,
|
|
19
|
+
}, { onCancel });
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { detectPackageManager } from './detectPackageManager.js';
|
|
4
|
+
export async function detectProjectContext(cwd = process.cwd()) {
|
|
5
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
6
|
+
if (!(await fs.pathExists(packageJsonPath))) {
|
|
7
|
+
throw new Error('No package.json found in current directory. Run this command from a project root.');
|
|
8
|
+
}
|
|
9
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
10
|
+
const hasTypescript = await fs.pathExists(path.join(cwd, 'tsconfig.json'));
|
|
11
|
+
const packageManager = detectPackageManager(cwd);
|
|
12
|
+
return {
|
|
13
|
+
root: cwd,
|
|
14
|
+
packageJson,
|
|
15
|
+
hasTypescript,
|
|
16
|
+
packageManager,
|
|
17
|
+
};
|
|
18
|
+
}
|