agentic-workflow-manager 9.14.0 → 9.15.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/dist/src/commands/context-budget/budget.js +50 -4
- package/dist/src/commands/doctor.js +4 -1
- package/dist/src/commands/hooks/cursor.js +359 -0
- package/dist/src/commands/hooks/install.js +3 -0
- package/dist/src/commands/hooks/resync.js +10 -0
- package/dist/src/commands/hooks/status.js +3 -0
- package/dist/src/commands/hooks/uninstall.js +3 -0
- package/dist/src/commands/update.js +4 -0
- package/dist/src/core/admission/index.js +12 -6
- package/dist/src/core/admission/registry-contracts.js +14 -1
- package/dist/src/core/context/strategies/codex-agents.js +6 -2
- package/dist/src/core/cursor-budgets.js +8 -0
- package/dist/src/core/dashboard/sanitize.js +2 -2
- package/dist/src/core/diagnostics/provider-checks.js +11 -2
- package/dist/src/core/init/migrate-cursor-mdc.js +72 -0
- package/dist/src/core/init/steps.js +4 -0
- package/dist/src/providers/index.js +18 -4
- package/dist/tests/commands/add.test.js +5 -7
- package/dist/tests/commands/context-budget/budget.test.js +68 -0
- package/dist/tests/commands/doctor.test.js +2 -2
- package/dist/tests/commands/hooks/cursor.test.js +257 -0
- package/dist/tests/commands/hooks/resync.test.js +8 -0
- package/dist/tests/commands/init.test.js +23 -22
- package/dist/tests/commands/plan/index.test.js +82 -2
- package/dist/tests/commands/sensors/exec.test.js +18 -9
- package/dist/tests/core/admission/index.test.js +39 -0
- package/dist/tests/core/bundle-install.test.js +8 -14
- package/dist/tests/core/context/strategies/codex-agents.test.js +31 -12
- package/dist/tests/core/diagnostics/provider-tier.test.js +46 -96
- package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +6 -4
- package/dist/tests/core/init/migrate-cursor-mdc.test.js +74 -0
- package/dist/tests/core/install-planner.test.js +16 -12
- package/dist/tests/integration/cursor-subset-gate.test.js +123 -0
- package/dist/tests/providers/index.test.js +34 -21
- package/dist/tests/structural/provider-paths-honor-config-home.test.js +3 -2
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -271,6 +271,17 @@ function hasOwnedGenerationAdmission(input) {
|
|
|
271
271
|
|| (generation.wrapperRef !== undefined && generation.wrapperRef.spawnNonce === generation.spawnNonce
|
|
272
272
|
&& (0, process_1.refIsAlive)(generation.wrapperRef));
|
|
273
273
|
}
|
|
274
|
+
/** R8: only a content registry (its manifest at the sensor project root) may close on a deliberate opt-out. */
|
|
275
|
+
function registryOptOutPolicy(sensors) {
|
|
276
|
+
if (sensors?.mode !== 'opt-out' || !sensors.projectRoot)
|
|
277
|
+
return undefined;
|
|
278
|
+
try {
|
|
279
|
+
return fs_1.default.statSync(path_1.default.join(sensors.projectRoot, registries_1.REGISTRY_MANIFEST_NAME)).isFile() ? 'r8-registry-opt-out' : undefined;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
274
285
|
/** One admission authority for the public command AND actual watch dispatch.
|
|
275
286
|
* No empirical execution occurs before provenance/currentness/CLI-floor gates. */
|
|
276
287
|
async function admitRegistryPlan(input, dependencies = {}) {
|
|
@@ -308,8 +319,9 @@ async function admitRegistryPlan(input, dependencies = {}) {
|
|
|
308
319
|
return scope.provenance === 'proven';
|
|
309
320
|
};
|
|
310
321
|
let generationBound = false;
|
|
322
|
+
let sensorClosurePolicy;
|
|
311
323
|
const compose = async (fields) => {
|
|
312
|
-
const report = await admission({ ...input, provenance: scope.provenance, consumedRegistryComponents: scope.consumedRegistryComponents, ...fields });
|
|
324
|
+
const report = await admission({ ...input, provenance: scope.provenance, consumedRegistryComponents: scope.consumedRegistryComponents, ...(sensorClosurePolicy ? { sensorClosurePolicy } : {}), ...fields });
|
|
313
325
|
const withEvidence = generationBound && report.sensors === 'pass' ? { ...report, sensorEvidence: 'generation-bound' } : report;
|
|
314
326
|
if (scope.provenanceDiagnostic && report.diagnostics.some(diagnostic => diagnostic.code === 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED')) {
|
|
315
327
|
return { ...withEvidence, diagnostics: report.diagnostics.map(diagnostic => diagnostic.code === 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED' ? scope.provenanceDiagnostic : diagnostic) };
|
|
@@ -337,6 +349,7 @@ async function admitRegistryPlan(input, dependencies = {}) {
|
|
|
337
349
|
const sensors = input.verifySensors ? input.sensors ?? (generationBound
|
|
338
350
|
? { sensors: [], overall: 'pass', reason: 'generation-bound-admission' }
|
|
339
351
|
: await (dependencies.runSensors ?? run_1.runSensors)({ cwd: input.cwd, all: true, readOnly: true })) : undefined;
|
|
352
|
+
sensorClosurePolicy = registryOptOutPolicy(sensors);
|
|
340
353
|
if (input.requireCurrent && input.verifySensors) {
|
|
341
354
|
if (!refresh())
|
|
342
355
|
return compose({ currentness, sensors });
|
|
@@ -52,8 +52,11 @@ class CodexAgentsStrategy {
|
|
|
52
52
|
if (!fs_1.default.existsSync(input.ref.absPath)) {
|
|
53
53
|
throw new Error(`materialized context not found at ${input.ref.absPath}`);
|
|
54
54
|
}
|
|
55
|
+
// Cursor: using-awm moves to the sessionStart hook (Plan B S2); project AGENTS.md
|
|
56
|
+
// carries PROJECT_GUIDANCE only (R4/R10). Copilot stays the sole full-context carrier.
|
|
55
57
|
const markdown = fs_1.default.readFileSync(input.ref.absPath, 'utf8');
|
|
56
|
-
|
|
58
|
+
const body = input.agent === 'cursor' ? PROJECT_GUIDANCE : withProjectGuidance(markdown, scope);
|
|
59
|
+
return injectFile(this.targetFile(provider, scope, input.projectRoot), body);
|
|
57
60
|
}
|
|
58
61
|
remove(input, provider) {
|
|
59
62
|
const scope = this.assertSupportedScope(input, provider);
|
|
@@ -78,7 +81,8 @@ class CodexAgentsStrategy {
|
|
|
78
81
|
const expected = fs_1.default.readFileSync(input.ref.absPath, 'utf8');
|
|
79
82
|
if ((0, provider_1.sha256)(expected) !== input.ref.contentHash)
|
|
80
83
|
return 'stale';
|
|
81
|
-
|
|
84
|
+
const expectedBody = input.agent === 'cursor' ? PROJECT_GUIDANCE : withProjectGuidance(expected, scope);
|
|
85
|
+
return body === (0, managed_block_1.normalizeManagedBody)(expectedBody) ? 'injected' : 'stale';
|
|
82
86
|
}
|
|
83
87
|
injectGlobal(context, provider) {
|
|
84
88
|
if (typeof context !== 'object' || context === null) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CURSOR_REANCHOR_PIN = exports.CURSOR_SESSION_START_PIN = exports.CURSOR_REANCHOR_MAX_BYTES = exports.CURSOR_SESSION_START_MAX_BYTES = void 0;
|
|
4
|
+
/** Cursor hook payload caps (Plan A / Plan B R5). Must match baseline registry scripts. */
|
|
5
|
+
exports.CURSOR_SESSION_START_MAX_BYTES = 24 * 1024;
|
|
6
|
+
exports.CURSOR_REANCHOR_MAX_BYTES = 4 * 1024;
|
|
7
|
+
exports.CURSOR_SESSION_START_PIN = 'cursor.sessionStartMaxBytes';
|
|
8
|
+
exports.CURSOR_REANCHOR_PIN = 'cursor.reanchorMaxBytes';
|
|
@@ -15,9 +15,9 @@ const CANONICAL_FINDING_IDS = new Set([
|
|
|
15
15
|
'machine.preferences.missing', 'machine.registries.stale', 'project.profile.missing',
|
|
16
16
|
'project.sensors.unavailable', 'project.preflight.degraded', 'planning.source.unavailable', 'execution.source.unavailable',
|
|
17
17
|
]);
|
|
18
|
-
const PROVIDER_FINDING_ID = /^machine\.provider\.(?:claude-code|codex|opencode|cursor|copilot|antigravity)\.(?:binary\.version|skills\.global|agents\.native|workflows\.global|context\.global|hook\.trust|guidance\.project|constitution\.delivery)$/;
|
|
18
|
+
const PROVIDER_FINDING_ID = /^machine\.provider\.(?:claude-code|codex|opencode|cursor|copilot|antigravity)\.(?:binary\.version|skills\.global|agents\.native|workflows\.global|context\.global|context\.overlap|hook\.trust|guidance\.project|constitution\.delivery)$/;
|
|
19
19
|
const PROJECT_FINDING_ID = /^project\.(?:profile\.present|extensions\.configured|registry-pins\.present|bundles\.coherent|context\.present|constitution\.present|sensors\.present|preflight\.not_collected)$/;
|
|
20
|
-
const PROVIDER_LABEL = /^Provider (?:claude-code|codex|opencode|cursor|copilot|antigravity): (?:binary\.version|skills\.global|agents\.native|workflows\.global|context\.global|hook\.trust|guidance\.project|constitution\.delivery)$/;
|
|
20
|
+
const PROVIDER_LABEL = /^Provider (?:claude-code|codex|opencode|cursor|copilot|antigravity): (?:binary\.version|skills\.global|agents\.native|workflows\.global|context\.global|context\.overlap|hook\.trust|guidance\.project|constitution\.delivery)$/;
|
|
21
21
|
// El slug ya lo validó PROCESS_NAME en core/process/model.ts. Repetir la forma
|
|
22
22
|
// acá es deliberado: este módulo es la frontera de render y no puede confiar en
|
|
23
23
|
// que su input pasó por aquel validador — un adapter nuevo podría no hacerlo.
|
|
@@ -375,11 +375,12 @@ function hookTrustCheck(agent) {
|
|
|
375
375
|
return { id: 'hook.trust', state: 'absent', remediationCode: 'awm-init' };
|
|
376
376
|
}
|
|
377
377
|
if (status.trust) {
|
|
378
|
-
// Codex: 'pending-trust' | 'healthy' | 'stale'
|
|
378
|
+
// Codex: 'pending-trust' | 'healthy' | 'stale'; Cursor: 'pending-first-run' | …
|
|
379
379
|
return {
|
|
380
380
|
id: 'hook.trust',
|
|
381
381
|
state: status.trust,
|
|
382
|
-
remediationCode: status.trust === 'pending-trust' ? 'open-hooks-trust'
|
|
382
|
+
remediationCode: status.trust === 'pending-trust' ? 'open-hooks-trust'
|
|
383
|
+
: status.trust === 'pending-first-run' ? 'open-cursor-session' : undefined,
|
|
383
384
|
};
|
|
384
385
|
}
|
|
385
386
|
return { id: 'hook.trust', state: status.overall === 'HEALTHY' ? 'healthy' : 'broken' };
|
|
@@ -470,6 +471,14 @@ function gatherProviderChecks(agents, scanSkills, projectRoot) {
|
|
|
470
471
|
hookTrustCheck(agent),
|
|
471
472
|
contextGlobalCheck(agent, projectRoot),
|
|
472
473
|
].filter((check) => check !== null);
|
|
474
|
+
// R19: informational only — never overall-red by itself (state: pending).
|
|
475
|
+
if (agent === 'cursor' && agents.includes('claude-code') && agents.includes('cursor')) {
|
|
476
|
+
checks.push({
|
|
477
|
+
id: 'context.overlap',
|
|
478
|
+
state: 'pending',
|
|
479
|
+
detail: 'Claude Code may import Cursor hooks as a third-party path; R15 dual-delivery suppression requires the Cursor session-start hook installed',
|
|
480
|
+
});
|
|
481
|
+
}
|
|
473
482
|
return { id: agent, label: provider.label, tier: providerTier(provider), checks };
|
|
474
483
|
});
|
|
475
484
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// cli/src/core/init/migrate-cursor-mdc.ts
|
|
3
|
+
//
|
|
4
|
+
// Plan B S3 / R8: after Cursor skills move to link under ~/.agents/skills, remove
|
|
5
|
+
// only artifact-state records that were rendered with cursor-mdc under Cursor rules
|
|
6
|
+
// dirs. Never touch unrecorded files; never remove awm.mdc (project guidance carrier).
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.migrateCursorMdcSkills = migrateCursorMdcSkills;
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const paths_1 = require("../paths");
|
|
15
|
+
const artifact_state_1 = require("../artifact-state");
|
|
16
|
+
function isUnderCursorRules(targetPath, projectRoot) {
|
|
17
|
+
const resolved = path_1.default.resolve(targetPath);
|
|
18
|
+
const globalRules = path_1.default.resolve((0, paths_1.homeDir)(), '.cursor', 'rules');
|
|
19
|
+
if (resolved === globalRules || resolved.startsWith(globalRules + path_1.default.sep))
|
|
20
|
+
return true;
|
|
21
|
+
if (!projectRoot)
|
|
22
|
+
return false;
|
|
23
|
+
const localRules = path_1.default.resolve(projectRoot, '.cursor', 'rules');
|
|
24
|
+
return resolved === localRules || resolved.startsWith(localRules + path_1.default.sep);
|
|
25
|
+
}
|
|
26
|
+
function isAwmMdcCarrier(record) {
|
|
27
|
+
return record.name === 'awm' || path_1.default.basename(record.targetPath) === 'awm.mdc';
|
|
28
|
+
}
|
|
29
|
+
function safeUnlinkManaged(targetPath) {
|
|
30
|
+
try {
|
|
31
|
+
const stat = fs_1.default.lstatSync(targetPath);
|
|
32
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
33
|
+
return false;
|
|
34
|
+
fs_1.default.unlinkSync(targetPath);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code === 'ENOENT')
|
|
39
|
+
return false;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Drop historical cursor-mdc skill renders under Cursor rules dirs once link
|
|
45
|
+
* skills are the delivery path. Idempotent; safe on machines without cursor.
|
|
46
|
+
*/
|
|
47
|
+
function migrateCursorMdcSkills(opts) {
|
|
48
|
+
const stateFile = opts.stateFile ?? (0, artifact_state_1.artifactStateFile)();
|
|
49
|
+
const existing = (0, artifact_state_1.readArtifactState)(stateFile);
|
|
50
|
+
const removed = [];
|
|
51
|
+
const kept = [];
|
|
52
|
+
for (const record of existing) {
|
|
53
|
+
if (record.renderer !== 'cursor-mdc') {
|
|
54
|
+
kept.push(record);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (isAwmMdcCarrier(record)) {
|
|
58
|
+
kept.push(record);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!isUnderCursorRules(record.targetPath, opts.projectRoot)) {
|
|
62
|
+
kept.push(record);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
safeUnlinkManaged(record.targetPath);
|
|
66
|
+
removed.push(record.targetPath);
|
|
67
|
+
}
|
|
68
|
+
if (removed.length > 0) {
|
|
69
|
+
(0, artifact_state_1.writeArtifactState)(kept, stateFile);
|
|
70
|
+
}
|
|
71
|
+
return { removed, kept: kept.length };
|
|
72
|
+
}
|
|
@@ -35,6 +35,7 @@ const skill_integrity_1 = require("../skill-integrity");
|
|
|
35
35
|
const registries_2 = require("../registries");
|
|
36
36
|
const project_constitution_inject_1 = require("../context/project-constitution-inject");
|
|
37
37
|
const codex_agents_1 = require("../context/strategies/codex-agents");
|
|
38
|
+
const migrate_cursor_mdc_1 = require("./migrate-cursor-mdc");
|
|
38
39
|
// ---------------------------------------------------------------------------
|
|
39
40
|
// defaultActions — bridges the real functions to the InitActions interface
|
|
40
41
|
// ---------------------------------------------------------------------------
|
|
@@ -231,6 +232,9 @@ function stepDevCore(d) {
|
|
|
231
232
|
...(localOnly ? { scopeOverride: 'local' } : {}),
|
|
232
233
|
});
|
|
233
234
|
}
|
|
235
|
+
if (d.enabledAgents.includes('cursor') || d.agent === 'cursor') {
|
|
236
|
+
(0, migrate_cursor_mdc_1.migrateCursorMdcSkills)({ projectRoot: d.ctx.project?.root ?? d.cwd });
|
|
237
|
+
}
|
|
234
238
|
return ok('machine.devCore', 'machine', 'applied', localOnly ? 'installed at project scope (no global skill dir)' : undefined);
|
|
235
239
|
}
|
|
236
240
|
/** Step 3.5 – Repair broken global skill symlinks (orphans outside the baseline). */
|
|
@@ -163,13 +163,27 @@ function providers() {
|
|
|
163
163
|
cursor: {
|
|
164
164
|
label: 'Cursor',
|
|
165
165
|
configHome: configHomeFor('cursor'),
|
|
166
|
+
// Skills join the Codex/OpenCode shared physical dirs (~/.agents/skills +
|
|
167
|
+
// .agents/skills, renderer link). cursor-mdc remains registered for S3
|
|
168
|
+
// migration reads of historical ~/.cursor/rules installs.
|
|
166
169
|
skill: {
|
|
167
|
-
global: path_1.default.join(
|
|
168
|
-
local: '.
|
|
169
|
-
renderer: '
|
|
170
|
+
global: path_1.default.join(home, '.agents/skills'),
|
|
171
|
+
local: '.agents/skills',
|
|
172
|
+
renderer: 'link',
|
|
170
173
|
},
|
|
171
174
|
workflow: null,
|
|
172
|
-
agent:
|
|
175
|
+
agent: {
|
|
176
|
+
global: path_1.default.join(root('cursor'), 'agents'),
|
|
177
|
+
local: '.cursor/agents',
|
|
178
|
+
renderer: 'link',
|
|
179
|
+
},
|
|
180
|
+
hooks: {
|
|
181
|
+
type: 'cursor-hooks-json',
|
|
182
|
+
settingsPath: path_1.default.join(root('cursor'), 'hooks.json'),
|
|
183
|
+
scriptsDir: path_1.default.join(awm, 'hooks/cursor'),
|
|
184
|
+
matcher: '',
|
|
185
|
+
eventName: '',
|
|
186
|
+
},
|
|
173
187
|
injection: {
|
|
174
188
|
type: 'managed-agents-md',
|
|
175
189
|
// Cursor has no confirmed user-level/global AGENTS.md-equivalent file — its
|
|
@@ -65,18 +65,16 @@ function makeProjectRoot() {
|
|
|
65
65
|
fs_1.default.writeFileSync(path_1.default.join(projectRoot, 'package.json'), JSON.stringify({ name: 'fixture' }));
|
|
66
66
|
return projectRoot;
|
|
67
67
|
}
|
|
68
|
-
it('awm add demo materializes a
|
|
68
|
+
it('awm add demo materializes a Cursor skill link under .agents/skills end-to-end', () => {
|
|
69
69
|
const content = makeContentFixture();
|
|
70
70
|
const projectRoot = makeProjectRoot();
|
|
71
71
|
const bundles = (0, bundles_1.discoverBundles)(content);
|
|
72
72
|
const outcome = (0, add_1.runAddBundleCore)({ name: 'demo', agent: 'cursor', cwd: projectRoot }, prefsFor('cursor'), bundles);
|
|
73
73
|
expect(outcome.code).toBe(0);
|
|
74
|
-
const
|
|
75
|
-
expect(fs_1.default.existsSync(
|
|
76
|
-
|
|
77
|
-
expect(
|
|
78
|
-
expect(rendered).toContain('alwaysApply: false');
|
|
79
|
-
expect(rendered).toContain('Follow the demo skill body.');
|
|
74
|
+
const linked = path_1.default.join(projectRoot, '.agents/skills/demo-skill');
|
|
75
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, '.cursor/rules/demo-skill.mdc'))).toBe(false);
|
|
76
|
+
expect(fs_1.default.lstatSync(linked).isSymbolicLink() || fs_1.default.existsSync(path_1.default.join(linked, 'SKILL.md'))).toBe(true);
|
|
77
|
+
expect(fs_1.default.readFileSync(path_1.default.join(linked, 'SKILL.md'), 'utf8')).toContain('Follow the demo skill body.');
|
|
80
78
|
fs_1.default.rmSync(content, { recursive: true, force: true });
|
|
81
79
|
fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
|
|
82
80
|
});
|
|
@@ -134,3 +134,71 @@ describe('pinning refuses to happen when there is nothing to measure', () => {
|
|
|
134
134
|
expect(fs_1.default.existsSync(path_1.default.join(cwd, budget_1.CONFIG_FILE))).toBe(true);
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
|
+
describe('Cursor hook budget pins (Plan B R5)', () => {
|
|
138
|
+
let tmpHome;
|
|
139
|
+
let originalHome;
|
|
140
|
+
let originalAwmHome;
|
|
141
|
+
const dirs = [];
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-cursor-budget-home-'));
|
|
144
|
+
originalHome = process.env.HOME;
|
|
145
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
146
|
+
process.env.HOME = tmpHome;
|
|
147
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
148
|
+
jest.resetModules();
|
|
149
|
+
});
|
|
150
|
+
afterEach(() => {
|
|
151
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
152
|
+
dirs.forEach((d) => fs_1.default.rmSync(d, { recursive: true, force: true }));
|
|
153
|
+
if (originalHome === undefined)
|
|
154
|
+
delete process.env.HOME;
|
|
155
|
+
else
|
|
156
|
+
process.env.HOME = originalHome;
|
|
157
|
+
if (originalAwmHome === undefined)
|
|
158
|
+
delete process.env.AWM_HOME;
|
|
159
|
+
else
|
|
160
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
161
|
+
});
|
|
162
|
+
it('skips cursor pins when hooks are not installed', () => {
|
|
163
|
+
const { checkBudget } = require('../../../src/commands/context-budget/budget');
|
|
164
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-budget-'));
|
|
165
|
+
dirs.push(dir);
|
|
166
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'AGENTS.md'), 'a'.repeat(100));
|
|
167
|
+
const report = checkBudget(dir);
|
|
168
|
+
expect(report.cursorHooks).toBeUndefined();
|
|
169
|
+
expect(report.status).toBe('pinned');
|
|
170
|
+
});
|
|
171
|
+
it('marks over when installed script caps exceed pin or disagree with module constants', () => {
|
|
172
|
+
const { checkBudget, writeConfig } = require('../../../src/commands/context-budget/budget');
|
|
173
|
+
const { CURSOR_SESSION_START_MAX_BYTES, CURSOR_REANCHOR_MAX_BYTES, } = require('../../../src/core/cursor-budgets');
|
|
174
|
+
expect(CURSOR_SESSION_START_MAX_BYTES).toBe(24 * 1024);
|
|
175
|
+
expect(CURSOR_REANCHOR_MAX_BYTES).toBe(4 * 1024);
|
|
176
|
+
const scripts = path_1.default.join(tmpHome, '.awm/hooks/cursor');
|
|
177
|
+
fs_1.default.mkdirSync(scripts, { recursive: true });
|
|
178
|
+
fs_1.default.writeFileSync(path_1.default.join(scripts, 'session-start'), 'const MAX_SESSION_BYTES = 24 * 1024;\nconst MAX_REANCHOR_BYTES = 4 * 1024;\n');
|
|
179
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-budget-'));
|
|
180
|
+
dirs.push(dir);
|
|
181
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'AGENTS.md'), 'a'.repeat(100));
|
|
182
|
+
writeConfig(dir, {
|
|
183
|
+
files: ['AGENTS.md'],
|
|
184
|
+
maxBytes: 1000,
|
|
185
|
+
'cursor.sessionStartMaxBytes': 1024, // below observed 24KiB
|
|
186
|
+
'cursor.reanchorMaxBytes': 4096,
|
|
187
|
+
});
|
|
188
|
+
const report = checkBudget(dir);
|
|
189
|
+
expect(report.cursorHooks?.status).toBe('over');
|
|
190
|
+
expect(report.status).toBe('over');
|
|
191
|
+
});
|
|
192
|
+
it('is within when script literals match defaults', () => {
|
|
193
|
+
const { checkBudget } = require('../../../src/commands/context-budget/budget');
|
|
194
|
+
const scripts = path_1.default.join(tmpHome, '.awm/hooks/cursor');
|
|
195
|
+
fs_1.default.mkdirSync(scripts, { recursive: true });
|
|
196
|
+
fs_1.default.writeFileSync(path_1.default.join(scripts, 'session-start'), 'const MAX_SESSION_BYTES = 24 * 1024;\nconst MAX_REANCHOR_BYTES = 4 * 1024;\n');
|
|
197
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-budget-'));
|
|
198
|
+
dirs.push(dir);
|
|
199
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'AGENTS.md'), 'a'.repeat(100));
|
|
200
|
+
const report = checkBudget(dir);
|
|
201
|
+
expect(report.cursorHooks?.status).toBe('within');
|
|
202
|
+
expect(report.cursorHooks?.observedSessionStartMaxBytes).toBe(24 * 1024);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -229,13 +229,13 @@ describe('renderProviderReport — capability tier (Task 4.4)', () => {
|
|
|
229
229
|
overall: 'healthy',
|
|
230
230
|
providers: [
|
|
231
231
|
{ id: 'claude-code', label: 'Claude Code', tier: 'hooks-native', checks: [] },
|
|
232
|
-
{ id: 'cursor', label: 'Cursor', tier: '
|
|
232
|
+
{ id: 'cursor', label: 'Cursor', tier: 'hooks-native', checks: [] },
|
|
233
233
|
{ id: 'antigravity', label: 'Antigravity', tier: 'context-only', checks: [] },
|
|
234
234
|
],
|
|
235
235
|
};
|
|
236
236
|
const out = (0, doctor_1.renderProviderReport)(report);
|
|
237
237
|
expect(out).toContain('Provider: Claude Code (hooks-native)');
|
|
238
|
-
expect(out).toContain('Provider: Cursor (
|
|
238
|
+
expect(out).toContain('Provider: Cursor (hooks-native)');
|
|
239
239
|
expect(out).toContain('Provider: Antigravity (context-only)');
|
|
240
240
|
});
|
|
241
241
|
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
describe('installHook / computeHookStatus / uninstallHook — Cursor adapter', () => {
|
|
11
|
+
let tmpHome;
|
|
12
|
+
let tmpRegistry;
|
|
13
|
+
let hooksJson;
|
|
14
|
+
let cursorScriptsDir;
|
|
15
|
+
let originalHome;
|
|
16
|
+
let originalAwmHome;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-cursor-hooks-'));
|
|
19
|
+
tmpRegistry = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-cursor-registry-'));
|
|
20
|
+
hooksJson = path_1.default.join(tmpHome, '.cursor/hooks.json');
|
|
21
|
+
cursorScriptsDir = path_1.default.join(tmpHome, '.awm/hooks/cursor');
|
|
22
|
+
originalHome = process.env.HOME;
|
|
23
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
24
|
+
process.env.HOME = tmpHome;
|
|
25
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
26
|
+
jest.resetModules();
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
30
|
+
fs_1.default.rmSync(tmpRegistry, { recursive: true, force: true });
|
|
31
|
+
if (originalHome === undefined)
|
|
32
|
+
delete process.env.HOME;
|
|
33
|
+
else
|
|
34
|
+
process.env.HOME = originalHome;
|
|
35
|
+
if (originalAwmHome === undefined)
|
|
36
|
+
delete process.env.AWM_HOME;
|
|
37
|
+
else
|
|
38
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
39
|
+
});
|
|
40
|
+
function writeRegistry(content = '#!/usr/bin/env node\nconsole.log("{}")') {
|
|
41
|
+
const regHooks = path_1.default.join(tmpRegistry, 'hooks');
|
|
42
|
+
const regSkill = path_1.default.join(tmpRegistry, 'skills/using-awm');
|
|
43
|
+
fs_1.default.mkdirSync(regHooks, { recursive: true });
|
|
44
|
+
fs_1.default.mkdirSync(regSkill, { recursive: true });
|
|
45
|
+
for (const name of ['cursor-session-start', 'cursor-pre-compact', 'cursor-post-tool-use']) {
|
|
46
|
+
fs_1.default.writeFileSync(path_1.default.join(regHooks, name), content, { mode: 0o755 });
|
|
47
|
+
}
|
|
48
|
+
fs_1.default.writeFileSync(path_1.default.join(regSkill, 'SKILL.md'), '---\nname: using-awm\n---\nMUST invoke skills.\n');
|
|
49
|
+
}
|
|
50
|
+
function installOpts(over = {}) {
|
|
51
|
+
return {
|
|
52
|
+
agent: 'cursor',
|
|
53
|
+
registryRoot: tmpRegistry,
|
|
54
|
+
installMethod: 'copy',
|
|
55
|
+
baselineRegistryVersion: '4.9.0',
|
|
56
|
+
...over,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function awmEntries() {
|
|
60
|
+
return {
|
|
61
|
+
sessionStart: [{ command: path_1.default.join(cursorScriptsDir, 'session-start') }],
|
|
62
|
+
preCompact: [{ command: path_1.default.join(cursorScriptsDir, 'pre-compact') }],
|
|
63
|
+
postToolUse: [{ command: path_1.default.join(cursorScriptsDir, 'post-tool-use') }],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
it('merges three AWM events and preserves non-AWM entries', () => {
|
|
67
|
+
fs_1.default.mkdirSync(path_1.default.dirname(hooksJson), { recursive: true });
|
|
68
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify({
|
|
69
|
+
version: 1,
|
|
70
|
+
hooks: {
|
|
71
|
+
sessionStart: [{ command: '/user/custom-start' }],
|
|
72
|
+
stop: [{ command: 'echo stop' }],
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
writeRegistry();
|
|
76
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
77
|
+
installHook(installOpts());
|
|
78
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf8'));
|
|
79
|
+
expect(cfg.version).toBe(1);
|
|
80
|
+
expect(cfg.hooks.stop).toEqual([{ command: 'echo stop' }]);
|
|
81
|
+
expect(cfg.hooks.sessionStart).toEqual([
|
|
82
|
+
{ command: '/user/custom-start' },
|
|
83
|
+
{ command: path_1.default.join(cursorScriptsDir, 'session-start') },
|
|
84
|
+
]);
|
|
85
|
+
expect(cfg.hooks.preCompact).toEqual([{ command: path_1.default.join(cursorScriptsDir, 'pre-compact') }]);
|
|
86
|
+
expect(cfg.hooks.postToolUse).toEqual([{ command: path_1.default.join(cursorScriptsDir, 'post-tool-use') }]);
|
|
87
|
+
expect(fs_1.default.existsSync(path_1.default.join(cursorScriptsDir, 'session-start'))).toBe(true);
|
|
88
|
+
expect(fs_1.default.readFileSync(path_1.default.join(cursorScriptsDir, 'using-awm.md'), 'utf8')).toContain('MUST invoke skills.');
|
|
89
|
+
});
|
|
90
|
+
it('installs cleanly with no pre-existing hooks.json and backs up on replace', () => {
|
|
91
|
+
writeRegistry();
|
|
92
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
93
|
+
const first = installHook(installOpts({ installMethod: 'symlink' }));
|
|
94
|
+
expect(first.status).toBe('installed');
|
|
95
|
+
expect(first.backupPath).toBeNull();
|
|
96
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf8'));
|
|
97
|
+
expect(cfg).toEqual({ version: 1, hooks: awmEntries() });
|
|
98
|
+
expect(fs_1.default.lstatSync(path_1.default.join(cursorScriptsDir, 'session-start')).isSymbolicLink()).toBe(true);
|
|
99
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify({ version: 1, hooks: { sessionStart: [] } }, null, 2));
|
|
100
|
+
const second = installHook(installOpts());
|
|
101
|
+
expect(second.status).toBe('installed');
|
|
102
|
+
expect(second.backupPath).toMatch(/hooks\.json\..*\.bak$/);
|
|
103
|
+
});
|
|
104
|
+
it('fails when registry lacks cursor-session-start', () => {
|
|
105
|
+
const regHooks = path_1.default.join(tmpRegistry, 'hooks');
|
|
106
|
+
fs_1.default.mkdirSync(regHooks, { recursive: true });
|
|
107
|
+
fs_1.default.writeFileSync(path_1.default.join(regHooks, 'cursor-pre-compact'), 'x', { mode: 0o755 });
|
|
108
|
+
fs_1.default.writeFileSync(path_1.default.join(regHooks, 'cursor-post-tool-use'), 'x', { mode: 0o755 });
|
|
109
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpRegistry, 'skills/using-awm'), { recursive: true });
|
|
110
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpRegistry, 'skills/using-awm/SKILL.md'), '---\nname: using-awm\n---\n');
|
|
111
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
112
|
+
expect(() => installHook(installOpts())).toThrow(/Cursor hook source missing/);
|
|
113
|
+
expect(fs_1.default.existsSync(hooksJson)).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
it('refuses invalid JSON, unknown version, and duplicate AWM entries without writing', () => {
|
|
116
|
+
writeRegistry();
|
|
117
|
+
fs_1.default.mkdirSync(path_1.default.dirname(hooksJson), { recursive: true });
|
|
118
|
+
fs_1.default.writeFileSync(hooksJson, '{not json');
|
|
119
|
+
const beforeBad = fs_1.default.readFileSync(hooksJson, 'utf8');
|
|
120
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
121
|
+
expect(() => installHook(installOpts())).toThrow(/not valid JSON/);
|
|
122
|
+
expect(fs_1.default.readFileSync(hooksJson, 'utf8')).toBe(beforeBad);
|
|
123
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify({ version: 2, hooks: {} }));
|
|
124
|
+
const beforeVer = fs_1.default.readFileSync(hooksJson, 'utf8');
|
|
125
|
+
expect(() => installHook(installOpts())).toThrow(/unsupported hooks\.json version/);
|
|
126
|
+
expect(fs_1.default.readFileSync(hooksJson, 'utf8')).toBe(beforeVer);
|
|
127
|
+
const dup = {
|
|
128
|
+
version: 1,
|
|
129
|
+
hooks: {
|
|
130
|
+
sessionStart: [
|
|
131
|
+
{ command: path_1.default.join(cursorScriptsDir, 'session-start') },
|
|
132
|
+
{ command: path_1.default.join(cursorScriptsDir, 'session-start') },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify(dup, null, 2));
|
|
137
|
+
const beforeDup = fs_1.default.readFileSync(hooksJson, 'utf8');
|
|
138
|
+
expect(() => installHook(installOpts())).toThrow(/multiple AWM sessionStart/);
|
|
139
|
+
expect(fs_1.default.readFileSync(hooksJson, 'utf8')).toBe(beforeDup);
|
|
140
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/backups'))).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
it('aborts when an event key is present but not an array', () => {
|
|
143
|
+
writeRegistry();
|
|
144
|
+
fs_1.default.mkdirSync(path_1.default.dirname(hooksJson), { recursive: true });
|
|
145
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify({ version: 1, hooks: { sessionStart: { command: 'x' } } }));
|
|
146
|
+
const before = fs_1.default.readFileSync(hooksJson, 'utf8');
|
|
147
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
148
|
+
expect(() => installHook(installOpts())).toThrow(/must be an array/);
|
|
149
|
+
expect(fs_1.default.readFileSync(hooksJson, 'utf8')).toBe(before);
|
|
150
|
+
});
|
|
151
|
+
it('prunes dead AWM entries from another AWM_HOME and preserves live parallels', () => {
|
|
152
|
+
writeRegistry();
|
|
153
|
+
const dead = '/tmp/awm-gone-never/hooks/cursor/session-start';
|
|
154
|
+
const liveOther = path_1.default.join(tmpHome, '.awm-other/hooks/cursor/session-start');
|
|
155
|
+
fs_1.default.mkdirSync(path_1.default.dirname(liveOther), { recursive: true });
|
|
156
|
+
fs_1.default.writeFileSync(liveOther, '#!/usr/bin/env node\n', { mode: 0o755 });
|
|
157
|
+
fs_1.default.mkdirSync(path_1.default.dirname(hooksJson), { recursive: true });
|
|
158
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify({
|
|
159
|
+
version: 1,
|
|
160
|
+
hooks: {
|
|
161
|
+
sessionStart: [
|
|
162
|
+
{ command: dead },
|
|
163
|
+
{ command: liveOther },
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
}, null, 2));
|
|
167
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
168
|
+
installHook(installOpts());
|
|
169
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf8'));
|
|
170
|
+
expect(cfg.hooks.sessionStart.map((e) => e.command)).toEqual([
|
|
171
|
+
liveOther,
|
|
172
|
+
path_1.default.join(cursorScriptsDir, 'session-start'),
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
it('is idempotent when nothing changed', () => {
|
|
176
|
+
writeRegistry();
|
|
177
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
178
|
+
const first = installHook(installOpts());
|
|
179
|
+
expect(first.status).toBe('installed');
|
|
180
|
+
const contentBefore = fs_1.default.readFileSync(hooksJson, 'utf8');
|
|
181
|
+
const mtimeBefore = fs_1.default.statSync(hooksJson).mtimeMs;
|
|
182
|
+
const second = installHook(installOpts());
|
|
183
|
+
expect(second.status).toBe('already-up-to-date');
|
|
184
|
+
expect(second.backupPath).toBeNull();
|
|
185
|
+
expect(fs_1.default.readFileSync(hooksJson, 'utf8')).toBe(contentBefore);
|
|
186
|
+
expect(fs_1.default.statSync(hooksJson).mtimeMs).toBe(mtimeBefore);
|
|
187
|
+
});
|
|
188
|
+
it.each([
|
|
189
|
+
[false, 'pending-first-run'],
|
|
190
|
+
[true, 'healthy'],
|
|
191
|
+
])('derives trust from heartbeat (present=%s → %s)', (heartbeat, expected) => {
|
|
192
|
+
fs_1.default.mkdirSync(cursorScriptsDir, { recursive: true });
|
|
193
|
+
const scriptPath = path_1.default.join(cursorScriptsDir, 'session-start');
|
|
194
|
+
fs_1.default.writeFileSync(scriptPath, '#!/usr/bin/env node\n', { mode: 0o755 });
|
|
195
|
+
fs_1.default.writeFileSync(path_1.default.join(cursorScriptsDir, 'using-awm.md'), '# using-awm\n');
|
|
196
|
+
fs_1.default.mkdirSync(path_1.default.dirname(hooksJson), { recursive: true });
|
|
197
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify({ version: 1, hooks: awmEntries() }, null, 2));
|
|
198
|
+
if (heartbeat) {
|
|
199
|
+
fs_1.default.writeFileSync(path_1.default.join(cursorScriptsDir, 'heartbeat.json'), JSON.stringify({ hash: crypto_1.default.createHash('sha256').update(fs_1.default.readFileSync(scriptPath)).digest('hex') }));
|
|
200
|
+
}
|
|
201
|
+
const { computeHookStatus } = require('../../../src/commands/hooks/status');
|
|
202
|
+
expect(computeHookStatus('cursor').trust).toBe(expected);
|
|
203
|
+
});
|
|
204
|
+
it('reports stale trust when heartbeat hash mismatches', () => {
|
|
205
|
+
fs_1.default.mkdirSync(cursorScriptsDir, { recursive: true });
|
|
206
|
+
const scriptPath = path_1.default.join(cursorScriptsDir, 'session-start');
|
|
207
|
+
fs_1.default.writeFileSync(scriptPath, '#!/usr/bin/env node\n', { mode: 0o755 });
|
|
208
|
+
fs_1.default.writeFileSync(path_1.default.join(cursorScriptsDir, 'using-awm.md'), '# using-awm\n');
|
|
209
|
+
fs_1.default.writeFileSync(path_1.default.join(cursorScriptsDir, 'heartbeat.json'), JSON.stringify({ hash: 'deadbeef' }));
|
|
210
|
+
fs_1.default.mkdirSync(path_1.default.dirname(hooksJson), { recursive: true });
|
|
211
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify({ version: 1, hooks: awmEntries() }, null, 2));
|
|
212
|
+
const { computeHookStatus } = require('../../../src/commands/hooks/status');
|
|
213
|
+
const status = computeHookStatus('cursor');
|
|
214
|
+
expect(status.trust).toBe('stale');
|
|
215
|
+
expect(status.overall).toBe('DEGRADED');
|
|
216
|
+
});
|
|
217
|
+
it('refuses install when baseline registry is below 4.9.0 and accepts 4.9.0', () => {
|
|
218
|
+
writeRegistry();
|
|
219
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
220
|
+
expect(() => installHook(installOpts({ baselineRegistryVersion: '4.8.1' })))
|
|
221
|
+
.toThrow("Cursor hooks require baseline registry >= 4.9.0; installed is 4.8.1. Run 'awm update -y'.");
|
|
222
|
+
expect(fs_1.default.existsSync(hooksJson)).toBe(false);
|
|
223
|
+
expect(installHook(installOpts({ baselineRegistryVersion: '4.9.0' })).status).toBe('installed');
|
|
224
|
+
});
|
|
225
|
+
it('shouldDisableDeferredReanchor follows R16.2 thresholds', () => {
|
|
226
|
+
const { shouldDisableDeferredReanchor } = require('../../../src/commands/hooks/cursor');
|
|
227
|
+
expect(shouldDisableDeferredReanchor({ deliveryVerified: false, p95Ms: 10 })).toBe(true);
|
|
228
|
+
expect(shouldDisableDeferredReanchor({ deliveryVerified: true, p95Ms: 51 })).toBe(true);
|
|
229
|
+
expect(shouldDisableDeferredReanchor({ deliveryVerified: true, p95Ms: 50 })).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
it('omits deferred events when disableDeferredReanchor is set', () => {
|
|
232
|
+
writeRegistry();
|
|
233
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
234
|
+
installHook(installOpts({ disableDeferredReanchor: true }));
|
|
235
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf8'));
|
|
236
|
+
expect(cfg.hooks.sessionStart).toEqual([{ command: path_1.default.join(cursorScriptsDir, 'session-start') }]);
|
|
237
|
+
expect(cfg.hooks.preCompact).toBeUndefined();
|
|
238
|
+
expect(cfg.hooks.postToolUse).toBeUndefined();
|
|
239
|
+
expect(fs_1.default.existsSync(path_1.default.join(cursorScriptsDir, 'session-start'))).toBe(true);
|
|
240
|
+
expect(fs_1.default.existsSync(path_1.default.join(cursorScriptsDir, 'pre-compact'))).toBe(false);
|
|
241
|
+
});
|
|
242
|
+
it('uninstall removes only AWM Cursor entries', () => {
|
|
243
|
+
writeRegistry();
|
|
244
|
+
const { installHook } = require('../../../src/commands/hooks/install');
|
|
245
|
+
const { uninstallHook } = require('../../../src/commands/hooks/uninstall');
|
|
246
|
+
installHook(installOpts());
|
|
247
|
+
const current = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf8'));
|
|
248
|
+
current.hooks.sessionStart.unshift({ command: '/user/keep' });
|
|
249
|
+
fs_1.default.writeFileSync(hooksJson, JSON.stringify(current, null, 2));
|
|
250
|
+
const result = uninstallHook({ agent: 'cursor' });
|
|
251
|
+
expect(result.status).toBe('uninstalled');
|
|
252
|
+
const cfg = JSON.parse(fs_1.default.readFileSync(hooksJson, 'utf8'));
|
|
253
|
+
expect(cfg.hooks.sessionStart).toEqual([{ command: '/user/keep' }]);
|
|
254
|
+
expect(cfg.hooks.preCompact).toBeUndefined();
|
|
255
|
+
expect(cfg.hooks.postToolUse).toBeUndefined();
|
|
256
|
+
});
|
|
257
|
+
});
|