@agiflowai/aicode-toolkit 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/LICENSE +661 -0
- package/README.md +151 -0
- package/dist/cli.cjs +732 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +727 -0
- package/dist/index.cjs +17 -0
- package/dist/index.d.cts +282 -0
- package/dist/index.d.ts +282 -0
- package/dist/index.js +4 -0
- package/dist/mcp-Bdxvi2Ej.cjs +4 -0
- package/dist/mcp-BmhiAfeF.js +47 -0
- package/dist/mcp-CZIiB-6Y.js +3 -0
- package/dist/mcp-Dwt8nYQV.cjs +65 -0
- package/dist/services-DNldrNnu.js +739 -0
- package/dist/services-s1vmufE4.cjs +859 -0
- package/package.json +85 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ProjectType, messages, print } from "@agiflowai/aicode-utils";
|
|
3
|
+
import * as fs from "fs-extra";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import gradient from "gradient-string";
|
|
6
|
+
import { execa } from "execa";
|
|
7
|
+
import { CLAUDE_CODE, CODEX, ClaudeCodeService, CodexService, GEMINI_CLI, GeminiCliService, NONE } from "@agiflowai/coding-agent-bridge";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
|
|
10
|
+
//#region src/constants/theme.ts
|
|
11
|
+
/**
|
|
12
|
+
* Theme color constants for AICode Toolkit
|
|
13
|
+
* Defines the brand color palette used throughout the CLI
|
|
14
|
+
*/
|
|
15
|
+
const THEME = { colors: {
|
|
16
|
+
primary: {
|
|
17
|
+
default: "#10b981",
|
|
18
|
+
dark: "#059669",
|
|
19
|
+
text: "#ffffff"
|
|
20
|
+
},
|
|
21
|
+
secondary: {
|
|
22
|
+
default: "#0d9488",
|
|
23
|
+
dark: "#0f766e",
|
|
24
|
+
light: "#14b8a6",
|
|
25
|
+
text: "#ffffff"
|
|
26
|
+
},
|
|
27
|
+
accent: {
|
|
28
|
+
default: "#c44569",
|
|
29
|
+
dark: "#c44569",
|
|
30
|
+
text: "#2a0b14"
|
|
31
|
+
},
|
|
32
|
+
semantic: {
|
|
33
|
+
info: "#5fb3d4",
|
|
34
|
+
success: "#5fb368",
|
|
35
|
+
error: "#d45959",
|
|
36
|
+
alert: "#d4b359"
|
|
37
|
+
},
|
|
38
|
+
cta: {
|
|
39
|
+
from: "#10b981",
|
|
40
|
+
to: "#0d9488",
|
|
41
|
+
text: "#ffffff"
|
|
42
|
+
},
|
|
43
|
+
transparent: "rgba(0, 0, 0, 0)",
|
|
44
|
+
white: "#c4cccf",
|
|
45
|
+
black: "#424549",
|
|
46
|
+
background: {
|
|
47
|
+
dark: {
|
|
48
|
+
default: "#0f0f0f",
|
|
49
|
+
shade: "#141414",
|
|
50
|
+
dark: "#0a0a0a",
|
|
51
|
+
light: "#1a1a1a"
|
|
52
|
+
},
|
|
53
|
+
light: {
|
|
54
|
+
default: "#fff",
|
|
55
|
+
shade: "#EAEAEA",
|
|
56
|
+
dark: "#17202a",
|
|
57
|
+
light: "#EAEAEA"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} };
|
|
61
|
+
/**
|
|
62
|
+
* Gradient colors for banner (primary green -> secondary teal)
|
|
63
|
+
*/
|
|
64
|
+
const BANNER_GRADIENT = [
|
|
65
|
+
THEME.colors.primary.default,
|
|
66
|
+
THEME.colors.primary.dark,
|
|
67
|
+
THEME.colors.secondary.default,
|
|
68
|
+
THEME.colors.secondary.dark
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/utils/banner.ts
|
|
73
|
+
/**
|
|
74
|
+
* ASCII art for AICode Toolkit - simple and highly readable design
|
|
75
|
+
* Uses clean block style with clear spacing
|
|
76
|
+
*/
|
|
77
|
+
const ASCII_ART = `
|
|
78
|
+
█████╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗
|
|
79
|
+
██╔══██╗██║██╔════╝██╔═══██╗██╔══██╗██╔════╝
|
|
80
|
+
███████║██║██║ ██║ ██║██║ ██║█████╗
|
|
81
|
+
██╔══██║██║██║ ██║ ██║██║ ██║██╔══╝
|
|
82
|
+
██║ ██║██║╚██████╗╚██████╔╝██████╔╝███████╗
|
|
83
|
+
╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
|
|
84
|
+
|
|
85
|
+
████████╗ ██████╗ ██████╗ ██╗ ██╗ ██╗██╗████████╗
|
|
86
|
+
╚══██╔══╝██╔═══██╗██╔═══██╗██║ ██║ ██╔╝██║╚══██╔══╝
|
|
87
|
+
██║ ██║ ██║██║ ██║██║ █████╔╝ ██║ ██║
|
|
88
|
+
██║ ██║ ██║██║ ██║██║ ██╔═██╗ ██║ ██║
|
|
89
|
+
██║ ╚██████╔╝╚██████╔╝███████╗██║ ██╗██║ ██║
|
|
90
|
+
╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
|
|
91
|
+
`;
|
|
92
|
+
/**
|
|
93
|
+
* Displays the AICode Toolkit banner with gradient effect
|
|
94
|
+
* Uses gradient-string with theme colors (primary green -> secondary teal)
|
|
95
|
+
*/
|
|
96
|
+
function displayBanner() {
|
|
97
|
+
const bannerGradient = gradient(BANNER_GRADIENT);
|
|
98
|
+
console.log(bannerGradient.multiline(ASCII_ART));
|
|
99
|
+
console.log(bannerGradient(" AI-Powered Code Toolkit for Modern Development"));
|
|
100
|
+
console.log(chalk.dim(" v0.6.0"));
|
|
101
|
+
console.log();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Simplified banner for compact display
|
|
105
|
+
*/
|
|
106
|
+
function displayCompactBanner() {
|
|
107
|
+
const titleGradient = gradient(BANNER_GRADIENT);
|
|
108
|
+
console.log();
|
|
109
|
+
console.log(chalk.bold("▸ ") + titleGradient("AICode Toolkit") + chalk.dim(" v0.6.0"));
|
|
110
|
+
console.log(chalk.dim(" AI-Powered Code Toolkit"));
|
|
111
|
+
console.log();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/utils/git.ts
|
|
116
|
+
/**
|
|
117
|
+
* Execute a git command safely using execa to prevent command injection
|
|
118
|
+
*/
|
|
119
|
+
async function execGit(args, cwd) {
|
|
120
|
+
try {
|
|
121
|
+
await execa("git", args, { cwd });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const execaError = error;
|
|
124
|
+
throw new Error(`Git command failed: ${execaError.stderr || execaError.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Execute git init safely using execa to prevent command injection
|
|
129
|
+
*/
|
|
130
|
+
async function gitInit(projectPath) {
|
|
131
|
+
try {
|
|
132
|
+
await execa("git", ["init", projectPath]);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const execaError = error;
|
|
135
|
+
throw new Error(`Git init failed: ${execaError.stderr || execaError.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Find the workspace root by searching upwards for .git folder
|
|
140
|
+
* Returns null if no .git folder is found (indicating a new project setup is needed)
|
|
141
|
+
*/
|
|
142
|
+
async function findWorkspaceRoot(startPath = process.cwd()) {
|
|
143
|
+
let currentPath = path.resolve(startPath);
|
|
144
|
+
const rootPath = path.parse(currentPath).root;
|
|
145
|
+
while (true) {
|
|
146
|
+
const gitPath = path.join(currentPath, ".git");
|
|
147
|
+
if (await fs.pathExists(gitPath)) return currentPath;
|
|
148
|
+
if (currentPath === rootPath) return null;
|
|
149
|
+
currentPath = path.dirname(currentPath);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Parse GitHub URL to detect if it's a subdirectory
|
|
154
|
+
* Supports formats:
|
|
155
|
+
* - https://github.com/user/repo
|
|
156
|
+
* - https://github.com/user/repo/tree/branch/path/to/dir
|
|
157
|
+
* - https://github.com/user/repo/tree/main/path/to/dir
|
|
158
|
+
*/
|
|
159
|
+
function parseGitHubUrl(url) {
|
|
160
|
+
const treeMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)$/);
|
|
161
|
+
const blobMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/);
|
|
162
|
+
const rootMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
163
|
+
if (treeMatch || blobMatch) {
|
|
164
|
+
const match = treeMatch || blobMatch;
|
|
165
|
+
return {
|
|
166
|
+
owner: match[1],
|
|
167
|
+
repo: match[2],
|
|
168
|
+
repoUrl: `https://github.com/${match[1]}/${match[2]}.git`,
|
|
169
|
+
branch: match[3],
|
|
170
|
+
subdirectory: match[4],
|
|
171
|
+
isSubdirectory: true
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (rootMatch) return {
|
|
175
|
+
owner: rootMatch[1],
|
|
176
|
+
repo: rootMatch[2],
|
|
177
|
+
repoUrl: `https://github.com/${rootMatch[1]}/${rootMatch[2]}.git`,
|
|
178
|
+
isSubdirectory: false
|
|
179
|
+
};
|
|
180
|
+
return {
|
|
181
|
+
repoUrl: url,
|
|
182
|
+
isSubdirectory: false
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Clone a subdirectory from a git repository using sparse checkout
|
|
187
|
+
*/
|
|
188
|
+
async function cloneSubdirectory(repoUrl, branch, subdirectory, targetFolder) {
|
|
189
|
+
const tempFolder = `${targetFolder}.tmp`;
|
|
190
|
+
try {
|
|
191
|
+
await execGit(["init", tempFolder]);
|
|
192
|
+
await execGit([
|
|
193
|
+
"remote",
|
|
194
|
+
"add",
|
|
195
|
+
"origin",
|
|
196
|
+
repoUrl
|
|
197
|
+
], tempFolder);
|
|
198
|
+
await execGit([
|
|
199
|
+
"config",
|
|
200
|
+
"core.sparseCheckout",
|
|
201
|
+
"true"
|
|
202
|
+
], tempFolder);
|
|
203
|
+
const sparseCheckoutFile = path.join(tempFolder, ".git", "info", "sparse-checkout");
|
|
204
|
+
await fs.writeFile(sparseCheckoutFile, `${subdirectory}\n`);
|
|
205
|
+
await execGit([
|
|
206
|
+
"pull",
|
|
207
|
+
"--depth=1",
|
|
208
|
+
"origin",
|
|
209
|
+
branch
|
|
210
|
+
], tempFolder);
|
|
211
|
+
const sourceDir = path.join(tempFolder, subdirectory);
|
|
212
|
+
if (!await fs.pathExists(sourceDir)) throw new Error(`Subdirectory '${subdirectory}' not found in repository at branch '${branch}'`);
|
|
213
|
+
if (await fs.pathExists(targetFolder)) throw new Error(`Target folder already exists: ${targetFolder}`);
|
|
214
|
+
await fs.move(sourceDir, targetFolder);
|
|
215
|
+
await fs.remove(tempFolder);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (await fs.pathExists(tempFolder)) await fs.remove(tempFolder);
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Clone entire repository
|
|
223
|
+
*/
|
|
224
|
+
async function cloneRepository(repoUrl, targetFolder) {
|
|
225
|
+
await execGit([
|
|
226
|
+
"clone",
|
|
227
|
+
repoUrl,
|
|
228
|
+
targetFolder
|
|
229
|
+
]);
|
|
230
|
+
const gitFolder = path.join(targetFolder, ".git");
|
|
231
|
+
if (await fs.pathExists(gitFolder)) await fs.remove(gitFolder);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Fetch directory listing from GitHub API
|
|
235
|
+
*/
|
|
236
|
+
async function fetchGitHubDirectoryContents(owner, repo, path$1, branch = "main") {
|
|
237
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path$1}?ref=${branch}`;
|
|
238
|
+
const response = await fetch(url, { headers: {
|
|
239
|
+
Accept: "application/vnd.github.v3+json",
|
|
240
|
+
"User-Agent": "scaffold-mcp"
|
|
241
|
+
} });
|
|
242
|
+
if (!response.ok) throw new Error(`Failed to fetch directory contents: ${response.statusText}`);
|
|
243
|
+
const data = await response.json();
|
|
244
|
+
if (!Array.isArray(data)) throw new Error("Expected directory but got file");
|
|
245
|
+
return data.map((item) => ({
|
|
246
|
+
name: item.name,
|
|
247
|
+
type: item.type,
|
|
248
|
+
path: item.path
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/services/CodingAgentService.ts
|
|
254
|
+
var CodingAgentService = class {
|
|
255
|
+
workspaceRoot;
|
|
256
|
+
constructor(workspaceRoot) {
|
|
257
|
+
this.workspaceRoot = workspaceRoot;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Detect which coding agent is enabled in the workspace
|
|
261
|
+
* Checks for Claude Code, Codex, and Gemini CLI installations
|
|
262
|
+
* @param workspaceRoot - The workspace root directory
|
|
263
|
+
* @returns Promise resolving to detected agent ID or null
|
|
264
|
+
*/
|
|
265
|
+
static async detectCodingAgent(workspaceRoot) {
|
|
266
|
+
if (await new ClaudeCodeService({ workspaceRoot }).isEnabled()) return CLAUDE_CODE;
|
|
267
|
+
if (await new CodexService({ workspaceRoot }).isEnabled()) return CODEX;
|
|
268
|
+
if (await new GeminiCliService({ workspaceRoot }).isEnabled()) return GEMINI_CLI;
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get available coding agents with their descriptions
|
|
273
|
+
*/
|
|
274
|
+
static getAvailableAgents() {
|
|
275
|
+
return [
|
|
276
|
+
{
|
|
277
|
+
value: CLAUDE_CODE,
|
|
278
|
+
name: "Claude Code",
|
|
279
|
+
description: "Anthropic Claude Code CLI agent"
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
value: CODEX,
|
|
283
|
+
name: "Codex",
|
|
284
|
+
description: "OpenAI Codex CLI agent"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
value: GEMINI_CLI,
|
|
288
|
+
name: "Gemini CLI",
|
|
289
|
+
description: "Google Gemini CLI agent"
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
value: NONE,
|
|
293
|
+
name: "Other",
|
|
294
|
+
description: "Other coding agent or skip MCP configuration"
|
|
295
|
+
}
|
|
296
|
+
];
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Setup MCP configuration for the selected coding agent
|
|
300
|
+
* @param agent - The coding agent to configure
|
|
301
|
+
*/
|
|
302
|
+
async setupMCP(agent) {
|
|
303
|
+
if (agent === NONE) {
|
|
304
|
+
print.info("Skipping MCP configuration");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
print.info(`\nSetting up MCP for ${agent}...`);
|
|
308
|
+
let service = null;
|
|
309
|
+
let configLocation = "";
|
|
310
|
+
let restartInstructions = "";
|
|
311
|
+
if (agent === CLAUDE_CODE) {
|
|
312
|
+
service = new ClaudeCodeService({ workspaceRoot: this.workspaceRoot });
|
|
313
|
+
configLocation = ".mcp.json";
|
|
314
|
+
restartInstructions = "Restart Claude Code to load the new MCP servers";
|
|
315
|
+
} else if (agent === CODEX) {
|
|
316
|
+
service = new CodexService({ workspaceRoot: this.workspaceRoot });
|
|
317
|
+
configLocation = "~/.codex/config.toml";
|
|
318
|
+
restartInstructions = "Restart Codex CLI to load the new MCP servers";
|
|
319
|
+
} else if (agent === GEMINI_CLI) {
|
|
320
|
+
service = new GeminiCliService({ workspaceRoot: this.workspaceRoot });
|
|
321
|
+
configLocation = "~/.gemini/settings.json";
|
|
322
|
+
restartInstructions = "Restart Gemini CLI to load the new MCP servers";
|
|
323
|
+
}
|
|
324
|
+
if (!service) {
|
|
325
|
+
print.info(`MCP configuration for ${agent} is not yet supported.`);
|
|
326
|
+
print.info("Please configure MCP servers manually for this coding agent.");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
await service.updateMcpSettings({ servers: {
|
|
330
|
+
"scaffold-mcp": {
|
|
331
|
+
type: "stdio",
|
|
332
|
+
command: "npx",
|
|
333
|
+
args: [
|
|
334
|
+
"-y",
|
|
335
|
+
"@agiflowai/scaffold-mcp",
|
|
336
|
+
"mcp-serve"
|
|
337
|
+
],
|
|
338
|
+
disabled: false
|
|
339
|
+
},
|
|
340
|
+
"architect-mcp": {
|
|
341
|
+
type: "stdio",
|
|
342
|
+
command: "npx",
|
|
343
|
+
args: [
|
|
344
|
+
"-y",
|
|
345
|
+
"@agiflowai/architect-mcp",
|
|
346
|
+
"mcp-serve"
|
|
347
|
+
],
|
|
348
|
+
disabled: false
|
|
349
|
+
}
|
|
350
|
+
} });
|
|
351
|
+
print.success(`Added scaffold-mcp and architect-mcp to ${configLocation}`);
|
|
352
|
+
print.info("\nNext steps:");
|
|
353
|
+
print.indent(`1. ${restartInstructions}`);
|
|
354
|
+
print.indent("2. The scaffold-mcp and architect-mcp servers will be available");
|
|
355
|
+
print.success("\nMCP configuration completed!");
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region src/services/NewProjectService.ts
|
|
361
|
+
const RESERVED_PROJECT_NAMES = [
|
|
362
|
+
".",
|
|
363
|
+
"..",
|
|
364
|
+
"CON",
|
|
365
|
+
"PRN",
|
|
366
|
+
"AUX",
|
|
367
|
+
"NUL",
|
|
368
|
+
"COM1",
|
|
369
|
+
"COM2",
|
|
370
|
+
"COM3",
|
|
371
|
+
"COM4",
|
|
372
|
+
"COM5",
|
|
373
|
+
"COM6",
|
|
374
|
+
"COM7",
|
|
375
|
+
"COM8",
|
|
376
|
+
"COM9",
|
|
377
|
+
"LPT1",
|
|
378
|
+
"LPT2",
|
|
379
|
+
"LPT3",
|
|
380
|
+
"LPT4",
|
|
381
|
+
"LPT5",
|
|
382
|
+
"LPT6",
|
|
383
|
+
"LPT7",
|
|
384
|
+
"LPT8",
|
|
385
|
+
"LPT9"
|
|
386
|
+
];
|
|
387
|
+
var NewProjectService = class {
|
|
388
|
+
providedName;
|
|
389
|
+
providedProjectType;
|
|
390
|
+
constructor(providedName, providedProjectType) {
|
|
391
|
+
this.providedName = providedName;
|
|
392
|
+
this.providedProjectType = providedProjectType;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Validate project name against naming rules
|
|
396
|
+
* @param value - Project name to validate
|
|
397
|
+
* @returns true if valid, error message string if invalid
|
|
398
|
+
*/
|
|
399
|
+
validateProjectName(value) {
|
|
400
|
+
const trimmed = value.trim();
|
|
401
|
+
if (!trimmed) return "Project name is required";
|
|
402
|
+
if (!/^[a-zA-Z0-9]/.test(trimmed)) return "Project name must start with a letter or number";
|
|
403
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(trimmed)) return "Project name can only contain letters, numbers, hyphens, and underscores";
|
|
404
|
+
if (RESERVED_PROJECT_NAMES.includes(trimmed.toUpperCase())) return "Project name uses a reserved name";
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Validate project type
|
|
409
|
+
* @param projectType - Project type to validate
|
|
410
|
+
* @throws Error if invalid project type
|
|
411
|
+
*/
|
|
412
|
+
validateProjectType(projectType) {
|
|
413
|
+
if (projectType !== ProjectType.MONOLITH && projectType !== ProjectType.MONOREPO) throw new Error(`Invalid project type '${projectType}'. Must be '${ProjectType.MONOLITH}' or '${ProjectType.MONOREPO}'`);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Get the provided name from constructor
|
|
417
|
+
*/
|
|
418
|
+
getProvidedName() {
|
|
419
|
+
return this.providedName;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Get the provided project type from constructor
|
|
423
|
+
*/
|
|
424
|
+
getProvidedProjectType() {
|
|
425
|
+
return this.providedProjectType;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Create project directory atomically
|
|
429
|
+
* @param projectPath - Full path where project should be created
|
|
430
|
+
* @param projectName - Name of the project (for error messages)
|
|
431
|
+
*/
|
|
432
|
+
async createProjectDirectory(projectPath, projectName) {
|
|
433
|
+
try {
|
|
434
|
+
await fs.mkdir(projectPath, { recursive: false });
|
|
435
|
+
print.success(`Created project directory: ${projectPath}`);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (error.code === "EEXIST") throw new Error(`Directory '${projectName}' already exists. Please choose a different name.`);
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Clone an existing Git repository
|
|
443
|
+
* @param repoUrl - Repository URL to clone
|
|
444
|
+
* @param projectPath - Destination path for the cloned repository
|
|
445
|
+
*/
|
|
446
|
+
async cloneExistingRepository(repoUrl, projectPath) {
|
|
447
|
+
print.info("Cloning repository...");
|
|
448
|
+
try {
|
|
449
|
+
const parsed = parseGitHubUrl(repoUrl.trim());
|
|
450
|
+
if (parsed.isSubdirectory && parsed.branch && parsed.subdirectory) await cloneSubdirectory(parsed.repoUrl, parsed.branch, parsed.subdirectory, projectPath);
|
|
451
|
+
else await cloneRepository(parsed.repoUrl, projectPath);
|
|
452
|
+
print.success("Repository cloned successfully");
|
|
453
|
+
} catch (error) {
|
|
454
|
+
await fs.remove(projectPath);
|
|
455
|
+
throw new Error(`Failed to clone repository: ${error.message}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Initialize a new Git repository
|
|
460
|
+
* @param projectPath - Path where git repository should be initialized
|
|
461
|
+
*/
|
|
462
|
+
async initializeGitRepository(projectPath) {
|
|
463
|
+
print.info("Initializing Git repository...");
|
|
464
|
+
try {
|
|
465
|
+
await gitInit(projectPath);
|
|
466
|
+
print.success("Git repository initialized");
|
|
467
|
+
} catch (error) {
|
|
468
|
+
messages.warning(`Failed to initialize Git: ${error.message}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Validate repository URL format
|
|
473
|
+
* @param value - Repository URL to validate
|
|
474
|
+
* @returns true if valid, error message string if invalid
|
|
475
|
+
*/
|
|
476
|
+
validateRepositoryUrl(value) {
|
|
477
|
+
if (!value.trim()) return "Repository URL is required";
|
|
478
|
+
if (!value.match(/^(https?:\/\/|git@)/)) return "Please enter a valid Git repository URL";
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/services/TemplateSelectionService.ts
|
|
485
|
+
var TemplateSelectionService = class {
|
|
486
|
+
tmpDir;
|
|
487
|
+
constructor(existingTmpDir) {
|
|
488
|
+
this.tmpDir = existingTmpDir || path.join(os.tmpdir(), `aicode-templates-${Date.now()}`);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Download templates to OS tmp directory
|
|
492
|
+
* @param repoConfig - Repository configuration
|
|
493
|
+
* @returns Path to the tmp directory containing templates
|
|
494
|
+
*/
|
|
495
|
+
async downloadTemplatesToTmp(repoConfig) {
|
|
496
|
+
print.info(`Downloading templates from ${repoConfig.owner}/${repoConfig.repo}...`);
|
|
497
|
+
try {
|
|
498
|
+
await fs.ensureDir(this.tmpDir);
|
|
499
|
+
const contents = await fetchGitHubDirectoryContents(repoConfig.owner, repoConfig.repo, repoConfig.path, repoConfig.branch);
|
|
500
|
+
const templateDirs = contents.filter((item) => item.type === "dir");
|
|
501
|
+
const globalFiles = contents.filter((item) => item.type === "file" && item.name === "RULES.yaml");
|
|
502
|
+
if (templateDirs.length === 0) throw new Error("No templates found in repository");
|
|
503
|
+
print.info(`Found ${templateDirs.length} template(s), downloading...`);
|
|
504
|
+
for (const template of templateDirs) {
|
|
505
|
+
const targetFolder = path.join(this.tmpDir, template.name);
|
|
506
|
+
print.info(`Downloading ${template.name}...`);
|
|
507
|
+
await cloneSubdirectory(`https://github.com/${repoConfig.owner}/${repoConfig.repo}.git`, repoConfig.branch, template.path, targetFolder);
|
|
508
|
+
print.success(`Downloaded ${template.name}`);
|
|
509
|
+
}
|
|
510
|
+
if (globalFiles.length > 0) {
|
|
511
|
+
print.info("Downloading global RULES.yaml...");
|
|
512
|
+
const rulesUrl = `https://raw.githubusercontent.com/${repoConfig.owner}/${repoConfig.repo}/${repoConfig.branch}/${repoConfig.path}/RULES.yaml`;
|
|
513
|
+
const targetFile = path.join(this.tmpDir, "RULES.yaml");
|
|
514
|
+
const response = await fetch(rulesUrl);
|
|
515
|
+
if (response.ok) {
|
|
516
|
+
const content = await response.text();
|
|
517
|
+
await fs.writeFile(targetFile, content, "utf-8");
|
|
518
|
+
print.success("Downloaded global RULES.yaml");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
print.success(`\nAll templates downloaded to ${this.tmpDir}`);
|
|
522
|
+
return this.tmpDir;
|
|
523
|
+
} catch (error) {
|
|
524
|
+
await this.cleanup();
|
|
525
|
+
throw new Error(`Failed to download templates: ${error.message}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* List available templates in the tmp directory
|
|
530
|
+
* @returns Array of template information
|
|
531
|
+
*/
|
|
532
|
+
async listTemplates() {
|
|
533
|
+
try {
|
|
534
|
+
const entries = await fs.readdir(this.tmpDir, { withFileTypes: true });
|
|
535
|
+
const templates = [];
|
|
536
|
+
for (const entry of entries) if (entry.isDirectory()) {
|
|
537
|
+
const templatePath = path.join(this.tmpDir, entry.name);
|
|
538
|
+
const description = await this.readTemplateDescription(templatePath);
|
|
539
|
+
templates.push({
|
|
540
|
+
name: entry.name,
|
|
541
|
+
path: templatePath,
|
|
542
|
+
description
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
return templates;
|
|
546
|
+
} catch (error) {
|
|
547
|
+
throw new Error(`Failed to list templates: ${error.message}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Copy selected templates to destination
|
|
552
|
+
* @param templateNames - Names of templates to copy
|
|
553
|
+
* @param destinationPath - Destination templates folder path
|
|
554
|
+
* @param projectType - Project type (monolith allows only single template)
|
|
555
|
+
* @param selectedMcpServers - Optional array of selected MCP servers to filter files
|
|
556
|
+
*/
|
|
557
|
+
async copyTemplates(templateNames, destinationPath, projectType, selectedMcpServers) {
|
|
558
|
+
try {
|
|
559
|
+
if (projectType === ProjectType.MONOLITH && templateNames.length > 1) throw new Error("Monolith projects can only use a single template");
|
|
560
|
+
await fs.ensureDir(destinationPath);
|
|
561
|
+
print.info(`\nCopying templates to ${destinationPath}...`);
|
|
562
|
+
for (const templateName of templateNames) {
|
|
563
|
+
const sourcePath = path.join(this.tmpDir, templateName);
|
|
564
|
+
const targetPath = path.join(destinationPath, templateName);
|
|
565
|
+
if (!await fs.pathExists(sourcePath)) throw new Error(`Template '${templateName}' not found in downloaded templates`);
|
|
566
|
+
if (await fs.pathExists(targetPath)) {
|
|
567
|
+
print.info(`Skipping ${templateName} (already exists)`);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
print.info(`Copying ${templateName}...`);
|
|
571
|
+
if (selectedMcpServers && selectedMcpServers.length > 0) await this.copyTemplateWithMcpFilter(sourcePath, targetPath, selectedMcpServers);
|
|
572
|
+
else await fs.copy(sourcePath, targetPath);
|
|
573
|
+
print.success(`Copied ${templateName}`);
|
|
574
|
+
}
|
|
575
|
+
const globalRulesSource = path.join(this.tmpDir, "RULES.yaml");
|
|
576
|
+
const globalRulesTarget = path.join(destinationPath, "RULES.yaml");
|
|
577
|
+
if (await fs.pathExists(globalRulesSource)) {
|
|
578
|
+
if (!await fs.pathExists(globalRulesTarget)) {
|
|
579
|
+
print.info("Copying global RULES.yaml...");
|
|
580
|
+
await fs.copy(globalRulesSource, globalRulesTarget);
|
|
581
|
+
print.success("Copied global RULES.yaml");
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
print.success("\nTemplates copied successfully!");
|
|
585
|
+
} catch (error) {
|
|
586
|
+
throw new Error(`Failed to copy templates: ${error.message}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Copy template files with MCP server filtering
|
|
591
|
+
* @param sourcePath - Source template path
|
|
592
|
+
* @param targetPath - Target template path
|
|
593
|
+
* @param selectedMcpServers - Selected MCP servers
|
|
594
|
+
*/
|
|
595
|
+
async copyTemplateWithMcpFilter(sourcePath, targetPath, selectedMcpServers) {
|
|
596
|
+
const { MCPServer: MCPServer$1, MCP_CONFIG_FILES: MCP_CONFIG_FILES$1 } = await import("./mcp-CZIiB-6Y.js");
|
|
597
|
+
const architectFiles = MCP_CONFIG_FILES$1[MCPServer$1.ARCHITECT];
|
|
598
|
+
const hasArchitect = selectedMcpServers.includes(MCPServer$1.ARCHITECT);
|
|
599
|
+
const hasScaffold = selectedMcpServers.includes(MCPServer$1.SCAFFOLD);
|
|
600
|
+
await fs.ensureDir(targetPath);
|
|
601
|
+
const entries = await fs.readdir(sourcePath, { withFileTypes: true });
|
|
602
|
+
for (const entry of entries) {
|
|
603
|
+
const entrySourcePath = path.join(sourcePath, entry.name);
|
|
604
|
+
const entryTargetPath = path.join(targetPath, entry.name);
|
|
605
|
+
const isArchitectFile = architectFiles.includes(entry.name);
|
|
606
|
+
if (hasArchitect && hasScaffold) if (entry.isDirectory()) await fs.copy(entrySourcePath, entryTargetPath);
|
|
607
|
+
else await fs.copy(entrySourcePath, entryTargetPath);
|
|
608
|
+
else if (hasArchitect && !hasScaffold) {
|
|
609
|
+
if (isArchitectFile) await fs.copy(entrySourcePath, entryTargetPath);
|
|
610
|
+
} else if (!hasArchitect && hasScaffold) {
|
|
611
|
+
if (!isArchitectFile) if (entry.isDirectory()) await fs.copy(entrySourcePath, entryTargetPath);
|
|
612
|
+
else await fs.copy(entrySourcePath, entryTargetPath);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Read template description from README or scaffold.yaml
|
|
618
|
+
* @param templatePath - Path to the template directory
|
|
619
|
+
* @returns Description string or undefined
|
|
620
|
+
*/
|
|
621
|
+
async readTemplateDescription(templatePath) {
|
|
622
|
+
try {
|
|
623
|
+
const scaffoldYamlPath = path.join(templatePath, "scaffold.yaml");
|
|
624
|
+
if (await fs.pathExists(scaffoldYamlPath)) {
|
|
625
|
+
const yaml = await import("js-yaml");
|
|
626
|
+
const content = await fs.readFile(scaffoldYamlPath, "utf-8");
|
|
627
|
+
const scaffoldConfig = yaml.load(content);
|
|
628
|
+
if (scaffoldConfig?.description) return scaffoldConfig.description;
|
|
629
|
+
if (scaffoldConfig?.boilerplate?.[0]?.description) return scaffoldConfig.boilerplate[0].description;
|
|
630
|
+
}
|
|
631
|
+
const readmePath = path.join(templatePath, "README.md");
|
|
632
|
+
if (await fs.pathExists(readmePath)) return (await fs.readFile(readmePath, "utf-8")).split("\n\n")[0].substring(0, 200).trim();
|
|
633
|
+
return;
|
|
634
|
+
} catch {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Get the tmp directory path
|
|
640
|
+
*/
|
|
641
|
+
getTmpDir() {
|
|
642
|
+
return this.tmpDir;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Clean up tmp directory
|
|
646
|
+
*/
|
|
647
|
+
async cleanup() {
|
|
648
|
+
try {
|
|
649
|
+
if (await fs.pathExists(this.tmpDir)) {
|
|
650
|
+
await fs.remove(this.tmpDir);
|
|
651
|
+
print.info("Cleaned up temporary files");
|
|
652
|
+
}
|
|
653
|
+
} catch (error) {
|
|
654
|
+
print.warning(`Warning: Failed to clean up tmp directory: ${error.message}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region src/services/TemplatesService.ts
|
|
661
|
+
var TemplatesService = class {
|
|
662
|
+
/**
|
|
663
|
+
* Download templates from a GitHub repository with UI feedback
|
|
664
|
+
* @param templatesPath - Local path where templates should be downloaded
|
|
665
|
+
* @param repoConfig - Repository configuration (owner, repo, branch, path)
|
|
666
|
+
*/
|
|
667
|
+
async downloadTemplates(templatesPath, repoConfig) {
|
|
668
|
+
print.info(`Fetching templates from ${repoConfig.owner}/${repoConfig.repo}...`);
|
|
669
|
+
try {
|
|
670
|
+
const templateDirs = (await fetchGitHubDirectoryContents(repoConfig.owner, repoConfig.repo, repoConfig.path, repoConfig.branch)).filter((item) => item.type === "dir");
|
|
671
|
+
if (templateDirs.length === 0) {
|
|
672
|
+
messages.warning("No templates found in repository");
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
print.info(`Found ${templateDirs.length} template(s)`);
|
|
676
|
+
let downloaded = 0;
|
|
677
|
+
let skipped = 0;
|
|
678
|
+
for (const template of templateDirs) {
|
|
679
|
+
const targetFolder = path.join(templatesPath, template.name);
|
|
680
|
+
if (await fs.pathExists(targetFolder)) {
|
|
681
|
+
print.info(`Skipping ${template.name} (already exists)`);
|
|
682
|
+
skipped++;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
print.info(`Downloading ${template.name}...`);
|
|
686
|
+
await cloneSubdirectory(`https://github.com/${repoConfig.owner}/${repoConfig.repo}.git`, repoConfig.branch, template.path, targetFolder);
|
|
687
|
+
print.success(`Downloaded ${template.name}`);
|
|
688
|
+
downloaded++;
|
|
689
|
+
}
|
|
690
|
+
print.success("\nAll templates downloaded successfully!");
|
|
691
|
+
} catch (error) {
|
|
692
|
+
throw new Error(`Failed to download templates: ${error.message}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Initialize templates folder with README
|
|
697
|
+
* @param templatesPath - Path where templates folder should be created
|
|
698
|
+
*/
|
|
699
|
+
async initializeTemplatesFolder(templatesPath) {
|
|
700
|
+
await fs.ensureDir(templatesPath);
|
|
701
|
+
await fs.writeFile(path.join(templatesPath, "README.md"), `# Templates
|
|
702
|
+
|
|
703
|
+
This folder contains boilerplate templates and scaffolding methods for your projects.
|
|
704
|
+
|
|
705
|
+
## Templates
|
|
706
|
+
|
|
707
|
+
Templates are organized by framework/technology and include configuration files (\`scaffold.yaml\`) that define:
|
|
708
|
+
- Boilerplates: Full project starter templates
|
|
709
|
+
- Features: Code scaffolding methods for adding new features to existing projects
|
|
710
|
+
|
|
711
|
+
## Adding More Templates
|
|
712
|
+
|
|
713
|
+
Use the \`add\` command to add templates from remote repositories:
|
|
714
|
+
|
|
715
|
+
\`\`\`bash
|
|
716
|
+
scaffold-mcp add --name my-template --url https://github.com/user/template
|
|
717
|
+
\`\`\`
|
|
718
|
+
|
|
719
|
+
Or add templates from subdirectories:
|
|
720
|
+
|
|
721
|
+
\`\`\`bash
|
|
722
|
+
scaffold-mcp add --name nextjs-template --url https://github.com/user/repo/tree/main/templates/nextjs
|
|
723
|
+
\`\`\`
|
|
724
|
+
|
|
725
|
+
## Creating Custom Templates
|
|
726
|
+
|
|
727
|
+
Each template should have a \`scaffold.yaml\` configuration file defining:
|
|
728
|
+
- \`boilerplate\`: Array of boilerplate configurations
|
|
729
|
+
- \`features\`: Array of feature scaffold configurations
|
|
730
|
+
|
|
731
|
+
Template files use Liquid syntax for variable placeholders: \`{{ variableName }}\`
|
|
732
|
+
|
|
733
|
+
See existing templates for examples and documentation for more details.
|
|
734
|
+
`);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
//#endregion
|
|
739
|
+
export { BANNER_GRADIENT, CodingAgentService, NewProjectService, THEME, TemplateSelectionService, TemplatesService, cloneRepository, cloneSubdirectory, displayBanner, displayCompactBanner, fetchGitHubDirectoryContents, findWorkspaceRoot, gitInit, parseGitHubUrl };
|