Skip to content

Commit 2eca22f

Browse files
royangerclaude
andcommitted
fix(*): address review feedback on Clerk agent rules
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ebb165a commit 2eca22f

10 files changed

Lines changed: 215 additions & 45 deletions

File tree

‎packages/expo/app.plugin.js‎

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* 3. If @clerk/expo-native-components is installed, its config plugin is applied for the Clerk native SDKs
88
*/
99
const { AndroidConfig, withAndroidManifest, withEntitlementsPlist, withInfoPlist } = require('@expo/config-plugins');
10+
const { isAgentEnvironment, isAutomatedEnvironment, isProductionEnvironment } = require('@clerk/shared/utils');
1011

1112
const fs = require('fs');
1213
const path = require('path');
@@ -15,43 +16,8 @@ const CLERK_EXPO_NATIVE = '@clerk/expo-native-components';
1516
const CLERK_EXPO_NATIVE_OPTIONS = ['keychainService', 'theme'];
1617
let didWriteAgentRules = false;
1718

18-
const isTruthyEnvValue = value => typeof value === 'string' && !!value && !['0', 'false', 'off', 'no'].includes(value);
19-
20-
const isAgentEnvironment = () =>
21-
[
22-
'CLAUDECODE',
23-
'CURSOR_AGENT',
24-
'CURSOR_CLI',
25-
'CURSOR_EXTENSION_HOST_ROLE',
26-
'CURSOR_TRACE_ID',
27-
'GITHUB_COPILOT_AGENT',
28-
'GITHUB_COPILOT_CHAT',
29-
'AGENT',
30-
'AGENTS',
31-
].some(name => isTruthyEnvValue(process.env[name]));
32-
33-
const isAutomatedEnvironment = () =>
34-
[
35-
'CI',
36-
'CONTINUOUS_INTEGRATION',
37-
'GITHUB_ACTIONS',
38-
'GITLAB_CI',
39-
'CIRCLECI',
40-
'TRAVIS',
41-
'BUILDKITE',
42-
'BITBUCKET_BUILD_NUMBER',
43-
'APPVEYOR',
44-
'CODEBUILD_BUILD_ID',
45-
'TF_BUILD',
46-
'TEAMCITY_VERSION',
47-
'JENKINS_URL',
48-
'HUDSON_URL',
49-
'BAMBOO_BUILDKEY',
50-
'CF_PAGES',
51-
].some(name => isTruthyEnvValue(process.env[name]));
52-
5319
const maybeWriteAgentRules = config => {
54-
if (didWriteAgentRules || isAutomatedEnvironment() || !isAgentEnvironment()) {
20+
if (didWriteAgentRules || isProductionEnvironment() || isAutomatedEnvironment() || !isAgentEnvironment()) {
5521
return;
5622
}
5723

@@ -211,6 +177,7 @@ const withClerkExpo = (config, props = {}) => {
211177
module.exports = withClerkExpo;
212178
module.exports._testing = {
213179
addHostedAuthIntentFilter,
180+
maybeWriteAgentRules,
214181
withClerkExpoNativeComponents,
215182
withClerkFaceIDPermission,
216183
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import fs from 'node:fs';
2+
import { createRequire } from 'node:module';
3+
import os from 'node:os';
4+
import path from 'node:path';
5+
6+
import { agentEnvironmentVariables, automatedEnvironmentVariables } from '@clerk/shared/utils';
7+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
8+
9+
const nodeRequire = createRequire(import.meta.url);
10+
const pluginPath = nodeRequire.resolve('../../app.plugin.js');
11+
12+
const loadMaybeWriteAgentRules = () => {
13+
delete nodeRequire.cache[pluginPath];
14+
return nodeRequire(pluginPath)._testing.maybeWriteAgentRules;
15+
};
16+
17+
describe('maybeWriteAgentRules', () => {
18+
let projectRoot;
19+
20+
beforeEach(() => {
21+
projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'clerk-expo-agent-rules-'));
22+
fs.writeFileSync(path.join(projectRoot, 'package.json'), '{}');
23+
for (const name of [...agentEnvironmentVariables, ...automatedEnvironmentVariables]) {
24+
vi.stubEnv(name, '');
25+
}
26+
vi.stubEnv('NODE_ENV', 'development');
27+
});
28+
29+
afterEach(() => {
30+
vi.unstubAllEnvs();
31+
fs.rmSync(projectRoot, { recursive: true, force: true });
32+
});
33+
34+
const agentsMdPath = () => path.join(projectRoot, 'AGENTS.md');
35+
const config = () => ({ _internal: { projectRoot } });
36+
37+
test('writes the Expo rules once per process under an AI agent', () => {
38+
vi.stubEnv('CLAUDECODE', '1');
39+
const maybeWriteAgentRules = loadMaybeWriteAgentRules();
40+
41+
maybeWriteAgentRules(config());
42+
expect(fs.readFileSync(agentsMdPath(), 'utf8')).toContain('<!-- BEGIN:clerk-agent-rules:expo -->');
43+
44+
fs.rmSync(agentsMdPath());
45+
maybeWriteAgentRules(config());
46+
expect(fs.existsSync(agentsMdPath())).toBe(false);
47+
});
48+
49+
test('skips when the agent variable is set to a false value', () => {
50+
vi.stubEnv('CLAUDECODE', 'False');
51+
52+
loadMaybeWriteAgentRules()(config());
53+
54+
expect(fs.existsSync(agentsMdPath())).toBe(false);
55+
});
56+
57+
test('skips in production', () => {
58+
vi.stubEnv('CLAUDECODE', '1');
59+
vi.stubEnv('NODE_ENV', 'production');
60+
61+
loadMaybeWriteAgentRules()(config());
62+
63+
expect(fs.existsSync(agentsMdPath())).toBe(false);
64+
});
65+
66+
test('skips in CI', () => {
67+
vi.stubEnv('CLAUDECODE', '1');
68+
vi.stubEnv('CI', 'true');
69+
70+
loadMaybeWriteAgentRules()(config());
71+
72+
expect(fs.existsSync(agentsMdPath())).toBe(false);
73+
});
74+
});

‎packages/shared/src/__tests__/agentRules.spec.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,36 @@ describe('writeClerkAgentRules', () => {
120120
});
121121
});
122122

123+
describe('writeClerkAgentRules with an unmatched BEGIN marker', () => {
124+
it('keeps text after an orphan shared BEGIN marker across runs', () => {
125+
const memory = createMemoryFs({
126+
'/repo/AGENTS.md': 'Team rules\n\n<!-- BEGIN:clerk-agent-rules -->\nold\n\n## My notes\nKeep this.\n',
127+
});
128+
129+
for (let run = 0; run < 2; run++) {
130+
writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] });
131+
132+
const content = memory.read('/repo/AGENTS.md');
133+
expect(content).toContain('Keep this.');
134+
expect(content?.match(/<!-- BEGIN:clerk-agent-rules -->/g)).toHaveLength(1);
135+
}
136+
});
137+
138+
it('keeps text after an orphan SDK BEGIN marker across runs', () => {
139+
const memory = createMemoryFs({
140+
'/repo/AGENTS.md': '<!-- BEGIN:clerk-agent-rules:nextjs -->\nold\n\n## My notes\nKeep this.\n',
141+
});
142+
143+
for (let run = 0; run < 2; run++) {
144+
writeClerkAgentRules({ cwd: '/repo', fs: memory.fs, path: pathAdapter, sdks: ['nextjs'] });
145+
146+
const content = memory.read('/repo/AGENTS.md');
147+
expect(content).toContain('Keep this.');
148+
expect(content?.match(/<!-- BEGIN:clerk-agent-rules:nextjs -->/g)).toHaveLength(1);
149+
}
150+
});
151+
});
152+
123153
describe('writeClerkAgentRules with an existing CLAUDE.md', () => {
124154
it('adds an @AGENTS.md import when CLAUDE.md does not load AGENTS.md', () => {
125155
const memory = createMemoryFs({

‎packages/shared/src/__tests__/runtimeEnvironment.spec.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,35 @@ describe('isAgentEnvironment', () => {
9595
expect(isAgentEnvironment()).toBe(false);
9696
});
9797
});
98+
99+
describe('isAgentEnvironment with Cursor', () => {
100+
beforeEach(() => {
101+
agentEnvironmentVariables.forEach(name => {
102+
vi.stubEnv(name, undefined);
103+
vi.stubGlobal(name, undefined);
104+
});
105+
});
106+
107+
afterEach(() => {
108+
vi.unstubAllEnvs();
109+
vi.unstubAllGlobals();
110+
});
111+
112+
it('returns true when CURSOR_EXTENSION_HOST_ROLE is agent-exec', () => {
113+
vi.stubEnv('CURSOR_EXTENSION_HOST_ROLE', 'agent-exec');
114+
115+
expect(isAgentEnvironment()).toBe(true);
116+
});
117+
118+
it('returns false for any other CURSOR_EXTENSION_HOST_ROLE value', () => {
119+
vi.stubEnv('CURSOR_EXTENSION_HOST_ROLE', 'user');
120+
121+
expect(isAgentEnvironment()).toBe(false);
122+
});
123+
124+
it('returns true when CURSOR_TRACE_ID is set', () => {
125+
vi.stubEnv('CURSOR_TRACE_ID', 'abc123');
126+
127+
expect(isAgentEnvironment()).toBe(true);
128+
});
129+
});

‎packages/shared/src/agentRules/index.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ function upsertMarkedBlock(
166166
return content.slice(0, startIndex) + block + content.slice(endIndex + end.length);
167167
}
168168

169+
if (startIndex !== -1) {
170+
return content;
171+
}
172+
169173
if (insertAfter) {
170174
const insertIndex = content.indexOf(insertAfter);
171175
if (insertIndex !== -1) {

‎packages/shared/src/utils/runtimeEnvironment.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,8 @@ export const isAutomatedEnvironment = (): boolean => {
7575
};
7676

7777
export const isAgentEnvironment = (): boolean => {
78-
return agentEnvironmentVariables.some(name => isTruthyEnvValue(getEnvVariable(name)));
78+
return agentEnvironmentVariables.some(name => {
79+
const value = getEnvVariable(name);
80+
return name === 'CURSOR_EXTENSION_HOST_ROLE' ? value === 'agent-exec' : isTruthyEnvValue(value);
81+
});
7982
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import fs from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
5+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6+
7+
import { detectClerkAgentRuleSdks } from '../../util/agent-rules.js';
8+
9+
describe('detectClerkAgentRuleSdks', () => {
10+
let root;
11+
12+
const writePackage = (dir, pkg) => {
13+
fs.mkdirSync(dir, { recursive: true });
14+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg));
15+
};
16+
17+
beforeEach(() => {
18+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'clerk-upgrade-agent-rules-'));
19+
});
20+
21+
afterEach(() => {
22+
fs.rmSync(root, { recursive: true, force: true });
23+
});
24+
25+
it('includes SDKs declared in the workspace root package.json', () => {
26+
writePackage(root, { name: 'app', workspaces: ['packages/*'], dependencies: { '@clerk/nextjs': '^7.0.0' } });
27+
writePackage(path.join(root, 'packages', 'ui'), { name: 'ui', dependencies: { react: '^19.0.0' } });
28+
29+
expect(detectClerkAgentRuleSdks(root)).toEqual(['nextjs']);
30+
});
31+
32+
it('combines SDKs from the workspace root and its packages', () => {
33+
writePackage(root, { name: 'app', workspaces: ['packages/*'], dependencies: { '@clerk/nextjs': '^7.0.0' } });
34+
writePackage(path.join(root, 'packages', 'api'), { name: 'api', dependencies: { '@clerk/express': '^2.0.0' } });
35+
36+
expect(detectClerkAgentRuleSdks(path.join(root, 'packages', 'api'))).toEqual(['express', 'nextjs']);
37+
});
38+
});

‎packages/upgrade/src/__tests__/integration/cli.test.js‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,27 @@ describe('CLI Integration', () => {
208208
});
209209
});
210210

211+
describe('--agents-md flag without a Clerk SDK', () => {
212+
let fixture;
213+
214+
beforeEach(() => {
215+
fixture = createTempFixture('no-clerk');
216+
});
217+
218+
afterEach(() => {
219+
fixture?.cleanup();
220+
});
221+
222+
it('exits with a failure status and writes nothing', async () => {
223+
const fs = await import('node:fs');
224+
const result = await runCli(['--dir', fixture.path, '--agents-md'], { timeout: 15000 });
225+
226+
expect(result.exitCode).toBe(1);
227+
expect(result.stdout + result.stderr).toContain('No Clerk SDK packages found for AGENTS.md');
228+
expect(fs.existsSync(path.join(fixture.path, 'AGENTS.md'))).toBe(false);
229+
});
230+
});
231+
211232
describe('Version Display', () => {
212233
it('shows current version in output', async () => {
213234
const dir = getFixturePath('nextjs-v6');

‎packages/upgrade/src/cli.js‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -246,13 +246,6 @@ async function main() {
246246

247247
console.log('');
248248

249-
if (isInteractive) {
250-
const sdks = detectClerkAgentRuleSdks(options.dir);
251-
if (sdks.length > 0 && (await promptConfirm('Write Clerk AGENTS.md sections?', true))) {
252-
writeAgentsMd(options, sdks);
253-
}
254-
}
255-
256249
// Step 6: Handle upgrade status
257250
if (options.skipUpgrade) {
258251
renderText('Skipping package upgrade (--skip-upgrade flag)', 'yellow');
@@ -268,6 +261,13 @@ async function main() {
268261
await performPackageReplacements(packageManager, config, options);
269262
}
270263

264+
if (isInteractive) {
265+
const sdks = detectClerkAgentRuleSdks(options.dir);
266+
if (sdks.length > 0 && (await promptConfirm('Write Clerk AGENTS.md sections?', true))) {
267+
writeAgentsMd(options, sdks);
268+
}
269+
}
270+
271271
// Step 7: Run codemods
272272
if (config.codemods?.length > 0) {
273273
renderText(`Running ${config.codemods.length} codemod(s)...`, 'blue');
@@ -292,6 +292,7 @@ function writeAgentsMd(options, detectedSdks) {
292292

293293
if (sdks.length === 0) {
294294
renderWarning('No Clerk SDK packages found for AGENTS.md');
295+
process.exitCode = 1;
295296
return;
296297
}
297298

‎packages/upgrade/src/util/agent-rules.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { findWorkspaceRoot, getInstalledClerkPackages, getWorkspacePackageDirs }
77

88
export function detectClerkAgentRuleSdks(dir) {
99
const workspaceRoot = findWorkspaceRoot(dir);
10-
const packageDirs = workspaceRoot ? getWorkspacePackageDirs(workspaceRoot) : [dir];
10+
const packageDirs = workspaceRoot ? [workspaceRoot, ...getWorkspacePackageDirs(workspaceRoot)] : [dir];
1111
const sdks = new Set();
1212

1313
for (const packageDir of packageDirs) {

0 commit comments

Comments
 (0)