@agiflowai/scaffold-mcp 0.2.0 → 0.3.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/README.md +54 -11
- package/dist/index.js +160 -36
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -154,15 +154,32 @@ Use scaffold-mcp as a standalone CLI tool for template management and scaffoldin
|
|
|
154
154
|
#### Template Management
|
|
155
155
|
|
|
156
156
|
```bash
|
|
157
|
-
# Initialize templates folder
|
|
157
|
+
# Initialize templates folder and auto-download official templates
|
|
158
158
|
scaffold-mcp init
|
|
159
|
-
scaffold-mcp init --path /path/to/templates
|
|
160
159
|
|
|
161
|
-
#
|
|
160
|
+
# Initialize at custom path
|
|
161
|
+
scaffold-mcp init --path ./custom-templates
|
|
162
|
+
|
|
163
|
+
# Initialize without downloading templates
|
|
164
|
+
scaffold-mcp init --no-download
|
|
165
|
+
|
|
166
|
+
# Add templates from repositories (full or subdirectory)
|
|
162
167
|
scaffold-mcp add --name my-template --url https://github.com/user/template
|
|
163
|
-
scaffold-mcp add --name
|
|
168
|
+
scaffold-mcp add --name nextjs-custom --url https://github.com/user/repo/tree/main/templates/nextjs
|
|
164
169
|
```
|
|
165
170
|
|
|
171
|
+
**What `init` does:**
|
|
172
|
+
1. Creates `templates/` folder in your workspace root
|
|
173
|
+
2. Automatically downloads official templates from [AgiFlow/aicode-toolkit](https://github.com/AgiFlow/aicode-toolkit/tree/main/templates)
|
|
174
|
+
3. Creates a README.md with usage instructions
|
|
175
|
+
4. Skips templates that already exist (safe to re-run)
|
|
176
|
+
|
|
177
|
+
**What `add` does:**
|
|
178
|
+
1. Parses GitHub URL to detect full repository vs subdirectory
|
|
179
|
+
2. Downloads template using git clone (full repo) or sparse checkout (subdirectory)
|
|
180
|
+
3. Validates template has required configuration files (scaffold.yaml)
|
|
181
|
+
4. Saves template to your templates folder
|
|
182
|
+
|
|
166
183
|
#### Boilerplate Commands
|
|
167
184
|
|
|
168
185
|
```bash
|
|
@@ -205,24 +222,50 @@ scaffold-mcp scaffold add scaffold-nextjs-page \
|
|
|
205
222
|
|
|
206
223
|
### 1. Initialize Templates
|
|
207
224
|
|
|
225
|
+
The `init` command sets up your templates folder and **automatically downloads official templates** from the AgiFlow repository:
|
|
226
|
+
|
|
208
227
|
```bash
|
|
209
|
-
# Initialize templates folder
|
|
228
|
+
# Initialize templates folder and download official templates
|
|
210
229
|
scaffold-mcp init
|
|
211
230
|
|
|
212
231
|
# Or specify a custom path
|
|
213
232
|
scaffold-mcp init --path ./my-templates
|
|
233
|
+
|
|
234
|
+
# Skip auto-download if you want to add templates manually
|
|
235
|
+
scaffold-mcp init --no-download
|
|
214
236
|
```
|
|
215
237
|
|
|
216
|
-
|
|
238
|
+
**What gets downloaded:**
|
|
239
|
+
- ✅ `nextjs-15-drizzle` - Next.js 15 with App Router, TypeScript, Tailwind CSS 4, Storybook, and optional Drizzle ORM
|
|
240
|
+
- ✅ More templates coming soon...
|
|
217
241
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
242
|
+
All templates from [github.com/AgiFlow/aicode-toolkit/templates](https://github.com/AgiFlow/aicode-toolkit/tree/main/templates) are automatically pulled into your workspace.
|
|
243
|
+
|
|
244
|
+
### 2. Add Custom Templates
|
|
245
|
+
|
|
246
|
+
Add additional templates from GitHub repositories or subdirectories:
|
|
221
247
|
|
|
222
|
-
|
|
223
|
-
|
|
248
|
+
```bash
|
|
249
|
+
# Add a template from a full repository
|
|
250
|
+
scaffold-mcp add --name my-template --url https://github.com/yourorg/nextjs-template
|
|
251
|
+
|
|
252
|
+
# Add a template from a repository subdirectory (NEW!)
|
|
253
|
+
scaffold-mcp add \
|
|
254
|
+
--name nextjs-15-drizzle \
|
|
255
|
+
--url https://github.com/AgiFlow/aicode-toolkit/tree/main/templates/nextjs-15-drizzle
|
|
256
|
+
|
|
257
|
+
# Add to a specific type folder
|
|
258
|
+
scaffold-mcp add \
|
|
259
|
+
--name react-component \
|
|
260
|
+
--url https://github.com/yourorg/react-component-scaffold \
|
|
261
|
+
--type scaffold
|
|
224
262
|
```
|
|
225
263
|
|
|
264
|
+
**Supported URL formats:**
|
|
265
|
+
- Full repository: `https://github.com/user/repo`
|
|
266
|
+
- Subdirectory: `https://github.com/user/repo/tree/branch/path/to/template`
|
|
267
|
+
- With `.git` extension: `https://github.com/user/repo.git`
|
|
268
|
+
|
|
226
269
|
### 3. Create a New Project
|
|
227
270
|
|
|
228
271
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -4,13 +4,13 @@ import { ScaffoldConfigLoader } from "./ScaffoldConfigLoader-CI0T6zdG.js";
|
|
|
4
4
|
import { TemplateService } from "./TemplateService-CnxvhRVW.js";
|
|
5
5
|
import { VariableReplacementService } from "./VariableReplacementService-Bq0GDhTo.js";
|
|
6
6
|
import { Command } from "commander";
|
|
7
|
-
import { exec } from "node:child_process";
|
|
8
7
|
import * as path$1 from "node:path";
|
|
9
8
|
import path from "node:path";
|
|
10
|
-
import { promisify } from "node:util";
|
|
11
9
|
import * as fs$1 from "fs-extra";
|
|
12
10
|
import fs from "fs-extra";
|
|
13
11
|
import chalk from "chalk";
|
|
12
|
+
import { exec } from "node:child_process";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
14
|
import { jsonSchemaToZod } from "@composio/json-schema-to-zod";
|
|
15
15
|
import * as yaml$1 from "js-yaml";
|
|
16
16
|
import yaml from "js-yaml";
|
|
@@ -82,7 +82,8 @@ const icons = {
|
|
|
82
82
|
download: "=�",
|
|
83
83
|
upload: "=�",
|
|
84
84
|
gear: "�",
|
|
85
|
-
clipboard: "=�"
|
|
85
|
+
clipboard: "=�",
|
|
86
|
+
skip: "⏭"
|
|
86
87
|
};
|
|
87
88
|
/**
|
|
88
89
|
* Themed message helpers
|
|
@@ -136,9 +137,92 @@ const sections = {
|
|
|
136
137
|
};
|
|
137
138
|
|
|
138
139
|
//#endregion
|
|
139
|
-
//#region src/
|
|
140
|
+
//#region src/utils/git.ts
|
|
140
141
|
const execAsync = promisify(exec);
|
|
141
142
|
/**
|
|
143
|
+
* Parse GitHub URL to detect if it's a subdirectory
|
|
144
|
+
* Supports formats:
|
|
145
|
+
* - https://github.com/user/repo
|
|
146
|
+
* - https://github.com/user/repo/tree/branch/path/to/dir
|
|
147
|
+
* - https://github.com/user/repo/tree/main/path/to/dir
|
|
148
|
+
*/
|
|
149
|
+
function parseGitHubUrl(url) {
|
|
150
|
+
const treeMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)$/);
|
|
151
|
+
const blobMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/);
|
|
152
|
+
const rootMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
153
|
+
if (treeMatch || blobMatch) {
|
|
154
|
+
const match = treeMatch || blobMatch;
|
|
155
|
+
return {
|
|
156
|
+
owner: match[1],
|
|
157
|
+
repo: match[2],
|
|
158
|
+
repoUrl: `https://github.com/${match[1]}/${match[2]}.git`,
|
|
159
|
+
branch: match[3],
|
|
160
|
+
subdirectory: match[4],
|
|
161
|
+
isSubdirectory: true
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (rootMatch) return {
|
|
165
|
+
owner: rootMatch[1],
|
|
166
|
+
repo: rootMatch[2],
|
|
167
|
+
repoUrl: `https://github.com/${rootMatch[1]}/${rootMatch[2]}.git`,
|
|
168
|
+
isSubdirectory: false
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
repoUrl: url,
|
|
172
|
+
isSubdirectory: false
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Clone a subdirectory from a git repository using sparse checkout
|
|
177
|
+
*/
|
|
178
|
+
async function cloneSubdirectory(repoUrl, branch, subdirectory, targetFolder) {
|
|
179
|
+
const tempFolder = `${targetFolder}.tmp`;
|
|
180
|
+
try {
|
|
181
|
+
await execAsync(`git init "${tempFolder}"`);
|
|
182
|
+
await execAsync(`git -C "${tempFolder}" remote add origin ${repoUrl}`);
|
|
183
|
+
await execAsync(`git -C "${tempFolder}" config core.sparseCheckout true`);
|
|
184
|
+
const sparseCheckoutFile = path.join(tempFolder, ".git", "info", "sparse-checkout");
|
|
185
|
+
await fs$1.writeFile(sparseCheckoutFile, `${subdirectory}\n`);
|
|
186
|
+
await execAsync(`git -C "${tempFolder}" pull --depth=1 origin ${branch}`);
|
|
187
|
+
const sourceDir = path.join(tempFolder, subdirectory);
|
|
188
|
+
if (!await fs$1.pathExists(sourceDir)) throw new Error(`Subdirectory '${subdirectory}' not found in repository at branch '${branch}'`);
|
|
189
|
+
await fs$1.move(sourceDir, targetFolder);
|
|
190
|
+
await fs$1.remove(tempFolder);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (await fs$1.pathExists(tempFolder)) await fs$1.remove(tempFolder);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Clone entire repository
|
|
198
|
+
*/
|
|
199
|
+
async function cloneRepository(repoUrl, targetFolder) {
|
|
200
|
+
await execAsync(`git clone ${repoUrl} "${targetFolder}"`);
|
|
201
|
+
const gitFolder = path.join(targetFolder, ".git");
|
|
202
|
+
if (await fs$1.pathExists(gitFolder)) await fs$1.remove(gitFolder);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Fetch directory listing from GitHub API
|
|
206
|
+
*/
|
|
207
|
+
async function fetchGitHubDirectoryContents(owner, repo, path$2, branch = "main") {
|
|
208
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path$2}?ref=${branch}`;
|
|
209
|
+
const response = await fetch(url, { headers: {
|
|
210
|
+
Accept: "application/vnd.github.v3+json",
|
|
211
|
+
"User-Agent": "scaffold-mcp"
|
|
212
|
+
} });
|
|
213
|
+
if (!response.ok) throw new Error(`Failed to fetch directory contents: ${response.statusText}`);
|
|
214
|
+
const data = await response.json();
|
|
215
|
+
if (!Array.isArray(data)) throw new Error("Expected directory but got file");
|
|
216
|
+
return data.map((item) => ({
|
|
217
|
+
name: item.name,
|
|
218
|
+
type: item.type,
|
|
219
|
+
path: item.path
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/cli/add.ts
|
|
225
|
+
/**
|
|
142
226
|
* Add command - add a template to templates folder
|
|
143
227
|
*/
|
|
144
228
|
const addCommand = new 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) => {
|
|
@@ -158,10 +242,12 @@ const addCommand = new Command("add").description("Add a template to templates f
|
|
|
158
242
|
}
|
|
159
243
|
logger.info(`${icons.download} Downloading template '${templateName}' from ${templateUrl}...`);
|
|
160
244
|
await fs$1.ensureDir(path.dirname(targetFolder));
|
|
245
|
+
const parsedUrl = parseGitHubUrl(templateUrl);
|
|
161
246
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
247
|
+
if (parsedUrl.isSubdirectory && parsedUrl.branch && parsedUrl.subdirectory) {
|
|
248
|
+
logger.info(`${icons.folder} Detected subdirectory: ${parsedUrl.subdirectory} (branch: ${parsedUrl.branch})`);
|
|
249
|
+
await cloneSubdirectory(parsedUrl.repoUrl, parsedUrl.branch, parsedUrl.subdirectory, targetFolder);
|
|
250
|
+
} else await cloneRepository(parsedUrl.repoUrl, targetFolder);
|
|
165
251
|
messages.success(`Template '${templateName}' added successfully!`);
|
|
166
252
|
logger.header(`\n${icons.folder} Template location:`);
|
|
167
253
|
logger.indent(targetFolder);
|
|
@@ -531,27 +617,60 @@ async function findWorkspaceRoot(startPath = process.cwd()) {
|
|
|
531
617
|
currentPath = path.dirname(currentPath);
|
|
532
618
|
}
|
|
533
619
|
}
|
|
620
|
+
const DEFAULT_TEMPLATE_REPO = {
|
|
621
|
+
owner: "AgiFlow",
|
|
622
|
+
repo: "aicode-toolkit",
|
|
623
|
+
branch: "main",
|
|
624
|
+
path: "templates"
|
|
625
|
+
};
|
|
626
|
+
/**
|
|
627
|
+
* Download templates from GitHub repository
|
|
628
|
+
*/
|
|
629
|
+
async function downloadTemplates(templatesPath) {
|
|
630
|
+
try {
|
|
631
|
+
logger.info(`${icons.download} Fetching templates from ${DEFAULT_TEMPLATE_REPO.owner}/${DEFAULT_TEMPLATE_REPO.repo}...`);
|
|
632
|
+
const templateDirs = (await fetchGitHubDirectoryContents(DEFAULT_TEMPLATE_REPO.owner, DEFAULT_TEMPLATE_REPO.repo, DEFAULT_TEMPLATE_REPO.path, DEFAULT_TEMPLATE_REPO.branch)).filter((item) => item.type === "dir");
|
|
633
|
+
if (templateDirs.length === 0) {
|
|
634
|
+
messages.warning("No templates found in repository");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
logger.info(`${icons.folder} Found ${templateDirs.length} template(s)`);
|
|
638
|
+
for (const template of templateDirs) {
|
|
639
|
+
const targetFolder = path.join(templatesPath, template.name);
|
|
640
|
+
if (await fs$1.pathExists(targetFolder)) {
|
|
641
|
+
logger.info(`${icons.skip} Skipping ${template.name} (already exists)`);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
logger.info(`${icons.download} Downloading ${template.name}...`);
|
|
645
|
+
const repoUrl = `https://github.com/${DEFAULT_TEMPLATE_REPO.owner}/${DEFAULT_TEMPLATE_REPO.repo}.git`;
|
|
646
|
+
await cloneSubdirectory(repoUrl, DEFAULT_TEMPLATE_REPO.branch, template.path, targetFolder);
|
|
647
|
+
logger.success(`${icons.check} Downloaded ${template.name}`);
|
|
648
|
+
}
|
|
649
|
+
logger.success(`\n${icons.check} All templates downloaded successfully!`);
|
|
650
|
+
} catch (error) {
|
|
651
|
+
throw new Error(`Failed to download templates: ${error.message}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
534
654
|
/**
|
|
535
655
|
* Init command - initialize templates folder
|
|
536
656
|
*/
|
|
537
|
-
const initCommand = new Command("init").description("Initialize templates folder structure at workspace root").action(async () => {
|
|
657
|
+
const initCommand = new 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) => {
|
|
538
658
|
try {
|
|
539
659
|
const workspaceRoot = await findWorkspaceRoot();
|
|
540
|
-
const templatesPath = path.join(workspaceRoot, "templates");
|
|
660
|
+
const templatesPath = options.path ? path.join(workspaceRoot, options.path) : path.join(workspaceRoot, "templates");
|
|
541
661
|
logger.info(`${icons.rocket} Initializing templates folder at: ${templatesPath}`);
|
|
542
662
|
await fs$1.ensureDir(templatesPath);
|
|
543
|
-
await fs$1.ensureDir(path.join(templatesPath, "boilerplates"));
|
|
544
|
-
await fs$1.ensureDir(path.join(templatesPath, "scaffolds"));
|
|
545
663
|
await fs$1.writeFile(path.join(templatesPath, "README.md"), `# Templates
|
|
546
664
|
|
|
547
665
|
This folder contains boilerplate templates and scaffolding methods for your projects.
|
|
548
666
|
|
|
549
|
-
##
|
|
667
|
+
## Templates
|
|
550
668
|
|
|
551
|
-
|
|
552
|
-
-
|
|
669
|
+
Templates are organized by framework/technology and include configuration files (\`scaffold.yaml\`) that define:
|
|
670
|
+
- Boilerplates: Full project starter templates
|
|
671
|
+
- Features: Code scaffolding methods for adding new features to existing projects
|
|
553
672
|
|
|
554
|
-
## Adding Templates
|
|
673
|
+
## Adding More Templates
|
|
555
674
|
|
|
556
675
|
Use the \`add\` command to add templates from remote repositories:
|
|
557
676
|
|
|
@@ -559,31 +678,38 @@ Use the \`add\` command to add templates from remote repositories:
|
|
|
559
678
|
scaffold-mcp add --name my-template --url https://github.com/user/template
|
|
560
679
|
\`\`\`
|
|
561
680
|
|
|
562
|
-
|
|
681
|
+
Or add templates from subdirectories:
|
|
563
682
|
|
|
564
|
-
|
|
683
|
+
\`\`\`bash
|
|
684
|
+
scaffold-mcp add --name nextjs-template --url https://github.com/user/repo/tree/main/templates/nextjs
|
|
685
|
+
\`\`\`
|
|
565
686
|
|
|
566
|
-
|
|
567
|
-
- \`boilerplate.yaml\` - Configuration file
|
|
568
|
-
- Template files with variable placeholders using Liquid syntax (\`{{ variableName }}\`)
|
|
687
|
+
## Creating Custom Templates
|
|
569
688
|
|
|
570
|
-
|
|
689
|
+
Each template should have a \`scaffold.yaml\` configuration file defining:
|
|
690
|
+
- \`boilerplate\`: Array of boilerplate configurations
|
|
691
|
+
- \`features\`: Array of feature scaffold configurations
|
|
571
692
|
|
|
572
|
-
|
|
573
|
-
- \`scaffold.yaml\` - Configuration file
|
|
574
|
-
- Template files organized by project type
|
|
693
|
+
Template files use Liquid syntax for variable placeholders: \`{{ variableName }}\`
|
|
575
694
|
|
|
576
|
-
See documentation for more details
|
|
695
|
+
See existing templates for examples and documentation for more details.
|
|
577
696
|
`);
|
|
578
|
-
logger.success(`${icons.check} Templates folder
|
|
579
|
-
|
|
580
|
-
logger.
|
|
581
|
-
logger.
|
|
582
|
-
logger.indent(
|
|
583
|
-
|
|
584
|
-
|
|
697
|
+
logger.success(`${icons.check} Templates folder created!`);
|
|
698
|
+
if (options.download !== false) await downloadTemplates(templatesPath);
|
|
699
|
+
else logger.info(`${icons.skip} Skipping template download (use --download to enable)`);
|
|
700
|
+
logger.header(`\n${icons.folder} Templates location:`);
|
|
701
|
+
logger.indent(templatesPath);
|
|
702
|
+
const nextSteps = [];
|
|
703
|
+
if (options.download === false) {
|
|
704
|
+
nextSteps.push(`Download templates: scaffold-mcp init --download`);
|
|
705
|
+
nextSteps.push(`Add templates manually: scaffold-mcp add --name <name> --url <url>`);
|
|
706
|
+
} else {
|
|
707
|
+
nextSteps.push(`List available boilerplates: scaffold-mcp boilerplate list`);
|
|
708
|
+
nextSteps.push(`Add more templates: scaffold-mcp add --name <name> --url <url>`);
|
|
709
|
+
}
|
|
710
|
+
sections.nextSteps(nextSteps);
|
|
585
711
|
} catch (error) {
|
|
586
|
-
|
|
712
|
+
messages.error(`Error initializing templates folder: ${error.message}`);
|
|
587
713
|
process.exit(1);
|
|
588
714
|
}
|
|
589
715
|
});
|
|
@@ -1759,9 +1885,7 @@ Best practices:
|
|
|
1759
1885
|
- Use conditional includes with ?variableName=value for optional files
|
|
1760
1886
|
- Use path mapping with -> when source and target paths differ
|
|
1761
1887
|
- Use {{ variableName }} in target paths for dynamic file placement
|
|
1762
|
-
- Avoid wildcards unless you have a good reason
|
|
1763
|
-
|
|
1764
|
-
See templates/nextjs-15/scaffold.yaml features section for examples.`,
|
|
1888
|
+
- Avoid wildcards unless you have a good reason`,
|
|
1765
1889
|
items: { type: "string" }
|
|
1766
1890
|
},
|
|
1767
1891
|
patterns: {
|
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.3.0",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"author": "AgiflowIO",
|
|
7
7
|
"repository": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"js-yaml": "4.1.0",
|
|
43
43
|
"liquidjs": "10.21.1",
|
|
44
44
|
"zod": "3.25.76",
|
|
45
|
-
"@agiflowai/scaffold-generator": "0.
|
|
45
|
+
"@agiflowai/scaffold-generator": "0.3.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/express": "^5.0.0",
|