@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.
@@ -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 };