@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.
- package/README.md +163 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +35 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +70 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +35 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/prompts.d.ts +5 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +185 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/template.d.ts +26 -0
- package/dist/utils/template.d.ts.map +1 -0
- package/dist/utils/template.js +170 -0
- package/dist/utils/template.js.map +1 -0
- package/eslint.config.mjs +21 -0
- package/package.json +52 -0
- package/src/cli.ts +42 -0
- package/src/commands/init.ts +83 -0
- package/src/types.ts +72 -0
- package/src/utils/prompts.ts +204 -0
- package/src/utils/template.ts +215 -0
- package/tsconfig.json +18 -0
|
@@ -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":"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"}
|
|
@@ -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
|
+
}
|