vk-typescript-cli 1.0.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.
Files changed (119) hide show
  1. package/Jenkinsfile +48 -0
  2. package/README.md +300 -0
  3. package/bin/sf.js +438 -0
  4. package/eslint.config.js +90 -0
  5. package/gulpfile.js +157 -0
  6. package/lib/commander.js +3370 -0
  7. package/lib/inquirer-prompts.js +2038 -0
  8. package/license +13 -0
  9. package/package.json +43 -0
  10. package/smoke-test.mjs +38 -0
  11. package/src/commands/add.js +276 -0
  12. package/src/commands/info.js +82 -0
  13. package/src/commands/interactive.js +493 -0
  14. package/src/commands/mcp.js +135 -0
  15. package/src/commands/new.js +264 -0
  16. package/src/commands/skills.js +42 -0
  17. package/src/commands/theme.js +200 -0
  18. package/src/commands/upgrade.js +219 -0
  19. package/src/constants.js +161 -0
  20. package/src/generators/angular.js +878 -0
  21. package/src/generators/nextjs.js +817 -0
  22. package/src/generators/react.js +769 -0
  23. package/src/generators/typescript.js +258 -0
  24. package/src/generators/vue.js +800 -0
  25. package/src/templates/angular/chart.component.html +23 -0
  26. package/src/templates/angular/chart.component.ts +172 -0
  27. package/src/templates/angular/diagram.component.html +13 -0
  28. package/src/templates/angular/diagram.component.ts +224 -0
  29. package/src/templates/angular/docx-editor.component.html +18 -0
  30. package/src/templates/angular/docx-editor.component.ts +37 -0
  31. package/src/templates/angular/filemanager.component.html +4 -0
  32. package/src/templates/angular/filemanager.component.ts +47 -0
  33. package/src/templates/angular/gantt.component.html +32 -0
  34. package/src/templates/angular/gantt.component.ts +95 -0
  35. package/src/templates/angular/grid.component.html +12 -0
  36. package/src/templates/angular/grid.component.ts +58 -0
  37. package/src/templates/angular/pdf-viewer.component.html +9 -0
  38. package/src/templates/angular/pdf-viewer.component.ts +48 -0
  39. package/src/templates/angular/rte.component.html +7 -0
  40. package/src/templates/angular/rte.component.ts +13 -0
  41. package/src/templates/angular/scheduler.component.html +17 -0
  42. package/src/templates/angular/scheduler.component.ts +75 -0
  43. package/src/templates/angular/spreadsheet-editor.component.html +42 -0
  44. package/src/templates/angular/spreadsheet-editor.component.ts +61 -0
  45. package/src/templates/chart-template.js +20 -0
  46. package/src/templates/diagram-template.js +26 -0
  47. package/src/templates/docx-template.js +20 -0
  48. package/src/templates/filemanager-template.js +20 -0
  49. package/src/templates/gantt-template.js +20 -0
  50. package/src/templates/grid-template.js +20 -0
  51. package/src/templates/pdfviewer-template.js +20 -0
  52. package/src/templates/react/Chart.jsx +81 -0
  53. package/src/templates/react/Chart.tsx +81 -0
  54. package/src/templates/react/DOCXEditor.jsx +37 -0
  55. package/src/templates/react/DOCXEditor.tsx +37 -0
  56. package/src/templates/react/Diagram.jsx +192 -0
  57. package/src/templates/react/Diagram.tsx +195 -0
  58. package/src/templates/react/FileManager.jsx +13 -0
  59. package/src/templates/react/FileManager.tsx +13 -0
  60. package/src/templates/react/Gantt.jsx +89 -0
  61. package/src/templates/react/Gantt.tsx +89 -0
  62. package/src/templates/react/Grid.jsx +45 -0
  63. package/src/templates/react/Grid.tsx +45 -0
  64. package/src/templates/react/PDFViewer.jsx +16 -0
  65. package/src/templates/react/PDFViewer.tsx +16 -0
  66. package/src/templates/react/RTE.jsx +14 -0
  67. package/src/templates/react/RTE.tsx +14 -0
  68. package/src/templates/react/Scheduler.jsx +55 -0
  69. package/src/templates/react/Scheduler.tsx +55 -0
  70. package/src/templates/react/SpreadsheetEditor.jsx +96 -0
  71. package/src/templates/react/SpreadsheetEditor.tsx +98 -0
  72. package/src/templates/rte-template.js +26 -0
  73. package/src/templates/scheduler-template.js +20 -0
  74. package/src/templates/spreadheet-template.js +20 -0
  75. package/src/templates/typescript/RTE.ts +33 -0
  76. package/src/templates/typescript/SpreadsheetEditor.ts +39 -0
  77. package/src/templates/typescript/chart.ts +37 -0
  78. package/src/templates/typescript/diagram.ts +30 -0
  79. package/src/templates/typescript/docxeditor.ts +30 -0
  80. package/src/templates/typescript/filemanager.ts +41 -0
  81. package/src/templates/typescript/gantt.ts +88 -0
  82. package/src/templates/typescript/grid.ts +30 -0
  83. package/src/templates/typescript/pdfviewer.ts +8 -0
  84. package/src/templates/typescript/scheduler.ts +39 -0
  85. package/src/templates/vue/Chart.vue +257 -0
  86. package/src/templates/vue/ChartView.vue +7 -0
  87. package/src/templates/vue/DOCXEditor.vue +61 -0
  88. package/src/templates/vue/DOCXEditorView.vue +7 -0
  89. package/src/templates/vue/Diagram.vue +245 -0
  90. package/src/templates/vue/DiagramView.vue +7 -0
  91. package/src/templates/vue/FileManager.vue +58 -0
  92. package/src/templates/vue/FileManagerView.vue +7 -0
  93. package/src/templates/vue/Gantt.vue +129 -0
  94. package/src/templates/vue/GanttView.vue +7 -0
  95. package/src/templates/vue/Grid.vue +141 -0
  96. package/src/templates/vue/GridView.vue +7 -0
  97. package/src/templates/vue/PDFViewer.vue +58 -0
  98. package/src/templates/vue/PDFViewerView.vue +7 -0
  99. package/src/templates/vue/RTE.vue +27 -0
  100. package/src/templates/vue/RTEView.vue +7 -0
  101. package/src/templates/vue/Scheduler.vue +94 -0
  102. package/src/templates/vue/SchedulerView.vue +7 -0
  103. package/src/templates/vue/SpreadsheetEditor.vue +115 -0
  104. package/src/templates/vue/SpreadsheetEditorView.vue +7 -0
  105. package/src/utils/ansi-colors.js +38 -0
  106. package/src/utils/banner.js +179 -0
  107. package/src/utils/common.js +64 -0
  108. package/src/utils/config-generator.js +73 -0
  109. package/src/utils/folder-structure.js +26 -0
  110. package/src/utils/logger.js +19 -0
  111. package/src/utils/mcp-generator.js +122 -0
  112. package/src/utils/package-manager.js +128 -0
  113. package/src/utils/project-bootstrap.js +98 -0
  114. package/src/utils/project-detector.js +195 -0
  115. package/src/utils/prompt-utils.js +29 -0
  116. package/src/utils/run-command.js +110 -0
  117. package/src/utils/skills-installer.js +69 -0
  118. package/src/utils/template-file.js +35 -0
  119. package/src/utils/theme-loader.js +47 -0
@@ -0,0 +1,264 @@
1
+ import path from 'node:path';
2
+ import { select } from '../../lib/inquirer-prompts.js';
3
+ import { TEMPLATES } from '../constants.js';
4
+ import { createReactProject } from '../generators/react.js';
5
+ import { createNextJsProject } from '../generators/nextjs.js';
6
+ import { createAngularProject } from '../generators/angular.js';
7
+ import { createVueProject } from '../generators/vue.js';
8
+ import { createTypeScriptProject } from '../generators/typescript.js';
9
+ import { logger } from '../utils/logger.js';
10
+ import {
11
+ applyMcpIntegration
12
+ } from '../utils/mcp-generator.js';
13
+ import {
14
+ installSkills,
15
+ promptSkillsInstallMode
16
+ } from '../utils/skills-installer.js';
17
+ import { updateSyncfusionConfig } from '../utils/config-generator.js';
18
+ import { promptAndBootstrap } from '../utils/project-bootstrap.js';
19
+ import { safePrompt } from '../utils/prompt-utils.js';
20
+
21
+ async function promptMcpEditorSelection() {
22
+ const editor = await safePrompt(select, {
23
+ message: 'Which editor do you want to add MCP configuration for?',
24
+ choices: [
25
+ { name: 'All Editors', value: 'all' },
26
+ { name: 'VS Code', value: 'vscode' },
27
+ { name: 'Code Studio', value: 'code-studio' },
28
+ { name: 'Cursor', value: 'cursor' },
29
+ { name: 'JetBrains', value: 'jetbrains' }
30
+ ],
31
+ default: 'all'
32
+ });
33
+
34
+ if (!editor) {
35
+ logger.warning('\n⚠️ MCP configuration cancelled.\n');
36
+ return [];
37
+ }
38
+
39
+ return [editor];
40
+ }
41
+
42
+ export async function createNewCommand(framework, appName, options = {}) {
43
+ const appPath = path.resolve(process.cwd(), appName);
44
+
45
+ try {
46
+ // Validate inputs
47
+ if (!appName || appName.trim() === '') {
48
+ logger.error('Application name is required');
49
+ process.exit(1);
50
+ }
51
+
52
+ // Normalize framework name (remove -app suffix if present)
53
+ const normalizedFramework = (framework || options.framework || 'react').toLowerCase();
54
+
55
+ const requestedType = (options.ts ? 'ts' : options.js ? 'js' : options.type || options.language || 'ts').toLowerCase();
56
+ const typeAliases = { ts: 'typescript', js: 'javascript' };
57
+ const type = typeAliases[requestedType] || requestedType;
58
+ const template = (options.template || TEMPLATES.EMPTY).toLowerCase();
59
+ const theme = (options.theme || 'material3').toLowerCase();
60
+ const buildTool = (options.buildTool || 'vite').toLowerCase();
61
+ const styling = (options.styling || 'css').toLowerCase();
62
+
63
+ // Determine which generator to use
64
+ let generator;
65
+ switch (normalizedFramework) {
66
+ case 'react':
67
+ if (buildTool === 'nextjs') {
68
+ generator = createNextJsProject;
69
+ } else {
70
+ generator = createReactProject;
71
+ }
72
+ break;
73
+
74
+ case 'angular':
75
+ generator = createAngularProject;
76
+ break;
77
+ case 'vue':
78
+ generator = createVueProject;
79
+ break;
80
+ case 'typescript':
81
+ generator = createTypeScriptProject;
82
+ break;
83
+
84
+ default:
85
+ logger.error(`Unsupported framework: ${normalizedFramework}`);
86
+ logger.info('Supported frameworks: react, angular, vue, typescript');
87
+ process.exit(1);
88
+ }
89
+
90
+ // Validate buildTool (only for React)
91
+ if (normalizedFramework === 'react') {
92
+ const validBuildTools = ['vite', 'nextjs'];
93
+ if (!validBuildTools.includes(buildTool)) {
94
+ logger.error(`Invalid build tool: ${buildTool}`);
95
+ logger.info(`Supported build tools: ${validBuildTools.join(', ')}`);
96
+ process.exit(1);
97
+ }
98
+ } else if (normalizedFramework === 'typescript') {
99
+ // TypeScript platform has no separate build-tool option (Vite is hard-wired).
100
+ buildTool = null;
101
+ }
102
+
103
+ // Validate styling
104
+ if (!['css', 'scss'].includes(styling)) {
105
+ logger.error(`Invalid styling: ${styling}`);
106
+ logger.info('Supported styling: css, scss');
107
+ process.exit(1);
108
+ }
109
+
110
+ // Validate template
111
+ if (!Object.values(TEMPLATES).includes(template)) {
112
+ logger.error(`Invalid template: ${template}`);
113
+ logger.info(`Supported templates: ${Object.values(TEMPLATES).join(', ')}`);
114
+ process.exit(1);
115
+ }
116
+
117
+ // Validate type
118
+ if (normalizedFramework === 'typescript') {
119
+ // The TypeScript platform is always TypeScript.
120
+ if (type !== 'typescript' && type !== 'ts') {
121
+ logger.error('TypeScript platform only supports TypeScript (ts)');
122
+ process.exit(1);
123
+ }
124
+ } else if (normalizedFramework !== 'angular') {
125
+ if (!['javascript', 'typescript'].includes(type)) {
126
+ logger.error(`Invalid type: ${type}`);
127
+ logger.info('Supported types: js, ts');
128
+ process.exit(1);
129
+ }
130
+ }
131
+
132
+ // Validate theme
133
+ const validThemes = ['tailwind3', 'bootstrap5.3', 'fluent2', 'material3'];
134
+ if (!validThemes.includes(theme)) {
135
+ logger.warning(`Unknown theme: ${theme}, using material3 instead`);
136
+ }
137
+
138
+ // Merge all options
139
+ const mergedOptions = {
140
+ framework: normalizedFramework,
141
+ type,
142
+ buildTool: normalizedFramework === 'react' ? buildTool : null,
143
+ appType: type === 'typescript' ? 'ts' : 'js',
144
+ template,
145
+ theme: validThemes.includes(theme) ? theme : 'material3',
146
+ styling,
147
+ empty: template === TEMPLATES.EMPTY,
148
+ ...options
149
+ };
150
+
151
+ // For the TypeScript platform, ensure the build tool is always null
152
+ // (it always uses Vite internally and we don't expose a separate flag).
153
+ if (normalizedFramework === 'typescript') {
154
+ mergedOptions.buildTool = null;
155
+ mergedOptions.appType = 'ts';
156
+ mergedOptions.type = 'typescript';
157
+ }
158
+
159
+ let mcpResult;
160
+ if ((options.mcp ?? options.mcp) === true) {
161
+ mcpResult = {
162
+ deferred: true,
163
+ editors: options.mcpEditors || ['all']
164
+ };
165
+ } else {
166
+ mcpResult = { integrated: false, editors: [] };
167
+ }
168
+
169
+ let skillsResult;
170
+ if ((options.skills ?? options.skills) === true) {
171
+ skillsResult = { deferred: true };
172
+ } else {
173
+ skillsResult = { installed: false };
174
+ }
175
+
176
+ let bootstrapOptions = { skip: false, force: false, deferred: false};
177
+ if (options.noStart) {
178
+ bootstrapOptions = { skip: true, force: false, deferred: true };
179
+ } else if (options.start) {
180
+ bootstrapOptions = { skip: false, force: true, deferred: true };
181
+ } else if (options.yes) {
182
+ bootstrapOptions = { skip: true, force: false, deferred: true };
183
+ }
184
+
185
+ logger.info(`\n🚀 Creating Syncfusion ${normalizedFramework} project...\n`);
186
+
187
+ await generator(appName, appPath, mergedOptions);
188
+
189
+ // Ask after project creation and immediately before applying MCP.
190
+ if (mcpResult?.deferred && options.promptMcpEditor) {
191
+ const selectedEditors = await promptMcpEditorSelection();
192
+ if (selectedEditors.length === 0) {
193
+ mcpResult = { integrated: false, editors: [] };
194
+ } else {
195
+ mcpResult.editors = selectedEditors;
196
+ }
197
+ }
198
+
199
+ if (mcpResult?.deferred || mcpResult?.integrated) {
200
+ const editors = mcpResult.editors || ['all'];
201
+ mcpResult = await applyMcpIntegration(
202
+ appPath,
203
+ mergedOptions.framework,
204
+ editors
205
+ );
206
+ }
207
+
208
+ if (skillsResult?.deferred || skillsResult?.installed) {
209
+ let allSkills = true;
210
+ if (options.promptSkillsInstallMode) {
211
+ const skillsMode = await promptSkillsInstallMode();
212
+ if (!skillsMode) {
213
+ skillsResult = { installed: false };
214
+ } else {
215
+ allSkills = skillsMode === 'all';
216
+ const installed = await installSkills(appPath, mergedOptions.framework, { allSkills });
217
+ skillsResult = { installed };
218
+ }
219
+ } else {
220
+ const installed = await installSkills(appPath, mergedOptions.framework, { allSkills });
221
+ skillsResult = { installed };
222
+ }
223
+ }
224
+
225
+ try {
226
+ await updateSyncfusionConfig(appPath, { mcp: mcpResult, skills: skillsResult });
227
+ } catch (error) {
228
+ logger.warning(
229
+ `⚠️ Failed to update syncfusion.config.json: ${error.message}`
230
+ );
231
+ }
232
+ // Success message
233
+ logger.success('\n✅ Project created successfully!\n');
234
+ logger.info(`📁 Project Name: ${appName}`);
235
+ logger.info(`🎯 Framework: ${mergedOptions.framework}`);
236
+ if (mergedOptions.buildTool) {
237
+ logger.info(`🔧 Build Tool: ${mergedOptions.buildTool}`);
238
+ }
239
+ logger.info(`📝 Type: ${mergedOptions.type}`);
240
+ logger.info(`🎨 Template: ${mergedOptions.template}`);
241
+ logger.info(`🌈 Theme: ${mergedOptions.theme}`);
242
+ logger.info(`💅 Styling: ${mergedOptions.styling.toUpperCase()}`);
243
+ if (mcpResult?.integrated) {
244
+ logger.info('🤖 MCP: Added');
245
+ }
246
+
247
+ if (skillsResult?.installed) {
248
+ logger.info('🧠 Skills: Added');
249
+ }
250
+ if (bootstrapOptions.deferred) {
251
+ await promptAndBootstrap(
252
+ appPath,
253
+ appName,
254
+ mergedOptions.framework,
255
+ mergedOptions.buildTool,
256
+ { skip: bootstrapOptions.skip, force: bootstrapOptions.force }
257
+ );
258
+ }
259
+ } catch (error) {
260
+ logger.error(`❌ Failed to create project: ${error.message}`);
261
+ console.error(error);
262
+ process.exit(1);
263
+ }
264
+ }
@@ -0,0 +1,42 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { logger } from '../utils/logger.js';
4
+ import { requireSyncfusionProject } from '../utils/project-detector.js';
5
+ import { updateSyncfusionConfig } from '../utils/config-generator.js';
6
+ import { installSkills, promptSkillsInstallMode } from '../utils/skills-installer.js';
7
+
8
+ /**
9
+ * `sf skills install` — install Syncfusion component skills for AI
10
+ * coding skills into the current project.
11
+ */
12
+ export async function skillsAddCommand(options = {}) {
13
+ const project = await requireSyncfusionProject(process.cwd());
14
+ if (!project) process.exit(1);
15
+
16
+ const configPath = path.join(project.root, 'syncfusion.config.json');
17
+ const config = await readFile(configPath, 'utf8').then(JSON.parse).catch(() => ({}));
18
+
19
+ if (config.skills?.installed === true) {
20
+ logger.info('\nℹ️ Skills already added. Skipping.\n');
21
+ return;
22
+ }
23
+ let allSkills = true;
24
+ if (options.promptInstallMode) {
25
+ const mode = await promptSkillsInstallMode();
26
+ if (!mode) return;
27
+ allSkills = mode === 'all';
28
+ }
29
+
30
+ const installed = await installSkills(project.root, project.framework, { allSkills });
31
+
32
+ try {
33
+ await updateSyncfusionConfig(project.root, {
34
+ skills: { installed }
35
+ });
36
+ } catch (error) {
37
+ logger.warning(
38
+ `⚠️ Failed to update syncfusion.config.json: ${error.message}`
39
+ );
40
+ }
41
+
42
+ }
@@ -0,0 +1,200 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { select } from '../../lib/inquirer-prompts.js';
4
+ import { runCommand } from '../utils/run-command.js';
5
+ import { logger } from '../utils/logger.js';
6
+ import { requireSyncfusionProject } from '../utils/project-detector.js';
7
+ import { updateSyncfusionConfig } from '../utils/config-generator.js';
8
+ import { addPackagesToJson, detectPackageManager, getInstallCommand } from '../utils/package-manager.js';
9
+ import { getThemePackages, generateThemeImportStatement, generateMainThemeImportStatement } from '../utils/theme-loader.js';
10
+ import { VALID_THEMES } from '../constants.js';
11
+ import { safePrompt } from '../utils/prompt-utils.js';
12
+ import { pathExists } from '../utils/common.js';
13
+
14
+ // Regex for SCSS/CSS @import/@use statements
15
+ const THEME_IMPORT_LINE = /^(@import|@use)\s+["']@syncfusion\/ej2-[\w.-]+-theme\/styles\/[\w.-]+\.(css|scss)["'].*$/m;
16
+
17
+ // Regex for JavaScript import statements in TypeScript/JSX files
18
+ const JS_IMPORT_LINE = /^import\s+["']@syncfusion\/ej2-[\w.-]+-theme\/styles\/[\w.-]+\.(css|scss)["'];?$/m;
19
+
20
+ function resolveStyleFilePath(project) {
21
+ const { root, framework, buildTool, template, styling } = project;
22
+ const ext = styling === 'scss' ? 'scss' : 'css';
23
+ const isEmpty = template === 'empty' || (project.components || []).length === 0;
24
+
25
+ if (framework === 'angular') {
26
+ return path.join(root, 'src', `styles.${ext}`);
27
+ }
28
+
29
+ if (framework === 'vue') {
30
+ return path.join(root, 'src', `style.${ext}`);
31
+ }
32
+
33
+ if (framework === 'typescript') {
34
+ return path.join(root, 'src', `style.${ext}`);
35
+ }
36
+
37
+ if (buildTool === 'nextjs') {
38
+ return path.join(root, 'src', 'app', 'globals.css');
39
+ }
40
+
41
+ // React (Vite)
42
+ return isEmpty
43
+ ? path.join(root, 'src', styling === 'scss' ? 'styles.scss' : 'App.css')
44
+ : path.join(root, 'src', styling === 'scss' ? 'styles.scss' : 'App.css');
45
+ }
46
+
47
+ /**
48
+ * For Next.js routed templates, get the layout.tsx file path
49
+ */
50
+ function getNextJsLayoutPath(project) {
51
+ return path.join(project.root, 'src', 'app', 'layout.tsx');
52
+ }
53
+
54
+ /**
55
+ * Swap theme import in SCSS/CSS files (empty templates)
56
+ */
57
+ async function swapThemeImportInStyleFile(filePath, newImportStatement) {
58
+ if (!(await pathExists(filePath))) {
59
+ logger.warning(` ⚠️ Style file not found: ${path.relative(process.cwd(), filePath)}`);
60
+ return false;
61
+ }
62
+
63
+ let content = await readFile(filePath, 'utf-8');
64
+
65
+ if (THEME_IMPORT_LINE.test(content)) {
66
+ content = content.replace(THEME_IMPORT_LINE, newImportStatement);
67
+ } else {
68
+ content = `${newImportStatement}\n\n${content}`;
69
+ }
70
+
71
+ await writeFile(filePath, content);
72
+ return true;
73
+ }
74
+
75
+ /**
76
+ * Swap theme import in layout.tsx file (routed templates)
77
+ * Uses JavaScript import syntax
78
+ */
79
+ async function swapThemeImportInLayoutFile(filePath, newImportStatement) {
80
+ if (!(await pathExists(filePath))) {
81
+ logger.warning(` ⚠️ Layout file not found: ${path.relative(process.cwd(), filePath)}`);
82
+ return false;
83
+ }
84
+
85
+ let content = await readFile(filePath, 'utf-8');
86
+
87
+ if (JS_IMPORT_LINE.test(content)) {
88
+ content = content.replace(JS_IMPORT_LINE, newImportStatement);
89
+ } else {
90
+ // Add import at the top of the file, before existing imports
91
+ const lines = content.split('\n');
92
+ const lastImportIndex = lines.findIndex((line, idx) =>
93
+ line.startsWith('import ') && (idx === lines.length - 1 || !lines[idx + 1].startsWith('import'))
94
+ );
95
+
96
+ if (lastImportIndex !== -1) {
97
+ lines.splice(lastImportIndex + 1, 0, newImportStatement);
98
+ } else {
99
+ lines.unshift(newImportStatement);
100
+ }
101
+ content = lines.join('\n');
102
+ }
103
+
104
+ await writeFile(filePath, content);
105
+ return true;
106
+ }
107
+
108
+ /**
109
+ * Run the detected package manager's install command inside the project root.
110
+ */
111
+ async function runInstall(project) {
112
+ const pkgManager = await detectPackageManager(project.root);
113
+ const { command, args } = getInstallCommand(pkgManager);
114
+
115
+ logger.info(`\n📦 Running ${command} ${args.join(' ')} to install the new theme package...\n`);
116
+
117
+ try {
118
+ await runCommand(command, args, { cwd: project.root });
119
+ logger.success(`✅ ${pkgManager} install completed.\n`);
120
+ return true;
121
+ } catch (error) {
122
+ logger.warning(`⚠️ ${pkgManager} install failed (${error.message}).`);
123
+ logger.info(' You can run it manually:');
124
+ logger.info(` cd ${path.relative(process.cwd(), project.root) || '.'}`);
125
+ logger.info(` ${command} ${args.join(' ')}\n`);
126
+ return false;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * `sf theme switch [theme]` — swap the active Syncfusion theme
132
+ * package + CSS/SCSS import for the current project.
133
+ */
134
+ export async function themeSwitchCommand(themeArg) {
135
+ const project = await requireSyncfusionProject(process.cwd());
136
+ if (!project) process.exit(1);
137
+
138
+ let theme = (themeArg || '').toLowerCase();
139
+
140
+ if (!theme) {
141
+ const selected = await safePrompt(select, {
142
+ message: 'Choose a theme:',
143
+ choices: [
144
+ { name: 'Material3', value: 'material3' },
145
+ { name: 'Tailwind3', value: 'tailwind3' },
146
+ { name: 'Bootstrap5.3', value: 'bootstrap5.3' },
147
+ { name: 'Fluent2', value: 'fluent2' }
148
+ ],
149
+ default: VALID_THEMES.includes(project.theme) ? project.theme : VALID_THEMES[0]
150
+ });
151
+ if (!selected) {
152
+ logger.warning('\n⚠️ Cancelled.\n');
153
+ return;
154
+ }
155
+ theme = selected;
156
+ }
157
+
158
+ if (!VALID_THEMES.includes(theme)) {
159
+ logger.error(`❌ Invalid theme: ${theme}`);
160
+ logger.info(` Supported themes: ${VALID_THEMES.join(', ')}`);
161
+ process.exit(1);
162
+ }
163
+
164
+ if (theme === project.theme) {
165
+ logger.info(`\nℹ️ Theme is already "${theme}". Nothing to do.\n`);
166
+ return;
167
+ }
168
+
169
+ logger.info(`\n🎨 Switching theme: ${project.theme} → ${theme}\n`);
170
+
171
+ const newThemeInfo = await getThemePackages(theme);
172
+
173
+ await addPackagesToJson(project.root, [newThemeInfo.base]);
174
+
175
+ let updated = false;
176
+
177
+ if (project.buildTool === 'nextjs' && project.styling === 'scss') {
178
+ // Routed template: update layout.tsx with JavaScript import
179
+ const layoutFilePath = getNextJsLayoutPath(project);
180
+ const newImportStatement = generateMainThemeImportStatement(theme, project.styling);
181
+ updated = await swapThemeImportInLayoutFile(layoutFilePath, newImportStatement);
182
+ } else {
183
+ // Non-Next.js frameworks (Angular, Vue, React Vite)
184
+ const stylingFilePath = resolveStyleFilePath(project);
185
+ const newImportStatement = generateThemeImportStatement(theme, project.styling);
186
+ updated = await swapThemeImportInStyleFile(stylingFilePath, newImportStatement);
187
+ }
188
+
189
+ try {
190
+ await updateSyncfusionConfig(project.root, { theme });
191
+ } catch (error) {
192
+ logger.warning(
193
+ `⚠️ Failed to update syncfusion.config.json: ${error.message}`
194
+ );
195
+ }
196
+ if (updated) {
197
+ logger.success(`✅ Theme switched to "${theme}".`);
198
+ }
199
+ await runInstall(project);
200
+ }