@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.
Files changed (2) hide show
  1. package/dist/index.cjs +219 -27
  2. 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 node_child_process = require("node:child_process");
16
- node_child_process = require_chunk.__toESM(node_child_process);
17
- let node_util = require("node:util");
18
- node_util = require_chunk.__toESM(node_util);
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
- const execAsync = (0, node_util.promisify)(node_child_process.exec);
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 execAsync(`git init "${tempFolder}"`);
82
- await execAsync(`git -C "${tempFolder}" remote add origin ${repoUrl}`);
83
- await execAsync(`git -C "${tempFolder}" config core.sparseCheckout true`);
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 execAsync(`git -C "${tempFolder}" pull --depth=1 origin ${branch}`);
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 execAsync(`git clone ${repoUrl} "${targetFolder}"`);
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>", "Path to templates folder", "./templates").option("--type <type>", "Template type: boilerplate or scaffold", "boilerplate").action(async (options) => {
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
- * Find the workspace root by searching upwards for .git folder
534
+ * Execute git init safely using execa to prevent command injection
492
535
  */
493
- async function findWorkspaceRoot(startPath = process.cwd()) {
494
- let currentPath = node_path.default.resolve(startPath);
495
- const rootPath = node_path.default.parse(currentPath).root;
496
- while (true) {
497
- const gitPath = node_path.default.join(currentPath, ".git");
498
- if (await fs_extra.pathExists(gitPath)) return currentPath;
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
- const workspaceRoot = await findWorkspaceRoot();
542
- const templatesPath = options.path ? node_path.default.join(workspaceRoot, options.path) : node_path.default.join(workspaceRoot, "templates");
543
- __agiflowai_aicode_utils.print.info(`${__agiflowai_aicode_utils.icons.rocket} Initializing templates folder at: ${templatesPath}`);
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.5.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.5.0"
49
+ "@agiflowai/aicode-utils": "0.6.0"
48
50
  },
49
51
  "devDependencies": {
50
52
  "@types/express": "^5.0.0",