@agiflowai/scaffold-mcp 0.5.0 → 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/README.md +11 -71
- package/dist/ScaffoldConfigLoader-CI0T6zdG.js +142 -0
- package/dist/{ScaffoldConfigLoader-DzcV5a_c.cjs → ScaffoldConfigLoader-DQMCLVGD.cjs} +1 -1
- package/dist/ScaffoldConfigLoader-DhthV6xq.js +3 -0
- package/dist/{ScaffoldService-BgFWAOLQ.cjs → ScaffoldService-B-L4gwHt.cjs} +6 -0
- package/dist/ScaffoldService-Cx4ZonaT.cjs +3 -0
- package/dist/ScaffoldService-DVsusUh5.js +3 -0
- package/dist/ScaffoldService-QgQKHMM-.js +290 -0
- package/dist/TemplateService-BZRt3NI8.cjs +3 -0
- package/dist/TemplateService-CiZJA06s.js +79 -0
- package/dist/TemplateService-DropYdp8.js +3 -0
- package/dist/VariableReplacementService-B3qARIC9.js +66 -0
- package/dist/{VariableReplacementService-YUpL5nAC.cjs → VariableReplacementService-BrJ1PdKm.cjs} +1 -1
- package/dist/VariableReplacementService-D8C-IsP-.js +3 -0
- package/dist/cli.cjs +1363 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1355 -0
- package/dist/index.cjs +32 -3144
- package/dist/index.d.cts +798 -0
- package/dist/index.d.ts +799 -0
- package/dist/index.js +7 -0
- package/dist/stdio-Cz5aRdvr.cjs +2210 -0
- package/dist/stdio-Dmpwju2k.js +2074 -0
- package/package.json +19 -4
- package/dist/ScaffoldService-BvD9WvRi.cjs +0 -3
- package/dist/TemplateService-B5EZjPB0.cjs +0 -3
- /package/dist/{ScaffoldConfigLoader-1Pcv9cxm.cjs → ScaffoldConfigLoader-BrmvENTo.cjs} +0 -0
- /package/dist/{TemplateService-_KpkoLfZ.cjs → TemplateService-DRubcvS9.cjs} +0 -0
- /package/dist/{VariableReplacementService-ClshNY_C.cjs → VariableReplacementService-BL84vnKk.cjs} +0 -0
package/README.md
CHANGED
|
@@ -10,7 +10,6 @@ A Model Context Protocol (MCP) server for scaffolding applications with boilerpl
|
|
|
10
10
|
- **Liquid templating**: Use powerful templating engine for dynamic file generation
|
|
11
11
|
- **Variable replacement**: Customize generated code with context-aware variable substitution
|
|
12
12
|
- **Dynamic template discovery**: Automatically finds templates in your workspace
|
|
13
|
-
- **Template management**: Initialize templates folder and add templates from remote repositories
|
|
14
13
|
- **Multiple frameworks**: Support for Next.js, Vite React, and custom boilerplates
|
|
15
14
|
- **Multiple modes**: MCP server mode (stdio/HTTP/SSE) and standalone CLI mode
|
|
16
15
|
- **MCP integration**: Seamlessly works with Claude Code and other MCP-compatible clients
|
|
@@ -149,36 +148,9 @@ Or if installed globally:
|
|
|
149
148
|
|
|
150
149
|
### 2. CLI Commands
|
|
151
150
|
|
|
152
|
-
Use scaffold-mcp as a standalone CLI tool for
|
|
151
|
+
Use scaffold-mcp as a standalone CLI tool for scaffolding projects and adding features.
|
|
153
152
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
```bash
|
|
157
|
-
# Initialize templates folder and auto-download official templates
|
|
158
|
-
scaffold-mcp init
|
|
159
|
-
|
|
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)
|
|
167
|
-
scaffold-mcp add --name my-template --url https://github.com/user/template
|
|
168
|
-
scaffold-mcp add --name nextjs-custom --url https://github.com/user/repo/tree/main/templates/nextjs
|
|
169
|
-
```
|
|
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
|
|
153
|
+
**Note:** Template management (init, add) is now handled by the `@agiflowai/aicode-toolkit` CLI. See [aicode-toolkit documentation](../../apps/aicode-toolkit/README.md) for details.
|
|
182
154
|
|
|
183
155
|
#### Boilerplate Commands
|
|
184
156
|
|
|
@@ -220,53 +192,21 @@ scaffold-mcp scaffold add scaffold-nextjs-page \
|
|
|
220
192
|
|
|
221
193
|
## Quick Start
|
|
222
194
|
|
|
223
|
-
### 1.
|
|
195
|
+
### 1. Setup Templates
|
|
224
196
|
|
|
225
|
-
|
|
197
|
+
For template management (downloading and managing templates), use the `@agiflowai/aicode-toolkit` CLI:
|
|
226
198
|
|
|
227
199
|
```bash
|
|
228
|
-
# Initialize
|
|
229
|
-
|
|
200
|
+
# Initialize workspace and download templates
|
|
201
|
+
npx @agiflowai/aicode-toolkit init
|
|
230
202
|
|
|
231
|
-
#
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
# Skip auto-download if you want to add templates manually
|
|
235
|
-
scaffold-mcp init --no-download
|
|
236
|
-
```
|
|
237
|
-
|
|
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...
|
|
241
|
-
|
|
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:
|
|
247
|
-
|
|
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
|
|
203
|
+
# Add custom templates
|
|
204
|
+
npx @agiflowai/aicode-toolkit add --name my-template --url https://github.com/user/template
|
|
262
205
|
```
|
|
263
206
|
|
|
264
|
-
|
|
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`
|
|
207
|
+
See the [aicode-toolkit documentation](../../apps/aicode-toolkit/README.md) for complete setup instructions.
|
|
268
208
|
|
|
269
|
-
###
|
|
209
|
+
### 2. Create a New Project
|
|
270
210
|
|
|
271
211
|
```bash
|
|
272
212
|
# List available boilerplates
|
|
@@ -280,7 +220,7 @@ scaffold-mcp boilerplate create nextjs-15-boilerplate \
|
|
|
280
220
|
--vars '{"projectName":"my-app","packageName":"@myorg/my-app","appName":"My App"}'
|
|
281
221
|
```
|
|
282
222
|
|
|
283
|
-
###
|
|
223
|
+
### 3. Add Features to Existing Projects
|
|
284
224
|
|
|
285
225
|
```bash
|
|
286
226
|
# List available features for your project
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import yaml from "js-yaml";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
//#region src/services/ScaffoldConfigLoader.ts
|
|
6
|
+
const VariablesSchemaSchema = z.object({
|
|
7
|
+
type: z.literal("object"),
|
|
8
|
+
properties: z.record(z.any()),
|
|
9
|
+
required: z.array(z.string()),
|
|
10
|
+
additionalProperties: z.boolean()
|
|
11
|
+
});
|
|
12
|
+
const ScaffoldConfigEntrySchema = z.object({
|
|
13
|
+
name: z.string(),
|
|
14
|
+
description: z.string().optional(),
|
|
15
|
+
instruction: z.string().optional(),
|
|
16
|
+
targetFolder: z.string().optional(),
|
|
17
|
+
variables_schema: VariablesSchemaSchema,
|
|
18
|
+
includes: z.array(z.string()),
|
|
19
|
+
generator: z.string().optional(),
|
|
20
|
+
patterns: z.array(z.string()).optional()
|
|
21
|
+
});
|
|
22
|
+
const ScaffoldYamlSchema = z.object({
|
|
23
|
+
boilerplate: z.union([ScaffoldConfigEntrySchema, z.array(ScaffoldConfigEntrySchema)]).optional(),
|
|
24
|
+
features: z.union([ScaffoldConfigEntrySchema, z.array(ScaffoldConfigEntrySchema)]).optional()
|
|
25
|
+
}).catchall(z.union([ScaffoldConfigEntrySchema, z.array(ScaffoldConfigEntrySchema)]));
|
|
26
|
+
var ScaffoldConfigLoader = class {
|
|
27
|
+
constructor(fileSystem, templateService) {
|
|
28
|
+
this.fileSystem = fileSystem;
|
|
29
|
+
this.templateService = templateService;
|
|
30
|
+
}
|
|
31
|
+
async parseArchitectConfig(templatePath) {
|
|
32
|
+
const architectPath = path.join(templatePath, "scaffold.yaml");
|
|
33
|
+
if (!await this.fileSystem.pathExists(architectPath)) return null;
|
|
34
|
+
try {
|
|
35
|
+
const content = await this.fileSystem.readFile(architectPath, "utf8");
|
|
36
|
+
const rawConfig = yaml.load(content);
|
|
37
|
+
return ScaffoldYamlSchema.parse(rawConfig);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error instanceof z.ZodError) {
|
|
40
|
+
const errorMessages = error.errors.map((err) => `${err.path.join(".")}: ${err.message}`).join("; ");
|
|
41
|
+
throw new Error(`scaffold.yaml validation failed: ${errorMessages}`);
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Failed to parse scaffold.yaml: ${error instanceof Error ? error.message : String(error)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
parseIncludeEntry(includeEntry, variables) {
|
|
47
|
+
const [pathPart, conditionsPart] = includeEntry.split("?");
|
|
48
|
+
const conditions = {};
|
|
49
|
+
if (conditionsPart) {
|
|
50
|
+
const conditionPairs = conditionsPart.split("&");
|
|
51
|
+
for (const pair of conditionPairs) {
|
|
52
|
+
const [key, value] = pair.split("=");
|
|
53
|
+
if (key && value) conditions[key.trim()] = value.trim();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (pathPart.includes("->")) {
|
|
57
|
+
const [sourcePath, targetPath] = pathPart.split("->").map((p) => p.trim());
|
|
58
|
+
return {
|
|
59
|
+
sourcePath,
|
|
60
|
+
targetPath: this.replaceVariablesInPath(targetPath, variables),
|
|
61
|
+
conditions
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const processedPath = this.replaceVariablesInPath(pathPart.trim(), variables);
|
|
65
|
+
return {
|
|
66
|
+
sourcePath: pathPart.trim(),
|
|
67
|
+
targetPath: processedPath,
|
|
68
|
+
conditions
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
replaceVariablesInPath(pathStr, variables) {
|
|
72
|
+
return this.templateService.renderString(pathStr, variables);
|
|
73
|
+
}
|
|
74
|
+
shouldIncludeFile(conditions, variables) {
|
|
75
|
+
if (!conditions || Object.keys(conditions).length === 0) return true;
|
|
76
|
+
for (const [conditionKey, conditionValue] of Object.entries(conditions)) {
|
|
77
|
+
const variableValue = variables[conditionKey];
|
|
78
|
+
if (conditionValue === "true" || conditionValue === "false") {
|
|
79
|
+
const expectedBoolean = conditionValue === "true";
|
|
80
|
+
if (Boolean(variableValue) !== expectedBoolean) return false;
|
|
81
|
+
} else if (String(variableValue) !== conditionValue) return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
async validateTemplate(templatePath, scaffoldType) {
|
|
86
|
+
const errors = [];
|
|
87
|
+
const missingFiles = [];
|
|
88
|
+
if (!await this.fileSystem.pathExists(templatePath)) {
|
|
89
|
+
errors.push(`Template directory ${templatePath} does not exist`);
|
|
90
|
+
return {
|
|
91
|
+
isValid: false,
|
|
92
|
+
errors,
|
|
93
|
+
missingFiles
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
let architectConfig;
|
|
97
|
+
try {
|
|
98
|
+
architectConfig = await this.parseArchitectConfig(templatePath);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
errors.push(`Failed to parse scaffold.yaml: ${error instanceof Error ? error.message : String(error)}`);
|
|
101
|
+
return {
|
|
102
|
+
isValid: false,
|
|
103
|
+
errors,
|
|
104
|
+
missingFiles
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (!architectConfig) {
|
|
108
|
+
errors.push("scaffold.yaml not found in template directory");
|
|
109
|
+
return {
|
|
110
|
+
isValid: false,
|
|
111
|
+
errors,
|
|
112
|
+
missingFiles
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (!architectConfig[scaffoldType]) {
|
|
116
|
+
const availableTypes = Object.keys(architectConfig).join(", ");
|
|
117
|
+
errors.push(`Scaffold type '${scaffoldType}' not found in scaffold.yaml. Available types: ${availableTypes}`);
|
|
118
|
+
return {
|
|
119
|
+
isValid: false,
|
|
120
|
+
errors,
|
|
121
|
+
missingFiles
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const config = architectConfig[scaffoldType];
|
|
125
|
+
if (config.includes && Array.isArray(config.includes)) for (const includeFile of config.includes) {
|
|
126
|
+
const parsed = this.parseIncludeEntry(includeFile, {});
|
|
127
|
+
const sourcePath = path.join(templatePath, parsed.sourcePath);
|
|
128
|
+
const liquidSourcePath = `${sourcePath}.liquid`;
|
|
129
|
+
const sourceExists = await this.fileSystem.pathExists(sourcePath);
|
|
130
|
+
const liquidExists = await this.fileSystem.pathExists(liquidSourcePath);
|
|
131
|
+
if (!sourceExists && !liquidExists) missingFiles.push(includeFile);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
isValid: errors.length === 0 && missingFiles.length === 0,
|
|
135
|
+
errors,
|
|
136
|
+
missingFiles
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
export { ScaffoldConfigLoader };
|
|
@@ -291,6 +291,12 @@ var ScaffoldService = class {
|
|
|
291
291
|
};
|
|
292
292
|
|
|
293
293
|
//#endregion
|
|
294
|
+
Object.defineProperty(exports, 'ScaffoldProcessingService', {
|
|
295
|
+
enumerable: true,
|
|
296
|
+
get: function () {
|
|
297
|
+
return ScaffoldProcessingService;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
294
300
|
Object.defineProperty(exports, 'ScaffoldService', {
|
|
295
301
|
enumerable: true,
|
|
296
302
|
get: function () {
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { TemplatesManagerService, log } from "@agiflowai/aicode-utils";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
//#region src/services/ScaffoldProcessingService.ts
|
|
6
|
+
/**
|
|
7
|
+
* Shared service for common scaffolding operations like processing templates and tracking files
|
|
8
|
+
*/
|
|
9
|
+
var ScaffoldProcessingService = class {
|
|
10
|
+
constructor(fileSystem, variableReplacer) {
|
|
11
|
+
this.fileSystem = fileSystem;
|
|
12
|
+
this.variableReplacer = variableReplacer;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Process a target path for variable replacement, handling both files and directories
|
|
16
|
+
*/
|
|
17
|
+
async processTargetForVariableReplacement(targetPath, variables) {
|
|
18
|
+
if ((await this.fileSystem.stat(targetPath)).isDirectory()) await this.variableReplacer.processFilesForVariableReplacement(targetPath, variables);
|
|
19
|
+
else await this.variableReplacer.replaceVariablesInFile(targetPath, variables);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Track all created files, handling both single files and directories
|
|
23
|
+
*/
|
|
24
|
+
async trackCreatedFiles(targetPath, createdFiles) {
|
|
25
|
+
if ((await this.fileSystem.stat(targetPath)).isDirectory()) await this.trackCreatedFilesRecursive(targetPath, createdFiles);
|
|
26
|
+
else createdFiles.push(targetPath);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Track all existing files, handling both single files and directories
|
|
30
|
+
*/
|
|
31
|
+
async trackExistingFiles(targetPath, existingFiles) {
|
|
32
|
+
if ((await this.fileSystem.stat(targetPath)).isDirectory()) await this.trackExistingFilesRecursive(targetPath, existingFiles);
|
|
33
|
+
else existingFiles.push(targetPath);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Copy source to target, then process templates and track files
|
|
37
|
+
* Now supports tracking existing files separately from created files
|
|
38
|
+
* Automatically handles .liquid template files by stripping the extension
|
|
39
|
+
*/
|
|
40
|
+
async copyAndProcess(sourcePath, targetPath, variables, createdFiles, existingFiles) {
|
|
41
|
+
await this.fileSystem.ensureDir(path.dirname(targetPath));
|
|
42
|
+
if (await this.fileSystem.pathExists(targetPath) && existingFiles) {
|
|
43
|
+
await this.trackExistingFiles(targetPath, existingFiles);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let actualSourcePath = sourcePath;
|
|
47
|
+
if (!await this.fileSystem.pathExists(sourcePath)) {
|
|
48
|
+
const liquidSourcePath = `${sourcePath}.liquid`;
|
|
49
|
+
if (await this.fileSystem.pathExists(liquidSourcePath)) actualSourcePath = liquidSourcePath;
|
|
50
|
+
else throw new Error(`Source file not found: ${sourcePath} (also tried ${liquidSourcePath})`);
|
|
51
|
+
}
|
|
52
|
+
await this.fileSystem.copy(actualSourcePath, targetPath);
|
|
53
|
+
await this.processTargetForVariableReplacement(targetPath, variables);
|
|
54
|
+
await this.trackCreatedFiles(targetPath, createdFiles);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Recursively collect all file paths in a directory for created files
|
|
58
|
+
*/
|
|
59
|
+
async trackCreatedFilesRecursive(dirPath, createdFiles) {
|
|
60
|
+
let items = [];
|
|
61
|
+
try {
|
|
62
|
+
items = await this.fileSystem.readdir(dirPath);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
log.warn(`Cannot read directory ${dirPath}: ${error}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
for (const item of items) {
|
|
68
|
+
if (!item) continue;
|
|
69
|
+
const itemPath = path.join(dirPath, item);
|
|
70
|
+
try {
|
|
71
|
+
const stat = await this.fileSystem.stat(itemPath);
|
|
72
|
+
if (stat.isDirectory()) await this.trackCreatedFilesRecursive(itemPath, createdFiles);
|
|
73
|
+
else if (stat.isFile()) createdFiles.push(itemPath);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
log.warn(`Cannot stat ${itemPath}: ${error}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Recursively collect all file paths in a directory for existing files
|
|
81
|
+
*/
|
|
82
|
+
async trackExistingFilesRecursive(dirPath, existingFiles) {
|
|
83
|
+
let items = [];
|
|
84
|
+
try {
|
|
85
|
+
items = await this.fileSystem.readdir(dirPath);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
log.warn(`Cannot read directory ${dirPath}: ${error}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const item of items) {
|
|
91
|
+
if (!item) continue;
|
|
92
|
+
const itemPath = path.join(dirPath, item);
|
|
93
|
+
try {
|
|
94
|
+
const stat = await this.fileSystem.stat(itemPath);
|
|
95
|
+
if (stat.isDirectory()) await this.trackExistingFilesRecursive(itemPath, existingFiles);
|
|
96
|
+
else if (stat.isFile()) existingFiles.push(itemPath);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
log.warn(`Cannot stat ${itemPath}: ${error}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/services/ScaffoldService.ts
|
|
106
|
+
var ScaffoldService = class {
|
|
107
|
+
templatesRootPath;
|
|
108
|
+
processingService;
|
|
109
|
+
constructor(fileSystem, scaffoldConfigLoader, variableReplacer, templatesRootPath) {
|
|
110
|
+
this.fileSystem = fileSystem;
|
|
111
|
+
this.scaffoldConfigLoader = scaffoldConfigLoader;
|
|
112
|
+
this.variableReplacer = variableReplacer;
|
|
113
|
+
this.templatesRootPath = templatesRootPath || TemplatesManagerService.findTemplatesPathSync();
|
|
114
|
+
this.processingService = new ScaffoldProcessingService(fileSystem, variableReplacer);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Scaffold a new project from a boilerplate template
|
|
118
|
+
*/
|
|
119
|
+
async useBoilerplate(options) {
|
|
120
|
+
try {
|
|
121
|
+
const { projectName, packageName, targetFolder, templateFolder, boilerplateName, variables = {} } = options;
|
|
122
|
+
const targetPath = path.isAbsolute(targetFolder) ? path.join(targetFolder, projectName) : path.join(process.cwd(), targetFolder, projectName);
|
|
123
|
+
const templatePath = path.join(this.templatesRootPath, templateFolder);
|
|
124
|
+
const validationResult = await this.scaffoldConfigLoader.validateTemplate(templatePath, "boilerplate");
|
|
125
|
+
if (!validationResult.isValid) return {
|
|
126
|
+
success: false,
|
|
127
|
+
message: `Template validation failed: ${[...validationResult.errors, ...validationResult.missingFiles.map((f) => `Template file not found: ${f}`)].join("; ")}`
|
|
128
|
+
};
|
|
129
|
+
if (await this.fileSystem.pathExists(targetPath)) return {
|
|
130
|
+
success: false,
|
|
131
|
+
message: `Directory ${targetPath} already exists`
|
|
132
|
+
};
|
|
133
|
+
const architectConfig = await this.scaffoldConfigLoader.parseArchitectConfig(templatePath);
|
|
134
|
+
if (!architectConfig || !architectConfig.boilerplate) return {
|
|
135
|
+
success: false,
|
|
136
|
+
message: `Invalid architect configuration: missing 'boilerplate' section in scaffold.yaml`
|
|
137
|
+
};
|
|
138
|
+
const boilerplateArray = architectConfig.boilerplate;
|
|
139
|
+
let config;
|
|
140
|
+
if (Array.isArray(boilerplateArray)) {
|
|
141
|
+
config = boilerplateArray.find((b) => b.name === boilerplateName);
|
|
142
|
+
if (!config) return {
|
|
143
|
+
success: false,
|
|
144
|
+
message: `Boilerplate '${boilerplateName}' not found in scaffold configuration`
|
|
145
|
+
};
|
|
146
|
+
} else config = architectConfig.boilerplate;
|
|
147
|
+
const allVariables = {
|
|
148
|
+
...variables,
|
|
149
|
+
projectName,
|
|
150
|
+
packageName
|
|
151
|
+
};
|
|
152
|
+
return await this.processScaffold({
|
|
153
|
+
config,
|
|
154
|
+
targetPath,
|
|
155
|
+
templatePath,
|
|
156
|
+
allVariables,
|
|
157
|
+
scaffoldType: "boilerplate"
|
|
158
|
+
});
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
message: `Error scaffolding boilerplate: ${error instanceof Error ? error.message : String(error)}`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Scaffold a new feature into an existing project
|
|
168
|
+
*/
|
|
169
|
+
async useFeature(options) {
|
|
170
|
+
try {
|
|
171
|
+
const { projectPath, templateFolder, featureName, variables = {} } = options;
|
|
172
|
+
const targetPath = path.resolve(projectPath);
|
|
173
|
+
const templatePath = path.join(this.templatesRootPath, templateFolder);
|
|
174
|
+
const projectName = path.basename(targetPath);
|
|
175
|
+
const validationResult = await this.scaffoldConfigLoader.validateTemplate(templatePath, "features");
|
|
176
|
+
if (!validationResult.isValid) return {
|
|
177
|
+
success: false,
|
|
178
|
+
message: `Template validation failed: ${[...validationResult.errors, ...validationResult.missingFiles.map((f) => `Template file not found: ${f}`)].join("; ")}`
|
|
179
|
+
};
|
|
180
|
+
if (!await this.fileSystem.pathExists(targetPath)) return {
|
|
181
|
+
success: false,
|
|
182
|
+
message: `Target directory ${targetPath} does not exist. Please create the parent directory first.`
|
|
183
|
+
};
|
|
184
|
+
const architectConfig = await this.scaffoldConfigLoader.parseArchitectConfig(templatePath);
|
|
185
|
+
if (!architectConfig || !architectConfig.features) return {
|
|
186
|
+
success: false,
|
|
187
|
+
message: `Invalid architect configuration: missing 'features' section in scaffold.yaml`
|
|
188
|
+
};
|
|
189
|
+
const featureArray = architectConfig.features;
|
|
190
|
+
let config;
|
|
191
|
+
if (Array.isArray(featureArray)) {
|
|
192
|
+
config = featureArray.find((f) => f.name === featureName);
|
|
193
|
+
if (!config) return {
|
|
194
|
+
success: false,
|
|
195
|
+
message: `Feature '${featureName}' not found in scaffold configuration`
|
|
196
|
+
};
|
|
197
|
+
} else config = architectConfig.features;
|
|
198
|
+
const allVariables = {
|
|
199
|
+
...variables,
|
|
200
|
+
projectName,
|
|
201
|
+
appPath: targetPath,
|
|
202
|
+
appName: projectName
|
|
203
|
+
};
|
|
204
|
+
return await this.processScaffold({
|
|
205
|
+
config,
|
|
206
|
+
targetPath,
|
|
207
|
+
templatePath,
|
|
208
|
+
allVariables,
|
|
209
|
+
scaffoldType: "feature"
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
return {
|
|
213
|
+
success: false,
|
|
214
|
+
message: `Error scaffolding feature: ${error instanceof Error ? error.message : String(error)}`
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Common scaffolding processing logic shared by both useBoilerplate and useFeature
|
|
220
|
+
*/
|
|
221
|
+
async processScaffold(params) {
|
|
222
|
+
const { config, targetPath, templatePath, allVariables, scaffoldType } = params;
|
|
223
|
+
log.debug("Config generator:", config.generator);
|
|
224
|
+
log.debug("Config:", JSON.stringify(config, null, 2));
|
|
225
|
+
if (config.generator) {
|
|
226
|
+
log.info("Using custom generator:", config.generator);
|
|
227
|
+
try {
|
|
228
|
+
const generator = (await import(path.join(templatePath, "generators", config.generator))).default;
|
|
229
|
+
if (typeof generator !== "function") return {
|
|
230
|
+
success: false,
|
|
231
|
+
message: `Invalid generator: ${config.generator} does not export a default function`
|
|
232
|
+
};
|
|
233
|
+
return await generator({
|
|
234
|
+
variables: allVariables,
|
|
235
|
+
config,
|
|
236
|
+
targetPath,
|
|
237
|
+
templatePath,
|
|
238
|
+
fileSystem: this.fileSystem,
|
|
239
|
+
scaffoldConfigLoader: this.scaffoldConfigLoader,
|
|
240
|
+
variableReplacer: this.variableReplacer,
|
|
241
|
+
ScaffoldProcessingService: this.processingService.constructor,
|
|
242
|
+
getRootPath: () => {
|
|
243
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
244
|
+
return path.join(__dirname, "../../../../..");
|
|
245
|
+
},
|
|
246
|
+
getProjectPath: (projectPath) => {
|
|
247
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
248
|
+
const rootPath = path.join(__dirname, "../../../../..");
|
|
249
|
+
return projectPath.replace(rootPath, "").replace("/", "");
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
253
|
+
return {
|
|
254
|
+
success: false,
|
|
255
|
+
message: `Error loading or executing generator ${config.generator}: ${error instanceof Error ? error.message : String(error)}`
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const parsedIncludes = [];
|
|
260
|
+
const warnings = [];
|
|
261
|
+
if (config.includes && Array.isArray(config.includes)) for (const includeEntry of config.includes) {
|
|
262
|
+
const parsed = this.scaffoldConfigLoader.parseIncludeEntry(includeEntry, allVariables);
|
|
263
|
+
if (!this.scaffoldConfigLoader.shouldIncludeFile(parsed.conditions, allVariables)) continue;
|
|
264
|
+
parsedIncludes.push(parsed);
|
|
265
|
+
const targetFilePath = path.join(targetPath, parsed.targetPath);
|
|
266
|
+
if (await this.fileSystem.pathExists(targetFilePath)) warnings.push(`File/folder ${parsed.targetPath} already exists and will be preserved`);
|
|
267
|
+
}
|
|
268
|
+
await this.fileSystem.ensureDir(targetPath);
|
|
269
|
+
const createdFiles = [];
|
|
270
|
+
const existingFiles = [];
|
|
271
|
+
for (const parsed of parsedIncludes) {
|
|
272
|
+
const sourcePath = path.join(templatePath, parsed.sourcePath);
|
|
273
|
+
const targetFilePath = path.join(targetPath, parsed.targetPath);
|
|
274
|
+
await this.processingService.copyAndProcess(sourcePath, targetFilePath, allVariables, createdFiles, existingFiles);
|
|
275
|
+
}
|
|
276
|
+
let message = `Successfully scaffolded ${scaffoldType} at ${targetPath}`;
|
|
277
|
+
if (existingFiles.length > 0) message += `. ${existingFiles.length} existing file(s) were preserved`;
|
|
278
|
+
message += ". Run 'pnpm install' to install dependencies.";
|
|
279
|
+
return {
|
|
280
|
+
success: true,
|
|
281
|
+
message,
|
|
282
|
+
warnings: warnings.length > 0 ? warnings : void 0,
|
|
283
|
+
createdFiles: createdFiles.length > 0 ? createdFiles : void 0,
|
|
284
|
+
existingFiles: existingFiles.length > 0 ? existingFiles : void 0
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
//#endregion
|
|
290
|
+
export { ScaffoldProcessingService, ScaffoldService };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { log } from "@agiflowai/aicode-utils";
|
|
2
|
+
import { Liquid } from "liquidjs";
|
|
3
|
+
|
|
4
|
+
//#region src/services/TemplateService.ts
|
|
5
|
+
var TemplateService = class {
|
|
6
|
+
liquid;
|
|
7
|
+
constructor() {
|
|
8
|
+
this.liquid = new Liquid({
|
|
9
|
+
strictFilters: false,
|
|
10
|
+
strictVariables: false
|
|
11
|
+
});
|
|
12
|
+
this.setupCustomFilters();
|
|
13
|
+
log.info("TemplateService initialized");
|
|
14
|
+
}
|
|
15
|
+
toPascalCase(str) {
|
|
16
|
+
const camelCase = str.replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : "");
|
|
17
|
+
return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
|
|
18
|
+
}
|
|
19
|
+
setupCustomFilters() {
|
|
20
|
+
this.liquid.registerFilter("camelCase", (str) => {
|
|
21
|
+
return str.replace(/[-_\s]+(.)?/g, (_, char) => char ? char.toUpperCase() : "");
|
|
22
|
+
});
|
|
23
|
+
this.liquid.registerFilter("pascalCase", (str) => {
|
|
24
|
+
return this.toPascalCase(str);
|
|
25
|
+
});
|
|
26
|
+
this.liquid.registerFilter("titleCase", (str) => {
|
|
27
|
+
return this.toPascalCase(str);
|
|
28
|
+
});
|
|
29
|
+
this.liquid.registerFilter("kebabCase", (str) => {
|
|
30
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
31
|
+
});
|
|
32
|
+
this.liquid.registerFilter("snakeCase", (str) => {
|
|
33
|
+
return str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").toLowerCase();
|
|
34
|
+
});
|
|
35
|
+
this.liquid.registerFilter("upperCase", (str) => {
|
|
36
|
+
return str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").toUpperCase();
|
|
37
|
+
});
|
|
38
|
+
this.liquid.registerFilter("lower", (str) => str.toLowerCase());
|
|
39
|
+
this.liquid.registerFilter("upper", (str) => str.toUpperCase());
|
|
40
|
+
this.liquid.registerFilter("pluralize", (str) => {
|
|
41
|
+
if (str.endsWith("y")) return `${str.slice(0, -1)}ies`;
|
|
42
|
+
else if (str.endsWith("s") || str.endsWith("sh") || str.endsWith("ch") || str.endsWith("x") || str.endsWith("z")) return `${str}es`;
|
|
43
|
+
else return `${str}s`;
|
|
44
|
+
});
|
|
45
|
+
this.liquid.registerFilter("singularize", (str) => {
|
|
46
|
+
if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
|
|
47
|
+
else if (str.endsWith("es")) return str.slice(0, -2);
|
|
48
|
+
else if (str.endsWith("s") && !str.endsWith("ss")) return str.slice(0, -1);
|
|
49
|
+
else return str;
|
|
50
|
+
});
|
|
51
|
+
this.liquid.registerFilter("strip", (str) => {
|
|
52
|
+
return str.trim();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
renderString(template, variables) {
|
|
56
|
+
try {
|
|
57
|
+
log.debug("Rendering template", {
|
|
58
|
+
variables,
|
|
59
|
+
templatePreview: template.substring(0, 100)
|
|
60
|
+
});
|
|
61
|
+
const result = this.liquid.parseAndRenderSync(template, variables);
|
|
62
|
+
log.debug("Rendered template", { resultPreview: result.substring(0, 100) });
|
|
63
|
+
return result;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
log.error("LiquidJS rendering error", {
|
|
66
|
+
error: error instanceof Error ? error.message : String(error),
|
|
67
|
+
templatePreview: template.substring(0, 200),
|
|
68
|
+
variables
|
|
69
|
+
});
|
|
70
|
+
return template;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
containsTemplateVariables(content) {
|
|
74
|
+
return [/\{\{.*?\}\}/, /\{%.*?%\}/].some((pattern) => pattern.test(content));
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
//#endregion
|
|
79
|
+
export { TemplateService };
|