@vue-skuilder/cli 0.1.5 → 0.1.7

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 (73) hide show
  1. package/CLAUDE.md +84 -0
  2. package/dist/cli.js +8 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +46 -1
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/commands/pack.js +17 -0
  8. package/dist/commands/pack.js.map +1 -1
  9. package/dist/commands/studio.d.ts +3 -0
  10. package/dist/commands/studio.d.ts.map +1 -0
  11. package/dist/commands/studio.js +396 -0
  12. package/dist/commands/studio.js.map +1 -0
  13. package/dist/commands/unpack.d.ts +3 -0
  14. package/dist/commands/unpack.d.ts.map +1 -0
  15. package/dist/commands/unpack.js +228 -0
  16. package/dist/commands/unpack.js.map +1 -0
  17. package/dist/studio-ui-assets/assets/BrowseView-BJbixGOU.js +2 -0
  18. package/dist/studio-ui-assets/assets/BrowseView-BJbixGOU.js.map +1 -0
  19. package/dist/studio-ui-assets/assets/BrowseView-CM4HBO4j.css +1 -0
  20. package/dist/studio-ui-assets/assets/BulkImportView-DB6DYDJU.js +2 -0
  21. package/dist/studio-ui-assets/assets/BulkImportView-DB6DYDJU.js.map +1 -0
  22. package/dist/studio-ui-assets/assets/BulkImportView-g4wQUfPA.css +1 -0
  23. package/dist/studio-ui-assets/assets/CourseEditorView-BIlhlhw1.js +2 -0
  24. package/dist/studio-ui-assets/assets/CourseEditorView-BIlhlhw1.js.map +1 -0
  25. package/dist/studio-ui-assets/assets/CourseEditorView-WuPNLVKp.css +1 -0
  26. package/dist/studio-ui-assets/assets/CreateCardView-CyNOKCkm.css +1 -0
  27. package/dist/studio-ui-assets/assets/CreateCardView-DPjPvzzt.js +2 -0
  28. package/dist/studio-ui-assets/assets/CreateCardView-DPjPvzzt.js.map +1 -0
  29. package/dist/studio-ui-assets/assets/edit-ui.es-DiUxqbgF.js +330 -0
  30. package/dist/studio-ui-assets/assets/edit-ui.es-DiUxqbgF.js.map +1 -0
  31. package/dist/studio-ui-assets/assets/index--zY88pg6.css +14 -0
  32. package/dist/studio-ui-assets/assets/index-BnAv1C72.js +287 -0
  33. package/dist/studio-ui-assets/assets/index-BnAv1C72.js.map +1 -0
  34. package/dist/studio-ui-assets/assets/index-DHMXQY3-.js +192 -0
  35. package/dist/studio-ui-assets/assets/index-DHMXQY3-.js.map +1 -0
  36. package/dist/studio-ui-assets/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
  37. package/dist/studio-ui-assets/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
  38. package/dist/studio-ui-assets/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
  39. package/dist/studio-ui-assets/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
  40. package/dist/studio-ui-assets/assets/vue-DZcMATiC.js +28 -0
  41. package/dist/studio-ui-assets/assets/vue-DZcMATiC.js.map +1 -0
  42. package/dist/studio-ui-assets/assets/vuetify-qg7mRxy_.js +6 -0
  43. package/dist/studio-ui-assets/assets/vuetify-qg7mRxy_.js.map +1 -0
  44. package/dist/studio-ui-assets/index.html +16 -0
  45. package/dist/types.d.ts +5 -0
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/types.js.map +1 -1
  48. package/dist/utils/NodeFileSystemAdapter.d.ts +14 -0
  49. package/dist/utils/NodeFileSystemAdapter.d.ts.map +1 -0
  50. package/dist/utils/NodeFileSystemAdapter.js +55 -0
  51. package/dist/utils/NodeFileSystemAdapter.js.map +1 -0
  52. package/dist/utils/pack-courses.d.ts +13 -0
  53. package/dist/utils/pack-courses.d.ts.map +1 -0
  54. package/dist/utils/pack-courses.js +54 -0
  55. package/dist/utils/pack-courses.js.map +1 -0
  56. package/dist/utils/prompts.d.ts.map +1 -1
  57. package/dist/utils/prompts.js +206 -21
  58. package/dist/utils/prompts.js.map +1 -1
  59. package/dist/utils/template.d.ts +5 -1
  60. package/dist/utils/template.d.ts.map +1 -1
  61. package/dist/utils/template.js +71 -7
  62. package/dist/utils/template.js.map +1 -1
  63. package/package.json +7 -4
  64. package/src/cli.ts +8 -0
  65. package/src/commands/init.ts +62 -6
  66. package/src/commands/pack.ts +24 -1
  67. package/src/commands/studio.ts +497 -0
  68. package/src/commands/unpack.ts +259 -0
  69. package/src/types.ts +6 -0
  70. package/src/utils/NodeFileSystemAdapter.ts +72 -0
  71. package/src/utils/pack-courses.ts +77 -0
  72. package/src/utils/prompts.ts +252 -39
  73. package/src/utils/template.ts +83 -8
@@ -1,21 +1,92 @@
1
1
  import inquirer from 'inquirer';
2
2
  import chalk from 'chalk';
3
+ import PouchDB from 'pouchdb';
3
4
  import { CliOptions, ProjectConfig, PREDEFINED_THEMES } from '../types.js';
4
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
+
5
75
  /**
6
76
  * Convert hex color to closest ANSI color code
7
77
  */
8
78
  function hexToAnsi(hex: string): string {
9
79
  // Remove # if present
10
80
  hex = hex.replace('#', '');
11
-
81
+
12
82
  // Convert hex to RGB
13
83
  const r = parseInt(hex.substr(0, 2), 16);
14
84
  const g = parseInt(hex.substr(2, 2), 16);
15
85
  const b = parseInt(hex.substr(4, 2), 16);
16
-
86
+
17
87
  // Convert to 256-color ANSI
18
- const ansiCode = 16 + (36 * Math.round(r / 255 * 5)) + (6 * Math.round(g / 255 * 5)) + Math.round(b / 255 * 5);
88
+ const ansiCode =
89
+ 16 + 36 * Math.round((r / 255) * 5) + 6 * Math.round((g / 255) * 5) + Math.round((b / 255) * 5);
19
90
  return `\x1b[48;5;${ansiCode}m`;
20
91
  }
21
92
 
@@ -34,11 +105,11 @@ function createColorSwatch(hex: string, label: string): string {
34
105
  function createThemePreview(themeName: string): string {
35
106
  const theme = PREDEFINED_THEMES[themeName];
36
107
  const lightColors = theme.light.colors;
37
-
108
+
38
109
  const primarySwatch = createColorSwatch(lightColors.primary, 'Primary');
39
110
  const secondarySwatch = createColorSwatch(lightColors.secondary, 'Secondary');
40
111
  const accentSwatch = createColorSwatch(lightColors.accent, 'Accent');
41
-
112
+
42
113
  return `${primarySwatch} ${secondarySwatch} ${accentSwatch}`;
43
114
  }
44
115
 
@@ -47,30 +118,34 @@ function createThemePreview(themeName: string): string {
47
118
  */
48
119
  export function displayThemePreview(themeName: string): void {
49
120
  const theme = PREDEFINED_THEMES[themeName];
50
-
121
+
51
122
  console.log(chalk.cyan('\n🎨 Theme Color Palette:'));
52
123
  console.log(chalk.white(` ${theme.name.toUpperCase()} THEME`));
53
-
124
+
54
125
  // Light theme colors
55
126
  console.log(chalk.white('\n Light Mode:'));
56
127
  const lightColors = theme.light.colors;
57
128
  console.log(` ${createColorSwatch(lightColors.primary, `Primary: ${lightColors.primary}`)}`);
58
- console.log(` ${createColorSwatch(lightColors.secondary, `Secondary: ${lightColors.secondary}`)}`);
129
+ console.log(
130
+ ` ${createColorSwatch(lightColors.secondary, `Secondary: ${lightColors.secondary}`)}`
131
+ );
59
132
  console.log(` ${createColorSwatch(lightColors.accent, `Accent: ${lightColors.accent}`)}`);
60
133
  console.log(` ${createColorSwatch(lightColors.success, `Success: ${lightColors.success}`)}`);
61
134
  console.log(` ${createColorSwatch(lightColors.warning, `Warning: ${lightColors.warning}`)}`);
62
135
  console.log(` ${createColorSwatch(lightColors.error, `Error: ${lightColors.error}`)}`);
63
-
136
+
64
137
  // Dark theme colors
65
138
  console.log(chalk.white('\n Dark Mode:'));
66
139
  const darkColors = theme.dark.colors;
67
140
  console.log(` ${createColorSwatch(darkColors.primary, `Primary: ${darkColors.primary}`)}`);
68
- console.log(` ${createColorSwatch(darkColors.secondary, `Secondary: ${darkColors.secondary}`)}`);
141
+ console.log(
142
+ ` ${createColorSwatch(darkColors.secondary, `Secondary: ${darkColors.secondary}`)}`
143
+ );
69
144
  console.log(` ${createColorSwatch(darkColors.accent, `Accent: ${darkColors.accent}`)}`);
70
145
  console.log(` ${createColorSwatch(darkColors.success, `Success: ${darkColors.success}`)}`);
71
146
  console.log(` ${createColorSwatch(darkColors.warning, `Warning: ${darkColors.warning}`)}`);
72
147
  console.log(` ${createColorSwatch(darkColors.error, `Error: ${darkColors.error}`)}`);
73
-
148
+
74
149
  console.log(chalk.gray(`\n Default mode: ${theme.defaultMode}`));
75
150
  }
76
151
 
@@ -81,7 +156,7 @@ export async function gatherProjectConfig(
81
156
  console.log(chalk.cyan('\n🚀 Creating a new Skuilder course application\n'));
82
157
 
83
158
  let config: Partial<ProjectConfig> = {
84
- projectName
159
+ projectName,
85
160
  };
86
161
 
87
162
  if (options.interactive) {
@@ -91,7 +166,7 @@ export async function gatherProjectConfig(
91
166
  name: 'title',
92
167
  message: 'Course title:',
93
168
  default: formatProjectName(projectName),
94
- validate: (input: string) => input.trim().length > 0 || 'Course title is required'
169
+ validate: (input: string) => input.trim().length > 0 || 'Course title is required',
95
170
  },
96
171
  {
97
172
  type: 'list',
@@ -100,14 +175,14 @@ export async function gatherProjectConfig(
100
175
  choices: [
101
176
  {
102
177
  name: 'Dynamic (Connect to CouchDB server)',
103
- value: 'couch'
178
+ value: 'couch',
104
179
  },
105
180
  {
106
181
  name: 'Static (Self-contained JSON files)',
107
- value: 'static'
108
- }
182
+ value: 'static',
183
+ },
109
184
  ],
110
- default: options.dataLayer === 'dynamic' ? 'couch' : 'static'
185
+ default: options.dataLayer === 'dynamic' ? 'couch' : 'static',
111
186
  },
112
187
  {
113
188
  type: 'input',
@@ -123,13 +198,48 @@ export async function gatherProjectConfig(
123
198
  } catch {
124
199
  return 'Please enter a valid URL';
125
200
  }
126
- }
201
+ },
127
202
  },
128
203
  {
129
204
  type: 'input',
130
205
  name: 'courseId',
131
206
  message: 'Course ID to import (optional):',
132
- 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,
133
243
  },
134
244
  {
135
245
  type: 'list',
@@ -138,23 +248,23 @@ export async function gatherProjectConfig(
138
248
  choices: [
139
249
  {
140
250
  name: `Default (Material Blue) ${createThemePreview('default')}`,
141
- value: 'default'
251
+ value: 'default',
142
252
  },
143
253
  {
144
254
  name: `Medical (Healthcare Green) ${createThemePreview('medical')}`,
145
- value: 'medical'
255
+ value: 'medical',
146
256
  },
147
257
  {
148
258
  name: `Educational (Academic Orange) ${createThemePreview('educational')}`,
149
- value: 'educational'
259
+ value: 'educational',
150
260
  },
151
261
  {
152
262
  name: `Corporate (Professional Gray) ${createThemePreview('corporate')}`,
153
- value: 'corporate'
154
- }
263
+ value: 'corporate',
264
+ },
155
265
  ],
156
- default: options.theme
157
- }
266
+ default: options.theme,
267
+ },
158
268
  ]);
159
269
 
160
270
  config = {
@@ -163,9 +273,111 @@ export async function gatherProjectConfig(
163
273
  dataLayerType: answers.dataLayerType,
164
274
  couchdbUrl: answers.couchdbUrl,
165
275
  course: answers.courseId,
166
- 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,
167
281
  };
168
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
+
169
381
  // Show comprehensive theme preview
170
382
  displayThemePreview(answers.themeName);
171
383
  } else {
@@ -176,12 +388,14 @@ export async function gatherProjectConfig(
176
388
  dataLayerType: options.dataLayer === 'dynamic' ? 'couch' : 'static',
177
389
  couchdbUrl: options.couchdbUrl,
178
390
  course: options.courseId,
179
- theme: PREDEFINED_THEMES[options.theme]
391
+ theme: PREDEFINED_THEMES[options.theme],
180
392
  };
181
393
 
182
394
  // Validate required fields for non-interactive mode
183
395
  if (config.dataLayerType === 'couch' && !config.couchdbUrl) {
184
- 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
+ );
185
399
  }
186
400
  }
187
401
 
@@ -196,16 +410,18 @@ export async function confirmProjectCreation(
196
410
  console.log(` Project Name: ${chalk.white(config.projectName)}`);
197
411
  console.log(` Course Title: ${chalk.white(config.title)}`);
198
412
  console.log(` Data Layer: ${chalk.white(config.dataLayerType)}`);
199
-
413
+
200
414
  if (config.couchdbUrl) {
201
415
  console.log(` CouchDB URL: ${chalk.white(config.couchdbUrl)}`);
202
416
  }
203
-
417
+
204
418
  if (config.course) {
205
419
  console.log(` Course ID: ${chalk.white(config.course)}`);
206
420
  }
207
-
208
- console.log(` Theme: ${chalk.white(config.theme.name)} ${createThemePreview(config.theme.name)}`);
421
+
422
+ console.log(
423
+ ` Theme: ${chalk.white(config.theme.name)} ${createThemePreview(config.theme.name)}`
424
+ );
209
425
  console.log(` Directory: ${chalk.white(projectPath)}`);
210
426
 
211
427
  const { confirmed } = await inquirer.prompt([
@@ -213,19 +429,16 @@ export async function confirmProjectCreation(
213
429
  type: 'confirm',
214
430
  name: 'confirmed',
215
431
  message: 'Create project with these settings?',
216
- default: true
217
- }
432
+ default: true,
433
+ },
218
434
  ]);
219
435
 
220
436
  return confirmed;
221
437
  }
222
438
 
223
-
224
-
225
439
  function formatProjectName(projectName: string): string {
226
440
  return projectName
227
441
  .split(/[-_\s]+/)
228
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
442
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
229
443
  .join(' ');
230
444
  }
231
-
@@ -62,7 +62,8 @@ export async function copyDirectory(
62
62
  export async function transformPackageJson(
63
63
  packageJsonPath: string,
64
64
  projectName: string,
65
- cliVersion: string
65
+ cliVersion: string,
66
+ config: ProjectConfig
66
67
  ): Promise<void> {
67
68
  const content = await fs.readFile(packageJsonPath, 'utf-8');
68
69
  const packageJson = JSON.parse(content);
@@ -87,6 +88,20 @@ export async function transformPackageJson(
87
88
  packageJson.devDependencies['terser'] = '^5.39.0';
88
89
  }
89
90
 
91
+ // Add CLI as devDependency for all projects
92
+ if (!packageJson.devDependencies) {
93
+ packageJson.devDependencies = {};
94
+ }
95
+ packageJson.devDependencies['@vue-skuilder/cli'] = `^${cliVersion}`;
96
+
97
+ // Add studio script for static data layer projects
98
+ if (config.dataLayerType === 'static') {
99
+ if (!packageJson.scripts) {
100
+ packageJson.scripts = {};
101
+ }
102
+ packageJson.scripts['studio'] = 'skuilder studio';
103
+ }
104
+
90
105
  // Remove CLI-specific fields that don't belong in generated projects
91
106
  delete packageJson.publishConfig;
92
107
 
@@ -174,10 +189,20 @@ export async function generateSkuilderConfig(
174
189
  dataLayerType: config.dataLayerType,
175
190
  };
176
191
 
177
- if (config.course) {
192
+ // For dynamic data layer, use the specified course ID
193
+ if (config.dataLayerType === 'couch' && config.course) {
178
194
  skuilderConfig.course = config.course;
179
195
  }
180
196
 
197
+ // For static data layer with imported courses, use the first course as primary
198
+ if (
199
+ config.dataLayerType === 'static' &&
200
+ config.importCourseIds &&
201
+ config.importCourseIds.length > 0
202
+ ) {
203
+ skuilderConfig.course = config.importCourseIds[0];
204
+ }
205
+
181
206
  if (config.couchdbUrl) {
182
207
  skuilderConfig.couchdbUrl = config.couchdbUrl;
183
208
  }
@@ -189,6 +214,42 @@ export async function generateSkuilderConfig(
189
214
  await fs.writeFile(configPath, JSON.stringify(skuilderConfig, null, 2));
190
215
  }
191
216
 
217
+ /**
218
+ * Transform tsconfig.json to be standalone (remove base config reference)
219
+ */
220
+ export async function transformTsConfig(tsconfigPath: string): Promise<void> {
221
+ const content = await fs.readFile(tsconfigPath, 'utf-8');
222
+ const tsconfig = JSON.parse(content);
223
+
224
+ // Remove the extends reference to the monorepo base config
225
+ delete tsconfig.extends;
226
+
227
+ // Merge in the essential settings from the base config that scaffolded apps need
228
+ tsconfig.compilerOptions = {
229
+ ...tsconfig.compilerOptions,
230
+ // Essential TypeScript settings from base config
231
+ strict: true,
232
+ skipLibCheck: true,
233
+ forceConsistentCasingInFileNames: true,
234
+ esModuleInterop: true,
235
+ allowSyntheticDefaultImports: true,
236
+ // Keep existing Vue/Vite-specific settings
237
+ target: tsconfig.compilerOptions.target || 'ESNext',
238
+ useDefineForClassFields: tsconfig.compilerOptions.useDefineForClassFields,
239
+ module: tsconfig.compilerOptions.module || 'ESNext',
240
+ moduleResolution: tsconfig.compilerOptions.moduleResolution || 'bundler',
241
+ jsx: tsconfig.compilerOptions.jsx || 'preserve',
242
+ resolveJsonModule: tsconfig.compilerOptions.resolveJsonModule,
243
+ isolatedModules: tsconfig.compilerOptions.isolatedModules,
244
+ lib: tsconfig.compilerOptions.lib || ['ESNext', 'DOM'],
245
+ noEmit: tsconfig.compilerOptions.noEmit,
246
+ baseUrl: tsconfig.compilerOptions.baseUrl || '.',
247
+ types: tsconfig.compilerOptions.types || ['vite/client'],
248
+ };
249
+
250
+ await fs.writeFile(tsconfigPath, JSON.stringify(tsconfig, null, 2));
251
+ }
252
+
192
253
  /**
193
254
  * Generate .gitignore file for the project
194
255
  */
@@ -316,10 +377,18 @@ Thumbs.db
316
377
  * Generate project README.md
317
378
  */
318
379
  export async function generateReadme(readmePath: string, config: ProjectConfig): Promise<void> {
319
- const dataLayerInfo =
320
- config.dataLayerType === 'static'
321
- ? 'This project uses a static data layer with JSON files.'
322
- : `This project connects to CouchDB at: ${config.couchdbUrl || '[URL not specified]'}`;
380
+ let dataLayerInfo = '';
381
+
382
+ if (config.dataLayerType === 'static') {
383
+ dataLayerInfo = 'This project uses a static data layer with JSON files.';
384
+
385
+ if (config.importCourseIds && config.importCourseIds.length > 0) {
386
+ const courseList = config.importCourseIds.map((id) => `- ${id}`).join('\n');
387
+ dataLayerInfo += `\n\n**Imported Courses:**\n${courseList}\n\nCourse data is stored in \`public/static-courses/\` and loaded automatically.`;
388
+ }
389
+ } else {
390
+ dataLayerInfo = `This project connects to CouchDB at: ${config.couchdbUrl || '[URL not specified]'}`;
391
+ }
323
392
 
324
393
  const readme = `# ${config.title}
325
394
 
@@ -358,7 +427,7 @@ Course configuration is managed in \`skuilder.config.json\`. You can modify:
358
427
 
359
428
  Current theme: **${config.theme.name}** (${config.theme.defaultMode} mode)
360
429
  - Primary: ${config.theme.light.colors.primary}
361
- - Secondary: ${config.theme.light.colors.secondary}
430
+ - Secondary: ${config.theme.light.colors.secondary}
362
431
  - Accent: ${config.theme.light.colors.accent}
363
432
 
364
433
  This theme includes both light and dark variants. The application will use the ${config.theme.defaultMode} theme by default, but users can toggle between light and dark modes in their settings.
@@ -429,7 +498,7 @@ export async function processTemplate(
429
498
 
430
499
  console.log(chalk.blue('⚙️ Configuring package.json...'));
431
500
  const packageJsonPath = path.join(projectPath, 'package.json');
432
- await transformPackageJson(packageJsonPath, config.projectName, cliVersion);
501
+ await transformPackageJson(packageJsonPath, config.projectName, cliVersion, config);
433
502
 
434
503
  console.log(chalk.blue('🔧 Creating vite.config.ts...'));
435
504
  const viteConfigPath = path.join(projectPath, 'vite.config.ts');
@@ -437,6 +506,12 @@ export async function processTemplate(
437
506
  await createViteConfig(viteConfigPath);
438
507
  }
439
508
 
509
+ console.log(chalk.blue('🔧 Transforming tsconfig.json...'));
510
+ const tsconfigPath = path.join(projectPath, 'tsconfig.json');
511
+ if (existsSync(tsconfigPath)) {
512
+ await transformTsConfig(tsconfigPath);
513
+ }
514
+
440
515
  console.log(chalk.blue('🔧 Generating configuration...'));
441
516
  const configPath = path.join(projectPath, 'skuilder.config.json');
442
517
  await generateSkuilderConfig(configPath, config);