devstarter-tool 0.2.2 → 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
@@ -4,12 +4,12 @@ CLI to generate projects with best practices and predefined configurations.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Project structures**: Basic (single project) or Monorepo (full-stack)
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
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,24 @@ 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
+
39
43
  # Preview without creating files
40
44
  devstarter init my-app --dry-run
41
45
  ```
42
46
 
43
- ## Usage
47
+ ## Commands
48
+
49
+ ### `devstarter init`
50
+
51
+ Scaffolds a new project from a template.
44
52
 
45
53
  ```bash
46
54
  devstarter init [project-name] [options]
47
55
  ```
48
56
 
49
- ### Options
50
-
51
57
  | Option | Description |
52
58
  |--------|-------------|
53
59
  | `-y, --yes` | Use default values without prompting |
@@ -55,9 +61,10 @@ devstarter init [project-name] [options]
55
61
  | `--template <name>` | Template to use (e.g., `basic`, `react`) |
56
62
  | `--vitest` | Add Vitest for testing |
57
63
  | `--no-git` | Skip Git repository initialization |
64
+ | `--no-vitest` | Skip Vitest testing framework setup |
58
65
  | `--dry-run` | Preview changes without creating files |
59
66
 
60
- ### Examples
67
+ #### Examples
61
68
 
62
69
  ```bash
63
70
  # Full interactive mode
@@ -79,6 +86,49 @@ devstarter init my-app --type frontend -y
79
86
  devstarter init my-app --no-git
80
87
  ```
81
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
+ ```
96
+
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
130
+ ```
131
+
82
132
  ## Project Structures
83
133
 
84
134
  ### Basic (single project)
@@ -130,23 +180,6 @@ my-app/
130
180
  |----------|-------------|
131
181
  | `basic` | Express + TypeScript |
132
182
 
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
183
  ## Requirements
151
184
 
152
185
  - Node.js 18+
@@ -175,7 +208,9 @@ node dist/cli.js init test-app --dry-run
175
208
  |--------|-------------|
176
209
  | `npm run build` | Compile TypeScript and copy templates |
177
210
  | `npm run dev` | Watch mode for development |
178
- | `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 |
179
214
  | `npm run lint` | Run ESLint |
180
215
  | `npm run format` | Format code with Prettier |
181
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')
@@ -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,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
+ }
@@ -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
+ };
@@ -106,7 +106,6 @@ ${projectName}/
106
106
  ## Getting Started
107
107
 
108
108
  \`\`\`bash
109
- pnpm install
110
109
  pnpm dev
111
110
  \`\`\`
112
111
  `;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devstarter-tool",
3
- "version": "0.2.2",
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",