devstarter-tool 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/dist/cli.js +17 -0
  4. package/dist/commands/init/collector.js +113 -0
  5. package/dist/commands/init/resolvers.js +48 -0
  6. package/dist/commands/init.js +38 -0
  7. package/dist/generators/createMonorepo.js +142 -0
  8. package/dist/generators/createProject.js +28 -0
  9. package/dist/prompts/initPrompts.js +80 -0
  10. package/dist/templates/backend/basic/README.md.tpl +7 -0
  11. package/dist/templates/backend/basic/package.json.tpl +15 -0
  12. package/dist/templates/backend/basic/src/index.ts +12 -0
  13. package/dist/templates/frontend/basic/README.md.tpl +3 -0
  14. package/dist/templates/frontend/basic/package.json.tpl +7 -0
  15. package/dist/templates/frontend/basic/src/main.ts +1 -0
  16. package/dist/templates/frontend/react/README.md.tpl +9 -0
  17. package/dist/templates/frontend/react/index.html +12 -0
  18. package/dist/templates/frontend/react/package.json.tpl +21 -0
  19. package/dist/templates/frontend/react/src/App.tsx +8 -0
  20. package/dist/templates/frontend/react/src/main.tsx +11 -0
  21. package/dist/templates/frontend/react/tsconfig.json +13 -0
  22. package/dist/templates/frontend/react/vite.config.ts +6 -0
  23. package/dist/types/cli.js +1 -0
  24. package/dist/types/project.js +5 -0
  25. package/dist/utils/copyTemplate.js +22 -0
  26. package/dist/utils/copyTemplate.test.js +74 -0
  27. package/dist/utils/detectPackageManager.js +11 -0
  28. package/dist/utils/detectPackageManager.test.js +34 -0
  29. package/dist/utils/getTemplatePath.js +7 -0
  30. package/dist/utils/git.js +29 -0
  31. package/dist/utils/listTemplate.js +15 -0
  32. package/dist/utils/listTemplate.test.js +32 -0
  33. package/dist/utils/listTemplateFiles.js +17 -0
  34. package/dist/utils/normalize.js +9 -0
  35. package/dist/utils/normalize.test.js +46 -0
  36. package/dist/utils/printDryRun.js +44 -0
  37. package/dist/utils/printSummary.js +20 -0
  38. package/dist/utils/styles.js +10 -0
  39. package/dist/utils/templates.js +7 -0
  40. package/package.json +64 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 abraham-diaz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # devStarter CLI
2
+
3
+ CLI para generar proyectos con buenas prácticas y configuraciones predefinidas.
4
+
5
+ ## Instalacion
6
+
7
+ ```bash
8
+ npm install -g devstarter-cli
9
+ ```
10
+
11
+ O ejecutar directamente con npx:
12
+
13
+ ```bash
14
+ npx devstarter-cli init my-app
15
+ ```
16
+
17
+ ## Uso
18
+
19
+ ### Comando basico
20
+
21
+ ```bash
22
+ devstarter init [nombre-proyecto]
23
+ ```
24
+
25
+ ### Opciones
26
+
27
+ | Opcion | Descripcion |
28
+ |--------|-------------|
29
+ | `-y, --yes` | Usar valores por defecto sin preguntar |
30
+ | `-t, --type <tipo>` | Tipo de proyecto: `frontend` o `backend` |
31
+ | `--dry-run` | Previsualizar cambios sin crear archivos |
32
+
33
+ ### Ejemplos
34
+
35
+ ```bash
36
+ # Modo interactivo completo
37
+ devstarter init
38
+
39
+ # Crear proyecto con nombre especifico
40
+ devstarter init my-app
41
+
42
+ # Crear proyecto frontend sin preguntas
43
+ devstarter init my-app --type frontend -y
44
+
45
+ # Previsualizar que archivos se crearian
46
+ devstarter init my-app --type frontend --dry-run
47
+ ```
48
+
49
+ ## Templates disponibles
50
+
51
+ ### Frontend
52
+
53
+ | Template | Descripcion |
54
+ |----------|-------------|
55
+ | `basic` | TypeScript minimal con estructura basica |
56
+ | `react` | React 18 + Vite + TypeScript |
57
+
58
+ ### Backend
59
+
60
+ | Template | Descripcion |
61
+ |----------|-------------|
62
+ | `basic` | Express + TypeScript |
63
+
64
+ ## Estructura de proyecto generado
65
+
66
+ ```
67
+ my-app/
68
+ ├── src/
69
+ │ └── main.ts (o main.tsx para React)
70
+ ├── package.json
71
+ ├── README.md
72
+ └── .git/ (si se inicializa git)
73
+ ```
74
+
75
+ ## Caracteristicas
76
+
77
+ - Deteccion automatica del package manager (npm, pnpm, yarn)
78
+ - Seleccion interactiva de templates
79
+ - Inicializacion de repositorio Git opcional
80
+ - Modo dry-run para previsualizar cambios
81
+ - Normalizacion automatica de nombres de proyecto (kebab-case)
82
+ - Output con colores para mejor legibilidad
83
+
84
+ ## Desarrollo
85
+
86
+ ### Requisitos
87
+
88
+ - Node.js 18+
89
+ - npm, pnpm o yarn
90
+
91
+ ### Setup
92
+
93
+ ```bash
94
+ # Clonar repositorio
95
+ git clone https://github.com/abraham-diaz/devStarter-cli.git
96
+ cd devStarter-cli
97
+
98
+ # Instalar dependencias
99
+ npm install
100
+
101
+ # Compilar
102
+ npm run build
103
+
104
+ # Ejecutar localmente
105
+ node dist/cli.js init test-app --dry-run
106
+ ```
107
+
108
+ ### Scripts disponibles
109
+
110
+ | Script | Descripcion |
111
+ |--------|-------------|
112
+ | `npm run build` | Compila TypeScript y copia templates |
113
+ | `npm run dev` | Modo watch para desarrollo |
114
+ | `npm run lint` | Ejecuta ESLint |
115
+ | `npm run format` | Formatea codigo con Prettier |
116
+
117
+ ### Agregar nuevos templates
118
+
119
+ 1. Crear carpeta en `src/templates/<tipo>/<nombre-template>/`
120
+ 2. Agregar archivos del template (usar `.tpl` para archivos con placeholders)
121
+ 3. Placeholders disponibles: `{{projectName}}`
122
+ 4. Ejecutar `npm run build`
123
+
124
+ ## Licencia
125
+
126
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { initCommand } from './commands/init.js';
4
+ const program = new Command();
5
+ program
6
+ .name('devstarter')
7
+ .description('CLI para generar proyectos con buenas prácticas')
8
+ .version('0.1.0');
9
+ program
10
+ .command('init [projectName]')
11
+ .description('Inicializa un nuevo proyecto')
12
+ .option('-y, --yes', 'Use default options and skip prompts')
13
+ .option('-t, --type <type>', 'Project type (frontend | backend)')
14
+ .option('--template <name>', 'Template variant (e.g. basic, react)')
15
+ .option('--dry-run', 'Show what would be generated without creating files')
16
+ .action(initCommand);
17
+ program.parse(process.argv);
@@ -0,0 +1,113 @@
1
+ import { DEFAULT_INIT_OPTIONS } from '../../types/project.js';
2
+ import { askProjectName, askProjectStructure, askInitQuestions, askTemplate, askInitGit, } from '../../prompts/initPrompts.js';
3
+ import { normalizeProjectName } from '../../utils/normalize.js';
4
+ import { detectPackageManager } from '../../utils/detectPackageManager.js';
5
+ import { listTemplates } from '../../utils/listTemplate.js';
6
+ import { resolveProjectType, resolveTemplateFlag, resolveProjectName, resolveTemplateFinal, } from './resolvers.js';
7
+ /**
8
+ * Recolecta todas las respuestas necesarias del usuario
9
+ * combinando flags, argumentos, defaults y prompts interactivos
10
+ */
11
+ export async function collectInitContext(projectNameArg, options) {
12
+ const useDefaults = options.yes ?? false;
13
+ // Paso 1: Obtener nombre del proyecto
14
+ const projectName = await collectProjectName(projectNameArg, useDefaults);
15
+ // Paso 2: Obtener estructura (basic/monorepo)
16
+ const structure = await collectStructure(useDefaults);
17
+ // Paso 3: Bifurcar según estructura
18
+ if (structure === 'monorepo') {
19
+ return collectMonorepoContext(projectName, useDefaults, options);
20
+ }
21
+ return collectBasicContext(projectName, options, useDefaults);
22
+ }
23
+ async function collectProjectName(projectNameArg, useDefaults) {
24
+ if (projectNameArg) {
25
+ return normalizeProjectName(projectNameArg);
26
+ }
27
+ if (useDefaults) {
28
+ return normalizeProjectName(resolveProjectName(projectNameArg));
29
+ }
30
+ const answer = await askProjectName();
31
+ return normalizeProjectName(answer.projectName);
32
+ }
33
+ async function collectStructure(useDefaults) {
34
+ if (useDefaults) {
35
+ return DEFAULT_INIT_OPTIONS.projectStructure;
36
+ }
37
+ const answer = await askProjectStructure();
38
+ return answer.projectStructure;
39
+ }
40
+ async function collectBasicContext(projectName, options, useDefaults) {
41
+ const typeFromFlag = resolveProjectType(options.type);
42
+ // Obtener tipo de proyecto e initGit
43
+ let projectType;
44
+ let initGit;
45
+ if (useDefaults) {
46
+ projectType = typeFromFlag ?? DEFAULT_INIT_OPTIONS.projectType;
47
+ initGit = DEFAULT_INIT_OPTIONS.initGit;
48
+ }
49
+ else {
50
+ const answers = await askInitQuestions({
51
+ skipProjectName: true,
52
+ skipProjectType: Boolean(typeFromFlag),
53
+ });
54
+ projectType = typeFromFlag ?? answers.projectType;
55
+ initGit = answers.initGit;
56
+ }
57
+ // Obtener template
58
+ const templates = listTemplates(projectType);
59
+ const templateFromFlag = resolveTemplateFlag(options.template, templates);
60
+ const template = await collectTemplate(templateFromFlag, templates, useDefaults);
61
+ return {
62
+ structure: 'basic',
63
+ projectName,
64
+ projectType,
65
+ template,
66
+ initGit,
67
+ packageManager: detectPackageManager(),
68
+ isDryRun: Boolean(options.dryRun),
69
+ };
70
+ }
71
+ async function collectMonorepoContext(projectName, useDefaults, options) {
72
+ // Templates para web (frontend) y api (backend)
73
+ const frontendTemplates = listTemplates('frontend');
74
+ const backendTemplates = listTemplates('backend');
75
+ let webTemplate;
76
+ let apiTemplate;
77
+ let initGit;
78
+ if (useDefaults) {
79
+ webTemplate = frontendTemplates[0] ?? 'basic';
80
+ apiTemplate = backendTemplates[0] ?? 'basic';
81
+ initGit = DEFAULT_INIT_OPTIONS.initGit;
82
+ }
83
+ else {
84
+ const webAnswer = await askTemplate({
85
+ templates: frontendTemplates,
86
+ message: 'Template for apps/web (frontend):',
87
+ });
88
+ webTemplate = webAnswer.template;
89
+ const apiAnswer = await askTemplate({
90
+ templates: backendTemplates,
91
+ message: 'Template for apps/api (backend):',
92
+ });
93
+ apiTemplate = apiAnswer.template;
94
+ const gitAnswer = await askInitGit();
95
+ initGit = gitAnswer.initGit;
96
+ }
97
+ return {
98
+ structure: 'monorepo',
99
+ projectName,
100
+ webTemplate,
101
+ apiTemplate,
102
+ initGit,
103
+ packageManager: 'pnpm', // Monorepo usa pnpm por defecto
104
+ isDryRun: Boolean(options.dryRun),
105
+ };
106
+ }
107
+ async function collectTemplate(templateFlag, templates, useDefaults) {
108
+ const resolved = resolveTemplateFinal(templateFlag, templates, useDefaults);
109
+ if (resolved)
110
+ return resolved;
111
+ const answer = await askTemplate({ templates });
112
+ return answer.template;
113
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Valida y resuelve el flag --type a ProjectType
3
+ */
4
+ export function resolveProjectType(optionType) {
5
+ if (!optionType)
6
+ return undefined;
7
+ if (optionType === 'frontend' || optionType === 'backend') {
8
+ return optionType;
9
+ }
10
+ throw new Error(`Invalid --type value "${optionType}". Use "frontend" or "backend".`);
11
+ }
12
+ /**
13
+ * Valida el flag --template contra templates disponibles
14
+ */
15
+ export function resolveTemplateFlag(optionTemplate, available) {
16
+ if (!optionTemplate)
17
+ return undefined;
18
+ if (available.includes(optionTemplate))
19
+ return optionTemplate;
20
+ throw new Error(`Invalid --template "${optionTemplate}". Available: ${available.join(', ')}`);
21
+ }
22
+ /**
23
+ * Resuelve el nombre del proyecto con fallbacks
24
+ */
25
+ export function resolveProjectName(argName, promptName) {
26
+ if (argName)
27
+ return argName;
28
+ if (promptName)
29
+ return promptName;
30
+ return process.cwd().split(/[\\/]/).pop() ?? 'my-app';
31
+ }
32
+ /**
33
+ * Determina el template final basado en múltiples fuentes
34
+ * Retorna undefined si necesita input del usuario
35
+ */
36
+ export function resolveTemplateFinal(templateFlag, templates, useDefaults) {
37
+ // Prioridad 1: Flag explícito
38
+ if (templateFlag)
39
+ return templateFlag;
40
+ // Prioridad 2: Sin templates disponibles
41
+ if (templates.length === 0)
42
+ return 'basic';
43
+ // Prioridad 3: Modo defaults o único template
44
+ if (useDefaults || templates.length === 1)
45
+ return templates[0];
46
+ // Necesita input del usuario
47
+ return undefined;
48
+ }
@@ -0,0 +1,38 @@
1
+ import { PromptCancelledError } from '../prompts/initPrompts.js';
2
+ import { createProject } from '../generators/createProject.js';
3
+ import { createMonorepo } from '../generators/createMonorepo.js';
4
+ import { printSummary } from '../utils/printSummary.js';
5
+ import { printDryRun } from '../utils/printDryRun.js';
6
+ import { styles } from '../utils/styles.js';
7
+ import { collectInitContext } from './init/collector.js';
8
+ /**
9
+ * Comando principal para inicializar un nuevo proyecto
10
+ * Orquesta recolección, validación y ejecución
11
+ */
12
+ export async function initCommand(projectNameArg, options) {
13
+ try {
14
+ const context = await collectInitContext(projectNameArg, options);
15
+ if (context.isDryRun) {
16
+ printDryRun(context);
17
+ return;
18
+ }
19
+ if (context.structure === 'monorepo') {
20
+ await createMonorepo(context);
21
+ }
22
+ else {
23
+ await createProject(context);
24
+ }
25
+ printSummary(context);
26
+ }
27
+ catch (error) {
28
+ handleError(error);
29
+ }
30
+ }
31
+ function handleError(error) {
32
+ if (error instanceof PromptCancelledError) {
33
+ console.log(`\n${styles.muted('Operation cancelled')}`);
34
+ return;
35
+ }
36
+ console.error(`\n${styles.error('Error creating project:')}`);
37
+ console.error(styles.muted(error.message));
38
+ }
@@ -0,0 +1,142 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { getTemplatePath } from '../utils/getTemplatePath.js';
4
+ import { copyTemplate } from '../utils/copyTemplate.js';
5
+ import { initGitRepo } from '../utils/git.js';
6
+ export async function createMonorepo({ projectName, webTemplate, apiTemplate, initGit, }) {
7
+ // 1. Resolver ruta absoluta del proyecto
8
+ const projectRoot = path.resolve(process.cwd(), projectName);
9
+ // 2. Evitar sobrescribir carpetas existentes
10
+ if (await fs.pathExists(projectRoot)) {
11
+ throw new Error(`Directory "${projectName}" already exists`);
12
+ }
13
+ // 3. Crear estructura base del monorepo
14
+ await fs.ensureDir(projectRoot);
15
+ await fs.ensureDir(path.join(projectRoot, 'apps'));
16
+ await fs.ensureDir(path.join(projectRoot, 'packages'));
17
+ // 4. Crear archivos de configuración del monorepo
18
+ await createMonorepoConfig(projectRoot, projectName);
19
+ // 5. Copiar template de frontend a apps/web
20
+ const webTemplatePath = getTemplatePath('frontend', webTemplate);
21
+ if (await fs.pathExists(webTemplatePath)) {
22
+ await copyTemplate(webTemplatePath, path.join(projectRoot, 'apps', 'web'), {
23
+ projectName: `${projectName}-web`,
24
+ });
25
+ }
26
+ // 6. Copiar template de backend a apps/api
27
+ const apiTemplatePath = getTemplatePath('backend', apiTemplate);
28
+ if (await fs.pathExists(apiTemplatePath)) {
29
+ await copyTemplate(apiTemplatePath, path.join(projectRoot, 'apps', 'api'), {
30
+ projectName: `${projectName}-api`,
31
+ });
32
+ }
33
+ // 7. Crear package shared básico
34
+ await createSharedPackage(projectRoot, projectName);
35
+ // 8. Inicializar Git (si aplica)
36
+ if (initGit) {
37
+ initGitRepo(projectRoot);
38
+ }
39
+ }
40
+ async function createMonorepoConfig(projectRoot, projectName) {
41
+ // package.json raíz
42
+ const rootPackageJson = {
43
+ name: projectName,
44
+ private: true,
45
+ scripts: {
46
+ dev: 'pnpm -r dev',
47
+ build: 'pnpm -r build',
48
+ lint: 'pnpm -r lint',
49
+ },
50
+ };
51
+ await fs.writeJson(path.join(projectRoot, 'package.json'), rootPackageJson, {
52
+ spaces: 2,
53
+ });
54
+ // pnpm-workspace.yaml
55
+ const workspaceConfig = `packages:
56
+ - 'apps/*'
57
+ - 'packages/*'
58
+ `;
59
+ await fs.writeFile(path.join(projectRoot, 'pnpm-workspace.yaml'), workspaceConfig);
60
+ // tsconfig.base.json
61
+ const tsconfigBase = {
62
+ compilerOptions: {
63
+ target: 'ES2022',
64
+ module: 'ESNext',
65
+ moduleResolution: 'bundler',
66
+ strict: true,
67
+ esModuleInterop: true,
68
+ skipLibCheck: true,
69
+ forceConsistentCasingInFileNames: true,
70
+ declaration: true,
71
+ declarationMap: true,
72
+ composite: true,
73
+ },
74
+ };
75
+ await fs.writeJson(path.join(projectRoot, 'tsconfig.base.json'), tsconfigBase, {
76
+ spaces: 2,
77
+ });
78
+ // README.md
79
+ const readme = `# ${projectName}
80
+
81
+ Monorepo project created with devstarter-cli.
82
+
83
+ ## Structure
84
+
85
+ \`\`\`
86
+ ${projectName}/
87
+ ├─ apps/
88
+ │ ├─ web/ ← frontend
89
+ │ └─ api/ ← backend
90
+ ├─ packages/
91
+ │ └─ shared/ ← shared code
92
+ ├─ package.json
93
+ ├─ pnpm-workspace.yaml
94
+ └─ tsconfig.base.json
95
+ \`\`\`
96
+
97
+ ## Getting Started
98
+
99
+ \`\`\`bash
100
+ pnpm install
101
+ pnpm dev
102
+ \`\`\`
103
+ `;
104
+ await fs.writeFile(path.join(projectRoot, 'README.md'), readme);
105
+ }
106
+ async function createSharedPackage(projectRoot, projectName) {
107
+ const sharedDir = path.join(projectRoot, 'packages', 'shared');
108
+ await fs.ensureDir(path.join(sharedDir, 'src'));
109
+ // package.json para shared
110
+ const sharedPackageJson = {
111
+ name: `@${projectName}/shared`,
112
+ version: '0.0.1',
113
+ private: true,
114
+ type: 'module',
115
+ main: './src/index.ts',
116
+ types: './src/index.ts',
117
+ scripts: {
118
+ build: 'tsc',
119
+ dev: 'tsc --watch',
120
+ },
121
+ };
122
+ await fs.writeJson(path.join(sharedDir, 'package.json'), sharedPackageJson, {
123
+ spaces: 2,
124
+ });
125
+ // tsconfig.json para shared
126
+ const sharedTsconfig = {
127
+ extends: '../../tsconfig.base.json',
128
+ compilerOptions: {
129
+ outDir: './dist',
130
+ rootDir: './src',
131
+ },
132
+ include: ['src'],
133
+ };
134
+ await fs.writeJson(path.join(sharedDir, 'tsconfig.json'), sharedTsconfig, {
135
+ spaces: 2,
136
+ });
137
+ // index.ts básico
138
+ const indexContent = `// Shared utilities and types
139
+ export {};
140
+ `;
141
+ await fs.writeFile(path.join(sharedDir, 'src', 'index.ts'), indexContent);
142
+ }
@@ -0,0 +1,28 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { getTemplatePath } from '../utils/getTemplatePath.js';
4
+ import { copyTemplate } from '../utils/copyTemplate.js';
5
+ import { initGitRepo } from '../utils/git.js';
6
+ export async function createProject({ projectName, projectType, template, initGit, }) {
7
+ // 1. Resolver ruta absoluta del proyecto
8
+ const projectRoot = path.resolve(process.cwd(), projectName);
9
+ // 2. Evitar sobrescribir carpetas existentes
10
+ if (await fs.pathExists(projectRoot)) {
11
+ throw new Error(`Directory "${projectName}" already exists`);
12
+ }
13
+ // 3. Resolver el template a usar
14
+ const templatePath = getTemplatePath(projectType, template);
15
+ if (!(await fs.pathExists(templatePath))) {
16
+ throw new Error(`Template not found for type "${projectType}"`);
17
+ }
18
+ // 4. Crear carpeta raíz del proyecto
19
+ await fs.ensureDir(projectRoot);
20
+ // 5. Copiar template y reemplazar placeholders
21
+ await copyTemplate(templatePath, projectRoot, {
22
+ projectName,
23
+ });
24
+ // 6. Inicializar Git (si aplica)
25
+ if (initGit) {
26
+ initGitRepo(projectRoot);
27
+ }
28
+ }
@@ -0,0 +1,80 @@
1
+ import prompts from 'prompts';
2
+ export class PromptCancelledError extends Error {
3
+ constructor() {
4
+ super('Operation cancelled');
5
+ this.name = 'PromptCancelledError';
6
+ }
7
+ }
8
+ const onCancel = () => {
9
+ throw new PromptCancelledError();
10
+ };
11
+ export async function askProjectName() {
12
+ return prompts({
13
+ type: 'text',
14
+ name: 'projectName',
15
+ message: 'Project name:',
16
+ validate: (value) => value.length < 1 ? 'Project name is required' : true,
17
+ }, { onCancel });
18
+ }
19
+ export async function askProjectStructure() {
20
+ return prompts({
21
+ type: 'select',
22
+ name: 'projectStructure',
23
+ message: 'Project structure:',
24
+ choices: [
25
+ { title: 'Basic', value: 'basic', description: 'Single project (frontend or backend)' },
26
+ { title: 'Monorepo', value: 'monorepo', description: 'Full-stack with apps/web + apps/api' },
27
+ ],
28
+ }, { onCancel });
29
+ }
30
+ export async function askInitQuestions(options = {}) {
31
+ const questions = [];
32
+ if (!options.skipProjectName) {
33
+ questions.push({
34
+ type: 'text',
35
+ name: 'projectName',
36
+ message: 'Project name:',
37
+ validate: (value) => value.length < 1 ? 'Project name is required' : true,
38
+ });
39
+ }
40
+ if (!options.skipProjectType) {
41
+ questions.push({
42
+ type: 'select',
43
+ name: 'projectType',
44
+ message: 'Project type:',
45
+ choices: [
46
+ { title: 'Frontend', value: 'frontend' },
47
+ { title: 'Backend', value: 'backend' },
48
+ ],
49
+ });
50
+ }
51
+ questions.push({
52
+ type: 'confirm',
53
+ name: 'initGit',
54
+ message: 'Initialize a git repository?',
55
+ initial: true,
56
+ });
57
+ return prompts(questions, { onCancel });
58
+ }
59
+ export async function askTemplate(options) {
60
+ if (options.templates.length === 1) {
61
+ return { template: options.templates[0] };
62
+ }
63
+ return prompts({
64
+ type: 'select',
65
+ name: 'template',
66
+ message: options.message ?? 'Template:',
67
+ choices: options.templates.map((t) => ({
68
+ title: t,
69
+ value: t,
70
+ })),
71
+ }, { onCancel });
72
+ }
73
+ export async function askInitGit() {
74
+ return prompts({
75
+ type: 'confirm',
76
+ name: 'initGit',
77
+ message: 'Initialize a git repository?',
78
+ initial: true,
79
+ }, { onCancel });
80
+ }
@@ -0,0 +1,7 @@
1
+ # {{projectName}}
2
+
3
+ Basic backend project generated with devstarter-cli.
4
+
5
+ ## Scripts
6
+
7
+ - `npm run dev` – start the server
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "tsx src/index.ts"
7
+ },
8
+ "dependencies": {
9
+ "express": "^4.19.2"
10
+ },
11
+ "devDependencies": {
12
+ "@types/express": "^4.17.21",
13
+ "tsx": "^4.15.7"
14
+ }
15
+ }
@@ -0,0 +1,12 @@
1
+ import express, { Request, Response } from 'express';
2
+
3
+ const app = express();
4
+ const PORT = 3000;
5
+
6
+ app.get('/', (_req: Request, res: Response) => {
7
+ res.json({ message: 'Hello from {{projectName}} backend' });
8
+ });
9
+
10
+ app.listen(PORT, () => {
11
+ console.log(`Server running on http://localhost:${PORT}`);
12
+ });
@@ -0,0 +1,3 @@
1
+ # {{projectName}}
2
+
3
+ Generated with devstarter-cli
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "private": true,
4
+ "scripts": {
5
+ "dev": "echo \"TODO\""
6
+ }
7
+ }
@@ -0,0 +1 @@
1
+ console.log('Hello from {{projectName}}');
@@ -0,0 +1,9 @@
1
+ # {{projectName}}
2
+
3
+ React project generated with devstarter-cli.
4
+
5
+ ## Scripts
6
+
7
+ - `npm run dev`
8
+ - `npm run build`
9
+ - `npm run preview`
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{projectName}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "react": "^18.3.1",
12
+ "react-dom": "^18.3.1"
13
+ },
14
+ "devDependencies": {
15
+ "@types/react": "^18.3.3",
16
+ "@types/react-dom": "^18.3.0",
17
+ "@vitejs/plugin-react": "^4.3.1",
18
+ "typescript": "^5.5.4",
19
+ "vite": "^5.4.0"
20
+ }
21
+ }
@@ -0,0 +1,8 @@
1
+ export default function App() {
2
+ return (
3
+ <div style={{ padding: 24 }}>
4
+ <h1>Welcome to {{projectName}}</h1>
5
+ <p>Generated with devstarter-cli</p>
6
+ </div>
7
+ );
8
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+
5
+ ReactDOM.createRoot(
6
+ document.getElementById('root')!,
7
+ ).render(
8
+ <React.StrictMode>
9
+ <App />
10
+ </React.StrictMode>,
11
+ );
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["DOM", "DOM.Iterable", "ES2020"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "Bundler",
9
+ "strict": true,
10
+ "jsx": "react-jsx"
11
+ },
12
+ "include": ["src"]
13
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export const DEFAULT_INIT_OPTIONS = {
2
+ projectStructure: 'basic',
3
+ projectType: 'frontend',
4
+ initGit: true,
5
+ };
@@ -0,0 +1,22 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ export async function copyTemplate(templatePath, targetPath, vars) {
4
+ await fs.ensureDir(targetPath);
5
+ const entries = await fs.readdir(templatePath);
6
+ for (const entry of entries) {
7
+ const src = path.join(templatePath, entry);
8
+ const dest = path.join(targetPath, entry.replace('.tpl', ''));
9
+ const stat = await fs.stat(src);
10
+ if (stat.isDirectory()) {
11
+ await fs.ensureDir(dest);
12
+ await copyTemplate(src, dest, vars);
13
+ }
14
+ else {
15
+ let content = await fs.readFile(src, 'utf8');
16
+ for (const [key, value] of Object.entries(vars)) {
17
+ content = content.replaceAll(`{{${key}}}`, value);
18
+ }
19
+ await fs.writeFile(dest, content);
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs-extra';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { copyTemplate } from './copyTemplate.js';
6
+ describe('copyTemplate', () => {
7
+ let tempDir;
8
+ let templateDir;
9
+ let targetDir;
10
+ beforeEach(async () => {
11
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'copy-template-test-'));
12
+ templateDir = path.join(tempDir, 'template');
13
+ targetDir = path.join(tempDir, 'target');
14
+ await fs.ensureDir(templateDir);
15
+ await fs.ensureDir(targetDir);
16
+ });
17
+ afterEach(async () => {
18
+ await fs.remove(tempDir);
19
+ });
20
+ it('should copy a simple file', async () => {
21
+ await fs.writeFile(path.join(templateDir, 'file.txt'), 'Hello World');
22
+ await copyTemplate(templateDir, targetDir, {});
23
+ const content = await fs.readFile(path.join(targetDir, 'file.txt'), 'utf8');
24
+ expect(content).toBe('Hello World');
25
+ });
26
+ it('should replace template variables', async () => {
27
+ await fs.writeFile(path.join(templateDir, 'package.json'), '{ "name": "{{projectName}}" }');
28
+ await copyTemplate(templateDir, targetDir, { projectName: 'my-app' });
29
+ const content = await fs.readFile(path.join(targetDir, 'package.json'), 'utf8');
30
+ expect(content).toBe('{ "name": "my-app" }');
31
+ });
32
+ it('should replace multiple occurrences of same variable', async () => {
33
+ await fs.writeFile(path.join(templateDir, 'readme.md'), '# {{projectName}}\n\nWelcome to {{projectName}}!');
34
+ await copyTemplate(templateDir, targetDir, { projectName: 'awesome-app' });
35
+ const content = await fs.readFile(path.join(targetDir, 'readme.md'), 'utf8');
36
+ expect(content).toBe('# awesome-app\n\nWelcome to awesome-app!');
37
+ });
38
+ it('should replace multiple different variables', async () => {
39
+ await fs.writeFile(path.join(templateDir, 'config.json'), '{ "name": "{{projectName}}", "author": "{{author}}" }');
40
+ await copyTemplate(templateDir, targetDir, {
41
+ projectName: 'my-app',
42
+ author: 'John Doe',
43
+ });
44
+ const content = await fs.readFile(path.join(targetDir, 'config.json'), 'utf8');
45
+ expect(content).toBe('{ "name": "my-app", "author": "John Doe" }');
46
+ });
47
+ it('should remove .tpl extension from files', async () => {
48
+ await fs.writeFile(path.join(templateDir, 'gitignore.tpl'), 'node_modules');
49
+ await copyTemplate(templateDir, targetDir, {});
50
+ expect(await fs.pathExists(path.join(targetDir, 'gitignore'))).toBe(true);
51
+ expect(await fs.pathExists(path.join(targetDir, 'gitignore.tpl'))).toBe(false);
52
+ });
53
+ it('should copy directories recursively', async () => {
54
+ const subDir = path.join(templateDir, 'src');
55
+ await fs.ensureDir(subDir);
56
+ await fs.writeFile(path.join(subDir, 'index.ts'), 'export default {}');
57
+ await copyTemplate(templateDir, targetDir, {});
58
+ const content = await fs.readFile(path.join(targetDir, 'src', 'index.ts'), 'utf8');
59
+ expect(content).toBe('export default {}');
60
+ });
61
+ it('should handle nested directories with variables', async () => {
62
+ const nestedDir = path.join(templateDir, 'src', 'components');
63
+ await fs.ensureDir(nestedDir);
64
+ await fs.writeFile(path.join(nestedDir, 'App.tsx'), 'const App = () => <div>{{projectName}}</div>');
65
+ await copyTemplate(templateDir, targetDir, { projectName: 'my-react-app' });
66
+ const content = await fs.readFile(path.join(targetDir, 'src', 'components', 'App.tsx'), 'utf8');
67
+ expect(content).toBe('const App = () => <div>my-react-app</div>');
68
+ });
69
+ it('should handle empty directories', async () => {
70
+ await fs.ensureDir(path.join(templateDir, 'empty-folder'));
71
+ await copyTemplate(templateDir, targetDir, {});
72
+ expect(await fs.pathExists(path.join(targetDir, 'empty-folder'))).toBe(true);
73
+ });
74
+ });
@@ -0,0 +1,11 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function detectPackageManager(cwd = process.cwd()) {
4
+ if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) {
5
+ return 'pnpm';
6
+ }
7
+ if (fs.existsSync(path.join(cwd, 'yarn.lock'))) {
8
+ return 'yarn';
9
+ }
10
+ return 'npm';
11
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs-extra';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { detectPackageManager } from './detectPackageManager.js';
6
+ describe('detectPackageManager', () => {
7
+ let tempDir;
8
+ beforeEach(async () => {
9
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'detect-pm-test-'));
10
+ });
11
+ afterEach(async () => {
12
+ await fs.remove(tempDir);
13
+ });
14
+ it('should detect pnpm when pnpm-lock.yaml exists', async () => {
15
+ await fs.writeFile(path.join(tempDir, 'pnpm-lock.yaml'), '');
16
+ expect(detectPackageManager(tempDir)).toBe('pnpm');
17
+ });
18
+ it('should detect yarn when yarn.lock exists', async () => {
19
+ await fs.writeFile(path.join(tempDir, 'yarn.lock'), '');
20
+ expect(detectPackageManager(tempDir)).toBe('yarn');
21
+ });
22
+ it('should default to npm when no lock file exists', async () => {
23
+ expect(detectPackageManager(tempDir)).toBe('npm');
24
+ });
25
+ it('should prioritize pnpm over yarn when both exist', async () => {
26
+ await fs.writeFile(path.join(tempDir, 'pnpm-lock.yaml'), '');
27
+ await fs.writeFile(path.join(tempDir, 'yarn.lock'), '');
28
+ expect(detectPackageManager(tempDir)).toBe('pnpm');
29
+ });
30
+ it('should detect npm when only package-lock.json exists', async () => {
31
+ await fs.writeFile(path.join(tempDir, 'package-lock.json'), '{}');
32
+ expect(detectPackageManager(tempDir)).toBe('npm');
33
+ });
34
+ });
@@ -0,0 +1,7 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ const __filename = fileURLToPath(import.meta.url);
4
+ const __dirname = path.dirname(__filename);
5
+ export function getTemplatePath(projectType, template) {
6
+ return path.resolve(__dirname, '../../dist/templates', projectType, template);
7
+ }
@@ -0,0 +1,29 @@
1
+ import { execSync } from 'node:child_process';
2
+ import fs from 'fs-extra';
3
+ import path from 'node:path';
4
+ export function initGitRepo(projectRoot) {
5
+ if (!fs.existsSync(projectRoot)) {
6
+ throw new Error('Project root does not exist');
7
+ }
8
+ // Evitar inicializar Git dentro de otro repo
9
+ if (fs.existsSync(path.join(projectRoot, '.git'))) {
10
+ throw new Error('Git repository already exists in project root');
11
+ }
12
+ try {
13
+ execSync('git init', {
14
+ cwd: projectRoot,
15
+ stdio: 'ignore',
16
+ });
17
+ execSync('git add .', {
18
+ cwd: projectRoot,
19
+ stdio: 'ignore',
20
+ });
21
+ execSync('git commit -m "Initial commit"', {
22
+ cwd: projectRoot,
23
+ stdio: 'ignore',
24
+ });
25
+ }
26
+ catch {
27
+ throw new Error('Failed to initialize git repository');
28
+ }
29
+ }
@@ -0,0 +1,15 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ export function listTemplates(projectType) {
7
+ const base = path.resolve(__dirname, '../../dist/templates', projectType);
8
+ if (!fs.existsSync(base))
9
+ return [];
10
+ return fs
11
+ .readdirSync(base, { withFileTypes: true })
12
+ .filter((d) => d.isDirectory())
13
+ .map((d) => d.name)
14
+ .sort();
15
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { listTemplates } from './listTemplate.js';
3
+ describe('listTemplates', () => {
4
+ it('should return array for frontend type', () => {
5
+ const templates = listTemplates('frontend');
6
+ expect(Array.isArray(templates)).toBe(true);
7
+ });
8
+ it('should return array for backend type', () => {
9
+ const templates = listTemplates('backend');
10
+ expect(Array.isArray(templates)).toBe(true);
11
+ });
12
+ it('should return sorted templates', () => {
13
+ const templates = listTemplates('frontend');
14
+ if (templates.length > 1) {
15
+ const sorted = [...templates].sort();
16
+ expect(templates).toEqual(sorted);
17
+ }
18
+ });
19
+ it('should include basic template for frontend if exists', () => {
20
+ const templates = listTemplates('frontend');
21
+ // Si hay templates, verificar que sean strings válidos
22
+ templates.forEach((template) => {
23
+ expect(typeof template).toBe('string');
24
+ expect(template.length).toBeGreaterThan(0);
25
+ });
26
+ });
27
+ it('should return empty array for non-existent project type directory', () => {
28
+ // Forzamos el tipo para probar el caso edge
29
+ const templates = listTemplates('nonexistent');
30
+ expect(templates).toEqual([]);
31
+ });
32
+ });
@@ -0,0 +1,17 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function listTemplateFiles(templateDir, baseDir = '') {
4
+ const entries = fs.readdirSync(templateDir, { withFileTypes: true });
5
+ const files = [];
6
+ for (const entry of entries) {
7
+ const fullPath = path.join(templateDir, entry.name);
8
+ const relativePath = path.join(baseDir, entry.name);
9
+ if (entry.isDirectory()) {
10
+ files.push(...listTemplateFiles(fullPath, relativePath));
11
+ }
12
+ else {
13
+ files.push(relativePath);
14
+ }
15
+ }
16
+ return files;
17
+ }
@@ -0,0 +1,9 @@
1
+ export function normalizeProjectName(input) {
2
+ return input
3
+ .trim()
4
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
5
+ .replace(/[\s_]+/g, '-')
6
+ .replace(/[^a-zA-Z0-9-]/g, '')
7
+ .replace(/-+/g, '-')
8
+ .toLowerCase();
9
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { normalizeProjectName } from './normalize.js';
3
+ describe('normalizeProjectName', () => {
4
+ it('should trim whitespace', () => {
5
+ expect(normalizeProjectName(' my-app ')).toBe('my-app');
6
+ });
7
+ it('should convert camelCase to kebab-case', () => {
8
+ expect(normalizeProjectName('myApp')).toBe('my-app');
9
+ expect(normalizeProjectName('myAwesomeApp')).toBe('my-awesome-app');
10
+ });
11
+ it('should convert PascalCase to kebab-case', () => {
12
+ expect(normalizeProjectName('MyApp')).toBe('my-app');
13
+ expect(normalizeProjectName('MyAwesomeApp')).toBe('my-awesome-app');
14
+ });
15
+ it('should replace spaces with dashes', () => {
16
+ expect(normalizeProjectName('my app')).toBe('my-app');
17
+ expect(normalizeProjectName('my awesome app')).toBe('my-awesome-app');
18
+ });
19
+ it('should replace underscores with dashes', () => {
20
+ expect(normalizeProjectName('my_app')).toBe('my-app');
21
+ expect(normalizeProjectName('my__app')).toBe('my-app');
22
+ });
23
+ it('should remove invalid characters', () => {
24
+ expect(normalizeProjectName('my@app!')).toBe('myapp');
25
+ expect(normalizeProjectName('my$app#name')).toBe('myappname');
26
+ });
27
+ it('should collapse multiple dashes', () => {
28
+ expect(normalizeProjectName('my--app')).toBe('my-app');
29
+ expect(normalizeProjectName('my---awesome---app')).toBe('my-awesome-app');
30
+ });
31
+ it('should convert to lowercase', () => {
32
+ expect(normalizeProjectName('MY-APP')).toBe('my-app');
33
+ expect(normalizeProjectName('MyApp')).toBe('my-app');
34
+ });
35
+ it('should handle complex cases', () => {
36
+ expect(normalizeProjectName(' My_Awesome App! ')).toBe('my-awesome-app');
37
+ expect(normalizeProjectName('myApp_v2')).toBe('my-app-v2');
38
+ });
39
+ it('should handle empty string', () => {
40
+ expect(normalizeProjectName('')).toBe('');
41
+ });
42
+ it('should handle already normalized names', () => {
43
+ expect(normalizeProjectName('my-app')).toBe('my-app');
44
+ expect(normalizeProjectName('my-awesome-app')).toBe('my-awesome-app');
45
+ });
46
+ });
@@ -0,0 +1,44 @@
1
+ import { styles } from './styles.js';
2
+ import { listTemplateFiles } from './listTemplateFiles.js';
3
+ import { getTemplatePath } from './getTemplatePath.js';
4
+ export function printDryRun(context) {
5
+ const baseDir = `./${context.projectName}`;
6
+ console.log(`\n${styles.warning('Dry run – no changes will be made')}\n`);
7
+ console.log(styles.title('Plan'));
8
+ console.log(`${styles.info('- Create directory:')} ${baseDir}`);
9
+ if (context.structure === 'monorepo') {
10
+ printMonorepoPlan(context);
11
+ }
12
+ else {
13
+ printBasicPlan(context);
14
+ }
15
+ console.log(`${styles.info('- Git:')} ${context.initGit ? 'would initialize' : 'skipped'}\n`);
16
+ console.log(styles.title('Next steps'));
17
+ console.log(` ${styles.highlight(`cd ${context.projectName}`)}`);
18
+ console.log(` ${styles.highlight(`${context.packageManager} install`)}`);
19
+ console.log(` ${styles.highlight(`${context.packageManager} run dev`)}`);
20
+ console.log('');
21
+ }
22
+ function printBasicPlan(context) {
23
+ const templatePath = getTemplatePath(context.projectType, context.template);
24
+ const files = listTemplateFiles(templatePath);
25
+ console.log(`${styles.info('- Template:')} ${context.projectType}/${context.template}`);
26
+ console.log(styles.info('- Files:'));
27
+ files.forEach((file) => {
28
+ console.log(` - ${file}`);
29
+ });
30
+ }
31
+ function printMonorepoPlan(context) {
32
+ console.log(`${styles.info('- Structure:')} monorepo`);
33
+ console.log(`${styles.info('- Web template:')} frontend/${context.webTemplate}`);
34
+ console.log(`${styles.info('- API template:')} backend/${context.apiTemplate}`);
35
+ console.log(styles.info('- Structure:'));
36
+ console.log(' - apps/');
37
+ console.log(' - web/ (frontend)');
38
+ console.log(' - api/ (backend)');
39
+ console.log(' - packages/');
40
+ console.log(' - shared/');
41
+ console.log(' - package.json');
42
+ console.log(' - pnpm-workspace.yaml');
43
+ console.log(' - tsconfig.base.json');
44
+ }
@@ -0,0 +1,20 @@
1
+ import { styles } from './styles.js';
2
+ export function printSummary(context) {
3
+ console.log(`\n${styles.success('✔ Project created successfully')}\n`);
4
+ console.log(styles.title('Summary'));
5
+ if (context.structure === 'monorepo') {
6
+ console.log(`${styles.info('- Structure:')} monorepo`);
7
+ console.log(`${styles.info('- Web template:')} frontend/${context.webTemplate}`);
8
+ console.log(`${styles.info('- API template:')} backend/${context.apiTemplate}`);
9
+ }
10
+ else {
11
+ console.log(`${styles.info('- Template:')} ${context.projectType}/${context.template}`);
12
+ }
13
+ console.log(`${styles.info('- Directory:')} ./${context.projectName}`);
14
+ console.log(`${styles.info('- Git:')} ${context.initGit ? styles.success('initialized') : styles.muted('not initialized')}\n`);
15
+ console.log(styles.title('Next steps'));
16
+ console.log(` ${styles.highlight(`cd ${context.projectName}`)}`);
17
+ console.log(` ${styles.highlight(`${context.packageManager} install`)}`);
18
+ console.log(` ${styles.highlight(`${context.packageManager} run dev`)}`);
19
+ console.log('');
20
+ }
@@ -0,0 +1,10 @@
1
+ import chalk from 'chalk';
2
+ export const styles = {
3
+ title: chalk.bold,
4
+ success: chalk.green,
5
+ info: chalk.cyan,
6
+ warning: chalk.yellow,
7
+ error: chalk.red,
8
+ muted: chalk.gray,
9
+ highlight: chalk.bold.cyan,
10
+ };
@@ -0,0 +1,7 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ const __filename = fileURLToPath(import.meta.url);
4
+ const __dirname = path.dirname(__filename);
5
+ export function getTemplatePath(projectType, templateName = 'basic') {
6
+ return path.resolve(__dirname, '..', 'templates', projectType, templateName);
7
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "devstarter-tool",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "CLI to generate projects with best practices (basic or monorepo)",
6
+ "author": "abraham-diaz",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/abraham-diaz/devstarter-cli.git"
11
+ },
12
+ "homepage": "https://github.com/abraham-diaz/devstarter-cli#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/abraham-diaz/devstarter-cli/issues"
15
+ },
16
+ "keywords": [
17
+ "cli",
18
+ "scaffold",
19
+ "generator",
20
+ "monorepo",
21
+ "frontend",
22
+ "backend",
23
+ "boilerplate",
24
+ "starter",
25
+ "devstarter"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "bin": {
34
+ "devstarter": "dist/cli.js"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc && node scripts/copyTemplates.mjs",
38
+ "dev": "tsc --watch",
39
+ "test": "vitest",
40
+ "test:run": "vitest run",
41
+ "test:coverage": "vitest run --coverage",
42
+ "lint": "eslint .",
43
+ "format": "prettier --write .",
44
+ "prepare": "husky",
45
+ "prepublishOnly": "npm run build && npm run test:run"
46
+ },
47
+ "devDependencies": {
48
+ "@types/fs-extra": "^11.0.4",
49
+ "@types/prompts": "^2.4.9",
50
+ "@typescript-eslint/eslint-plugin": "^8.50.1",
51
+ "@typescript-eslint/parser": "^8.50.1",
52
+ "chalk": "^5.6.2",
53
+ "eslint": "^9.39.2",
54
+ "husky": "^9.1.7",
55
+ "prettier": "^3.7.4",
56
+ "typescript": "5.9.3",
57
+ "vitest": "^4.0.16"
58
+ },
59
+ "dependencies": {
60
+ "commander": "^14.0.2",
61
+ "fs-extra": "^11.3.3",
62
+ "prompts": "^2.4.2"
63
+ }
64
+ }