@vue-skuilder/cli 0.1.4 → 0.1.6

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,6 +1,153 @@
1
1
  import inquirer from 'inquirer';
2
2
  import chalk from 'chalk';
3
- import { CliOptions, ProjectConfig, PREDEFINED_THEMES, ThemeConfig } from '../types.js';
3
+ import PouchDB from 'pouchdb';
4
+ import { CliOptions, ProjectConfig, PREDEFINED_THEMES } from '../types.js';
5
+
6
+ interface CourseInfo {
7
+ id: string;
8
+ name: string;
9
+ description?: string;
10
+ }
11
+
12
+ interface CourseDocument {
13
+ name?: string;
14
+ title?: string;
15
+ description?: string;
16
+ }
17
+
18
+ /**
19
+ * Fetch available courses from a CouchDB server
20
+ */
21
+ async function fetchAvailableCourses(
22
+ serverUrl: string,
23
+ username?: string,
24
+ password?: string
25
+ ): Promise<CourseInfo[]> {
26
+ const dbUrl = `${serverUrl}/coursedb-lookup`;
27
+ const dbOptions: Record<string, unknown> = {};
28
+
29
+ if (username && password) {
30
+ dbOptions.auth = {
31
+ username,
32
+ password,
33
+ };
34
+ }
35
+
36
+ console.log(chalk.gray(`šŸ“” Connecting to: ${dbUrl}`));
37
+ const lookupDB = new PouchDB(dbUrl, dbOptions);
38
+
39
+ try {
40
+ await lookupDB.info();
41
+ console.log(chalk.green('āœ… Connected to course lookup database'));
42
+ } catch (error: unknown) {
43
+ let errorMessage = 'Unknown error';
44
+ if (error instanceof Error) {
45
+ errorMessage = error.message;
46
+ } else if (typeof error === 'string') {
47
+ errorMessage = error;
48
+ } else if (error && typeof error === 'object' && 'message' in error) {
49
+ errorMessage = String((error as { message: unknown }).message);
50
+ }
51
+ throw new Error(`Failed to connect to course lookup database: ${errorMessage}`);
52
+ }
53
+
54
+ try {
55
+ const result = await lookupDB.allDocs({ include_docs: true });
56
+ const courses: CourseInfo[] = result.rows
57
+ .filter((row) => row.doc && !row.id.startsWith('_'))
58
+ .map((row) => {
59
+ const doc = row.doc as CourseDocument;
60
+ return {
61
+ id: row.id,
62
+ name: doc.name || doc.title || `Course ${row.id}`,
63
+ description: doc.description || undefined,
64
+ };
65
+ });
66
+
67
+ console.log(chalk.green(`āœ… Found ${courses.length} available courses`));
68
+ return courses;
69
+ } catch {
70
+ console.warn(chalk.yellow('āš ļø Could not list courses from lookup database'));
71
+ return [];
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Convert hex color to closest ANSI color code
77
+ */
78
+ function hexToAnsi(hex: string): string {
79
+ // Remove # if present
80
+ hex = hex.replace('#', '');
81
+
82
+ // Convert hex to RGB
83
+ const r = parseInt(hex.substr(0, 2), 16);
84
+ const g = parseInt(hex.substr(2, 2), 16);
85
+ const b = parseInt(hex.substr(4, 2), 16);
86
+
87
+ // Convert to 256-color ANSI
88
+ const ansiCode =
89
+ 16 + 36 * Math.round((r / 255) * 5) + 6 * Math.round((g / 255) * 5) + Math.round((b / 255) * 5);
90
+ return `\x1b[48;5;${ansiCode}m`;
91
+ }
92
+
93
+ /**
94
+ * Create a color swatch for terminal display
95
+ */
96
+ function createColorSwatch(hex: string, label: string): string {
97
+ const colorCode = hexToAnsi(hex);
98
+ const reset = '\x1b[0m';
99
+ return `${colorCode} ${reset} ${label}`;
100
+ }
101
+
102
+ /**
103
+ * Create theme preview with color swatches
104
+ */
105
+ function createThemePreview(themeName: string): string {
106
+ const theme = PREDEFINED_THEMES[themeName];
107
+ const lightColors = theme.light.colors;
108
+
109
+ const primarySwatch = createColorSwatch(lightColors.primary, 'Primary');
110
+ const secondarySwatch = createColorSwatch(lightColors.secondary, 'Secondary');
111
+ const accentSwatch = createColorSwatch(lightColors.accent, 'Accent');
112
+
113
+ return `${primarySwatch} ${secondarySwatch} ${accentSwatch}`;
114
+ }
115
+
116
+ /**
117
+ * Display comprehensive theme preview after selection
118
+ */
119
+ export function displayThemePreview(themeName: string): void {
120
+ const theme = PREDEFINED_THEMES[themeName];
121
+
122
+ console.log(chalk.cyan('\nšŸŽØ Theme Color Palette:'));
123
+ console.log(chalk.white(` ${theme.name.toUpperCase()} THEME`));
124
+
125
+ // Light theme colors
126
+ console.log(chalk.white('\n Light Mode:'));
127
+ const lightColors = theme.light.colors;
128
+ console.log(` ${createColorSwatch(lightColors.primary, `Primary: ${lightColors.primary}`)}`);
129
+ console.log(
130
+ ` ${createColorSwatch(lightColors.secondary, `Secondary: ${lightColors.secondary}`)}`
131
+ );
132
+ console.log(` ${createColorSwatch(lightColors.accent, `Accent: ${lightColors.accent}`)}`);
133
+ console.log(` ${createColorSwatch(lightColors.success, `Success: ${lightColors.success}`)}`);
134
+ console.log(` ${createColorSwatch(lightColors.warning, `Warning: ${lightColors.warning}`)}`);
135
+ console.log(` ${createColorSwatch(lightColors.error, `Error: ${lightColors.error}`)}`);
136
+
137
+ // Dark theme colors
138
+ console.log(chalk.white('\n Dark Mode:'));
139
+ const darkColors = theme.dark.colors;
140
+ console.log(` ${createColorSwatch(darkColors.primary, `Primary: ${darkColors.primary}`)}`);
141
+ console.log(
142
+ ` ${createColorSwatch(darkColors.secondary, `Secondary: ${darkColors.secondary}`)}`
143
+ );
144
+ console.log(` ${createColorSwatch(darkColors.accent, `Accent: ${darkColors.accent}`)}`);
145
+ console.log(` ${createColorSwatch(darkColors.success, `Success: ${darkColors.success}`)}`);
146
+ console.log(` ${createColorSwatch(darkColors.warning, `Warning: ${darkColors.warning}`)}`);
147
+ console.log(` ${createColorSwatch(darkColors.error, `Error: ${darkColors.error}`)}`);
148
+
149
+ console.log(chalk.gray(`\n Default mode: ${theme.defaultMode}`));
150
+ }
4
151
 
5
152
  export async function gatherProjectConfig(
6
153
  projectName: string,
@@ -9,7 +156,7 @@ export async function gatherProjectConfig(
9
156
  console.log(chalk.cyan('\nšŸš€ Creating a new Skuilder course application\n'));
10
157
 
11
158
  let config: Partial<ProjectConfig> = {
12
- projectName
159
+ projectName,
13
160
  };
14
161
 
15
162
  if (options.interactive) {
@@ -19,7 +166,7 @@ export async function gatherProjectConfig(
19
166
  name: 'title',
20
167
  message: 'Course title:',
21
168
  default: formatProjectName(projectName),
22
- validate: (input: string) => input.trim().length > 0 || 'Course title is required'
169
+ validate: (input: string) => input.trim().length > 0 || 'Course title is required',
23
170
  },
24
171
  {
25
172
  type: 'list',
@@ -28,14 +175,14 @@ export async function gatherProjectConfig(
28
175
  choices: [
29
176
  {
30
177
  name: 'Dynamic (Connect to CouchDB server)',
31
- value: 'couch'
178
+ value: 'couch',
32
179
  },
33
180
  {
34
181
  name: 'Static (Self-contained JSON files)',
35
- value: 'static'
36
- }
182
+ value: 'static',
183
+ },
37
184
  ],
38
- default: options.dataLayer === 'dynamic' ? 'couch' : 'static'
185
+ default: options.dataLayer === 'dynamic' ? 'couch' : 'static',
39
186
  },
40
187
  {
41
188
  type: 'input',
@@ -51,13 +198,48 @@ export async function gatherProjectConfig(
51
198
  } catch {
52
199
  return 'Please enter a valid URL';
53
200
  }
54
- }
201
+ },
55
202
  },
56
203
  {
57
204
  type: 'input',
58
205
  name: 'courseId',
59
206
  message: 'Course ID to import (optional):',
60
- when: (answers) => answers.dataLayerType === 'couch'
207
+ when: (answers) => answers.dataLayerType === 'couch',
208
+ },
209
+ {
210
+ type: 'confirm',
211
+ name: 'importCourseData',
212
+ message: 'Would you like to import course data from a CouchDB server?',
213
+ default: false,
214
+ when: (answers) => answers.dataLayerType === 'static',
215
+ },
216
+ {
217
+ type: 'input',
218
+ name: 'importServerUrl',
219
+ message: 'CouchDB server URL:',
220
+ default: 'http://localhost:5984',
221
+ when: (answers) => answers.dataLayerType === 'static' && answers.importCourseData,
222
+ validate: (input: string) => {
223
+ if (!input.trim()) return 'CouchDB URL is required';
224
+ try {
225
+ new URL(input);
226
+ return true;
227
+ } catch {
228
+ return 'Please enter a valid URL';
229
+ }
230
+ },
231
+ },
232
+ {
233
+ type: 'input',
234
+ name: 'importUsername',
235
+ message: 'Username:',
236
+ when: (answers) => answers.dataLayerType === 'static' && answers.importCourseData,
237
+ },
238
+ {
239
+ type: 'password',
240
+ name: 'importPassword',
241
+ message: 'Password:',
242
+ when: (answers) => answers.dataLayerType === 'static' && answers.importCourseData,
61
243
  },
62
244
  {
63
245
  type: 'list',
@@ -65,24 +247,24 @@ export async function gatherProjectConfig(
65
247
  message: 'Select theme:',
66
248
  choices: [
67
249
  {
68
- name: 'Default (Material Blue)',
69
- value: 'default'
250
+ name: `Default (Material Blue) ${createThemePreview('default')}`,
251
+ value: 'default',
70
252
  },
71
253
  {
72
- name: 'Medical (Healthcare Green)',
73
- value: 'medical'
254
+ name: `Medical (Healthcare Green) ${createThemePreview('medical')}`,
255
+ value: 'medical',
74
256
  },
75
257
  {
76
- name: 'Educational (Academic Orange)',
77
- value: 'educational'
258
+ name: `Educational (Academic Orange) ${createThemePreview('educational')}`,
259
+ value: 'educational',
78
260
  },
79
261
  {
80
- name: 'Corporate (Professional Gray)',
81
- value: 'corporate'
82
- }
262
+ name: `Corporate (Professional Gray) ${createThemePreview('corporate')}`,
263
+ value: 'corporate',
264
+ },
83
265
  ],
84
- default: options.theme
85
- }
266
+ default: options.theme,
267
+ },
86
268
  ]);
87
269
 
88
270
  config = {
@@ -91,8 +273,113 @@ export async function gatherProjectConfig(
91
273
  dataLayerType: answers.dataLayerType,
92
274
  couchdbUrl: answers.couchdbUrl,
93
275
  course: answers.courseId,
94
- theme: PREDEFINED_THEMES[answers.themeName]
276
+ theme: PREDEFINED_THEMES[answers.themeName],
277
+ importCourseData: answers.importCourseData,
278
+ importServerUrl: answers.importServerUrl,
279
+ importUsername: answers.importUsername,
280
+ importPassword: answers.importPassword,
95
281
  };
282
+
283
+ // If user wants to import course data, fetch available courses and let them select
284
+ if (answers.importCourseData && answers.importServerUrl) {
285
+ try {
286
+ console.log(chalk.cyan('\nšŸ“š Fetching available courses...'));
287
+ const availableCourses = await fetchAvailableCourses(
288
+ answers.importServerUrl,
289
+ answers.importUsername,
290
+ answers.importPassword
291
+ );
292
+
293
+ if (availableCourses.length > 0) {
294
+ const courseSelectionAnswers = await inquirer.prompt([
295
+ {
296
+ type: 'checkbox',
297
+ name: 'selectedCourseIds',
298
+ message: 'Select courses to import:',
299
+ choices: availableCourses.map((course) => ({
300
+ name: `${course.name} (${course.id})`,
301
+ value: course.id,
302
+ short: course.name,
303
+ })),
304
+ validate: (selected: string[]) => {
305
+ if (selected.length === 0) {
306
+ return 'Please select at least one course to import';
307
+ }
308
+ return true;
309
+ },
310
+ },
311
+ ]);
312
+
313
+ config.importCourseIds = courseSelectionAnswers.selectedCourseIds;
314
+ } else {
315
+ console.log(chalk.yellow('āš ļø No courses found in the lookup database.'));
316
+ const manualCourseAnswers = await inquirer.prompt([
317
+ {
318
+ type: 'input',
319
+ name: 'manualCourseIds',
320
+ message: 'Enter course IDs to import (comma-separated):',
321
+ validate: (input: string) => {
322
+ if (!input.trim()) {
323
+ return 'Please enter at least one course ID';
324
+ }
325
+ return true;
326
+ },
327
+ },
328
+ ]);
329
+
330
+ config.importCourseIds = manualCourseAnswers.manualCourseIds
331
+ .split(',')
332
+ .map((id: string) => id.trim())
333
+ .filter((id: string) => id.length > 0);
334
+ }
335
+ } catch (error: unknown) {
336
+ console.error(chalk.red('āŒ Failed to fetch courses:'));
337
+ let errorMessage = 'Unknown error';
338
+ if (error instanceof Error) {
339
+ errorMessage = error.message;
340
+ } else if (typeof error === 'string') {
341
+ errorMessage = error;
342
+ } else if (error && typeof error === 'object' && 'message' in error) {
343
+ errorMessage = String((error as { message: unknown }).message);
344
+ }
345
+ console.error(chalk.red(errorMessage));
346
+
347
+ const fallbackAnswers = await inquirer.prompt([
348
+ {
349
+ type: 'confirm',
350
+ name: 'continueAnyway',
351
+ message: 'Continue with manual course ID entry?',
352
+ default: true,
353
+ },
354
+ ]);
355
+
356
+ if (fallbackAnswers.continueAnyway) {
357
+ const manualCourseAnswers = await inquirer.prompt([
358
+ {
359
+ type: 'input',
360
+ name: 'manualCourseIds',
361
+ message: 'Enter course IDs to import (comma-separated):',
362
+ validate: (input: string) => {
363
+ if (!input.trim()) {
364
+ return 'Please enter at least one course ID';
365
+ }
366
+ return true;
367
+ },
368
+ },
369
+ ]);
370
+
371
+ config.importCourseIds = manualCourseAnswers.manualCourseIds
372
+ .split(',')
373
+ .map((id: string) => id.trim())
374
+ .filter((id: string) => id.length > 0);
375
+ } else {
376
+ config.importCourseData = false;
377
+ }
378
+ }
379
+ }
380
+
381
+ // Show comprehensive theme preview
382
+ displayThemePreview(answers.themeName);
96
383
  } else {
97
384
  // Non-interactive mode: use provided options
98
385
  config = {
@@ -101,12 +388,14 @@ export async function gatherProjectConfig(
101
388
  dataLayerType: options.dataLayer === 'dynamic' ? 'couch' : 'static',
102
389
  couchdbUrl: options.couchdbUrl,
103
390
  course: options.courseId,
104
- theme: PREDEFINED_THEMES[options.theme]
391
+ theme: PREDEFINED_THEMES[options.theme],
105
392
  };
106
393
 
107
394
  // Validate required fields for non-interactive mode
108
395
  if (config.dataLayerType === 'couch' && !config.couchdbUrl) {
109
- throw new Error('CouchDB URL is required when using dynamic data layer. Use --couchdb-url option.');
396
+ throw new Error(
397
+ 'CouchDB URL is required when using dynamic data layer. Use --couchdb-url option.'
398
+ );
110
399
  }
111
400
  }
112
401
 
@@ -121,16 +410,18 @@ export async function confirmProjectCreation(
121
410
  console.log(` Project Name: ${chalk.white(config.projectName)}`);
122
411
  console.log(` Course Title: ${chalk.white(config.title)}`);
123
412
  console.log(` Data Layer: ${chalk.white(config.dataLayerType)}`);
124
-
413
+
125
414
  if (config.couchdbUrl) {
126
415
  console.log(` CouchDB URL: ${chalk.white(config.couchdbUrl)}`);
127
416
  }
128
-
417
+
129
418
  if (config.course) {
130
419
  console.log(` Course ID: ${chalk.white(config.course)}`);
131
420
  }
132
-
133
- console.log(` Theme: ${chalk.white(config.theme.name)}`);
421
+
422
+ console.log(
423
+ ` Theme: ${chalk.white(config.theme.name)} ${createThemePreview(config.theme.name)}`
424
+ );
134
425
  console.log(` Directory: ${chalk.white(projectPath)}`);
135
426
 
136
427
  const { confirmed } = await inquirer.prompt([
@@ -138,67 +429,16 @@ export async function confirmProjectCreation(
138
429
  type: 'confirm',
139
430
  name: 'confirmed',
140
431
  message: 'Create project with these settings?',
141
- default: true
142
- }
143
- ]);
144
-
145
- return confirmed;
146
- }
147
-
148
- export async function promptForCustomTheme(): Promise<ThemeConfig> {
149
- console.log(chalk.cyan('\nšŸŽØ Custom Theme Configuration\n'));
150
-
151
- const answers = await inquirer.prompt([
152
- {
153
- type: 'input',
154
- name: 'name',
155
- message: 'Theme name:',
156
- validate: (input: string) => input.trim().length > 0 || 'Theme name is required'
157
- },
158
- {
159
- type: 'input',
160
- name: 'primary',
161
- message: 'Primary color (hex):',
162
- default: '#1976D2',
163
- validate: validateHexColor
432
+ default: true,
164
433
  },
165
- {
166
- type: 'input',
167
- name: 'secondary',
168
- message: 'Secondary color (hex):',
169
- default: '#424242',
170
- validate: validateHexColor
171
- },
172
- {
173
- type: 'input',
174
- name: 'accent',
175
- message: 'Accent color (hex):',
176
- default: '#82B1FF',
177
- validate: validateHexColor
178
- }
179
434
  ]);
180
435
 
181
- return {
182
- name: answers.name,
183
- colors: {
184
- primary: answers.primary,
185
- secondary: answers.secondary,
186
- accent: answers.accent
187
- }
188
- };
436
+ return confirmed;
189
437
  }
190
438
 
191
439
  function formatProjectName(projectName: string): string {
192
440
  return projectName
193
441
  .split(/[-_\s]+/)
194
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
442
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
195
443
  .join(' ');
196
444
  }
197
-
198
- function validateHexColor(input: string): boolean | string {
199
- const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
200
- if (!hexColorRegex.test(input)) {
201
- return 'Please enter a valid hex color (e.g., #1976D2)';
202
- }
203
- return true;
204
- }