convex 1.34.0 → 1.34.1
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/CHANGELOG.md +12 -0
- package/dist/browser.bundle.js +6 -9
- package/dist/browser.bundle.js.map +2 -2
- package/dist/cjs/browser/sync/authentication_manager.js +4 -1
- package/dist/cjs/browser/sync/authentication_manager.js.map +2 -2
- package/dist/cjs/browser/sync/web_socket_manager.js +1 -7
- package/dist/cjs/browser/sync/web_socket_manager.js.map +2 -2
- package/dist/cjs/cli/aiFiles.js +15 -14
- package/dist/cjs/cli/aiFiles.js.map +2 -2
- package/dist/cjs/cli/configure.js +15 -10
- package/dist/cjs/cli/configure.js.map +2 -2
- package/dist/cjs/cli/lib/aiFiles/agentsmd.js +69 -0
- package/dist/cjs/cli/lib/aiFiles/agentsmd.js.map +7 -0
- package/dist/cjs/cli/lib/aiFiles/claudemd.js +69 -0
- package/dist/cjs/cli/lib/aiFiles/claudemd.js.map +7 -0
- package/dist/cjs/cli/lib/{ai → aiFiles}/config.js +73 -46
- package/dist/cjs/cli/lib/aiFiles/config.js.map +7 -0
- package/dist/cjs/cli/lib/aiFiles/cursorrules.js +48 -0
- package/dist/cjs/cli/lib/aiFiles/cursorrules.js.map +7 -0
- package/dist/cjs/cli/lib/aiFiles/guidelinesmd.js +51 -0
- package/dist/cjs/cli/lib/aiFiles/guidelinesmd.js.map +7 -0
- package/dist/cjs/cli/lib/aiFiles/index.js +231 -0
- package/dist/cjs/cli/lib/aiFiles/index.js.map +7 -0
- package/dist/cjs/cli/lib/aiFiles/paths.js.map +7 -0
- package/dist/cjs/cli/lib/aiFiles/skills.js +180 -0
- package/dist/cjs/cli/lib/aiFiles/skills.js.map +7 -0
- package/dist/cjs/cli/lib/aiFiles/status.js +195 -0
- package/dist/cjs/cli/lib/aiFiles/status.js.map +7 -0
- package/dist/cjs/cli/lib/aiFiles/utils.js +111 -0
- package/dist/cjs/cli/lib/aiFiles/utils.js.map +7 -0
- package/dist/cjs/cli/lib/command.js +6 -1
- package/dist/cjs/cli/lib/command.js.map +2 -2
- package/dist/cjs/cli/lib/config.js +3 -4
- package/dist/cjs/cli/lib/config.js.map +2 -2
- package/dist/cjs/cli/lib/localDeployment/anonymous.js +2 -2
- package/dist/cjs/cli/lib/localDeployment/anonymous.js.map +2 -2
- package/dist/cjs/cli/lib/updates.js +8 -8
- package/dist/cjs/cli/lib/updates.js.map +2 -2
- package/dist/cjs/cli/lib/versionApi.js +7 -4
- package/dist/cjs/cli/lib/versionApi.js.map +2 -2
- package/dist/cjs/cli/lib/workos/workos.js +4 -6
- package/dist/cjs/cli/lib/workos/workos.js.map +2 -2
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs-types/browser/sync/authentication_manager.d.ts.map +1 -1
- package/dist/cjs-types/browser/sync/web_socket_manager.d.ts.map +1 -1
- package/dist/cjs-types/cli/aiFiles.d.ts.map +1 -1
- package/dist/cjs-types/cli/configure.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/aiFiles/agentsmd.d.ts +19 -0
- package/dist/cjs-types/cli/lib/aiFiles/agentsmd.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/agentsmd.test.d.ts +2 -0
- package/dist/cjs-types/cli/lib/aiFiles/agentsmd.test.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/claudemd.d.ts +19 -0
- package/dist/cjs-types/cli/lib/aiFiles/claudemd.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/claudemd.test.d.ts +2 -0
- package/dist/cjs-types/cli/lib/aiFiles/claudemd.test.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/config.d.ts +46 -0
- package/dist/cjs-types/cli/lib/aiFiles/config.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/config.test.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/cursorrules.d.ts +10 -0
- package/dist/cjs-types/cli/lib/aiFiles/cursorrules.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/guidelinesmd.d.ts +12 -0
- package/dist/cjs-types/cli/lib/aiFiles/guidelinesmd.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/guidelinesmd.test.d.ts +2 -0
- package/dist/cjs-types/cli/lib/aiFiles/guidelinesmd.test.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/index.d.ts +40 -0
- package/dist/cjs-types/cli/lib/aiFiles/index.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/index.test.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/integration.test.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/{ai → aiFiles}/paths.d.ts +4 -0
- package/dist/cjs-types/cli/lib/aiFiles/paths.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/prompt.test.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/skills.d.ts +18 -0
- package/dist/cjs-types/cli/lib/aiFiles/skills.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/status.d.ts +3 -0
- package/dist/cjs-types/cli/lib/aiFiles/status.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/aiFiles/utils.d.ts +46 -0
- package/dist/cjs-types/cli/lib/aiFiles/utils.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/config.d.ts +1 -0
- package/dist/cjs-types/cli/lib/config.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/versionApi.d.ts +7 -1
- package/dist/cjs-types/cli/lib/versionApi.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/workos/workos.d.ts.map +1 -1
- package/dist/cjs-types/index.d.ts +1 -1
- package/dist/cli.bundle.cjs +1605 -1548
- package/dist/cli.bundle.cjs.map +4 -4
- package/dist/esm/browser/sync/authentication_manager.js +4 -1
- package/dist/esm/browser/sync/authentication_manager.js.map +2 -2
- package/dist/esm/browser/sync/web_socket_manager.js +1 -7
- package/dist/esm/browser/sync/web_socket_manager.js.map +2 -2
- package/dist/esm/cli/aiFiles.js +17 -17
- package/dist/esm/cli/aiFiles.js.map +2 -2
- package/dist/esm/cli/configure.js +15 -10
- package/dist/esm/cli/configure.js.map +2 -2
- package/dist/esm/cli/lib/aiFiles/agentsmd.js +52 -0
- package/dist/esm/cli/lib/aiFiles/agentsmd.js.map +7 -0
- package/dist/esm/cli/lib/aiFiles/claudemd.js +52 -0
- package/dist/esm/cli/lib/aiFiles/claudemd.js.map +7 -0
- package/dist/esm/cli/lib/{ai → aiFiles}/config.js +71 -45
- package/dist/esm/cli/lib/aiFiles/config.js.map +7 -0
- package/dist/esm/cli/lib/aiFiles/cursorrules.js +16 -0
- package/dist/esm/cli/lib/aiFiles/cursorrules.js.map +7 -0
- package/dist/esm/cli/lib/aiFiles/guidelinesmd.js +28 -0
- package/dist/esm/cli/lib/aiFiles/guidelinesmd.js.map +7 -0
- package/dist/esm/cli/lib/aiFiles/index.js +210 -0
- package/dist/esm/cli/lib/aiFiles/index.js.map +7 -0
- package/dist/esm/cli/lib/aiFiles/paths.js.map +7 -0
- package/dist/esm/cli/lib/aiFiles/skills.js +147 -0
- package/dist/esm/cli/lib/aiFiles/skills.js.map +7 -0
- package/dist/esm/cli/lib/aiFiles/status.js +175 -0
- package/dist/esm/cli/lib/aiFiles/status.js.map +7 -0
- package/dist/esm/cli/lib/aiFiles/utils.js +82 -0
- package/dist/esm/cli/lib/aiFiles/utils.js.map +7 -0
- package/dist/esm/cli/lib/command.js +6 -1
- package/dist/esm/cli/lib/command.js.map +2 -2
- package/dist/esm/cli/lib/config.js +3 -4
- package/dist/esm/cli/lib/config.js.map +2 -2
- package/dist/esm/cli/lib/localDeployment/anonymous.js +2 -2
- package/dist/esm/cli/lib/localDeployment/anonymous.js.map +2 -2
- package/dist/esm/cli/lib/updates.js +8 -8
- package/dist/esm/cli/lib/updates.js.map +2 -2
- package/dist/esm/cli/lib/versionApi.js +7 -4
- package/dist/esm/cli/lib/versionApi.js.map +2 -2
- package/dist/esm/cli/lib/workos/workos.js +4 -6
- package/dist/esm/cli/lib/workos/workos.js.map +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm-types/browser/sync/authentication_manager.d.ts.map +1 -1
- package/dist/esm-types/browser/sync/web_socket_manager.d.ts.map +1 -1
- package/dist/esm-types/cli/aiFiles.d.ts.map +1 -1
- package/dist/esm-types/cli/configure.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/aiFiles/agentsmd.d.ts +19 -0
- package/dist/esm-types/cli/lib/aiFiles/agentsmd.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/agentsmd.test.d.ts +2 -0
- package/dist/esm-types/cli/lib/aiFiles/agentsmd.test.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/claudemd.d.ts +19 -0
- package/dist/esm-types/cli/lib/aiFiles/claudemd.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/claudemd.test.d.ts +2 -0
- package/dist/esm-types/cli/lib/aiFiles/claudemd.test.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/config.d.ts +46 -0
- package/dist/esm-types/cli/lib/aiFiles/config.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/config.test.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/cursorrules.d.ts +10 -0
- package/dist/esm-types/cli/lib/aiFiles/cursorrules.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/guidelinesmd.d.ts +12 -0
- package/dist/esm-types/cli/lib/aiFiles/guidelinesmd.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/guidelinesmd.test.d.ts +2 -0
- package/dist/esm-types/cli/lib/aiFiles/guidelinesmd.test.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/index.d.ts +40 -0
- package/dist/esm-types/cli/lib/aiFiles/index.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/index.test.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/integration.test.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/{ai → aiFiles}/paths.d.ts +4 -0
- package/dist/esm-types/cli/lib/aiFiles/paths.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/prompt.test.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/skills.d.ts +18 -0
- package/dist/esm-types/cli/lib/aiFiles/skills.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/status.d.ts +3 -0
- package/dist/esm-types/cli/lib/aiFiles/status.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/aiFiles/utils.d.ts +46 -0
- package/dist/esm-types/cli/lib/aiFiles/utils.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/config.d.ts +1 -0
- package/dist/esm-types/cli/lib/config.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/versionApi.d.ts +7 -1
- package/dist/esm-types/cli/lib/versionApi.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/workos/workos.d.ts.map +1 -1
- package/dist/esm-types/index.d.ts +1 -1
- package/dist/react.bundle.js +6 -9
- package/dist/react.bundle.js.map +2 -2
- package/package.json +1 -1
- package/schemas/convex.schema.json +7 -1
- package/src/browser/sync/authentication_manager.ts +9 -4
- package/src/browser/sync/client_node.test.ts +125 -0
- package/src/browser/sync/web_socket_manager.ts +1 -7
- package/src/cli/aiFiles.ts +20 -27
- package/src/cli/configure.ts +17 -11
- package/src/cli/deploymentSelection.test.ts +56 -2
- package/src/cli/lib/{ai → aiFiles}/MANUAL_TESTING.md +6 -2
- package/src/cli/lib/aiFiles/agentsmd.test.ts +133 -0
- package/src/cli/lib/aiFiles/agentsmd.ts +77 -0
- package/src/cli/lib/aiFiles/claudemd.test.ts +92 -0
- package/src/cli/lib/aiFiles/claudemd.ts +77 -0
- package/src/cli/lib/{ai → aiFiles}/config.test.ts +181 -59
- package/src/cli/lib/{ai → aiFiles}/config.ts +92 -63
- package/src/cli/lib/aiFiles/cursorrules.ts +25 -0
- package/src/cli/lib/aiFiles/guidelinesmd.test.ts +40 -0
- package/src/cli/lib/aiFiles/guidelinesmd.ts +41 -0
- package/src/cli/lib/{ai → aiFiles}/index.test.ts +200 -339
- package/src/cli/lib/aiFiles/index.ts +303 -0
- package/src/cli/lib/{ai → aiFiles}/integration.test.ts +117 -147
- package/src/cli/lib/{ai → aiFiles}/paths.ts +5 -0
- package/src/cli/lib/{ai → aiFiles}/prompt.test.ts +78 -30
- package/src/cli/lib/aiFiles/skills.ts +213 -0
- package/src/cli/lib/aiFiles/status.ts +240 -0
- package/src/cli/lib/aiFiles/utils.ts +163 -0
- package/src/cli/lib/command.ts +6 -1
- package/src/cli/lib/config.test.ts +1 -1
- package/src/cli/lib/config.ts +6 -5
- package/src/cli/lib/localDeployment/anonymous.ts +2 -2
- package/src/cli/lib/updates.test.ts +40 -30
- package/src/cli/lib/updates.ts +8 -8
- package/src/cli/lib/versionApi.test.ts +13 -10
- package/src/cli/lib/versionApi.ts +13 -5
- package/src/cli/lib/workos/workos.ts +4 -5
- package/src/index.ts +1 -1
- package/src/values/.claude/settings.local.json +10 -0
- package/dist/cjs/cli/lib/ai/config.js.map +0 -7
- package/dist/cjs/cli/lib/ai/index.js +0 -704
- package/dist/cjs/cli/lib/ai/index.js.map +0 -7
- package/dist/cjs/cli/lib/ai/paths.js.map +0 -7
- package/dist/cjs-types/cli/lib/ai/config.d.ts +0 -50
- package/dist/cjs-types/cli/lib/ai/config.d.ts.map +0 -1
- package/dist/cjs-types/cli/lib/ai/config.test.d.ts.map +0 -1
- package/dist/cjs-types/cli/lib/ai/index.d.ts +0 -56
- package/dist/cjs-types/cli/lib/ai/index.d.ts.map +0 -1
- package/dist/cjs-types/cli/lib/ai/index.test.d.ts.map +0 -1
- package/dist/cjs-types/cli/lib/ai/integration.test.d.ts.map +0 -1
- package/dist/cjs-types/cli/lib/ai/paths.d.ts.map +0 -1
- package/dist/cjs-types/cli/lib/ai/prompt.test.d.ts.map +0 -1
- package/dist/esm/cli/lib/ai/config.js.map +0 -7
- package/dist/esm/cli/lib/ai/index.js +0 -684
- package/dist/esm/cli/lib/ai/index.js.map +0 -7
- package/dist/esm/cli/lib/ai/paths.js.map +0 -7
- package/dist/esm-types/cli/lib/ai/config.d.ts +0 -50
- package/dist/esm-types/cli/lib/ai/config.d.ts.map +0 -1
- package/dist/esm-types/cli/lib/ai/config.test.d.ts.map +0 -1
- package/dist/esm-types/cli/lib/ai/index.d.ts +0 -56
- package/dist/esm-types/cli/lib/ai/index.d.ts.map +0 -1
- package/dist/esm-types/cli/lib/ai/index.test.d.ts.map +0 -1
- package/dist/esm-types/cli/lib/ai/integration.test.d.ts.map +0 -1
- package/dist/esm-types/cli/lib/ai/paths.d.ts.map +0 -1
- package/dist/esm-types/cli/lib/ai/prompt.test.d.ts.map +0 -1
- package/src/cli/lib/ai/index.ts +0 -1006
- /package/dist/cjs/cli/lib/{ai → aiFiles}/paths.js +0 -0
- /package/dist/cjs-types/cli/lib/{ai → aiFiles}/config.test.d.ts +0 -0
- /package/dist/cjs-types/cli/lib/{ai → aiFiles}/index.test.d.ts +0 -0
- /package/dist/cjs-types/cli/lib/{ai → aiFiles}/integration.test.d.ts +0 -0
- /package/dist/cjs-types/cli/lib/{ai → aiFiles}/prompt.test.d.ts +0 -0
- /package/dist/esm/cli/lib/{ai → aiFiles}/paths.js +0 -0
- /package/dist/esm-types/cli/lib/{ai → aiFiles}/config.test.d.ts +0 -0
- /package/dist/esm-types/cli/lib/{ai → aiFiles}/index.test.d.ts +0 -0
- /package/dist/esm-types/cli/lib/{ai → aiFiles}/integration.test.d.ts +0 -0
- /package/dist/esm-types/cli/lib/{ai → aiFiles}/prompt.test.d.ts +0 -0
package/package.json
CHANGED
|
@@ -189,9 +189,15 @@
|
|
|
189
189
|
"type": "object",
|
|
190
190
|
"description": "User-configurable settings for Convex AI files behavior.",
|
|
191
191
|
"properties": {
|
|
192
|
+
"enabled": {
|
|
193
|
+
"type": "boolean",
|
|
194
|
+
"description": "When false, disables all Convex AI files prompts and staleness messages in `npx convex dev`. Use `npx convex ai-files enable` to re-enable.",
|
|
195
|
+
"default": true
|
|
196
|
+
},
|
|
192
197
|
"disableStalenessMessage": {
|
|
193
198
|
"type": "boolean",
|
|
194
|
-
"description": "
|
|
199
|
+
"description": "Deprecated. Use `enabled` instead.",
|
|
200
|
+
"deprecated": true,
|
|
195
201
|
"default": false
|
|
196
202
|
}
|
|
197
203
|
},
|
|
@@ -174,6 +174,14 @@ export class AuthenticationManager {
|
|
|
174
174
|
return;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
this._logVerbose(
|
|
178
|
+
`auth state is ${this.authState.state} when handling transition`,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// This transition advanced the auth version, which means the token used was valid
|
|
182
|
+
// and the client and server auth states are in sync.
|
|
183
|
+
this.syncState.markAuthCompletion();
|
|
184
|
+
|
|
177
185
|
if (this.authState.state === "waitingForServerConfirmationOfCachedToken") {
|
|
178
186
|
this._logVerbose("server confirmed auth token is valid");
|
|
179
187
|
void this.refetchToken();
|
|
@@ -455,11 +463,8 @@ export class AuthenticationManager {
|
|
|
455
463
|
}
|
|
456
464
|
}
|
|
457
465
|
if (this.authState.state === "waitingForScheduledRefetch") {
|
|
466
|
+
// TODO: this side-effect would be better situated with scheduling refetch
|
|
458
467
|
clearTimeout(this.authState.refetchTokenTimeoutId);
|
|
459
|
-
|
|
460
|
-
// The waitingForScheduledRefetch state is the most quiesced authed state.
|
|
461
|
-
// Let the syncState know that auth is in a good state, so it can reset failure backoffs
|
|
462
|
-
this.syncState.markAuthCompletion();
|
|
463
468
|
}
|
|
464
469
|
this.authState = newAuth;
|
|
465
470
|
}
|
|
@@ -2,6 +2,7 @@ import child_process from "child_process";
|
|
|
2
2
|
|
|
3
3
|
import { test, expect } from "vitest";
|
|
4
4
|
import { Long } from "../../vendor/long.js";
|
|
5
|
+
import { createHmac } from "crypto";
|
|
5
6
|
|
|
6
7
|
import { BaseConvexClient } from "./client.js";
|
|
7
8
|
import {
|
|
@@ -1026,3 +1027,127 @@ test("Backoff does not reset for idle client until server re-confirms queries",
|
|
|
1026
1027
|
await client.close();
|
|
1027
1028
|
});
|
|
1028
1029
|
});
|
|
1030
|
+
|
|
1031
|
+
// We had an issue where fixing a bug related to having the client do proper exponential backoff
|
|
1032
|
+
// resulted in revealing that if a client was authenticated and had a WebSocket disconnect, the
|
|
1033
|
+
// count of retries would never get reset upon reconnect. This test covers that scenario.
|
|
1034
|
+
test("Retries do not increase across connections for auth'd clients", async () => {
|
|
1035
|
+
await withInMemoryWebSocket(async ({ address, receive, send, close }) => {
|
|
1036
|
+
const client = new BaseConvexClient(address, () => null, {
|
|
1037
|
+
webSocketConstructor: nodeWebSocket,
|
|
1038
|
+
unsavedChangesWarning: false,
|
|
1039
|
+
logger: true,
|
|
1040
|
+
verbose: true,
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// Setup auth
|
|
1044
|
+
const iat = Math.floor(Date.now() / 1000);
|
|
1045
|
+
const exp = iat + 3600;
|
|
1046
|
+
const token = encodeJwt({ sub: "user_123", exp, iat }, "test-secret");
|
|
1047
|
+
client.setAuth(
|
|
1048
|
+
async () => token,
|
|
1049
|
+
(_) => {},
|
|
1050
|
+
);
|
|
1051
|
+
|
|
1052
|
+
// Verify client handshake on "server" side.
|
|
1053
|
+
expect((await receive()).type).toEqual("Connect");
|
|
1054
|
+
expect((await receive()).type).toEqual("Authenticate");
|
|
1055
|
+
expect((await receive()).type).toEqual("ModifyQuerySet");
|
|
1056
|
+
|
|
1057
|
+
// Respond to the handshake noting that the identity was accepted (increased version).
|
|
1058
|
+
send({
|
|
1059
|
+
type: "Transition",
|
|
1060
|
+
startVersion: { querySet: 0, identity: 0, ts: Long.fromNumber(0) },
|
|
1061
|
+
endVersion: { querySet: 0, identity: 1, ts: Long.fromNumber(100) },
|
|
1062
|
+
modifications: [],
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
// Subscribe to a query and verify the "server" receives it.
|
|
1066
|
+
client.subscribe("queries:myQuery", {});
|
|
1067
|
+
expect((await receive()).type).toEqual("ModifyQuerySet");
|
|
1068
|
+
|
|
1069
|
+
// Respond to the subscription with a result.
|
|
1070
|
+
send({
|
|
1071
|
+
type: "Transition",
|
|
1072
|
+
startVersion: { querySet: 0, identity: 1, ts: Long.fromNumber(100) },
|
|
1073
|
+
endVersion: { querySet: 1, identity: 1, ts: Long.fromNumber(200) },
|
|
1074
|
+
modifications: [
|
|
1075
|
+
{
|
|
1076
|
+
type: "QueryUpdated",
|
|
1077
|
+
queryId: 0,
|
|
1078
|
+
value: "result",
|
|
1079
|
+
logLines: [],
|
|
1080
|
+
journal: null,
|
|
1081
|
+
},
|
|
1082
|
+
],
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
// Verify the client gets the subscription result.
|
|
1086
|
+
for (let i = 0; i < 20; i++) {
|
|
1087
|
+
if (client.localQueryResult("queries:myQuery", {}) === "result") break;
|
|
1088
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1089
|
+
}
|
|
1090
|
+
expect(client.localQueryResult("queries:myQuery", {})).toEqual("result");
|
|
1091
|
+
|
|
1092
|
+
// Unceremoniously drop the connection.
|
|
1093
|
+
close();
|
|
1094
|
+
|
|
1095
|
+
// The "server" should see the client handshake for a reconnect.
|
|
1096
|
+
expect((await receive()).type).toEqual("Connect");
|
|
1097
|
+
expect((await receive()).type).toEqual("Authenticate");
|
|
1098
|
+
expect((await receive()).type).toEqual("ModifyQuerySet");
|
|
1099
|
+
|
|
1100
|
+
// The client should record that it has begun a retry.
|
|
1101
|
+
expect(client.connectionState().connectionRetries).toBe(1);
|
|
1102
|
+
|
|
1103
|
+
// Respond to the handshake noting that the identity was accepted (increased version).
|
|
1104
|
+
send({
|
|
1105
|
+
type: "Transition",
|
|
1106
|
+
startVersion: { querySet: 0, identity: 0, ts: Long.fromNumber(0) },
|
|
1107
|
+
endVersion: { querySet: 0, identity: 1, ts: Long.fromNumber(100) },
|
|
1108
|
+
modifications: [],
|
|
1109
|
+
});
|
|
1110
|
+
// Let client handle the transition.
|
|
1111
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1112
|
+
|
|
1113
|
+
// Respond to the re-subscription with a result.
|
|
1114
|
+
send({
|
|
1115
|
+
type: "Transition",
|
|
1116
|
+
startVersion: { querySet: 0, identity: 1, ts: Long.fromNumber(100) },
|
|
1117
|
+
endVersion: { querySet: 1, identity: 1, ts: Long.fromNumber(200) },
|
|
1118
|
+
modifications: [
|
|
1119
|
+
{
|
|
1120
|
+
type: "QueryUpdated",
|
|
1121
|
+
queryId: 0,
|
|
1122
|
+
value: "updated result",
|
|
1123
|
+
logLines: [],
|
|
1124
|
+
journal: null,
|
|
1125
|
+
},
|
|
1126
|
+
],
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
for (let i = 0; i < 20; i++) {
|
|
1130
|
+
if (client.localQueryResult("queries:myQuery", {}) === "updated result")
|
|
1131
|
+
break;
|
|
1132
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1133
|
+
}
|
|
1134
|
+
// Verify the client gets the subscription result.
|
|
1135
|
+
expect(client.localQueryResult("queries:myQuery", {})).toEqual(
|
|
1136
|
+
"updated result",
|
|
1137
|
+
);
|
|
1138
|
+
|
|
1139
|
+
// Client has resynced - should be at 0 retries
|
|
1140
|
+
expect(client.connectionState().connectionRetries).toBe(0);
|
|
1141
|
+
});
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
function encodeJwt(payload: object, secret: string): string {
|
|
1145
|
+
const header = Buffer.from(
|
|
1146
|
+
JSON.stringify({ alg: "HS256", typ: "JWT" }),
|
|
1147
|
+
).toString("base64url");
|
|
1148
|
+
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
1149
|
+
const sig = createHmac("sha256", secret)
|
|
1150
|
+
.update(`${header}.${body}`)
|
|
1151
|
+
.digest("base64url");
|
|
1152
|
+
return `${header}.${body}.${sig}`;
|
|
1153
|
+
}
|
|
@@ -126,12 +126,6 @@ const serverDisconnectErrors = {
|
|
|
126
126
|
VectorIndexesUnavailable: { timeout: 1000 },
|
|
127
127
|
SearchIndexesUnavailable: { timeout: 1000 },
|
|
128
128
|
TableSummariesUnavailable: { timeout: 1000 },
|
|
129
|
-
// ErrorMetadata::service_unavailable() when backend/conductor is unreachable
|
|
130
|
-
ServiceUnavailable: { timeout: 3000 },
|
|
131
|
-
// ErrorMetadata::rejected_before_execution() when funrun workers are unavailable
|
|
132
|
-
WorkerOverloaded: { timeout: 3000 },
|
|
133
|
-
IsolateNotClean: { timeout: 3000 },
|
|
134
|
-
InitialPermitTimeoutError: { timeout: 3000 },
|
|
135
129
|
// More ErrorMetadata::overloaded()
|
|
136
130
|
VectorIndexTooLarge: { timeout: 3000 },
|
|
137
131
|
SearchIndexTooLarge: { timeout: 3000 },
|
|
@@ -235,7 +229,7 @@ export class WebSocketManager {
|
|
|
235
229
|
|
|
236
230
|
// backoff for unknown errors
|
|
237
231
|
this.defaultInitialBackoff = 1000;
|
|
238
|
-
this.maxBackoff =
|
|
232
|
+
this.maxBackoff = 16000;
|
|
239
233
|
this.retries = 0;
|
|
240
234
|
|
|
241
235
|
// Ping messages (sync protocol Pings, not WebSocket protocol Pings) are
|
package/src/cli/aiFiles.ts
CHANGED
|
@@ -4,12 +4,12 @@ import { oneoffContext } from "../bundler/context.js";
|
|
|
4
4
|
import { readProjectConfig } from "./lib/config.js";
|
|
5
5
|
import { functionsDir } from "./lib/utils/utils.js";
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
installAiFiles,
|
|
8
8
|
enableAiFiles,
|
|
9
9
|
removeAiFiles,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from "./lib/
|
|
10
|
+
safelyAttemptToDisableAiFiles,
|
|
11
|
+
} from "./lib/aiFiles/index.js";
|
|
12
|
+
import { statusAiFiles } from "./lib/aiFiles/status.js";
|
|
13
13
|
|
|
14
14
|
async function resolveProjectPaths() {
|
|
15
15
|
const ctx = await oneoffContext({});
|
|
@@ -26,53 +26,46 @@ const aiInstall = new Command("install")
|
|
|
26
26
|
" - convex/_generated/ai/guidelines.md\n" +
|
|
27
27
|
" - AGENTS.md (Convex section only)\n" +
|
|
28
28
|
" - CLAUDE.md (Convex section only)\n" +
|
|
29
|
-
" - Agent skills (installed to each coding agent's native path)
|
|
30
|
-
"Files you have edited yourself are detected via content hashing and skipped\n" +
|
|
31
|
-
"rather than silently overwritten.",
|
|
29
|
+
" - Agent skills (installed to each coding agent's native path)",
|
|
32
30
|
)
|
|
33
31
|
.allowExcessArguments(false)
|
|
34
32
|
.action(async () => {
|
|
35
33
|
const { projectDir, convexDir } = await resolveProjectPaths();
|
|
36
|
-
await
|
|
34
|
+
await installAiFiles({ projectDir, convexDir });
|
|
37
35
|
});
|
|
38
36
|
|
|
39
37
|
const aiEnable = new Command("enable")
|
|
40
|
-
.summary("Enable Convex AI
|
|
38
|
+
.summary("Enable Convex AI files")
|
|
41
39
|
.description(
|
|
42
|
-
"Re-enables AI
|
|
43
|
-
"`
|
|
44
|
-
"or refreshes the managed AI files.",
|
|
40
|
+
"Re-enables Convex AI files by writing `aiFiles.enabled: true` to\n" +
|
|
41
|
+
"`convex.json`, then installs or refreshes the managed AI files.",
|
|
45
42
|
)
|
|
46
43
|
.allowExcessArguments(false)
|
|
47
44
|
.action(async () => {
|
|
48
45
|
const { projectDir, convexDir } = await resolveProjectPaths();
|
|
49
|
-
await enableAiFiles(projectDir, convexDir);
|
|
46
|
+
await enableAiFiles({ projectDir, convexDir });
|
|
50
47
|
});
|
|
51
48
|
|
|
52
49
|
const aiUpdate = new Command("update")
|
|
53
50
|
.summary("Update Convex AI files to the latest version")
|
|
54
51
|
.description(
|
|
55
|
-
"Updates the following
|
|
52
|
+
"Updates the following to their latest versions:\n" +
|
|
56
53
|
" - convex/_generated/ai/guidelines.md\n" +
|
|
57
54
|
" - AGENTS.md (Convex section only)\n" +
|
|
58
55
|
" - CLAUDE.md (Convex section only)\n" +
|
|
59
|
-
" - Agent skills (installed to each coding agent's native path)\n\n"
|
|
60
|
-
"Files you have edited yourself are detected via content hashing and skipped\n" +
|
|
61
|
-
"rather than silently overwritten.\n\n" +
|
|
62
|
-
"Does not change `aiFiles.disableStalenessMessage` in `convex.json`.\n" +
|
|
63
|
-
"Run `npx convex ai-files enable` to enable staleness/install messages.",
|
|
56
|
+
" - Agent skills (installed to each coding agent's native path)\n\n",
|
|
64
57
|
)
|
|
65
58
|
.allowExcessArguments(false)
|
|
66
59
|
.action(async () => {
|
|
67
60
|
const { projectDir, convexDir } = await resolveProjectPaths();
|
|
68
|
-
await
|
|
61
|
+
await installAiFiles({ projectDir, convexDir });
|
|
69
62
|
});
|
|
70
63
|
|
|
71
64
|
const aiDisable = new Command("disable")
|
|
72
|
-
.summary("
|
|
65
|
+
.summary("Disable Convex AI files without removing them")
|
|
73
66
|
.description(
|
|
74
|
-
"Writes
|
|
75
|
-
"
|
|
67
|
+
"Writes `aiFiles.enabled: false` to `convex.json` so `npx convex dev`\n" +
|
|
68
|
+
"stops prompting to install AI files and suppresses staleness messages.\n\n" +
|
|
76
69
|
"Files already installed are left untouched - use `npx convex ai-files remove`\n" +
|
|
77
70
|
"if you also want to delete them.\n\n" +
|
|
78
71
|
"Run `npx convex ai-files enable` to re-enable at any time.",
|
|
@@ -80,7 +73,7 @@ const aiDisable = new Command("disable")
|
|
|
80
73
|
.allowExcessArguments(false)
|
|
81
74
|
.action(async () => {
|
|
82
75
|
const { projectDir } = await resolveProjectPaths();
|
|
83
|
-
await
|
|
76
|
+
await safelyAttemptToDisableAiFiles(projectDir);
|
|
84
77
|
});
|
|
85
78
|
|
|
86
79
|
const aiStatus = new Command("status")
|
|
@@ -98,7 +91,7 @@ const aiStatus = new Command("status")
|
|
|
98
91
|
.allowExcessArguments(false)
|
|
99
92
|
.action(async () => {
|
|
100
93
|
const { projectDir, convexDir } = await resolveProjectPaths();
|
|
101
|
-
await statusAiFiles(projectDir, convexDir);
|
|
94
|
+
await statusAiFiles({ projectDir, convexDir });
|
|
102
95
|
});
|
|
103
96
|
|
|
104
97
|
const aiRemove = new Command("remove")
|
|
@@ -112,12 +105,12 @@ const aiRemove = new Command("remove")
|
|
|
112
105
|
"empty file is deleted. Otherwise the rest of the file is kept.\n\n" +
|
|
113
106
|
"Skills installed from other sources are not affected.\n\n" +
|
|
114
107
|
"Note: after `remove`, `npx convex dev` will suggest reinstalling AI files.\n" +
|
|
115
|
-
"Use `npx convex ai-files disable` to
|
|
108
|
+
"Use `npx convex ai-files disable` to opt out entirely without deleting files.",
|
|
116
109
|
)
|
|
117
110
|
.allowExcessArguments(false)
|
|
118
111
|
.action(async () => {
|
|
119
112
|
const { projectDir, convexDir } = await resolveProjectPaths();
|
|
120
|
-
await removeAiFiles(projectDir, convexDir);
|
|
113
|
+
await removeAiFiles({ projectDir, convexDir });
|
|
121
114
|
});
|
|
122
115
|
|
|
123
116
|
export const aiFiles = new Command("ai-files")
|
package/src/cli/configure.ts
CHANGED
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
promptYesNo,
|
|
45
45
|
} from "./lib/utils/prompts.js";
|
|
46
46
|
import { readGlobalConfig } from "./lib/utils/globalConfig.js";
|
|
47
|
-
import { maybeSetupAiFiles } from "./lib/
|
|
47
|
+
import { maybeSetupAiFiles } from "./lib/aiFiles/index.js";
|
|
48
48
|
import {
|
|
49
49
|
DeploymentSelection,
|
|
50
50
|
deploymentNameFromSelection,
|
|
@@ -382,14 +382,20 @@ async function handleDeploymentWithinProject(
|
|
|
382
382
|
selectedDeployment.deploymentFields !== null &&
|
|
383
383
|
selectedDeployment.deploymentFields.deploymentType === "local"
|
|
384
384
|
) {
|
|
385
|
-
// Start running the local backend
|
|
386
|
-
|
|
385
|
+
// Start running the local backend, which may bind to different ports
|
|
386
|
+
// than what was saved from a previous run.
|
|
387
|
+
const localDeployment = await handleLocalDeployment(ctx, {
|
|
387
388
|
teamSlug: selectedDeployment.deploymentFields.teamSlug!,
|
|
388
389
|
projectSlug: selectedDeployment.deploymentFields.projectSlug!,
|
|
389
390
|
forceUpgrade: cmdOptions.localOptions.forceUpgrade,
|
|
390
391
|
ports: cmdOptions.localOptions.ports,
|
|
391
392
|
backendVersion: cmdOptions.localOptions.backendVersion,
|
|
392
393
|
});
|
|
394
|
+
return {
|
|
395
|
+
url: localDeployment.deploymentUrl,
|
|
396
|
+
adminKey: localDeployment.adminKey,
|
|
397
|
+
deploymentFields: selectedDeployment.deploymentFields,
|
|
398
|
+
};
|
|
393
399
|
}
|
|
394
400
|
return {
|
|
395
401
|
url: selectedDeployment.url,
|
|
@@ -619,11 +625,11 @@ async function selectNewProject(
|
|
|
619
625
|
await doInitConvexFolder(ctx);
|
|
620
626
|
const { configPath, projectConfig } = await readProjectConfig(ctx);
|
|
621
627
|
const folder = functionsDir(configPath, projectConfig);
|
|
622
|
-
await maybeSetupAiFiles(
|
|
628
|
+
await maybeSetupAiFiles({
|
|
623
629
|
ctx,
|
|
624
|
-
path.resolve(folder),
|
|
625
|
-
path.resolve(path.dirname(configPath)),
|
|
626
|
-
);
|
|
630
|
+
convexDir: path.resolve(folder),
|
|
631
|
+
projectDir: path.resolve(path.dirname(configPath)),
|
|
632
|
+
});
|
|
627
633
|
return { teamSlug, projectSlug, devDeployment };
|
|
628
634
|
}
|
|
629
635
|
|
|
@@ -679,11 +685,11 @@ async function selectExistingProject(
|
|
|
679
685
|
|
|
680
686
|
const { configPath, projectConfig } = await readProjectConfig(ctx);
|
|
681
687
|
const folder = functionsDir(configPath, projectConfig);
|
|
682
|
-
await maybeSetupAiFiles(
|
|
688
|
+
await maybeSetupAiFiles({
|
|
683
689
|
ctx,
|
|
684
|
-
path.resolve(folder),
|
|
685
|
-
path.resolve(path.dirname(configPath)),
|
|
686
|
-
);
|
|
690
|
+
convexDir: path.resolve(folder),
|
|
691
|
+
projectDir: path.resolve(path.dirname(configPath)),
|
|
692
|
+
});
|
|
687
693
|
|
|
688
694
|
return { teamSlug, projectSlug, devDeployment };
|
|
689
695
|
}
|
|
@@ -161,8 +161,9 @@ vi.mock("./lib/login.js", async (importOriginal) => {
|
|
|
161
161
|
return { ...actual, ensureLoggedIn: vi.fn() };
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
vi.mock("./lib/
|
|
165
|
-
const actual =
|
|
164
|
+
vi.mock("./lib/aiFiles/index.js", async (importOriginal) => {
|
|
165
|
+
const actual =
|
|
166
|
+
await importOriginal<typeof import("./lib/aiFiles/index.js")>();
|
|
166
167
|
return { ...actual, maybeSetupAiFiles: vi.fn() };
|
|
167
168
|
});
|
|
168
169
|
|
|
@@ -1555,6 +1556,59 @@ describe("deployment selection flows", () => {
|
|
|
1555
1556
|
expect(bigBrainAPIMaybeThrows).not.toHaveBeenCalled();
|
|
1556
1557
|
});
|
|
1557
1558
|
|
|
1559
|
+
it("dev with CONVEX_DEPLOYMENT=local:... uses fresh credentials from handleLocalDeployment", async () => {
|
|
1560
|
+
process.env.CONVEX_DEPLOYMENT = "local:my-local-deployment";
|
|
1561
|
+
vi.mocked(readGlobalConfig).mockReturnValue({
|
|
1562
|
+
accessToken: "test-token",
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
// loadLocalDeploymentCredentials returns stale saved config (e.g. from a
|
|
1566
|
+
// previous run on a different port).
|
|
1567
|
+
vi.mocked(loadLocalDeploymentCredentials).mockResolvedValue({
|
|
1568
|
+
deploymentName: "my-local-deployment",
|
|
1569
|
+
deploymentUrl: "http://127.0.0.1:3212",
|
|
1570
|
+
adminKey: "stale|admin|key",
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
// handleLocalDeployment starts a new backend, potentially on different
|
|
1574
|
+
// ports, and returns the actual credentials.
|
|
1575
|
+
vi.mocked(handleLocalDeployment).mockResolvedValue({
|
|
1576
|
+
deploymentName: "my-local-deployment",
|
|
1577
|
+
deploymentUrl: "http://127.0.0.1:3210",
|
|
1578
|
+
adminKey: "fresh|admin|key",
|
|
1579
|
+
onActivity: async () => {},
|
|
1580
|
+
} as any);
|
|
1581
|
+
|
|
1582
|
+
setupBigBrainRoutes({
|
|
1583
|
+
"deployment/my-local-deployment/team_and_project": () => ({
|
|
1584
|
+
team: "my-team",
|
|
1585
|
+
project: "my-project",
|
|
1586
|
+
teamId: 1,
|
|
1587
|
+
projectId: 1,
|
|
1588
|
+
}),
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
await dev.parseAsync([], { from: "user" });
|
|
1592
|
+
|
|
1593
|
+
expect(handleLocalDeployment).toHaveBeenCalledWith(
|
|
1594
|
+
expect.anything(),
|
|
1595
|
+
expect.objectContaining({
|
|
1596
|
+
teamSlug: "my-team",
|
|
1597
|
+
projectSlug: "my-project",
|
|
1598
|
+
}),
|
|
1599
|
+
);
|
|
1600
|
+
// Must use the fresh credentials from handleLocalDeployment, not the
|
|
1601
|
+
// stale ones from loadLocalDeploymentCredentials.
|
|
1602
|
+
expect(devAgainstDeployment).toHaveBeenCalledWith(
|
|
1603
|
+
expect.anything(),
|
|
1604
|
+
expect.objectContaining({
|
|
1605
|
+
url: "http://127.0.0.1:3210",
|
|
1606
|
+
adminKey: "fresh|admin|key",
|
|
1607
|
+
}),
|
|
1608
|
+
expect.anything(),
|
|
1609
|
+
);
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1558
1612
|
it("dev --local crashes when local deployments are globally disabled", async () => {
|
|
1559
1613
|
vi.mocked(readGlobalConfig).mockReturnValue({
|
|
1560
1614
|
accessToken: "test-token",
|
|
@@ -27,8 +27,12 @@ After `npx convex ai-files install` (or `update`) verify:
|
|
|
27
27
|
- `.cursor/skills/` (Cursor symlinks)
|
|
28
28
|
- `.claude/skills/` (Claude Code symlinks)
|
|
29
29
|
|
|
30
|
-
Expected skill set
|
|
31
|
-
`convex-
|
|
30
|
+
Expected skill set (as of Mar 2026): `convex-create-component`,
|
|
31
|
+
`convex-migration-helper`, `convex-performance-audit`, `convex-quickstart`,
|
|
32
|
+
`convex-setup-auth`.
|
|
33
|
+
|
|
34
|
+
Note: the live skill set is fetched remotely and may change. Check the current
|
|
35
|
+
list against what `npx convex ai-files status` reports after install.
|
|
32
36
|
|
|
33
37
|
**Why manual:** these paths and symlink behaviors are environment-dependent and
|
|
34
38
|
are mocked in automated tests.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { injectAgentsMdSection, hasAgentsMdInstalled } from "./agentsmd.js";
|
|
6
|
+
import {
|
|
7
|
+
AGENTS_MD_START_MARKER,
|
|
8
|
+
AGENTS_MD_END_MARKER,
|
|
9
|
+
} from "../../codegen_templates/agentsmd.js";
|
|
10
|
+
|
|
11
|
+
describe("injectAgentsMdSection", () => {
|
|
12
|
+
let tmpDir: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tmpDir = fs.mkdtempSync(`${os.tmpdir()}${path.sep}`);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const section = `${AGENTS_MD_START_MARKER}\n## Convex\nRead guidelines.\n${AGENTS_MD_END_MARKER}`;
|
|
23
|
+
|
|
24
|
+
test("creates AGENTS.md when it does not exist", async () => {
|
|
25
|
+
await injectAgentsMdSection({ section, projectDir: tmpDir });
|
|
26
|
+
|
|
27
|
+
const content = fs.readFileSync(path.join(tmpDir, "AGENTS.md"), "utf8");
|
|
28
|
+
expect(content).toContain(AGENTS_MD_START_MARKER);
|
|
29
|
+
expect(content).toContain(AGENTS_MD_END_MARKER);
|
|
30
|
+
expect(content).toContain("## Convex");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("appends to an existing AGENTS.md that has no Convex section", async () => {
|
|
34
|
+
const existing = "# My project\n\nSome existing content.\n";
|
|
35
|
+
fs.writeFileSync(path.join(tmpDir, "AGENTS.md"), existing);
|
|
36
|
+
|
|
37
|
+
await injectAgentsMdSection({ section, projectDir: tmpDir });
|
|
38
|
+
|
|
39
|
+
const content = fs.readFileSync(path.join(tmpDir, "AGENTS.md"), "utf8");
|
|
40
|
+
expect(content).toContain("# My project");
|
|
41
|
+
expect(content).toContain("Some existing content.");
|
|
42
|
+
expect(content).toContain(AGENTS_MD_START_MARKER);
|
|
43
|
+
expect(content).toContain("## Convex");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("replaces an existing Convex section when markers are present", async () => {
|
|
47
|
+
const oldSection = `${AGENTS_MD_START_MARKER}\n## Convex\nOld content.\n${AGENTS_MD_END_MARKER}`;
|
|
48
|
+
const existing = `# My project\n\n${oldSection}\n`;
|
|
49
|
+
fs.writeFileSync(path.join(tmpDir, "AGENTS.md"), existing);
|
|
50
|
+
|
|
51
|
+
const newSection = `${AGENTS_MD_START_MARKER}\n## Convex\nNew content.\n${AGENTS_MD_END_MARKER}`;
|
|
52
|
+
await injectAgentsMdSection({ section: newSection, projectDir: tmpDir });
|
|
53
|
+
|
|
54
|
+
const content = fs.readFileSync(path.join(tmpDir, "AGENTS.md"), "utf8");
|
|
55
|
+
expect(content).toContain("New content.");
|
|
56
|
+
expect(content).not.toContain("Old content.");
|
|
57
|
+
expect(content.split(AGENTS_MD_START_MARKER).length - 1).toBe(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("preserves content before and after an existing Convex section", async () => {
|
|
61
|
+
const oldSection = `${AGENTS_MD_START_MARKER}\n## Convex\nOld.\n${AGENTS_MD_END_MARKER}`;
|
|
62
|
+
const existing = `# Before\n\n${oldSection}\n\n# After\n`;
|
|
63
|
+
fs.writeFileSync(path.join(tmpDir, "AGENTS.md"), existing);
|
|
64
|
+
|
|
65
|
+
await injectAgentsMdSection({ section, projectDir: tmpDir });
|
|
66
|
+
|
|
67
|
+
const content = fs.readFileSync(path.join(tmpDir, "AGENTS.md"), "utf8");
|
|
68
|
+
expect(content).toContain("# Before");
|
|
69
|
+
expect(content).toContain("# After");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("returns a non-null hash of the written content", async () => {
|
|
73
|
+
const result = await injectAgentsMdSection({ section, projectDir: tmpDir });
|
|
74
|
+
expect(typeof result.sectionHash).toBe("string");
|
|
75
|
+
expect(result.sectionHash.length).toBeGreaterThan(0);
|
|
76
|
+
expect(result.didWrite).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("returns hash of the section content, not the entire file", async () => {
|
|
80
|
+
fs.writeFileSync(
|
|
81
|
+
path.join(tmpDir, "AGENTS.md"),
|
|
82
|
+
"# My project\n\nExisting content.\n",
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const result = await injectAgentsMdSection({ section, projectDir: tmpDir });
|
|
86
|
+
|
|
87
|
+
const { hashSha256 } = await import("../utils/hash.js");
|
|
88
|
+
expect(result.sectionHash).toBe(hashSha256(section));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("does not write when content is unchanged", async () => {
|
|
92
|
+
await injectAgentsMdSection({ section, projectDir: tmpDir });
|
|
93
|
+
const result = await injectAgentsMdSection({ section, projectDir: tmpDir });
|
|
94
|
+
expect(result.didWrite).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("hasAgentsMdInstalled", () => {
|
|
99
|
+
let tmpDir: string;
|
|
100
|
+
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
tmpDir = fs.mkdtempSync(`${os.tmpdir()}${path.sep}`);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
afterEach(() => {
|
|
106
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("returns false when AGENTS.md does not exist", async () => {
|
|
110
|
+
expect(await hasAgentsMdInstalled(tmpDir)).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("returns false when AGENTS.md exists but has no managed markers", async () => {
|
|
114
|
+
fs.writeFileSync(path.join(tmpDir, "AGENTS.md"), "User content only\n");
|
|
115
|
+
expect(await hasAgentsMdInstalled(tmpDir)).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("returns false when only the start marker is present", async () => {
|
|
119
|
+
fs.writeFileSync(
|
|
120
|
+
path.join(tmpDir, "AGENTS.md"),
|
|
121
|
+
`${AGENTS_MD_START_MARKER}\npartial content\n`,
|
|
122
|
+
);
|
|
123
|
+
expect(await hasAgentsMdInstalled(tmpDir)).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("returns true when both markers are present", async () => {
|
|
127
|
+
fs.writeFileSync(
|
|
128
|
+
path.join(tmpDir, "AGENTS.md"),
|
|
129
|
+
`# Project\n\n${AGENTS_MD_START_MARKER}\n## Convex\n${AGENTS_MD_END_MARKER}\n`,
|
|
130
|
+
);
|
|
131
|
+
expect(await hasAgentsMdInstalled(tmpDir)).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
});
|