@cleocode/adapters 2026.9.19 → 2026.9.20
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/dist/index.js +270 -289
- package/dist/index.js.map +2 -2
- package/dist/providers/claude-code/hooks.d.ts +18 -2
- package/dist/providers/claude-code/hooks.d.ts.map +1 -1
- package/dist/providers/claude-code/install.d.ts.map +1 -1
- package/dist/providers/claude-code/paths.d.ts +96 -0
- package/dist/providers/claude-code/paths.d.ts.map +1 -1
- package/dist/providers/cursor/install.d.ts +11 -22
- package/dist/providers/cursor/install.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/providers/claude-code/__tests__/adapter.test.ts +1 -0
- package/src/providers/claude-code/__tests__/hooks-install.test.ts +63 -2
- package/src/providers/claude-code/hooks.ts +60 -89
- package/src/providers/claude-code/install.ts +70 -96
- package/src/providers/claude-code/paths.ts +155 -0
- package/src/providers/cursor/__tests__/adapter.test.ts +7 -0
- package/src/providers/cursor/__tests__/hooks-install.test.ts +36 -0
- package/src/providers/cursor/install.ts +86 -133
|
@@ -10,23 +10,23 @@
|
|
|
10
10
|
* @task T5240
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
copyFileSync,
|
|
15
|
-
existsSync,
|
|
16
|
-
mkdirSync,
|
|
17
|
-
readdirSync,
|
|
18
|
-
readFileSync,
|
|
19
|
-
writeFileSync,
|
|
20
|
-
} from 'node:fs';
|
|
13
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
21
14
|
import { homedir } from 'node:os';
|
|
22
15
|
import { dirname, join } from 'node:path';
|
|
23
16
|
import { fileURLToPath } from 'node:url';
|
|
24
|
-
import { ensureProviderInstructionFile } from '@cleocode/caamp';
|
|
17
|
+
import { ensureProviderInstructionFile, updateJsonConfigFile } from '@cleocode/caamp';
|
|
25
18
|
import type { AdapterInstallProvider, InstallOptions, InstallResult } from '@cleocode/contracts';
|
|
26
19
|
import {
|
|
27
20
|
type InstallHookTemplatesResult,
|
|
28
21
|
installProviderHookTemplates,
|
|
29
22
|
} from '../shared/hook-template-installer.js';
|
|
23
|
+
import {
|
|
24
|
+
appendHookEntry,
|
|
25
|
+
claudeSettingsPath,
|
|
26
|
+
hasCleoHook,
|
|
27
|
+
hookMap,
|
|
28
|
+
isPlainObject,
|
|
29
|
+
} from './paths.js';
|
|
30
30
|
|
|
31
31
|
/** Resolve Claude Code's config directory, honoring the documented CI override. */
|
|
32
32
|
function getClaudeConfigDir(): string {
|
|
@@ -80,21 +80,37 @@ export class ClaudeCodeInstallProvider implements AdapterInstallProvider {
|
|
|
80
80
|
details.commands = commandsInstalled;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// T12385: settings.json writes are locked + atomic, and a malformed file
|
|
84
|
+
// aborts the write and is reported here — it is never reset to `{}`.
|
|
85
|
+
const settingsErrors: string[] = [];
|
|
86
|
+
|
|
83
87
|
// Step 3: Register plugin in ~/.claude/settings.json
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
try {
|
|
89
|
+
const pluginResult = await this.registerPlugin();
|
|
90
|
+
if (pluginResult) {
|
|
91
|
+
details.plugin = pluginResult;
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
settingsErrors.push(err instanceof Error ? err.message : String(err));
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
// Step 4 (T1013): Install PreCompact hook templates + wire the handler
|
|
90
98
|
// command into ~/.claude/settings.json's `PreCompact` event.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
try {
|
|
100
|
+
const hookResult = await this.installHookTemplates();
|
|
101
|
+
if (hookResult) {
|
|
102
|
+
details.hookTemplates = hookResult;
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
settingsErrors.push(err instanceof Error ? err.message : String(err));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (settingsErrors.length > 0) {
|
|
109
|
+
details.settingsErrors = settingsErrors;
|
|
94
110
|
}
|
|
95
111
|
|
|
96
112
|
return {
|
|
97
|
-
success:
|
|
113
|
+
success: settingsErrors.length === 0,
|
|
98
114
|
installedAt,
|
|
99
115
|
instructionFileUpdated,
|
|
100
116
|
details,
|
|
@@ -115,7 +131,7 @@ export class ClaudeCodeInstallProvider implements AdapterInstallProvider {
|
|
|
115
131
|
*/
|
|
116
132
|
async isInstalled(): Promise<boolean> {
|
|
117
133
|
// Check ~/.claude/settings.json for plugin registration
|
|
118
|
-
const settingsPath =
|
|
134
|
+
const settingsPath = claudeSettingsPath();
|
|
119
135
|
if (existsSync(settingsPath)) {
|
|
120
136
|
try {
|
|
121
137
|
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
@@ -179,38 +195,28 @@ export class ClaudeCodeInstallProvider implements AdapterInstallProvider {
|
|
|
179
195
|
*
|
|
180
196
|
* @returns Description of what was registered, or null if no change needed
|
|
181
197
|
*/
|
|
182
|
-
private registerPlugin(): string | null {
|
|
183
|
-
const claudeDir = getClaudeConfigDir();
|
|
184
|
-
const settingsPath = join(claudeDir, 'settings.json');
|
|
185
|
-
|
|
186
|
-
let settings: Record<string, unknown> = {};
|
|
187
|
-
if (existsSync(settingsPath)) {
|
|
188
|
-
try {
|
|
189
|
-
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
190
|
-
} catch {
|
|
191
|
-
// Start fresh
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const enabledPlugins = (settings.enabledPlugins as Record<string, boolean>) ?? {};
|
|
198
|
+
private async registerPlugin(): Promise<string | null> {
|
|
196
199
|
const pluginKey = 'cleo@cleocode';
|
|
200
|
+
const wrote = await updateJsonConfigFile(claudeSettingsPath(), (settings) => {
|
|
201
|
+
const enabledPlugins = settings.enabledPlugins ?? {};
|
|
202
|
+
if (!isPlainObject(enabledPlugins)) {
|
|
203
|
+
throw new Error('settings.json "enabledPlugins" is not an object; not modifying it');
|
|
204
|
+
}
|
|
205
|
+
if (enabledPlugins[pluginKey] === true) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
197
208
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// Disable old claude-mem if present
|
|
203
|
-
if (enabledPlugins['claude-mem@thedotmack'] === true) {
|
|
204
|
-
enabledPlugins['claude-mem@thedotmack'] = false;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
enabledPlugins[pluginKey] = true;
|
|
208
|
-
settings.enabledPlugins = enabledPlugins;
|
|
209
|
+
// Disable old claude-mem if present
|
|
210
|
+
if (enabledPlugins['claude-mem@thedotmack'] === true) {
|
|
211
|
+
enabledPlugins['claude-mem@thedotmack'] = false;
|
|
212
|
+
}
|
|
209
213
|
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
enabledPlugins[pluginKey] = true;
|
|
215
|
+
settings.enabledPlugins = enabledPlugins;
|
|
216
|
+
return true;
|
|
217
|
+
});
|
|
212
218
|
|
|
213
|
-
return `Enabled ${pluginKey} in ~/.claude/settings.json
|
|
219
|
+
return wrote ? `Enabled ${pluginKey} in ~/.claude/settings.json` : null;
|
|
214
220
|
}
|
|
215
221
|
|
|
216
222
|
/**
|
|
@@ -233,10 +239,10 @@ export class ClaudeCodeInstallProvider implements AdapterInstallProvider {
|
|
|
233
239
|
*
|
|
234
240
|
* @task T1013
|
|
235
241
|
*/
|
|
236
|
-
private installHookTemplates(): {
|
|
242
|
+
private async installHookTemplates(): Promise<{
|
|
237
243
|
templates: InstallHookTemplatesResult;
|
|
238
244
|
settingsEntryAdded: boolean;
|
|
239
|
-
} | null {
|
|
245
|
+
} | null> {
|
|
240
246
|
const claudeDir = getClaudeConfigDir();
|
|
241
247
|
const hooksDir = join(claudeDir, 'hooks');
|
|
242
248
|
|
|
@@ -254,7 +260,7 @@ export class ClaudeCodeInstallProvider implements AdapterInstallProvider {
|
|
|
254
260
|
}
|
|
255
261
|
|
|
256
262
|
// 2. Wire the PreCompact event in ~/.claude/settings.json.
|
|
257
|
-
const settingsEntryAdded = this.registerPreCompactHook(
|
|
263
|
+
const settingsEntryAdded = await this.registerPreCompactHook(
|
|
258
264
|
join(hooksDir, 'precompact-safestop.sh'),
|
|
259
265
|
);
|
|
260
266
|
|
|
@@ -279,55 +285,23 @@ export class ClaudeCodeInstallProvider implements AdapterInstallProvider {
|
|
|
279
285
|
*
|
|
280
286
|
* @task T1013
|
|
281
287
|
*/
|
|
282
|
-
private registerPreCompactHook(shimPath: string): boolean {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (existsSync(settingsPath)) {
|
|
288
|
-
try {
|
|
289
|
-
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
290
|
-
} catch {
|
|
291
|
-
// Start fresh on corrupt settings — safer than aborting install.
|
|
288
|
+
private async registerPreCompactHook(shimPath: string): Promise<boolean> {
|
|
289
|
+
return updateJsonConfigFile(claudeSettingsPath(), (settings) => {
|
|
290
|
+
const hooks = hookMap(settings);
|
|
291
|
+
if (hasCleoHook(hooks.PreCompact, 'precompact-safestop.sh')) {
|
|
292
|
+
return false;
|
|
292
293
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
(h) =>
|
|
305
|
-
typeof h.command === 'string' &&
|
|
306
|
-
(h.command as string).includes('# cleo-hook') &&
|
|
307
|
-
(h.command as string).includes('precompact-safestop.sh'),
|
|
308
|
-
),
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
if (alreadyWired) {
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
preCompactEntries.push({
|
|
316
|
-
matcher: '',
|
|
317
|
-
hooks: [
|
|
318
|
-
{
|
|
319
|
-
type: 'command',
|
|
320
|
-
command: `"${shimPath}" # cleo-hook`,
|
|
321
|
-
timeout: 30,
|
|
322
|
-
},
|
|
323
|
-
],
|
|
294
|
+
appendHookEntry(hooks, 'PreCompact', {
|
|
295
|
+
matcher: '',
|
|
296
|
+
hooks: [
|
|
297
|
+
{
|
|
298
|
+
type: 'command',
|
|
299
|
+
command: `"${shimPath}" # cleo-hook`,
|
|
300
|
+
timeout: 30,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
});
|
|
304
|
+
return true;
|
|
324
305
|
});
|
|
325
|
-
|
|
326
|
-
hooks.PreCompact = preCompactEntries;
|
|
327
|
-
settings.hooks = hooks;
|
|
328
|
-
|
|
329
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
330
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
331
|
-
return true;
|
|
332
306
|
}
|
|
333
307
|
}
|
|
@@ -44,3 +44,158 @@ export class ClaudeCodePathProvider implements AdapterPathProvider {
|
|
|
44
44
|
return process.env['CLAUDE_MEM_DB'] ?? join(homedir(), '.claude-mem', 'claude-mem.db');
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve Claude Code's `settings.json`, honouring `CLAUDE_SETTINGS` and
|
|
50
|
+
* `CLAUDE_HOME`.
|
|
51
|
+
*
|
|
52
|
+
* @remarks
|
|
53
|
+
* The single resolver for every adapter write to the settings file (T12385).
|
|
54
|
+
* Hook registration previously hard-coded `~/.claude/settings.json` while the
|
|
55
|
+
* installer honoured `CLAUDE_HOME`, so the two wrote different files whenever
|
|
56
|
+
* the override was set.
|
|
57
|
+
*
|
|
58
|
+
* @returns Absolute path of the settings file
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const settingsPath = claudeSettingsPath();
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function claudeSettingsPath(): string {
|
|
66
|
+
return (
|
|
67
|
+
new ClaudeCodePathProvider().getSettingsPath() ?? join(homedir(), '.claude', 'settings.json')
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Marker CLEO puts in every hook command it owns. */
|
|
72
|
+
const CLEO_HOOK_MARKER = '# cleo-hook';
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Whether a parsed settings value is a plain JSON object.
|
|
76
|
+
*
|
|
77
|
+
* @param value - Any parsed JSON value
|
|
78
|
+
* @returns `true` for a non-null, non-array object
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* isPlainObject(settings.enabledPlugins);
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
86
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether a settings.json hook entry carries a CLEO-owned command.
|
|
91
|
+
*
|
|
92
|
+
* @param entry - One element of a `hooks.<Event>` array
|
|
93
|
+
* @param commandIncludes - Extra substring the CLEO command must contain
|
|
94
|
+
* @returns `true` when one of the entry's commands has the `# cleo-hook` marker
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* isCleoHookEntry({ hooks: [{ type: "command", command: "x # cleo-hook" }] }); // true
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export function isCleoHookEntry(entry: unknown, commandIncludes?: string): boolean {
|
|
102
|
+
if (!isPlainObject(entry) || !Array.isArray(entry.hooks)) return false;
|
|
103
|
+
return entry.hooks.some(
|
|
104
|
+
(hook) =>
|
|
105
|
+
isPlainObject(hook) &&
|
|
106
|
+
typeof hook.command === 'string' &&
|
|
107
|
+
hook.command.includes(CLEO_HOOK_MARKER) &&
|
|
108
|
+
(commandIncludes === undefined || hook.command.includes(commandIncludes)),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether a `hooks.<Event>` value holds a CLEO-owned entry.
|
|
114
|
+
*
|
|
115
|
+
* @param entries - The event's value (non-arrays hold none)
|
|
116
|
+
* @param commandIncludes - Extra substring the CLEO command must contain
|
|
117
|
+
* @returns `true` when a CLEO entry is present
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* hasCleoHook(hookMap(settings).Stop);
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export function hasCleoHook(entries: unknown, commandIncludes?: string): boolean {
|
|
125
|
+
return Array.isArray(entries) && entries.some((e) => isCleoHookEntry(e, commandIncludes));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Return the settings object's `hooks` map, creating an empty one if absent.
|
|
130
|
+
*
|
|
131
|
+
* @param settings - Parsed settings.json
|
|
132
|
+
* @returns The live `hooks` object (mutations apply to `settings`)
|
|
133
|
+
* @throws Error when `hooks` exists but is not an object — the caller's write
|
|
134
|
+
* is aborted and the file left untouched
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* const hooks = hookMap(settings);
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export function hookMap(settings: Record<string, unknown>): Record<string, unknown> {
|
|
142
|
+
const existing = settings.hooks;
|
|
143
|
+
if (existing === undefined) {
|
|
144
|
+
const created: Record<string, unknown> = {};
|
|
145
|
+
settings.hooks = created;
|
|
146
|
+
return created;
|
|
147
|
+
}
|
|
148
|
+
if (!isPlainObject(existing)) {
|
|
149
|
+
throw new Error('settings.json "hooks" is not an object; not modifying it');
|
|
150
|
+
}
|
|
151
|
+
return existing;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Append a hook entry to `hooks[event]`, preserving existing entries.
|
|
156
|
+
*
|
|
157
|
+
* @param hooks - The settings `hooks` map
|
|
158
|
+
* @param event - Native event name (e.g. `Stop`, `PreCompact`)
|
|
159
|
+
* @param entry - Entry to append
|
|
160
|
+
* @throws Error when `hooks[event]` exists but is not an array
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```typescript
|
|
164
|
+
* appendHookEntry(hookMap(settings), "Stop", { matcher: "", hooks: [] });
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export function appendHookEntry(
|
|
168
|
+
hooks: Record<string, unknown>,
|
|
169
|
+
event: string,
|
|
170
|
+
entry: Record<string, unknown>,
|
|
171
|
+
): void {
|
|
172
|
+
const existing = hooks[event];
|
|
173
|
+
if (existing !== undefined && !Array.isArray(existing)) {
|
|
174
|
+
throw new Error(`settings.json "hooks.${event}" is not an array; not modifying it`);
|
|
175
|
+
}
|
|
176
|
+
hooks[event] = [...(existing ?? []), entry];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Remove every CLEO-owned entry from every event array in `hooks`.
|
|
181
|
+
*
|
|
182
|
+
* @param hooks - The settings `hooks` map
|
|
183
|
+
* @returns Whether anything was removed
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* const changed = removeCleoHookEntries(hookMap(settings));
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
export function removeCleoHookEntries(hooks: Record<string, unknown>): boolean {
|
|
191
|
+
let changed = false;
|
|
192
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
193
|
+
if (!Array.isArray(entries)) continue;
|
|
194
|
+
const kept = entries.filter((e) => !isCleoHookEntry(e));
|
|
195
|
+
if (kept.length !== entries.length) {
|
|
196
|
+
hooks[event] = kept;
|
|
197
|
+
changed = true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return changed;
|
|
201
|
+
}
|
|
@@ -45,6 +45,13 @@ vi.mock('@cleocode/caamp', () => ({
|
|
|
45
45
|
action: 'added',
|
|
46
46
|
providerId: 'cursor',
|
|
47
47
|
}),
|
|
48
|
+
// T12385: Cursor's rule files and hooks.json go through the CAAMP writers.
|
|
49
|
+
ensureProviderRuleFile: vi.fn().mockResolvedValue({
|
|
50
|
+
filePath: '/tmp/test-project/.cursor/rules/cleo.mdc',
|
|
51
|
+
action: 'created',
|
|
52
|
+
providerId: 'cursor',
|
|
53
|
+
}),
|
|
54
|
+
updateJsonConfigFile: vi.fn().mockResolvedValue(true),
|
|
48
55
|
}));
|
|
49
56
|
|
|
50
57
|
describe('CursorAdapter', () => {
|
|
@@ -95,4 +95,40 @@ describe('CursorInstallProvider — PreCompact hook templates', () => {
|
|
|
95
95
|
};
|
|
96
96
|
expect((config.hooks?.preCompact ?? []).length).toBe(1);
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
it('T12385: reports a malformed hooks.json and leaves it byte-identical', async () => {
|
|
100
|
+
const hooksJsonPath = join(projectDir, '.cursor', 'hooks.json');
|
|
101
|
+
mkdirSync(join(projectDir, '.cursor'), { recursive: true });
|
|
102
|
+
const malformed = '{ "hooks": { "preCompact": [ ';
|
|
103
|
+
writeFileSync(hooksJsonPath, malformed, 'utf-8');
|
|
104
|
+
|
|
105
|
+
const result = await new CursorInstallProvider().install({ projectDir });
|
|
106
|
+
|
|
107
|
+
expect(readFileSync(hooksJsonPath, 'utf-8')).toBe(malformed);
|
|
108
|
+
expect(result.success).toBe(false);
|
|
109
|
+
expect(String(result.details?.hooksError)).toMatch(/not a valid JSON object/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('T12385: writes rule files through the CAAMP writer (managed block, registry references)', async () => {
|
|
113
|
+
const legacyPath = join(projectDir, '.cursorrules');
|
|
114
|
+
// A pre-CAAMP .cursorrules with the old bare reference lines appended.
|
|
115
|
+
writeFileSync(
|
|
116
|
+
legacyPath,
|
|
117
|
+
'# Project Rules\nUse TypeScript.\n@~/.cleo/templates/CLEO-INJECTION.md\n@.cleo/memory-bridge.md\n',
|
|
118
|
+
'utf-8',
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
await new CursorInstallProvider().install({ projectDir });
|
|
122
|
+
|
|
123
|
+
const legacy = readFileSync(legacyPath, 'utf-8');
|
|
124
|
+
expect(legacy).toContain('# Project Rules\nUse TypeScript.');
|
|
125
|
+
expect(legacy).toContain('<!-- CAAMP:START -->');
|
|
126
|
+
expect(legacy.match(/CLEO-INJECTION\.md/g)).toHaveLength(1);
|
|
127
|
+
|
|
128
|
+
const mdc = readFileSync(join(projectDir, '.cursor', 'rules', 'cleo.mdc'), 'utf-8');
|
|
129
|
+
expect(mdc.startsWith('---\n')).toBe(true);
|
|
130
|
+
expect(mdc).toContain('alwaysApply: true');
|
|
131
|
+
expect(mdc).toContain('<!-- CAAMP:START -->');
|
|
132
|
+
expect(mdc).toContain('@.cleo/memory-bridge.md');
|
|
133
|
+
});
|
|
98
134
|
});
|