@b-apm/integration-cursor 0.1.0
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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +366 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sdju
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, 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,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @b-apm/integration-cursor
|
|
2
|
+
|
|
3
|
+
> **UNSTABLE:** Early public release. APIs and on-disk layouts may change without a major bump. Not production-ready.
|
|
4
|
+
|
|
5
|
+
Minimal **Cursor** host for bapm (M5 polish + M9 MCP configure).
|
|
6
|
+
|
|
7
|
+
## Detection vs forced target
|
|
8
|
+
|
|
9
|
+
| Mode | When active | Creates roots? |
|
|
10
|
+
| ------------------------------ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| **Auto-detect** | `.cursor/` **directory** **or** legacy `.cursorrules` **file** at project root | Only when materialize / configureMcp runs after a positive detect |
|
|
12
|
+
| **Forced** (`--target cursor`) | Explicit CLI/core forced target id, even if detect is false | MAY `mkdir` registered roots (`.cursor/`, `.agents/skills`, `.cursor/rules`, `.cursor/agents`) as needed |
|
|
13
|
+
|
|
14
|
+
Auto-detect without force MUST NOT create `.cursor/` solely for MCP opt-in. Forced activation is owned by Install / CLI; this package’s `detect` stays an honest presence predicate.
|
|
15
|
+
|
|
16
|
+
## Deploy roots (tg-002 / tg-003)
|
|
17
|
+
|
|
18
|
+
Registered roots:
|
|
19
|
+
|
|
20
|
+
- `.agents/skills` — skills → `<name>/SKILL.md`
|
|
21
|
+
- `.cursor` — companion root (detection + rules/agents + MCP config)
|
|
22
|
+
|
|
23
|
+
## Materialize routing
|
|
24
|
+
|
|
25
|
+
| Primitive type | Destination |
|
|
26
|
+
| -------------- | -------------------------------- |
|
|
27
|
+
| skill | `.agents/skills/<name>/SKILL.md` |
|
|
28
|
+
| instruction | `.cursor/rules/<name>.mdc` |
|
|
29
|
+
| agent | `.cursor/agents/<name>.md` |
|
|
30
|
+
|
|
31
|
+
Thin copy/write from source content (minimal frontmatter only when content is missing). Writes are idempotent overwrites and never escape registered roots.
|
|
32
|
+
|
|
33
|
+
**Skills / rules / agents materialize does not write `.cursor/mcp.json`.** MCP config is written only via the separate `configureMcp` path when install requests it.
|
|
34
|
+
|
|
35
|
+
`materialize` returns a `MaterializeReport` (`deployedFiles: { path }[]`) via `@b-apm/integration-api` for lock inventory.
|
|
36
|
+
|
|
37
|
+
## MCP configure (`configureMcp`)
|
|
38
|
+
|
|
39
|
+
When install invokes `configureMcp` with eligible servers:
|
|
40
|
+
|
|
41
|
+
- Writes / updates `.cursor/mcp.json` under the registered `.cursor/` root only
|
|
42
|
+
- Shape: `{ "mcpServers": { "<name>": { "command"|"url"|… } } }` (stdio / http)
|
|
43
|
+
- Idempotent overwrite of bapm-owned server keys (keyed by server name); unknown user keys are preserved when present
|
|
44
|
+
- Returns a `ConfigureMcpReport` (`configPath`, `servers`, `deployedFiles`) for lock `mcp_*` inventory
|
|
45
|
+
|
|
46
|
+
## Dependencies
|
|
47
|
+
|
|
48
|
+
Depends only on `@b-apm/integration-api` among bapm packages — not `@b-apm/core`.
|
|
49
|
+
Shared materialize helpers (`sanitizeName`, `assertUnderDeployRoots`, `readPrimitiveContent`, …) come from `@b-apm/integration-api`; this package keeps Cursor detect and path routing only.
|
|
50
|
+
Register via `createIntegrationRegistry().register(createCursorIntegration())` in CLI or tests.
|
|
51
|
+
|
|
52
|
+
Third-party authors publishing a custom host for object-map `targets` should prefer a named `createIntegration` factory (see `@b-apm/integration-api` README — loadable export contract). This package’s `createCursorIntegration` remains the Cursor-specific factory accepted by the CLI loader.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BapmIntegration } from "@b-apm/integration-api";
|
|
2
|
+
//#region src/createCursorIntegration.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Create the Cursor target.
|
|
5
|
+
* Detect: `.cursor/` directory **or** legacy `.cursorrules` file.
|
|
6
|
+
* Materialize: skills → `.agents/skills/<name>/SKILL.md`,
|
|
7
|
+
* instructions → `.cursor/rules/<name>.mdc`,
|
|
8
|
+
* agents → `.cursor/agents/<name>.md`,
|
|
9
|
+
* commands → `.cursor/commands/<name>.md`,
|
|
10
|
+
* hooks → merge `.cursor/hooks.json` (+ script copy under `.cursor/hooks/`).
|
|
11
|
+
* MCP: optional `configureMcp` writes `.cursor/mcp.json` (not via materialize).
|
|
12
|
+
*/
|
|
13
|
+
declare function createCursorIntegration(options?: {
|
|
14
|
+
id?: string;
|
|
15
|
+
deployRoots?: string[];
|
|
16
|
+
}): BapmIntegration;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { createCursorIntegration };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { SHARED_COMMAND_FRONTMATTER_KEYS, assertUnderDeployRoots, compileMarkdownReport, copyHookScript, filterFrontmatterKeys, materializeSkill, primitivesList, primitivesMaterialize, readHookOwnershipSidecar, readPrimitiveContent, renderPrimitivesMarkdown, sanitizeName, stripOwnedHookCommands, writeDeployedFile, writeHookOwnershipSidecar } from "@b-apm/integration-api";
|
|
4
|
+
//#region src/createCursorIntegration.ts
|
|
5
|
+
const DEFAULT_DEPLOY_ROOTS = [".agents/skills", ".cursor"];
|
|
6
|
+
const MCP_JSON_REL = ".cursor/mcp.json";
|
|
7
|
+
const HOOKS_JSON_REL = ".cursor/hooks.json";
|
|
8
|
+
const HOOKS_OWNERSHIP_REL = ".cursor/bapm-hooks.json";
|
|
9
|
+
/**
|
|
10
|
+
* Create the Cursor target.
|
|
11
|
+
* Detect: `.cursor/` directory **or** legacy `.cursorrules` file.
|
|
12
|
+
* Materialize: skills → `.agents/skills/<name>/SKILL.md`,
|
|
13
|
+
* instructions → `.cursor/rules/<name>.mdc`,
|
|
14
|
+
* agents → `.cursor/agents/<name>.md`,
|
|
15
|
+
* commands → `.cursor/commands/<name>.md`,
|
|
16
|
+
* hooks → merge `.cursor/hooks.json` (+ script copy under `.cursor/hooks/`).
|
|
17
|
+
* MCP: optional `configureMcp` writes `.cursor/mcp.json` (not via materialize).
|
|
18
|
+
*/
|
|
19
|
+
function createCursorIntegration(options) {
|
|
20
|
+
const id = options?.id ?? "cursor";
|
|
21
|
+
const deployRoots = [...options?.deployRoots ?? DEFAULT_DEPLOY_ROOTS];
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
deployRoots,
|
|
25
|
+
detect: ({ cwd }) => {
|
|
26
|
+
const cursorDir = join(cwd, ".cursor");
|
|
27
|
+
if (existsSync(cursorDir) && statSync(cursorDir).isDirectory()) return true;
|
|
28
|
+
const legacy = join(cwd, ".cursorrules");
|
|
29
|
+
return existsSync(legacy) && statSync(legacy).isFile();
|
|
30
|
+
},
|
|
31
|
+
getDeployRoots: () => [...deployRoots],
|
|
32
|
+
async compile(primitives, context) {
|
|
33
|
+
const content = renderPrimitivesMarkdown({
|
|
34
|
+
primitives: primitivesList(primitives),
|
|
35
|
+
title: "# AGENTS.md"
|
|
36
|
+
});
|
|
37
|
+
return compileMarkdownReport({
|
|
38
|
+
cwd: context.cwd,
|
|
39
|
+
outputFile: context.outputFile ?? "AGENTS.md",
|
|
40
|
+
write: context.write,
|
|
41
|
+
content,
|
|
42
|
+
outsideCwdMessage: "Cursor compile output must be a cwd-relative file path"
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
async materialize(primitives, ctx) {
|
|
46
|
+
const cwd = resolve(ctx?.cwd ?? process.cwd());
|
|
47
|
+
const roots = ctx?.deployRoots?.length ? [...ctx.deployRoots] : deployRoots;
|
|
48
|
+
if (!roots.some((r) => r === ".agents/skills" || r.startsWith(".agents/skills"))) throw new Error("cursor target missing .agents/skills deploy root");
|
|
49
|
+
if (!roots.some((r) => r === ".cursor" || r.startsWith(".cursor"))) throw new Error("cursor target missing .cursor deploy root");
|
|
50
|
+
const deployedFiles = [];
|
|
51
|
+
const diagnostics = [];
|
|
52
|
+
const hookPrimitives = [];
|
|
53
|
+
await primitivesMaterialize(primitives, {
|
|
54
|
+
skill(p, { name }) {
|
|
55
|
+
deployedFiles.push(...materializeSkill({
|
|
56
|
+
primitive: p,
|
|
57
|
+
cwd,
|
|
58
|
+
deployRoots: roots,
|
|
59
|
+
destDir: join(".agents", "skills", name)
|
|
60
|
+
}));
|
|
61
|
+
},
|
|
62
|
+
instruction(p, { name }) {
|
|
63
|
+
deployedFiles.push(writeDeployedFile({
|
|
64
|
+
cwd,
|
|
65
|
+
deployRoots: roots,
|
|
66
|
+
destRel: join(".cursor", "rules", `${name}.mdc`),
|
|
67
|
+
content: readPrimitiveContent(p),
|
|
68
|
+
primitive: {
|
|
69
|
+
name: String(p.name),
|
|
70
|
+
packageName: p.packageName
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
73
|
+
},
|
|
74
|
+
agent(p, { name }) {
|
|
75
|
+
deployedFiles.push(writeDeployedFile({
|
|
76
|
+
cwd,
|
|
77
|
+
deployRoots: roots,
|
|
78
|
+
destRel: join(".cursor", "agents", `${name}.md`),
|
|
79
|
+
content: readPrimitiveContent(p),
|
|
80
|
+
primitive: {
|
|
81
|
+
name: String(p.name),
|
|
82
|
+
packageName: p.packageName
|
|
83
|
+
}
|
|
84
|
+
}));
|
|
85
|
+
},
|
|
86
|
+
command(p, { name }) {
|
|
87
|
+
const { content, droppedKeys } = filterFrontmatterKeys(readPrimitiveContent(p), SHARED_COMMAND_FRONTMATTER_KEYS);
|
|
88
|
+
deployedFiles.push(writeDeployedFile({
|
|
89
|
+
cwd,
|
|
90
|
+
deployRoots: roots,
|
|
91
|
+
destRel: join(".cursor", "commands", `${name}.md`),
|
|
92
|
+
content,
|
|
93
|
+
primitive: {
|
|
94
|
+
name: String(p.name),
|
|
95
|
+
packageName: p.packageName
|
|
96
|
+
}
|
|
97
|
+
}));
|
|
98
|
+
if (droppedKeys.length > 0) diagnostics.push({
|
|
99
|
+
code: "CURSOR_COMMAND_FRONTMATTER_DROPPED",
|
|
100
|
+
message: `Dropped non-preserved command frontmatter keys for "${p.name}": ${droppedKeys.join(", ")}`,
|
|
101
|
+
primitive: String(p.name),
|
|
102
|
+
droppedKeys
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
hook(p) {
|
|
106
|
+
hookPrimitives.push(p);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
if (hookPrimitives.length > 0) {
|
|
110
|
+
const hookDeployed = materializeCursorHooks({
|
|
111
|
+
cwd,
|
|
112
|
+
roots,
|
|
113
|
+
hooks: hookPrimitives
|
|
114
|
+
});
|
|
115
|
+
deployedFiles.push(...hookDeployed.deployedFiles);
|
|
116
|
+
diagnostics.push(...hookDeployed.diagnostics);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
targetId: id,
|
|
120
|
+
deployedFiles,
|
|
121
|
+
...diagnostics.length > 0 ? { diagnostics } : {}
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
async configureMcp(servers, ctx) {
|
|
125
|
+
return writeCursorMcpConfig(servers, {
|
|
126
|
+
cwd: resolve(ctx?.cwd ?? process.cwd()),
|
|
127
|
+
deployRoots: ctx?.deployRoots?.length ? [...ctx.deployRoots] : deployRoots,
|
|
128
|
+
targetId: ctx?.targetId ?? id
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function writeCursorMcpConfig(servers, ctx) {
|
|
134
|
+
const { cwd, deployRoots } = ctx;
|
|
135
|
+
if (!deployRoots.some((r) => r === ".cursor" || r.startsWith(".cursor"))) throw new Error("cursor configureMcp requires a registered .cursor deploy root");
|
|
136
|
+
const destFile = join(cwd, ".cursor", "mcp.json");
|
|
137
|
+
assertUnderDeployRoots(cwd, destFile, deployRoots);
|
|
138
|
+
mkdirSync(join(cwd, ".cursor"), { recursive: true });
|
|
139
|
+
const existing = readExistingMcpServers(destFile);
|
|
140
|
+
const { entries: incoming, diagnostics } = normalizeServerEntries(servers);
|
|
141
|
+
const next = { ...existing };
|
|
142
|
+
const writtenNames = [];
|
|
143
|
+
for (const [name, entry] of Object.entries(incoming)) {
|
|
144
|
+
next[name] = entry;
|
|
145
|
+
writtenNames.push(name);
|
|
146
|
+
}
|
|
147
|
+
writeFileSync(destFile, `${JSON.stringify({ mcpServers: next }, null, 2)}\n`, "utf8");
|
|
148
|
+
return {
|
|
149
|
+
targetId: ctx.targetId,
|
|
150
|
+
configPath: MCP_JSON_REL,
|
|
151
|
+
servers: writtenNames,
|
|
152
|
+
deployedFiles: [{ path: MCP_JSON_REL }],
|
|
153
|
+
diagnostics
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function readExistingMcpServers(path) {
|
|
157
|
+
if (!existsSync(path)) return {};
|
|
158
|
+
try {
|
|
159
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
160
|
+
return raw.mcpServers && typeof raw.mcpServers === "object" ? { ...raw.mcpServers } : {};
|
|
161
|
+
} catch {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function normalizeServerEntries(servers) {
|
|
166
|
+
const out = {};
|
|
167
|
+
const diagnostics = [];
|
|
168
|
+
const list = Array.isArray(servers) ? servers : Object.entries(servers).map(([name, value]) => ({
|
|
169
|
+
...value,
|
|
170
|
+
name: value.name ?? name
|
|
171
|
+
}));
|
|
172
|
+
for (const server of list) {
|
|
173
|
+
const name = String(server.name ?? "").trim();
|
|
174
|
+
if (!name) continue;
|
|
175
|
+
const entry = toCursorServerEntry(server);
|
|
176
|
+
if (!entry) {
|
|
177
|
+
diagnostics.push({
|
|
178
|
+
code: "CURSOR_PORTABLE_MCP_UNSUPPORTED",
|
|
179
|
+
message: `Cursor cannot faithfully represent portable MCP server "${name}"`,
|
|
180
|
+
server: name
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
out[name] = entry;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
entries: out,
|
|
188
|
+
diagnostics
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function toCursorServerEntry(server) {
|
|
192
|
+
const transport = String(server.transport ?? server.type ?? "").toLowerCase();
|
|
193
|
+
const entry = {};
|
|
194
|
+
if (server.format === "agent-plugin") {
|
|
195
|
+
if (transport === "stdio") {
|
|
196
|
+
if (typeof server.command !== "string") return void 0;
|
|
197
|
+
entry.command = server.command;
|
|
198
|
+
if (Array.isArray(server.args)) entry.args = server.args;
|
|
199
|
+
if (typeof server.cwd === "string") entry.cwd = server.cwd;
|
|
200
|
+
entry.type = "stdio";
|
|
201
|
+
} else if (transport === "streamable-http" || transport === "sse") {
|
|
202
|
+
if (typeof server.url !== "string") return void 0;
|
|
203
|
+
entry.url = server.url;
|
|
204
|
+
entry.type = transport === "streamable-http" ? "http" : "sse";
|
|
205
|
+
} else return;
|
|
206
|
+
if (server.env && typeof server.env === "object") entry.env = server.env;
|
|
207
|
+
return entry;
|
|
208
|
+
}
|
|
209
|
+
if (transport === "http" || transport === "sse" || typeof server.url === "string") {
|
|
210
|
+
if (server.url) entry.url = server.url;
|
|
211
|
+
if (transport) entry.type = transport === "sse" ? "sse" : "http";
|
|
212
|
+
} else {
|
|
213
|
+
if (server.command) entry.command = server.command;
|
|
214
|
+
if (Array.isArray(server.args)) entry.args = server.args;
|
|
215
|
+
entry.type = "stdio";
|
|
216
|
+
}
|
|
217
|
+
if (server.env && typeof server.env === "object") entry.env = server.env;
|
|
218
|
+
for (const [key, value] of Object.entries(server)) {
|
|
219
|
+
if ([
|
|
220
|
+
"name",
|
|
221
|
+
"transport",
|
|
222
|
+
"type",
|
|
223
|
+
"command",
|
|
224
|
+
"args",
|
|
225
|
+
"url",
|
|
226
|
+
"env",
|
|
227
|
+
"packageName",
|
|
228
|
+
"registry",
|
|
229
|
+
"format",
|
|
230
|
+
"cwd"
|
|
231
|
+
].includes(key)) continue;
|
|
232
|
+
if (value !== void 0) entry[key] = value;
|
|
233
|
+
}
|
|
234
|
+
return entry;
|
|
235
|
+
}
|
|
236
|
+
function materializeCursorHooks(args) {
|
|
237
|
+
const { cwd, roots, hooks } = args;
|
|
238
|
+
const deployedFiles = [];
|
|
239
|
+
const diagnostics = [];
|
|
240
|
+
const hooksPath = join(cwd, HOOKS_JSON_REL);
|
|
241
|
+
const ownershipPath = join(cwd, HOOKS_OWNERSHIP_REL);
|
|
242
|
+
assertUnderDeployRoots(cwd, hooksPath, roots);
|
|
243
|
+
assertUnderDeployRoots(cwd, ownershipPath, roots);
|
|
244
|
+
mkdirSync(join(cwd, ".cursor"), { recursive: true });
|
|
245
|
+
const doc = readHooksDoc(hooksPath);
|
|
246
|
+
const ownership = readHookOwnershipSidecar(ownershipPath);
|
|
247
|
+
if (doc.hooks && typeof doc.hooks === "object") stripOwnedHookCommands(doc.hooks, ownership);
|
|
248
|
+
const nextOwned = {};
|
|
249
|
+
for (const p of hooks) {
|
|
250
|
+
const name = sanitizeName(String(p.name));
|
|
251
|
+
const srcPath = p.path ? resolve(p.path) : void 0;
|
|
252
|
+
if (!srcPath || !existsSync(srcPath) || !statSync(srcPath).isFile()) {
|
|
253
|
+
diagnostics.push({
|
|
254
|
+
code: "CURSOR_HOOK_SOURCE_MISSING",
|
|
255
|
+
message: `Hook primitive "${p.name}" has no readable source JSON`,
|
|
256
|
+
primitive: String(p.name)
|
|
257
|
+
});
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
let parsed;
|
|
261
|
+
try {
|
|
262
|
+
parsed = JSON.parse(readFileSync(srcPath, "utf8"));
|
|
263
|
+
} catch (cause) {
|
|
264
|
+
diagnostics.push({
|
|
265
|
+
code: "CURSOR_HOOK_JSON_INVALID",
|
|
266
|
+
message: `Hook "${p.name}" JSON is invalid: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
267
|
+
primitive: String(p.name)
|
|
268
|
+
});
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const sourceHooks = parsed.hooks && typeof parsed.hooks === "object" ? parsed.hooks : {};
|
|
272
|
+
const ownedEntries = [];
|
|
273
|
+
const scripts = [];
|
|
274
|
+
if (!doc.hooks || typeof doc.hooks !== "object") doc.hooks = {};
|
|
275
|
+
if (typeof parsed.version === "number" && doc.version === void 0) doc.version = parsed.version;
|
|
276
|
+
for (const [event, entries] of Object.entries(sourceHooks)) {
|
|
277
|
+
if (!Array.isArray(entries)) continue;
|
|
278
|
+
const destList = Array.isArray(doc.hooks[event]) ? [...doc.hooks[event]] : [];
|
|
279
|
+
for (const entry of entries) {
|
|
280
|
+
if (!entry || typeof entry !== "object") continue;
|
|
281
|
+
const command = typeof entry.command === "string" ? entry.command.trim() : "";
|
|
282
|
+
if (!command) {
|
|
283
|
+
destList.push({ ...entry });
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const rewritten = copyHookScript({
|
|
287
|
+
cwd,
|
|
288
|
+
deployRoots: roots,
|
|
289
|
+
hookFile: srcPath,
|
|
290
|
+
command,
|
|
291
|
+
alreadyDeployedNeedle: ".cursor/",
|
|
292
|
+
destRel: `.cursor/hooks/${name}/${basename(command.replace(/^\.\//, ""))}`,
|
|
293
|
+
commandAsDotSlash: true
|
|
294
|
+
});
|
|
295
|
+
const nextEntry = {
|
|
296
|
+
...entry,
|
|
297
|
+
command: rewritten.commandRel
|
|
298
|
+
};
|
|
299
|
+
destList.push(nextEntry);
|
|
300
|
+
ownedEntries.push({
|
|
301
|
+
event,
|
|
302
|
+
command: rewritten.commandRel
|
|
303
|
+
});
|
|
304
|
+
if (rewritten.scriptRel) {
|
|
305
|
+
scripts.push(rewritten.scriptRel);
|
|
306
|
+
deployedFiles.push({
|
|
307
|
+
path: rewritten.scriptRel,
|
|
308
|
+
primitive: {
|
|
309
|
+
name: String(p.name),
|
|
310
|
+
packageName: p.packageName
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
doc.hooks[event] = destList;
|
|
316
|
+
}
|
|
317
|
+
nextOwned[name] = {
|
|
318
|
+
...p.packageName ? { packageName: p.packageName } : {},
|
|
319
|
+
entries: ownedEntries,
|
|
320
|
+
scripts
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (doc.version === void 0) doc.version = 1;
|
|
324
|
+
writeFileSync(hooksPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
|
|
325
|
+
writeHookOwnershipSidecar(ownershipPath, { owned: nextOwned });
|
|
326
|
+
deployedFiles.push({
|
|
327
|
+
path: HOOKS_JSON_REL,
|
|
328
|
+
...hooks[0] ? { primitive: {
|
|
329
|
+
name: String(hooks[0].name),
|
|
330
|
+
packageName: hooks[0].packageName
|
|
331
|
+
} } : {}
|
|
332
|
+
});
|
|
333
|
+
deployedFiles.push({
|
|
334
|
+
path: HOOKS_OWNERSHIP_REL,
|
|
335
|
+
...hooks[0] ? { primitive: {
|
|
336
|
+
name: String(hooks[0].name),
|
|
337
|
+
packageName: hooks[0].packageName
|
|
338
|
+
} } : {}
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
deployedFiles,
|
|
342
|
+
diagnostics
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function readHooksDoc(path) {
|
|
346
|
+
if (!existsSync(path)) return {
|
|
347
|
+
version: 1,
|
|
348
|
+
hooks: {}
|
|
349
|
+
};
|
|
350
|
+
try {
|
|
351
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
352
|
+
if (!raw || typeof raw !== "object") return {
|
|
353
|
+
version: 1,
|
|
354
|
+
hooks: {}
|
|
355
|
+
};
|
|
356
|
+
if (!raw.hooks || typeof raw.hooks !== "object") raw.hooks = {};
|
|
357
|
+
return raw;
|
|
358
|
+
} catch {
|
|
359
|
+
return {
|
|
360
|
+
version: 1,
|
|
361
|
+
hooks: {}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
//#endregion
|
|
366
|
+
export { createCursorIntegration };
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@b-apm/integration-cursor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cursor integration for bapm",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.mjs",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@b-apm/integration-api": "0.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^26.1.2",
|
|
22
|
+
"typescript": "^7.0.2",
|
|
23
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
|
24
|
+
"vite-plus": "latest"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22.12.0"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/Sdju/bapm.git",
|
|
32
|
+
"directory": "packages/integration-cursor"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/Sdju/bapm#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/Sdju/bapm/issues"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"unstable",
|
|
40
|
+
"bapm",
|
|
41
|
+
"b-apm"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "vp pack",
|
|
45
|
+
"dev": "vp pack --watch",
|
|
46
|
+
"test": "vp test",
|
|
47
|
+
"check": "vp check"
|
|
48
|
+
}
|
|
49
|
+
}
|