Skip to content

Commit 9fd184e

Browse files
committed
feat: add Coder plugin for Cursor marketplace
0 parents  commit 9fd184e

31 files changed

Lines changed: 2822 additions & 0 deletions

.cursor-plugin/marketplace.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "coder-cursor-plugins",
3+
"owner": {
4+
"name": "Coder",
5+
"email": "support@coder.com"
6+
},
7+
"metadata": {
8+
"description": "Coder plugins for Cursor",
9+
"version": "0.1.0",
10+
"pluginRoot": "plugins"
11+
},
12+
"plugins": [
13+
{
14+
"name": "coder",
15+
"source": "coder",
16+
"description": "Connect Cursor to your self-hosted Coder deployment: manage workspaces, templates, and Coder Agents through MCP, plus skills for installing and operating Coder."
17+
}
18+
]
19+
}

.cursor/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"repo": "coder/skills",
3+
"pin": "0ecba140e91d81eb72bf29e08eabc27bf05d49e9",
4+
"paths": [
5+
"skills/setup",
6+
"skills/templates",
7+
"skills/modules"
8+
]
9+
}

.github/scripts/sync-skills.mjs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env node
2+
// Vendors skills from the upstream coder/skills repository into
3+
// plugins/coder/skills/. Run manually when bumping the pin: update `pin`
4+
// in sync-skills-vendor.json, run this script, then commit both together.
5+
//
6+
// Usage:
7+
// node .github/scripts/sync-skills.mjs
8+
//
9+
// Steps:
10+
// 1. Read sync-skills-vendor.json for the repo, ref, and paths.
11+
// 2. Download the tarball from codeload.github.com (public, no auth).
12+
// 3. Extract it into a temp directory.
13+
// 4. Replace each listed path under plugins/coder/, removing the previous
14+
// copy first so stale files never survive a sync.
15+
//
16+
// The pin is the single source of truth. There is no runtime override.
17+
18+
import { promises as fs, createWriteStream } from "node:fs";
19+
import { Readable } from "node:stream";
20+
import { pipeline } from "node:stream/promises";
21+
import path from "node:path";
22+
import { spawnSync } from "node:child_process";
23+
import { tmpdir } from "node:os";
24+
import { fileURLToPath } from "node:url";
25+
26+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
27+
const repoRoot = path.resolve(scriptDir, "..", "..");
28+
const pluginDir = path.join(repoRoot, "plugins", "coder");
29+
const vendorFile = path.join(scriptDir, "sync-skills-vendor.json");
30+
31+
async function fileExists(filePath) {
32+
try {
33+
await fs.access(filePath);
34+
return true;
35+
} catch {
36+
return false;
37+
}
38+
}
39+
40+
async function downloadTarball(repo, ref, destPath) {
41+
const url = `https://codeload.github.com/${repo}/tar.gz/${encodeURIComponent(ref)}`;
42+
const res = await fetch(url, { redirect: "follow" });
43+
if (!res.ok) {
44+
throw new Error(`Could not download ${repo}@${ref} (HTTP ${res.status})`);
45+
}
46+
await pipeline(Readable.fromWeb(res.body), createWriteStream(destPath));
47+
console.log(` fetched ${url}`);
48+
}
49+
50+
// GitHub tarballs contain exactly one top-level directory named after the
51+
// repo and ref. Return it so callers know where the tree lives.
52+
async function extractTarball(tarballPath, intoDir) {
53+
await fs.mkdir(intoDir, { recursive: true });
54+
const result = spawnSync("tar", ["-xzf", tarballPath, "-C", intoDir], {
55+
stdio: "inherit",
56+
});
57+
if (result.status !== 0) {
58+
throw new Error(`tar exited with status ${result.status}`);
59+
}
60+
const [topLevel] = await fs.readdir(intoDir);
61+
return path.join(intoDir, topLevel);
62+
}
63+
64+
async function copyPath(fromDir, toDir, relativePath) {
65+
const from = path.join(fromDir, relativePath);
66+
const to = path.join(toDir, relativePath);
67+
if (!(await fileExists(from))) {
68+
throw new Error(`path missing in upstream tarball: ${relativePath}`);
69+
}
70+
await fs.rm(to, { recursive: true, force: true });
71+
await fs.mkdir(path.dirname(to), { recursive: true });
72+
await fs.cp(from, to, { recursive: true });
73+
console.log(` ${relativePath} -> ${path.relative(repoRoot, to)}`);
74+
}
75+
76+
async function main() {
77+
const vendor = JSON.parse(await fs.readFile(vendorFile, "utf8"));
78+
const { repo, pin, paths } = vendor;
79+
if (!repo || !pin || !Array.isArray(paths) || paths.length === 0) {
80+
throw new Error(`${vendorFile} must define repo, pin, and a non-empty paths array`);
81+
}
82+
83+
console.log(`Syncing ${repo}@${pin}`);
84+
const workDir = await fs.mkdtemp(path.join(tmpdir(), "coder-skills-sync-"));
85+
try {
86+
const tarball = path.join(workDir, "upstream.tar.gz");
87+
await downloadTarball(repo, pin, tarball);
88+
const extracted = await extractTarball(tarball, path.join(workDir, "extract"));
89+
for (const relativePath of paths) {
90+
await copyPath(extracted, pluginDir, relativePath);
91+
}
92+
} finally {
93+
await fs.rm(workDir, { recursive: true, force: true });
94+
}
95+
console.log("Done. Review the diff, bump the plugin version, and commit.");
96+
}
97+
98+
main().catch((error) => {
99+
console.error(error.message);
100+
process.exit(1);
101+
});

.github/workflows/release.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Tags and publishes a GitHub Release when the plugin version on main has no
2+
# tag yet. The version in plugin.json is canonical.
3+
name: Release
4+
5+
on:
6+
push:
7+
branches: [main]
8+
9+
permissions: {}
10+
11+
concurrency:
12+
group: ${{ github.workflow }}
13+
cancel-in-progress: false
14+
15+
jobs:
16+
release:
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: write
20+
steps:
21+
- uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 0
24+
25+
- name: Read version
26+
id: version
27+
run: |
28+
set -euo pipefail
29+
VERSION=$(jq -er '.version' plugins/coder/.cursor-plugin/plugin.json)
30+
if git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null; then
31+
echo "v$VERSION already released"
32+
echo "release=false" >> "$GITHUB_OUTPUT"
33+
else
34+
echo "release=true" >> "$GITHUB_OUTPUT"
35+
fi
36+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
37+
38+
- uses: actions/setup-node@v4
39+
if: steps.version.outputs.release == 'true'
40+
with:
41+
node-version: "22"
42+
43+
- name: Validate before releasing
44+
if: steps.version.outputs.release == 'true'
45+
run: node scripts/validate-template.mjs
46+
47+
- name: Package plugin
48+
if: steps.version.outputs.release == 'true'
49+
run: git archive --format=zip --output=coder-cursor-plugin.zip HEAD -- ':(exclude).github'
50+
51+
- name: Create GitHub Release
52+
if: steps.version.outputs.release == 'true'
53+
env:
54+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55+
run: |
56+
gh release create "v${{ steps.version.outputs.version }}" \
57+
coder-cursor-plugin.zip \
58+
--target "$GITHUB_SHA" \
59+
--title "v${{ steps.version.outputs.version }}" \
60+
--generate-notes

.github/workflows/validate.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Validate
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
workflow_dispatch:
8+
9+
permissions: {}
10+
11+
jobs:
12+
validate:
13+
name: Validate plugin
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: read
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- uses: actions/setup-node@v4
21+
with:
22+
node-version: "22"
23+
24+
- name: Validate manifests and frontmatter
25+
run: node scripts/validate-template.mjs
26+
27+
- name: Check plugin and marketplace versions match
28+
run: |
29+
set -euo pipefail
30+
PLUGIN=$(jq -er '.version' plugins/coder/.cursor-plugin/plugin.json)
31+
MARKET=$(jq -er '.metadata.version' .cursor-plugin/marketplace.json)
32+
if [ "$PLUGIN" != "$MARKET" ]; then
33+
echo "::error::plugin.json is $PLUGIN but marketplace.json is $MARKET"
34+
exit 1
35+
fi
36+
if ! printf '%s' "$PLUGIN" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
37+
echo "::error::version '$PLUGIN' is not X.Y.Z"
38+
exit 1
39+
fi
40+
41+
- name: Check vendored skills match the pin
42+
run: |
43+
set -euo pipefail
44+
node .github/scripts/sync-skills.mjs
45+
if ! git diff --quiet -- plugins/coder/skills; then
46+
echo "::error::vendored skills differ from the pinned coder/skills ref. Run node .github/scripts/sync-skills.mjs and commit."
47+
git diff --stat -- plugins/coder/skills
48+
exit 1
49+
fi

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Environment and secrets
2+
.env
3+
.env.*
4+
!.env.example
5+
6+
# OS and editor
7+
.DS_Store
8+
*.swp
9+
*.swo
10+
11+
# Logs and temp
12+
*.log
13+
tmp/
14+
temp/

CONTRIBUTING.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Contributing
2+
3+
## Layout
4+
5+
```text
6+
.cursor-plugin/marketplace.json Marketplace manifest
7+
plugins/coder/ The Coder plugin
8+
.cursor-plugin/plugin.json Plugin manifest (canonical version)
9+
mcp.json Coder remote MCP server
10+
skills/ coder-workspaces (native) + vendored coder/skills
11+
commands/ /coder-connect
12+
assets/logo.svg
13+
scripts/validate-template.mjs Manifest and frontmatter validation
14+
.github/scripts/sync-skills.mjs Re-vendor skills from coder/skills
15+
```
16+
17+
## Making changes
18+
19+
1. Branch from `main`.
20+
2. Edit the plugin. Vendored skill directories are regenerated by the sync
21+
script; change them in [coder/skills](https://github.com/coder/skills)
22+
instead. See [VENDOR.md](VENDOR.md).
23+
3. Validate:
24+
25+
```sh
26+
node scripts/validate-template.mjs
27+
```
28+
29+
4. Test locally by copying `plugins/coder` to `~/.cursor/plugins/local/coder`
30+
and reloading Cursor.
31+
5. Open a pull request. Use conventional commit titles
32+
(`feat:`, `fix:`, `chore:`, `docs:`).
33+
34+
## Releasing
35+
36+
1. Bump `version` in `plugins/coder/.cursor-plugin/plugin.json` and
37+
`metadata.version` in `.cursor-plugin/marketplace.json` to the same value.
38+
The `validate` workflow fails if they differ.
39+
2. Merge to `main`. The `release` workflow tags `vX.Y.Z` and publishes a
40+
GitHub Release when the version has no existing tag.
41+
42+
Cursor re-indexes the marketplace from `main`, so users receive the update
43+
once the merge lands and the version has changed.

LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
The MIT License
2+
3+
Copyright (c) 2026 Coder Technologies Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of
6+
this software and associated documentation files (the "Software"), to deal in
7+
the Software without restriction, including without limitation the rights to
8+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9+
the Software, and to permit persons to whom the Software is furnished to do so,
10+
subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

0 commit comments

Comments
 (0)