@agiflowai/aicode-utils 1.0.7 → 1.0.9
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/dist/index.cjs +276 -70
- package/dist/index.d.cts +129 -47
- package/dist/index.d.mts +129 -47
- package/dist/index.mjs +304 -92
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import * as path
|
|
3
|
-
import
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import nodePath from "node:path";
|
|
4
4
|
import * as fs from "node:fs/promises";
|
|
5
5
|
import { accessSync, mkdirSync, readFileSync, readFileSync as readFileSync$1, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import pino from "pino";
|
|
8
8
|
import * as yaml from "js-yaml";
|
|
9
|
+
import { execa } from "execa";
|
|
9
10
|
import chalk from "chalk";
|
|
10
11
|
|
|
11
12
|
//#region rolldown:runtime
|
|
@@ -65,12 +66,6 @@ async function ensureDir(dirPath) {
|
|
|
65
66
|
await fs.mkdir(dirPath, { recursive: true });
|
|
66
67
|
}
|
|
67
68
|
/**
|
|
68
|
-
* Ensure a directory exists (sync), creating it recursively if needed
|
|
69
|
-
*/
|
|
70
|
-
function ensureDirSync(dirPath) {
|
|
71
|
-
mkdirSync(dirPath, { recursive: true });
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
69
|
* Remove a file or directory recursively
|
|
75
70
|
*/
|
|
76
71
|
async function remove(filePath) {
|
|
@@ -89,7 +84,7 @@ async function copy(src, dest) {
|
|
|
89
84
|
* Move a file or directory
|
|
90
85
|
*/
|
|
91
86
|
async function move(src, dest) {
|
|
92
|
-
await ensureDir(
|
|
87
|
+
await ensureDir(nodePath.dirname(dest));
|
|
93
88
|
await fs.rename(src, dest);
|
|
94
89
|
}
|
|
95
90
|
/**
|
|
@@ -113,19 +108,6 @@ async function writeJson(filePath, data, options) {
|
|
|
113
108
|
const content = JSON.stringify(data, null, options?.spaces ?? 2);
|
|
114
109
|
await fs.writeFile(filePath, `${content}\n`, "utf-8");
|
|
115
110
|
}
|
|
116
|
-
/**
|
|
117
|
-
* Write an object as JSON to a file (sync)
|
|
118
|
-
*/
|
|
119
|
-
function writeJsonSync(filePath, data, options) {
|
|
120
|
-
writeFileSync(filePath, `${JSON.stringify(data, null, options?.spaces ?? 2)}\n`, "utf-8");
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Output file - writes content ensuring directory exists
|
|
124
|
-
*/
|
|
125
|
-
async function outputFile(filePath, content) {
|
|
126
|
-
await ensureDir(path.dirname(filePath));
|
|
127
|
-
await fs.writeFile(filePath, content, "utf-8");
|
|
128
|
-
}
|
|
129
111
|
const readFile = fs.readFile;
|
|
130
112
|
const writeFile = fs.writeFile;
|
|
131
113
|
const readdir = fs.readdir;
|
|
@@ -138,12 +120,12 @@ const cp = fs.cp;
|
|
|
138
120
|
|
|
139
121
|
//#endregion
|
|
140
122
|
//#region src/utils/logger.ts
|
|
141
|
-
const logsDir = path
|
|
123
|
+
const logsDir = path.join(os.tmpdir(), "scaffold-mcp-logs");
|
|
142
124
|
const logger = pino({
|
|
143
125
|
level: process.env.LOG_LEVEL || "debug",
|
|
144
126
|
timestamp: pino.stdTimeFunctions.isoTime
|
|
145
127
|
}, pino.destination({
|
|
146
|
-
dest: path
|
|
128
|
+
dest: path.join(logsDir, "scaffold-mcp.log"),
|
|
147
129
|
mkdir: true,
|
|
148
130
|
sync: true
|
|
149
131
|
}));
|
|
@@ -195,36 +177,35 @@ var TemplatesManagerService = class TemplatesManagerService {
|
|
|
195
177
|
* 4. Verify the templates directory exists
|
|
196
178
|
*
|
|
197
179
|
* @param startPath - The path to start searching from (defaults to process.cwd())
|
|
198
|
-
* @returns The absolute path to the templates directory
|
|
199
|
-
* @throws Error if templates directory is not found
|
|
180
|
+
* @returns The absolute path to the templates directory, or null if not found
|
|
200
181
|
*/
|
|
201
182
|
static async findTemplatesPath(startPath = process.cwd()) {
|
|
202
183
|
const workspaceRoot = await TemplatesManagerService.findWorkspaceRoot(startPath);
|
|
203
|
-
const toolkitConfigPath =
|
|
184
|
+
const toolkitConfigPath = nodePath.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
|
|
204
185
|
if (await pathExists(toolkitConfigPath)) {
|
|
205
186
|
const yaml$1 = await import("js-yaml");
|
|
206
187
|
const content = await fs.readFile(toolkitConfigPath, "utf-8");
|
|
207
188
|
const config = yaml$1.load(content);
|
|
208
189
|
if (config?.templatesPath) {
|
|
209
|
-
const templatesPath$1 =
|
|
190
|
+
const templatesPath$1 = nodePath.isAbsolute(config.templatesPath) ? config.templatesPath : nodePath.join(workspaceRoot, config.templatesPath);
|
|
210
191
|
if (await pathExists(templatesPath$1)) return templatesPath$1;
|
|
211
|
-
else
|
|
192
|
+
else return null;
|
|
212
193
|
}
|
|
213
194
|
}
|
|
214
|
-
const templatesPath =
|
|
195
|
+
const templatesPath = nodePath.join(workspaceRoot, TemplatesManagerService.TEMPLATES_FOLDER);
|
|
215
196
|
if (await pathExists(templatesPath)) return templatesPath;
|
|
216
|
-
|
|
197
|
+
return null;
|
|
217
198
|
}
|
|
218
199
|
/**
|
|
219
200
|
* Find the workspace root by searching upwards for .git folder
|
|
220
201
|
*/
|
|
221
202
|
static async findWorkspaceRoot(startPath) {
|
|
222
|
-
let currentPath =
|
|
223
|
-
const rootPath =
|
|
203
|
+
let currentPath = nodePath.resolve(startPath);
|
|
204
|
+
const rootPath = nodePath.parse(currentPath).root;
|
|
224
205
|
while (true) {
|
|
225
|
-
if (await pathExists(
|
|
206
|
+
if (await pathExists(nodePath.join(currentPath, ".git"))) return currentPath;
|
|
226
207
|
if (currentPath === rootPath) return process.cwd();
|
|
227
|
-
currentPath =
|
|
208
|
+
currentPath = nodePath.dirname(currentPath);
|
|
228
209
|
}
|
|
229
210
|
}
|
|
230
211
|
/**
|
|
@@ -232,36 +213,35 @@ var TemplatesManagerService = class TemplatesManagerService {
|
|
|
232
213
|
* Use this when you need immediate access and are sure templates exist.
|
|
233
214
|
*
|
|
234
215
|
* @param startPath - The path to start searching from (defaults to process.cwd())
|
|
235
|
-
* @returns The absolute path to the templates directory
|
|
236
|
-
* @throws Error if templates directory is not found
|
|
216
|
+
* @returns The absolute path to the templates directory, or null if not found
|
|
237
217
|
*/
|
|
238
218
|
static findTemplatesPathSync(startPath = process.cwd()) {
|
|
239
219
|
const workspaceRoot = TemplatesManagerService.findWorkspaceRootSync(startPath);
|
|
240
|
-
const toolkitConfigPath =
|
|
220
|
+
const toolkitConfigPath = nodePath.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
|
|
241
221
|
if (pathExistsSync(toolkitConfigPath)) {
|
|
242
222
|
const yaml$1 = __require("js-yaml");
|
|
243
223
|
const content = readFileSync$1(toolkitConfigPath, "utf-8");
|
|
244
224
|
const config = yaml$1.load(content);
|
|
245
225
|
if (config?.templatesPath) {
|
|
246
|
-
const templatesPath$1 =
|
|
226
|
+
const templatesPath$1 = nodePath.isAbsolute(config.templatesPath) ? config.templatesPath : nodePath.join(workspaceRoot, config.templatesPath);
|
|
247
227
|
if (pathExistsSync(templatesPath$1)) return templatesPath$1;
|
|
248
|
-
else
|
|
228
|
+
else return null;
|
|
249
229
|
}
|
|
250
230
|
}
|
|
251
|
-
const templatesPath =
|
|
231
|
+
const templatesPath = nodePath.join(workspaceRoot, TemplatesManagerService.TEMPLATES_FOLDER);
|
|
252
232
|
if (pathExistsSync(templatesPath)) return templatesPath;
|
|
253
|
-
|
|
233
|
+
return null;
|
|
254
234
|
}
|
|
255
235
|
/**
|
|
256
236
|
* Find the workspace root synchronously by searching upwards for .git folder
|
|
257
237
|
*/
|
|
258
238
|
static findWorkspaceRootSync(startPath) {
|
|
259
|
-
let currentPath =
|
|
260
|
-
const rootPath =
|
|
239
|
+
let currentPath = nodePath.resolve(startPath);
|
|
240
|
+
const rootPath = nodePath.parse(currentPath).root;
|
|
261
241
|
while (true) {
|
|
262
|
-
if (pathExistsSync(
|
|
242
|
+
if (pathExistsSync(nodePath.join(currentPath, ".git"))) return currentPath;
|
|
263
243
|
if (currentPath === rootPath) return process.cwd();
|
|
264
|
-
currentPath =
|
|
244
|
+
currentPath = nodePath.dirname(currentPath);
|
|
265
245
|
}
|
|
266
246
|
}
|
|
267
247
|
/**
|
|
@@ -294,7 +274,7 @@ var TemplatesManagerService = class TemplatesManagerService {
|
|
|
294
274
|
*/
|
|
295
275
|
static async readToolkitConfig(startPath = process.cwd()) {
|
|
296
276
|
const workspaceRoot = await TemplatesManagerService.findWorkspaceRoot(startPath);
|
|
297
|
-
const toolkitConfigPath =
|
|
277
|
+
const toolkitConfigPath = nodePath.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
|
|
298
278
|
if (!await pathExists(toolkitConfigPath)) return null;
|
|
299
279
|
const yaml$1 = await import("js-yaml");
|
|
300
280
|
const content = await fs.readFile(toolkitConfigPath, "utf-8");
|
|
@@ -308,7 +288,7 @@ var TemplatesManagerService = class TemplatesManagerService {
|
|
|
308
288
|
*/
|
|
309
289
|
static readToolkitConfigSync(startPath = process.cwd()) {
|
|
310
290
|
const workspaceRoot = TemplatesManagerService.findWorkspaceRootSync(startPath);
|
|
311
|
-
const toolkitConfigPath =
|
|
291
|
+
const toolkitConfigPath = nodePath.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
|
|
312
292
|
if (!pathExistsSync(toolkitConfigPath)) return null;
|
|
313
293
|
const yaml$1 = __require("js-yaml");
|
|
314
294
|
const content = readFileSync$1(toolkitConfigPath, "utf-8");
|
|
@@ -322,7 +302,7 @@ var TemplatesManagerService = class TemplatesManagerService {
|
|
|
322
302
|
*/
|
|
323
303
|
static async writeToolkitConfig(config, startPath = process.cwd()) {
|
|
324
304
|
const workspaceRoot = await TemplatesManagerService.findWorkspaceRoot(startPath);
|
|
325
|
-
const toolkitConfigPath =
|
|
305
|
+
const toolkitConfigPath = nodePath.join(workspaceRoot, TemplatesManagerService.TOOLKIT_CONFIG_FILE);
|
|
326
306
|
const content = (await import("js-yaml")).dump(config, { indent: 2 });
|
|
327
307
|
await fs.writeFile(toolkitConfigPath, content, "utf-8");
|
|
328
308
|
}
|
|
@@ -393,13 +373,13 @@ var ProjectConfigResolver = class ProjectConfigResolver {
|
|
|
393
373
|
*/
|
|
394
374
|
static async resolveProjectConfig(projectPath, explicitTemplate) {
|
|
395
375
|
try {
|
|
396
|
-
const absolutePath =
|
|
376
|
+
const absolutePath = nodePath.resolve(projectPath);
|
|
397
377
|
if (explicitTemplate) return {
|
|
398
378
|
type: ProjectType.MONOLITH,
|
|
399
379
|
sourceTemplate: explicitTemplate,
|
|
400
380
|
configSource: ConfigSource.TOOLKIT_YAML
|
|
401
381
|
};
|
|
402
|
-
const projectJsonPath =
|
|
382
|
+
const projectJsonPath = nodePath.join(absolutePath, "project.json");
|
|
403
383
|
if (await pathExists(projectJsonPath)) {
|
|
404
384
|
const projectJson = await readJson(projectJsonPath);
|
|
405
385
|
if (projectJson.sourceTemplate && typeof projectJson.sourceTemplate === "string" && projectJson.sourceTemplate.trim()) return {
|
|
@@ -490,12 +470,12 @@ Run 'scaffold-mcp scaffold list --help' for more info.`;
|
|
|
490
470
|
* @param sourceTemplate - The template identifier
|
|
491
471
|
*/
|
|
492
472
|
static async createProjectJson(projectPath, projectName, sourceTemplate) {
|
|
493
|
-
const projectJsonPath =
|
|
473
|
+
const projectJsonPath = nodePath.join(projectPath, "project.json");
|
|
494
474
|
try {
|
|
495
475
|
let projectJson;
|
|
496
476
|
if (await pathExists(projectJsonPath)) projectJson = await readJson(projectJsonPath);
|
|
497
477
|
else {
|
|
498
|
-
const relativePath =
|
|
478
|
+
const relativePath = nodePath.relative(projectPath, process.cwd());
|
|
499
479
|
projectJson = {
|
|
500
480
|
name: projectName,
|
|
501
481
|
$schema: relativePath ? `${relativePath}/node_modules/nx/schemas/project-schema.json` : "node_modules/nx/schemas/project-schema.json",
|
|
@@ -547,15 +527,15 @@ var ProjectFinderService = class {
|
|
|
547
527
|
* @returns Project configuration or null if not found
|
|
548
528
|
*/
|
|
549
529
|
async findProjectForFile(filePath) {
|
|
550
|
-
const normalizedPath =
|
|
551
|
-
let currentDir =
|
|
530
|
+
const normalizedPath = nodePath.isAbsolute(filePath) ? filePath : nodePath.join(this.workspaceRoot, filePath);
|
|
531
|
+
let currentDir = nodePath.dirname(normalizedPath);
|
|
552
532
|
while (currentDir !== "/" && currentDir.startsWith(this.workspaceRoot)) {
|
|
553
|
-
const projectJsonPath =
|
|
533
|
+
const projectJsonPath = nodePath.join(currentDir, "project.json");
|
|
554
534
|
try {
|
|
555
535
|
const project = await this.loadProjectConfig(projectJsonPath);
|
|
556
536
|
if (project) return project;
|
|
557
537
|
} catch {}
|
|
558
|
-
currentDir =
|
|
538
|
+
currentDir = nodePath.dirname(currentDir);
|
|
559
539
|
}
|
|
560
540
|
return null;
|
|
561
541
|
}
|
|
@@ -566,15 +546,15 @@ var ProjectFinderService = class {
|
|
|
566
546
|
* @returns Project configuration or null if not found
|
|
567
547
|
*/
|
|
568
548
|
findProjectForFileSync(filePath) {
|
|
569
|
-
const normalizedPath =
|
|
570
|
-
let currentDir =
|
|
549
|
+
const normalizedPath = nodePath.isAbsolute(filePath) ? filePath : nodePath.join(this.workspaceRoot, filePath);
|
|
550
|
+
let currentDir = nodePath.dirname(normalizedPath);
|
|
571
551
|
while (currentDir !== "/" && currentDir.startsWith(this.workspaceRoot)) {
|
|
572
|
-
const projectJsonPath =
|
|
552
|
+
const projectJsonPath = nodePath.join(currentDir, "project.json");
|
|
573
553
|
try {
|
|
574
554
|
const project = this.loadProjectConfigSync(projectJsonPath);
|
|
575
555
|
if (project) return project;
|
|
576
556
|
} catch {}
|
|
577
|
-
currentDir =
|
|
557
|
+
currentDir = nodePath.dirname(currentDir);
|
|
578
558
|
}
|
|
579
559
|
return null;
|
|
580
560
|
}
|
|
@@ -587,8 +567,8 @@ var ProjectFinderService = class {
|
|
|
587
567
|
const content = await fs.readFile(projectJsonPath, "utf-8");
|
|
588
568
|
const config = JSON.parse(content);
|
|
589
569
|
const projectConfig = {
|
|
590
|
-
name: config.name ||
|
|
591
|
-
root:
|
|
570
|
+
name: config.name || nodePath.basename(nodePath.dirname(projectJsonPath)),
|
|
571
|
+
root: nodePath.dirname(projectJsonPath),
|
|
592
572
|
sourceTemplate: config.sourceTemplate,
|
|
593
573
|
projectType: config.projectType
|
|
594
574
|
};
|
|
@@ -607,8 +587,8 @@ var ProjectFinderService = class {
|
|
|
607
587
|
const content = readFileSync$1(projectJsonPath, "utf-8");
|
|
608
588
|
const config = JSON.parse(content);
|
|
609
589
|
const projectConfig = {
|
|
610
|
-
name: config.name ||
|
|
611
|
-
root:
|
|
590
|
+
name: config.name || nodePath.basename(nodePath.dirname(projectJsonPath)),
|
|
591
|
+
root: nodePath.dirname(projectJsonPath),
|
|
612
592
|
sourceTemplate: config.sourceTemplate,
|
|
613
593
|
projectType: config.projectType
|
|
614
594
|
};
|
|
@@ -668,7 +648,7 @@ var ScaffoldProcessingService = class {
|
|
|
668
648
|
* Now supports tracking existing files separately from created files
|
|
669
649
|
*/
|
|
670
650
|
async copyAndProcess(sourcePath, targetPath, variables, createdFiles, existingFiles) {
|
|
671
|
-
await this.fileSystem.ensureDir(
|
|
651
|
+
await this.fileSystem.ensureDir(nodePath.dirname(targetPath));
|
|
672
652
|
if (await this.fileSystem.pathExists(targetPath) && existingFiles) {
|
|
673
653
|
await this.trackExistingFiles(targetPath, existingFiles);
|
|
674
654
|
return;
|
|
@@ -690,7 +670,7 @@ var ScaffoldProcessingService = class {
|
|
|
690
670
|
}
|
|
691
671
|
for (const item of items) {
|
|
692
672
|
if (!item) continue;
|
|
693
|
-
const itemPath =
|
|
673
|
+
const itemPath = nodePath.join(dirPath, item);
|
|
694
674
|
try {
|
|
695
675
|
const stat$1 = await this.fileSystem.stat(itemPath);
|
|
696
676
|
if (stat$1.isDirectory()) await this.trackCreatedFilesRecursive(itemPath, createdFiles);
|
|
@@ -713,7 +693,7 @@ var ScaffoldProcessingService = class {
|
|
|
713
693
|
}
|
|
714
694
|
for (const item of items) {
|
|
715
695
|
if (!item) continue;
|
|
716
|
-
const itemPath =
|
|
696
|
+
const itemPath = nodePath.join(dirPath, item);
|
|
717
697
|
try {
|
|
718
698
|
const stat$1 = await this.fileSystem.stat(itemPath);
|
|
719
699
|
if (stat$1.isDirectory()) await this.trackExistingFilesRecursive(itemPath, existingFiles);
|
|
@@ -755,6 +735,258 @@ function generateStableId(length = 8) {
|
|
|
755
735
|
return result;
|
|
756
736
|
}
|
|
757
737
|
|
|
738
|
+
//#endregion
|
|
739
|
+
//#region src/utils/git.ts
|
|
740
|
+
/**
|
|
741
|
+
* Git Utilities
|
|
742
|
+
*
|
|
743
|
+
* DESIGN PATTERNS:
|
|
744
|
+
* - Safe command execution: Use execa with array arguments to prevent shell injection
|
|
745
|
+
* - Defense in depth: Use '--' separator to prevent option injection attacks
|
|
746
|
+
*
|
|
747
|
+
* CODING STANDARDS:
|
|
748
|
+
* - All git commands must use execGit helper with array arguments
|
|
749
|
+
* - Use '--' separator before user-provided arguments (URLs, branches, paths)
|
|
750
|
+
* - Validate inputs where appropriate
|
|
751
|
+
*
|
|
752
|
+
* AVOID:
|
|
753
|
+
* - Shell string interpolation
|
|
754
|
+
* - Passing unsanitized user input as command options
|
|
755
|
+
*
|
|
756
|
+
* NOTE: These utilities perform I/O operations (git commands, file system) by necessity.
|
|
757
|
+
* Pure utility functions like parseGitHubUrl are side-effect free.
|
|
758
|
+
*/
|
|
759
|
+
/**
|
|
760
|
+
* Type guard to check if an error has the expected execa error shape
|
|
761
|
+
* @param error - The error to check
|
|
762
|
+
* @returns True if the error has message property (and optionally stderr)
|
|
763
|
+
* @example
|
|
764
|
+
* try {
|
|
765
|
+
* await execa('git', ['status']);
|
|
766
|
+
* } catch (error) {
|
|
767
|
+
* if (isExecaError(error)) {
|
|
768
|
+
* console.error(error.stderr || error.message);
|
|
769
|
+
* }
|
|
770
|
+
* }
|
|
771
|
+
*/
|
|
772
|
+
function isExecaError(error) {
|
|
773
|
+
if (typeof error !== "object" || error === null) return false;
|
|
774
|
+
if (!("message" in error)) return false;
|
|
775
|
+
return typeof error.message === "string";
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Execute a git command safely using execa to prevent command injection
|
|
779
|
+
* @param args - Array of git command arguments
|
|
780
|
+
* @param cwd - Optional working directory for the command
|
|
781
|
+
* @throws Error when git command fails
|
|
782
|
+
*/
|
|
783
|
+
async function execGit(args, cwd) {
|
|
784
|
+
try {
|
|
785
|
+
await execa("git", args, { cwd });
|
|
786
|
+
} catch (error) {
|
|
787
|
+
if (isExecaError(error)) throw new Error(`Git command failed: ${error.stderr || error.message}`);
|
|
788
|
+
throw error;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Execute git init safely using execa to prevent command injection
|
|
793
|
+
* Uses '--' to prevent projectPath from being interpreted as an option
|
|
794
|
+
* @param projectPath - Path where to initialize the git repository
|
|
795
|
+
* @throws Error when git init fails
|
|
796
|
+
*/
|
|
797
|
+
async function gitInit(projectPath) {
|
|
798
|
+
try {
|
|
799
|
+
await execa("git", [
|
|
800
|
+
"init",
|
|
801
|
+
"--",
|
|
802
|
+
projectPath
|
|
803
|
+
]);
|
|
804
|
+
} catch (error) {
|
|
805
|
+
if (isExecaError(error)) throw new Error(`Git init failed: ${error.stderr || error.message}`);
|
|
806
|
+
throw error;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Find the workspace root by searching upwards for .git folder
|
|
811
|
+
* Returns null if no .git folder is found (indicating a new project setup is needed)
|
|
812
|
+
* @param startPath - The path to start searching from (default: current working directory)
|
|
813
|
+
* @returns The workspace root path or null if not in a git repository
|
|
814
|
+
* @example
|
|
815
|
+
* const root = await findWorkspaceRoot('/path/to/project/src');
|
|
816
|
+
* if (root) {
|
|
817
|
+
* console.log('Workspace root:', root);
|
|
818
|
+
* } else {
|
|
819
|
+
* console.log('No git repository found');
|
|
820
|
+
* }
|
|
821
|
+
*/
|
|
822
|
+
async function findWorkspaceRoot(startPath = process.cwd()) {
|
|
823
|
+
let currentPath = nodePath.resolve(startPath);
|
|
824
|
+
const rootPath = nodePath.parse(currentPath).root;
|
|
825
|
+
while (true) {
|
|
826
|
+
if (await pathExists(nodePath.join(currentPath, ".git"))) return currentPath;
|
|
827
|
+
if (currentPath === rootPath) return null;
|
|
828
|
+
currentPath = nodePath.dirname(currentPath);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Parse GitHub URL to detect if it's a subdirectory
|
|
833
|
+
* Supports formats:
|
|
834
|
+
* - https://github.com/user/repo
|
|
835
|
+
* - https://github.com/user/repo/tree/branch/path/to/dir
|
|
836
|
+
* - https://github.com/user/repo/tree/main/path/to/dir
|
|
837
|
+
* @param url - The GitHub URL to parse
|
|
838
|
+
* @returns Parsed URL components including owner, repo, branch, and subdirectory
|
|
839
|
+
* @example
|
|
840
|
+
* const result = parseGitHubUrl('https://github.com/user/repo/tree/main/src');
|
|
841
|
+
* // result.owner === 'user'
|
|
842
|
+
* // result.repo === 'repo'
|
|
843
|
+
* // result.branch === 'main'
|
|
844
|
+
* // result.subdirectory === 'src'
|
|
845
|
+
* // result.isSubdirectory === true
|
|
846
|
+
*/
|
|
847
|
+
function parseGitHubUrl(url) {
|
|
848
|
+
const treeMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)$/);
|
|
849
|
+
const blobMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/);
|
|
850
|
+
const rootMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
851
|
+
if (treeMatch || blobMatch) {
|
|
852
|
+
const match = treeMatch || blobMatch;
|
|
853
|
+
return {
|
|
854
|
+
owner: match?.[1],
|
|
855
|
+
repo: match?.[2],
|
|
856
|
+
repoUrl: `https://github.com/${match?.[1]}/${match?.[2]}.git`,
|
|
857
|
+
branch: match?.[3],
|
|
858
|
+
subdirectory: match?.[4],
|
|
859
|
+
isSubdirectory: true
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
if (rootMatch) return {
|
|
863
|
+
owner: rootMatch[1],
|
|
864
|
+
repo: rootMatch[2],
|
|
865
|
+
repoUrl: `https://github.com/${rootMatch[1]}/${rootMatch[2]}.git`,
|
|
866
|
+
isSubdirectory: false
|
|
867
|
+
};
|
|
868
|
+
return {
|
|
869
|
+
repoUrl: url,
|
|
870
|
+
isSubdirectory: false
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Clone a subdirectory from a git repository using sparse checkout
|
|
875
|
+
* Uses '--' to mark end of options - prevents malicious URLs like '--upload-pack=evil'
|
|
876
|
+
* from being interpreted as git options
|
|
877
|
+
* @param repoUrl - The git repository URL
|
|
878
|
+
* @param branch - The branch to clone from
|
|
879
|
+
* @param subdirectory - The subdirectory path within the repository
|
|
880
|
+
* @param targetFolder - The local folder to clone into
|
|
881
|
+
* @throws Error if subdirectory not found or target folder already exists
|
|
882
|
+
* @example
|
|
883
|
+
* await cloneSubdirectory(
|
|
884
|
+
* 'https://github.com/user/repo.git',
|
|
885
|
+
* 'main',
|
|
886
|
+
* 'packages/core',
|
|
887
|
+
* './my-project'
|
|
888
|
+
* );
|
|
889
|
+
*/
|
|
890
|
+
async function cloneSubdirectory(repoUrl, branch, subdirectory, targetFolder) {
|
|
891
|
+
const tempFolder = `${targetFolder}.tmp`;
|
|
892
|
+
try {
|
|
893
|
+
await execGit([
|
|
894
|
+
"init",
|
|
895
|
+
"--",
|
|
896
|
+
tempFolder
|
|
897
|
+
]);
|
|
898
|
+
await execGit([
|
|
899
|
+
"remote",
|
|
900
|
+
"add",
|
|
901
|
+
"origin",
|
|
902
|
+
"--",
|
|
903
|
+
repoUrl
|
|
904
|
+
], tempFolder);
|
|
905
|
+
await execGit([
|
|
906
|
+
"config",
|
|
907
|
+
"core.sparseCheckout",
|
|
908
|
+
"true"
|
|
909
|
+
], tempFolder);
|
|
910
|
+
await writeFile(nodePath.join(tempFolder, ".git", "info", "sparse-checkout"), `${subdirectory}\n`);
|
|
911
|
+
await execGit([
|
|
912
|
+
"pull",
|
|
913
|
+
"--depth=1",
|
|
914
|
+
"origin",
|
|
915
|
+
"--",
|
|
916
|
+
branch
|
|
917
|
+
], tempFolder);
|
|
918
|
+
const sourceDir = nodePath.join(tempFolder, subdirectory);
|
|
919
|
+
if (!await pathExists(sourceDir)) throw new Error(`Subdirectory '${subdirectory}' not found in repository at branch '${branch}'`);
|
|
920
|
+
if (await pathExists(targetFolder)) throw new Error(`Target folder already exists: ${targetFolder}`);
|
|
921
|
+
await move(sourceDir, targetFolder);
|
|
922
|
+
await remove(tempFolder);
|
|
923
|
+
} catch (error) {
|
|
924
|
+
if (await pathExists(tempFolder)) await remove(tempFolder);
|
|
925
|
+
throw error;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Clone entire repository
|
|
930
|
+
* Uses '--' to mark end of options - prevents malicious URLs like '--upload-pack=evil'
|
|
931
|
+
* from being interpreted as git options
|
|
932
|
+
* @param repoUrl - The git repository URL to clone
|
|
933
|
+
* @param targetFolder - The local folder path to clone into
|
|
934
|
+
* @throws Error if git clone fails
|
|
935
|
+
* @example
|
|
936
|
+
* await cloneRepository('https://github.com/user/repo.git', './my-project');
|
|
937
|
+
*/
|
|
938
|
+
async function cloneRepository(repoUrl, targetFolder) {
|
|
939
|
+
await execGit([
|
|
940
|
+
"clone",
|
|
941
|
+
"--",
|
|
942
|
+
repoUrl,
|
|
943
|
+
targetFolder
|
|
944
|
+
]);
|
|
945
|
+
const gitFolder = nodePath.join(targetFolder, ".git");
|
|
946
|
+
if (await pathExists(gitFolder)) await remove(gitFolder);
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Type guard to validate GitHub API content item structure
|
|
950
|
+
* @param item - The item to validate
|
|
951
|
+
* @returns True if the item has required GitHubContentItem properties
|
|
952
|
+
*/
|
|
953
|
+
function isGitHubContentItem(item) {
|
|
954
|
+
if (typeof item !== "object" || item === null) return false;
|
|
955
|
+
const obj = item;
|
|
956
|
+
return typeof obj.name === "string" && typeof obj.type === "string" && typeof obj.path === "string";
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Fetch directory listing from GitHub API
|
|
960
|
+
* @param owner - The GitHub repository owner/organization
|
|
961
|
+
* @param repo - The repository name
|
|
962
|
+
* @param dirPath - The directory path within the repository
|
|
963
|
+
* @param branch - The branch to fetch from (default: 'main')
|
|
964
|
+
* @returns Array of directory entries with name, type, and path
|
|
965
|
+
* @throws Error if the API request fails or returns non-directory content
|
|
966
|
+
* @remarks
|
|
967
|
+
* - Requires network access to GitHub API
|
|
968
|
+
* - Subject to GitHub API rate limits (60 requests/hour unauthenticated)
|
|
969
|
+
* - Only works with public repositories without authentication
|
|
970
|
+
* @example
|
|
971
|
+
* const contents = await fetchGitHubDirectoryContents('facebook', 'react', 'packages', 'main');
|
|
972
|
+
* // contents: [{ name: 'react', type: 'dir', path: 'packages/react' }, ...]
|
|
973
|
+
*/
|
|
974
|
+
async function fetchGitHubDirectoryContents(owner, repo, dirPath, branch = "main") {
|
|
975
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/contents/${dirPath}?ref=${branch}`;
|
|
976
|
+
const response = await fetch(url, { headers: {
|
|
977
|
+
Accept: "application/vnd.github.v3+json",
|
|
978
|
+
"User-Agent": "scaffold-mcp"
|
|
979
|
+
} });
|
|
980
|
+
if (!response.ok) throw new Error(`Failed to fetch directory contents: ${response.statusText}`);
|
|
981
|
+
const data = await response.json();
|
|
982
|
+
if (!Array.isArray(data)) throw new Error("Expected directory but got file");
|
|
983
|
+
return data.filter(isGitHubContentItem).map((item) => ({
|
|
984
|
+
name: item.name,
|
|
985
|
+
type: item.type,
|
|
986
|
+
path: item.path
|
|
987
|
+
}));
|
|
988
|
+
}
|
|
989
|
+
|
|
758
990
|
//#endregion
|
|
759
991
|
//#region src/utils/print.ts
|
|
760
992
|
/**
|
|
@@ -927,7 +1159,7 @@ const MONOREPO_INDICATOR_FILES = [
|
|
|
927
1159
|
*/
|
|
928
1160
|
async function detectProjectType(workspaceRoot) {
|
|
929
1161
|
const indicators = [];
|
|
930
|
-
const toolkitYamlPath =
|
|
1162
|
+
const toolkitYamlPath = nodePath.join(workspaceRoot, "toolkit.yaml");
|
|
931
1163
|
if (await pathExists(toolkitYamlPath)) try {
|
|
932
1164
|
const content = await fs.readFile(toolkitYamlPath, "utf-8");
|
|
933
1165
|
const config = yaml.load(content);
|
|
@@ -939,14 +1171,14 @@ async function detectProjectType(workspaceRoot) {
|
|
|
939
1171
|
};
|
|
940
1172
|
}
|
|
941
1173
|
} catch {}
|
|
942
|
-
for (const filename of MONOREPO_INDICATOR_FILES) if (await pathExists(
|
|
1174
|
+
for (const filename of MONOREPO_INDICATOR_FILES) if (await pathExists(nodePath.join(workspaceRoot, filename))) {
|
|
943
1175
|
indicators.push(`${filename} found`);
|
|
944
1176
|
return {
|
|
945
1177
|
projectType: ProjectType.MONOREPO,
|
|
946
1178
|
indicators
|
|
947
1179
|
};
|
|
948
1180
|
}
|
|
949
|
-
const packageJsonPath =
|
|
1181
|
+
const packageJsonPath = nodePath.join(workspaceRoot, "package.json");
|
|
950
1182
|
if (await pathExists(packageJsonPath)) try {
|
|
951
1183
|
if ((await readJson(packageJsonPath)).workspaces) {
|
|
952
1184
|
indicators.push("package.json with workspaces found");
|
|
@@ -962,26 +1194,6 @@ async function detectProjectType(workspaceRoot) {
|
|
|
962
1194
|
indicators
|
|
963
1195
|
};
|
|
964
1196
|
}
|
|
965
|
-
/**
|
|
966
|
-
* Check if a workspace is a monorepo
|
|
967
|
-
* Convenience function that returns a boolean
|
|
968
|
-
*
|
|
969
|
-
* @param workspaceRoot - Absolute path to the workspace root directory
|
|
970
|
-
* @returns True if workspace is detected as monorepo, false otherwise
|
|
971
|
-
*/
|
|
972
|
-
async function isMonorepo(workspaceRoot) {
|
|
973
|
-
return (await detectProjectType(workspaceRoot)).projectType === ProjectType.MONOREPO;
|
|
974
|
-
}
|
|
975
|
-
/**
|
|
976
|
-
* Check if a workspace is a monolith
|
|
977
|
-
* Convenience function that returns a boolean
|
|
978
|
-
*
|
|
979
|
-
* @param workspaceRoot - Absolute path to the workspace root directory
|
|
980
|
-
* @returns True if workspace is detected as monolith, false otherwise
|
|
981
|
-
*/
|
|
982
|
-
async function isMonolith(workspaceRoot) {
|
|
983
|
-
return (await detectProjectType(workspaceRoot)).projectType === ProjectType.MONOLITH;
|
|
984
|
-
}
|
|
985
1197
|
|
|
986
1198
|
//#endregion
|
|
987
|
-
export { ConfigSource, ProjectConfigResolver, ProjectFinderService, ProjectType, ScaffoldProcessingService, TemplatesManagerService, accessSync,
|
|
1199
|
+
export { ConfigSource, ProjectConfigResolver, ProjectFinderService, ProjectType, ScaffoldProcessingService, TemplatesManagerService, accessSync, cloneRepository, cloneSubdirectory, copy, detectProjectType, ensureDir, fetchGitHubDirectoryContents, findWorkspaceRoot, generateStableId, gitInit, icons, log, logger, messages, mkdir, mkdirSync, move, parseGitHubUrl, pathExists, pathExistsSync, print, readFile, readFileSync, readJson, readJsonSync, readdir, remove, sections, stat, statSync, writeFile, writeFileSync };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agiflowai/aicode-utils",
|
|
3
3
|
"description": "Shared utilities and types for AI-powered code generation, scaffolding, and analysis",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.9",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"author": "AgiflowIO",
|
|
7
7
|
"repository": {
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"chalk": "5.6.2",
|
|
34
|
-
"
|
|
34
|
+
"execa": "^9.5.2",
|
|
35
|
+
"js-yaml": "4.1.1",
|
|
35
36
|
"ora": "^9.0.0",
|
|
36
37
|
"pino": "^10.0.0"
|
|
37
38
|
},
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"chance": "^1.1.13",
|
|
44
45
|
"tsdown": "^0.16.4",
|
|
45
46
|
"typescript": "5.9.3",
|
|
46
|
-
"vitest": "
|
|
47
|
+
"vitest": "4.0.15"
|
|
47
48
|
},
|
|
48
49
|
"type": "module",
|
|
49
50
|
"publishConfig": {
|