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.
- package/Jenkinsfile +48 -0
- package/README.md +300 -0
- package/bin/sf.js +438 -0
- package/eslint.config.js +90 -0
- package/gulpfile.js +157 -0
- package/lib/commander.js +3370 -0
- package/lib/inquirer-prompts.js +2038 -0
- package/license +13 -0
- package/package.json +43 -0
- package/smoke-test.mjs +38 -0
- package/src/commands/add.js +276 -0
- package/src/commands/info.js +82 -0
- package/src/commands/interactive.js +493 -0
- package/src/commands/mcp.js +135 -0
- package/src/commands/new.js +264 -0
- package/src/commands/skills.js +42 -0
- package/src/commands/theme.js +200 -0
- package/src/commands/upgrade.js +219 -0
- package/src/constants.js +161 -0
- package/src/generators/angular.js +878 -0
- package/src/generators/nextjs.js +817 -0
- package/src/generators/react.js +769 -0
- package/src/generators/typescript.js +258 -0
- package/src/generators/vue.js +800 -0
- package/src/templates/angular/chart.component.html +23 -0
- package/src/templates/angular/chart.component.ts +172 -0
- package/src/templates/angular/diagram.component.html +13 -0
- package/src/templates/angular/diagram.component.ts +224 -0
- package/src/templates/angular/docx-editor.component.html +18 -0
- package/src/templates/angular/docx-editor.component.ts +37 -0
- package/src/templates/angular/filemanager.component.html +4 -0
- package/src/templates/angular/filemanager.component.ts +47 -0
- package/src/templates/angular/gantt.component.html +32 -0
- package/src/templates/angular/gantt.component.ts +95 -0
- package/src/templates/angular/grid.component.html +12 -0
- package/src/templates/angular/grid.component.ts +58 -0
- package/src/templates/angular/pdf-viewer.component.html +9 -0
- package/src/templates/angular/pdf-viewer.component.ts +48 -0
- package/src/templates/angular/rte.component.html +7 -0
- package/src/templates/angular/rte.component.ts +13 -0
- package/src/templates/angular/scheduler.component.html +17 -0
- package/src/templates/angular/scheduler.component.ts +75 -0
- package/src/templates/angular/spreadsheet-editor.component.html +42 -0
- package/src/templates/angular/spreadsheet-editor.component.ts +61 -0
- package/src/templates/chart-template.js +20 -0
- package/src/templates/diagram-template.js +26 -0
- package/src/templates/docx-template.js +20 -0
- package/src/templates/filemanager-template.js +20 -0
- package/src/templates/gantt-template.js +20 -0
- package/src/templates/grid-template.js +20 -0
- package/src/templates/pdfviewer-template.js +20 -0
- package/src/templates/react/Chart.jsx +81 -0
- package/src/templates/react/Chart.tsx +81 -0
- package/src/templates/react/DOCXEditor.jsx +37 -0
- package/src/templates/react/DOCXEditor.tsx +37 -0
- package/src/templates/react/Diagram.jsx +192 -0
- package/src/templates/react/Diagram.tsx +195 -0
- package/src/templates/react/FileManager.jsx +13 -0
- package/src/templates/react/FileManager.tsx +13 -0
- package/src/templates/react/Gantt.jsx +89 -0
- package/src/templates/react/Gantt.tsx +89 -0
- package/src/templates/react/Grid.jsx +45 -0
- package/src/templates/react/Grid.tsx +45 -0
- package/src/templates/react/PDFViewer.jsx +16 -0
- package/src/templates/react/PDFViewer.tsx +16 -0
- package/src/templates/react/RTE.jsx +14 -0
- package/src/templates/react/RTE.tsx +14 -0
- package/src/templates/react/Scheduler.jsx +55 -0
- package/src/templates/react/Scheduler.tsx +55 -0
- package/src/templates/react/SpreadsheetEditor.jsx +96 -0
- package/src/templates/react/SpreadsheetEditor.tsx +98 -0
- package/src/templates/rte-template.js +26 -0
- package/src/templates/scheduler-template.js +20 -0
- package/src/templates/spreadheet-template.js +20 -0
- package/src/templates/typescript/RTE.ts +33 -0
- package/src/templates/typescript/SpreadsheetEditor.ts +39 -0
- package/src/templates/typescript/chart.ts +37 -0
- package/src/templates/typescript/diagram.ts +30 -0
- package/src/templates/typescript/docxeditor.ts +30 -0
- package/src/templates/typescript/filemanager.ts +41 -0
- package/src/templates/typescript/gantt.ts +88 -0
- package/src/templates/typescript/grid.ts +30 -0
- package/src/templates/typescript/pdfviewer.ts +8 -0
- package/src/templates/typescript/scheduler.ts +39 -0
- package/src/templates/vue/Chart.vue +257 -0
- package/src/templates/vue/ChartView.vue +7 -0
- package/src/templates/vue/DOCXEditor.vue +61 -0
- package/src/templates/vue/DOCXEditorView.vue +7 -0
- package/src/templates/vue/Diagram.vue +245 -0
- package/src/templates/vue/DiagramView.vue +7 -0
- package/src/templates/vue/FileManager.vue +58 -0
- package/src/templates/vue/FileManagerView.vue +7 -0
- package/src/templates/vue/Gantt.vue +129 -0
- package/src/templates/vue/GanttView.vue +7 -0
- package/src/templates/vue/Grid.vue +141 -0
- package/src/templates/vue/GridView.vue +7 -0
- package/src/templates/vue/PDFViewer.vue +58 -0
- package/src/templates/vue/PDFViewerView.vue +7 -0
- package/src/templates/vue/RTE.vue +27 -0
- package/src/templates/vue/RTEView.vue +7 -0
- package/src/templates/vue/Scheduler.vue +94 -0
- package/src/templates/vue/SchedulerView.vue +7 -0
- package/src/templates/vue/SpreadsheetEditor.vue +115 -0
- package/src/templates/vue/SpreadsheetEditorView.vue +7 -0
- package/src/utils/ansi-colors.js +38 -0
- package/src/utils/banner.js +179 -0
- package/src/utils/common.js +64 -0
- package/src/utils/config-generator.js +73 -0
- package/src/utils/folder-structure.js +26 -0
- package/src/utils/logger.js +19 -0
- package/src/utils/mcp-generator.js +122 -0
- package/src/utils/package-manager.js +128 -0
- package/src/utils/project-bootstrap.js +98 -0
- package/src/utils/project-detector.js +195 -0
- package/src/utils/prompt-utils.js +29 -0
- package/src/utils/run-command.js +110 -0
- package/src/utils/skills-installer.js +69 -0
- package/src/utils/template-file.js +35 -0
- package/src/utils/theme-loader.js +47 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { confirm } from '../../lib/inquirer-prompts.js';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
import { runCommand } from './run-command.js';
|
|
5
|
+
import { detectPackageManager, getInstallCommand } from './package-manager.js';
|
|
6
|
+
import { safePrompt } from './prompt-utils.js';
|
|
7
|
+
import { pathExists } from './common.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the dev-server command + args for a freshly created project, based
|
|
11
|
+
* on framework / build tool.
|
|
12
|
+
*/
|
|
13
|
+
export function getDevCommand(framework, buildTool) {
|
|
14
|
+
if (framework === 'angular') return { command: 'npm', args: ['run', 'start'] };
|
|
15
|
+
if (buildTool === 'nextjs') return { command: 'npm', args: ['run', 'dev'] };
|
|
16
|
+
return { command: 'npm', args: ['run', 'dev'] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Print the standard "Next steps" block. Used when the user declines to
|
|
21
|
+
* install/start automatically.
|
|
22
|
+
*/
|
|
23
|
+
export function printNextSteps(appName, framework, buildTool) {
|
|
24
|
+
const dev = getDevCommand(framework, buildTool);
|
|
25
|
+
const devString = framework === 'angular' ? 'ng serve' : `${dev.command} ${dev.args.join(' ')}`;
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
logger.info(`\n📖 Next steps:`);
|
|
29
|
+
logger.info(` cd ${appName}`);
|
|
30
|
+
logger.info(` npm install`);
|
|
31
|
+
logger.info(` ${devString}\n`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* After scaffolding, ask: "Install dependencies and start now?" unless
|
|
36
|
+
* `skip` is true (e.g. --yes / non-TTY / programmatic invocation). When
|
|
37
|
+
* yes, runs install in `appPath` and then starts the dev server (which keeps
|
|
38
|
+
* the process attached — exit with Ctrl+C).
|
|
39
|
+
*
|
|
40
|
+
* Returns { installed: boolean, started: boolean } for callers that want to
|
|
41
|
+
* record the result (e.g. into syncfusion.config.json).
|
|
42
|
+
*/
|
|
43
|
+
export async function promptAndBootstrap(appPath, appName, framework, buildTool, { skip = false, force = false } = {}) {
|
|
44
|
+
if (skip) {
|
|
45
|
+
printNextSteps(appName, framework, buildTool);
|
|
46
|
+
return { installed: false, started: false };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Don't prompt when there is no interactive terminal (e.g. CI).
|
|
50
|
+
if (!process.stdout.isTTY) {
|
|
51
|
+
printNextSteps(appName, framework, buildTool);
|
|
52
|
+
return { installed: false, started: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!force) {
|
|
56
|
+
const wantsBootstrap = await safePrompt(confirm, {
|
|
57
|
+
message: '📦 Install dependencies and start the dev server now?',
|
|
58
|
+
default: true
|
|
59
|
+
});
|
|
60
|
+
if (!wantsBootstrap) {
|
|
61
|
+
printNextSteps(appName, framework, buildTool);
|
|
62
|
+
return { installed: false, started: false };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 1) Install
|
|
67
|
+
const pkgManager = await detectPackageManager(appPath);
|
|
68
|
+
const { command: instCmd, args: instArgs } = getInstallCommand(pkgManager);
|
|
69
|
+
logger.info(`\n📦 Installing dependencies with ${pkgManager} (this can take a minute)...\n`);
|
|
70
|
+
try {
|
|
71
|
+
await runCommand(instCmd, instArgs, { cwd: appPath });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
logger.error(`❌ Install failed: ${error.message}`);
|
|
74
|
+
printNextSteps(appName, framework, buildTool);
|
|
75
|
+
return { installed: false, started: false };
|
|
76
|
+
}
|
|
77
|
+
logger.success('\n✅ Dependencies installed.\n');
|
|
78
|
+
|
|
79
|
+
// 2) Start the dev server (blocks until the user kills it with Ctrl+C)
|
|
80
|
+
const dev = getDevCommand(framework, buildTool);
|
|
81
|
+
const hasNodeModules = await pathExists(path.join(appPath, 'node_modules'));
|
|
82
|
+
if (!hasNodeModules) {
|
|
83
|
+
logger.warning('⚠️ node_modules is still missing — skipping dev server start.');
|
|
84
|
+
printNextSteps(appName, framework, buildTool);
|
|
85
|
+
return { installed: true, started: false };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
logger.info(`🚀 Starting dev server (${dev.command} ${dev.args.join(' ')})...\n`);
|
|
89
|
+
logger.info(' Press Ctrl+C to stop.\n');
|
|
90
|
+
try {
|
|
91
|
+
await runCommand(dev.command, dev.args, { cwd: appPath });
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// Dev server exiting (Ctrl+C, port-in-use, etc.) is non-fatal.
|
|
94
|
+
logger.error(`❌ Dev Server failed: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { installed: true, started: true };
|
|
98
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readSyncfusionConfig } from './config-generator.js';
|
|
5
|
+
import { logger } from './logger.js';
|
|
6
|
+
|
|
7
|
+
const THEME_PACKAGE_TO_NAME = {
|
|
8
|
+
'@syncfusion/ej2-tailwind3-theme': 'tailwind3',
|
|
9
|
+
'@syncfusion/ej2-bootstrap5.3-theme': 'bootstrap5.3',
|
|
10
|
+
'@syncfusion/ej2-fluent2-theme': 'fluent2',
|
|
11
|
+
'@syncfusion/ej2-material3-theme': 'material3'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Walk upward from `startDir` looking for a file/folder that indicates a
|
|
16
|
+
* project root (package.json). Returns the directory or null if not found.
|
|
17
|
+
*/
|
|
18
|
+
function findProjectRoot(startDir, markerFile = 'package.json', maxDepth = 6) {
|
|
19
|
+
let dir = path.resolve(startDir);
|
|
20
|
+
for (let i = 0; i <= maxDepth; i++) {
|
|
21
|
+
if (existsSync(path.join(dir, markerFile))) {
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
const parent = path.dirname(dir);
|
|
25
|
+
if (parent === dir) break;
|
|
26
|
+
dir = parent;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function detectFrameworkFromPackageJson(pkgJson) {
|
|
32
|
+
const deps = { ...(pkgJson.dependencies || {}), ...(pkgJson.devDependencies || {}) };
|
|
33
|
+
|
|
34
|
+
if (deps['@angular/core']) return 'angular';
|
|
35
|
+
if (deps['vue']) return 'vue';
|
|
36
|
+
if (deps['react']) return 'react';
|
|
37
|
+
// The TypeScript (vanilla EJ2) platform is identified by the presence of
|
|
38
|
+
// a core Syncfusion EJ2 package such as `@syncfusion/ej2-grids`, with no
|
|
39
|
+
// framework wrapper (no react/angular/vue dependencies).
|
|
40
|
+
const hasSyncfusionCore = Object.keys(deps).some(
|
|
41
|
+
(name) => /^@syncfusion\/ej2-[a-z0-9-]+$/.test(name)
|
|
42
|
+
);
|
|
43
|
+
if (hasSyncfusionCore && !deps['@angular/core'] && !deps['vue'] && !deps['react']) {
|
|
44
|
+
return 'typescript';
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function detectBuildTool(pkgJson, framework) {
|
|
50
|
+
if (framework !== 'react') return null;
|
|
51
|
+
const deps = { ...(pkgJson.dependencies || {}), ...(pkgJson.devDependencies || {}) };
|
|
52
|
+
return deps['next'] ? 'nextjs' : 'vite';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function detectLanguage(rootPath) {
|
|
56
|
+
return existsSync(path.join(rootPath, 'tsconfig.json')) ? 'typescript' : 'javascript';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function detectStyle(pkgJson) {
|
|
60
|
+
const deps = { ...(pkgJson.dependencies || {}), ...(pkgJson.devDependencies || {}) };
|
|
61
|
+
return deps['sass'] ? 'scss' : 'css';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function detectTheme(pkgJson) {
|
|
65
|
+
const deps = { ...(pkgJson.dependencies || {}), ...(pkgJson.devDependencies || {}) };
|
|
66
|
+
for (const [pkg, theme] of Object.entries(THEME_PACKAGE_TO_NAME)) {
|
|
67
|
+
if (deps[pkg]) return theme;
|
|
68
|
+
}
|
|
69
|
+
return 'material3';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function detectComponentsFromDisk(rootPath, framework) {
|
|
73
|
+
const found = [];
|
|
74
|
+
const componentsDir = path.join(rootPath, 'src', 'components');
|
|
75
|
+
if (!existsSync(componentsDir)) return found;
|
|
76
|
+
|
|
77
|
+
const checks = {
|
|
78
|
+
grid: framework === 'angular' ? 'grid' : /^Grid\.(jsx|tsx|vue|js|ts)$/,
|
|
79
|
+
chart: framework === 'angular' ? 'chart' : /^Chart\.(jsx|tsx|vue|js|ts)$/,
|
|
80
|
+
scheduler: framework === 'angular' ? 'scheduler' : /^Scheduler\.(jsx|tsx|vue|js|ts)$/,
|
|
81
|
+
gantt: framework === 'angular' ? 'gantt' : /^Gantt\.(jsx|tsx|vue|js|ts)$/,
|
|
82
|
+
'file-manager': framework === 'angular' ? 'file-manager' : /^FileManager\.(jsx|tsx|vue|js|ts)$/,
|
|
83
|
+
diagram: framework === 'angular' ? 'diagram' : /^Diagram\.(jsx|tsx|vue|js|ts)$/,
|
|
84
|
+
'rich-text-editor': framework === 'angular' ? 'rich-text-editor' : /^RTE\.(jsx|tsx|vue|js|ts)$/,
|
|
85
|
+
'pdf-viewer': framework === 'angular' ? 'pdf-viewer' : /^PDFViewer\.(jsx|tsx|vue|js|ts)$/,
|
|
86
|
+
'docx-editor': framework === 'angular' ? 'docx-editor' : /^DOCXEditor\.(jsx|tsx|vue|js|ts)$/,
|
|
87
|
+
'spreadsheet-editor': framework === 'angular' ? 'spreadsheet-editor' : /^SpreadsheetEditor\.(jsx|tsx|vue|js|ts)$/
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const entries = readdirSync(componentsDir);
|
|
91
|
+
for (const [key, matcher] of Object.entries(checks)) {
|
|
92
|
+
if (framework === 'angular') {
|
|
93
|
+
if (entries.includes(matcher)) found.push(key);
|
|
94
|
+
} else if (entries.some((e) => matcher.test(e))) {
|
|
95
|
+
found.push(key);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return found;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Detect the Syncfusion project rooted at (or above) `cwd`.
|
|
103
|
+
*
|
|
104
|
+
* Resolution order:
|
|
105
|
+
* 1. `syncfusion.config.json` (authoritative, written by this CLI).
|
|
106
|
+
* 2. Best-effort detection from package.json + tsconfig.json + node_modules.
|
|
107
|
+
*
|
|
108
|
+
* Returns `null` when no package.json can be found at all (i.e. not inside
|
|
109
|
+
* any Node project).
|
|
110
|
+
*/
|
|
111
|
+
export async function detectProject(cwd = process.cwd()) {
|
|
112
|
+
const root = findProjectRoot(cwd);
|
|
113
|
+
if (!root) return null;
|
|
114
|
+
|
|
115
|
+
let pkgJson = {};
|
|
116
|
+
try {
|
|
117
|
+
pkgJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
|
118
|
+
} catch {
|
|
119
|
+
pkgJson = {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const config = await readSyncfusionConfig(root);
|
|
123
|
+
|
|
124
|
+
if (config) {
|
|
125
|
+
return {
|
|
126
|
+
root,
|
|
127
|
+
framework: config.framework,
|
|
128
|
+
type: config.type,
|
|
129
|
+
styling: config.styling || 'css',
|
|
130
|
+
theme: config.theme,
|
|
131
|
+
buildTool: config.buildTool || null,
|
|
132
|
+
template: config.template || 'empty',
|
|
133
|
+
components: config.components || [],
|
|
134
|
+
appName: pkgJson.name || path.basename(root),
|
|
135
|
+
fromConfig: true,
|
|
136
|
+
config
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Fall back to best-effort detection.
|
|
141
|
+
const framework = detectFrameworkFromPackageJson(pkgJson);
|
|
142
|
+
if (!framework) {
|
|
143
|
+
return {
|
|
144
|
+
root,
|
|
145
|
+
framework: null,
|
|
146
|
+
appName: pkgJson.name || path.basename(root),
|
|
147
|
+
fromConfig: false,
|
|
148
|
+
config: null
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const components = detectComponentsFromDisk(root, framework);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
root,
|
|
156
|
+
framework,
|
|
157
|
+
// The TypeScript (vanilla EJ2) platform is always TypeScript.
|
|
158
|
+
type: framework === 'typescript' ? 'typescript' : detectLanguage(root),
|
|
159
|
+
styling: detectStyle(pkgJson),
|
|
160
|
+
theme: detectTheme(pkgJson),
|
|
161
|
+
buildTool: detectBuildTool(pkgJson, framework),
|
|
162
|
+
template: components[0] || 'empty',
|
|
163
|
+
components,
|
|
164
|
+
appName: pkgJson.name || path.basename(root),
|
|
165
|
+
fromConfig: false,
|
|
166
|
+
config: null
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Detect the project, or print a friendly error and return null.
|
|
172
|
+
*/
|
|
173
|
+
export async function requireSyncfusionProject(cwd = process.cwd()) {
|
|
174
|
+
const project = await detectProject(cwd);
|
|
175
|
+
|
|
176
|
+
if (!project) {
|
|
177
|
+
logger.error('❌ No package.json found. Run this command inside a project folder.');
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!project.framework) {
|
|
182
|
+
logger.error(
|
|
183
|
+
'❌ Could not detect a Syncfusion project created with @syncfusion/syncfusion-cli. This command only works inside projects scaffolded using @syncfusion/syncfusion-cli.'
|
|
184
|
+
);
|
|
185
|
+
logger.info('');
|
|
186
|
+
logger.info('Quick fix:');
|
|
187
|
+
logger.info(' 1. Navigate to your project folder (e.g. cd my-app)');
|
|
188
|
+
logger.info(' 2. Re-run the command');
|
|
189
|
+
logger.info('');
|
|
190
|
+
logger.info('Or scaffold a new project:');
|
|
191
|
+
logger.info(' sf new my-app');
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return project;
|
|
195
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { select } from '../../lib/inquirer-prompts.js';
|
|
2
|
+
|
|
3
|
+
export async function safePrompt(promptFn, config) {
|
|
4
|
+
try {
|
|
5
|
+
return await promptFn(config);
|
|
6
|
+
} catch (error) {
|
|
7
|
+
if (error?.name === 'ExitPromptError') {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const SELECT_OPTION_INDENT = ' ';
|
|
15
|
+
|
|
16
|
+
export function withIndentedChoices(choices) {
|
|
17
|
+
return choices.map((choice) => ({
|
|
18
|
+
...choice,
|
|
19
|
+
name: `${SELECT_OPTION_INDENT}${choice.name}`,
|
|
20
|
+
short: choice.short || choice.name
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function selectWithIndentedChoices(config) {
|
|
25
|
+
return safePrompt(select, {
|
|
26
|
+
...config,
|
|
27
|
+
choices: withIndentedChoices(config.choices)
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const WINDOWS_SHIM_COMMANDS = new Set(['npm', 'npx', 'pnpm', 'yarn']);
|
|
5
|
+
|
|
6
|
+
function isWindows() {
|
|
7
|
+
return process.platform === 'win32';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeArgs(args) {
|
|
11
|
+
if (!args) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(args)) {
|
|
15
|
+
return args;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return [String(args)];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isWindowsShim(command) {
|
|
22
|
+
return isWindows() && /\.(cmd|bat)$/i.test(command);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveWindowsCommand(command) {
|
|
26
|
+
if (!command || typeof command !== 'string') {
|
|
27
|
+
throw new TypeError('Command must be a non-empty string');
|
|
28
|
+
}
|
|
29
|
+
if (!isWindows()) {
|
|
30
|
+
return command;
|
|
31
|
+
}
|
|
32
|
+
const commandName = path.basename(command).toLowerCase();
|
|
33
|
+
if (WINDOWS_SHIM_COMMANDS.has(commandName)) {
|
|
34
|
+
return `${command}.cmd`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return command;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getSpawnCommand(command, args) {
|
|
41
|
+
const normalizedArgs = normalizeArgs(args);
|
|
42
|
+
const resolvedCommand = resolveWindowsCommand(command);
|
|
43
|
+
if (isWindowsShim(resolvedCommand)) {
|
|
44
|
+
return {
|
|
45
|
+
command: 'cmd.exe',
|
|
46
|
+
args: ['/d', '/s', '/c', resolvedCommand, ...normalizedArgs]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
command: resolvedCommand,
|
|
52
|
+
args: normalizedArgs
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function runCommand(command, args = [], options = {}) {
|
|
57
|
+
const { capture = false, ...spawnOptions } = options;
|
|
58
|
+
const {
|
|
59
|
+
command: effectiveCommand,
|
|
60
|
+
args: effectiveArgs
|
|
61
|
+
} = getSpawnCommand(command, args);
|
|
62
|
+
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const child = spawn(effectiveCommand, effectiveArgs, {
|
|
65
|
+
cwd: spawnOptions.cwd,
|
|
66
|
+
env: spawnOptions.env,
|
|
67
|
+
shell: false,
|
|
68
|
+
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit'
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let stdout = '';
|
|
72
|
+
let stderr = '';
|
|
73
|
+
|
|
74
|
+
if (capture) {
|
|
75
|
+
child.stdout?.on('data', (chunk) => {
|
|
76
|
+
stdout += chunk.toString();
|
|
77
|
+
});
|
|
78
|
+
child.stderr?.on('data', (chunk) => {
|
|
79
|
+
stderr += chunk.toString();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
child.on('error', (error) => {
|
|
83
|
+
error.command = effectiveCommand;
|
|
84
|
+
error.args = effectiveArgs;
|
|
85
|
+
reject(error);
|
|
86
|
+
});
|
|
87
|
+
child.on('exit', (code, signal) => {
|
|
88
|
+
if (code === 0) {
|
|
89
|
+
resolve({ stdout, stderr });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const message =
|
|
93
|
+
stderr.trim() ||
|
|
94
|
+
(signal
|
|
95
|
+
? `Command terminated with signal ${signal}`
|
|
96
|
+
: `Command exited with code ${code}`);
|
|
97
|
+
const error = new Error(message);
|
|
98
|
+
error.code = code;
|
|
99
|
+
error.signal = signal;
|
|
100
|
+
error.stdout = stdout;
|
|
101
|
+
error.stderr = stderr;
|
|
102
|
+
error.command = effectiveCommand;
|
|
103
|
+
error.args = effectiveArgs;
|
|
104
|
+
reject(error);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { runCommand };
|
|
110
|
+
export default runCommand;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { select, confirm } from '../../lib/inquirer-prompts.js';
|
|
2
|
+
import { runCommand } from './run-command.js';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
import { SKILLS_PACKAGES } from '../constants.js';
|
|
5
|
+
import { safePrompt } from './prompt-utils.js';
|
|
6
|
+
|
|
7
|
+
export async function promptSkillsInstallMode() {
|
|
8
|
+
const mode = await safePrompt(select, {
|
|
9
|
+
message: 'How do you want to install Syncfusion Component Skills?',
|
|
10
|
+
choices: [
|
|
11
|
+
{ name: 'Install all skills', value: 'all' },
|
|
12
|
+
{ name: 'Install selected skills', value: 'selected' }
|
|
13
|
+
],
|
|
14
|
+
default: 'all'
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (!mode) {
|
|
18
|
+
logger.warning('\n⚠️ Skills installation cancelled.\n');
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return mode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run `npx skills add <package>` for the given framework inside `appPath`.
|
|
27
|
+
* By default, `-y` is used to install all skills/non-interactive selection.
|
|
28
|
+
* When `allSkills` is false, `-y` is not passed so the skills CLI can ask for selected skills.
|
|
29
|
+
*/
|
|
30
|
+
export async function installSkills(appPath, framework, { allSkills = true } = {}) {
|
|
31
|
+
const pkg = SKILLS_PACKAGES[framework] || SKILLS_PACKAGES.react;
|
|
32
|
+
const args = ['skills', 'add', pkg];
|
|
33
|
+
|
|
34
|
+
if (allSkills) {
|
|
35
|
+
args.push('-y');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
logger.info(`\n📦 Installing Syncfusion ${framework} component skills (npx ${args.join(' ')})...`);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await runCommand('npx', args, { cwd: appPath });
|
|
42
|
+
|
|
43
|
+
logger.success('✅ Syncfusion component skills installed.\n');
|
|
44
|
+
return true;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const fallbackCommand = `npx skills add ${pkg}${allSkills ? ' -y' : ''}`;
|
|
47
|
+
logger.warning(`⚠️ Could not install skills automatically (${error.message}).`);
|
|
48
|
+
logger.info(` You can install them later by running:\n ${fallbackCommand}\n`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Interactive prompt flow: "Want to install Syncfusion component skills?"
|
|
55
|
+
* Skips entirely when `skip` is true (e.g. --yes flag).
|
|
56
|
+
*/
|
|
57
|
+
export async function promptAndInstallSkills(appPath, framework, { skip = false } = {}) {
|
|
58
|
+
if (skip) return { installed: false };
|
|
59
|
+
|
|
60
|
+
const wantsSkills = await safePrompt(confirm, {
|
|
61
|
+
message: '🧠 Want to install Syncfusion component skills for AI coding agents?',
|
|
62
|
+
default: false
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!wantsSkills) return { installed: false };
|
|
66
|
+
|
|
67
|
+
const installed = await installSkills(appPath, framework);
|
|
68
|
+
return { installed };
|
|
69
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { copyFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
// templates/ is a sibling of utils/ under src/
|
|
9
|
+
export const TEMPLATES_ROOT = path.resolve(__dirname, '..', 'templates');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Copy a single template file from `src/templates/<framework>/<name>`
|
|
13
|
+
* to `<destDir>/<name>` inside the user's project.
|
|
14
|
+
*/
|
|
15
|
+
export async function copyTemplateFile(framework, name, destDir) {
|
|
16
|
+
const sourcePath = path.join(TEMPLATES_ROOT, framework, name);
|
|
17
|
+
const destPath = path.join(destDir, name);
|
|
18
|
+
|
|
19
|
+
await mkdir(destDir, { recursive: true });
|
|
20
|
+
await copyFile(sourcePath, destPath);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Copy every file in a framework subfolder (e.g. all files for an Angular
|
|
25
|
+
* component that has a .ts + .html pair) — given by the explicit list of
|
|
26
|
+
* file names. We iterate the list so we don't need a real subfolder on disk
|
|
27
|
+
* for each Angular component.
|
|
28
|
+
*/
|
|
29
|
+
export async function copyTemplateFiles(framework, files, srcDir, destDir) {
|
|
30
|
+
const sourceDir = path.join(TEMPLATES_ROOT, framework, srcDir);
|
|
31
|
+
await mkdir(destDir, { recursive: true });
|
|
32
|
+
for (const name of files) {
|
|
33
|
+
await copyFile(path.join(sourceDir, name), path.join(destDir, name));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const THEME_MAP = {
|
|
2
|
+
'tailwind3': {
|
|
3
|
+
package: '@syncfusion/ej2-tailwind3-theme',
|
|
4
|
+
path: 'tailwind3'
|
|
5
|
+
},
|
|
6
|
+
'bootstrap5.3': {
|
|
7
|
+
package: '@syncfusion/ej2-bootstrap5.3-theme',
|
|
8
|
+
path: 'bootstrap5.3'
|
|
9
|
+
},
|
|
10
|
+
'fluent2': {
|
|
11
|
+
package: '@syncfusion/ej2-fluent2-theme',
|
|
12
|
+
path: 'fluent2'
|
|
13
|
+
},
|
|
14
|
+
'material3': {
|
|
15
|
+
package: '@syncfusion/ej2-material3-theme',
|
|
16
|
+
path: 'material3'
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function getThemePackages(theme) {
|
|
21
|
+
const themeConfig = THEME_MAP[theme] || THEME_MAP.material3;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
base: themeConfig.package,
|
|
25
|
+
themePath: `../node_modules/${themeConfig.package}/styles/${themeConfig.path}`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function generateThemeImportStatement(theme, styling = 'css') {
|
|
30
|
+
const themeConfig = THEME_MAP[theme] || THEME_MAP.material3;
|
|
31
|
+
if (styling === 'scss') {
|
|
32
|
+
return `@use "${themeConfig.package}/styles/${themeConfig.path}.scss" as *;`;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
return `@import "${themeConfig.package}/styles/${themeConfig.path}.css";`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function generateMainThemeImportStatement(theme, styling = 'css') {
|
|
40
|
+
const themeConfig = THEME_MAP[theme] || THEME_MAP.material3;
|
|
41
|
+
if (styling === 'scss') {
|
|
42
|
+
return `import "${themeConfig.package}/styles/${themeConfig.path}.scss";`;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
return `import "${themeConfig.package}/styles/${themeConfig.path}.css";`;
|
|
46
|
+
}
|
|
47
|
+
}
|