-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add cursor-scope prompt for project vs global install #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "permissions": { | ||
| "allow": [ | ||
| "Bash(npm run *)", | ||
| "Bash(npx vitest *)", | ||
| "Bash(npm test *)", | ||
| "Bash(git add:*)", | ||
| "Bash(git commit -m *)", | ||
| "Bash(git push:*)" | ||
| ] | ||
| } | ||
| } |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,7 +72,41 @@ export async function scaffold(options = {}) { | |
| process.exit(1); | ||
| } | ||
|
|
||
| // 2. Installation Location Choice | ||
| // 2. Cursor Config Scope | ||
| let installScope = options.installScope; | ||
|
|
||
| if (!installScope) { | ||
| installScope = await p.select({ | ||
| message: "Where should Cursor config (MCP, Rules, Skills) be installed?", | ||
| options: [ | ||
| { | ||
| label: "Project (.cursor/ in project folder)", | ||
| value: "project", | ||
| hint: "Only active for this project", | ||
| }, | ||
| { | ||
| label: "Global (~/.cursor/)", | ||
| value: "global", | ||
| hint: "Installs global MCP config (~/.cursor/mcp.json)", | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| if (p.isCancel(installScope)) { | ||
| p.cancel("Operation cancelled"); | ||
| process.exit(0); | ||
| } | ||
| } | ||
|
|
||
| if (installScope === "global") { | ||
| p.log.warn( | ||
| "Global scope installs mcp.json and skills/ to ~/.cursor/.\n" + | ||
| " If you scaffold multiple frameworks globally, all their rule sets\n" + | ||
| " will be active on every project on this machine." | ||
| ); | ||
| } | ||
|
|
||
| // 3. Installation Location Choice | ||
| let installInCurrentFolder = options.currentFolder; | ||
| let projectName = options.projectName; | ||
|
|
||
|
|
@@ -103,7 +137,7 @@ export async function scaffold(options = {}) { | |
| installInCurrentFolder = location === "current"; | ||
| } | ||
|
|
||
| // 3. Project Name | ||
| // 4. Project Name | ||
| if (installInCurrentFolder) { | ||
| projectName = getCurrentFolderName(); | ||
| p.log.info(`Using current folder: ${colors.brand(projectName)}`); | ||
|
|
@@ -141,19 +175,22 @@ export async function scaffold(options = {}) { | |
| p.log.message( | ||
| `Location: ${installInCurrentFolder ? "Current directory" : projectName}`, | ||
| ); | ||
| p.log.message( | ||
| `Cursor config: ${installScope === "global" ? "~/.cursor/mcp.json (global)" : ".cursor/ (project)"}`, | ||
| ); | ||
| p.outro("Preview complete"); | ||
| process.exit(0); | ||
| } | ||
|
|
||
| // 4. Copy Template with spinner | ||
| // 5. Copy Template with spinner | ||
| const copySpinner = p.spinner(); | ||
| copySpinner.start(`Creating ${config.name} project`); | ||
|
|
||
| try { | ||
| await copyTemplate(framework, projectPath); | ||
| await copyTemplate(framework, projectPath, { cursorScope: installScope }); | ||
| copySpinner.stop(`${colors.success("\u2713")} Project created`); | ||
|
|
||
| // 5. Update package.json with project name | ||
| // 6. Update package.json with project name | ||
| try { | ||
| await updatePackageJson(projectPath, { | ||
| name: projectName, | ||
|
|
@@ -170,7 +207,7 @@ export async function scaffold(options = {}) { | |
| process.exit(1); | ||
| } | ||
|
|
||
| // 6. Install Dependencies (optional) | ||
| // 7. Install Dependencies | ||
| let install = options.install; | ||
|
|
||
| if (install === undefined) { | ||
|
|
@@ -198,7 +235,7 @@ export async function scaffold(options = {}) { | |
| } | ||
| } | ||
|
|
||
| // 7. Post-scaffold validation (only if deps were installed) | ||
| // 8. Post-scaffold validation | ||
| if (install) { | ||
| const validateSpinner = p.spinner(); | ||
| validateSpinner.start("Validating template integrity"); | ||
|
|
@@ -221,9 +258,8 @@ export async function scaffold(options = {}) { | |
| } | ||
| } | ||
|
|
||
| // 8. Success outro | ||
| // 9. Success outro | ||
| p.outro(colors.success("Done! Configuration updated.")); | ||
|
|
||
| // 8. Detailed next steps | ||
| logger.nextSteps(projectName, config.name, install, installInCurrentFolder); | ||
| logger.nextSteps(projectName, config.name, install, installInCurrentFolder, installScope); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicate |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,12 +22,19 @@ const SKIP_FILES = new Set([ | |
| ]); | ||
|
|
||
| /** | ||
| * Copy bundled template to target directory | ||
| * Copy bundled template to target directory. | ||
| * When cursorScope is "global", only mcp.json and skills/ from .cursor/ are | ||
| * written to ~/.cursor/ (or globalCursorPath) instead of the project directory. | ||
| * @param {string} templateName - Name of the template (react, angular, solidjs) | ||
| * @param {string} targetPath - Target directory path | ||
| * @param {{ cursorScope?: "project" | "global", globalCursorPath?: string }} options | ||
| * @returns {Promise<boolean>} | ||
| */ | ||
| export async function copyTemplate(templateName, targetPath) { | ||
| export async function copyTemplate( | ||
| templateName, | ||
| targetPath, | ||
| { cursorScope = "project", globalCursorPath } = {} | ||
| ) { | ||
| const fs = await import("fs/promises"); | ||
| const path = await import("path"); | ||
|
|
||
|
|
@@ -50,16 +57,98 @@ export async function copyTemplate(templateName, targetPath) { | |
| ); | ||
| } | ||
|
|
||
| await copyDirectory(bundledPath, targetPath); | ||
| if (cursorScope === "global") { | ||
| // Global scope: template's .cursor/ is redirected to ~/.cursor/ below, | ||
| // so we don't also want it inside the project folder. | ||
| const projectSkipDirs = new Set([...SKIP_DIRECTORIES, ".cursor"]); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor β consider also skipping // Global scope: template's .cursor/ is redirected to ~/.cursor/ below,
// so we don't also want it inside the project folder.
const projectSkipDirs = new Set([...SKIP_DIRECTORIES, ".cursor"]); |
||
| await copyDirectory(bundledPath, targetPath, projectSkipDirs); | ||
|
|
||
| // Copy only mcp.json and skills/ to the global cursor directory | ||
| const { homedir } = await import("os"); | ||
| const cursorSrc = path.join(bundledPath, ".cursor"); | ||
| const target = globalCursorPath ?? path.join(homedir(), ".cursor"); | ||
| await copyGlobalCursor(cursorSrc, target); | ||
| } else { | ||
| await copyDirectory(bundledPath, targetPath); | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Copy only mcp.json (with backup) and skills/ (skip-if-exists) to global cursor dir. | ||
| * @param {string} src - Source .cursor/ directory from template | ||
| * @param {string} dest - Destination ~/.cursor/ directory | ||
| */ | ||
| async function copyGlobalCursor(src, dest) { | ||
| const fs = await import("fs/promises"); | ||
| const path = await import("path"); | ||
|
|
||
| await fs.mkdir(dest, { recursive: true }); | ||
|
|
||
| // Copy mcp.json, backing up any existing file first | ||
| const srcMcp = path.join(src, "mcp.json"); | ||
| const destMcp = path.join(dest, "mcp.json"); | ||
| try { | ||
| await fs.access(srcMcp); | ||
| try { | ||
| await fs.access(destMcp); | ||
| const ts = new Date().toISOString().replace(/[:.]/g, "-"); | ||
| await fs.copyFile(destMcp, path.join(dest, `mcp.json.bak-${ts}`)); | ||
| } catch { | ||
| // No existing mcp.json to back up | ||
| } | ||
| await fs.copyFile(srcMcp, destMcp); | ||
| } catch { | ||
| // No mcp.json in template | ||
| } | ||
|
|
||
| // Copy skills/ with skip-if-exists per file | ||
| const srcSkills = path.join(src, "skills"); | ||
| const destSkills = path.join(dest, "skills"); | ||
| try { | ||
| await fs.access(srcSkills); | ||
| await copyDirectorySkipExisting(srcSkills, destSkills); | ||
| } catch { | ||
| // No skills/ in template | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Copy directory recursively, skipping files that already exist at the destination. | ||
| * @param {string} src - Source directory | ||
| * @param {string} dest - Destination directory | ||
| */ | ||
| async function copyDirectorySkipExisting(src, dest) { | ||
| const fs = await import("fs/promises"); | ||
| const path = await import("path"); | ||
|
|
||
| await fs.mkdir(dest, { recursive: true }); | ||
| const entries = await fs.readdir(src, { withFileTypes: true }); | ||
|
|
||
| for (const entry of entries) { | ||
| const srcPath = path.join(src, entry.name); | ||
| const destPath = path.join(dest, entry.name); | ||
| if (entry.isDirectory()) { | ||
| await copyDirectorySkipExisting(srcPath, destPath); | ||
| } else { | ||
| try { | ||
| await fs.access(destPath); | ||
| // File already exists β skip | ||
| } catch { | ||
| await fs.copyFile(srcPath, destPath); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Copy directory recursively | ||
| * @param {string} src - Source directory | ||
| * @param {string} dest - Destination directory | ||
| * @param {Set<string>} skipDirs - Directory names to skip (defaults to SKIP_DIRECTORIES) | ||
| */ | ||
| async function copyDirectory(src, dest) { | ||
| async function copyDirectory(src, dest, skipDirs = SKIP_DIRECTORIES) { | ||
| const fs = await import("fs/promises"); | ||
| const path = await import("path"); | ||
|
|
||
|
|
@@ -69,7 +158,7 @@ async function copyDirectory(src, dest) { | |
|
|
||
| for (const entry of entries) { | ||
| // Skip excluded directories (node_modules, dist, .angular, etc.) | ||
| if (entry.isDirectory() && SKIP_DIRECTORIES.has(entry.name)) { | ||
| if (entry.isDirectory() && skipDirs.has(entry.name)) { | ||
| continue; | ||
| } | ||
|
|
||
|
|
@@ -86,7 +175,7 @@ async function copyDirectory(src, dest) { | |
| const destPath = path.join(dest, destName); | ||
|
|
||
| if (entry.isDirectory()) { | ||
| await copyDirectory(srcPath, destPath); | ||
| await copyDirectory(srcPath, destPath, skipDirs); | ||
| } else { | ||
| await fs.copyFile(srcPath, destPath); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you also add a
--cursor-scopeexample to theExamplessection of the help footer (theboxenblock starting around line 66)? The flag is listed inOptionsbut not demonstrated, which makes it easy to miss. Something like: