create-ng-tailwind 1.0.0 → 2.0.2

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.
@@ -1,412 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { program } = require('commander');
4
- const chalk = require('chalk');
5
- const inquirer = require('inquirer');
6
- const ora = require('ora');
7
- const fs = require('fs-extra');
8
- const path = require('path');
9
- const execa = require('execa');
3
+ const CLI = require("../lib/cli");
10
4
 
11
- const packageJson = require('../package.json');
12
-
13
- program
14
- .name('create-ng-tailwind')
15
- .description('Create a new Angular project with Tailwind CSS preconfigured')
16
- .version(packageJson.version)
17
- .argument('[project-name]', 'name of the project')
18
- .option('-d, --directory <dir>', 'target directory (defaults to project name)')
19
- .option('--skip-install', 'skip npm install')
20
- .option('--no-routing', 'disable Angular routing (enabled by default)')
21
- .option('--style <style>', 'stylesheet format (css, scss, sass, less)', 'css')
22
- .option('--ssr', 'enable Server-Side Rendering (SSR)')
23
- .option('--zoneless', 'create zoneless application without zone.js')
24
- .option('--ai-config <tools>', 'AI tools to configure (none, claude, cursor, gemini, copilot, jetbrains, windsurf)', 'none')
25
- .option('--interactive', 'enable interactive prompts for project configuration')
26
- .parse();
27
-
28
- const options = program.opts();
29
- const projectName = program.args[0];
30
-
31
- async function getInteractiveOptions() {
32
- console.log(chalk.yellow('šŸ“‹ Configure your Angular project:\n'));
33
-
34
- const questions = [
35
- {
36
- type: 'list',
37
- name: 'style',
38
- message: 'Which stylesheet format would you like to use?',
39
- choices: [
40
- { name: 'CSS', value: 'css' },
41
- { name: 'SCSS [ https://sass-lang.com/documentation/syntax#scss ]', value: 'scss' },
42
- { name: 'Sass [ https://sass-lang.com/documentation/syntax#the-indented-syntax ]', value: 'sass' },
43
- { name: 'Less [ https://lesscss.org ]', value: 'less' }
44
- ],
45
- default: 'css'
46
- },
47
- {
48
- type: 'confirm',
49
- name: 'routing',
50
- message: 'Would you like to add Angular routing?',
51
- default: true
52
- },
53
- {
54
- type: 'confirm',
55
- name: 'ssr',
56
- message: 'Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)?',
57
- default: false
58
- },
59
- {
60
- type: 'confirm',
61
- name: 'zoneless',
62
- message: 'Do you want to create a \'zoneless\' application without zone.js?',
63
- default: false
64
- },
65
- {
66
- type: 'checkbox',
67
- name: 'aiConfig',
68
- message: 'Which AI tools do you want to configure with Angular best practices?',
69
- choices: [
70
- { name: 'None', value: 'none', checked: true },
71
- { name: 'Claude [ https://docs.anthropic.com/en/docs/claude-code/memory ]', value: 'claude' },
72
- { name: 'Cursor [ https://docs.cursor.com/en/context/rules ]', value: 'cursor' },
73
- { name: 'Gemini [ https://ai.google.dev/gemini-api/docs ]', value: 'gemini' },
74
- { name: 'GitHub Copilot [ https://code.visualstudio.com/docs/copilot/copilot-customization#_custom-instructions ]', value: 'copilot' },
75
- { name: 'JetBrains AI Assistant [ https://www.jetbrains.com/help/junie/customize-guidelines.html ]', value: 'jetbrains' },
76
- { name: 'Windsurf [ https://docs.windsurf.com/windsurf/cascade/memories#rules ]', value: 'windsurf' }
77
- ],
78
- validate: (answer) => {
79
- if (answer.includes('none') && answer.length > 1) {
80
- return 'Please select either "None" or specific AI tools, not both.';
81
- }
82
- return true;
83
- }
84
- }
85
- ];
86
-
87
- return await inquirer.prompt(questions);
88
- }
89
-
90
- async function main() {
91
- console.log(chalk.cyan.bold(`\nšŸš€ Create Angular + Tailwind CSS Project\n`));
92
-
93
- let finalProjectName = projectName;
94
- let targetDirectory = options.directory;
95
-
96
- // If no project name provided, prompt for it
97
- if (!finalProjectName) {
98
- const answers = await inquirer.prompt([
99
- {
100
- type: 'input',
101
- name: 'projectName',
102
- message: 'What is your project name?',
103
- default: 'my-angular-app',
104
- validate: (input) => {
105
- if (!input.trim()) {
106
- return 'Project name is required';
107
- }
108
- if (!/^[a-zA-Z0-9-_]+$/.test(input)) {
109
- return 'Project name can only contain letters, numbers, hyphens, and underscores';
110
- }
111
- return true;
112
- }
113
- }
114
- ]);
115
- finalProjectName = answers.projectName;
116
- }
117
-
118
- // Get interactive configuration if requested
119
- let interactiveOptions = {};
120
- if (options.interactive) {
121
- interactiveOptions = await getInteractiveOptions();
122
-
123
- // Override command line options with interactive choices
124
- options.style = interactiveOptions.style;
125
- options.noRouting = !interactiveOptions.routing; // Flip because we use --no-routing
126
- options.ssr = interactiveOptions.ssr;
127
- options.zoneless = interactiveOptions.zoneless;
128
- options.aiConfig = interactiveOptions.aiConfig.includes('none') ? 'none' : interactiveOptions.aiConfig.join(',');
129
- }
130
-
131
- // Set target directory
132
- if (!targetDirectory) {
133
- targetDirectory = finalProjectName;
134
- }
135
-
136
- const fullPath = path.resolve(process.cwd(), targetDirectory);
137
-
138
- // Check if directory already exists
139
- if (await fs.pathExists(fullPath)) {
140
- const { overwrite } = await inquirer.prompt([
141
- {
142
- type: 'confirm',
143
- name: 'overwrite',
144
- message: `Directory ${targetDirectory} already exists. Do you want to overwrite it?`,
145
- default: false
146
- }
147
- ]);
148
-
149
- if (!overwrite) {
150
- console.log(chalk.yellow('Operation cancelled.'));
151
- process.exit(0);
152
- }
153
-
154
- await fs.remove(fullPath);
155
- }
156
-
157
- try {
158
- await createAngularProject(finalProjectName, targetDirectory);
159
- await configureTailwind(fullPath);
160
- await setupProjectFiles(fullPath, finalProjectName);
161
-
162
- console.log(chalk.green.bold('\nāœ… Project created successfully!\n'));
163
- console.log(chalk.cyan('Next steps:'));
164
- console.log(chalk.white(` cd ${targetDirectory}`));
165
- if (options.skipInstall) {
166
- console.log(chalk.white(' npm install'));
167
- }
168
- console.log(chalk.white(' ng serve'));
169
- console.log(chalk.green('\nšŸŽ‰ Happy coding with Angular and Tailwind CSS!'));
170
-
171
- } catch (error) {
172
- console.error(chalk.red('Error creating project:'), error.message);
173
- process.exit(1);
174
- }
175
- }
176
-
177
- async function createAngularProject(projectName, directory) {
178
- const spinner = ora('Creating Angular project...').start();
179
-
180
- try {
181
- // Routing is enabled by default, disabled only with --no-routing flag
182
- const routingFlag = options.noRouting ? '--routing=false' : '--routing=true';
183
- const styleFlag = `--style=${options.style}`;
184
- const ssrFlag = options.ssr ? '--ssr=true' : '--ssr=false';
185
- const zonelessFlag = options.zoneless ? '--zoneless=true' : '--zoneless=false';
186
- const aiConfigFlag = `--ai-config=${options.aiConfig}`;
187
-
188
- await execa.command(`npx @angular/cli@latest new ${projectName} ${routingFlag} ${styleFlag} ${ssrFlag} ${zonelessFlag} ${aiConfigFlag} --skip-git --package-manager=npm --interactive=false`, {
189
- cwd: process.cwd(),
190
- stdio: 'pipe'
191
- });
192
-
193
- // If directory name is different from project name, rename it
194
- if (directory !== projectName) {
195
- await fs.move(path.resolve(process.cwd(), projectName), path.resolve(process.cwd(), directory));
196
- }
197
-
198
- spinner.succeed('Angular project created');
199
- } catch (error) {
200
- spinner.fail('Failed to create Angular project');
201
- throw error;
202
- }
203
- }
204
-
205
- async function configureTailwind(projectPath) {
206
- const spinner = ora('Installing and configuring Tailwind CSS...').start();
207
-
208
- try {
209
- // Install Tailwind CSS dependencies or add them to package.json
210
- if (!options.skipInstall) {
211
- await execa.command('npm install tailwindcss @tailwindcss/postcss postcss --force', {
212
- cwd: projectPath,
213
- stdio: 'pipe'
214
- });
215
- } else {
216
- // Add dependencies to package.json when skipping install
217
- const packageJsonPath = path.join(projectPath, 'package.json');
218
- const packageJson = await fs.readJson(packageJsonPath);
219
-
220
- if (!packageJson.dependencies) {
221
- packageJson.dependencies = {};
222
- }
223
-
224
- packageJson.dependencies['tailwindcss'] = '^3.4.0';
225
- packageJson.dependencies['@tailwindcss/postcss'] = '^1.0.0';
226
- packageJson.dependencies['postcss'] = '^8.4.0';
227
-
228
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
229
- }
230
-
231
- // Create .postcssrc.json file with @tailwindcss/postcss plugin
232
- const postcssConfig = {
233
- "plugins": {
234
- "@tailwindcss/postcss": {}
235
- }
236
- };
237
-
238
- await fs.writeFile(path.join(projectPath, '.postcssrc.json'), JSON.stringify(postcssConfig, null, 2));
239
-
240
- // Update styles file with Tailwind import
241
- const stylesPath = path.join(projectPath, `src/styles.${options.style}`);
242
- const tailwindImport = `@import "tailwindcss";
243
-
244
- /* Your custom styles here */
245
- `;
246
-
247
- await fs.writeFile(stylesPath, tailwindImport);
248
-
249
- spinner.succeed('Tailwind CSS configured');
250
- } catch (error) {
251
- spinner.fail('Failed to configure Tailwind CSS');
252
- throw error;
253
- }
254
- }
255
-
256
- async function setupProjectFiles(projectPath, projectName) {
257
- const spinner = ora('Setting up project files...').start();
258
-
259
- try {
260
- // Rename files to traditional Angular naming convention
261
- const appPath = path.join(projectPath, 'src/app');
262
- await fs.move(path.join(appPath, 'app.html'), path.join(appPath, 'app.component.html'));
263
- await fs.move(path.join(appPath, 'app.ts'), path.join(appPath, 'app.component.ts'));
264
- await fs.move(path.join(appPath, `app.${options.style}`), path.join(appPath, `app.component.${options.style}`));
265
- await fs.move(path.join(appPath, 'app.spec.ts'), path.join(appPath, 'app.component.spec.ts'));
266
-
267
- // Update app.component.html with a Tailwind example
268
- const appComponentHtml = `<div class="min-h-screen bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
269
- <div class="bg-white rounded-lg shadow-2xl p-8 max-w-md w-full mx-4">
270
- <div class="text-center">
271
- <h1 class="text-3xl font-bold text-gray-800 mb-4">
272
- Welcome to {{ title }}!
273
- </h1>
274
- <div class="mb-6">
275
- <svg class="mx-auto h-16 w-16 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
276
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
277
- </svg>
278
- </div>
279
- <p class="text-gray-600 mb-6">
280
- Your Angular project with Tailwind CSS is ready to go!
281
- </p>
282
- <div class="space-y-4">
283
- <button class="w-full bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded-lg transition duration-200">
284
- Get Started
285
- </button>
286
- <a href="https://tailwindcss.com/docs" target="_blank"
287
- class="block w-full bg-gray-100 hover:bg-gray-200 text-gray-800 font-semibold py-2 px-4 rounded-lg transition duration-200 text-center">
288
- View Tailwind Docs
289
- </a>
290
- </div>
291
- </div>
292
- </div>
293
- </div>
294
-
295
- <!-- Example of responsive grid with Tailwind -->
296
- <div class="bg-gray-50 py-12">
297
- <div class="max-w-6xl mx-auto px-4">
298
- <h2 class="text-2xl font-bold text-center text-gray-800 mb-8">
299
- Built with Angular & Tailwind CSS
300
- </h2>
301
- <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
302
- <div class="bg-white rounded-lg shadow-md p-6 text-center">
303
- <div class="bg-red-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
304
- <svg class="w-8 h-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
305
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"></path>
306
- </svg>
307
- </div>
308
- <h3 class="text-lg font-semibold text-gray-800 mb-2">Angular</h3>
309
- <p class="text-gray-600 text-sm">Powerful TypeScript framework for building web applications</p>
310
- </div>
311
- <div class="bg-white rounded-lg shadow-md p-6 text-center">
312
- <div class="bg-green-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
313
- <svg class="w-8 h-8 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
314
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
315
- </svg>
316
- </div>
317
- <h3 class="text-lg font-semibold text-gray-800 mb-2">Tailwind CSS</h3>
318
- <p class="text-gray-600 text-sm">Utility-first CSS framework for rapid UI development</p>
319
- </div>
320
- <div class="bg-white rounded-lg shadow-md p-6 text-center">
321
- <div class="bg-blue-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
322
- <svg class="w-8 h-8 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
323
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
324
- </svg>
325
- </div>
326
- <h3 class="text-lg font-semibold text-gray-800 mb-2">Ready to Go</h3>
327
- <p class="text-gray-600 text-sm">Zero configuration required - start building immediately</p>
328
- </div>
329
- </div>
330
- </div>
331
- </div>
332
- `;
333
-
334
- // Update app.component.html with our Tailwind template
335
- await fs.writeFile(path.join(projectPath, 'src/app/app.component.html'), appComponentHtml);
336
-
337
- // Update app.component.ts with traditional naming and structure
338
- const appComponentTs = `import { Component } from '@angular/core';
339
-
340
- @Component({
341
- selector: 'app-root',
342
- templateUrl: './app.component.html',
343
- styleUrl: './app.component.${options.style}'
344
- })
345
- export class AppComponent {
346
- title = '${projectName}';
347
- }
348
- `;
349
-
350
- await fs.writeFile(path.join(projectPath, 'src/app/app.component.ts'), appComponentTs);
351
-
352
- // Update the component style file
353
- const componentStyleContent = `/* Add your component-specific styles here */
354
- `;
355
- await fs.writeFile(path.join(projectPath, `src/app/app.component.${options.style}`), componentStyleContent);
356
-
357
- // Update main.ts to import AppComponent instead of App
358
- const mainTsContent = `import { bootstrapApplication } from '@angular/platform-browser';
359
- import { appConfig } from './app/app.config';
360
- import { AppComponent } from './app/app.component';
361
-
362
- bootstrapApplication(AppComponent, appConfig)
363
- .catch((err) => console.error(err));
364
- `;
365
-
366
- await fs.writeFile(path.join(projectPath, 'src/main.ts'), mainTsContent);
367
-
368
- // Update the spec file if needed
369
- const specContent = `import { TestBed } from '@angular/core/testing';
370
- import { AppComponent } from './app.component';
371
-
372
- describe('AppComponent', () => {
373
- beforeEach(async () => {
374
- await TestBed.configureTestingModule({
375
- imports: [AppComponent],
376
- }).compileComponents();
377
- });
378
-
379
- it('should create the app', () => {
380
- const fixture = TestBed.createComponent(AppComponent);
381
- const app = fixture.componentInstance;
382
- expect(app).toBeTruthy();
383
- });
384
-
385
- it(\`should have the '\${projectName}' title\`, () => {
386
- const fixture = TestBed.createComponent(AppComponent);
387
- const app = fixture.componentInstance;
388
- expect(app.title).toEqual('${projectName}');
389
- });
390
-
391
- it('should render title', () => {
392
- const fixture = TestBed.createComponent(AppComponent);
393
- fixture.detectChanges();
394
- const compiled = fixture.nativeElement as HTMLElement;
395
- expect(compiled.querySelector('h1')?.textContent).toContain('Welcome to ${projectName}');
396
- });
397
- });
398
- `;
399
-
400
- await fs.writeFile(path.join(projectPath, 'src/app/app.component.spec.ts'), specContent);
401
-
402
- spinner.succeed('Project files configured');
403
- } catch (error) {
404
- spinner.fail('Failed to setup project files');
405
- throw error;
406
- }
407
- }
408
-
409
- main().catch((error) => {
410
- console.error(chalk.red('Unexpected error:'), error);
5
+ // Create and run the CLI
6
+ const cli = new CLI();
7
+ cli.run().catch((error) => {
8
+ console.error("Unexpected error:", error);
411
9
  process.exit(1);
412
10
  });
@@ -0,0 +1,222 @@
1
+ const { program } = require("commander");
2
+ const inquirer = require("inquirer");
3
+ const chalk = require("chalk");
4
+ const CLI = require("./interactive");
5
+ const { createLogger } = require("../utils/logger");
6
+
7
+ class CLIHandler {
8
+ constructor() {
9
+ this.logger = createLogger();
10
+ }
11
+
12
+ parseOptions() {
13
+ program
14
+ .name("create-ng-tailwind")
15
+ .description("Create Angular projects with Tailwind CSS")
16
+ .version(require("../../package.json").version)
17
+ .argument("[project-name]", "Project name")
18
+ .option("-t, --template <template>", "Template to use (minimal, starter)")
19
+ .option("--routing", "Enable Angular routing")
20
+ .option("--no-routing", "Disable Angular routing")
21
+ .option(
22
+ "--ssr",
23
+ "Enable Server-Side Rendering (SSR) and Static Site Generation",
24
+ )
25
+ .option("--zoneless", "Create zoneless application without zone.js")
26
+ .option(
27
+ "--ai-config <tools>",
28
+ "AI tools to configure (none, claude, cursor, gemini, copilot, jetbrains, windsurf)",
29
+ )
30
+ .option("--skip-install", "Skip npm install")
31
+ .parse();
32
+
33
+ return program.opts();
34
+ }
35
+
36
+ async getProjectName() {
37
+ const projectName = program.args[0];
38
+
39
+ if (!projectName) {
40
+ const { name } = await this.logger.prompt([
41
+ {
42
+ type: "input",
43
+ name: "name",
44
+ message: "What is your project name?",
45
+ default: "my-angular-app",
46
+ validate: (input) => {
47
+ if (!input.trim()) return "Project name is required";
48
+ if (!/^[a-z0-9-]+$/.test(input)) {
49
+ return "Project name can only contain lowercase letters, numbers, and hyphens";
50
+ }
51
+ return true;
52
+ },
53
+ },
54
+ ]);
55
+ return name;
56
+ }
57
+
58
+ return projectName;
59
+ }
60
+
61
+ async getTemplateChoice() {
62
+ const { template } = await this.logger.prompt([
63
+ {
64
+ type: "list",
65
+ name: "template",
66
+ message: "Which template would you like to use?",
67
+ choices: [
68
+ {
69
+ name: "Minimal - Clean Angular + Tailwind CSS (zero features, build your own way)",
70
+ value: "minimal",
71
+ },
72
+ {
73
+ name: "Starter - Professional foundation (routing, i18n, auth UI, components, services, interceptors)",
74
+ value: "starter",
75
+ },
76
+ ],
77
+ default: "starter",
78
+ },
79
+ ]);
80
+ return template;
81
+ }
82
+
83
+ async getConfigurationOptions() {
84
+ const answers = await this.logger.prompt([
85
+ {
86
+ type: "confirm",
87
+ name: "ssr",
88
+ message:
89
+ "Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)?",
90
+ default: false,
91
+ },
92
+ {
93
+ type: "confirm",
94
+ name: "zoneless",
95
+ message:
96
+ "Do you want to create a 'zoneless' application without zone.js?",
97
+ default: false,
98
+ },
99
+ {
100
+ type: "checkbox",
101
+ name: "aiConfig",
102
+ message:
103
+ "Which AI tools do you want to configure with Angular best practices? https://angular.dev/ai/develop-with-ai",
104
+ choices: [
105
+ { name: "None", value: "none", checked: true },
106
+ {
107
+ name: "Claude [ https://docs.anthropic.com/en/docs/claude-code/memory ]",
108
+ value: "claude",
109
+ },
110
+ {
111
+ name: "Cursor [ https://docs.cursor.com/en/context/rules ]",
112
+ value: "cursor",
113
+ },
114
+ {
115
+ name: "Gemini [ https://ai.google.dev/gemini-api/docs ]",
116
+ value: "gemini",
117
+ },
118
+ {
119
+ name: "GitHub Copilot [ https://docs.github.com/copilot ]",
120
+ value: "copilot",
121
+ },
122
+ {
123
+ name: "JetBrains AI [ https://www.jetbrains.com/ai/ ]",
124
+ value: "jetbrains",
125
+ },
126
+ {
127
+ name: "Windsurf [ https://codeium.com/windsurf ]",
128
+ value: "windsurf",
129
+ },
130
+ ],
131
+ validate: (answer) => {
132
+ // If "None" is selected with other options, remove "none"
133
+ if (answer.includes("none") && answer.length > 1) {
134
+ return "Please select either 'None' or specific AI tools, not both";
135
+ }
136
+ return true;
137
+ },
138
+ },
139
+ ]);
140
+
141
+ return answers;
142
+ }
143
+
144
+ async buildConfiguration(options, projectName, configOptions) {
145
+ const config = {
146
+ projectName,
147
+ template: options.template || "starter",
148
+ style: "css", // Always use CSS (Tailwind v4 official approach)
149
+ routing: options.routing !== false, // Default to true unless --no-routing is specified
150
+ ssr: configOptions.ssr || false,
151
+ zoneless: configOptions.zoneless || false,
152
+ aiConfig: configOptions.aiConfig || ["none"], // Array of AI tools
153
+ skipInstall: options.skipInstall || false,
154
+ fullPath: require("path").resolve(process.cwd(), projectName),
155
+ };
156
+
157
+ return config;
158
+ }
159
+
160
+ async run() {
161
+ try {
162
+ this.logger.welcome();
163
+
164
+ const options = this.parseOptions();
165
+
166
+ // Check if options were provided via CLI flags
167
+ const wasTemplateProvided = process.argv.some(
168
+ (arg) => arg.startsWith("--template") || arg.startsWith("-t"),
169
+ );
170
+ const wasSsrProvided = process.argv.some((arg) =>
171
+ arg.startsWith("--ssr"),
172
+ );
173
+ const wasZonelessProvided = process.argv.some((arg) =>
174
+ arg.startsWith("--zoneless"),
175
+ );
176
+ const wasAiConfigProvided = process.argv.some((arg) =>
177
+ arg.startsWith("--ai-config"),
178
+ );
179
+
180
+ // 1. Get project name
181
+ const projectName = await this.getProjectName();
182
+
183
+ // 2. Get template choice (if not provided via CLI)
184
+ if (!wasTemplateProvided) {
185
+ const template = await this.getTemplateChoice();
186
+ options.template = template;
187
+ }
188
+
189
+ // 3. Get configuration options (SSR, Zoneless, AI tools)
190
+ let configOptions = {
191
+ ssr: options.ssr || false,
192
+ zoneless: options.zoneless || false,
193
+ aiConfig: options.aiConfig ? [options.aiConfig] : ["none"], // Array
194
+ };
195
+
196
+ // Only ask for config options if NONE of them were provided via CLI
197
+ if (!wasSsrProvided && !wasZonelessProvided && !wasAiConfigProvided) {
198
+ const promptedOptions = await this.getConfigurationOptions();
199
+ configOptions.ssr = promptedOptions.ssr;
200
+ configOptions.zoneless = promptedOptions.zoneless;
201
+ configOptions.aiConfig = promptedOptions.aiConfig;
202
+ }
203
+
204
+ const config = await this.buildConfiguration(
205
+ options,
206
+ projectName,
207
+ configOptions,
208
+ );
209
+
210
+ const ProjectManager = require("../managers/ProjectManager");
211
+ const projectManager = new ProjectManager(config, this.logger);
212
+ await projectManager.create();
213
+
214
+ this.logger.success(config);
215
+ } catch (error) {
216
+ this.logger.error("Failed to create project", error);
217
+ process.exit(1);
218
+ }
219
+ }
220
+ }
221
+
222
+ module.exports = CLIHandler;
@@ -0,0 +1,26 @@
1
+ const inquirer = require("inquirer");
2
+
3
+ async function getInteractiveOptions() {
4
+ const questions = [
5
+ {
6
+ type: "list",
7
+ name: "template",
8
+ message: "Which template would you like to use?",
9
+ choices: [
10
+ {
11
+ name: "Minimal - Clean Angular + Tailwind CSS (zero features, build your own way)",
12
+ value: "minimal",
13
+ },
14
+ {
15
+ name: "Starter - Professional foundation (routing, i18n, auth UI, components, services, interceptors)",
16
+ value: "starter",
17
+ },
18
+ ],
19
+ default: "starter",
20
+ },
21
+ ];
22
+
23
+ return await inquirer.prompt(questions);
24
+ }
25
+
26
+ module.exports = { getInteractiveOptions };
@@ -0,0 +1,23 @@
1
+ function validateProjectName(input) {
2
+ if (!input.trim()) {
3
+ return "Project name is required";
4
+ }
5
+
6
+ if (!/^[a-z0-9-]+$/.test(input)) {
7
+ return "Project name can only contain lowercase letters, numbers, and hyphens";
8
+ }
9
+
10
+ if (input.length < 2) {
11
+ return "Project name must be at least 2 characters";
12
+ }
13
+
14
+ if (input.length > 100) {
15
+ return "Project name must be less than 100 characters";
16
+ }
17
+
18
+ return true;
19
+ }
20
+
21
+ module.exports = {
22
+ validateProjectName,
23
+ };