feat: turborepo monorepo scaffold and provisioning

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-21 16:44:30 -08:00
parent e22f5e379f
commit 8587644a62
35 changed files with 841 additions and 5 deletions

55
lib/scaffold/index.ts Normal file
View File

@@ -0,0 +1,55 @@
/**
* Turborepo scaffold loader
*
* Reads template files from lib/scaffold/turborepo/, replaces
* {{project-slug}} and {{project-name}} placeholders, then pushes
* each file to the user's Gitea repo via the contents API.
*/
import { readdir, readFile } from 'fs/promises';
import { join, relative } from 'path';
import { giteaPushFile } from '@/lib/gitea';
const TEMPLATES_DIR = join(process.cwd(), 'lib/scaffold/turborepo');
const HIDDEN_FILES = ['.gitignore'];
async function walkDir(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...await walkDir(fullPath));
} else {
files.push(fullPath);
}
}
return files;
}
export async function pushTurborepoScaffold(
owner: string,
repoName: string,
projectSlug: string,
projectName: string,
): Promise<void> {
const allFiles = await walkDir(TEMPLATES_DIR);
for (const filePath of allFiles) {
let relPath = relative(TEMPLATES_DIR, filePath);
// Restore leading dot for hidden files (e.g. gitignore → .gitignore)
const basename = relPath.split('/').pop() ?? '';
if (HIDDEN_FILES.includes(`.${basename}`)) {
relPath = relPath.replace(basename, `.${basename}`);
}
let content = await readFile(filePath, 'utf-8');
content = content
.replaceAll('{{project-slug}}', projectSlug)
.replaceAll('{{project-name}}', projectName);
await giteaPushFile(owner, repoName, relPath, content, `chore: scaffold ${relPath}`);
}
}