@agiflowai/scaffold-mcp 0.5.0 → 0.6.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/dist/index.cjs +219 -27
- package/package.json +4 -2
package/dist/index.cjs
CHANGED
|
@@ -8,20 +8,20 @@ let commander = require("commander");
|
|
|
8
8
|
commander = require_chunk.__toESM(commander);
|
|
9
9
|
let node_path = require("node:path");
|
|
10
10
|
node_path = require_chunk.__toESM(node_path);
|
|
11
|
-
let fs_extra = require("fs-extra");
|
|
12
|
-
fs_extra = require_chunk.__toESM(fs_extra);
|
|
13
11
|
let __agiflowai_aicode_utils = require("@agiflowai/aicode-utils");
|
|
14
12
|
__agiflowai_aicode_utils = require_chunk.__toESM(__agiflowai_aicode_utils);
|
|
15
|
-
let
|
|
16
|
-
|
|
17
|
-
let
|
|
18
|
-
|
|
13
|
+
let fs_extra = require("fs-extra");
|
|
14
|
+
fs_extra = require_chunk.__toESM(fs_extra);
|
|
15
|
+
let execa = require("execa");
|
|
16
|
+
execa = require_chunk.__toESM(execa);
|
|
19
17
|
let __composio_json_schema_to_zod = require("@composio/json-schema-to-zod");
|
|
20
18
|
__composio_json_schema_to_zod = require_chunk.__toESM(__composio_json_schema_to_zod);
|
|
21
19
|
let js_yaml = require("js-yaml");
|
|
22
20
|
js_yaml = require_chunk.__toESM(js_yaml);
|
|
23
21
|
let zod = require("zod");
|
|
24
22
|
zod = require_chunk.__toESM(zod);
|
|
23
|
+
let __inquirer_prompts = require("@inquirer/prompts");
|
|
24
|
+
__inquirer_prompts = require_chunk.__toESM(__inquirer_prompts);
|
|
25
25
|
let __modelcontextprotocol_sdk_server_index_js = require("@modelcontextprotocol/sdk/server/index.js");
|
|
26
26
|
__modelcontextprotocol_sdk_server_index_js = require_chunk.__toESM(__modelcontextprotocol_sdk_server_index_js);
|
|
27
27
|
let __modelcontextprotocol_sdk_types_js = require("@modelcontextprotocol/sdk/types.js");
|
|
@@ -38,7 +38,31 @@ let __modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/
|
|
|
38
38
|
__modelcontextprotocol_sdk_server_stdio_js = require_chunk.__toESM(__modelcontextprotocol_sdk_server_stdio_js);
|
|
39
39
|
|
|
40
40
|
//#region src/utils/git.ts
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Execute a git command safely using execa to prevent command injection
|
|
43
|
+
*/
|
|
44
|
+
async function execGit(args, cwd) {
|
|
45
|
+
try {
|
|
46
|
+
await (0, execa.execa)("git", args, { cwd });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const execaError = error;
|
|
49
|
+
throw new Error(`Git command failed: ${execaError.stderr || execaError.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Find the workspace root by searching upwards for .git folder
|
|
54
|
+
* Returns null if no .git folder is found (indicating a new project setup is needed)
|
|
55
|
+
*/
|
|
56
|
+
async function findWorkspaceRoot(startPath = process.cwd()) {
|
|
57
|
+
let currentPath = node_path.default.resolve(startPath);
|
|
58
|
+
const rootPath = node_path.default.parse(currentPath).root;
|
|
59
|
+
while (true) {
|
|
60
|
+
const gitPath = node_path.default.join(currentPath, ".git");
|
|
61
|
+
if (await fs_extra.pathExists(gitPath)) return currentPath;
|
|
62
|
+
if (currentPath === rootPath) return null;
|
|
63
|
+
currentPath = node_path.default.dirname(currentPath);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
42
66
|
/**
|
|
43
67
|
* Parse GitHub URL to detect if it's a subdirectory
|
|
44
68
|
* Supports formats:
|
|
@@ -78,14 +102,29 @@ function parseGitHubUrl(url) {
|
|
|
78
102
|
async function cloneSubdirectory(repoUrl, branch, subdirectory, targetFolder) {
|
|
79
103
|
const tempFolder = `${targetFolder}.tmp`;
|
|
80
104
|
try {
|
|
81
|
-
await
|
|
82
|
-
await
|
|
83
|
-
|
|
105
|
+
await execGit(["init", tempFolder]);
|
|
106
|
+
await execGit([
|
|
107
|
+
"remote",
|
|
108
|
+
"add",
|
|
109
|
+
"origin",
|
|
110
|
+
repoUrl
|
|
111
|
+
], tempFolder);
|
|
112
|
+
await execGit([
|
|
113
|
+
"config",
|
|
114
|
+
"core.sparseCheckout",
|
|
115
|
+
"true"
|
|
116
|
+
], tempFolder);
|
|
84
117
|
const sparseCheckoutFile = node_path.default.join(tempFolder, ".git", "info", "sparse-checkout");
|
|
85
118
|
await fs_extra.writeFile(sparseCheckoutFile, `${subdirectory}\n`);
|
|
86
|
-
await
|
|
119
|
+
await execGit([
|
|
120
|
+
"pull",
|
|
121
|
+
"--depth=1",
|
|
122
|
+
"origin",
|
|
123
|
+
branch
|
|
124
|
+
], tempFolder);
|
|
87
125
|
const sourceDir = node_path.default.join(tempFolder, subdirectory);
|
|
88
126
|
if (!await fs_extra.pathExists(sourceDir)) throw new Error(`Subdirectory '${subdirectory}' not found in repository at branch '${branch}'`);
|
|
127
|
+
if (await fs_extra.pathExists(targetFolder)) throw new Error(`Target folder already exists: ${targetFolder}`);
|
|
89
128
|
await fs_extra.move(sourceDir, targetFolder);
|
|
90
129
|
await fs_extra.remove(tempFolder);
|
|
91
130
|
} catch (error) {
|
|
@@ -97,7 +136,11 @@ async function cloneSubdirectory(repoUrl, branch, subdirectory, targetFolder) {
|
|
|
97
136
|
* Clone entire repository
|
|
98
137
|
*/
|
|
99
138
|
async function cloneRepository(repoUrl, targetFolder) {
|
|
100
|
-
await
|
|
139
|
+
await execGit([
|
|
140
|
+
"clone",
|
|
141
|
+
repoUrl,
|
|
142
|
+
targetFolder
|
|
143
|
+
]);
|
|
101
144
|
const gitFolder = node_path.default.join(targetFolder, ".git");
|
|
102
145
|
if (await fs_extra.pathExists(gitFolder)) await fs_extra.remove(gitFolder);
|
|
103
146
|
}
|
|
@@ -125,9 +168,9 @@ async function fetchGitHubDirectoryContents(owner, repo, path$6, branch = "main"
|
|
|
125
168
|
/**
|
|
126
169
|
* Add command - add a template to templates folder
|
|
127
170
|
*/
|
|
128
|
-
const addCommand = new commander.Command("add").description("Add a template to templates folder").requiredOption("--name <name>", "Template name").requiredOption("--url <url>", "URL of the template repository to download").option("--path <path>", "
|
|
171
|
+
const addCommand = new commander.Command("add").description("Add a template to templates folder").requiredOption("--name <name>", "Template name").requiredOption("--url <url>", "URL of the template repository to download").option("--path <path>", "Override templates folder path (uses toolkit.yaml config by default)").option("--type <type>", "Template type: boilerplate or scaffold", "boilerplate").action(async (options) => {
|
|
129
172
|
try {
|
|
130
|
-
const templatesPath = node_path.default.resolve(options.path);
|
|
173
|
+
const templatesPath = options.path ? node_path.default.resolve(options.path) : await __agiflowai_aicode_utils.TemplatesManagerService.findTemplatesPath();
|
|
131
174
|
const templateType = options.type.toLowerCase();
|
|
132
175
|
const templateName = options.name;
|
|
133
176
|
const templateUrl = options.url;
|
|
@@ -488,16 +531,14 @@ boilerplateCommand.command("info <boilerplateName>").description("Show detailed
|
|
|
488
531
|
//#endregion
|
|
489
532
|
//#region src/cli/init.ts
|
|
490
533
|
/**
|
|
491
|
-
*
|
|
534
|
+
* Execute git init safely using execa to prevent command injection
|
|
492
535
|
*/
|
|
493
|
-
async function
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
if (currentPath === rootPath) return process.cwd();
|
|
500
|
-
currentPath = node_path.default.dirname(currentPath);
|
|
536
|
+
async function gitInit(projectPath) {
|
|
537
|
+
try {
|
|
538
|
+
await (0, execa.execa)("git", ["init", projectPath]);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
const execaError = error;
|
|
541
|
+
throw new Error(`Git init failed: ${execaError.stderr || execaError.message}`);
|
|
501
542
|
}
|
|
502
543
|
}
|
|
503
544
|
const DEFAULT_TEMPLATE_REPO = {
|
|
@@ -507,6 +548,125 @@ const DEFAULT_TEMPLATE_REPO = {
|
|
|
507
548
|
path: "templates"
|
|
508
549
|
};
|
|
509
550
|
/**
|
|
551
|
+
* Interactive setup for new projects
|
|
552
|
+
* Prompts user for project details when no .git folder is found
|
|
553
|
+
* @param providedName - Optional project name from CLI argument
|
|
554
|
+
* @param providedProjectType - Optional project type from CLI argument
|
|
555
|
+
*/
|
|
556
|
+
async function setupNewProject(providedName, providedProjectType) {
|
|
557
|
+
__agiflowai_aicode_utils.print.header(`\n${__agiflowai_aicode_utils.icons.rocket} New Project Setup`);
|
|
558
|
+
__agiflowai_aicode_utils.print.info("No Git repository detected. Let's set up a new project!\n");
|
|
559
|
+
let projectName;
|
|
560
|
+
const reservedNames = [
|
|
561
|
+
".",
|
|
562
|
+
"..",
|
|
563
|
+
"CON",
|
|
564
|
+
"PRN",
|
|
565
|
+
"AUX",
|
|
566
|
+
"NUL",
|
|
567
|
+
"COM1",
|
|
568
|
+
"COM2",
|
|
569
|
+
"COM3",
|
|
570
|
+
"COM4",
|
|
571
|
+
"COM5",
|
|
572
|
+
"COM6",
|
|
573
|
+
"COM7",
|
|
574
|
+
"COM8",
|
|
575
|
+
"COM9",
|
|
576
|
+
"LPT1",
|
|
577
|
+
"LPT2",
|
|
578
|
+
"LPT3",
|
|
579
|
+
"LPT4",
|
|
580
|
+
"LPT5",
|
|
581
|
+
"LPT6",
|
|
582
|
+
"LPT7",
|
|
583
|
+
"LPT8",
|
|
584
|
+
"LPT9"
|
|
585
|
+
];
|
|
586
|
+
const validateProjectName = (value) => {
|
|
587
|
+
const trimmed = value.trim();
|
|
588
|
+
if (!trimmed) return "Project name is required";
|
|
589
|
+
if (!/^[a-zA-Z0-9]/.test(trimmed)) return "Project name must start with a letter or number";
|
|
590
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(trimmed)) return "Project name can only contain letters, numbers, hyphens, and underscores";
|
|
591
|
+
if (reservedNames.includes(trimmed.toUpperCase())) return "Project name uses a reserved name";
|
|
592
|
+
return true;
|
|
593
|
+
};
|
|
594
|
+
if (providedName) {
|
|
595
|
+
const trimmedName = providedName.trim();
|
|
596
|
+
const validationResult = validateProjectName(trimmedName);
|
|
597
|
+
if (validationResult !== true) throw new Error(validationResult);
|
|
598
|
+
projectName = trimmedName;
|
|
599
|
+
__agiflowai_aicode_utils.print.info(`Project name: ${projectName}`);
|
|
600
|
+
} else projectName = await (0, __inquirer_prompts.input)({
|
|
601
|
+
message: "Enter your project name:",
|
|
602
|
+
validate: validateProjectName
|
|
603
|
+
});
|
|
604
|
+
let projectType;
|
|
605
|
+
if (providedProjectType) {
|
|
606
|
+
if (providedProjectType !== __agiflowai_aicode_utils.ProjectType.MONOLITH && providedProjectType !== __agiflowai_aicode_utils.ProjectType.MONOREPO) throw new Error(`Invalid project type '${providedProjectType}'. Must be '${__agiflowai_aicode_utils.ProjectType.MONOLITH}' or '${__agiflowai_aicode_utils.ProjectType.MONOREPO}'`);
|
|
607
|
+
projectType = providedProjectType;
|
|
608
|
+
__agiflowai_aicode_utils.print.info(`Project type: ${projectType}`);
|
|
609
|
+
} else projectType = await (0, __inquirer_prompts.select)({
|
|
610
|
+
message: "Select project type:",
|
|
611
|
+
choices: [{
|
|
612
|
+
name: "Monolith - Single application structure",
|
|
613
|
+
value: __agiflowai_aicode_utils.ProjectType.MONOLITH,
|
|
614
|
+
description: "Traditional single-application project structure"
|
|
615
|
+
}, {
|
|
616
|
+
name: "Monorepo - Multiple packages/apps in one repository",
|
|
617
|
+
value: __agiflowai_aicode_utils.ProjectType.MONOREPO,
|
|
618
|
+
description: "Multiple packages managed together (uses workspaces)"
|
|
619
|
+
}]
|
|
620
|
+
});
|
|
621
|
+
const hasExistingRepo = await (0, __inquirer_prompts.confirm)({
|
|
622
|
+
message: "Do you have an existing Git repository you want to use?",
|
|
623
|
+
default: false
|
|
624
|
+
});
|
|
625
|
+
const projectPath = node_path.default.join(process.cwd(), projectName.trim());
|
|
626
|
+
try {
|
|
627
|
+
await fs_extra.mkdir(projectPath, { recursive: false });
|
|
628
|
+
__agiflowai_aicode_utils.print.success(`${__agiflowai_aicode_utils.icons.check} Created project directory: ${projectPath}`);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
if (error.code === "EEXIST") throw new Error(`Directory '${projectName}' already exists. Please choose a different name.`);
|
|
631
|
+
throw error;
|
|
632
|
+
}
|
|
633
|
+
if (hasExistingRepo) {
|
|
634
|
+
const repoUrl = await (0, __inquirer_prompts.input)({
|
|
635
|
+
message: "Enter Git repository URL:",
|
|
636
|
+
validate: (value) => {
|
|
637
|
+
if (!value.trim()) return "Repository URL is required";
|
|
638
|
+
if (!value.match(/^(https?:\/\/|git@)/)) return "Please enter a valid Git repository URL";
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
__agiflowai_aicode_utils.print.info(`${__agiflowai_aicode_utils.icons.download} Cloning repository...`);
|
|
643
|
+
try {
|
|
644
|
+
const parsed = parseGitHubUrl(repoUrl.trim());
|
|
645
|
+
if (parsed.isSubdirectory && parsed.branch && parsed.subdirectory) await cloneSubdirectory(parsed.repoUrl, parsed.branch, parsed.subdirectory, projectPath);
|
|
646
|
+
else await cloneRepository(parsed.repoUrl, projectPath);
|
|
647
|
+
__agiflowai_aicode_utils.print.success(`${__agiflowai_aicode_utils.icons.check} Repository cloned successfully`);
|
|
648
|
+
} catch (error) {
|
|
649
|
+
await fs_extra.remove(projectPath);
|
|
650
|
+
throw new Error(`Failed to clone repository: ${error.message}`);
|
|
651
|
+
}
|
|
652
|
+
} else if (await (0, __inquirer_prompts.confirm)({
|
|
653
|
+
message: "Initialize a new Git repository?",
|
|
654
|
+
default: true
|
|
655
|
+
})) {
|
|
656
|
+
__agiflowai_aicode_utils.print.info(`${__agiflowai_aicode_utils.icons.rocket} Initializing Git repository...`);
|
|
657
|
+
try {
|
|
658
|
+
await gitInit(projectPath);
|
|
659
|
+
__agiflowai_aicode_utils.print.success(`${__agiflowai_aicode_utils.icons.check} Git repository initialized`);
|
|
660
|
+
} catch (error) {
|
|
661
|
+
__agiflowai_aicode_utils.messages.warning(`Failed to initialize Git: ${error.message}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
projectPath,
|
|
666
|
+
projectType
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
510
670
|
* Download templates from GitHub repository
|
|
511
671
|
*/
|
|
512
672
|
async function downloadTemplates(templatesPath) {
|
|
@@ -536,11 +696,43 @@ async function downloadTemplates(templatesPath) {
|
|
|
536
696
|
/**
|
|
537
697
|
* Init command - initialize templates folder
|
|
538
698
|
*/
|
|
539
|
-
const initCommand = new commander.Command("init").description("Initialize templates folder structure at workspace root").option("--no-download", "Skip downloading templates from repository").option("--path <path>", "Custom path for templates folder (relative to workspace root)").action(async (options) => {
|
|
699
|
+
const initCommand = new commander.Command("init").description("Initialize templates folder structure at workspace root or create new project").option("--no-download", "Skip downloading templates from repository").option("--path <path>", "Custom path for templates folder (relative to workspace root)").option("--name <name>", "Project name (for new projects)").option("--project-type <type>", "Project type: monolith or monorepo (for new projects)").action(async (options) => {
|
|
540
700
|
try {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
701
|
+
let workspaceRoot = await findWorkspaceRoot();
|
|
702
|
+
let projectType;
|
|
703
|
+
if (!workspaceRoot) {
|
|
704
|
+
const projectSetup = await setupNewProject(options.name, options.projectType);
|
|
705
|
+
workspaceRoot = projectSetup.projectPath;
|
|
706
|
+
projectType = projectSetup.projectType;
|
|
707
|
+
__agiflowai_aicode_utils.print.info(`\n${__agiflowai_aicode_utils.icons.folder} Project type: ${projectType}`);
|
|
708
|
+
}
|
|
709
|
+
let templatesPath = options.path ? node_path.default.join(workspaceRoot, options.path) : node_path.default.join(workspaceRoot, "templates");
|
|
710
|
+
if (await fs_extra.pathExists(templatesPath)) {
|
|
711
|
+
__agiflowai_aicode_utils.messages.warning(`\n⚠️ Templates folder already exists at: ${templatesPath}`);
|
|
712
|
+
if (await (0, __inquirer_prompts.confirm)({
|
|
713
|
+
message: "Do you want to use a different folder for templates?",
|
|
714
|
+
default: false
|
|
715
|
+
})) {
|
|
716
|
+
const alternateFolder = await (0, __inquirer_prompts.input)({
|
|
717
|
+
message: "Enter alternate folder name for templates:",
|
|
718
|
+
default: "my-templates",
|
|
719
|
+
validate: (value) => {
|
|
720
|
+
if (!value.trim()) return "Folder name is required";
|
|
721
|
+
if (!/^[a-zA-Z0-9_\-/]+$/.test(value)) return "Folder name can only contain letters, numbers, hyphens, underscores, and slashes";
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
templatesPath = node_path.default.join(workspaceRoot, alternateFolder.trim());
|
|
726
|
+
const toolkitConfig = {
|
|
727
|
+
templatesPath: alternateFolder.trim(),
|
|
728
|
+
projectType
|
|
729
|
+
};
|
|
730
|
+
__agiflowai_aicode_utils.print.info(`\n${__agiflowai_aicode_utils.icons.config} Creating toolkit.yaml with custom templates path...`);
|
|
731
|
+
await __agiflowai_aicode_utils.TemplatesManagerService.writeToolkitConfig(toolkitConfig, workspaceRoot);
|
|
732
|
+
__agiflowai_aicode_utils.print.success(`${__agiflowai_aicode_utils.icons.check} toolkit.yaml created`);
|
|
733
|
+
} else __agiflowai_aicode_utils.print.info(`\n${__agiflowai_aicode_utils.icons.info} Using existing templates folder`);
|
|
734
|
+
}
|
|
735
|
+
__agiflowai_aicode_utils.print.info(`\n${__agiflowai_aicode_utils.icons.rocket} Initializing templates folder at: ${templatesPath}`);
|
|
544
736
|
await fs_extra.ensureDir(templatesPath);
|
|
545
737
|
await fs_extra.writeFile(node_path.default.join(templatesPath, "README.md"), `# Templates
|
|
546
738
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agiflowai/scaffold-mcp",
|
|
3
3
|
"description": "MCP server for scaffolding applications with boilerplate templates",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"author": "AgiflowIO",
|
|
7
7
|
"repository": {
|
|
@@ -34,9 +34,11 @@
|
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@composio/json-schema-to-zod": "0.1.15",
|
|
37
|
+
"@inquirer/prompts": "^7.8.6",
|
|
37
38
|
"@modelcontextprotocol/sdk": "1.19.1",
|
|
38
39
|
"chalk": "5.6.2",
|
|
39
40
|
"commander": "14.0.1",
|
|
41
|
+
"execa": "^9.5.2",
|
|
40
42
|
"express": "^4.21.2",
|
|
41
43
|
"fs-extra": "11.3.2",
|
|
42
44
|
"js-yaml": "4.1.0",
|
|
@@ -44,7 +46,7 @@
|
|
|
44
46
|
"pino": "^10.0.0",
|
|
45
47
|
"pino-pretty": "^13.1.1",
|
|
46
48
|
"zod": "3.25.76",
|
|
47
|
-
"@agiflowai/aicode-utils": "0.
|
|
49
|
+
"@agiflowai/aicode-utils": "0.6.0"
|
|
48
50
|
},
|
|
49
51
|
"devDependencies": {
|
|
50
52
|
"@types/express": "^5.0.0",
|