cvx-kit 0.1.3 → 0.1.4

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +69 -24
  3. package/dist/agent-tools.d.mts +28 -12
  4. package/dist/agent-tools.mjs +1 -1
  5. package/dist/agent-tools.mjs.map +1 -1
  6. package/dist/auth.d.mts +4 -2
  7. package/dist/auth.mjs +1 -1
  8. package/dist/auth.mjs.map +1 -1
  9. package/dist/components/approvals/_generated/api.d.mts +1 -1
  10. package/dist/components/approvals/audit.mjs +1 -1
  11. package/dist/components/approvals/audit.mjs.map +1 -1
  12. package/dist/components/approvals/client.d.mts +1 -1
  13. package/dist/components/approvals/client.mjs +1 -1
  14. package/dist/components/approvals/client.mjs.map +1 -1
  15. package/dist/components/approvals/constants.mjs.map +1 -1
  16. package/dist/components/approvals/decisions.d.mts +6 -7
  17. package/dist/components/approvals/decisions.mjs +1 -1
  18. package/dist/components/approvals/decisions.mjs.map +1 -1
  19. package/dist/components/approvals/functions.mjs +1 -1
  20. package/dist/components/approvals/functions.mjs.map +1 -1
  21. package/dist/components/approvals/health.mjs.map +1 -1
  22. package/dist/components/approvals/requests.mjs.map +1 -1
  23. package/dist/components/approvals/validators.mjs.map +1 -1
  24. package/dist/components/approvals/workflow.mjs +1 -1
  25. package/dist/components/approvals/workflow.mjs.map +1 -1
  26. package/dist/components/approvals/workflow_steps.d.mts +3 -3
  27. package/dist/components/approvals/workflow_steps.mjs +1 -1
  28. package/dist/components/approvals/workflow_steps.mjs.map +1 -1
  29. package/dist/components/foundation/client.d.mts +17 -24
  30. package/dist/components/foundation/client.mjs +1 -1
  31. package/dist/components/foundation/client.mjs.map +1 -1
  32. package/dist/components/foundation/modules/command/command.d.mts +4 -4
  33. package/dist/components/foundation/modules/command/command.mjs.map +1 -1
  34. package/dist/components/foundation/modules/observability/observability.d.mts +1 -1
  35. package/dist/components/foundation/modules/observability/observability.mjs +1 -1
  36. package/dist/components/foundation/modules/observability/observability.mjs.map +1 -1
  37. package/dist/components/foundation/modules/query/query.d.mts +8 -8
  38. package/dist/components/foundation/modules/query/query.mjs +1 -1
  39. package/dist/components/foundation/modules/query/query.mjs.map +1 -1
  40. package/dist/components/foundation/operation.d.mts +1 -1
  41. package/dist/components/foundation/operation.mjs.map +1 -1
  42. package/dist/components/foundation/result.d.mts +1 -1
  43. package/dist/components/foundation/result.mjs.map +1 -1
  44. package/dist/components/foundation/telemetry.mjs +1 -1
  45. package/dist/components/foundation/telemetry.mjs.map +1 -1
  46. package/dist/crud.d.mts +177 -16
  47. package/dist/crud.mjs +1 -1
  48. package/dist/crud.mjs.map +1 -1
  49. package/dist/middleware.d.mts +7 -7
  50. package/dist/middleware.mjs +1 -1
  51. package/dist/middleware.mjs.map +1 -1
  52. package/dist/oxlint.d.mts +9 -0
  53. package/dist/oxlint.mjs +2 -0
  54. package/dist/oxlint.mjs.map +1 -0
  55. package/dist/tenancy.mjs +1 -1
  56. package/dist/tenancy.mjs.map +1 -1
  57. package/dist/triggers.mjs +1 -1
  58. package/dist/triggers.mjs.map +1 -1
  59. package/dist/webhooks.d.mts +17 -106
  60. package/dist/webhooks.mjs.map +1 -1
  61. package/dist/zod-table.d.mts +46 -90
  62. package/dist/zod-table.mjs +1 -1
  63. package/dist/zod-table.mjs.map +1 -1
  64. package/docs/approvals.md +57 -56
  65. package/docs/architecture.md +1 -1
  66. package/docs/auth.md +57 -33
  67. package/docs/commands.md +53 -53
  68. package/docs/conventions.md +40 -40
  69. package/docs/crud.md +5 -5
  70. package/docs/llms.md +117 -71
  71. package/docs/maintainability.md +4 -4
  72. package/docs/migration.md +33 -33
  73. package/docs/tenancy.md +5 -8
  74. package/docs/triggers.md +4 -2
  75. package/docs/upgrading.md +32 -7
  76. package/docs/zod-table.md +48 -45
  77. package/package.json +33 -22
  78. package/scripts/convex-config-bridge.mjs +7 -18
  79. package/scripts/smoke-packed-components.mjs +20 -61
  80. package/scripts/test-packed-ids.mjs +16 -24
  81. package/scripts/test-packed-oxlint.mjs +121 -0
  82. package/src/test.ts +8 -17
package/docs/zod-table.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The core idea: an entity's shape is declared **once** as a zod object, and every
4
4
  boundary that shape crosses — storage, insert, update, command input, public
5
- DTO, LLM tool input — is a *derived mask* of that single declaration. There is
5
+ DTO, LLM tool input — is a _derived mask_ of that single declaration. There is
6
6
  no second source of truth to drift.
7
7
 
8
8
  ```ts
@@ -10,18 +10,18 @@ import { z } from 'zod'
10
10
  import { zodTable } from 'cvx-kit/zod-table'
11
11
 
12
12
  export const documents = zodTable(
13
- 'documents',
14
- (id) => ({
15
- title: z.string(),
16
- ownerId: id('users'), // `id` is convex-helpers' zid — typed Id<'users'>
17
- secretNote: z.string(),
18
- reviewState: z.enum(['draft', 'published']),
19
- }),
20
- {
21
- serverFields: ['reviewState'], // excluded from inserts — server assigns it
22
- commandFields: ['title'], // the ONLY fields a command may carry
23
- publicFields: ['title', 'ownerId'], // the DTO allowlist — everything else is redacted
24
- },
13
+ 'documents',
14
+ (id) => ({
15
+ title: z.string(),
16
+ ownerId: id('users'), // `id` is convex-helpers' zid — typed Id<'users'>
17
+ secretNote: z.string(),
18
+ reviewState: z.enum(['draft', 'published']),
19
+ }),
20
+ {
21
+ serverFields: ['reviewState'], // excluded from inserts — server assigns it
22
+ commandFields: ['title'], // the ONLY fields a command may carry
23
+ publicFields: ['title', 'ownerId'], // the DTO allowlist — everything else is redacted
24
+ },
25
25
  )
26
26
  ```
27
27
 
@@ -49,19 +49,19 @@ unregistered foreign ID and does not throw; peer installation prevents the
49
49
  supported package graph from creating that ambiguity. Verify critical ID
50
50
  validators retain `kind === 'id'` and the expected `tableName` after installation.
51
51
 
52
- | Property | What it is | Where you use it |
53
- |---|---|---|
54
- | `table` | `defineTable(...)` from the storage shape | `convex/schema.ts` |
55
- | `storage` | zod object of the raw row (no `_id`/`_creationTime`) | validating rows |
56
- | `schema` | `storage` + `_id` + `_creationTime` | full-document validation |
57
- | `insertSchema` | storage minus timestamps minus `serverFields` | mutation args |
58
- | `updateSchema` | `insertSchema.partial()` | patch mutation args |
59
- | `commandInput` | strict pick of `commandFields` | command protocol inputs |
60
- | `publicDto` | strict pick of `publicFields` | query return validators |
61
- | `toPublicDto(row)` | projects **and re-parses** a row into the DTO | query handlers |
62
- | `insert(omit?)` / `update(omit?)` | schema with per-callsite field omission | specialized mutations |
63
- | `tools.insert` / `tools.update` / `tools.id` | ready-made LLM tool input schemas | agent tool definitions |
64
- | `tableName` | the literal table name | indexes, triggers registration |
52
+ | Property | What it is | Where you use it |
53
+ | -------------------------------------------- | ---------------------------------------------------- | ------------------------------ |
54
+ | `table` | `defineTable(...)` from the storage shape | `convex/schema.ts` |
55
+ | `storage` | zod object of the raw row (no `_id`/`_creationTime`) | validating rows |
56
+ | `schema` | `storage` + `_id` + `_creationTime` | full-document validation |
57
+ | `insertSchema` | storage minus timestamps minus `serverFields` | mutation args |
58
+ | `updateSchema` | `insertSchema.partial()` | patch mutation args |
59
+ | `commandInput` | strict pick of `commandFields` | command protocol inputs |
60
+ | `publicDto` | strict pick of `publicFields` | query return validators |
61
+ | `toPublicDto(row)` | projects **and re-parses** a row into the DTO | query handlers |
62
+ | `insert(omit?)` / `update(omit?)` | schema with per-callsite field omission | specialized mutations |
63
+ | `tools.insert` / `tools.update` / `tools.id` | ready-made LLM tool input schemas | agent tool definitions |
64
+ | `tableName` | the literal table name | indexes, triggers registration |
65
65
 
66
66
  ## Redaction is runtime, not just types
67
67
 
@@ -72,13 +72,13 @@ row a public query returns:
72
72
 
73
73
  ```ts
74
74
  export const get = authQuery({
75
- args: { id: zid('documents') },
76
- returns: documents.publicDto,
77
- handler: async (ctx, { id }) => {
78
- const row = await ctx.db.get(id)
79
- if (!row) throw new KitError({ code: 'NOT_FOUND' })
80
- return documents.toPublicDto(row) // secretNote can never leak
81
- },
75
+ args: { id: zid('documents') },
76
+ returns: documents.publicDto,
77
+ handler: async (ctx, { id }) => {
78
+ const row = await ctx.db.get(id)
79
+ if (!row) throw new KitError({ code: 'NOT_FOUND' })
80
+ return documents.toPublicDto(row) // secretNote can never leak
81
+ },
82
82
  })
83
83
  ```
84
84
 
@@ -91,7 +91,7 @@ Every `zodTable` bakes in three optional, **server-owned** fields:
91
91
  - `archivedAt` — soft-delete marker, under application control
92
92
 
93
93
  They are always excluded from `insertSchema`, `updateSchema`, and
94
- `commandInput` — a client or command can never write them. They are *not* in
94
+ `commandInput` — a client or command can never write them. They are _not_ in
95
95
  `publicFields` by default; list them explicitly when a DTO needs them
96
96
  (`publicFields: ['title', 'createdAt']`).
97
97
 
@@ -105,11 +105,11 @@ vocabulary is yours; `_creationTime` remains available for index ordering.
105
105
 
106
106
  Think of the options as three independent allowlists over the same shape:
107
107
 
108
- - `serverFields` — *the server assigns these.* Excluded from `insertSchema`
108
+ - `serverFields` — _the server assigns these._ Excluded from `insertSchema`
109
109
  and `updateSchema`. Example: `reviewState`, denormalized counters.
110
- - `commandFields` — *a command may say these.* The command protocol's input
110
+ - `commandFields` — _a command may say these._ The command protocol's input
111
111
  is exactly this pick, strict. Everything else must be derived by the handler.
112
- - `publicFields` — *a client may see these.* The DTO is exactly this pick;
112
+ - `publicFields` — _a client may see these._ The DTO is exactly this pick;
113
113
  may include timestamp fields.
114
114
 
115
115
  All derived objects are `.strict()` — unknown keys are rejected, in both
@@ -146,10 +146,13 @@ keeps the same "one owner per table" convention while accepting any zod type
146
146
  as storage:
147
147
 
148
148
  ```ts
149
- const events = zodVariantTable('events', z.discriminatedUnion('kind', [
150
- z.object({ kind: z.literal('created'), by: zid('users') }),
151
- z.object({ kind: z.literal('archived'), reason: z.string() }),
152
- ]))
149
+ const events = zodVariantTable(
150
+ 'events',
151
+ z.discriminatedUnion('kind', [
152
+ z.object({ kind: z.literal('created'), by: zid('users') }),
153
+ z.object({ kind: z.literal('archived'), reason: z.string() }),
154
+ ]),
155
+ )
153
156
  // events.table → defineTable, events.insertSchema → the union itself
154
157
  ```
155
158
 
@@ -165,7 +168,7 @@ the type level:
165
168
 
166
169
  ```ts
167
170
  const assignInput = z.object({
168
- documentId: jsonSafeZid('documents'), // LLM sees: string, "Convex document id for table \"documents\""
171
+ documentId: jsonSafeZid('documents'), // LLM sees: string, "Convex document id for table \"documents\""
169
172
  })
170
173
  ```
171
174
 
@@ -177,9 +180,9 @@ import { defineSchema } from 'convex/server'
177
180
  import { documents } from './tables/documents'
178
181
 
179
182
  export default defineSchema({
180
- documents: documents.table
181
- .index('by_owner', ['ownerId'])
182
- .index('by_owner_archived', ['ownerId', 'archivedAt']),
183
+ documents: documents.table
184
+ .index('by_owner', ['ownerId'])
185
+ .index('by_owner_archived', ['ownerId', 'archivedAt']),
183
186
  })
184
187
  ```
185
188
 
package/package.json CHANGED
@@ -1,9 +1,24 @@
1
1
  {
2
2
  "name": "cvx-kit",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Reusable Convex application kit: foundation kernels, approvals component, zod table boundaries, WorkOS-backed auth function constructors, and audited command protocol.",
5
- "type": "module",
5
+ "keywords": [
6
+ "component",
7
+ "convex",
8
+ "workos",
9
+ "zod"
10
+ ],
11
+ "homepage": "https://github.com/ImRLopezAG/cvx-kit#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/ImRLopezAG/cvx-kit/issues"
14
+ },
6
15
  "license": "MIT",
16
+ "author": "Angel Lopez",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/ImRLopezAG/cvx-kit.git"
20
+ },
21
+ "type": "module",
7
22
  "sideEffects": false,
8
23
  "exports": {
9
24
  "./package.json": "./package.json",
@@ -85,14 +100,22 @@
85
100
  },
86
101
  "./components/approvals/_generated/component": {
87
102
  "types": "./dist/components/approvals/_generated/component.d.mts"
103
+ },
104
+ "./oxlint": {
105
+ "types": "./dist/oxlint.d.mts",
106
+ "import": "./dist/oxlint.mjs"
88
107
  }
89
108
  },
90
109
  "scripts": {
110
+ "lint": "vp lint",
111
+ "check": "vp check",
112
+ "test:lint": "vp test run tools/oxlint/cvx/index.test.mjs",
91
113
  "build": "vp pack && node scripts/convex-config-bridge.mjs && publint",
92
114
  "typecheck": "tsc --noEmit",
93
115
  "test": "vp test run --exclude test/package-contents.test.mjs",
94
116
  "test:package": "bun run build && vp test run test/package-contents.test.mjs",
95
117
  "test:package:ids": "bun run build && node scripts/test-packed-ids.mjs",
118
+ "test:package:oxlint": "bun run build && node scripts/test-packed-oxlint.mjs",
96
119
  "test:package:smoke:bun": "bun scripts/smoke-packed-components.mjs bun",
97
120
  "test:package:smoke:npm": "node scripts/smoke-packed-components.mjs npm",
98
121
  "docs:sync": "rm -f docs/*.md && cp src/docs/*.md docs/",
@@ -103,36 +126,24 @@
103
126
  },
104
127
  "dependencies": {
105
128
  "@convex-dev/workflow": "0.4.6",
129
+ "@oxlint/plugins": "1.79.0",
106
130
  "convex-audit-log": "^0.2.0"
107
131
  },
108
- "peerDependencies": {
109
- "convex-helpers": ">=0.1.123 <0.2.0",
110
- "convex": "^1.45.0",
111
- "zod": "^4.5.4"
112
- },
113
132
  "devDependencies": {
114
- "convex-helpers": "0.1.124",
115
133
  "@edge-runtime/vm": "^5.0.0",
134
+ "@types/node": "^26.6.1",
116
135
  "convex": "1.45.0",
136
+ "convex-helpers": "0.1.124",
117
137
  "convex-test": "^0.0.56",
138
+ "oxlint": "1.79.0",
118
139
  "publint": "^0.3.24",
119
140
  "typescript": "7.0.2",
120
141
  "vite-plus": "0.3.0",
121
142
  "zod": "4.5.4"
122
143
  },
123
- "keywords": [
124
- "convex",
125
- "component",
126
- "workos",
127
- "zod"
128
- ],
129
- "author": "Angel Lopez",
130
- "repository": {
131
- "type": "git",
132
- "url": "git+https://github.com/ImRLopezAG/cvx-kit.git"
133
- },
134
- "homepage": "https://github.com/ImRLopezAG/cvx-kit#readme",
135
- "bugs": {
136
- "url": "https://github.com/ImRLopezAG/cvx-kit/issues"
144
+ "peerDependencies": {
145
+ "convex": "^1.45.0",
146
+ "convex-helpers": ">=0.1.123 <0.2.0",
147
+ "zod": "^4.5.4"
137
148
  }
138
149
  }
@@ -2,13 +2,7 @@
2
2
  // is literally named `convex.config.js` (see DEFINITION_FILENAME_JS in the
3
3
  // convex CLI). vp pack emits .mjs, so this postbuild step materializes the
4
4
  // compiled definition as convex.config.js in each component's dist directory.
5
- import {
6
- readFileSync,
7
- readdirSync,
8
- renameSync,
9
- rmSync,
10
- writeFileSync,
11
- } from 'node:fs'
5
+ import { readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
12
6
  import { dirname, join } from 'node:path'
13
7
  import { fileURLToPath } from 'node:url'
14
8
 
@@ -16,8 +10,10 @@ const root = join(dirname(fileURLToPath(import.meta.url)), '..')
16
10
 
17
11
  for (const component of ['foundation', 'approvals']) {
18
12
  const directory = join(root, 'dist', 'components', component)
19
- const config = readFileSync(join(directory, 'convex.config.mjs'), 'utf8')
20
- .replace(/\/\/# sourceMappingURL=.*\n?/, '')
13
+ const config = readFileSync(join(directory, 'convex.config.mjs'), 'utf8').replace(
14
+ /\/\/# sourceMappingURL=.*\n?/,
15
+ '',
16
+ )
21
17
  writeFileSync(join(directory, 'convex.config.js'), config)
22
18
 
23
19
  // Component implementations are discovered from schema.ts/schema.js only.
@@ -36,17 +32,10 @@ for (const component of ['foundation', 'approvals']) {
36
32
  if (bridged !== source) writeFileSync(path, bridged)
37
33
  }
38
34
  renameSync(join(directory, 'schema.d.mts'), join(directory, 'schema.d.ts'))
39
- const dataModelDeclaration = join(
40
- directory,
41
- '_generated',
42
- 'dataModel.d.mts',
43
- )
35
+ const dataModelDeclaration = join(directory, '_generated', 'dataModel.d.mts')
44
36
  writeFileSync(
45
37
  dataModelDeclaration,
46
- readFileSync(dataModelDeclaration, 'utf8').replace(
47
- '../schema.mjs',
48
- '../schema.js',
49
- ),
38
+ readFileSync(dataModelDeclaration, 'utf8').replace('../schema.mjs', '../schema.js'),
50
39
  )
51
40
  rmSync(join(directory, 'schema.mjs'))
52
41
  rmSync(join(directory, 'schema.mjs.map'))
@@ -1,12 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process'
2
- import {
3
- copyFileSync,
4
- mkdirSync,
5
- mkdtempSync,
6
- readFileSync,
7
- rmSync,
8
- writeFileSync,
9
- } from 'node:fs'
2
+ import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
10
3
  import { tmpdir } from 'node:os'
11
4
  import { join } from 'node:path'
12
5
 
@@ -18,9 +11,7 @@ if (installer !== 'bun' && installer !== 'npm') {
18
11
  const root = join(import.meta.dirname, '..')
19
12
  const temporaryRoot = mkdtempSync(join(tmpdir(), 'cvx-kit-smoke-'))
20
13
  const fixture = join(temporaryRoot, 'fixture')
21
- const packageManifest = JSON.parse(
22
- readFileSync(join(root, 'package.json'), 'utf8'),
23
- )
14
+ const packageManifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
24
15
  const packageVersion = packageManifest.version
25
16
  const tarball = join(temporaryRoot, `cvx-kit-${packageVersion}.tgz`)
26
17
 
@@ -42,10 +33,7 @@ function runStreaming(command, args, cwd = fixture) {
42
33
  }
43
34
 
44
35
  function runConvex(...args) {
45
- return run('node', [
46
- join(fixture, 'node_modules', 'convex', 'bin', 'main.js'),
47
- ...args,
48
- ])
36
+ return run('node', [join(fixture, 'node_modules', 'convex', 'bin', 'main.js'), ...args])
49
37
  }
50
38
 
51
39
  function write(relativePath, contents) {
@@ -59,11 +47,7 @@ function step(label, callback) {
59
47
  }
60
48
 
61
49
  try {
62
- run(
63
- 'bun',
64
- ['pm', 'pack', '--destination', temporaryRoot, '--ignore-scripts'],
65
- root,
66
- )
50
+ run('bun', ['pm', 'pack', '--destination', temporaryRoot, '--ignore-scripts'], root)
67
51
  mkdirSync(join(fixture, 'convex'), { recursive: true })
68
52
  write(
69
53
  'package.json',
@@ -252,13 +236,8 @@ export const status = query({
252
236
  copyFileSync(join(root, 'bun.lock'), join(fixture, 'bun.lock'))
253
237
  }
254
238
  if (installer === 'bun')
255
- step('install with bun', () =>
256
- runStreaming('bun', ['install', '--ignore-scripts']),
257
- )
258
- else
259
- step('install with npm', () =>
260
- runStreaming('npm', ['install', '--ignore-scripts']),
261
- )
239
+ step('install with bun', () => runStreaming('bun', ['install', '--ignore-scripts']))
240
+ else step('install with npm', () => runStreaming('npm', ['install', '--ignore-scripts']))
262
241
  if (installer === 'bun')
263
242
  step('test helper with bun', () =>
264
243
  runStreaming('bunx', ['vp', 'test', 'run', 'packed-test-helper.test.ts']),
@@ -268,30 +247,18 @@ export const status = query({
268
247
  runStreaming('npx', ['vp', 'test', 'run', 'packed-test-helper.test.ts']),
269
248
  )
270
249
 
271
- step('deploy Convex fixture', () =>
272
- runConvex('dev', '--once', '--typecheck=disable'),
273
- )
250
+ step('deploy Convex fixture', () => runConvex('dev', '--once', '--typecheck=disable'))
274
251
  const health = JSON.parse(runConvex('run', 'smoke:health'))
275
252
  if (health.status !== 'ready' || health.schemaVersion !== 1) {
276
- throw new Error(
277
- `Unexpected approvals health response: ${JSON.stringify(health)}`,
278
- )
253
+ throw new Error(`Unexpected approvals health response: ${JSON.stringify(health)}`)
279
254
  }
280
- const foundationHealth = JSON.parse(
281
- runConvex('run', 'smoke:foundationHealth'),
282
- )
255
+ const foundationHealth = JSON.parse(runConvex('run', 'smoke:foundationHealth'))
283
256
  if (foundationHealth !== 'ready') {
284
- throw new Error(
285
- `Unexpected foundation health response: ${JSON.stringify(foundationHealth)}`,
286
- )
257
+ throw new Error(`Unexpected foundation health response: ${JSON.stringify(foundationHealth)}`)
287
258
  }
288
259
  const { runId } = JSON.parse(runConvex('run', 'smoke:start'))
289
260
  const { runId: secondRunId } = JSON.parse(
290
- runConvex(
291
- 'run',
292
- 'smoke:start',
293
- JSON.stringify({ resourceRef: 'packed-component-2' }),
294
- ),
261
+ runConvex('run', 'smoke:start', JSON.stringify({ resourceRef: 'packed-component-2' })),
295
262
  )
296
263
  const firstPage = JSON.parse(
297
264
  runConvex(
@@ -324,20 +291,18 @@ export const status = query({
324
291
  secondPage.page[0]._id !== runId ||
325
292
  secondPage.page[0]._id === firstPage.page[0]._id
326
293
  ) {
327
- throw new Error(
328
- `Unexpected continuation approval page: ${JSON.stringify(secondPage)}`,
329
- )
294
+ throw new Error(`Unexpected continuation approval page: ${JSON.stringify(secondPage)}`)
330
295
  }
331
296
 
332
297
  // start schedules the workflow; a successful start is not decision readiness.
333
298
  let readyStatus
334
299
  const readyDeadline = Date.now() + 60_000
335
300
  while (Date.now() < readyDeadline) {
336
- readyStatus = JSON.parse(
337
- runConvex('run', 'smoke:status', JSON.stringify({ runId })),
338
- )
339
- if (readyStatus?.run.state !== 'pending' ||
340
- ['failed', 'canceled', 'completed'].includes(readyStatus?.execution?.type)) {
301
+ readyStatus = JSON.parse(runConvex('run', 'smoke:status', JSON.stringify({ runId })))
302
+ if (
303
+ readyStatus?.run.state !== 'pending' ||
304
+ ['failed', 'canceled', 'completed'].includes(readyStatus?.execution?.type)
305
+ ) {
341
306
  throw new Error(`Approval ended before decision readiness: ${JSON.stringify(readyStatus)}`)
342
307
  }
343
308
  if (readyStatus.run.currentStepKey === 'releaseDecision') break
@@ -346,23 +311,17 @@ export const status = query({
346
311
  if (readyStatus?.run.currentStepKey !== 'releaseDecision') {
347
312
  throw new Error(`Approval did not become decision-ready: ${JSON.stringify(readyStatus)}`)
348
313
  }
349
- const decision = JSON.parse(
350
- runConvex('run', 'smoke:decide', JSON.stringify({ runId })),
351
- )
314
+ const decision = JSON.parse(runConvex('run', 'smoke:decide', JSON.stringify({ runId })))
352
315
  if (decision.state !== 'approved') {
353
316
  throw new Error(`Unexpected decision response: ${JSON.stringify(decision)}`)
354
317
  }
355
- const history = JSON.parse(
356
- runConvex('run', 'smoke:history', JSON.stringify({ runId })),
357
- )
318
+ const history = JSON.parse(runConvex('run', 'smoke:history', JSON.stringify({ runId })))
358
319
  if (history.length !== 1 || history[0].decision !== 'approved') {
359
320
  throw new Error(`Unexpected approval history: ${JSON.stringify(history)}`)
360
321
  }
361
322
  let status
362
323
  for (let attempt = 0; attempt < 20; attempt += 1) {
363
- status = JSON.parse(
364
- runConvex('run', 'smoke:status', JSON.stringify({ runId })),
365
- )
324
+ status = JSON.parse(runConvex('run', 'smoke:status', JSON.stringify({ runId })))
366
325
  if (status.execution?.type === 'completed') break
367
326
  await new Promise((resolve) => setTimeout(resolve, 500))
368
327
  }
@@ -1,19 +1,11 @@
1
1
  import { execFileSync } from 'node:child_process'
2
- import {
3
- mkdtempSync,
4
- mkdirSync,
5
- readFileSync,
6
- rmSync,
7
- writeFileSync,
8
- } from 'node:fs'
2
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
9
3
  import { tmpdir } from 'node:os'
10
4
  import { join } from 'node:path'
11
5
 
12
6
  const root = join(import.meta.dirname, '..')
13
7
  const temporary = mkdtempSync(join(tmpdir(), 'cvx-kit-ids-'))
14
- const manifest = JSON.parse(
15
- readFileSync(join(root, 'package.json'), 'utf8'),
16
- )
8
+ const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
17
9
  function run(command, args, cwd) {
18
10
  return execFileSync(command, args, {
19
11
  cwd,
@@ -22,11 +14,7 @@ function run(command, args, cwd) {
22
14
  })
23
15
  }
24
16
  try {
25
- run(
26
- 'bun',
27
- ['pm', 'pack', '--destination', temporary, '--ignore-scripts'],
28
- root,
29
- )
17
+ run('bun', ['pm', 'pack', '--destination', temporary, '--ignore-scripts'], root)
30
18
  for (const installer of ['npm', 'bun']) {
31
19
  const fixture = join(temporary, installer)
32
20
  mkdirSync(fixture)
@@ -75,21 +63,21 @@ for (const validator of [
75
63
  `,
76
64
  )
77
65
  run('node', ['probe.mjs'], fixture)
78
- for (const testFile of [
79
- 'command.test.ts',
80
- 'command-lifecycle.test.ts',
81
- ]) {
66
+ for (const testFile of ['command.test.ts', 'command-lifecycle.test.ts']) {
82
67
  const source = readFileSync(join(root, 'test', testFile), 'utf8')
83
- .replace(
84
- '../src/components/foundation/client',
85
- 'cvx-kit/components/foundation',
86
- )
68
+ .replace('../src/components/foundation/client', 'cvx-kit/components/foundation')
87
69
  .replace(
88
70
  "const modules = import.meta.glob('./fixture/**/*.ts')",
89
71
  "const modules = { './_generated/server.ts': async () => ({}) }",
90
72
  )
91
73
  writeFileSync(join(fixture, testFile), source)
92
74
  }
75
+ for (const testFile of ['zod-table.test.ts', 'agent-tools.test.ts']) {
76
+ const source = readFileSync(join(root, 'test', testFile), 'utf8')
77
+ .replaceAll('../src/zod-table', 'cvx-kit/zod-table')
78
+ .replaceAll('../src/agent-tools', 'cvx-kit/agent-tools')
79
+ writeFileSync(join(fixture, testFile), source)
80
+ }
93
81
  writeFileSync(
94
82
  join(fixture, 'command-types.ts'),
95
83
  readFileSync(join(root, 'test/command-types.ts'), 'utf8').replace(
@@ -134,6 +122,8 @@ it('registers concrete and generic backends from the packed package', () => {
134
122
  'vite/client',
135
123
  'command-types.ts',
136
124
  'registration.test.ts',
125
+ 'zod-table.test.ts',
126
+ 'agent-tools.test.ts',
137
127
  ],
138
128
  fixture,
139
129
  )
@@ -145,11 +135,13 @@ it('registers concrete and generic backends from the packed package', () => {
145
135
  'registration.test.ts',
146
136
  'command.test.ts',
147
137
  'command-lifecycle.test.ts',
138
+ 'zod-table.test.ts',
139
+ 'agent-tools.test.ts',
148
140
  ],
149
141
  fixture,
150
142
  )
151
143
  console.log(
152
- `${installer}: packed IDs, strict declarations, registration, and command lifecycle pass with host convex-helpers 0.1.123`,
144
+ `${installer}: packed IDs, table/tool types, registration, and command lifecycle pass with host convex-helpers 0.1.123`,
153
145
  )
154
146
  }
155
147
  } finally {
@@ -0,0 +1,121 @@
1
+ import { strict as assert } from 'node:assert'
2
+ import { execFileSync, spawnSync } from 'node:child_process'
3
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { rules } from '../dist/oxlint.mjs'
7
+
8
+ const root = join(import.meta.dirname, '..')
9
+ const temporary = mkdtempSync(join(tmpdir(), 'cvx-kit-oxlint-'))
10
+ const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
11
+ const ruleNames = Object.keys(rules)
12
+ assert.equal(ruleNames.length, 6)
13
+
14
+ try {
15
+ run('bun', ['pm', 'pack', '--destination', temporary, '--ignore-scripts'], root)
16
+ for (const installer of ['npm', 'bun']) {
17
+ const fixture = join(temporary, installer)
18
+ mkdirSync(join(fixture, 'src/components/example'), { recursive: true })
19
+ writeFileSync(
20
+ join(fixture, 'package.json'),
21
+ JSON.stringify({
22
+ private: true,
23
+ type: 'module',
24
+ dependencies: {
25
+ 'cvx-kit': `file:${join(temporary, `cvx-kit-${manifest.version}.tgz`)}`,
26
+ convex: manifest.devDependencies.convex,
27
+ 'convex-helpers': manifest.devDependencies['convex-helpers'],
28
+ zod: manifest.devDependencies.zod,
29
+ },
30
+ devDependencies: { oxlint: manifest.devDependencies.oxlint },
31
+ }),
32
+ )
33
+ run(installer, ['install', '--ignore-scripts'], fixture)
34
+ writeFileSync(
35
+ join(fixture, '.oxlintrc.json'),
36
+ JSON.stringify({
37
+ jsPlugins: [{ name: 'cvx', specifier: 'cvx-kit/oxlint' }],
38
+ rules: {
39
+ ...Object.fromEntries(ruleNames.map((name) => [`cvx/${name}`, 'error'])),
40
+ 'cvx/no-internal-reexports': ['error', { entryPoints: ['src/public.ts'] }],
41
+ },
42
+ }),
43
+ )
44
+ writeFileSync(
45
+ join(fixture, 'src/public.ts'),
46
+ "export { run } from './components/example/client'\n",
47
+ )
48
+ writeFileSync(
49
+ join(fixture, 'src/components/example/client.ts'),
50
+ 'export function run() { return 1 }\n',
51
+ )
52
+ const oxlint = join(fixture, 'node_modules/.bin/oxlint')
53
+ run(oxlint, ['--config', '.oxlintrc.json', 'src'], fixture)
54
+ writeFileSync(
55
+ join(fixture, 'src/components/example/bad.ts'),
56
+ `
57
+ import { defineTable } from 'convex/server'
58
+ import { host } from '../../host'
59
+ function helper() { return process.env.SECRET }
60
+ const privateHelper = () => helper()
61
+ export const table = defineTable({})
62
+ export { host }
63
+ export const value = privateHelper()
64
+ `,
65
+ )
66
+ writeFileSync(
67
+ join(fixture, 'src/host.ts'),
68
+ "export const host = 1\nimport './components/example/bad'\n",
69
+ )
70
+ const invalid = spawnSync(oxlint, ['--config', '.oxlintrc.json', '--format', 'json', 'src'], {
71
+ cwd: fixture,
72
+ encoding: 'utf8',
73
+ })
74
+ assert.ifError(invalid.error)
75
+ assert.equal(invalid.status, 1, invalid.stderr)
76
+ const diagnostics = JSON.parse(invalid.stdout).diagnostics
77
+ for (const name of ruleNames) {
78
+ assert.ok(
79
+ diagnostics.some((item) => item.code === `cvx(${name})`),
80
+ `${installer}: missing ${name}: ${invalid.stdout}`,
81
+ )
82
+ }
83
+ writeFileSync(
84
+ join(fixture, 'plugin-types.ts'),
85
+ `
86
+ import plugin, { rules, type RuleName } from 'cvx-kit/oxlint'
87
+ import type { Plugin, Rule } from '@oxlint/plugins'
88
+ const compatible: Plugin = plugin
89
+ const name: RuleName = 'component-boundaries'
90
+ const rule: Rule = rules[name]
91
+ void compatible
92
+ void rule
93
+ `,
94
+ )
95
+ run(
96
+ join(root, 'node_modules/.bin/tsc'),
97
+ [
98
+ '--noEmit',
99
+ '--strict',
100
+ '--skipLibCheck',
101
+ '--target',
102
+ 'es2025',
103
+ '--module',
104
+ 'esnext',
105
+ '--moduleResolution',
106
+ 'bundler',
107
+ 'plugin-types.ts',
108
+ ],
109
+ fixture,
110
+ )
111
+ console.log(
112
+ `${installer}: packed Oxlint plugin loads, all six rules report, and declarations typecheck`,
113
+ )
114
+ }
115
+ } finally {
116
+ rmSync(temporary, { recursive: true, force: true })
117
+ }
118
+
119
+ function run(command, args, cwd) {
120
+ return execFileSync(command, args, { cwd, encoding: 'utf8', stdio: 'pipe' })
121
+ }