@vue-skuilder/cli 0.1.3

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.
@@ -0,0 +1,170 @@
1
+ import { promises as fs } from 'fs';
2
+ import { existsSync } from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import chalk from 'chalk';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ /**
9
+ * Find the standalone-ui package in node_modules
10
+ */
11
+ export async function findStandaloneUiPath() {
12
+ // Start from CLI package root and work upward
13
+ let currentDir = path.join(__dirname, '..', '..');
14
+ while (currentDir !== path.dirname(currentDir)) {
15
+ const nodeModulesPath = path.join(currentDir, 'node_modules', '@vue-skuilder', 'standalone-ui');
16
+ if (existsSync(nodeModulesPath)) {
17
+ return nodeModulesPath;
18
+ }
19
+ currentDir = path.dirname(currentDir);
20
+ }
21
+ throw new Error('Could not find @vue-skuilder/standalone-ui package. Please ensure it is installed.');
22
+ }
23
+ /**
24
+ * Copy directory recursively, excluding certain files/directories
25
+ */
26
+ export async function copyDirectory(source, destination, excludePatterns = ['node_modules', 'dist', '.git', 'cypress']) {
27
+ const entries = await fs.readdir(source, { withFileTypes: true });
28
+ await fs.mkdir(destination, { recursive: true });
29
+ for (const entry of entries) {
30
+ const sourcePath = path.join(source, entry.name);
31
+ const destPath = path.join(destination, entry.name);
32
+ // Skip excluded patterns
33
+ if (excludePatterns.some(pattern => entry.name.includes(pattern))) {
34
+ continue;
35
+ }
36
+ if (entry.isDirectory()) {
37
+ await copyDirectory(sourcePath, destPath, excludePatterns);
38
+ }
39
+ else {
40
+ await fs.copyFile(sourcePath, destPath);
41
+ }
42
+ }
43
+ }
44
+ /**
45
+ * Transform package.json to use published dependencies instead of workspace references
46
+ */
47
+ export async function transformPackageJson(packageJsonPath, projectName, cliVersion) {
48
+ const content = await fs.readFile(packageJsonPath, 'utf-8');
49
+ const packageJson = JSON.parse(content);
50
+ // Update basic project info
51
+ packageJson.name = projectName;
52
+ packageJson.description = `Skuilder course application: ${projectName}`;
53
+ packageJson.version = '1.0.0';
54
+ // Transform workspace dependencies to published versions
55
+ if (packageJson.dependencies) {
56
+ for (const [depName, version] of Object.entries(packageJson.dependencies)) {
57
+ if (typeof version === 'string' && version.startsWith('workspace:')) {
58
+ // Replace workspace references with CLI's version
59
+ packageJson.dependencies[depName] = `^${cliVersion}`;
60
+ }
61
+ }
62
+ }
63
+ // Remove CLI-specific fields that don't belong in generated projects
64
+ delete packageJson.publishConfig;
65
+ await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
66
+ }
67
+ /**
68
+ * Generate skuilder.config.json based on project configuration
69
+ */
70
+ export async function generateSkuilderConfig(configPath, config) {
71
+ const skuilderConfig = {
72
+ title: config.title,
73
+ dataLayerType: config.dataLayerType
74
+ };
75
+ if (config.course) {
76
+ skuilderConfig.course = config.course;
77
+ }
78
+ if (config.couchdbUrl) {
79
+ skuilderConfig.couchdbUrl = config.couchdbUrl;
80
+ }
81
+ if (config.theme) {
82
+ skuilderConfig.theme = config.theme;
83
+ }
84
+ await fs.writeFile(configPath, JSON.stringify(skuilderConfig, null, 2));
85
+ }
86
+ /**
87
+ * Generate project README.md
88
+ */
89
+ export async function generateReadme(readmePath, config) {
90
+ const dataLayerInfo = config.dataLayerType === 'static'
91
+ ? 'This project uses a static data layer with JSON files.'
92
+ : `This project connects to CouchDB at: ${config.couchdbUrl || '[URL not specified]'}`;
93
+ const readme = `# ${config.title}
94
+
95
+ A Skuilder course application built with Vue 3, Vuetify, and Pinia.
96
+
97
+ ## Data Layer
98
+
99
+ ${dataLayerInfo}
100
+
101
+ ## Development
102
+
103
+ Install dependencies:
104
+ \`\`\`bash
105
+ npm install
106
+ \`\`\`
107
+
108
+ Start the development server:
109
+ \`\`\`bash
110
+ npm run dev
111
+ \`\`\`
112
+
113
+ Build for production:
114
+ \`\`\`bash
115
+ npm run build
116
+ \`\`\`
117
+
118
+ ## Configuration
119
+
120
+ Course configuration is managed in \`skuilder.config.json\`. You can modify:
121
+ - Course title
122
+ - Data layer settings
123
+ - Theme customization
124
+ - Database connection details (for dynamic data layer)
125
+
126
+ ## Theme
127
+
128
+ Current theme: **${config.theme.name}**
129
+ - Primary: ${config.theme.colors.primary}
130
+ - Secondary: ${config.theme.colors.secondary}
131
+ - Accent: ${config.theme.colors.accent}
132
+
133
+ ## Testing
134
+
135
+ Run end-to-end tests:
136
+ \`\`\`bash
137
+ npm run test:e2e
138
+ \`\`\`
139
+
140
+ Run tests in headless mode:
141
+ \`\`\`bash
142
+ npm run test:e2e:headless
143
+ \`\`\`
144
+
145
+ ## Learn More
146
+
147
+ Visit the [Skuilder documentation](https://github.com/NiloCK/vue-skuilder) for more information about building course applications.
148
+ `;
149
+ await fs.writeFile(readmePath, readme);
150
+ }
151
+ /**
152
+ * Copy and transform the standalone-ui template to create a new project
153
+ */
154
+ export async function processTemplate(projectPath, config, cliVersion) {
155
+ console.log(chalk.blue('📦 Locating standalone-ui template...'));
156
+ const templatePath = await findStandaloneUiPath();
157
+ console.log(chalk.blue('📂 Copying project files...'));
158
+ await copyDirectory(templatePath, projectPath);
159
+ console.log(chalk.blue('⚙️ Configuring package.json...'));
160
+ const packageJsonPath = path.join(projectPath, 'package.json');
161
+ await transformPackageJson(packageJsonPath, config.projectName, cliVersion);
162
+ console.log(chalk.blue('🔧 Generating configuration...'));
163
+ const configPath = path.join(projectPath, 'skuilder.config.json');
164
+ await generateSkuilderConfig(configPath, config);
165
+ console.log(chalk.blue('📝 Creating README...'));
166
+ const readmePath = path.join(projectPath, 'README.md');
167
+ await generateReadme(readmePath, config);
168
+ console.log(chalk.green('✅ Template processing complete!'));
169
+ }
170
+ //# sourceMappingURL=template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.js","sourceRoot":"","sources":["../../src/utils/template.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3C;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,8CAA8C;IAC9C,IAAI,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAElD,OAAO,UAAU,KAAK,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/C,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,EAAE,eAAe,EAAE,eAAe,CAAC,CAAC;QAChG,IAAI,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;YAChC,OAAO,eAAe,CAAC;QACzB,CAAC;QACD,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACxC,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,oFAAoF,CAAC,CAAC;AACxG,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAc,EACd,WAAmB,EACnB,kBAA4B,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC;IAEvE,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAElE,MAAM,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEjD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACjD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAEpD,yBAAyB;QACzB,IAAI,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YAClE,SAAS;QACX,CAAC;QAED,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,MAAM,aAAa,CAAC,UAAU,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC;QAC7D,CAAC;aAAM,CAAC;YACN,MAAM,EAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,eAAuB,EACvB,WAAmB,EACnB,UAAkB;IAElB,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IAC5D,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAExC,4BAA4B;IAC5B,WAAW,CAAC,IAAI,GAAG,WAAW,CAAC;IAC/B,WAAW,CAAC,WAAW,GAAG,gCAAgC,WAAW,EAAE,CAAC;IACxE,WAAW,CAAC,OAAO,GAAG,OAAO,CAAC;IAE9B,yDAAyD;IACzD,IAAI,WAAW,CAAC,YAAY,EAAE,CAAC;QAC7B,KAAK,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,EAAE,CAAC;YAC1E,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;gBACpE,kDAAkD;gBAClD,WAAW,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,IAAI,UAAU,EAAE,CAAC;YACvD,CAAC;QACH,CAAC;IACH,CAAC;IAED,qEAAqE;IACrE,OAAO,WAAW,CAAC,aAAa,CAAC;IAEjC,MAAM,EAAE,CAAC,SAAS,CAAC,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,UAAkB,EAClB,MAAqB;IAErB,MAAM,cAAc,GAAmB;QACrC,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,aAAa,EAAE,MAAM,CAAC,aAAa;KACpC,CAAC;IAEF,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,cAAc,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IACxC,CAAC;IAED,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACtB,cAAc,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;IAChD,CAAC;IAED,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,cAAc,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IACtC,CAAC;IAED,MAAM,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,UAAkB,EAClB,MAAqB;IAErB,MAAM,aAAa,GAAG,MAAM,CAAC,aAAa,KAAK,QAAQ;QACrD,CAAC,CAAC,wDAAwD;QAC1D,CAAC,CAAC,wCAAwC,MAAM,CAAC,UAAU,IAAI,qBAAqB,EAAE,CAAC;IAEzF,MAAM,MAAM,GAAG,KAAK,MAAM,CAAC,KAAK;;;;;;EAMhC,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBA6BI,MAAM,CAAC,KAAK,CAAC,IAAI;aACvB,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO;eACzB,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS;YAChC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM;;;;;;;;;;;;;;;;;CAiBrC,CAAC;IAEA,MAAM,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AACzC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,WAAmB,EACnB,MAAqB,EACrB,UAAkB;IAElB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,uCAAuC,CAAC,CAAC,CAAC;IACjE,MAAM,YAAY,GAAG,MAAM,oBAAoB,EAAE,CAAC;IAElD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC,CAAC;IACvD,MAAM,aAAa,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IAE/C,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC,CAAC;IAC3D,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IAC/D,MAAM,oBAAoB,CAAC,eAAe,EAAE,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IAE5E,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC,CAAC;IAC1D,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,sBAAsB,CAAC,CAAC;IAClE,MAAM,sBAAsB,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAEjD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC;IACjD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IACvD,MAAM,cAAc,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAEzC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,21 @@
1
+ import backendConfig from '../../eslint.config.backend.mjs';
2
+
3
+ export default [
4
+ ...backendConfig,
5
+ {
6
+ ignores: ['node_modules/**', 'dist/**', 'eslint.config.mjs', 'tsconfig.json'],
7
+ },
8
+ {
9
+ languageOptions: {
10
+ parserOptions: {
11
+ project: './tsconfig.json',
12
+ tsconfigRootDir: import.meta.dirname,
13
+ },
14
+ },
15
+ rules: {
16
+ // CLI-specific rules - CLI tools need console output
17
+ 'no-console': 'off',
18
+ '@typescript-eslint/no-console': 'off',
19
+ },
20
+ },
21
+ ];
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@vue-skuilder/cli",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "0.1.3",
7
+ "type": "module",
8
+ "description": "CLI scaffolding tool for vue-skuilder projects",
9
+ "bin": {
10
+ "skuilder": "./dist/cli.js"
11
+ },
12
+ "main": "dist/cli.js",
13
+ "types": "dist/cli.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/cli.d.ts",
17
+ "import": "./dist/cli.js"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "build": "rm -rf dist && tsc",
22
+ "dev": "tsc --watch",
23
+ "lint": "npx eslint .",
24
+ "lint:fix": "npx eslint . --fix",
25
+ "lint:check": "npx eslint . --max-warnings 0"
26
+ },
27
+ "keywords": [
28
+ "cli",
29
+ "scaffolding",
30
+ "vue",
31
+ "skuilder",
32
+ "education",
33
+ "course"
34
+ ],
35
+ "dependencies": {
36
+ "@vue-skuilder/standalone-ui": "^0.1.3",
37
+ "chalk": "^5.3.0",
38
+ "commander": "^11.0.0",
39
+ "fs-extra": "^11.2.0",
40
+ "inquirer": "^9.2.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/fs-extra": "^11.0.0",
44
+ "@types/inquirer": "^9.0.0",
45
+ "@types/node": "^20.0.0",
46
+ "typescript": "~5.7.2"
47
+ },
48
+ "engines": {
49
+ "node": ">=18.0.0"
50
+ },
51
+ "packageManager": "yarn@4.6.0"
52
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { readFileSync } from 'fs';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname, join } from 'path';
7
+ import { initCommand } from './commands/init.js';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ // Read package.json to get version
13
+ const packagePath = join(__dirname, '..', 'package.json');
14
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
15
+
16
+ const program = new Command();
17
+
18
+ program
19
+ .name('skuilder')
20
+ .description('CLI tool for scaffolding Skuilder course applications')
21
+ .version(packageJson.version);
22
+
23
+ program
24
+ .command('init')
25
+ .argument('<project-name>', 'name of the project to create')
26
+ .description('create a new Skuilder course application')
27
+ .option('--data-layer <type>', 'data layer type (static|dynamic)', 'dynamic')
28
+ .option('--theme <name>', 'theme name (default|medical|educational|corporate)', 'default')
29
+ .option('--no-interactive', 'skip interactive prompts')
30
+ .option('--couchdb-url <url>', 'CouchDB server URL (for dynamic data layer)')
31
+ .option('--course-id <id>', 'course ID to import (for dynamic data layer)')
32
+ .action(initCommand);
33
+
34
+ program.on('--help', () => {
35
+ console.log('');
36
+ console.log('Examples:');
37
+ console.log(' $ skuilder init my-anatomy-course');
38
+ console.log(' $ skuilder init biology-101 --data-layer=static --theme=medical');
39
+ console.log(' $ skuilder init physics --no-interactive --data-layer=dynamic');
40
+ });
41
+
42
+ program.parse();
@@ -0,0 +1,83 @@
1
+ import { existsSync } from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { CliOptions } from '../types.js';
5
+ import { gatherProjectConfig, confirmProjectCreation } from '../utils/prompts.js';
6
+ import { processTemplate } from '../utils/template.js';
7
+ import { readFileSync } from 'fs';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+
14
+ export async function initCommand(
15
+ projectName: string,
16
+ options: CliOptions
17
+ ): Promise<void> {
18
+ try {
19
+ // Validate project name
20
+ if (!isValidProjectName(projectName)) {
21
+ console.error(chalk.red('❌ Invalid project name. Use only letters, numbers, hyphens, and underscores.'));
22
+ process.exit(1);
23
+ }
24
+
25
+ // Check if directory already exists
26
+ const projectPath = path.resolve(process.cwd(), projectName);
27
+ if (existsSync(projectPath)) {
28
+ console.error(chalk.red(`❌ Directory "${projectName}" already exists.`));
29
+ process.exit(1);
30
+ }
31
+
32
+ // Get CLI version for dependency transformation
33
+ const packagePath = join(__dirname, '..', '..', 'package.json');
34
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
35
+ const cliVersion = packageJson.version;
36
+
37
+ // Gather project configuration
38
+ const config = await gatherProjectConfig(projectName, options);
39
+
40
+ // Confirm project creation (only in interactive mode)
41
+ if (options.interactive) {
42
+ const confirmed = await confirmProjectCreation(config, projectPath);
43
+ if (!confirmed) {
44
+ console.log(chalk.yellow('Project creation cancelled.'));
45
+ return;
46
+ }
47
+ }
48
+
49
+ console.log(chalk.cyan(`\n🛠️ Creating project "${projectName}"...\n`));
50
+
51
+ // Process template and create project
52
+ await processTemplate(projectPath, config, cliVersion);
53
+
54
+ // Success message
55
+ console.log(chalk.green('\n🎉 Project created successfully!\n'));
56
+ console.log(chalk.cyan('Next steps:'));
57
+ console.log(` ${chalk.white('cd')} ${projectName}`);
58
+ console.log(` ${chalk.white('npm install')}`);
59
+ console.log(` ${chalk.white('npm run dev')}`);
60
+ console.log('');
61
+
62
+ if (config.dataLayerType === 'couch') {
63
+ console.log(chalk.yellow('📝 Note: Make sure your CouchDB server is running and accessible.'));
64
+ if (config.course) {
65
+ console.log(chalk.yellow(`📚 Course ID "${config.course}" will be loaded from the database.`));
66
+ }
67
+ } else {
68
+ console.log(chalk.yellow('📝 Note: This project uses static data. Sample course data has been included.'));
69
+ }
70
+
71
+ } catch (error) {
72
+ console.error(chalk.red('\n❌ Failed to create project:'));
73
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ function isValidProjectName(name: string): boolean {
79
+ // Allow letters, numbers, hyphens, and underscores
80
+ // Must start with a letter or number
81
+ const validNameRegex = /^[a-zA-Z0-9][a-zA-Z0-9-_]*$/;
82
+ return validNameRegex.test(name) && name.length > 0 && name.length <= 214;
83
+ }
package/src/types.ts ADDED
@@ -0,0 +1,72 @@
1
+ export interface CliOptions {
2
+ dataLayer: 'static' | 'dynamic';
3
+ theme: 'default' | 'medical' | 'educational' | 'corporate';
4
+ interactive: boolean;
5
+ couchdbUrl?: string;
6
+ courseId?: string;
7
+ }
8
+
9
+ export interface ProjectConfig {
10
+ projectName: string;
11
+ title: string;
12
+ dataLayerType: 'static' | 'couch';
13
+ course?: string;
14
+ couchdbUrl?: string;
15
+ theme: ThemeConfig;
16
+ }
17
+
18
+ export interface ThemeConfig {
19
+ name: string;
20
+ colors: {
21
+ primary: string;
22
+ secondary: string;
23
+ accent: string;
24
+ };
25
+ }
26
+
27
+ export interface SkuilderConfig {
28
+ title: string;
29
+ course?: string;
30
+ dataLayerType: 'static' | 'couch';
31
+ couchdbUrl?: string;
32
+ theme?: ThemeConfig;
33
+ }
34
+
35
+ export interface InitCommandOptions extends CliOptions {
36
+ projectName: string;
37
+ }
38
+
39
+ export const PREDEFINED_THEMES: Record<string, ThemeConfig> = {
40
+ default: {
41
+ name: 'default',
42
+ colors: {
43
+ primary: '#1976D2',
44
+ secondary: '#424242',
45
+ accent: '#82B1FF'
46
+ }
47
+ },
48
+ medical: {
49
+ name: 'medical',
50
+ colors: {
51
+ primary: '#2E7D32',
52
+ secondary: '#558B2F',
53
+ accent: '#66BB6A'
54
+ }
55
+ },
56
+ educational: {
57
+ name: 'educational',
58
+ colors: {
59
+ primary: '#F57C00',
60
+ secondary: '#FF9800',
61
+ accent: '#FFB74D'
62
+ }
63
+ },
64
+ corporate: {
65
+ name: 'corporate',
66
+ colors: {
67
+ primary: '#37474F',
68
+ secondary: '#546E7A',
69
+ accent: '#78909C'
70
+ }
71
+ }
72
+ };
@@ -0,0 +1,204 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { CliOptions, ProjectConfig, PREDEFINED_THEMES, ThemeConfig } from '../types.js';
4
+
5
+ export async function gatherProjectConfig(
6
+ projectName: string,
7
+ options: CliOptions
8
+ ): Promise<ProjectConfig> {
9
+ console.log(chalk.cyan('\n🚀 Creating a new Skuilder course application\n'));
10
+
11
+ let config: Partial<ProjectConfig> = {
12
+ projectName
13
+ };
14
+
15
+ if (options.interactive) {
16
+ const answers = await inquirer.prompt([
17
+ {
18
+ type: 'input',
19
+ name: 'title',
20
+ message: 'Course title:',
21
+ default: formatProjectName(projectName),
22
+ validate: (input: string) => input.trim().length > 0 || 'Course title is required'
23
+ },
24
+ {
25
+ type: 'list',
26
+ name: 'dataLayerType',
27
+ message: 'Data layer type:',
28
+ choices: [
29
+ {
30
+ name: 'Dynamic (Connect to CouchDB server)',
31
+ value: 'couch'
32
+ },
33
+ {
34
+ name: 'Static (Self-contained JSON files)',
35
+ value: 'static'
36
+ }
37
+ ],
38
+ default: options.dataLayer === 'dynamic' ? 'couch' : 'static'
39
+ },
40
+ {
41
+ type: 'input',
42
+ name: 'couchdbUrl',
43
+ message: 'CouchDB server URL:',
44
+ default: 'http://localhost:5984',
45
+ when: (answers) => answers.dataLayerType === 'couch',
46
+ validate: (input: string) => {
47
+ if (!input.trim()) return 'CouchDB URL is required for dynamic data layer';
48
+ try {
49
+ new URL(input);
50
+ return true;
51
+ } catch {
52
+ return 'Please enter a valid URL';
53
+ }
54
+ }
55
+ },
56
+ {
57
+ type: 'input',
58
+ name: 'courseId',
59
+ message: 'Course ID to import (optional):',
60
+ when: (answers) => answers.dataLayerType === 'couch'
61
+ },
62
+ {
63
+ type: 'list',
64
+ name: 'themeName',
65
+ message: 'Select theme:',
66
+ choices: [
67
+ {
68
+ name: 'Default (Material Blue)',
69
+ value: 'default'
70
+ },
71
+ {
72
+ name: 'Medical (Healthcare Green)',
73
+ value: 'medical'
74
+ },
75
+ {
76
+ name: 'Educational (Academic Orange)',
77
+ value: 'educational'
78
+ },
79
+ {
80
+ name: 'Corporate (Professional Gray)',
81
+ value: 'corporate'
82
+ }
83
+ ],
84
+ default: options.theme
85
+ }
86
+ ]);
87
+
88
+ config = {
89
+ ...config,
90
+ title: answers.title,
91
+ dataLayerType: answers.dataLayerType,
92
+ couchdbUrl: answers.couchdbUrl,
93
+ course: answers.courseId,
94
+ theme: PREDEFINED_THEMES[answers.themeName]
95
+ };
96
+ } else {
97
+ // Non-interactive mode: use provided options
98
+ config = {
99
+ projectName,
100
+ title: formatProjectName(projectName),
101
+ dataLayerType: options.dataLayer === 'dynamic' ? 'couch' : 'static',
102
+ couchdbUrl: options.couchdbUrl,
103
+ course: options.courseId,
104
+ theme: PREDEFINED_THEMES[options.theme]
105
+ };
106
+
107
+ // Validate required fields for non-interactive mode
108
+ if (config.dataLayerType === 'couch' && !config.couchdbUrl) {
109
+ throw new Error('CouchDB URL is required when using dynamic data layer. Use --couchdb-url option.');
110
+ }
111
+ }
112
+
113
+ return config as ProjectConfig;
114
+ }
115
+
116
+ export async function confirmProjectCreation(
117
+ config: ProjectConfig,
118
+ projectPath: string
119
+ ): Promise<boolean> {
120
+ console.log(chalk.yellow('\n📋 Project Configuration Summary:'));
121
+ console.log(` Project Name: ${chalk.white(config.projectName)}`);
122
+ console.log(` Course Title: ${chalk.white(config.title)}`);
123
+ console.log(` Data Layer: ${chalk.white(config.dataLayerType)}`);
124
+
125
+ if (config.couchdbUrl) {
126
+ console.log(` CouchDB URL: ${chalk.white(config.couchdbUrl)}`);
127
+ }
128
+
129
+ if (config.course) {
130
+ console.log(` Course ID: ${chalk.white(config.course)}`);
131
+ }
132
+
133
+ console.log(` Theme: ${chalk.white(config.theme.name)}`);
134
+ console.log(` Directory: ${chalk.white(projectPath)}`);
135
+
136
+ const { confirmed } = await inquirer.prompt([
137
+ {
138
+ type: 'confirm',
139
+ name: 'confirmed',
140
+ 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
164
+ },
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
+ ]);
180
+
181
+ return {
182
+ name: answers.name,
183
+ colors: {
184
+ primary: answers.primary,
185
+ secondary: answers.secondary,
186
+ accent: answers.accent
187
+ }
188
+ };
189
+ }
190
+
191
+ function formatProjectName(projectName: string): string {
192
+ return projectName
193
+ .split(/[-_\s]+/)
194
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
195
+ .join(' ');
196
+ }
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
+ }