create-marp-presentation 1.0.1 → 1.2.0

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 CHANGED
@@ -1,34 +1,73 @@
1
1
  # Create Marp Presentation
2
2
 
3
- Создайте новую Marp презентацию одной командой.
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`.
14
+
15
+ ## VSCode Setup
16
+
17
+ For the best editing experience, install [Marp for VSCode](https://marketplace.visualstudio.com/items?itemName=marp-team.marp-vscode):
18
+
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
23
+
24
+ After installing, open any `.md` file and click the **Marp: Preview** button in the top-right corner.
25
+
26
+ ## What You Get
27
+
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
33
+
34
+ ## Commands
35
+
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 |
41
+
42
+ ## Theme Management
43
+
44
+ Add and switch themes anytime.
45
+
46
+ ### Quick Theme Commands
12
47
 
13
48
  ```bash
14
- cd my-presentation
15
- npm run dev # Live preview
16
- npm run build:all # Build all formats
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
17
53
  ```
18
54
 
19
- ## Возможности
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)
20
58
 
21
- - 🚀 One-command setup
22
- - 📝 Markdown slides
23
- - 🎨 Marp themes
24
- - 📦 HTML, PDF, PPTX export
25
- - 📁 Static files support
26
- - 🔥 Live preview
59
+ ## Local Development
27
60
 
28
- ## Документация
61
+ To test without publishing to npm:
29
62
 
30
- После создания проекта читайте README в папке проекта.
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
+ ```
31
70
 
32
- ## Лицензия
71
+ ## License
33
72
 
34
73
  MIT
package/index.js CHANGED
@@ -1,96 +1,131 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { spawnSync } = require('child_process');
3
+ /**
4
+ * create-marp-presentation - CLI entry point
5
+ * Supports dual entry points:
6
+ * 1. npx create-marp-presentation <name> [--path <dir>] - Create new project
7
+ * 2. npx create-marp-presentation theme:add <path> [themes...] - Add themes to existing project
8
+ */
6
9
 
7
- const projectName = process.argv[2];
10
+ const path = require('path');
8
11
 
9
- if (!projectName) {
10
- console.error('Please provide a project name:');
11
- console.error(' npx create-marp-presentation <project-name>');
12
- process.exit(1);
13
- }
12
+ const { createProject, validateProjectName, parsePathArg } = require('./cli/commands/create-project');
13
+ const { addThemesToExistingProject } = require('./cli/commands/add-themes-cli');
14
+ const { validateOutputPath } = require('./cli/utils/file-utils');
14
15
 
15
- // Валидация имени проекта
16
- const validName = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
17
- if (!validName.test(projectName)) {
18
- console.error(`Invalid project name: "${projectName}"`);
19
- console.error('Project name must be lowercase, contain only letters, numbers, and hyphens.');
20
- process.exit(1);
16
+ // Paths
17
+ const templatePath = path.join(__dirname, 'template');
18
+ const themesLibraryPath = path.join(__dirname, 'themes');
19
+
20
+ /**
21
+ * Show usage information
22
+ * @param {boolean} isError - If true, write to stderr and exit with error code
23
+ */
24
+ function showUsage(isError = false) {
25
+ const output = isError ? console.error : console.log;
26
+ output('Please provide a project name:');
27
+ output(' npx create-marp-presentation <project-name> [--path <output-dir>]');
28
+ output('');
29
+ output('Or use the theme:add command:');
30
+ output(' npx create-marp-presentation theme:add <project-path> [theme-names...]');
31
+ output('');
32
+ output('Examples:');
33
+ output(' npx create-marp-presentation my-project');
34
+ output(' npx create-marp-presentation my-project --path /tmp');
35
+ output(' npx create-marp-presentation my-project --path ~/projects');
36
+ output(' npx create-marp-presentation theme:add ./my-project');
37
+ output(' npx create-marp-presentation theme:add ./my-project beam marpx');
38
+ output('');
39
+
40
+ if (isError) {
41
+ process.exit(1);
42
+ }
21
43
  }
22
44
 
23
- const projectPath = path.join(process.cwd(), projectName);
45
+ /**
46
+ * Handle theme:add command
47
+ * @param {string[]} args - Command arguments
48
+ */
49
+ async function handleThemeAdd(args) {
50
+ const targetPath = args[0];
51
+ if (!targetPath) {
52
+ console.error('Usage: npx create-marp-presentation theme:add <project-path> [theme-names...]');
53
+ process.exit(1);
54
+ }
24
55
 
25
- // Validate path to prevent traversal attacks
26
- const resolvedPath = path.resolve(projectPath);
27
- if (!resolvedPath.startsWith(path.resolve(process.cwd()))) {
28
- console.error('Invalid project path: path traversal detected.');
29
- process.exit(1);
30
- }
56
+ const themeNames = args.slice(1); // Additional arguments are theme names
31
57
 
32
- // Проверка существования папки
33
- if (fs.existsSync(projectPath)) {
34
- console.error(`Directory "${projectName}" already exists.`);
35
- process.exit(1);
58
+ try {
59
+ await addThemesToExistingProject(targetPath, {
60
+ themesLibraryPath,
61
+ themeNames: themeNames.length > 0 ? themeNames : null
62
+ });
63
+ } catch (error) {
64
+ console.error(`Error: ${error.message}`);
65
+ process.exit(1);
66
+ }
36
67
  }
37
68
 
38
- // Получаем путь к template
39
- const templatePath = path.join(__dirname, 'template');
40
-
41
- console.log(`Creating Marp presentation: ${projectName}`);
42
- console.log();
43
-
44
- try {
45
- // Создаём папку проекта
46
- fs.mkdirSync(projectPath, { recursive: true });
47
-
48
- // Рекурсивно копируем template
49
- const copyDir = (src, dest) => {
50
- const entries = fs.readdirSync(src, { withFileTypes: true });
51
-
52
- for (const entry of entries) {
53
- const srcPath = path.join(src, entry.name);
54
- const destPath = path.join(dest, entry.name);
55
-
56
- if (entry.isDirectory()) {
57
- fs.mkdirSync(destPath, { recursive: true });
58
- copyDir(srcPath, destPath);
59
- } else {
60
- fs.copyFileSync(srcPath, destPath);
61
- }
69
+ /**
70
+ * Handle project creation command
71
+ * @param {string} projectName - Name of the project
72
+ * @param {string[]} args - Remaining arguments (e.g., --path)
73
+ */
74
+ async function handleProjectCreation(projectName, args = []) {
75
+ // Parse --path argument if present
76
+ const { pathIndex } = parsePathArg(args);
77
+ let outputPath = process.cwd();
78
+
79
+ if (pathIndex !== null) {
80
+ const pathArg = args[pathIndex + 1];
81
+ const validation = validateOutputPath(pathArg);
82
+ if (!validation.valid) {
83
+ console.error(`Invalid --path: "${pathArg}"`);
84
+ console.error(validation.error);
85
+ process.exit(1);
62
86
  }
63
- };
64
-
65
- copyDir(templatePath, projectPath);
66
-
67
- console.log('✓ Project created');
68
- console.log();
69
-
70
- // Запускаем npm install
71
- console.log('Installing dependencies...');
72
- const installResult = spawnSync('npm', ['install'], {
73
- cwd: projectPath,
74
- stdio: 'inherit',
75
- });
87
+ outputPath = validation.resolvedPath;
88
+ }
76
89
 
77
- if (installResult.status !== 0) {
78
- console.error();
79
- console.error('Failed to install dependencies.');
80
- console.error('Please run "cd ' + projectName + ' && npm install" manually.');
90
+ try {
91
+ await createProject(projectName, {
92
+ outputPath,
93
+ templatePath,
94
+ themesLibraryPath
95
+ });
96
+ } catch (error) {
97
+ if (error.message === 'Invalid project name' || error.message === 'Project directory already exists') {
98
+ process.exit(1);
99
+ }
100
+ console.error('Error creating project:', error.message);
81
101
  process.exit(1);
82
102
  }
103
+ }
83
104
 
84
- console.log();
85
- console.log('✓ Dependencies installed');
86
- console.log();
87
- console.log('Next steps:');
88
- console.log(` cd ${projectName}`);
89
- console.log(' npm run dev # Start live preview');
90
- console.log(' npm run build:all # Build all formats');
91
- console.log();
92
-
93
- } catch (err) {
94
- console.error('Error creating project:', err.message);
95
- process.exit(1);
105
+ /**
106
+ * Main entry point
107
+ */
108
+ async function main() {
109
+ const [command, ...args] = process.argv.slice(2);
110
+
111
+ switch (command) {
112
+ case 'theme:add':
113
+ await handleThemeAdd(args);
114
+ break;
115
+
116
+ case undefined:
117
+ showUsage(true); // Exit with error code 1
118
+ break;
119
+
120
+ default:
121
+ // Treat as project name (backward compatible)
122
+ await handleProjectCreation(command, args);
123
+ break;
124
+ }
96
125
  }
126
+
127
+ // Run
128
+ main().catch(error => {
129
+ console.error('Unexpected error:', error.message);
130
+ process.exit(1);
131
+ });
@@ -0,0 +1,381 @@
1
+ // lib/add-themes-command.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { ThemeResolver, Theme } = require('./theme-resolver');
5
+ const { VSCodeIntegration, VSCodeSettingsDTO } = require('./vscode-integration');
6
+ const { ThemeManager } = require('./theme-manager');
7
+ const { Prompts } = require('./prompts');
8
+ const {
9
+ ThemeError,
10
+ ThemeNotFoundError,
11
+ ThemeAlreadyExistsError
12
+ } = require('./errors');
13
+
14
+ /**
15
+ * Shared function for adding Marp themes to a project
16
+ * Handles theme copying, dependency resolution, conflict detection, and VSCode sync
17
+ */
18
+ class AddThemesCommand {
19
+ /**
20
+ * @param {Object} options - Configuration options
21
+ * @param {string} options.templatePath - Path to themes library directory (optional, defaults to __dirname/../themes)
22
+ * @param {boolean} options.interactive - Enable interactive prompts (default: true)
23
+ * @param {Object} options.prompts - Prompt functions for testing (optional)
24
+ */
25
+ constructor(options = {}) {
26
+ this.options = {
27
+ interactive: options.interactive !== false,
28
+ templatePath: options.templatePath || path.join(__dirname, '..', 'themes'),
29
+ ...options
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Returns the path to the themes library directory
35
+ * @returns {string} Absolute path to themes library
36
+ */
37
+ getTemplateThemesPath() {
38
+ // templatePath now points directly to the themes library
39
+ return this.options.templatePath;
40
+ }
41
+
42
+ /**
43
+ * Main execution method - adds themes to target project
44
+ *
45
+ * @param {string} targetPath - Path to target project directory
46
+ * @param {Object} options - Execution options
47
+ * @param {string[]} options.themes - Theme names to add (optional, prompts if not provided)
48
+ * @param {string[]} options.skip - Theme names to skip conflicts (optional)
49
+ * @param {boolean} options.force - Overwrite existing themes (default: false)
50
+ * @param {boolean} options.noVscode - Skip VSCode settings sync (default: false)
51
+ * @returns {Promise<Object>} Result object with copied, skipped, and conflicts arrays
52
+ */
53
+ async execute(targetPath, options = {}) {
54
+ const execOptions = {
55
+ themes: options.themes || null,
56
+ skip: options.skip || [],
57
+ force: options.force || false,
58
+ noVscode: options.noVscode || false,
59
+ ...options
60
+ };
61
+
62
+ // Validate target path
63
+ if (!fs.existsSync(targetPath)) {
64
+ throw new ThemeError(`Target path does not exist: ${targetPath}`);
65
+ }
66
+
67
+ const templateThemesPath = this.getTemplateThemesPath();
68
+
69
+ // Scan available themes from themes library
70
+ if (!fs.existsSync(templateThemesPath)) {
71
+ throw new ThemeError(`Themes library directory not found: ${templateThemesPath}`);
72
+ }
73
+
74
+ const availableThemes = ThemeResolver.scanDirectory(templateThemesPath);
75
+
76
+ if (availableThemes.length === 0) {
77
+ throw new ThemeError('No themes found in themes library');
78
+ }
79
+
80
+ // Determine which themes to copy
81
+ let themesToCopy;
82
+ if (execOptions.themes != null) {
83
+ // themes explicitly provided (even if empty array = copy nothing)
84
+ if (execOptions.themes.length > 0) {
85
+ themesToCopy = this._resolveThemeNames(execOptions.themes, availableThemes);
86
+ } else {
87
+ // Empty array means user chose no themes
88
+ themesToCopy = [];
89
+ }
90
+ } else if (this.options.interactive) {
91
+ // Interactive mode - prompt user using built-in Prompts.promptThemes()
92
+ const selectedNames = await this._promptThemes(availableThemes);
93
+ // Filter availableThemes to only include selected ones
94
+ themesToCopy = availableThemes.filter(t => selectedNames.includes(t.name));
95
+ } else {
96
+ // Non-interactive without explicit themes: copy all available themes
97
+ themesToCopy = availableThemes;
98
+ }
99
+
100
+ // Resolve dependencies
101
+ const themesWithDependencies = ThemeResolver.resolveDependencies(
102
+ themesToCopy,
103
+ availableThemes
104
+ );
105
+
106
+ // Scan existing themes in project
107
+ const existingThemes = this._scanProjectThemes(targetPath);
108
+
109
+ // Find conflicts
110
+ const conflicts = this._findConflicts(themesWithDependencies, existingThemes);
111
+
112
+ // Handle conflicts
113
+ let skipList = new Set(execOptions.skip);
114
+
115
+ if (conflicts.length > 0 && !execOptions.force) {
116
+ if (this.options.interactive) {
117
+ const resolvedConflicts = await this._resolveConflictsInteractive(
118
+ conflicts,
119
+ execOptions.force
120
+ );
121
+ // Add overwrite choices to skip list (inverted logic - skip if not overwriting)
122
+ conflicts.forEach(conflict => {
123
+ if (!resolvedConflicts.overwrite.includes(conflict)) {
124
+ skipList.add(conflict);
125
+ }
126
+ });
127
+ } else {
128
+ // Non-interactive: skip all conflicts
129
+ conflicts.forEach(conflict => skipList.add(conflict));
130
+ }
131
+ }
132
+
133
+ // Copy themes
134
+ const copyResult = this.copyThemes(
135
+ themesWithDependencies,
136
+ templateThemesPath,
137
+ targetPath,
138
+ Array.from(skipList)
139
+ );
140
+
141
+ // Sync VSCode settings
142
+ if (!execOptions.noVscode && copyResult.copied.length > 0) {
143
+ this._syncVscodeSettings(targetPath, copyResult.copied);
144
+ }
145
+
146
+ // Ensure marp.themeSet is configured in package.json
147
+ if (copyResult.copied.length > 0) {
148
+ ThemeManager.ensureThemeSetConfig(targetPath, { silent: true });
149
+ }
150
+
151
+ // Return result with conflicts included
152
+ return {
153
+ ...copyResult,
154
+ conflicts
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Copy themes from themes library to target project
160
+ *
161
+ * @param {Theme[]} themes - Theme objects to copy
162
+ * @param {string} templatePath - Path to themes library directory
163
+ * @param {string} targetPath - Path to target project directory
164
+ * @param {string[]} skipList - Theme names to skip
165
+ * @returns {Object} Result with copied, skipped arrays
166
+ */
167
+ copyThemes(themes, templatePath, targetPath, skipList = []) {
168
+ const skipSet = new Set(skipList);
169
+ const copied = [];
170
+ const skipped = [];
171
+
172
+ // Ensure target themes directory exists
173
+ const targetThemesDir = path.join(targetPath, 'themes');
174
+ if (!fs.existsSync(targetThemesDir)) {
175
+ fs.mkdirSync(targetThemesDir, { recursive: true });
176
+ }
177
+
178
+ for (const theme of themes) {
179
+ if (skipSet.has(theme.name)) {
180
+ skipped.push(theme.name);
181
+ continue;
182
+ }
183
+
184
+ // Calculate relative path from themes library
185
+ const relativePath = path.relative(templatePath, theme.path);
186
+ const targetFilePath = path.join(targetThemesDir, relativePath);
187
+
188
+ // Ensure target directory exists
189
+ const targetDir = path.dirname(targetFilePath);
190
+ if (!fs.existsSync(targetDir)) {
191
+ fs.mkdirSync(targetDir, { recursive: true });
192
+ }
193
+
194
+ // Copy file
195
+ fs.copyFileSync(theme.path, targetFilePath);
196
+ copied.push(theme); // Return Theme object, not just name
197
+ }
198
+
199
+ return { copied, skipped };
200
+ }
201
+
202
+ /**
203
+ * Scan project themes directory for existing theme files
204
+ * @private
205
+ *
206
+ * @param {string} projectPath - Path to project directory
207
+ * @returns {Theme[]} Array of existing theme objects
208
+ */
209
+ _scanProjectThemes(projectPath) {
210
+ const themesPath = path.join(projectPath, 'themes');
211
+
212
+ if (!fs.existsSync(themesPath)) {
213
+ return [];
214
+ }
215
+
216
+ try {
217
+ return ThemeResolver.scanDirectory(themesPath);
218
+ } catch (error) {
219
+ // If scan fails, return empty array
220
+ return [];
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Find conflicts between themes to copy and existing themes
226
+ * @private
227
+ *
228
+ * @param {Theme[]} themesToCopy - Themes that will be copied
229
+ * @param {Theme[]} existingThemes - Themes that already exist
230
+ * @returns {string[]} Array of conflicting theme names
231
+ */
232
+ _findConflicts(themesToCopy, existingThemes) {
233
+ const existingNames = new Set(existingThemes.map(t => t.name));
234
+ const conflicts = [];
235
+
236
+ for (const theme of themesToCopy) {
237
+ if (existingNames.has(theme.name)) {
238
+ conflicts.push(theme.name);
239
+ }
240
+ }
241
+
242
+ return conflicts;
243
+ }
244
+
245
+ /**
246
+ * Resolve theme names to Theme objects
247
+ * @private
248
+ *
249
+ * @param {string[]} themeNames - Theme names to resolve
250
+ * @param {Theme[]} availableThemes - Available themes from themes library
251
+ * @returns {Theme[]} Resolved theme objects
252
+ * @throws {ThemeNotFoundError} If a theme name is not found
253
+ */
254
+ _resolveThemeNames(themeNames, availableThemes) {
255
+ const themesMap = new Map();
256
+ for (const theme of availableThemes) {
257
+ themesMap.set(theme.name, theme);
258
+ }
259
+
260
+ const resolved = [];
261
+ const notFound = [];
262
+
263
+ for (const name of themeNames) {
264
+ const theme = themesMap.get(name);
265
+ if (theme) {
266
+ resolved.push(theme);
267
+ } else {
268
+ notFound.push(name);
269
+ }
270
+ }
271
+
272
+ if (notFound.length > 0) {
273
+ const message = notFound.length === 1
274
+ ? notFound[0]
275
+ : `${notFound.join(', ')}`;
276
+ throw new ThemeNotFoundError(message);
277
+ }
278
+
279
+ return resolved;
280
+ }
281
+
282
+ /**
283
+ * Prompt user to select themes interactively
284
+ * Uses Prompts.promptThemes() for built-in theme selection
285
+ * @private
286
+ *
287
+ * @param {Theme[]} availableThemes - Available themes
288
+ * @returns {Promise<string[]>} Selected theme names
289
+ */
290
+ async _promptThemes(availableThemes) {
291
+ // Use built-in Prompts.promptThemes() for interactive selection
292
+ return await Prompts.promptThemes(availableThemes);
293
+ }
294
+
295
+ /**
296
+ * Resolve conflicts interactively
297
+ * @private
298
+ *
299
+ * @param {string[]} conflicts - Conflicting theme names
300
+ * @param {boolean} force - Force overwrite all
301
+ * @returns {Promise<Object>} Resolution result with overwrite array
302
+ */
303
+ async _resolveConflictsInteractive(conflicts, force) {
304
+ if (force) {
305
+ return { overwrite: conflicts };
306
+ }
307
+
308
+ // Use custom prompt if provided (backward compatibility for tests)
309
+ if (this.options.prompts?.resolveConflicts) {
310
+ return await this.options.prompts.resolveConflicts(conflicts);
311
+ }
312
+
313
+ // Use built-in Prompts.promptConflictResolution
314
+ const result = await Prompts.promptConflictResolution(
315
+ conflicts.map(c => ({ name: c }))
316
+ );
317
+
318
+ if (result === 'skip-all') return { overwrite: [] };
319
+ if (result === 'overwrite-all') return { overwrite: conflicts };
320
+ if (result === 'cancel') return { overwrite: [] };
321
+
322
+ // Handle 'choose-each' or individual conflicts
323
+ const overwrite = [];
324
+ for (const conflict of conflicts) {
325
+ const choice = await Prompts.promptSingleConflict(conflict);
326
+ if (choice === 'overwrite') {
327
+ overwrite.push(conflict);
328
+ } else if (choice === 'cancel') {
329
+ break;
330
+ }
331
+ }
332
+ return { overwrite };
333
+ }
334
+
335
+ /**
336
+ * Sync themes to VSCode settings
337
+ * @private
338
+ *
339
+ * @param {string} projectPath - Path to project directory
340
+ * @param {string[]} copiedThemes - Names of copied themes
341
+ */
342
+ _syncVscodeSettings(projectPath, copiedThemes) {
343
+ const vscode = new VSCodeIntegration(projectPath);
344
+
345
+ // Use DTO for type-safe access
346
+ const dto = vscode.createSettingsDTO();
347
+ const existingThemes = dto.getMarpThemes();
348
+
349
+ // Add new theme paths (relative to themes/)
350
+ // copiedThemes are Theme objects - use their paths to compute relative paths
351
+ const newThemePaths = copiedThemes.map(theme => {
352
+ // theme.path is absolute, we need relative path from themes directory
353
+ // e.g., /library/themes/marpx/socrates.css -> themes/marpx/socrates.css
354
+ const filename = path.basename(theme.path);
355
+ const themeName = path.basename(path.dirname(theme.path));
356
+ // For single-file themes: themes/themeName/themeName.css
357
+ // For themes in subdirs: themes/subdir/themeName.css
358
+ return `themes/${themeName}/${filename}`;
359
+ });
360
+
361
+ // Merge without duplicates
362
+ const allThemes = [...new Set([...existingThemes, ...newThemePaths])];
363
+
364
+ dto.setMarpThemes(allThemes);
365
+ vscode.writeSettings(dto.toObject());
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Convenience function to create and execute AddThemesCommand
371
+ *
372
+ * @param {string} targetPath - Path to target project directory
373
+ * @param {Object} options - Options passed to AddThemesCommand
374
+ * @returns {Promise<Object>} Result object from execute()
375
+ */
376
+ async function addThemesCommand(targetPath, options = {}) {
377
+ const command = new AddThemesCommand(options);
378
+ return await command.execute(targetPath, options);
379
+ }
380
+
381
+ module.exports = { AddThemesCommand, addThemesCommand };