devstarter-tool 0.2.1 → 0.2.3

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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  CLI to generate projects with best practices and predefined configurations.
4
4
 
5
+ ## Features
6
+
7
+ - **Project scaffolding**: Create frontend, backend, or monorepo projects in seconds
8
+ - **`add` command**: Add features like ESLint, Vitest, and Prettier to existing projects
9
+ - **Interactive prompts**: Guided project creation or flag-based configuration
10
+ - **Automatic dependency installation**: No need to run `npm install` manually
11
+ - **Package manager detection**: Automatically uses npm, pnpm, or yarn
12
+ - **Git initialization**: Optional repository setup
13
+ - **Dry-run mode**: Preview changes before creating files
14
+
5
15
  ## Installation
6
16
 
7
17
  ```bash
@@ -14,26 +24,47 @@ Or run directly with npx:
14
24
  npx devstarter-tool init my-app
15
25
  ```
16
26
 
17
- ## Usage
18
-
19
- ### Basic command
27
+ ## Quick Start
20
28
 
21
29
  ```bash
22
- devstarter init [project-name]
30
+ # Interactive mode - guided setup
31
+ devstarter init
32
+
33
+ # Quick project with defaults
34
+ devstarter init my-app -y
35
+
36
+ # Frontend project with Vitest
37
+ devstarter init my-app --type frontend --vitest
38
+
39
+ # Add features to an existing project
40
+ devstarter add prettier
41
+ devstarter add eslint
42
+
43
+ # Preview without creating files
44
+ devstarter init my-app --dry-run
23
45
  ```
24
46
 
25
- ### Options
47
+ ## Commands
48
+
49
+ ### `devstarter init`
50
+
51
+ Scaffolds a new project from a template.
52
+
53
+ ```bash
54
+ devstarter init [project-name] [options]
55
+ ```
26
56
 
27
57
  | Option | Description |
28
58
  |--------|-------------|
29
59
  | `-y, --yes` | Use default values without prompting |
30
60
  | `-t, --type <type>` | Project type: `frontend` or `backend` |
31
- | `--template <name>` | Template to use |
32
- | `--dry-run` | Preview changes without creating files |
33
- | `--no-git` | Skip git repository initialization |
61
+ | `--template <name>` | Template to use (e.g., `basic`, `react`) |
62
+ | `--vitest` | Add Vitest for testing |
63
+ | `--no-git` | Skip Git repository initialization |
34
64
  | `--no-vitest` | Skip Vitest testing framework setup |
65
+ | `--dry-run` | Preview changes without creating files |
35
66
 
36
- ### Examples
67
+ #### Examples
37
68
 
38
69
  ```bash
39
70
  # Full interactive mode
@@ -42,17 +73,60 @@ devstarter init
42
73
  # Create project with specific name
43
74
  devstarter init my-app
44
75
 
45
- # Create frontend project without prompts
46
- devstarter init my-app --type frontend -y
76
+ # Frontend with React template
77
+ devstarter init my-app --type frontend --template react
78
+
79
+ # Backend with testing setup
80
+ devstarter init my-api --type backend --vitest
47
81
 
48
- # Preview what files would be created
49
- devstarter init my-app --type frontend --dry-run
82
+ # Quick frontend with all defaults
83
+ devstarter init my-app --type frontend -y
50
84
 
51
- # Create project without git initialization
85
+ # Create without Git
52
86
  devstarter init my-app --no-git
87
+ ```
88
+
89
+ ### `devstarter add`
90
+
91
+ Adds features to an existing project. Automatically detects features already configured and skips them.
92
+
93
+ ```bash
94
+ devstarter add [feature] [options]
95
+ ```
53
96
 
54
- # Create project without Vitest setup
55
- devstarter init my-app --no-vitest
97
+ | Option | Description |
98
+ |--------|-------------|
99
+ | `--list` | List all available features |
100
+ | `-y, --yes` | Add all available features without prompting |
101
+ | `--dry-run` | Show what would be added without making changes |
102
+
103
+ #### Available Features
104
+
105
+ | Feature | Description | What it adds |
106
+ |---------|-------------|--------------|
107
+ | `eslint` | Linter for JavaScript and TypeScript | `eslint.config.js`, lint script, ESLint + typescript-eslint deps |
108
+ | `vitest` | Unit testing framework | `vitest.config.ts`, test scripts, Vitest dep |
109
+ | `prettier` | Code formatter | `.prettierrc`, format script, Prettier dep |
110
+
111
+ #### Examples
112
+
113
+ ```bash
114
+ # Add a specific feature
115
+ devstarter add prettier
116
+ devstarter add eslint
117
+ devstarter add vitest
118
+
119
+ # Interactive mode - choose from available features
120
+ devstarter add
121
+
122
+ # Add all available features at once
123
+ devstarter add -y
124
+
125
+ # List available features
126
+ devstarter add --list
127
+
128
+ # Preview changes
129
+ devstarter add prettier --dry-run
56
130
  ```
57
131
 
58
132
  ## Project Structures
@@ -62,13 +136,12 @@ devstarter init my-app --no-vitest
62
136
  ```
63
137
  my-app/
64
138
  ├── src/
65
- ├── main.ts (or main.tsx for React)
66
- │ └── __tests__/
67
- │ └── example.test.ts
68
- ├── vitest.config.ts
139
+ └── main.ts
69
140
  ├── package.json
70
- ├── README.md
71
- └── .git/ (if git is initialized)
141
+ ├── tsconfig.json
142
+ ├── vitest.config.ts # if --vitest
143
+ ├── node_modules/
144
+ └── .git/ # if git initialized
72
145
  ```
73
146
 
74
147
  ### Monorepo (full-stack)
@@ -76,10 +149,16 @@ my-app/
76
149
  ```
77
150
  my-app/
78
151
  ├── apps/
79
- │ ├── web/ <- frontend template
80
- └── api/ <- backend template
152
+ │ ├── web/ # frontend template
153
+ │ ├── src/
154
+ │ │ ├── package.json
155
+ │ │ └── vitest.config.ts
156
+ │ └── api/ # backend template
157
+ │ ├── src/
158
+ │ ├── package.json
159
+ │ └── vitest.config.ts
81
160
  ├── packages/
82
- │ └── shared/ <- shared code
161
+ │ └── shared/ # shared code
83
162
  ├── package.json
84
163
  ├── pnpm-workspace.yaml
85
164
  ├── tsconfig.base.json
@@ -92,7 +171,7 @@ my-app/
92
171
 
93
172
  | Template | Description |
94
173
  |----------|-------------|
95
- | `basic` | Minimal TypeScript with basic structure |
174
+ | `basic` | Vite + TypeScript |
96
175
  | `react` | React 18 + Vite + TypeScript |
97
176
 
98
177
  ### Backend
@@ -101,42 +180,12 @@ my-app/
101
180
  |----------|-------------|
102
181
  | `basic` | Express + TypeScript |
103
182
 
104
- ## Features
105
-
106
- - Project structure selection (basic or monorepo)
107
- - Automatic package manager detection (npm, pnpm, yarn)
108
- - Interactive template selection
109
- - Optional Git repository initialization
110
- - Optional Vitest testing framework setup
111
- - Dry-run mode to preview changes
112
- - Automatic project name normalization (kebab-case)
113
- - Colored output for better readability
114
-
115
- ## Testing Setup (Vitest)
116
-
117
- By default, projects are created with Vitest configured. This includes:
118
-
119
- - `vitest` as a dev dependency
120
- - `vitest.config.ts` configuration file
121
- - Example test file in `src/__tests__/example.test.ts`
122
- - `test` script in package.json
123
-
124
- For React projects, it also adds `jsdom` for DOM testing.
125
-
126
- To skip Vitest setup, use the `--no-vitest` flag:
127
-
128
- ```bash
129
- devstarter init my-app --no-vitest
130
- ```
131
-
132
- ## Development
133
-
134
- ### Requirements
183
+ ## Requirements
135
184
 
136
185
  - Node.js 18+
137
- - npm, pnpm or yarn
186
+ - npm, pnpm, or yarn
138
187
 
139
- ### Setup
188
+ ## Development
140
189
 
141
190
  ```bash
142
191
  # Clone repository
@@ -153,13 +202,15 @@ npm run build
153
202
  node dist/cli.js init test-app --dry-run
154
203
  ```
155
204
 
156
- ### Available Scripts
205
+ ### Scripts
157
206
 
158
207
  | Script | Description |
159
208
  |--------|-------------|
160
209
  | `npm run build` | Compile TypeScript and copy templates |
161
210
  | `npm run dev` | Watch mode for development |
162
- | `npm run test` | Run tests |
211
+ | `npm run test` | Run tests in watch mode |
212
+ | `npm run test:run` | Single test run |
213
+ | `npm run test:coverage` | Coverage with HTML + text reports |
163
214
  | `npm run lint` | Run ESLint |
164
215
  | `npm run format` | Format code with Prettier |
165
216
 
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')
@@ -14,5 +15,13 @@ program
14
15
  .option('--template <name>', 'Template variant (e.g. basic, react)')
15
16
  .option('--dry-run', 'Show what would be generated without creating files')
16
17
  .option('--no-git', 'Skip git repository initialization')
18
+ .option('--vitest', 'Add Vitest for testing')
17
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);
18
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,14 @@
1
+ import { eslintFeature } from '../../features/eslint.js';
2
+ import { vitestFeature } from '../../features/vitest.js';
3
+ import { prettierFeature } from '../../features/prettier.js';
4
+ const features = [eslintFeature, vitestFeature, prettierFeature];
5
+ const featureMap = new Map(features.map((f) => [f.id, f]));
6
+ export function getAvailableFeatureIds() {
7
+ return features.map((f) => f.id);
8
+ }
9
+ export function getFeature(id) {
10
+ return featureMap.get(id);
11
+ }
12
+ export function getAllFeatures() {
13
+ return features;
14
+ }
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_INIT_OPTIONS } from '../../types/project.js';
2
- import { askProjectName, askProjectStructure, askInitQuestions, askTemplate, askInitGit, } from '../../prompts/initPrompts.js';
2
+ import { askProjectName, askProjectStructure, askInitQuestions, askTemplate, askInitGit, askUseVitest, } from '../../prompts/initPrompts.js';
3
3
  import { normalizeProjectName } from '../../utils/normalize.js';
4
4
  import { detectPackageManager } from '../../utils/detectPackageManager.js';
5
5
  import { listTemplates } from '../../utils/listTemplate.js';
@@ -60,12 +60,25 @@ async function collectBasicContext(projectName, options, useDefaults) {
60
60
  const templates = listTemplates(projectType);
61
61
  const templateFromFlag = resolveTemplateFlag(options.template, templates);
62
62
  const template = await collectTemplate(templateFromFlag, templates, useDefaults);
63
+ // Obtener useVitest
64
+ const vitestFlagProvided = options.vitest !== undefined;
65
+ let useVitest;
66
+ if (vitestFlagProvided) {
67
+ useVitest = options.vitest;
68
+ }
69
+ else if (useDefaults) {
70
+ useVitest = DEFAULT_INIT_OPTIONS.useVitest;
71
+ }
72
+ else {
73
+ useVitest = (await askUseVitest()).useVitest;
74
+ }
63
75
  return {
64
76
  structure: 'basic',
65
77
  projectName,
66
78
  projectType,
67
79
  template,
68
80
  initGit,
81
+ useVitest,
69
82
  packageManager: detectPackageManager(),
70
83
  isDryRun: Boolean(options.dryRun),
71
84
  };
@@ -102,12 +115,25 @@ async function collectMonorepoContext(projectName, useDefaults, options) {
102
115
  initGit = gitAnswer.initGit;
103
116
  }
104
117
  }
118
+ // Obtener useVitest
119
+ const vitestFlagProvided = options.vitest !== undefined;
120
+ let useVitest;
121
+ if (vitestFlagProvided) {
122
+ useVitest = options.vitest;
123
+ }
124
+ else if (useDefaults) {
125
+ useVitest = DEFAULT_INIT_OPTIONS.useVitest;
126
+ }
127
+ else {
128
+ useVitest = (await askUseVitest()).useVitest;
129
+ }
105
130
  return {
106
131
  structure: 'monorepo',
107
132
  projectName,
108
133
  webTemplate,
109
134
  apiTemplate,
110
135
  initGit,
136
+ useVitest,
111
137
  packageManager: 'pnpm', // Monorepo usa pnpm por defecto
112
138
  isDryRun: Boolean(options.dryRun),
113
139
  };
@@ -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,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
+ };
@@ -3,7 +3,9 @@ import path from 'node:path';
3
3
  import { getTemplatePath } from '../utils/getTemplatePath.js';
4
4
  import { copyTemplate } from '../utils/copyTemplate.js';
5
5
  import { initGitRepo } from '../utils/git.js';
6
- export async function createMonorepo({ projectName, webTemplate, apiTemplate, initGit, }) {
6
+ import { setupVitest } from '../utils/setupVitest.js';
7
+ import { installDependencies } from '../utils/installDependencies.js';
8
+ export async function createMonorepo({ projectName, webTemplate, apiTemplate, initGit, useVitest, packageManager, }) {
7
9
  // 1. Resolver ruta absoluta del proyecto
8
10
  const projectRoot = path.resolve(process.cwd(), projectName);
9
11
  // 2. Evitar sobrescribir carpetas existentes
@@ -32,7 +34,14 @@ export async function createMonorepo({ projectName, webTemplate, apiTemplate, in
32
34
  }
33
35
  // 7. Crear package shared básico
34
36
  await createSharedPackage(projectRoot, projectName);
35
- // 8. Inicializar Git (si aplica)
37
+ // 8. Configurar Vitest en apps (si aplica)
38
+ if (useVitest) {
39
+ await setupVitest(path.join(projectRoot, 'apps', 'web'));
40
+ await setupVitest(path.join(projectRoot, 'apps', 'api'));
41
+ }
42
+ // 9. Instalar dependencias
43
+ installDependencies(projectRoot, packageManager);
44
+ // 10. Inicializar Git (si aplica)
36
45
  if (initGit) {
37
46
  initGitRepo(projectRoot);
38
47
  }
@@ -97,7 +106,6 @@ ${projectName}/
97
106
  ## Getting Started
98
107
 
99
108
  \`\`\`bash
100
- pnpm install
101
109
  pnpm dev
102
110
  \`\`\`
103
111
  `;
@@ -3,7 +3,9 @@ import path from 'node:path';
3
3
  import { getTemplatePath } from '../utils/getTemplatePath.js';
4
4
  import { copyTemplate } from '../utils/copyTemplate.js';
5
5
  import { initGitRepo } from '../utils/git.js';
6
- export async function createProject({ projectName, projectType, template, initGit, }) {
6
+ import { setupVitest } from '../utils/setupVitest.js';
7
+ import { installDependencies } from '../utils/installDependencies.js';
8
+ export async function createProject({ projectName, projectType, template, initGit, useVitest, packageManager, }) {
7
9
  // 1. Resolver ruta absoluta del proyecto
8
10
  const projectRoot = path.resolve(process.cwd(), projectName);
9
11
  // 2. Evitar sobrescribir carpetas existentes
@@ -21,7 +23,13 @@ export async function createProject({ projectName, projectType, template, initGi
21
23
  await copyTemplate(templatePath, projectRoot, {
22
24
  projectName,
23
25
  });
24
- // 6. Inicializar Git (si aplica)
26
+ // 6. Configurar Vitest (si aplica)
27
+ if (useVitest) {
28
+ await setupVitest(projectRoot);
29
+ }
30
+ // 7. Instalar dependencias
31
+ installDependencies(projectRoot, packageManager);
32
+ // 8. Inicializar Git (si aplica)
25
33
  if (initGit) {
26
34
  initGitRepo(projectRoot);
27
35
  }
@@ -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
+ }
@@ -80,3 +80,11 @@ export async function askInitGit() {
80
80
  initial: true,
81
81
  }, { onCancel });
82
82
  }
83
+ export async function askUseVitest() {
84
+ return prompts({
85
+ type: 'confirm',
86
+ name: 'useVitest',
87
+ message: 'Add Vitest for testing?',
88
+ initial: false,
89
+ }, { onCancel });
90
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -2,4 +2,5 @@ export const DEFAULT_INIT_OPTIONS = {
2
2
  projectStructure: 'basic',
3
3
  projectType: 'frontend',
4
4
  initGit: true,
5
+ useVitest: false,
5
6
  };
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ import { execSync } from 'node:child_process';
2
+ export function installDependencies(projectRoot, packageManager) {
3
+ const command = packageManager === 'yarn' ? 'yarn' : `${packageManager} install`;
4
+ execSync(command, {
5
+ cwd: projectRoot,
6
+ stdio: 'inherit',
7
+ });
8
+ }
@@ -14,7 +14,6 @@ export function printSummary(context) {
14
14
  console.log(`${styles.info('- Git:')} ${context.initGit ? styles.success('initialized') : styles.muted('not initialized')}\n`);
15
15
  console.log(styles.title('Next steps'));
16
16
  console.log(` ${styles.highlight(`cd ${context.projectName}`)}`);
17
- console.log(` ${styles.highlight(`${context.packageManager} install`)}`);
18
17
  console.log(` ${styles.highlight(`${context.packageManager} run dev`)}`);
19
18
  console.log('');
20
19
  }
@@ -0,0 +1,31 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ const VITEST_CONFIG = `import { defineConfig } from 'vitest/config';
4
+
5
+ export default defineConfig({
6
+ test: {
7
+ globals: true,
8
+ environment: 'node',
9
+ },
10
+ });
11
+ `;
12
+ export async function setupVitest(projectRoot) {
13
+ const packageJsonPath = path.join(projectRoot, 'package.json');
14
+ // Leer package.json existente
15
+ const packageJson = await fs.readJson(packageJsonPath);
16
+ // Añadir devDependencies
17
+ packageJson.devDependencies = {
18
+ ...packageJson.devDependencies,
19
+ vitest: '^3.0.0',
20
+ };
21
+ // Añadir scripts de test
22
+ packageJson.scripts = {
23
+ ...packageJson.scripts,
24
+ test: 'vitest',
25
+ 'test:run': 'vitest run',
26
+ };
27
+ // Escribir package.json actualizado
28
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
29
+ // Crear vitest.config.ts
30
+ await fs.writeFile(path.join(projectRoot, 'vitest.config.ts'), VITEST_CONFIG);
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devstarter-tool",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "CLI to generate projects with best practices (basic or monorepo)",
6
6
  "author": "abraham-diaz",
@@ -22,7 +22,9 @@
22
22
  "backend",
23
23
  "boilerplate",
24
24
  "starter",
25
- "devstarter"
25
+ "devstarter",
26
+ "vitest",
27
+ "testing"
26
28
  ],
27
29
  "engines": {
28
30
  "node": ">=18.0.0"