create-marp-presentation 1.1.0 → 1.2.1
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 +45 -37
- package/cli/commands/add-themes-cli.js +85 -0
- package/cli/commands/create-project.js +199 -0
- package/cli/utils/file-utils.js +106 -0
- package/cli/utils/prompt-utils.js +89 -0
- package/docs/plans/2025-02-19-marp-template-design.md +207 -0
- package/docs/plans/2025-02-19-marp-template-implementation.md +848 -0
- package/docs/plans/2026-02-20-example-slides-design.md +179 -0
- package/docs/plans/2026-02-20-example-slides-implementation.md +811 -0
- package/docs/plans/2026-02-23-theme-management-design.md +836 -0
- package/docs/plans/2026-02-23-theme-management-implementation.md +3585 -0
- package/docs/plans/2026-02-26-theme-addition-refactoring-design.md +172 -0
- package/docs/plans/2026-02-26-theme-addition-refactoring.md +456 -0
- package/docs/plans/2026-02-27-theme-add-fix-design.md +136 -0
- package/docs/plans/2026-02-27-theme-add-fix.md +353 -0
- package/docs/plans/TODO.md +5 -0
- package/docs/reqs/themes-requirements.md +49 -0
- package/docs/theme-management.md +261 -0
- package/index.js +111 -164
- package/lib/add-themes-command.js +381 -0
- package/lib/errors.js +62 -0
- package/lib/frontmatter.js +71 -0
- package/lib/prompts.js +222 -0
- package/lib/theme-manager.js +238 -0
- package/lib/theme-resolver.js +227 -0
- package/lib/vscode-integration.js +198 -0
- package/package.json +15 -5
- package/template/README.md +28 -17
- package/template/package.json +13 -1
- package/template/scripts/theme-cli.js +248 -0
- package/themes/beam/beam.css +141 -0
- package/themes/default-clean/default-clean.css +57 -0
- package/themes/gaia-dark/gaia-dark.css +27 -0
- package/themes/marpx/marpx.css +1735 -0
- package/themes/marpx/socrates.css +105 -0
- package/themes/uncover-minimal/uncover-minimal.css +22 -0
package/README.md
CHANGED
|
@@ -1,64 +1,72 @@
|
|
|
1
1
|
# Create Marp Presentation
|
|
2
2
|
|
|
3
|
-
Create
|
|
3
|
+
Create beautiful presentations in Markdown. Zero setup.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npx create-marp-presentation my-presentation
|
|
9
|
+
cd my-presentation
|
|
10
|
+
npm run dev
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
That's it! Edit `presentation.md` and see changes live at `http://localhost:8080`.
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
cd my-presentation
|
|
15
|
-
npm run dev # Live preview
|
|
16
|
-
npm run build:all # Build all formats
|
|
17
|
-
```
|
|
15
|
+
## VSCode Setup
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
For the best editing experience, install [Marp for VSCode](https://marketplace.visualstudio.com/items?itemName=marp-team.marp-vscode):
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- 📁 Static files support
|
|
26
|
-
- 🔥 Live preview
|
|
19
|
+
1. Open VSCode
|
|
20
|
+
2. Press `Ctrl+Shift+X` (or `Cmd+Shift+X` on Mac)
|
|
21
|
+
3. Search "Marp for VSCode"
|
|
22
|
+
4. Click Install
|
|
27
23
|
|
|
28
|
-
|
|
24
|
+
After installing, open any `.md` file and click the **Marp: Preview** button in the top-right corner.
|
|
29
25
|
|
|
30
|
-
|
|
26
|
+
## What You Get
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
- **Markdown slides** - Write presentations in plain text
|
|
29
|
+
- **Live preview** - See changes instantly in browser
|
|
30
|
+
- **Export anywhere** - HTML, PDF, PPTX with one command
|
|
31
|
+
- **Themes** - Beautiful built-in themes, add more anytime
|
|
32
|
+
- **Static files** - Images and assets copied automatically
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
npm install
|
|
34
|
+
## Commands
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
| Command | What it does |
|
|
37
|
+
|---------|--------------|
|
|
38
|
+
| `npm run dev` | Live preview at http://localhost:8080 |
|
|
39
|
+
| `npm run build:all` | Export to HTML, PDF, and PPTX |
|
|
40
|
+
| `npm run clean` | Remove output folder |
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
node index.js test-project
|
|
42
|
+
## Theme Management
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
cd test-project
|
|
48
|
-
ls -la
|
|
49
|
-
npm run dev
|
|
50
|
-
```
|
|
44
|
+
Add and switch themes anytime.
|
|
51
45
|
|
|
52
|
-
|
|
46
|
+
### Quick Theme Commands
|
|
53
47
|
|
|
54
48
|
```bash
|
|
55
|
-
#
|
|
56
|
-
|
|
49
|
+
npm run theme:add # Interactive: select themes to add
|
|
50
|
+
npm run theme:add beam # Add specific themes
|
|
51
|
+
npm run theme:list # See what's available
|
|
52
|
+
npm run theme:set marpx # Change active theme
|
|
57
53
|
```
|
|
58
54
|
|
|
59
|
-
|
|
55
|
+
When you create a project, you'll be prompted to add themes. Add more later with `npm run theme:add`.
|
|
56
|
+
|
|
57
|
+
[See full theme documentation →](docs/theme-management.md)
|
|
58
|
+
|
|
59
|
+
## Local Development
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
To test without publishing to npm:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/echernyshev/marp-presentation-template.git
|
|
65
|
+
cd marp-presentation-template
|
|
66
|
+
npm install
|
|
67
|
+
npm test
|
|
68
|
+
node index.js test-project
|
|
69
|
+
```
|
|
62
70
|
|
|
63
71
|
## License
|
|
64
72
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// cli/commands/add-themes-cli.js
|
|
2
|
+
/**
|
|
3
|
+
* Add themes to existing project command for meta-package CLI
|
|
4
|
+
* This file is NOT copied to generated projects
|
|
5
|
+
*/
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const { AddThemesCommand } = require('../../lib/add-themes-command');
|
|
10
|
+
const { ThemeError } = require('../../lib/errors');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Add themes to an existing project
|
|
14
|
+
* @param {string} targetPath - Path to the target project
|
|
15
|
+
* @param {Object} options - Configuration options
|
|
16
|
+
* @param {string} options.themesLibraryPath - Path to themes library
|
|
17
|
+
* @param {string[]} options.themeNames - Specific theme names to add (optional)
|
|
18
|
+
* @returns {Promise<void>}
|
|
19
|
+
*/
|
|
20
|
+
async function addThemesToExistingProject(targetPath, options = {}) {
|
|
21
|
+
const {
|
|
22
|
+
themesLibraryPath = path.join(__dirname, '../..', 'themes'),
|
|
23
|
+
themeNames = null
|
|
24
|
+
} = options;
|
|
25
|
+
|
|
26
|
+
// Validate target path exists
|
|
27
|
+
const resolvedPath = path.resolve(targetPath);
|
|
28
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
29
|
+
console.error(`Error: Directory does not exist: ${targetPath}`);
|
|
30
|
+
throw new ThemeError(`Target path does not exist: ${targetPath}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if it's a valid Marp project (has presentation.md or package.json)
|
|
34
|
+
const hasPresentation = fs.existsSync(path.join(resolvedPath, 'presentation.md'));
|
|
35
|
+
const hasPackageJson = fs.existsSync(path.join(resolvedPath, 'package.json'));
|
|
36
|
+
|
|
37
|
+
if (!hasPresentation && !hasPackageJson) {
|
|
38
|
+
console.warn('Warning: This does not appear to be a Marp presentation project.');
|
|
39
|
+
console.warn(' (no presentation.md or package.json found)');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Create AddThemesCommand with themes library path
|
|
43
|
+
const command = new AddThemesCommand({
|
|
44
|
+
templatePath: themesLibraryPath,
|
|
45
|
+
interactive: true
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
console.log(`Adding themes to: ${resolvedPath}`);
|
|
50
|
+
console.log();
|
|
51
|
+
|
|
52
|
+
// Execute command - pass theme names if provided, otherwise prompt
|
|
53
|
+
const { copied, skipped, conflicts } = await command.execute(resolvedPath, {
|
|
54
|
+
themes: themeNames || undefined
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Show summary
|
|
58
|
+
const copiedNames = copied.map(t => t.name);
|
|
59
|
+
console.log(`\nCopied themes: ${copiedNames.join(', ') || 'none'}`);
|
|
60
|
+
if (skipped.length > 0) {
|
|
61
|
+
console.log(`Skipped: ${skipped.join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
if (conflicts.length > 0) {
|
|
64
|
+
console.log(`Conflicts: ${conflicts.join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log();
|
|
68
|
+
console.log('Themes added successfully!');
|
|
69
|
+
console.log(`\nNext steps in ${targetPath}:`);
|
|
70
|
+
console.log(' npm run theme:list # List available themes');
|
|
71
|
+
console.log(' npm run theme:set <theme> # Set active theme');
|
|
72
|
+
console.log();
|
|
73
|
+
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof ThemeError) {
|
|
76
|
+
console.error(`Error: ${error.message}`);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
addThemesToExistingProject
|
|
85
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// cli/commands/create-project.js
|
|
2
|
+
/**
|
|
3
|
+
* Project creation command for meta-package CLI
|
|
4
|
+
* This file is NOT copied to generated projects
|
|
5
|
+
*/
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const { AddThemesCommand } = require('../../lib/add-themes-command');
|
|
11
|
+
const { Prompts } = require('../../lib/prompts');
|
|
12
|
+
const { ThemeManager } = require('../../lib/theme-manager');
|
|
13
|
+
|
|
14
|
+
const { copyDir, copyOptionalFiles } = require('../utils/file-utils');
|
|
15
|
+
const { askCreateExamples } = require('../utils/prompt-utils');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate project name
|
|
19
|
+
* @param {string} name - Project name to validate
|
|
20
|
+
* @returns {boolean} True if valid
|
|
21
|
+
*/
|
|
22
|
+
function validateProjectName(name) {
|
|
23
|
+
const validName = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
|
|
24
|
+
return validName.test(name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse --path argument from argv
|
|
29
|
+
* @param {string[]} argv - Process argv (should start from index 2)
|
|
30
|
+
* @returns {{outputPath: string, pathIndex: number|null}} Parsed arguments
|
|
31
|
+
*/
|
|
32
|
+
function parsePathArg(argv) {
|
|
33
|
+
let outputPath = process.cwd();
|
|
34
|
+
const pathIndex = argv.indexOf('--path');
|
|
35
|
+
if (pathIndex !== -1 && pathIndex + 1 < argv.length) {
|
|
36
|
+
return { outputPath: null, pathIndex }; // Signal that validation is needed
|
|
37
|
+
}
|
|
38
|
+
return { outputPath, pathIndex: null };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a new Marp presentation project
|
|
43
|
+
* @param {string} projectName - Name of the project
|
|
44
|
+
* @param {Object} options - Configuration options
|
|
45
|
+
* @param {string} options.outputPath - Output directory path (default: current directory)
|
|
46
|
+
* @param {string} options.templatePath - Path to template directory
|
|
47
|
+
* @param {string} options.themesLibraryPath - Path to themes library
|
|
48
|
+
* @param {Function} options.validatePath - Path validation function
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
async function createProject(projectName, options = {}) {
|
|
52
|
+
const {
|
|
53
|
+
outputPath: providedOutputPath,
|
|
54
|
+
templatePath = path.join(__dirname, '../..', 'template'),
|
|
55
|
+
themesLibraryPath = path.join(__dirname, '../..', 'themes'),
|
|
56
|
+
validatePath
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
// Validate project name
|
|
60
|
+
if (!validateProjectName(projectName)) {
|
|
61
|
+
console.error(`Invalid project name: "${projectName}"`);
|
|
62
|
+
console.error('Project name must be lowercase, contain only letters, numbers, and hyphens.');
|
|
63
|
+
throw new Error('Invalid project name');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Determine output path
|
|
67
|
+
let outputPath = providedOutputPath || process.cwd();
|
|
68
|
+
|
|
69
|
+
// Build final project path
|
|
70
|
+
const projectPath = path.join(outputPath, projectName);
|
|
71
|
+
|
|
72
|
+
// Check if project already exists
|
|
73
|
+
if (fs.existsSync(projectPath)) {
|
|
74
|
+
console.error(`Directory "${projectName}" already exists.`);
|
|
75
|
+
throw new Error('Project directory already exists');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`Creating Marp presentation: ${projectName}`);
|
|
79
|
+
if (outputPath !== process.cwd()) {
|
|
80
|
+
console.log(` Location: ${projectPath}`);
|
|
81
|
+
}
|
|
82
|
+
console.log();
|
|
83
|
+
|
|
84
|
+
// Ask about example slides
|
|
85
|
+
const createExamples = await askCreateExamples();
|
|
86
|
+
|
|
87
|
+
// Create project directory
|
|
88
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
89
|
+
|
|
90
|
+
// Copy template completely
|
|
91
|
+
copyDir(templatePath, projectPath);
|
|
92
|
+
|
|
93
|
+
// Copy lib scripts to project
|
|
94
|
+
const rootLibPath = path.join(__dirname, '../..', 'lib');
|
|
95
|
+
const projectScriptsLibPath = path.join(projectPath, 'scripts', 'lib');
|
|
96
|
+
fs.mkdirSync(projectScriptsLibPath, { recursive: true });
|
|
97
|
+
copyDir(rootLibPath, projectScriptsLibPath);
|
|
98
|
+
|
|
99
|
+
// Verify marp.themeSet configuration exists in package.json
|
|
100
|
+
ThemeManager.ensureThemeSetConfig(projectPath);
|
|
101
|
+
|
|
102
|
+
console.log('✓ Project created');
|
|
103
|
+
|
|
104
|
+
// Copy optional files
|
|
105
|
+
if (createExamples) {
|
|
106
|
+
copyOptionalFiles(projectPath);
|
|
107
|
+
console.log('✓ Example slides added');
|
|
108
|
+
console.log('✓ Demo image added to static/');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Copy theme management documentation
|
|
112
|
+
const themeDocsSource = path.join(__dirname, '../..', 'docs', 'theme-management.md');
|
|
113
|
+
const themeDocsDest = path.join(projectPath, 'docs', 'theme-management.md');
|
|
114
|
+
if (fs.existsSync(themeDocsSource)) {
|
|
115
|
+
fs.mkdirSync(path.dirname(themeDocsDest), { recursive: true });
|
|
116
|
+
fs.copyFileSync(themeDocsSource, themeDocsDest);
|
|
117
|
+
console.log('✓ Theme management guide added');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Add themes to project (prompts user interactively if TTY)
|
|
121
|
+
let copied = [];
|
|
122
|
+
if (process.stdin.isTTY) {
|
|
123
|
+
// Interactive mode - prompt for themes
|
|
124
|
+
console.log();
|
|
125
|
+
const command = new AddThemesCommand({
|
|
126
|
+
templatePath: themesLibraryPath,
|
|
127
|
+
interactive: true
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const result = await command.execute(projectPath, {
|
|
131
|
+
themes: null // Triggers built-in prompt
|
|
132
|
+
});
|
|
133
|
+
copied = result.copied;
|
|
134
|
+
}
|
|
135
|
+
// Non-interactive mode: skip theme addition entirely
|
|
136
|
+
|
|
137
|
+
// Select and set active theme from copied themes
|
|
138
|
+
if (copied && copied.length > 0) {
|
|
139
|
+
console.log();
|
|
140
|
+
const themeNames = copied.map(t => t.name);
|
|
141
|
+
const activeTheme = await Prompts.promptActiveTheme(themeNames);
|
|
142
|
+
const themeManager = new ThemeManager(projectPath);
|
|
143
|
+
try {
|
|
144
|
+
themeManager.setActiveTheme(activeTheme);
|
|
145
|
+
console.log(`✓ Set active theme to "${activeTheme}"`);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.warn(` Warning: Could not set active theme: ${error.message}`);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
// No themes copied, set default active theme
|
|
151
|
+
const themeManager = new ThemeManager(projectPath);
|
|
152
|
+
try {
|
|
153
|
+
themeManager.setActiveTheme('default');
|
|
154
|
+
console.log(`✓ Set active theme to "default"`);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.warn(` Warning: Could not set active theme: ${error.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log();
|
|
161
|
+
|
|
162
|
+
// Run npm install
|
|
163
|
+
console.log('Installing dependencies...');
|
|
164
|
+
const installResult = spawnSync('npm', ['install'], {
|
|
165
|
+
cwd: projectPath,
|
|
166
|
+
stdio: 'inherit',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (installResult.status !== 0) {
|
|
170
|
+
console.error();
|
|
171
|
+
console.error('Failed to install dependencies.');
|
|
172
|
+
console.error('Please run "cd ' + projectName + ' && npm install" manually.');
|
|
173
|
+
throw new Error('npm install failed');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log();
|
|
177
|
+
console.log('✓ Dependencies installed');
|
|
178
|
+
console.log();
|
|
179
|
+
console.log('Next steps:');
|
|
180
|
+
const cwdRelativePath = path.relative(process.cwd(), projectPath);
|
|
181
|
+
const cdPath = cwdRelativePath.startsWith('..') ? projectPath : cwdRelativePath;
|
|
182
|
+
const readmePath = path.join(cdPath, 'README.md');
|
|
183
|
+
console.log(` cd ${cdPath}`);
|
|
184
|
+
console.log(' npm run dev # Start live preview');
|
|
185
|
+
console.log(' npm run theme:list # List available themes');
|
|
186
|
+
console.log(' npm run theme:set <theme-name> # Set active theme');
|
|
187
|
+
console.log(' npm run theme:add # Add more themes to the project');
|
|
188
|
+
console.log(' npm run build:all # Build all formats');
|
|
189
|
+
console.log(' Open project folder in vscode with Marp extension for editing your presentation');
|
|
190
|
+
console.log(` Read ${readmePath} for more information`);
|
|
191
|
+
console.log(' Enjoy!');
|
|
192
|
+
console.log();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
createProject,
|
|
197
|
+
validateProjectName,
|
|
198
|
+
parsePathArg
|
|
199
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// cli/utils/file-utils.js
|
|
2
|
+
/**
|
|
3
|
+
* File and directory utilities for meta-package CLI commands
|
|
4
|
+
* These functions are NOT copied to generated projects
|
|
5
|
+
*/
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Expand ~ to home directory
|
|
12
|
+
* @param {string} input - Path that may start with ~
|
|
13
|
+
* @returns {string} Expanded path
|
|
14
|
+
*/
|
|
15
|
+
function expandHomePath(input) {
|
|
16
|
+
if (input.startsWith('~')) {
|
|
17
|
+
return path.join(os.homedir(), input.slice(1));
|
|
18
|
+
}
|
|
19
|
+
return input;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validate and normalize an output directory path
|
|
24
|
+
* @param {string} inputPath - Path to validate
|
|
25
|
+
* @returns {{valid: boolean, error?: string, resolvedPath?: string}} Validation result
|
|
26
|
+
*/
|
|
27
|
+
function validateOutputPath(inputPath) {
|
|
28
|
+
if (!inputPath || inputPath.trim() === '') {
|
|
29
|
+
return { valid: false, error: 'Path cannot be empty' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check for null bytes (security)
|
|
33
|
+
if (inputPath.includes('\0')) {
|
|
34
|
+
return { valid: false, error: 'Path contains null bytes' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const expanded = expandHomePath(inputPath);
|
|
38
|
+
const resolved = path.resolve(expanded);
|
|
39
|
+
const normalizedResolved = resolved.toLowerCase();
|
|
40
|
+
|
|
41
|
+
// Block system-sensitive directories
|
|
42
|
+
const sensitiveDirs = ['/etc', '/sys', '/proc', '/root', '/boot'];
|
|
43
|
+
for (const sensitive of sensitiveDirs) {
|
|
44
|
+
if (normalizedResolved.startsWith(sensitive.toLowerCase() + path.sep) ||
|
|
45
|
+
normalizedResolved === sensitive.toLowerCase()) {
|
|
46
|
+
return { valid: false, error: `Cannot create project in system directory: ${sensitive}` };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Don't allow creation inside node_modules
|
|
51
|
+
if (normalizedResolved.includes('node_modules')) {
|
|
52
|
+
return { valid: false, error: 'Cannot create project inside node_modules directory' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { valid: true, resolvedPath: resolved };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Recursively copy a directory
|
|
60
|
+
* @param {string} src - Source directory path
|
|
61
|
+
* @param {string} dest - Destination directory path
|
|
62
|
+
*/
|
|
63
|
+
function copyDir(src, dest) {
|
|
64
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
65
|
+
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const srcPath = path.join(src, entry.name);
|
|
68
|
+
const destPath = path.join(dest, entry.name);
|
|
69
|
+
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
72
|
+
copyDir(srcPath, destPath);
|
|
73
|
+
} else {
|
|
74
|
+
fs.copyFileSync(srcPath, destPath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Copy optional template files to destination
|
|
81
|
+
* @param {string} destPath - Destination project path
|
|
82
|
+
*/
|
|
83
|
+
function copyOptionalFiles(destPath) {
|
|
84
|
+
const optionalPath = path.join(__dirname, '..', '..', 'template-optional');
|
|
85
|
+
|
|
86
|
+
// Copy examples.md
|
|
87
|
+
const examplesSrc = path.join(optionalPath, 'examples.md');
|
|
88
|
+
const examplesDest = path.join(destPath, 'examples.md');
|
|
89
|
+
if (fs.existsSync(examplesSrc)) {
|
|
90
|
+
fs.copyFileSync(examplesSrc, examplesDest);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Copy demo image
|
|
94
|
+
const demoImageSrc = path.join(optionalPath, 'static', 'demo-image.png');
|
|
95
|
+
const demoImageDest = path.join(destPath, 'static', 'demo-image.png');
|
|
96
|
+
if (fs.existsSync(demoImageSrc)) {
|
|
97
|
+
fs.copyFileSync(demoImageSrc, demoImageDest);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
expandHomePath,
|
|
103
|
+
validateOutputPath,
|
|
104
|
+
copyDir,
|
|
105
|
+
copyOptionalFiles
|
|
106
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// cli/utils/prompt-utils.js
|
|
2
|
+
/**
|
|
3
|
+
* Prompt utilities for meta-package CLI commands
|
|
4
|
+
* These functions are NOT copied to generated projects
|
|
5
|
+
*/
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Ask user if they want to create example slides
|
|
10
|
+
* @returns {Promise<boolean>} True if user wants examples
|
|
11
|
+
*/
|
|
12
|
+
async function askCreateExamples() {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
// Interactive mode - ask user
|
|
15
|
+
if (process.stdin.isTTY) {
|
|
16
|
+
const rl = readline.createInterface({
|
|
17
|
+
input: process.stdin,
|
|
18
|
+
output: process.stdout
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
rl.question('Create example slides file? (Y/n) ', (answer) => {
|
|
22
|
+
rl.close();
|
|
23
|
+
const normalized = answer.toLowerCase().trim();
|
|
24
|
+
resolve(normalized !== 'n' && normalized !== 'no');
|
|
25
|
+
});
|
|
26
|
+
} else {
|
|
27
|
+
// Non-interactive mode - read from stdin if there's data
|
|
28
|
+
let input = '';
|
|
29
|
+
process.stdin.setEncoding('utf8');
|
|
30
|
+
|
|
31
|
+
process.stdin.on('readable', () => {
|
|
32
|
+
let chunk;
|
|
33
|
+
while ((chunk = process.stdin.read()) !== null) {
|
|
34
|
+
input += chunk;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
process.stdin.on('end', () => {
|
|
39
|
+
const normalized = input.toLowerCase().trim();
|
|
40
|
+
// If input is empty, create examples by default
|
|
41
|
+
if (normalized === '') {
|
|
42
|
+
resolve(true);
|
|
43
|
+
} else {
|
|
44
|
+
resolve(normalized !== 'n' && normalized !== 'no');
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// If stdin has no data immediately, finish
|
|
49
|
+
if (process.stdin.readableLength === 0) {
|
|
50
|
+
// Give a short time for data to appear
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
if (input === '') {
|
|
53
|
+
process.stdin.destroy();
|
|
54
|
+
resolve(true);
|
|
55
|
+
}
|
|
56
|
+
}, 10);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Ask user if they want to add themes to the project
|
|
64
|
+
* @returns {Promise<boolean>} True if user wants themes
|
|
65
|
+
*/
|
|
66
|
+
async function askAddThemes() {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
if (process.stdin.isTTY) {
|
|
69
|
+
const rl = readline.createInterface({
|
|
70
|
+
input: process.stdin,
|
|
71
|
+
output: process.stdout
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
rl.question('Add custom themes to the project? (Y/n) ', (answer) => {
|
|
75
|
+
rl.close();
|
|
76
|
+
const normalized = answer.toLowerCase().trim();
|
|
77
|
+
resolve(normalized !== 'n' && normalized !== 'no');
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
// Non-interactive mode: default to false (don't add themes without consent)
|
|
81
|
+
resolve(false);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
askCreateExamples,
|
|
88
|
+
askAddThemes
|
|
89
|
+
};
|