@vue-skuilder/cli 0.1.5 ā 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.
- package/CLAUDE.md +84 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +46 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pack.js +17 -0
- package/dist/commands/pack.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/pack-courses.d.ts +13 -0
- package/dist/utils/pack-courses.d.ts.map +1 -0
- package/dist/utils/pack-courses.js +54 -0
- package/dist/utils/pack-courses.js.map +1 -0
- package/dist/utils/prompts.d.ts.map +1 -1
- package/dist/utils/prompts.js +206 -21
- package/dist/utils/prompts.js.map +1 -1
- package/dist/utils/template.d.ts +4 -0
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +54 -4
- package/dist/utils/template.js.map +1 -1
- package/package.json +3 -3
- package/src/commands/init.ts +62 -6
- package/src/commands/pack.ts +24 -1
- package/src/types.ts +6 -0
- package/src/utils/pack-courses.ts +77 -0
- package/src/utils/prompts.ts +252 -39
- package/src/utils/template.ts +61 -5
package/src/commands/pack.ts
CHANGED
|
@@ -3,7 +3,7 @@ import PouchDB from 'pouchdb';
|
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import { CouchDBToStaticPacker } from '@vue-skuilder/db/packer';
|
|
6
|
+
import { CouchDBToStaticPacker, AttachmentData } from '@vue-skuilder/db/packer';
|
|
7
7
|
|
|
8
8
|
export function createPackCommand(): Command {
|
|
9
9
|
return new Command('pack')
|
|
@@ -94,6 +94,9 @@ async function packCourse(courseId: string, options: PackOptions) {
|
|
|
94
94
|
console.log(chalk.white(`š Documents: ${packedData.manifest.documentCount}`));
|
|
95
95
|
console.log(chalk.white(`šļø Chunks: ${packedData.manifest.chunks.length}`));
|
|
96
96
|
console.log(chalk.white(`šļø Indices: ${packedData.manifest.indices.length}`));
|
|
97
|
+
if (packedData.attachments && packedData.attachments.size > 0) {
|
|
98
|
+
console.log(chalk.white(`š Attachments: ${packedData.attachments.size}`));
|
|
99
|
+
}
|
|
97
100
|
console.log(chalk.white(`š Location: ${outputDir}`));
|
|
98
101
|
|
|
99
102
|
} catch (error: unknown) {
|
|
@@ -124,6 +127,7 @@ interface PackedData {
|
|
|
124
127
|
};
|
|
125
128
|
chunks: Map<string, unknown[]>;
|
|
126
129
|
indices: Map<string, unknown>;
|
|
130
|
+
attachments?: Map<string, AttachmentData>;
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
async function writePackedData(
|
|
@@ -160,4 +164,23 @@ async function writePackedData(
|
|
|
160
164
|
indexCount++;
|
|
161
165
|
}
|
|
162
166
|
console.log(chalk.gray(`šļø Wrote ${indexCount} indices`));
|
|
167
|
+
|
|
168
|
+
// Write attachments
|
|
169
|
+
if (packedData.attachments && packedData.attachments.size > 0) {
|
|
170
|
+
console.log(chalk.cyan('š Writing attachments...'));
|
|
171
|
+
|
|
172
|
+
let attachmentCount = 0;
|
|
173
|
+
for (const [attachmentPath, attachmentData] of packedData.attachments) {
|
|
174
|
+
const fullAttachmentPath = path.join(outputDir, attachmentPath);
|
|
175
|
+
|
|
176
|
+
// Ensure directory exists
|
|
177
|
+
await fs.ensureDir(path.dirname(fullAttachmentPath));
|
|
178
|
+
|
|
179
|
+
// Write binary file
|
|
180
|
+
await fs.writeFile(fullAttachmentPath, attachmentData.buffer);
|
|
181
|
+
attachmentCount++;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(chalk.gray(`š Wrote ${attachmentCount} attachment files`));
|
|
185
|
+
}
|
|
163
186
|
}
|
package/src/types.ts
CHANGED
|
@@ -13,6 +13,12 @@ export interface ProjectConfig {
|
|
|
13
13
|
course?: string;
|
|
14
14
|
couchdbUrl?: string;
|
|
15
15
|
theme: ThemeConfig;
|
|
16
|
+
// Course import configuration for static data layer
|
|
17
|
+
importCourseData?: boolean;
|
|
18
|
+
importServerUrl?: string;
|
|
19
|
+
importUsername?: string;
|
|
20
|
+
importPassword?: string;
|
|
21
|
+
importCourseIds?: string[];
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export interface VuetifyThemeDefinition {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
export interface PackCoursesOptions {
|
|
9
|
+
server: string;
|
|
10
|
+
username?: string;
|
|
11
|
+
password?: string;
|
|
12
|
+
courseIds: string[];
|
|
13
|
+
targetProjectDir: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pack courses using the existing CLI pack command
|
|
18
|
+
* Outputs to targetProjectDir/public/static-courses/
|
|
19
|
+
*/
|
|
20
|
+
export async function packCourses(options: PackCoursesOptions): Promise<void> {
|
|
21
|
+
const { server, username, password, courseIds, targetProjectDir } = options;
|
|
22
|
+
|
|
23
|
+
// Output directory will be public/static-courses in the target project
|
|
24
|
+
const outputDir = path.join(targetProjectDir, 'public', 'static-courses');
|
|
25
|
+
|
|
26
|
+
console.log(chalk.cyan(`\nš¦ Packing ${courseIds.length} course(s) to ${outputDir}...`));
|
|
27
|
+
|
|
28
|
+
for (const courseId of courseIds) {
|
|
29
|
+
const args = ['pack', courseId, '--server', server, '--output', outputDir];
|
|
30
|
+
|
|
31
|
+
if (username) {
|
|
32
|
+
args.push('--username', username);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (password) {
|
|
36
|
+
args.push('--password', password);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const cliPath = path.join(process.cwd(), 'dist', 'cli.js');
|
|
40
|
+
const commandArgs = ['pack', courseId, '--server', server, '--output', outputDir];
|
|
41
|
+
|
|
42
|
+
if (username) {
|
|
43
|
+
commandArgs.push('--username', username);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (password) {
|
|
47
|
+
commandArgs.push('--password', password);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
console.log(chalk.gray(`š Packing course: ${courseId}`));
|
|
52
|
+
|
|
53
|
+
const { stdout, stderr } = await execFileAsync('node', [cliPath, ...commandArgs], {
|
|
54
|
+
cwd: process.cwd(),
|
|
55
|
+
maxBuffer: 1024 * 1024 * 10, // 10MB buffer for large outputs
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (stdout) {
|
|
59
|
+
console.log(stdout);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (stderr) {
|
|
63
|
+
console.error(chalk.yellow(stderr));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(chalk.green(`ā
Successfully packed course: ${courseId}`));
|
|
67
|
+
} catch (error: unknown) {
|
|
68
|
+
console.error(chalk.red(`ā Failed to pack course ${courseId}:`));
|
|
69
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
70
|
+
|
|
71
|
+
// Continue with other courses instead of failing completely
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(chalk.green(`\nš All courses packed to: ${outputDir}`));
|
|
77
|
+
}
|
package/src/utils/prompts.ts
CHANGED
|
@@ -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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
package/src/utils/template.ts
CHANGED
|
@@ -174,10 +174,16 @@ export async function generateSkuilderConfig(
|
|
|
174
174
|
dataLayerType: config.dataLayerType,
|
|
175
175
|
};
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
// For dynamic data layer, use the specified course ID
|
|
178
|
+
if (config.dataLayerType === 'couch' && config.course) {
|
|
178
179
|
skuilderConfig.course = config.course;
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
// For static data layer with imported courses, use the first course as primary
|
|
183
|
+
if (config.dataLayerType === 'static' && config.importCourseIds && config.importCourseIds.length > 0) {
|
|
184
|
+
skuilderConfig.course = config.importCourseIds[0];
|
|
185
|
+
}
|
|
186
|
+
|
|
181
187
|
if (config.couchdbUrl) {
|
|
182
188
|
skuilderConfig.couchdbUrl = config.couchdbUrl;
|
|
183
189
|
}
|
|
@@ -189,6 +195,42 @@ export async function generateSkuilderConfig(
|
|
|
189
195
|
await fs.writeFile(configPath, JSON.stringify(skuilderConfig, null, 2));
|
|
190
196
|
}
|
|
191
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Transform tsconfig.json to be standalone (remove base config reference)
|
|
200
|
+
*/
|
|
201
|
+
export async function transformTsConfig(tsconfigPath: string): Promise<void> {
|
|
202
|
+
const content = await fs.readFile(tsconfigPath, 'utf-8');
|
|
203
|
+
const tsconfig = JSON.parse(content);
|
|
204
|
+
|
|
205
|
+
// Remove the extends reference to the monorepo base config
|
|
206
|
+
delete tsconfig.extends;
|
|
207
|
+
|
|
208
|
+
// Merge in the essential settings from the base config that scaffolded apps need
|
|
209
|
+
tsconfig.compilerOptions = {
|
|
210
|
+
...tsconfig.compilerOptions,
|
|
211
|
+
// Essential TypeScript settings from base config
|
|
212
|
+
strict: true,
|
|
213
|
+
skipLibCheck: true,
|
|
214
|
+
forceConsistentCasingInFileNames: true,
|
|
215
|
+
esModuleInterop: true,
|
|
216
|
+
allowSyntheticDefaultImports: true,
|
|
217
|
+
// Keep existing Vue/Vite-specific settings
|
|
218
|
+
target: tsconfig.compilerOptions.target || 'ESNext',
|
|
219
|
+
useDefineForClassFields: tsconfig.compilerOptions.useDefineForClassFields,
|
|
220
|
+
module: tsconfig.compilerOptions.module || 'ESNext',
|
|
221
|
+
moduleResolution: tsconfig.compilerOptions.moduleResolution || 'bundler',
|
|
222
|
+
jsx: tsconfig.compilerOptions.jsx || 'preserve',
|
|
223
|
+
resolveJsonModule: tsconfig.compilerOptions.resolveJsonModule,
|
|
224
|
+
isolatedModules: tsconfig.compilerOptions.isolatedModules,
|
|
225
|
+
lib: tsconfig.compilerOptions.lib || ['ESNext', 'DOM'],
|
|
226
|
+
noEmit: tsconfig.compilerOptions.noEmit,
|
|
227
|
+
baseUrl: tsconfig.compilerOptions.baseUrl || '.',
|
|
228
|
+
types: tsconfig.compilerOptions.types || ['vite/client'],
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
await fs.writeFile(tsconfigPath, JSON.stringify(tsconfig, null, 2));
|
|
232
|
+
}
|
|
233
|
+
|
|
192
234
|
/**
|
|
193
235
|
* Generate .gitignore file for the project
|
|
194
236
|
*/
|
|
@@ -316,10 +358,18 @@ Thumbs.db
|
|
|
316
358
|
* Generate project README.md
|
|
317
359
|
*/
|
|
318
360
|
export async function generateReadme(readmePath: string, config: ProjectConfig): Promise<void> {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
361
|
+
let dataLayerInfo = '';
|
|
362
|
+
|
|
363
|
+
if (config.dataLayerType === 'static') {
|
|
364
|
+
dataLayerInfo = 'This project uses a static data layer with JSON files.';
|
|
365
|
+
|
|
366
|
+
if (config.importCourseIds && config.importCourseIds.length > 0) {
|
|
367
|
+
const courseList = config.importCourseIds.map(id => `- ${id}`).join('\n');
|
|
368
|
+
dataLayerInfo += `\n\n**Imported Courses:**\n${courseList}\n\nCourse data is stored in \`public/static-courses/\` and loaded automatically.`;
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
dataLayerInfo = `This project connects to CouchDB at: ${config.couchdbUrl || '[URL not specified]'}`;
|
|
372
|
+
}
|
|
323
373
|
|
|
324
374
|
const readme = `# ${config.title}
|
|
325
375
|
|
|
@@ -437,6 +487,12 @@ export async function processTemplate(
|
|
|
437
487
|
await createViteConfig(viteConfigPath);
|
|
438
488
|
}
|
|
439
489
|
|
|
490
|
+
console.log(chalk.blue('š§ Transforming tsconfig.json...'));
|
|
491
|
+
const tsconfigPath = path.join(projectPath, 'tsconfig.json');
|
|
492
|
+
if (existsSync(tsconfigPath)) {
|
|
493
|
+
await transformTsConfig(tsconfigPath);
|
|
494
|
+
}
|
|
495
|
+
|
|
440
496
|
console.log(chalk.blue('š§ Generating configuration...'));
|
|
441
497
|
const configPath = path.join(projectPath, 'skuilder.config.json');
|
|
442
498
|
await generateSkuilderConfig(configPath, config);
|