Fix: install by name always fetches from GitHub (#180)
* feat(cursor): add Cursor CLI as target provider Add converter, writer, types, and tests for converting Claude Code plugins to Cursor-compatible format (.mdc rules, commands, skills, mcp.json). Agents become Agent Requested rules (alwaysApply: false), commands are plain markdown, skills copy directly, MCP is 1:1 JSON. * docs: add Cursor spec and update README with cursor target * chore: bump CLI version to 0.5.0 for cursor target Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: note Cursor IDE + CLI compatibility in README * fix: install by name always fetches from GitHub Previously, `install compound-engineering` would resolve to any local directory named `compound-engineering` in the current working directory before trying GitHub. This broke installs when users had a same-named directory that wasn't a valid plugin. Now bare names always go to GitHub. Only explicit paths (starting with ./ or / or ~) are treated as local paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -131,12 +131,15 @@ type ResolvedPluginPath = {
|
||||
}
|
||||
|
||||
async function resolvePluginPath(input: string): Promise<ResolvedPluginPath> {
|
||||
const directPath = path.resolve(input)
|
||||
// Only treat as a local path if it explicitly looks like one
|
||||
if (input.startsWith(".") || input.startsWith("/") || input.startsWith("~")) {
|
||||
const expanded = expandHome(input)
|
||||
const directPath = path.resolve(expanded)
|
||||
if (await pathExists(directPath)) return { path: directPath }
|
||||
throw new Error(`Local plugin path not found: ${directPath}`)
|
||||
}
|
||||
|
||||
const pluginsPath = path.join(process.cwd(), "plugins", input)
|
||||
if (await pathExists(pluginsPath)) return { path: pluginsPath }
|
||||
|
||||
// Otherwise, always fetch the latest from GitHub
|
||||
return await resolveGitHubPluginPath(input)
|
||||
}
|
||||
|
||||
|
||||
@@ -180,6 +180,68 @@ describe("CLI", () => {
|
||||
expect(await exists(path.join(tempRoot, ".config", "opencode", "agents", "repo-research-analyst.md"))).toBe(true)
|
||||
})
|
||||
|
||||
test("install by name ignores same-named local directory", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-shadow-"))
|
||||
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-shadow-workspace-"))
|
||||
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-shadow-repo-"))
|
||||
|
||||
// Create a directory with the plugin name that is NOT a valid plugin
|
||||
const shadowDir = path.join(workspaceRoot, "compound-engineering")
|
||||
await fs.mkdir(shadowDir, { recursive: true })
|
||||
await fs.writeFile(path.join(shadowDir, "README.md"), "Not a plugin")
|
||||
|
||||
// Set up a fake GitHub source with a valid plugin
|
||||
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
|
||||
const pluginRoot = path.join(repoRoot, "plugins", "compound-engineering")
|
||||
await fs.mkdir(path.dirname(pluginRoot), { recursive: true })
|
||||
await fs.cp(fixtureRoot, pluginRoot, { recursive: true })
|
||||
|
||||
const gitEnv = {
|
||||
...process.env,
|
||||
GIT_AUTHOR_NAME: "Test",
|
||||
GIT_AUTHOR_EMAIL: "test@example.com",
|
||||
GIT_COMMITTER_NAME: "Test",
|
||||
GIT_COMMITTER_EMAIL: "test@example.com",
|
||||
}
|
||||
await runGit(["init"], repoRoot, gitEnv)
|
||||
await runGit(["add", "."], repoRoot, gitEnv)
|
||||
await runGit(["commit", "-m", "fixture"], repoRoot, gitEnv)
|
||||
|
||||
const projectRoot = path.join(import.meta.dir, "..")
|
||||
const proc = Bun.spawn([
|
||||
"bun",
|
||||
"run",
|
||||
path.join(projectRoot, "src", "index.ts"),
|
||||
"install",
|
||||
"compound-engineering",
|
||||
"--to",
|
||||
"opencode",
|
||||
"--output",
|
||||
tempRoot,
|
||||
], {
|
||||
cwd: workspaceRoot,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: tempRoot,
|
||||
COMPOUND_PLUGIN_GITHUB_SOURCE: repoRoot,
|
||||
},
|
||||
})
|
||||
|
||||
const exitCode = await proc.exited
|
||||
const stdout = await new Response(proc.stdout).text()
|
||||
const stderr = await new Response(proc.stderr).text()
|
||||
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`)
|
||||
}
|
||||
|
||||
// Should succeed by fetching from GitHub, NOT failing on the local shadow directory
|
||||
expect(stdout).toContain("Installed compound-engineering")
|
||||
expect(await exists(path.join(tempRoot, "opencode.json"))).toBe(true)
|
||||
})
|
||||
|
||||
test("convert writes OpenCode output", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-convert-"))
|
||||
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
|
||||
|
||||
Reference in New Issue
Block a user