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,38 @@
|
|
|
1
|
+
const ansi = {
|
|
2
|
+
reset: '\x1b[0m',
|
|
3
|
+
bold: '\x1b[1m',
|
|
4
|
+
dim: '\x1b[2m',
|
|
5
|
+
gray: '\x1b[90m',
|
|
6
|
+
white: '\x1b[97m',
|
|
7
|
+
red: '\x1b[31m',
|
|
8
|
+
green: '\x1b[32m',
|
|
9
|
+
yellow: '\x1b[33m',
|
|
10
|
+
blue: '\x1b[34m',
|
|
11
|
+
cyan: '\x1b[36m'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function wrap(text, ...codes) {
|
|
15
|
+
return `${codes.join('')}${text}${ansi.reset}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hexToRgb(hex) {
|
|
19
|
+
const normalized = hex.replace('#', '');
|
|
20
|
+
const value = normalized.length === 3
|
|
21
|
+
? normalized.split('').map((char) => char + char).join('')
|
|
22
|
+
: normalized;
|
|
23
|
+
|
|
24
|
+
const parsed = Number.parseInt(value, 16);
|
|
25
|
+
return {
|
|
26
|
+
r: (parsed >> 16) & 255,
|
|
27
|
+
g: (parsed >> 8) & 255,
|
|
28
|
+
b: parsed & 255
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rgb(text, hex, { bold = false } = {}) {
|
|
33
|
+
const { r, g, b } = hexToRgb(hex);
|
|
34
|
+
const rgbCode = `\x1b[38;2;${r};${g};${b}m`;
|
|
35
|
+
return wrap(text, bold ? ansi.bold : '', rgbCode);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { ansi, wrap, hexToRgb, rgb };
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ansi, rgb, wrap } from './ansi-colors.js';
|
|
4
|
+
|
|
5
|
+
function gray(text) {
|
|
6
|
+
return wrap(text, ansi.gray);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function cyan(text) {
|
|
10
|
+
return wrap(text, ansi.cyan);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function bold(text) {
|
|
14
|
+
return wrap(text, ansi.bold);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function boldWhite(text) {
|
|
18
|
+
return wrap(text, ansi.bold, ansi.white);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const brand = (text) => rgb(text, '#00BCEB');
|
|
22
|
+
const brandBold = (text) => rgb(text, '#00BCEB', { bold: true });
|
|
23
|
+
const brandDark = (text) => rgb(text, '#1B6E7A');
|
|
24
|
+
const accent = (text) => rgb(text, '#7C3AED');
|
|
25
|
+
|
|
26
|
+
function getTerminalWidth() {
|
|
27
|
+
const width = process.stdout.columns || 75;
|
|
28
|
+
const MIN_WIDTH = 75;
|
|
29
|
+
const MAX_WIDTH = 60;
|
|
30
|
+
return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, width));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Then use it dynamically:
|
|
34
|
+
const BOX_WIDTH = getTerminalWidth();
|
|
35
|
+
const INNER_WIDTH = BOX_WIDTH - 2;
|
|
36
|
+
|
|
37
|
+
function stripAnsi(text) {
|
|
38
|
+
const ESC = String.fromCharCode(27);
|
|
39
|
+
return String(text).replace(new RegExp(`${ESC}\\[[0-9;]*m`, 'g'), '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function boxLine(content, color = brandDark) {
|
|
43
|
+
const plainLen = stripAnsi(content).length;
|
|
44
|
+
const padding = Math.max(0, INNER_WIDTH - plainLen);
|
|
45
|
+
|
|
46
|
+
return color('║') + content + ' '.repeat(padding) + color('║');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function boxTop(color = brandDark) {
|
|
50
|
+
return color('╔' + '═'.repeat(INNER_WIDTH) + '╗');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function boxBottom(color = brandDark) {
|
|
54
|
+
return color('╚' + '═'.repeat(INNER_WIDTH) + '╝');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function boxCenterLine(content, color = brandDark) {
|
|
58
|
+
const plainLen = stripAnsi(content).length;
|
|
59
|
+
const totalPadding = Math.max(0, INNER_WIDTH - plainLen);
|
|
60
|
+
const leftPadding = Math.floor(totalPadding / 2);
|
|
61
|
+
const rightPadding = totalPadding - leftPadding;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
color('║') +
|
|
65
|
+
' '.repeat(leftPadding) +
|
|
66
|
+
content +
|
|
67
|
+
' '.repeat(rightPadding) +
|
|
68
|
+
color('║')
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function printBanner(version = '1.0.1') {
|
|
73
|
+
const width = 64;
|
|
74
|
+
const leftText = bold('Syncfusion CLI');
|
|
75
|
+
const rightText = gray(`v${version}`);
|
|
76
|
+
|
|
77
|
+
const plainLeftLength = 'Syncfusion CLI'.length;
|
|
78
|
+
const plainRightLength = `v${version}`.length;
|
|
79
|
+
const gapSize = Math.max(1, width - plainLeftLength - plainRightLength);
|
|
80
|
+
const gap = ' '.repeat(gapSize);
|
|
81
|
+
|
|
82
|
+
// Render Straight-Line Layout (Flawless f and i separation)
|
|
83
|
+
console.log(gray(' ──────────────────────────────────────────────────────────────────'));
|
|
84
|
+
console.log(` ${leftText}${gap}${rightText}`);
|
|
85
|
+
console.log(gray(' ──────────────────────────────────────────────────────────────────'));
|
|
86
|
+
}
|
|
87
|
+
const HELP_SECTION_INDENT = 2;
|
|
88
|
+
const HELP_ROW_INDENT = 5;
|
|
89
|
+
const HELP_COMMAND_WIDTH = 38;
|
|
90
|
+
|
|
91
|
+
export function printHelpFooter() {
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(boxTop(brandDark));
|
|
94
|
+
console.log(
|
|
95
|
+
boxCenterLine(`${accent('◆')} ${boldWhite('Command Reference')}`, brandDark)
|
|
96
|
+
);
|
|
97
|
+
console.log(boxBottom(brandDark));
|
|
98
|
+
|
|
99
|
+
const section = (title) => {
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(' '.repeat(HELP_SECTION_INDENT) + brandBold(title));
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
section('⚡ Interactive Mode');
|
|
105
|
+
helpRow('sf', 'Launch interactive mode');
|
|
106
|
+
|
|
107
|
+
section('🚀 Project Creation');
|
|
108
|
+
helpRow('sf new <appName> react ts grid', 'Create a new Syncfusion project');
|
|
109
|
+
|
|
110
|
+
section('🧩 Component Commands');
|
|
111
|
+
helpRow('sf add grid', 'Add a Grid sample');
|
|
112
|
+
helpRow('sf add chart', 'Add a Chart sample');
|
|
113
|
+
helpRow('sf add scheduler', 'Add a Scheduler sample');
|
|
114
|
+
helpRow('sf add gantt', 'Add a Gantt sample');
|
|
115
|
+
helpRow('sf add file-manager', 'Add a File Manager sample');
|
|
116
|
+
helpRow('sf add diagram', 'Add a Diagram sample');
|
|
117
|
+
helpRow('sf add rich-text-editor', 'Add a Rich Text Editor sample');
|
|
118
|
+
helpRow('sf add pdf-viewer', 'Add a PDF Viewer sample');
|
|
119
|
+
helpRow('sf add docx-editor', 'Add a DOCX Editor sample');
|
|
120
|
+
helpRow('sf add spreadsheet-editor', 'Add a Spreadsheet Editor sample');
|
|
121
|
+
|
|
122
|
+
section('🤖 AI & MCP');
|
|
123
|
+
helpRow('sf mcp init [editor]', 'Configure the Syncfusion MCP server for your editor');
|
|
124
|
+
helpRow('sf skills install', 'Install the Syncfusion component skills');
|
|
125
|
+
|
|
126
|
+
// section('↑ Upgrade');
|
|
127
|
+
// helpRow('sf upgrade', 'Update installed Syncfusion packages to latest');
|
|
128
|
+
|
|
129
|
+
section('🎨 Theme Management');
|
|
130
|
+
helpRow('sf theme switch [themeName]', 'Switch the active Syncfusion theme');
|
|
131
|
+
|
|
132
|
+
section('ℹ️ Project Information');
|
|
133
|
+
helpRow('sf info', 'Show environment & project information');
|
|
134
|
+
|
|
135
|
+
const framework = getProjectFramework();
|
|
136
|
+
const docsLink = getDocumentationLink(framework);
|
|
137
|
+
console.log('');
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log(boxTop(brandDark));
|
|
140
|
+
console.log(
|
|
141
|
+
boxLine(` ${gray('Docs')} ${accent('-')} ${cyan(docsLink)}`, brandDark)
|
|
142
|
+
);
|
|
143
|
+
console.log(boxBottom(brandDark));
|
|
144
|
+
console.log('');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function helpRow(usage, desc) {
|
|
148
|
+
const command = usage.padEnd(HELP_COMMAND_WIDTH);
|
|
149
|
+
console.log(`${' '.repeat(HELP_ROW_INDENT)}${brand(command)} ${gray(desc)}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getProjectFramework(projectDir = process.cwd()) {
|
|
153
|
+
const configPath = path.join(projectDir, 'syncfusion.config.json');
|
|
154
|
+
if (!fs.existsSync(configPath)) {
|
|
155
|
+
return 'react';
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const configText = fs.readFileSync(configPath, 'utf8');
|
|
159
|
+
const config = JSON.parse(configText);
|
|
160
|
+
const framework = String(config.framework || '').trim().toLowerCase();
|
|
161
|
+
|
|
162
|
+
if (['react', 'angular', 'vue'].includes(framework)) {
|
|
163
|
+
return framework;
|
|
164
|
+
}
|
|
165
|
+
return 'react';
|
|
166
|
+
} catch {
|
|
167
|
+
return 'react';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getDocumentationLink(framework) {
|
|
172
|
+
const docsLinks = {
|
|
173
|
+
react: 'https://ej2.syncfusion.com/react/documentation/introduction',
|
|
174
|
+
angular: 'https://ej2.syncfusion.com/angular/documentation/introduction',
|
|
175
|
+
vue: 'https://ej2.syncfusion.com/vue/documentation/introduction'
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return docsLinks[framework] || docsLinks.react;
|
|
179
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { access, readFile } from 'node:fs/promises';
|
|
3
|
+
import { TEMPLATE_PACKAGES } from '../constants.js';
|
|
4
|
+
import { addPackagesToJson } from './package-manager.js';
|
|
5
|
+
|
|
6
|
+
export async function pathExists(targetPath) {
|
|
7
|
+
try {
|
|
8
|
+
await access(targetPath);
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function addTemplatePackages(appPath, selectedTemplate, frameworkKey) {
|
|
16
|
+
const emptyPackages = TEMPLATE_PACKAGES.empty?.[frameworkKey] || [];
|
|
17
|
+
const componentPackages = TEMPLATE_PACKAGES[selectedTemplate]?.[frameworkKey] || [];
|
|
18
|
+
const packages = [
|
|
19
|
+
...emptyPackages,
|
|
20
|
+
...componentPackages
|
|
21
|
+
];
|
|
22
|
+
if (!packages.length) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
await addPackagesToJson(appPath, packages);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function readJson(filePath) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function updateVersions(root, packages) {
|
|
37
|
+
const packageJsonPath = path.join(root, 'package.json');
|
|
38
|
+
const packageJson = await readJson(packageJsonPath);
|
|
39
|
+
if (!packageJson) {
|
|
40
|
+
return packages;
|
|
41
|
+
}
|
|
42
|
+
const dependencies = {
|
|
43
|
+
...(packageJson.dependencies || {}),
|
|
44
|
+
...(packageJson.devDependencies || {})
|
|
45
|
+
};
|
|
46
|
+
const existingThemePackageName = Object.keys(dependencies).find(
|
|
47
|
+
(packageName) => /^@syncfusion\/ej2-.+-theme$/.test(packageName)
|
|
48
|
+
);
|
|
49
|
+
if (!existingThemePackageName) {
|
|
50
|
+
return packages;
|
|
51
|
+
}
|
|
52
|
+
const version = dependencies[existingThemePackageName];
|
|
53
|
+
|
|
54
|
+
return packages.map((packageName) => {
|
|
55
|
+
if (
|
|
56
|
+
!packageName.startsWith('@syncfusion/ej2-') ||
|
|
57
|
+
packageName.lastIndexOf('@') > 0
|
|
58
|
+
) {
|
|
59
|
+
return packageName;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return `${packageName}@${version}`;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
import packageJson from '../../package.json' with { type: 'json' };
|
|
5
|
+
import { pathExists } from './common.js';
|
|
6
|
+
|
|
7
|
+
export async function generateSyncfusionConfig(appPath, options) {
|
|
8
|
+
try {
|
|
9
|
+
const template = options.template || 'empty';
|
|
10
|
+
const config = {
|
|
11
|
+
framework: options.framework || 'react',
|
|
12
|
+
type: options.type || 'typescript',
|
|
13
|
+
theme: options.theme || 'material3',
|
|
14
|
+
styling: options.styling || 'css',
|
|
15
|
+
template,
|
|
16
|
+
buildTool: options.buildTool || null,
|
|
17
|
+
// Tracks every routed component present in the project (used by `sf add`).
|
|
18
|
+
components: template === 'empty' ? [] : [template],
|
|
19
|
+
mcp: { integrated: false, editors: [] },
|
|
20
|
+
skills: { installed: false },
|
|
21
|
+
version: packageJson.version,
|
|
22
|
+
createdAt: new Date().toISOString(),
|
|
23
|
+
updatedAt: new Date().toISOString(),
|
|
24
|
+
syncfusionVersion: packageJson.version
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const configPath = path.join(appPath, 'syncfusion.config.json');
|
|
28
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
29
|
+
|
|
30
|
+
return config;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
logger.error(`Failed to generate config: ${error.message}`);
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Path to the syncfusion.config.json for a given project root.
|
|
39
|
+
*/
|
|
40
|
+
export function getConfigPath(appPath) {
|
|
41
|
+
return path.join(appPath, 'syncfusion.config.json');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read syncfusion.config.json from a project root. Returns null if it doesn't exist
|
|
46
|
+
* or can't be parsed.
|
|
47
|
+
*/
|
|
48
|
+
export async function readSyncfusionConfig(appPath) {
|
|
49
|
+
const configPath = getConfigPath(appPath);
|
|
50
|
+
try {
|
|
51
|
+
if (!(await pathExists(configPath))) return null;
|
|
52
|
+
return JSON.parse(await readFile(configPath, 'utf8'));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
logger.warning(`Could not read syncfusion.config.json: ${error.message}`);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Shallow-merge `patch` into the existing syncfusion.config.json (creating a
|
|
61
|
+
* minimal one if it doesn't exist yet) and persist it back to disk.
|
|
62
|
+
*/
|
|
63
|
+
export async function updateSyncfusionConfig(appPath, patch = {}) {
|
|
64
|
+
const configPath = getConfigPath(appPath);
|
|
65
|
+
const existing = (await readSyncfusionConfig(appPath)) || {};
|
|
66
|
+
const merged = {
|
|
67
|
+
...existing,
|
|
68
|
+
...patch,
|
|
69
|
+
updatedAt: new Date().toISOString()
|
|
70
|
+
};
|
|
71
|
+
await writeFile(configPath, JSON.stringify(merged, null, 2));
|
|
72
|
+
return merged;
|
|
73
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
|
|
5
|
+
export async function createProjectFolderStructure(appPath, template = 'empty') {
|
|
6
|
+
try {
|
|
7
|
+
// Base folders - always create these
|
|
8
|
+
const baseFolders = [
|
|
9
|
+
path.join(appPath, 'src')
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
// Additional folders - only for non-empty templates
|
|
13
|
+
const additionalFolders = template !== 'empty' ? [
|
|
14
|
+
path.join(appPath, 'src', 'components')
|
|
15
|
+
] : [];
|
|
16
|
+
|
|
17
|
+
const allFolders = [...baseFolders, ...additionalFolders];
|
|
18
|
+
|
|
19
|
+
for (const folder of allFolders) {
|
|
20
|
+
await mkdir(folder, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
logger.error(`Failed to create folder structure: ${error.message}`);
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ansi, wrap } from './ansi-colors.js';
|
|
2
|
+
|
|
3
|
+
export const logger = {
|
|
4
|
+
info(message) {
|
|
5
|
+
console.log(wrap(message, ansi.blue));
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
success(message) {
|
|
9
|
+
console.log(wrap(message, ansi.green));
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
warning(message) {
|
|
13
|
+
console.log(wrap(message, ansi.yellow));
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
error(message) {
|
|
17
|
+
console.log(wrap(message, ansi.red));
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
import { MCP_ASSISTANT_PACKAGES } from '../constants.js';
|
|
5
|
+
import { pathExists } from './common.js';
|
|
6
|
+
|
|
7
|
+
const EDITOR_CONFIGS = {
|
|
8
|
+
vscode: { configDir: '.vscode', configFile: 'mcp.json', rootKey: 'servers', includeType: true },
|
|
9
|
+
cursor: { configDir: '.cursor', configFile: 'mcp.json', rootKey: 'mcpServers', includeType: false },
|
|
10
|
+
codestudio: { configDir: '.codestudio', configFile: 'mcp.json', rootKey: 'servers', includeType: true },
|
|
11
|
+
jetbrains: { configDir: '.jetbrains', configFile: 'mcp.json', rootKey: 'mcpServers', includeType: false, command: process.platform === 'win32' ? 'npx.cmd' : 'npx' }
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const EDITOR_ALIASES = {
|
|
15
|
+
vscode: 'vscode',
|
|
16
|
+
'vs-code': 'vscode',
|
|
17
|
+
vsCode: 'vscode',
|
|
18
|
+
cursor: 'cursor',
|
|
19
|
+
codestudio: 'codestudio',
|
|
20
|
+
codeStudio: 'codestudio',
|
|
21
|
+
'code-studio': 'codestudio',
|
|
22
|
+
jetbrains: 'jetbrains',
|
|
23
|
+
jetBrains: 'jetbrains'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function normalizeEditor(editorKey) {
|
|
27
|
+
return EDITOR_ALIASES[editorKey] || editorKey;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getAssistantPackage(framework) {
|
|
31
|
+
return MCP_ASSISTANT_PACKAGES[framework] || MCP_ASSISTANT_PACKAGES.react;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function serverName(framework) {
|
|
35
|
+
return `sf-${framework}-mcp`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildServerBlock(framework, editorConfig) {
|
|
39
|
+
const serverBlock = {
|
|
40
|
+
command: editorConfig.command || 'npx',
|
|
41
|
+
args: ['-y', `${getAssistantPackage(framework)}@latest`],
|
|
42
|
+
env: {
|
|
43
|
+
// eslint-disable-next-line camelcase
|
|
44
|
+
Syncfusion_API_Key_Path: 'YOUR_API_KEY_FILE_PATH'
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (editorConfig.includeType) {
|
|
49
|
+
return {
|
|
50
|
+
type: 'stdio',
|
|
51
|
+
...serverBlock
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return serverBlock;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function writeMcpJson(appPath, framework, editorKey) {
|
|
59
|
+
const normalizedEditor = normalizeEditor(editorKey);
|
|
60
|
+
const editorConfig = EDITOR_CONFIGS[normalizedEditor];
|
|
61
|
+
|
|
62
|
+
if (!editorConfig) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const configDir = path.join(appPath, editorConfig.configDir);
|
|
67
|
+
const configPath = path.join(configDir, editorConfig.configFile);
|
|
68
|
+
|
|
69
|
+
await mkdir(configDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
let existing = {};
|
|
72
|
+
if (await pathExists(configPath)) {
|
|
73
|
+
try {
|
|
74
|
+
existing = JSON.parse(await readFile(configPath, 'utf8'));
|
|
75
|
+
} catch {
|
|
76
|
+
existing = {};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const rootKey = editorConfig.rootKey;
|
|
81
|
+
const merged = {
|
|
82
|
+
...existing,
|
|
83
|
+
[rootKey]: {
|
|
84
|
+
...(existing[rootKey] || {}),
|
|
85
|
+
[serverName(framework)]: buildServerBlock(framework, editorConfig)
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await writeFile(configPath, JSON.stringify(merged, null, 2));
|
|
90
|
+
return path.relative(appPath, configPath);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
export async function setupMcpForEditors(appPath, framework, editorKeys = ['all']) {
|
|
95
|
+
const normalizedKeys = editorKeys.map(normalizeEditor);
|
|
96
|
+
const keys = normalizedKeys.includes('all')
|
|
97
|
+
? ['vscode', 'cursor', 'codestudio', 'jetbrains']
|
|
98
|
+
: normalizedKeys;
|
|
99
|
+
|
|
100
|
+
const written = [];
|
|
101
|
+
|
|
102
|
+
for (const key of keys) {
|
|
103
|
+
const relPath = await writeMcpJson(appPath, framework, key);
|
|
104
|
+
if (relPath) {
|
|
105
|
+
written.push(key);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return written;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function applyMcpIntegration(appPath, framework, editorKeys) {
|
|
113
|
+
const keys = Array.isArray(editorKeys) ? editorKeys : ['all'];
|
|
114
|
+
const written = await setupMcpForEditors(appPath, framework, keys);
|
|
115
|
+
|
|
116
|
+
if (written.length > 0) {
|
|
117
|
+
logger.success('\n✅ MCP server configuration added.');
|
|
118
|
+
logger.info(`Update the configuration file with your Syncfusion API key, then open the file(s) and click "Start" to connect to the MCP server. \nFor more information, see: https://ej2.syncfusion.com/${framework}/documentation/mcp-server/ai-coding-assistant/getting-started`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { integrated: written.length > 0, editors: written };
|
|
122
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { pathExists } from './common.js';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_SYNCFUSION_VERSION = '34.2.2';
|
|
6
|
+
|
|
7
|
+
async function readJsonFile(filePath) {
|
|
8
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function writeJsonFile(filePath, data) {
|
|
12
|
+
await writeFile(filePath, JSON.stringify(data, null, 2));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect which package manager the project uses, based on lockfiles.
|
|
17
|
+
* Defaults to npm.
|
|
18
|
+
*/
|
|
19
|
+
export async function detectPackageManager(appPath) {
|
|
20
|
+
if (await pathExists(path.join(appPath, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
21
|
+
if (await pathExists(path.join(appPath, 'yarn.lock'))) return 'yarn';
|
|
22
|
+
if (await pathExists(path.join(appPath, 'bun.lockb'))) return 'bun';
|
|
23
|
+
return 'npm';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Return the install command/args for a given package manager, used to
|
|
28
|
+
* (re)install dependencies, e.g. `npm install`.
|
|
29
|
+
*/
|
|
30
|
+
export function getInstallCommand(pkgManager) {
|
|
31
|
+
switch (pkgManager) {
|
|
32
|
+
case 'yarn':
|
|
33
|
+
return { command: 'yarn', args: ['install'] };
|
|
34
|
+
case 'pnpm':
|
|
35
|
+
return { command: 'pnpm', args: ['install'] };
|
|
36
|
+
case 'bun':
|
|
37
|
+
return { command: 'bun', args: ['install'] };
|
|
38
|
+
default:
|
|
39
|
+
return { command: 'npm', args: ['install'] };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Return the command/args to upgrade one or more packages to latest, for the
|
|
45
|
+
* given package manager.
|
|
46
|
+
*/
|
|
47
|
+
export function getUpgradeCommand(pkgManager, packages, version = 'latest') {
|
|
48
|
+
const versionRange = version === 'latest' ? 'latest' : normalizeSyncfusionVersion(version);
|
|
49
|
+
const targets = packages.map((p) => `${p}@${versionRange}`);
|
|
50
|
+
switch (pkgManager) {
|
|
51
|
+
case 'yarn':
|
|
52
|
+
return { command: 'yarn', args: ['add', ...targets] };
|
|
53
|
+
case 'pnpm':
|
|
54
|
+
return { command: 'pnpm', args: ['add', ...targets] };
|
|
55
|
+
case 'bun':
|
|
56
|
+
return { command: 'bun', args: ['add', ...targets] };
|
|
57
|
+
default:
|
|
58
|
+
return { command: 'npm', args: ['install', ...targets] };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Read package.json from a project root and return every @syncfusion/*
|
|
64
|
+
* dependency (name -> version string) found in dependencies/devDependencies.
|
|
65
|
+
*/
|
|
66
|
+
export async function getSyncfusionPackages(appPath) {
|
|
67
|
+
const packageJsonPath = path.join(appPath, 'package.json');
|
|
68
|
+
if (!(await pathExists(packageJsonPath))) return {};
|
|
69
|
+
|
|
70
|
+
const packageJson = await readJsonFile(packageJsonPath);
|
|
71
|
+
const deps = { ...(packageJson.dependencies || {}), ...(packageJson.devDependencies || {}) };
|
|
72
|
+
|
|
73
|
+
return Object.fromEntries(
|
|
74
|
+
Object.entries(deps).filter(([name]) => name.startsWith('@syncfusion/'))
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeSyncfusionVersion(version = DEFAULT_SYNCFUSION_VERSION) {
|
|
79
|
+
const normalized = String(version || DEFAULT_SYNCFUSION_VERSION)
|
|
80
|
+
.trim()
|
|
81
|
+
.replace(/^--/, '')
|
|
82
|
+
.replace(/^~/, '');
|
|
83
|
+
|
|
84
|
+
return normalized === 'latest' ? 'latest' : `~${normalized}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function splitPackageSpec(pkg) {
|
|
88
|
+
const lastAtIndex = pkg.lastIndexOf('@');
|
|
89
|
+
|
|
90
|
+
if (pkg.startsWith('@') && lastAtIndex === 0) {
|
|
91
|
+
return { name: pkg, version: undefined };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (lastAtIndex > 0) {
|
|
95
|
+
return {
|
|
96
|
+
name: pkg.substring(0, lastAtIndex),
|
|
97
|
+
version: pkg.substring(lastAtIndex + 1)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { name: pkg, version: undefined };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolvePackageVersion(name, version, syncfusionVersion) {
|
|
105
|
+
if (name.startsWith('@syncfusion/')) {
|
|
106
|
+
return normalizeSyncfusionVersion(version || syncfusionVersion);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return version || '*';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function addPackagesToJson(appPath, packages) {
|
|
113
|
+
if (!packages.length) return;
|
|
114
|
+
|
|
115
|
+
const packageJsonPath = path.join(appPath, 'package.json');
|
|
116
|
+
const packageJson = await readJsonFile(packageJsonPath);
|
|
117
|
+
packageJson.dependencies = packageJson.dependencies || {};
|
|
118
|
+
|
|
119
|
+
// Add packages to dependencies. Syncfusion packages are pinned with the
|
|
120
|
+
// default tilde range, for example: "@syncfusion/ej2-react-grids": "~34.2.2".
|
|
121
|
+
// Non-Syncfusion packages continue to use their specified version or "*".
|
|
122
|
+
for (const pkg of packages) {
|
|
123
|
+
const { name, version } = splitPackageSpec(pkg);
|
|
124
|
+
packageJson.dependencies[name] = resolvePackageVersion(name, version, DEFAULT_SYNCFUSION_VERSION);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await writeJsonFile(packageJsonPath, packageJson);
|
|
128
|
+
}
|