@meshxdata/fops 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/commands/index.js +163 -1
- package/src/doctor.js +155 -17
- package/src/plugins/bundled/coda/auth.js +79 -0
- package/src/plugins/bundled/coda/client.js +187 -0
- package/src/plugins/bundled/coda/fops.plugin.json +7 -0
- package/src/plugins/bundled/coda/index.js +284 -0
- package/src/plugins/bundled/coda/package.json +3 -0
- package/src/plugins/bundled/coda/skills/coda/SKILL.md +82 -0
- package/src/plugins/bundled/cursor/fops.plugin.json +7 -0
- package/src/plugins/bundled/cursor/index.js +432 -0
- package/src/plugins/bundled/cursor/package.json +1 -0
- package/src/plugins/bundled/cursor/skills/cursor/SKILL.md +48 -0
- package/src/plugins/bundled/fops-plugin-1password/fops.plugin.json +7 -0
- package/src/plugins/bundled/fops-plugin-1password/index.js +239 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/env.js +100 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/op.js +111 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/setup.js +235 -0
- package/src/plugins/bundled/fops-plugin-1password/lib/sync.js +61 -0
- package/src/plugins/bundled/fops-plugin-1password/package.json +1 -0
- package/src/plugins/bundled/fops-plugin-1password/skills/1password/SKILL.md +79 -0
- package/src/plugins/bundled/fops-plugin-ecr/fops.plugin.json +7 -0
- package/src/plugins/bundled/fops-plugin-ecr/index.js +302 -0
- package/src/plugins/bundled/fops-plugin-ecr/lib/aws.js +147 -0
- package/src/plugins/bundled/fops-plugin-ecr/lib/images.js +73 -0
- package/src/plugins/bundled/fops-plugin-ecr/lib/setup.js +180 -0
- package/src/plugins/bundled/fops-plugin-ecr/lib/sync.js +74 -0
- package/src/plugins/bundled/fops-plugin-ecr/package.json +1 -0
- package/src/plugins/bundled/fops-plugin-ecr/skills/ecr/SKILL.md +105 -0
- package/src/plugins/bundled/fops-plugin-memory/fops.plugin.json +7 -0
- package/src/plugins/bundled/fops-plugin-memory/index.js +148 -0
- package/src/plugins/bundled/fops-plugin-memory/lib/relevance.js +72 -0
- package/src/plugins/bundled/fops-plugin-memory/lib/store.js +75 -0
- package/src/plugins/bundled/fops-plugin-memory/package.json +1 -0
- package/src/plugins/bundled/fops-plugin-memory/skills/memory/SKILL.md +58 -0
- package/src/plugins/loader.js +40 -0
- package/src/setup/aws.js +51 -38
- package/src/setup/setup.js +2 -0
- package/src/setup/wizard.js +137 -12
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the cursor CLI binary path.
|
|
8
|
+
* Checks PATH first, then known macOS/Linux install locations.
|
|
9
|
+
*/
|
|
10
|
+
function resolveCursorBin() {
|
|
11
|
+
// Check if cursor is on PATH
|
|
12
|
+
try {
|
|
13
|
+
execFileSync("cursor", ["--version"], { encoding: "utf8", timeout: 3000, stdio: "pipe" });
|
|
14
|
+
return "cursor";
|
|
15
|
+
} catch {
|
|
16
|
+
// Not on PATH — check common install locations
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const candidates = [
|
|
20
|
+
// macOS: Cursor.app
|
|
21
|
+
"/Applications/Cursor.app/Contents/Resources/app/bin/cursor",
|
|
22
|
+
// macOS: user-local
|
|
23
|
+
path.join(os.homedir(), "Applications", "Cursor.app", "Contents", "Resources", "app", "bin", "cursor"),
|
|
24
|
+
// Linux: common install paths
|
|
25
|
+
"/usr/share/cursor/bin/cursor",
|
|
26
|
+
"/opt/cursor/bin/cursor",
|
|
27
|
+
path.join(os.homedir(), ".local", "bin", "cursor"),
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
for (const candidate of candidates) {
|
|
31
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let _cursorBin;
|
|
37
|
+
function cursorBin() {
|
|
38
|
+
if (_cursorBin === undefined) _cursorBin = resolveCursorBin();
|
|
39
|
+
return _cursorBin;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run a command and return stdout, or null on failure.
|
|
44
|
+
*/
|
|
45
|
+
function run(cmd, args, opts = {}) {
|
|
46
|
+
try {
|
|
47
|
+
return execFileSync(cmd, args, { encoding: "utf8", timeout: 5000, stdio: "pipe", ...opts }).trim();
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Spawn a command with inherited stdio (fire-and-forget for GUI apps).
|
|
55
|
+
*/
|
|
56
|
+
function open(cmd, args) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const child = spawn(cmd, args, { stdio: "inherit" });
|
|
59
|
+
child.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited with ${code}`))));
|
|
60
|
+
child.on("error", reject);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Trigger Cursor's Composer (Cmd/Ctrl+I), paste the instruction, and submit.
|
|
66
|
+
* macOS: AppleScript, Linux: xdotool, Windows: PowerShell SendKeys.
|
|
67
|
+
*/
|
|
68
|
+
function triggerComposer(instruction, files) {
|
|
69
|
+
const prompt = [
|
|
70
|
+
instruction,
|
|
71
|
+
"",
|
|
72
|
+
"Target files:",
|
|
73
|
+
...files.map((f) => `- ${f}`),
|
|
74
|
+
].join("\n");
|
|
75
|
+
|
|
76
|
+
if (process.platform === "darwin") {
|
|
77
|
+
const escaped = prompt.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
78
|
+
const script = `
|
|
79
|
+
delay 2
|
|
80
|
+
tell application "Cursor" to activate
|
|
81
|
+
delay 0.5
|
|
82
|
+
tell application "System Events"
|
|
83
|
+
tell process "Cursor"
|
|
84
|
+
keystroke "i" using {command down}
|
|
85
|
+
delay 0.8
|
|
86
|
+
set the clipboard to "${escaped}"
|
|
87
|
+
keystroke "v" using {command down}
|
|
88
|
+
delay 0.3
|
|
89
|
+
key code 36
|
|
90
|
+
end tell
|
|
91
|
+
end tell
|
|
92
|
+
`;
|
|
93
|
+
const child = spawn("osascript", ["-e", script], { stdio: "ignore", detached: true });
|
|
94
|
+
child.unref();
|
|
95
|
+
} else if (process.platform === "linux") {
|
|
96
|
+
// xdotool: wait for Cursor window, send Ctrl+I, type instruction, submit
|
|
97
|
+
const escaped = prompt.replace(/'/g, "'\\''");
|
|
98
|
+
const script = `
|
|
99
|
+
sleep 2
|
|
100
|
+
xdotool search --name "Cursor" windowactivate --sync
|
|
101
|
+
sleep 0.5
|
|
102
|
+
xdotool key ctrl+i
|
|
103
|
+
sleep 0.8
|
|
104
|
+
xdotool type --clearmodifiers -- '${escaped}'
|
|
105
|
+
sleep 0.3
|
|
106
|
+
xdotool key Return
|
|
107
|
+
`;
|
|
108
|
+
const child = spawn("bash", ["-c", script], { stdio: "ignore", detached: true });
|
|
109
|
+
child.unref();
|
|
110
|
+
} else if (process.platform === "win32") {
|
|
111
|
+
// PowerShell: activate Cursor, send Ctrl+I, paste, submit
|
|
112
|
+
const escaped = prompt.replace(/'/g, "''").replace(/`/g, "``");
|
|
113
|
+
const ps = `
|
|
114
|
+
Start-Sleep -Seconds 2
|
|
115
|
+
$wshell = New-Object -ComObject wscript.shell
|
|
116
|
+
$wshell.AppActivate('Cursor')
|
|
117
|
+
Start-Sleep -Milliseconds 500
|
|
118
|
+
$wshell.SendKeys('^i')
|
|
119
|
+
Start-Sleep -Milliseconds 800
|
|
120
|
+
Set-Clipboard '${escaped}'
|
|
121
|
+
$wshell.SendKeys('^v')
|
|
122
|
+
Start-Sleep -Milliseconds 300
|
|
123
|
+
$wshell.SendKeys('{ENTER}')
|
|
124
|
+
`;
|
|
125
|
+
const child = spawn("powershell", ["-NoProfile", "-Command", ps], { stdio: "ignore", detached: true });
|
|
126
|
+
child.unref();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse SKILL.md frontmatter → { meta, body }.
|
|
132
|
+
*/
|
|
133
|
+
function parseFrontmatter(content) {
|
|
134
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
135
|
+
if (!match) return { meta: {}, body: content };
|
|
136
|
+
const meta = {};
|
|
137
|
+
for (const line of match[1].split("\n")) {
|
|
138
|
+
const kv = line.match(/^(\w+)\s*:\s*(.+)/);
|
|
139
|
+
if (kv) meta[kv[1]] = kv[2].trim();
|
|
140
|
+
}
|
|
141
|
+
return { meta, body: match[2] };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Collect all SKILL.md files from known locations.
|
|
146
|
+
* Returns array of { name, description, content }.
|
|
147
|
+
*/
|
|
148
|
+
function collectSkills() {
|
|
149
|
+
const skills = [];
|
|
150
|
+
const seen = new Set();
|
|
151
|
+
|
|
152
|
+
function scanDir(dir) {
|
|
153
|
+
if (!fs.existsSync(dir)) return;
|
|
154
|
+
try {
|
|
155
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
if (!entry.isDirectory()) continue;
|
|
158
|
+
const skillMd = path.join(dir, entry.name, "SKILL.md");
|
|
159
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
160
|
+
const raw = fs.readFileSync(skillMd, "utf8");
|
|
161
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
162
|
+
const name = meta.name || entry.name;
|
|
163
|
+
if (seen.has(name)) continue;
|
|
164
|
+
seen.add(name);
|
|
165
|
+
skills.push({ name, description: meta.description || "", content: body.trim() });
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// ignore
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 1. Built-in CLI skills (resolve from the CLI entry point)
|
|
173
|
+
const cliSkillsDirs = [
|
|
174
|
+
process.argv[1] ? path.resolve(path.dirname(process.argv[1]), "src", "skills") : null,
|
|
175
|
+
path.resolve(os.homedir(), ".fops", "cli", "src", "skills"),
|
|
176
|
+
].filter(Boolean);
|
|
177
|
+
|
|
178
|
+
for (const dir of cliSkillsDirs) {
|
|
179
|
+
scanDir(dir);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 2. User skills ~/.fops/skills/
|
|
183
|
+
scanDir(path.join(os.homedir(), ".fops", "skills"));
|
|
184
|
+
|
|
185
|
+
// 3. Plugin skills ~/.fops/plugins/*/skills/
|
|
186
|
+
const pluginsDir = path.join(os.homedir(), ".fops", "plugins");
|
|
187
|
+
if (fs.existsSync(pluginsDir)) {
|
|
188
|
+
try {
|
|
189
|
+
const plugins = fs.readdirSync(pluginsDir, { withFileTypes: true });
|
|
190
|
+
for (const p of plugins) {
|
|
191
|
+
if (!p.isDirectory()) continue;
|
|
192
|
+
scanDir(path.join(pluginsDir, p.name, "skills"));
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// ignore
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return skills;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build .mdc frontmatter block.
|
|
204
|
+
*/
|
|
205
|
+
function mdcFrontmatter({ description, globs, alwaysApply }) {
|
|
206
|
+
const lines = ["---"];
|
|
207
|
+
if (description) lines.push(`description: ${description}`);
|
|
208
|
+
if (globs) lines.push(`globs: ${globs}`);
|
|
209
|
+
if (alwaysApply != null) lines.push(`alwaysApply: ${alwaysApply}`);
|
|
210
|
+
lines.push("---");
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Write .cursorrules + .cursor/rules/fops-*.mdc from collected skills.
|
|
216
|
+
* Returns number of skills synced.
|
|
217
|
+
*/
|
|
218
|
+
function syncRules(cwd, skills) {
|
|
219
|
+
if (skills.length === 0) return 0;
|
|
220
|
+
|
|
221
|
+
// Write concatenated .cursorrules
|
|
222
|
+
const combined = skills
|
|
223
|
+
.map((s) => `# ${s.name}\n\n${s.content}`)
|
|
224
|
+
.join("\n\n---\n\n");
|
|
225
|
+
fs.writeFileSync(path.join(cwd, ".cursorrules"), combined, "utf8");
|
|
226
|
+
|
|
227
|
+
// Write individual .cursor/rules/fops-<name>.mdc files
|
|
228
|
+
const rulesDir = path.join(cwd, ".cursor", "rules");
|
|
229
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
230
|
+
|
|
231
|
+
for (const skill of skills) {
|
|
232
|
+
const safeName = skill.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
233
|
+
const front = mdcFrontmatter({
|
|
234
|
+
description: skill.description || skill.name,
|
|
235
|
+
alwaysApply: true,
|
|
236
|
+
});
|
|
237
|
+
fs.writeFileSync(path.join(rulesDir, `fops-${safeName}.mdc`), `${front}\n\n${skill.content}\n`, "utf8");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return skills.length;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function register(api) {
|
|
244
|
+
// ── Doctor check ──────────────────────────────────
|
|
245
|
+
api.registerDoctorCheck({
|
|
246
|
+
name: "Cursor IDE",
|
|
247
|
+
fn: async (ok, warn) => {
|
|
248
|
+
const bin = cursorBin();
|
|
249
|
+
if (bin) {
|
|
250
|
+
const version = run(bin, ["--version"]);
|
|
251
|
+
ok("Cursor IDE", version || bin);
|
|
252
|
+
} else {
|
|
253
|
+
warn("Cursor IDE", "not found — install from cursor.com, then: Cmd+Shift+P → Shell Command: Install 'cursor'");
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ── Auto-run patterns (agent executes these immediately) ──
|
|
259
|
+
api.registerAutoRunPattern("fops cursor open");
|
|
260
|
+
api.registerAutoRunPattern("fops cursor edit");
|
|
261
|
+
api.registerAutoRunPattern("fops cursor rules sync");
|
|
262
|
+
|
|
263
|
+
// ── Commands ──────────────────────────────────────
|
|
264
|
+
api.registerCommand((program) => {
|
|
265
|
+
const cursor = program
|
|
266
|
+
.command("cursor")
|
|
267
|
+
.description("Cursor IDE integration — open files, sync rules, edit with AI");
|
|
268
|
+
|
|
269
|
+
// fops cursor open [path] [-g line]
|
|
270
|
+
cursor
|
|
271
|
+
.command("open [path]")
|
|
272
|
+
.description("Open file or folder in Cursor")
|
|
273
|
+
.option("-g <line>", "Go to line number")
|
|
274
|
+
.action(async (targetPath, opts) => {
|
|
275
|
+
const bin = cursorBin();
|
|
276
|
+
if (!bin) { console.error("Cursor CLI not found. Install from cursor.com, then: Cmd+Shift+P → 'Install cursor command'"); process.exit(1); }
|
|
277
|
+
const target = targetPath || ".";
|
|
278
|
+
const args = [];
|
|
279
|
+
if (opts.g) args.push("-g", `${target}:${opts.g}`);
|
|
280
|
+
else args.push(target);
|
|
281
|
+
try {
|
|
282
|
+
await open(bin, args);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.error(`Failed to open Cursor: ${err.message}`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// fops cursor rules sync | show | clean
|
|
290
|
+
const rules = cursor
|
|
291
|
+
.command("rules")
|
|
292
|
+
.description("Manage Cursor AI rules synced from fops skills");
|
|
293
|
+
|
|
294
|
+
rules
|
|
295
|
+
.command("sync")
|
|
296
|
+
.description("Sync all fops skills into .cursorrules and .cursor/rules/")
|
|
297
|
+
.action(async () => {
|
|
298
|
+
const cwd = process.cwd();
|
|
299
|
+
const skills = collectSkills();
|
|
300
|
+
|
|
301
|
+
if (skills.length === 0) {
|
|
302
|
+
console.log("No skills found to sync.");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
syncRules(cwd, skills);
|
|
307
|
+
|
|
308
|
+
console.log(`Synced ${skills.length} skill(s):`);
|
|
309
|
+
for (const s of skills) {
|
|
310
|
+
console.log(` - ${s.name}`);
|
|
311
|
+
}
|
|
312
|
+
console.log(`\nWrote .cursorrules and ${skills.length} .cursor/rules/fops-*.mdc file(s).`);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
rules
|
|
316
|
+
.command("show")
|
|
317
|
+
.description("Display current .cursorrules and .cursor/rules/*.mdc files")
|
|
318
|
+
.action(async () => {
|
|
319
|
+
const cwd = process.cwd();
|
|
320
|
+
const cursorrules = path.join(cwd, ".cursorrules");
|
|
321
|
+
|
|
322
|
+
if (fs.existsSync(cursorrules)) {
|
|
323
|
+
console.log("── .cursorrules ──");
|
|
324
|
+
console.log(fs.readFileSync(cursorrules, "utf8"));
|
|
325
|
+
} else {
|
|
326
|
+
console.log("No .cursorrules file found. Run: fops cursor rules sync");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const rulesDir = path.join(cwd, ".cursor", "rules");
|
|
330
|
+
if (fs.existsSync(rulesDir)) {
|
|
331
|
+
try {
|
|
332
|
+
const files = fs.readdirSync(rulesDir).filter((f) => f.endsWith(".mdc"));
|
|
333
|
+
if (files.length > 0) {
|
|
334
|
+
console.log("\n── .cursor/rules/*.mdc ──");
|
|
335
|
+
for (const f of files) {
|
|
336
|
+
console.log(` ${f}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
// ignore
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
rules
|
|
346
|
+
.command("clean")
|
|
347
|
+
.description("Remove .cursorrules and fops-generated .cursor/rules/fops-*.mdc files")
|
|
348
|
+
.action(async () => {
|
|
349
|
+
const cwd = process.cwd();
|
|
350
|
+
let removed = 0;
|
|
351
|
+
|
|
352
|
+
const cursorrules = path.join(cwd, ".cursorrules");
|
|
353
|
+
if (fs.existsSync(cursorrules)) {
|
|
354
|
+
fs.unlinkSync(cursorrules);
|
|
355
|
+
console.log("Removed .cursorrules");
|
|
356
|
+
removed++;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const rulesDir = path.join(cwd, ".cursor", "rules");
|
|
360
|
+
if (fs.existsSync(rulesDir)) {
|
|
361
|
+
try {
|
|
362
|
+
const files = fs.readdirSync(rulesDir).filter((f) => f.startsWith("fops-") && f.endsWith(".mdc"));
|
|
363
|
+
for (const f of files) {
|
|
364
|
+
fs.unlinkSync(path.join(rulesDir, f));
|
|
365
|
+
console.log(`Removed .cursor/rules/${f}`);
|
|
366
|
+
removed++;
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
// ignore
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (removed === 0) console.log("Nothing to clean.");
|
|
374
|
+
else console.log(`\nRemoved ${removed} file(s).`);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// fops cursor edit <files...> -m <instruction> [--rules-sync]
|
|
378
|
+
cursor
|
|
379
|
+
.command("edit <files...>")
|
|
380
|
+
.description("Open files in Cursor with a task-specific AI instruction")
|
|
381
|
+
.requiredOption("-m, --message <instruction>", "AI instruction for the task")
|
|
382
|
+
.option("--rules-sync", "Sync fops skills into .cursorrules before opening")
|
|
383
|
+
.action(async (files, opts) => {
|
|
384
|
+
const cwd = process.cwd();
|
|
385
|
+
|
|
386
|
+
// Auto-sync rules if flag is set or .cursorrules doesn't exist
|
|
387
|
+
if (opts.rulesSync || !fs.existsSync(path.join(cwd, ".cursorrules"))) {
|
|
388
|
+
const skills = collectSkills();
|
|
389
|
+
const count = syncRules(cwd, skills);
|
|
390
|
+
if (count > 0) {
|
|
391
|
+
console.log(`Synced ${count} skill(s) into .cursorrules`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Create task-specific rule
|
|
396
|
+
const rulesDir = path.join(cwd, ".cursor", "rules");
|
|
397
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
398
|
+
|
|
399
|
+
const globs = files.join(", ");
|
|
400
|
+
const front = mdcFrontmatter({
|
|
401
|
+
description: opts.message,
|
|
402
|
+
globs,
|
|
403
|
+
alwaysApply: true,
|
|
404
|
+
});
|
|
405
|
+
const body = [
|
|
406
|
+
"## Task",
|
|
407
|
+
"",
|
|
408
|
+
opts.message,
|
|
409
|
+
"",
|
|
410
|
+
"## Target files",
|
|
411
|
+
"",
|
|
412
|
+
...files.map((f) => `- ${f}`),
|
|
413
|
+
"",
|
|
414
|
+
"Read the target files above and apply the instruction.",
|
|
415
|
+
].join("\n");
|
|
416
|
+
|
|
417
|
+
fs.writeFileSync(path.join(rulesDir, "fops-task.mdc"), `${front}\n\n${body}\n`, "utf8");
|
|
418
|
+
console.log("Created .cursor/rules/fops-task.mdc");
|
|
419
|
+
|
|
420
|
+
// Open files in Cursor and trigger Composer with instruction
|
|
421
|
+
const bin = cursorBin();
|
|
422
|
+
if (!bin) { console.error("Cursor CLI not found. Install from cursor.com, then: Cmd+Shift+P → 'Install cursor command'"); process.exit(1); }
|
|
423
|
+
try {
|
|
424
|
+
await open(bin, files);
|
|
425
|
+
triggerComposer(opts.message, files);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
console.error(`Failed to open Cursor: ${err.message}`);
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "type": "module" }
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cursor
|
|
3
|
+
description: Cursor IDE integration — open files, sync AI rules, edit with instructions
|
|
4
|
+
requires: cursor
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Cursor IDE Commands
|
|
8
|
+
|
|
9
|
+
The `fops cursor` commands bridge fops knowledge into Cursor's AI via `.cursorrules` and `.cursor/rules/*.mdc` files.
|
|
10
|
+
|
|
11
|
+
**IMPORTANT: When the user asks to edit code, open a file, or work on something in Cursor — DO NOT just suggest the command. Output the `fops cursor edit` or `fops cursor open` command in a bash block and it will execute automatically.** Be direct: "Opening in Cursor..." not "you could run...".
|
|
12
|
+
|
|
13
|
+
### When to act
|
|
14
|
+
|
|
15
|
+
- User says "edit X", "fix X", "refactor X", "open X in cursor" → immediately output `fops cursor edit <files> -m "<instruction>"`
|
|
16
|
+
- User says "open X" or "open in cursor" → immediately output `fops cursor open <path>`
|
|
17
|
+
- User asks to modify code with AI → `fops cursor edit`
|
|
18
|
+
|
|
19
|
+
### Open files in Cursor
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
fops cursor open # open current directory
|
|
23
|
+
fops cursor open src/api.py # open a specific file
|
|
24
|
+
fops cursor open src/api.py -g 42 # open at a specific line
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Sync fops skills as Cursor rules
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
fops cursor rules sync # write all fops skills to .cursorrules + .cursor/rules/fops-*.mdc
|
|
31
|
+
fops cursor rules show # display current rules
|
|
32
|
+
fops cursor rules clean # remove fops-generated rules (keeps user-authored .mdc files)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Edit with AI instructions
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
fops cursor edit src/agent/context.js -m "add error handling for missing skills"
|
|
39
|
+
fops cursor edit src/api.py src/models.py -m "add pagination to the list endpoint"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This creates a `.cursor/rules/fops-task.mdc` with the instruction and target files, then opens them in Cursor. If `.cursorrules` doesn't exist yet, it auto-syncs rules first.
|
|
43
|
+
|
|
44
|
+
Use `--rules-sync` to force a fresh rules sync before opening:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
fops cursor edit src/api.py -m "refactor auth" --rules-sync
|
|
48
|
+
```
|