Skip to content

Commit 37a380a

Browse files
authored
Merge pull request #47 from flightctl/cursor-command-wrappers
Generate flat Cursor command wrappers during install
2 parents 29c2876 + 39105c0 commit 37a380a

4 files changed

Lines changed: 108 additions & 7 deletions

File tree

CONTRIBUTING.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,20 @@ The installer (`install.sh`) auto-discovers workflows by scanning for `*/SKILL.m
8888

8989
**Claude Code integration**: The installer:
9090
1. Appends workflow references to `CLAUDE.md` (or `.claude/CLAUDE.md` for project-level) beneath the `# ai-workflows` marker
91-
2. Symlinks workflows into the user-level Claude skills directory (or `.claude/skills/` for project-level) for slash command discovery
92-
3. Removes stale references (old controller.md paths) to avoid duplicates
91+
2. Symlinks workflows into the Claude skills directory (or `.claude/skills/` for project-level) for slash command discovery
92+
3. Symlinks each workflow's `commands/` directory into `.claude/commands/` so phases are discoverable as `/{workflow}:{command}` slash commands (e.g., `/bugfix:assess`, `/cve-fix:patch`)
93+
4. Removes stale references (old controller.md paths) to avoid duplicates
9394

94-
**Uninstall** (`uninstall.sh`) mirrors the install logic with removal.
95+
**Cursor integration**: Cursor uses two discovery mechanisms — skills (`SKILL.md` in `.cursor/skills/*/`) and commands (`.md` files in `.cursor/commands/`). The installer uses both:
96+
97+
1. Symlinks each workflow directory into `.cursor/skills/{workflow}/` for top-level skill discovery
98+
2. For each `commands/{phase}.md` in a workflow, generates a command file `.cursor/commands/{workflow}-{phase}.md` — a thin dispatch prompt that reads the workflow's controller and dispatches the phase
99+
100+
Cursor scans both project-level (`.cursor/commands/`) and user-level (`~/.cursor/commands/`) directories, so commands work at either scope. No manifest file is needed — uninstall identifies generated commands by matching `{workflow}-*.md` filenames against existing `commands/*.md` source files.
101+
102+
**Note on symlinks**: The skill symlinks (`.cursor/skills/{workflow}/` -> `~/.ai-workflows/{workflow}`) depend on Cursor following symlinks for top-level skill discovery. There are [reported issues](https://forum.cursor.com/t/cursor-doesnt-follow-symlinks-to-discover-skills/149693) with this in some Cursor versions. The generated command files avoid this problem by using absolute paths to `$INSTALL_DIR`, so the slash commands work independently of symlink resolution.
103+
104+
**Uninstall** (`uninstall.sh`) mirrors the install logic with removal. For Cursor, it removes generated command files by matching `{workflow}-{phase}.md` against the source workflow's `commands/` directory to avoid removing unrelated files. Selective uninstall (`--workflows`) only removes commands belonging to the specified workflows.
95105

96106
## Testing Your Changes
97107

README.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,21 +125,41 @@ Use `--workflows` to install only the workflows relevant to a given project:
125125
./install.sh --list # show available workflows
126126
```
127127

128+
For project-level Cursor installs, add the generated commands directory to `.gitignore`:
129+
130+
```gitignore
131+
.cursor/commands/
132+
```
133+
134+
The skill symlinks under `.cursor/skills/` may also need ignoring depending on your project's conventions.
135+
128136
## Scopes
129137

130138
| Scope | Cursor | Claude Code |
131139
|-------|--------|-------------|
132-
| **User** (default) | `~/.cursor/skills/<workflow>` | `~/.claude/CLAUDE.md` |
133-
| **Project** (`--project`) | `.cursor/skills/<workflow>` | `.claude/CLAUDE.md` |
140+
| **User** (default) | `~/.cursor/skills/<workflow>` + `~/.cursor/commands/` | `~/.claude/CLAUDE.md` |
141+
| **Project** (`--project`) | `.cursor/skills/<workflow>` + `.cursor/commands/` | `.claude/CLAUDE.md` |
134142

135143
## Usage
136144

137-
Invoke a workflow command (works in both Cursor and Claude Code):
145+
### Claude Code
146+
147+
Invoke a workflow command using the colon-namespaced format:
138148

139149
- `/bugfix:assess`, `/bugfix:diagnose`, `/bugfix:fix`, ...
140150
- `/code-review:start`, `/code-review:continue`, `/code-review:clean`
141151
- `/docs-writer:gather`, `/docs-writer:plan`, `/docs-writer:draft`, ...
142152

153+
### Cursor
154+
155+
The installer generates flat command files in `.cursor/commands/` so each phase appears in the Cursor slash menu:
156+
157+
- `/bugfix-assess`, `/bugfix-diagnose`, `/bugfix-fix`, ...
158+
- `/code-review-start`, `/code-review-continue`, `/code-review-clean`
159+
- `/docs-writer-gather`, `/docs-writer-plan`, `/docs-writer-draft`, ...
160+
161+
Cursor scans both project-level (`.cursor/commands/`) and user-level (`~/.cursor/commands/`) directories. Commands are plain `.md` files — no manifest or wrapper directories needed. They are created by `install.sh` and cleaned up by `uninstall.sh`.
162+
143163
## Updating
144164

145165
```bash

install.sh

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,19 +135,65 @@ install_shared() {
135135
echo " Linked ${target_dir}/_shared -> ${INSTALL_DIR}/_shared ($SCOPE)"
136136
}
137137

138+
generate_cursor_commands() {
139+
local cmds_dir="$1"
140+
local generated=0
141+
142+
for wf in "${WORKFLOWS[@]}"; do
143+
local wf_dir="${INSTALL_DIR}/${wf}"
144+
[[ -d "${wf_dir}/commands" ]] || continue
145+
146+
for cmd_file in "${wf_dir}"/commands/*.md; do
147+
[[ -f "$cmd_file" ]] || continue
148+
local phase
149+
phase="$(basename "$cmd_file" .md)"
150+
local cmd_name="${wf}-${phase}"
151+
152+
local description=""
153+
if head -1 "$cmd_file" | grep -q "^---"; then
154+
description="$(awk '/^---/{n++; next} n==1 && /^description:/{sub(/^description:[[:space:]]*"?/, ""); sub(/"[[:space:]]*$/, ""); print; exit}' "$cmd_file")"
155+
fi
156+
if [[ -z "$description" ]] && [[ -f "${wf_dir}/skills/${phase}.md" ]]; then
157+
description="$(awk '/^---/{n++; next} n==1 && /^description:/{sub(/^description:[[:space:]]*"?/, ""); sub(/"[[:space:]]*$/, ""); print; exit}' "${wf_dir}/skills/${phase}.md")"
158+
fi
159+
[[ -z "$description" ]] && description="Run the ${phase} phase of the ${wf} workflow."
160+
description="${description//\"/\\\"}"
161+
162+
cat > "${cmds_dir}/${cmd_name}.md" <<CMD_EOF
163+
---
164+
description: "${description}"
165+
---
166+
# /${phase} (${wf})
167+
168+
Read \`${INSTALL_DIR}/${wf}/skills/controller.md\` and follow it.
169+
170+
Dispatch the **${phase}** phase. Context:
171+
172+
\$ARGUMENTS
173+
CMD_EOF
174+
generated=$((generated + 1))
175+
done
176+
done
177+
178+
[[ $generated -gt 0 ]] && echo " Generated ${generated} command(s) in ${cmds_dir} ($SCOPE)"
179+
}
180+
138181
install_cursor() {
139182
if [[ "$SCOPE" == "project" ]]; then
140183
SKILLS_DIR="${PROJECT_ROOT}/.cursor/skills"
184+
CMDS_DIR="${PROJECT_ROOT}/.cursor/commands"
141185
else
142186
SKILLS_DIR="${HOME}/.cursor/skills"
187+
CMDS_DIR="${HOME}/.cursor/commands"
143188
fi
144189

145-
mkdir -p "$SKILLS_DIR"
190+
mkdir -p "$SKILLS_DIR" "$CMDS_DIR"
146191
install_shared "$SKILLS_DIR"
147192
for wf in "${WORKFLOWS[@]}"; do
148193
ln -sfn "${INSTALL_DIR}/${wf}" "${SKILLS_DIR}/${wf}"
149194
echo " Linked ${SKILLS_DIR}/${wf} -> ${INSTALL_DIR}/${wf} ($SCOPE)"
150195
done
196+
generate_cursor_commands "$CMDS_DIR"
151197
}
152198

153199
install_claude() {

uninstall.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,38 @@ has_remaining_workflows() {
116116
return 1
117117
}
118118

119+
remove_cursor_commands() {
120+
local cmds_dir="$1"
121+
local removed=0
122+
123+
[[ -d "$cmds_dir" ]] || return 0
124+
125+
for wf in "${WORKFLOWS[@]}"; do
126+
for cmd_file in "${cmds_dir}/${wf}"-*.md; do
127+
[[ -f "$cmd_file" ]] || continue
128+
local base
129+
base="$(basename "$cmd_file" .md)"
130+
local suffix="${base#"${wf}-"}"
131+
if [[ -f "${INSTALL_DIR}/${wf}/commands/${suffix}.md" ]]; then
132+
rm -f "$cmd_file"
133+
removed=$((removed + 1))
134+
fi
135+
done
136+
done
137+
138+
[[ $removed -gt 0 ]] && echo " Removed ${removed} command(s) from ${cmds_dir} ($SCOPE)"
139+
}
140+
119141
uninstall_cursor() {
120142
if [[ "$SCOPE" == "project" ]]; then
121143
SKILLS_DIR="${PROJECT_ROOT}/.cursor/skills"
144+
CMDS_DIR="${PROJECT_ROOT}/.cursor/commands"
122145
else
123146
SKILLS_DIR="${HOME}/.cursor/skills"
147+
CMDS_DIR="${HOME}/.cursor/commands"
124148
fi
125149

150+
remove_cursor_commands "$CMDS_DIR"
126151
if [[ "$SELECTIVE" == false ]]; then
127152
uninstall_shared "$SKILLS_DIR"
128153
fi

0 commit comments

Comments
 (0)