convex 1.48.0 → 1.49.0-alpha.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.
Files changed (248) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/browser.bundle.js +1 -1
  3. package/dist/browser.bundle.js.map +1 -1
  4. package/dist/cjs/bundler/context.js +7 -5
  5. package/dist/cjs/bundler/context.js.map +2 -2
  6. package/dist/cjs/bundler/fs.js +47 -6
  7. package/dist/cjs/bundler/fs.js.map +2 -2
  8. package/dist/cjs/cli/account.js +237 -0
  9. package/dist/cjs/cli/account.js.map +7 -0
  10. package/dist/cjs/cli/configure.js +15 -2
  11. package/dist/cjs/cli/configure.js.map +2 -2
  12. package/dist/cjs/cli/lib/accountManagement.js +196 -0
  13. package/dist/cjs/cli/lib/accountManagement.js.map +7 -0
  14. package/dist/cjs/cli/lib/accounts.js +334 -0
  15. package/dist/cjs/cli/lib/accounts.js.map +7 -0
  16. package/dist/cjs/cli/lib/deployKeyWarning.js +43 -0
  17. package/dist/cjs/cli/lib/deployKeyWarning.js.map +7 -0
  18. package/dist/cjs/cli/lib/deployment.js +2 -0
  19. package/dist/cjs/cli/lib/deployment.js.map +2 -2
  20. package/dist/cjs/cli/lib/deploymentSelection.js +27 -18
  21. package/dist/cjs/cli/lib/deploymentSelection.js.map +2 -2
  22. package/dist/cjs/cli/lib/envvars.js +32 -0
  23. package/dist/cjs/cli/lib/envvars.js.map +2 -2
  24. package/dist/cjs/cli/lib/fsUtils.js +1 -1
  25. package/dist/cjs/cli/lib/fsUtils.js.map +2 -2
  26. package/dist/cjs/cli/lib/generateDocs.js +1 -1
  27. package/dist/cjs/cli/lib/generateDocs.js.map +1 -1
  28. package/dist/cjs/cli/lib/login.js +42 -4
  29. package/dist/cjs/cli/lib/login.js.map +3 -3
  30. package/dist/cjs/cli/lib/mcp/requestContext.js +5 -1
  31. package/dist/cjs/cli/lib/mcp/requestContext.js.map +2 -2
  32. package/dist/cjs/cli/lib/plugins/claudeCode.js +188 -0
  33. package/dist/cjs/cli/lib/plugins/claudeCode.js.map +7 -0
  34. package/dist/cjs/cli/lib/plugins/codex.js +167 -0
  35. package/dist/cjs/cli/lib/plugins/codex.js.map +7 -0
  36. package/dist/cjs/cli/lib/plugins/harness.js +50 -0
  37. package/dist/cjs/cli/lib/plugins/harness.js.map +7 -0
  38. package/dist/cjs/cli/lib/plugins/marketplaces.js +76 -0
  39. package/dist/cjs/cli/lib/plugins/marketplaces.js.map +7 -0
  40. package/dist/cjs/cli/lib/plugins/skills.js +98 -0
  41. package/dist/cjs/cli/lib/plugins/skills.js.map +7 -0
  42. package/dist/cjs/cli/lib/plugins/utils.js +125 -0
  43. package/dist/cjs/cli/lib/plugins/utils.js.map +7 -0
  44. package/dist/cjs/cli/lib/utils/utils.js +18 -9
  45. package/dist/cjs/cli/lib/utils/utils.js.map +2 -2
  46. package/dist/cjs/cli/login.js +213 -114
  47. package/dist/cjs/cli/login.js.map +3 -3
  48. package/dist/cjs/cli/logout.js +113 -12
  49. package/dist/cjs/cli/logout.js.map +3 -3
  50. package/dist/cjs/cli/mcp.js +43 -10
  51. package/dist/cjs/cli/mcp.js.map +3 -3
  52. package/dist/cjs/cli/plugins.js +114 -0
  53. package/dist/cjs/cli/plugins.js.map +7 -0
  54. package/dist/cjs/cli/program.js +3 -1
  55. package/dist/cjs/cli/program.js.map +2 -2
  56. package/dist/cjs/index.js +1 -1
  57. package/dist/cjs/index.js.map +1 -1
  58. package/dist/cjs/server/impl/actions_impl.js.map +2 -2
  59. package/dist/cjs-types/bundler/context.d.ts +5 -0
  60. package/dist/cjs-types/bundler/context.d.ts.map +1 -1
  61. package/dist/cjs-types/bundler/fs.d.ts +7 -2
  62. package/dist/cjs-types/bundler/fs.d.ts.map +1 -1
  63. package/dist/cjs-types/cli/account.d.ts +6 -0
  64. package/dist/cjs-types/cli/account.d.ts.map +1 -0
  65. package/dist/cjs-types/cli/configure.d.ts.map +1 -1
  66. package/dist/cjs-types/cli/lib/accountManagement.d.ts +10 -0
  67. package/dist/cjs-types/cli/lib/accountManagement.d.ts.map +1 -0
  68. package/dist/cjs-types/cli/lib/accounts.d.ts +59 -0
  69. package/dist/cjs-types/cli/lib/accounts.d.ts.map +1 -0
  70. package/dist/cjs-types/cli/lib/deployKeyWarning.d.ts +3 -0
  71. package/dist/cjs-types/cli/lib/deployKeyWarning.d.ts.map +1 -0
  72. package/dist/cjs-types/cli/lib/deployment.d.ts +2 -0
  73. package/dist/cjs-types/cli/lib/deployment.d.ts.map +1 -1
  74. package/dist/cjs-types/cli/lib/deploymentSelection.d.ts +2 -1
  75. package/dist/cjs-types/cli/lib/deploymentSelection.d.ts.map +1 -1
  76. package/dist/cjs-types/cli/lib/envvars.d.ts +6 -0
  77. package/dist/cjs-types/cli/lib/envvars.d.ts.map +1 -1
  78. package/dist/cjs-types/cli/lib/fsUtils.d.ts +1 -0
  79. package/dist/cjs-types/cli/lib/fsUtils.d.ts.map +1 -1
  80. package/dist/cjs-types/cli/lib/login.d.ts +3 -1
  81. package/dist/cjs-types/cli/lib/login.d.ts.map +1 -1
  82. package/dist/cjs-types/cli/lib/mcp/requestContext.d.ts.map +1 -1
  83. package/dist/cjs-types/cli/lib/mcp/tools/insights.d.ts +2 -2
  84. package/dist/cjs-types/cli/lib/plugins/claudeCode.d.ts +25 -0
  85. package/dist/cjs-types/cli/lib/plugins/claudeCode.d.ts.map +1 -0
  86. package/dist/cjs-types/cli/lib/plugins/codex.d.ts +25 -0
  87. package/dist/cjs-types/cli/lib/plugins/codex.d.ts.map +1 -0
  88. package/dist/cjs-types/cli/lib/plugins/harness.d.ts +33 -0
  89. package/dist/cjs-types/cli/lib/plugins/harness.d.ts.map +1 -0
  90. package/dist/cjs-types/cli/lib/plugins/marketplaces.d.ts +40 -0
  91. package/dist/cjs-types/cli/lib/plugins/marketplaces.d.ts.map +1 -0
  92. package/dist/cjs-types/cli/lib/plugins/skills.d.ts +18 -0
  93. package/dist/cjs-types/cli/lib/plugins/skills.d.ts.map +1 -0
  94. package/dist/cjs-types/cli/lib/plugins/utils.d.ts +21 -0
  95. package/dist/cjs-types/cli/lib/plugins/utils.d.ts.map +1 -0
  96. package/dist/cjs-types/cli/lib/utils/utils.d.ts +5 -0
  97. package/dist/cjs-types/cli/lib/utils/utils.d.ts.map +1 -1
  98. package/dist/cjs-types/cli/login.d.ts +21 -0
  99. package/dist/cjs-types/cli/login.d.ts.map +1 -1
  100. package/dist/cjs-types/cli/logout.d.ts +8 -1
  101. package/dist/cjs-types/cli/logout.d.ts.map +1 -1
  102. package/dist/cjs-types/cli/mcp.d.ts.map +1 -1
  103. package/dist/cjs-types/cli/plugins.d.ts +3 -0
  104. package/dist/cjs-types/cli/plugins.d.ts.map +1 -0
  105. package/dist/cjs-types/cli/program.d.ts.map +1 -1
  106. package/dist/cjs-types/index.d.ts +1 -1
  107. package/dist/cjs-types/index.d.ts.map +1 -1
  108. package/dist/cjs-types/server/impl/actions_impl.d.ts +3 -2
  109. package/dist/cjs-types/server/impl/actions_impl.d.ts.map +1 -1
  110. package/dist/cli.bundle.cjs +10059 -7094
  111. package/dist/cli.bundle.cjs.map +4 -4
  112. package/dist/esm/bundler/context.js +7 -5
  113. package/dist/esm/bundler/context.js.map +2 -2
  114. package/dist/esm/bundler/fs.js +47 -6
  115. package/dist/esm/bundler/fs.js.map +2 -2
  116. package/dist/esm/cli/account.js +221 -0
  117. package/dist/esm/cli/account.js.map +7 -0
  118. package/dist/esm/cli/configure.js +15 -2
  119. package/dist/esm/cli/configure.js.map +2 -2
  120. package/dist/esm/cli/lib/accountManagement.js +179 -0
  121. package/dist/esm/cli/lib/accountManagement.js.map +7 -0
  122. package/dist/esm/cli/lib/accounts.js +289 -0
  123. package/dist/esm/cli/lib/accounts.js.map +7 -0
  124. package/dist/esm/cli/lib/deployKeyWarning.js +27 -0
  125. package/dist/esm/cli/lib/deployKeyWarning.js.map +7 -0
  126. package/dist/esm/cli/lib/deployment.js +2 -2
  127. package/dist/esm/cli/lib/deployment.js.map +2 -2
  128. package/dist/esm/cli/lib/deploymentSelection.js +35 -20
  129. package/dist/esm/cli/lib/deploymentSelection.js.map +2 -2
  130. package/dist/esm/cli/lib/envvars.js +35 -1
  131. package/dist/esm/cli/lib/envvars.js.map +2 -2
  132. package/dist/esm/cli/lib/fsUtils.js +1 -1
  133. package/dist/esm/cli/lib/fsUtils.js.map +2 -2
  134. package/dist/esm/cli/lib/generateDocs.js +1 -1
  135. package/dist/esm/cli/lib/generateDocs.js.map +1 -1
  136. package/dist/esm/cli/lib/login.js +52 -4
  137. package/dist/esm/cli/lib/login.js.map +3 -3
  138. package/dist/esm/cli/lib/mcp/requestContext.js +5 -1
  139. package/dist/esm/cli/lib/mcp/requestContext.js.map +2 -2
  140. package/dist/esm/cli/lib/plugins/claudeCode.js +160 -0
  141. package/dist/esm/cli/lib/plugins/claudeCode.js.map +7 -0
  142. package/dist/esm/cli/lib/plugins/codex.js +140 -0
  143. package/dist/esm/cli/lib/plugins/codex.js.map +7 -0
  144. package/dist/esm/cli/lib/plugins/harness.js +17 -0
  145. package/dist/esm/cli/lib/plugins/harness.js.map +7 -0
  146. package/dist/esm/cli/lib/plugins/marketplaces.js +53 -0
  147. package/dist/esm/cli/lib/plugins/marketplaces.js.map +7 -0
  148. package/dist/esm/cli/lib/plugins/skills.js +65 -0
  149. package/dist/esm/cli/lib/plugins/skills.js.map +7 -0
  150. package/dist/esm/cli/lib/plugins/utils.js +86 -0
  151. package/dist/esm/cli/lib/plugins/utils.js.map +7 -0
  152. package/dist/esm/cli/lib/utils/utils.js +16 -9
  153. package/dist/esm/cli/lib/utils/utils.js.map +2 -2
  154. package/dist/esm/cli/login.js +225 -112
  155. package/dist/esm/cli/login.js.map +3 -3
  156. package/dist/esm/cli/logout.js +110 -13
  157. package/dist/esm/cli/logout.js.map +2 -2
  158. package/dist/esm/cli/mcp.js +34 -10
  159. package/dist/esm/cli/mcp.js.map +2 -2
  160. package/dist/esm/cli/plugins.js +99 -0
  161. package/dist/esm/cli/plugins.js.map +7 -0
  162. package/dist/esm/cli/program.js +3 -1
  163. package/dist/esm/cli/program.js.map +2 -2
  164. package/dist/esm/index.js +1 -1
  165. package/dist/esm/index.js.map +1 -1
  166. package/dist/esm/server/impl/actions_impl.js.map +2 -2
  167. package/dist/esm-types/bundler/context.d.ts +5 -0
  168. package/dist/esm-types/bundler/context.d.ts.map +1 -1
  169. package/dist/esm-types/bundler/fs.d.ts +7 -2
  170. package/dist/esm-types/bundler/fs.d.ts.map +1 -1
  171. package/dist/esm-types/cli/account.d.ts +6 -0
  172. package/dist/esm-types/cli/account.d.ts.map +1 -0
  173. package/dist/esm-types/cli/configure.d.ts.map +1 -1
  174. package/dist/esm-types/cli/lib/accountManagement.d.ts +10 -0
  175. package/dist/esm-types/cli/lib/accountManagement.d.ts.map +1 -0
  176. package/dist/esm-types/cli/lib/accounts.d.ts +59 -0
  177. package/dist/esm-types/cli/lib/accounts.d.ts.map +1 -0
  178. package/dist/esm-types/cli/lib/deployKeyWarning.d.ts +3 -0
  179. package/dist/esm-types/cli/lib/deployKeyWarning.d.ts.map +1 -0
  180. package/dist/esm-types/cli/lib/deployment.d.ts +2 -0
  181. package/dist/esm-types/cli/lib/deployment.d.ts.map +1 -1
  182. package/dist/esm-types/cli/lib/deploymentSelection.d.ts +2 -1
  183. package/dist/esm-types/cli/lib/deploymentSelection.d.ts.map +1 -1
  184. package/dist/esm-types/cli/lib/envvars.d.ts +6 -0
  185. package/dist/esm-types/cli/lib/envvars.d.ts.map +1 -1
  186. package/dist/esm-types/cli/lib/fsUtils.d.ts +1 -0
  187. package/dist/esm-types/cli/lib/fsUtils.d.ts.map +1 -1
  188. package/dist/esm-types/cli/lib/login.d.ts +3 -1
  189. package/dist/esm-types/cli/lib/login.d.ts.map +1 -1
  190. package/dist/esm-types/cli/lib/mcp/requestContext.d.ts.map +1 -1
  191. package/dist/esm-types/cli/lib/mcp/tools/insights.d.ts +2 -2
  192. package/dist/esm-types/cli/lib/plugins/claudeCode.d.ts +25 -0
  193. package/dist/esm-types/cli/lib/plugins/claudeCode.d.ts.map +1 -0
  194. package/dist/esm-types/cli/lib/plugins/codex.d.ts +25 -0
  195. package/dist/esm-types/cli/lib/plugins/codex.d.ts.map +1 -0
  196. package/dist/esm-types/cli/lib/plugins/harness.d.ts +33 -0
  197. package/dist/esm-types/cli/lib/plugins/harness.d.ts.map +1 -0
  198. package/dist/esm-types/cli/lib/plugins/marketplaces.d.ts +40 -0
  199. package/dist/esm-types/cli/lib/plugins/marketplaces.d.ts.map +1 -0
  200. package/dist/esm-types/cli/lib/plugins/skills.d.ts +18 -0
  201. package/dist/esm-types/cli/lib/plugins/skills.d.ts.map +1 -0
  202. package/dist/esm-types/cli/lib/plugins/utils.d.ts +21 -0
  203. package/dist/esm-types/cli/lib/plugins/utils.d.ts.map +1 -0
  204. package/dist/esm-types/cli/lib/utils/utils.d.ts +5 -0
  205. package/dist/esm-types/cli/lib/utils/utils.d.ts.map +1 -1
  206. package/dist/esm-types/cli/login.d.ts +21 -0
  207. package/dist/esm-types/cli/login.d.ts.map +1 -1
  208. package/dist/esm-types/cli/logout.d.ts +8 -1
  209. package/dist/esm-types/cli/logout.d.ts.map +1 -1
  210. package/dist/esm-types/cli/mcp.d.ts.map +1 -1
  211. package/dist/esm-types/cli/plugins.d.ts +3 -0
  212. package/dist/esm-types/cli/plugins.d.ts.map +1 -0
  213. package/dist/esm-types/cli/program.d.ts.map +1 -1
  214. package/dist/esm-types/index.d.ts +1 -1
  215. package/dist/esm-types/index.d.ts.map +1 -1
  216. package/dist/esm-types/server/impl/actions_impl.d.ts +3 -2
  217. package/dist/esm-types/server/impl/actions_impl.d.ts.map +1 -1
  218. package/dist/react.bundle.js +1 -1
  219. package/dist/react.bundle.js.map +1 -1
  220. package/package.json +2 -2
  221. package/src/bundler/context.ts +9 -5
  222. package/src/bundler/fs.ts +54 -5
  223. package/src/cli/account.ts +320 -0
  224. package/src/cli/configure.ts +18 -2
  225. package/src/cli/lib/accountManagement.ts +262 -0
  226. package/src/cli/lib/accounts.ts +400 -0
  227. package/src/cli/lib/deployKeyWarning.ts +43 -0
  228. package/src/cli/lib/deployment.ts +2 -2
  229. package/src/cli/lib/deploymentSelection.ts +37 -18
  230. package/src/cli/lib/envvars.ts +43 -1
  231. package/src/cli/lib/fsUtils.ts +5 -2
  232. package/src/cli/lib/generateDocs.ts +1 -1
  233. package/src/cli/lib/login.ts +64 -3
  234. package/src/cli/lib/mcp/requestContext.ts +7 -1
  235. package/src/cli/lib/plugins/claudeCode.ts +201 -0
  236. package/src/cli/lib/plugins/codex.ts +177 -0
  237. package/src/cli/lib/plugins/harness.ts +49 -0
  238. package/src/cli/lib/plugins/marketplaces.ts +94 -0
  239. package/src/cli/lib/plugins/skills.ts +101 -0
  240. package/src/cli/lib/plugins/utils.ts +121 -0
  241. package/src/cli/lib/utils/utils.ts +19 -15
  242. package/src/cli/login.ts +293 -168
  243. package/src/cli/logout.ts +142 -17
  244. package/src/cli/mcp.ts +48 -11
  245. package/src/cli/plugins.ts +154 -0
  246. package/src/cli/program.ts +4 -0
  247. package/src/index.ts +1 -1
  248. package/src/server/impl/actions_impl.ts +3 -2
@@ -0,0 +1,177 @@
1
+ import os from "os";
2
+ import path from "path";
3
+ import { z } from "zod";
4
+ import { nodeFs } from "../../../bundler/fs.js";
5
+ import type { Cli, Places } from "./harness.js";
6
+ import {
7
+ convexMarketplaces,
8
+ type Marketplace,
9
+ type Plugin,
10
+ } from "./marketplaces.js";
11
+ import {
12
+ fetchGitHubJson,
13
+ onPath,
14
+ parseList,
15
+ subfolders,
16
+ versionOf,
17
+ } from "./utils.js";
18
+
19
+ /** Builds `getCodexLocations` for the given places, so tests can point it at fixtures. */
20
+ export const createGetCodexLocations =
21
+ (places: Places) => async (): Promise<Cli[]> => {
22
+ // The ChatGPT app also has `codex-cli/bin/codex`, a script that runs this
23
+ // binary. Running that script from another app sets off macOS's App
24
+ // Management prompt, and running the binary doesn't.
25
+ const desktop = path.join(
26
+ places.applications,
27
+ "ChatGPT.app",
28
+ "Contents",
29
+ "Resources",
30
+ "codex-cli",
31
+ "CodexCLI.app",
32
+ "Contents",
33
+ "MacOS",
34
+ "codex",
35
+ );
36
+ const found: Omit<Cli, "version">[] = [
37
+ ...onPath("codex", places.path).map((p) => ({
38
+ source: "terminal" as const,
39
+ path: p,
40
+ })),
41
+ ...(nodeFs.exists(desktop)
42
+ ? [{ source: "desktop" as const, path: desktop }]
43
+ : []),
44
+ // The extension keeps its binary in a folder named for the platform, like
45
+ // `bin/macos-aarch64`.
46
+ ...subfolders(path.join(places.home, ".vscode", "extensions"))
47
+ .filter((dir) => path.basename(dir).startsWith("openai.chatgpt-"))
48
+ .flatMap((dir) => subfolders(path.join(dir, "bin")))
49
+ .map((dir) => ({
50
+ source: "vscode" as const,
51
+ path: path.join(dir, "codex"),
52
+ })),
53
+ ];
54
+ return Promise.all(
55
+ found.map(async (cli) => ({
56
+ ...cli,
57
+ version: await versionOf(cli.path),
58
+ })),
59
+ );
60
+ };
61
+
62
+ /** Every copy of Codex on this machine: the terminal CLI, the ChatGPT app's copy and the VS Code extension's copies. */
63
+ export const getCodexLocations = createGetCodexLocations({
64
+ home: os.homedir(),
65
+ path: process.env.PATH ?? "",
66
+ applications: "/Applications",
67
+ });
68
+
69
+ /** The newest Convex plugin a Codex user can install, which is main of our repo. */
70
+ async function codexLatest(): Promise<Record<string, string>> {
71
+ const plugin = await fetchGitHubJson<{ version: string }>(
72
+ "convex/convex-codex-plugin/main/plugins/convex/.codex-plugin/plugin.json",
73
+ );
74
+ return plugin === null ? {} : { convex: plugin.version };
75
+ }
76
+
77
+ /** `codex plugin marketplace list --json` */
78
+ const codexMarketplaceList = z.object({
79
+ marketplaces: z.array(
80
+ z.object({
81
+ name: z.string(),
82
+ root: z.string(),
83
+ marketplaceSource: z
84
+ .object({ sourceType: z.string(), source: z.string() })
85
+ .optional(),
86
+ }),
87
+ ),
88
+ });
89
+
90
+ /** `codex plugin list --json` */
91
+ const codexPluginList = z.object({
92
+ installed: z.array(
93
+ z.object({
94
+ name: z.string(),
95
+ marketplaceName: z.string(),
96
+ version: z.string(),
97
+ enabled: z.boolean(),
98
+ }),
99
+ ),
100
+ });
101
+
102
+ /** Reads the plugins in `codex plugin list --json`, or returns null when it doesn't match. */
103
+ function codexPlugins(pluginList: string): Plugin[] | null {
104
+ const plugins = parseList(
105
+ codexPluginList,
106
+ pluginList,
107
+ "codex plugin list --json",
108
+ );
109
+ if (plugins === null) return null;
110
+ return plugins.installed.map((p) => ({
111
+ name: p.name,
112
+ marketplace: p.marketplaceName,
113
+ version: p.version,
114
+ enabled: p.enabled,
115
+ }));
116
+ }
117
+
118
+ /** Reads Codex's Convex marketplaces from `codex plugin marketplace list --json` and `codex plugin list --json`, or returns null when either doesn't match. */
119
+ export function codexMarketplaces(
120
+ marketplaceList: string,
121
+ pluginList: string,
122
+ ): Marketplace[] | null {
123
+ const marketplaces = parseList(
124
+ codexMarketplaceList,
125
+ marketplaceList,
126
+ "codex plugin marketplace list --json",
127
+ );
128
+ const plugins = codexPlugins(pluginList);
129
+ if (marketplaces === null || plugins === null) return null;
130
+ return convexMarketplaces(
131
+ marketplaces.marketplaces.map((m) => ({
132
+ name: m.name,
133
+ // Anything that isn't a git marketplace is shown by its folder.
134
+ source:
135
+ m.marketplaceSource?.sourceType === "git"
136
+ ? { kind: "git", url: m.marketplaceSource.source }
137
+ : { kind: "folder", path: m.root },
138
+ })),
139
+ plugins,
140
+ );
141
+ }
142
+
143
+ const pluginId = "convex@convex-codex-plugin";
144
+ /** The install step that brings an installed plugin to the latest version. When it fails, the user keeps an old version. */
145
+ const upgradeStep = ["plugin", "marketplace", "upgrade", "convex-codex-plugin"];
146
+
147
+ /** Codex as `npx convex plugins` installs and reads it. */
148
+ export const codex = {
149
+ id: "codex" as const,
150
+ name: "Codex",
151
+ plugin: pluginId,
152
+ upgradeStep,
153
+ installSteps: [
154
+ // Notes:
155
+ //
156
+ // 1. The marketplace is added from the `get-convex` org, the org's name
157
+ // before the rename to `convex`. Codex refuses to re-add a marketplace
158
+ // from a different source, so switching would make install fail for
159
+ // anyone who installed before. Switch once install handles an existing
160
+ // marketplace.
161
+ //
162
+ // 2. Codex installs plugins from its own local copy of each marketplace, so
163
+ // it doesn't see the new plugin versions we publish until that copy is
164
+ // updated.
165
+
166
+ // Add the marketplace
167
+ ["plugin", "marketplace", "add", "get-convex/convex-codex-plugin"],
168
+ // Update the local copy from GitHub, so the latest plugin version is
169
+ // available to install - this is important
170
+ upgradeStep,
171
+ // Install the latest plugin from the updated marketplace
172
+ ["plugin", "add", pluginId],
173
+ ],
174
+ plugins: codexPlugins,
175
+ marketplaces: codexMarketplaces,
176
+ latest: codexLatest,
177
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The Claude Code and Codex CLIs found on the user's machine. Users of the
3
+ * desktop apps or the VS Code extensions often have no CLI on PATH, but each
4
+ * app ships its own copy, which takes the same commands and shares the same
5
+ * plugin state as the terminal CLI.
6
+ */
7
+ import * as semver from "semver";
8
+ import { getClaudeCodeLocations } from "./claudeCode.js";
9
+ import { getCodexLocations } from "./codex.js";
10
+
11
+ /** Where a copy of a harness CLI was found. */
12
+ type CliSource = "terminal" | "desktop" | "vscode";
13
+
14
+ /** A copy of a harness CLI and the first line it prints for `--version`. */
15
+ export type Cli = { source: CliSource; path: string; version: string };
16
+
17
+ /** Where to look for harness CLIs. */
18
+ export type Places = {
19
+ home: string;
20
+ /** Directories separated like `$PATH`. */
21
+ path: string;
22
+ /** Where macOS apps are installed, `/Applications`. */
23
+ applications: string;
24
+ };
25
+
26
+ /** The copy with the highest version. Versions compare as semver, where a release beats its prereleases. */
27
+ function newest(clis: Cli[]): Cli | undefined {
28
+ // Every copy printed its version when the finder ran it.
29
+ const version = (cli: Cli) =>
30
+ semver.coerce(cli.version, { includePrerelease: true })!;
31
+ return [...clis].sort((a, b) => semver.rcompare(version(a), version(b)))[0];
32
+ }
33
+
34
+ /** Builds `getNewestHarnessLocations` from the given location getters, so tests can pass fixed locations. */
35
+ export const createGetNewestHarnessLocations =
36
+ (getters: {
37
+ getCodexLocations: () => Promise<Cli[]>;
38
+ getClaudeCodeLocations: () => Promise<Cli[]>;
39
+ }) =>
40
+ async () => ({
41
+ codex: newest(await getters.getCodexLocations()),
42
+ claudeCode: newest(await getters.getClaudeCodeLocations()),
43
+ });
44
+
45
+ /** The newest copy of each harness's CLI on this machine, or `undefined` for a harness that isn't installed. */
46
+ export const getNewestHarnessLocations = createGetNewestHarnessLocations({
47
+ getCodexLocations,
48
+ getClaudeCodeLocations,
49
+ });
@@ -0,0 +1,94 @@
1
+ import { logOutput } from "../../../bundler/log.js";
2
+ import { formatRows } from "./utils.js";
3
+
4
+ /** Names of the Convex plugins, including the old internal beta. */
5
+ const pluginNames = ["convex", "convex-beta"];
6
+
7
+ /** A Convex plugin as its harness reports it. */
8
+ export type Plugin = {
9
+ name: string;
10
+ marketplace: string;
11
+ /** The version that runs. */
12
+ version: string;
13
+ /** Claude Code only. */
14
+ scope?: string;
15
+ enabled: boolean;
16
+ /** Claude Code folder installs only. The copy cached at install time, when it differs from the folder. */
17
+ cachedVersion?: string | undefined;
18
+ };
19
+
20
+ /** Where a marketplace comes from. `other` is a source type we haven't seen, by its type name. */
21
+ export type Source =
22
+ | { kind: "github"; repo: string }
23
+ | { kind: "git"; url: string }
24
+ | { kind: "folder"; path: string }
25
+ | { kind: "other"; type: string };
26
+
27
+ /** A marketplace named after Convex or holding a Convex plugin, with the Convex plugins installed from it. */
28
+ export type Marketplace = { name: string; source: Source; plugins: Plugin[] };
29
+
30
+ /** Keeps the marketplaces named after Convex or holding a Convex plugin, each with its Convex plugins. */
31
+ export function convexMarketplaces(
32
+ marketplaces: { name: string; source: Source }[],
33
+ plugins: Plugin[],
34
+ ): Marketplace[] {
35
+ const convex = plugins.filter((p) => pluginNames.includes(p.name));
36
+ return marketplaces
37
+ .map((m) => ({
38
+ ...m,
39
+ plugins: convex.filter((p) => p.marketplace === m.name),
40
+ }))
41
+ .filter((m) => m.name.startsWith("convex") || m.plugins.length > 0);
42
+ }
43
+
44
+ /** Describes a marketplace's source for people. */
45
+ function sourceText(source: Source): string {
46
+ switch (source.kind) {
47
+ case "github":
48
+ return `github.com/${source.repo}`;
49
+ case "git":
50
+ return source.url.replace(/^https?:\/\//, "").replace(/\.git$/, "");
51
+ case "folder":
52
+ return source.path;
53
+ case "other":
54
+ return source.type;
55
+ }
56
+ }
57
+
58
+ /** Prints each Convex marketplace with its Convex plugins indented under it. */
59
+ export function logMarketplaces(
60
+ title: string,
61
+ marketplaces: Marketplace[],
62
+ latest: Record<string, string>,
63
+ ) {
64
+ logOutput(title);
65
+ if (marketplaces.length === 0) {
66
+ logOutput(" No Convex plugin installed");
67
+ return;
68
+ }
69
+ const marketplaceLines = formatRows(
70
+ marketplaces.map((m) => [m.name, sourceText(m.source)]),
71
+ );
72
+ // Plugin rows are formatted together so their columns line up across marketplaces.
73
+ const pluginLines = formatRows(
74
+ marketplaces
75
+ .flatMap((m) => m.plugins)
76
+ .map((p) => [
77
+ p.name,
78
+ p.version,
79
+ latest[p.name] === undefined ? "" : `latest ${latest[p.name]}`,
80
+ p.scope ?? "-",
81
+ p.enabled ? "enabled" : "disabled",
82
+ p.cachedVersion === undefined ? "" : `cached copy ${p.cachedVersion}`,
83
+ ]),
84
+ " ",
85
+ );
86
+ let next = 0;
87
+ marketplaces.forEach((m, i) => {
88
+ logOutput(marketplaceLines[i]);
89
+ for (const line of pluginLines.slice(next, next + m.plugins.length)) {
90
+ logOutput(line);
91
+ }
92
+ next += m.plugins.length;
93
+ });
94
+ }
@@ -0,0 +1,101 @@
1
+ import os from "os";
2
+ import path from "path";
3
+ import { nodeFs } from "../../../bundler/fs.js";
4
+ import { logOutput } from "../../../bundler/log.js";
5
+ import { fetchAgentSkillsCatalog } from "../versionApi.js";
6
+
7
+ /** Finds folders named `skills` in a folder's children and grandchildren, without following links. */
8
+ function skillsFolders(dir: string, depth: number): string[] {
9
+ let entries;
10
+ try {
11
+ entries = nodeFs.listDir(dir);
12
+ } catch {
13
+ return [];
14
+ }
15
+ return entries
16
+ .filter((entry) => entry.isDirectory())
17
+ .flatMap((entry) => {
18
+ const child = path.join(dir, entry.name);
19
+ if (entry.name === "skills") return [child];
20
+ return depth > 1 ? skillsFolders(child, depth - 1) : [];
21
+ });
22
+ }
23
+
24
+ /** Convex skill names from the catalog on version.convex.dev, split into current and deleted ones. */
25
+ type SkillNames = { current: Set<string>; deleted: Set<string> };
26
+
27
+ /** A skills folder holding Convex skills. */
28
+ type SkillsFolder = {
29
+ path: string;
30
+ current: number;
31
+ deleted: number;
32
+ brokenLinks: number;
33
+ };
34
+
35
+ /** Finds the skills folders in a root's hidden folders that hold Convex skills, counting deleted skills and broken links. */
36
+ export function convexSkillsFolders(
37
+ root: string,
38
+ names: SkillNames,
39
+ ): SkillsFolder[] {
40
+ return nodeFs
41
+ .listDir(root)
42
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith("."))
43
+ .flatMap((entry) => skillsFolders(path.join(root, entry.name), 2))
44
+ .map((folder) => {
45
+ const entries = nodeFs.listDir(folder).map((entry) => entry.name);
46
+ const current = entries.filter((name) => names.current.has(name));
47
+ const deleted = entries.filter((name) => names.deleted.has(name));
48
+ // exists follows links, so it's false for a link whose target is gone.
49
+ const brokenLinks = [...current, ...deleted].filter(
50
+ (name) => !nodeFs.exists(path.join(folder, name)),
51
+ ).length;
52
+ return {
53
+ path: folder,
54
+ current: current.length,
55
+ deleted: deleted.length,
56
+ brokenLinks,
57
+ };
58
+ })
59
+ .filter((folder) => folder.current + folder.deleted > 0);
60
+ }
61
+
62
+ /** Prints each skills folder in a root's hidden folders that holds Convex skills, counting deleted skills and broken links. */
63
+ function logSkills(
64
+ title: string,
65
+ root: string,
66
+ label: string,
67
+ names: SkillNames,
68
+ ) {
69
+ const folders = convexSkillsFolders(root, names);
70
+ logOutput(title);
71
+ if (folders.length === 0) {
72
+ logOutput(" No Convex skills found");
73
+ return;
74
+ }
75
+ for (const folder of folders) {
76
+ logOutput(
77
+ ` ${folder.path.replace(root, label)} ${folder.current} Convex skills, ${folder.deleted} deleted, ${folder.brokenLinks} broken links`,
78
+ );
79
+ }
80
+ }
81
+
82
+ /** Prints the Convex skills in the hidden folders of the home folder and the current folder. */
83
+ export async function logConvexSkills() {
84
+ const catalog = await fetchAgentSkillsCatalog();
85
+ if (catalog.kind === "error") {
86
+ logOutput("Skills: couldn't fetch the list of Convex skills");
87
+ } else {
88
+ const names = (kind: "active" | "deleted") =>
89
+ new Set(
90
+ catalog.data.skills
91
+ .filter((skill) => skill.status.kind === kind)
92
+ .map((skill) => skill.skillName),
93
+ );
94
+ const skillNames = {
95
+ current: names("active"),
96
+ deleted: names("deleted"),
97
+ };
98
+ logSkills("Global skills", os.homedir(), "~", skillNames);
99
+ logSkills("Project skills", process.cwd(), ".", skillNames);
100
+ }
101
+ }
@@ -0,0 +1,121 @@
1
+ import child_process from "child_process";
2
+ import path from "path";
3
+ import { promisify } from "util";
4
+ import * as Sentry from "@sentry/node";
5
+ import { z } from "zod";
6
+ import { nodeFs } from "../../../bundler/fs.js";
7
+
8
+ /** The file the shell runs for `name`, like `which`. */
9
+ export function onPath(name: string, pathDirs: string): string[] {
10
+ const dir = pathDirs
11
+ .split(path.delimiter)
12
+ .find((d) => nodeFs.exists(path.join(d, name)));
13
+ return dir === undefined ? [] : [path.join(dir, name)];
14
+ }
15
+
16
+ /** The folders directly inside `dir`, sorted by name, or none when `dir` doesn't exist. */
17
+ export function subfolders(dir: string): string[] {
18
+ let entries;
19
+ try {
20
+ entries = nodeFs.listDir(dir);
21
+ } catch {
22
+ return [];
23
+ }
24
+ return entries
25
+ .filter((entry) => entry.isDirectory())
26
+ .map((entry) => path.join(dir, entry.name))
27
+ .sort();
28
+ }
29
+
30
+ /** The first line a CLI prints for `--version`. */
31
+ export async function versionOf(cli: string): Promise<string> {
32
+ const { stdout } = await promisify(child_process.execFile)(cli, [
33
+ "--version",
34
+ ]);
35
+ return stdout.split("\n")[0];
36
+ }
37
+
38
+ /** Runs a command and resolves to whether it succeeded and everything it printed. */
39
+ export function run(
40
+ command: string,
41
+ args: string[],
42
+ ): Promise<{ ok: boolean; output: string }> {
43
+ return new Promise((resolve) => {
44
+ let output = "";
45
+
46
+ const proc = child_process.spawn(command, args, {
47
+ stdio: ["ignore", "pipe", "pipe"],
48
+ });
49
+
50
+ proc.stdout.on("data", (chunk) => (output += chunk));
51
+ proc.stderr.on("data", (chunk) => (output += chunk));
52
+ proc.on("error", () => resolve({ ok: false, output }));
53
+ proc.on("close", (code) => resolve({ ok: code === 0, output }));
54
+ });
55
+ }
56
+
57
+ /** Runs a command and resolves to its stdout, or null when it can't be started. */
58
+ export function capture(
59
+ command: string,
60
+ args: string[],
61
+ ): Promise<string | null> {
62
+ return new Promise((resolve) => {
63
+ let stdout = "";
64
+
65
+ const proc = child_process.spawn(command, args, {
66
+ stdio: ["ignore", "pipe", "inherit"],
67
+ });
68
+
69
+ proc.stdout.on("data", (chunk) => (stdout += chunk));
70
+ proc.on("error", () => resolve(null));
71
+ proc.on("close", () => resolve(stdout));
72
+ });
73
+ }
74
+
75
+ /** Fetches a JSON file from GitHub, or resolves to null when it can't, reporting why to Sentry. */
76
+ export async function fetchGitHubJson<T>(path: string): Promise<T | null> {
77
+ try {
78
+ const response = await fetch(`https://raw.githubusercontent.com/${path}`);
79
+ if (!response.ok) {
80
+ Sentry.captureException(
81
+ new Error(`Failed to fetch ${path}: status = ${response.status}`),
82
+ );
83
+ return null;
84
+ }
85
+ return (await response.json()) as T;
86
+ } catch (error) {
87
+ Sentry.captureException(error);
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /** Parses a JSON list from a harness CLI, or returns null and reports to Sentry when it isn't JSON or doesn't match. */
93
+ export function parseList<T>(
94
+ schema: z.ZodType<T, z.ZodTypeDef, unknown>,
95
+ list: string,
96
+ command: string,
97
+ ): T | null {
98
+ let result;
99
+ try {
100
+ result = schema.safeParse(JSON.parse(list));
101
+ } catch {
102
+ // A failing CLI can print nothing, which isn't JSON.
103
+ result = undefined;
104
+ }
105
+ if (!result?.success) {
106
+ Sentry.captureMessage(`Unexpected output from \`${command}\``, "error");
107
+ return null;
108
+ }
109
+ return result.data;
110
+ }
111
+
112
+ /** Pads each column to its widest cell and indents every row. */
113
+ export function formatRows(rows: string[][], indent = " "): string[] {
114
+ if (rows.length === 0) return [];
115
+ const widths = rows[0].map((_, i) =>
116
+ Math.max(...rows.map((row) => row[i].length)),
117
+ );
118
+ return rows.map((row) =>
119
+ `${indent}${row.map((cell, i) => cell.padEnd(widths[i])).join(" ")}`.trimEnd(),
120
+ );
121
+ }
@@ -45,6 +45,7 @@ export const CONVEX_DEPLOYMENT_ENV_VAR_NAME = "CONVEX_DEPLOYMENT";
45
45
  export const CONVEX_SELF_HOSTED_URL_VAR_NAME = "CONVEX_SELF_HOSTED_URL";
46
46
  export const CONVEX_SELF_HOSTED_ADMIN_KEY_VAR_NAME =
47
47
  "CONVEX_SELF_HOSTED_ADMIN_KEY";
48
+ export const IGNORED_DEPLOY_KEY = "<ignore_deploy_key>";
48
49
  const MAX_RETRIES = 6;
49
50
  // After 3 retries, log a progress message that we're retrying the request
50
51
  const RETRY_LOG_THRESHOLD = 3;
@@ -69,7 +70,7 @@ export async function processDeployKeyValue(
69
70
  }
70
71
 
71
72
  // Check for <ignore_deploy_key> sentinel
72
- if (deployKey === "<ignore_deploy_key>") {
73
+ if (deployKey === IGNORED_DEPLOY_KEY) {
73
74
  return undefined;
74
75
  }
75
76
 
@@ -96,9 +97,20 @@ export async function processDeployKeyValue(
96
97
  export function readDeployKeyFromEnv(
97
98
  getEnv: (name: string) => string | undefined | null,
98
99
  ): string | undefined {
99
- const fromDeployKey = getEnv(CONVEX_DEPLOY_KEY_ENV_VAR_NAME);
100
- const fromToken = getEnv(CONVEX_DEPLOYMENT_TOKEN_ENV_VAR_NAME);
101
- return fromDeployKey || fromToken || undefined;
100
+ return readDeployKeyEnvVar(getEnv)?.value;
101
+ }
102
+
103
+ export function readDeployKeyEnvVar(
104
+ getEnv: (name: string) => string | undefined | null,
105
+ ): { name: string; value: string } | undefined {
106
+ for (const name of [
107
+ CONVEX_DEPLOY_KEY_ENV_VAR_NAME,
108
+ CONVEX_DEPLOYMENT_TOKEN_ENV_VAR_NAME,
109
+ ]) {
110
+ const value = getEnv(name);
111
+ if (value) return { name, value };
112
+ }
113
+ return undefined;
102
114
  }
103
115
 
104
116
  // The prefix the backend's `require_operation` checks use when an identity
@@ -108,17 +120,9 @@ const PERMISSION_DENIED_MESSAGE_PREFIX =
108
120
  "You do not have permission to perform this operation";
109
121
 
110
122
  function deployKeyPermissionAdvice(): string | null {
111
- // Name whichever env var is actually set. `CONVEX_DEPLOY_KEY` takes
112
- // precedence over `CONVEX_DEPLOYMENT_TOKEN` when both are present.
113
- const envVarName = process.env[CONVEX_DEPLOY_KEY_ENV_VAR_NAME]
114
- ? CONVEX_DEPLOY_KEY_ENV_VAR_NAME
115
- : process.env[CONVEX_DEPLOYMENT_TOKEN_ENV_VAR_NAME]
116
- ? CONVEX_DEPLOYMENT_TOKEN_ENV_VAR_NAME
117
- : null;
118
- if (envVarName === null) {
119
- return null;
120
- }
121
- return `This is determined by the permissions granted to ${envVarName}.`;
123
+ const envVar = readDeployKeyEnvVar((name) => process.env[name]);
124
+ if (envVar === undefined || envVar.value === IGNORED_DEPLOY_KEY) return null;
125
+ return `This is determined by the permissions granted to ${envVar.name}.`;
122
126
  }
123
127
 
124
128
  /**