@robelest/convex-auth 0.0.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.
Files changed (280) hide show
  1. package/README.md +6 -0
  2. package/dist/bin.cjs +27733 -0
  3. package/dist/client/index.d.ts +49 -0
  4. package/dist/client/index.d.ts.map +1 -0
  5. package/dist/client/index.js +283 -0
  6. package/dist/client/index.js.map +1 -0
  7. package/dist/component/_generated/api.d.ts +36 -0
  8. package/dist/component/_generated/api.d.ts.map +1 -0
  9. package/dist/component/_generated/api.js +31 -0
  10. package/dist/component/_generated/api.js.map +1 -0
  11. package/dist/component/_generated/component.d.ts +295 -0
  12. package/dist/component/_generated/component.d.ts.map +1 -0
  13. package/dist/component/_generated/component.js +11 -0
  14. package/dist/component/_generated/component.js.map +1 -0
  15. package/dist/component/_generated/dataModel.d.ts +46 -0
  16. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  17. package/dist/component/_generated/dataModel.js +11 -0
  18. package/dist/component/_generated/dataModel.js.map +1 -0
  19. package/dist/component/_generated/server.d.ts +121 -0
  20. package/dist/component/_generated/server.d.ts.map +1 -0
  21. package/dist/component/_generated/server.js +78 -0
  22. package/dist/component/_generated/server.js.map +1 -0
  23. package/dist/component/convex.config.d.ts +3 -0
  24. package/dist/component/convex.config.d.ts.map +1 -0
  25. package/dist/component/convex.config.js +4 -0
  26. package/dist/component/convex.config.js.map +1 -0
  27. package/dist/component/index.d.ts +15 -0
  28. package/dist/component/index.d.ts.map +1 -0
  29. package/dist/component/index.js +13 -0
  30. package/dist/component/index.js.map +1 -0
  31. package/dist/component/public.d.ts +450 -0
  32. package/dist/component/public.d.ts.map +1 -0
  33. package/dist/component/public.js +528 -0
  34. package/dist/component/public.js.map +1 -0
  35. package/dist/component/schema.d.ts +107 -0
  36. package/dist/component/schema.d.ts.map +1 -0
  37. package/dist/component/schema.js +26 -0
  38. package/dist/component/schema.js.map +1 -0
  39. package/dist/providers/Anonymous.d.ts +50 -0
  40. package/dist/providers/Anonymous.d.ts.map +1 -0
  41. package/dist/providers/Anonymous.js +39 -0
  42. package/dist/providers/Anonymous.js.map +1 -0
  43. package/dist/providers/ConvexCredentials.d.ts +88 -0
  44. package/dist/providers/ConvexCredentials.d.ts.map +1 -0
  45. package/dist/providers/ConvexCredentials.js +37 -0
  46. package/dist/providers/ConvexCredentials.js.map +1 -0
  47. package/dist/providers/Email.d.ts +33 -0
  48. package/dist/providers/Email.d.ts.map +1 -0
  49. package/dist/providers/Email.js +50 -0
  50. package/dist/providers/Email.js.map +1 -0
  51. package/dist/providers/Password.d.ts +95 -0
  52. package/dist/providers/Password.d.ts.map +1 -0
  53. package/dist/providers/Password.js +174 -0
  54. package/dist/providers/Password.js.map +1 -0
  55. package/dist/providers/Phone.d.ts +22 -0
  56. package/dist/providers/Phone.d.ts.map +1 -0
  57. package/dist/providers/Phone.js +37 -0
  58. package/dist/providers/Phone.js.map +1 -0
  59. package/dist/server/convex_types.d.ts +17 -0
  60. package/dist/server/convex_types.d.ts.map +1 -0
  61. package/dist/server/convex_types.js +2 -0
  62. package/dist/server/convex_types.js.map +1 -0
  63. package/dist/server/cookies.d.ts +35 -0
  64. package/dist/server/cookies.d.ts.map +1 -0
  65. package/dist/server/cookies.js +34 -0
  66. package/dist/server/cookies.js.map +1 -0
  67. package/dist/server/implementation/db.d.ts +80 -0
  68. package/dist/server/implementation/db.d.ts.map +1 -0
  69. package/dist/server/implementation/db.js +59 -0
  70. package/dist/server/implementation/db.js.map +1 -0
  71. package/dist/server/implementation/index.d.ts +370 -0
  72. package/dist/server/implementation/index.d.ts.map +1 -0
  73. package/dist/server/implementation/index.js +521 -0
  74. package/dist/server/implementation/index.js.map +1 -0
  75. package/dist/server/implementation/mutations/createAccountFromCredentials.d.ts +33 -0
  76. package/dist/server/implementation/mutations/createAccountFromCredentials.d.ts.map +1 -0
  77. package/dist/server/implementation/mutations/createAccountFromCredentials.js +71 -0
  78. package/dist/server/implementation/mutations/createAccountFromCredentials.js.map +1 -0
  79. package/dist/server/implementation/mutations/createVerificationCode.d.ts +25 -0
  80. package/dist/server/implementation/mutations/createVerificationCode.d.ts.map +1 -0
  81. package/dist/server/implementation/mutations/createVerificationCode.js +84 -0
  82. package/dist/server/implementation/mutations/createVerificationCode.js.map +1 -0
  83. package/dist/server/implementation/mutations/index.d.ts +304 -0
  84. package/dist/server/implementation/mutations/index.d.ts.map +1 -0
  85. package/dist/server/implementation/mutations/index.js +108 -0
  86. package/dist/server/implementation/mutations/index.js.map +1 -0
  87. package/dist/server/implementation/mutations/invalidateSessions.d.ts +13 -0
  88. package/dist/server/implementation/mutations/invalidateSessions.d.ts.map +1 -0
  89. package/dist/server/implementation/mutations/invalidateSessions.js +35 -0
  90. package/dist/server/implementation/mutations/invalidateSessions.js.map +1 -0
  91. package/dist/server/implementation/mutations/modifyAccount.d.ts +23 -0
  92. package/dist/server/implementation/mutations/modifyAccount.d.ts.map +1 -0
  93. package/dist/server/implementation/mutations/modifyAccount.js +48 -0
  94. package/dist/server/implementation/mutations/modifyAccount.js.map +1 -0
  95. package/dist/server/implementation/mutations/refreshSession.d.ts +16 -0
  96. package/dist/server/implementation/mutations/refreshSession.d.ts.map +1 -0
  97. package/dist/server/implementation/mutations/refreshSession.js +116 -0
  98. package/dist/server/implementation/mutations/refreshSession.js.map +1 -0
  99. package/dist/server/implementation/mutations/retrieveAccountWithCredentials.d.ts +27 -0
  100. package/dist/server/implementation/mutations/retrieveAccountWithCredentials.d.ts.map +1 -0
  101. package/dist/server/implementation/mutations/retrieveAccountWithCredentials.js +55 -0
  102. package/dist/server/implementation/mutations/retrieveAccountWithCredentials.js.map +1 -0
  103. package/dist/server/implementation/mutations/signIn.d.ts +17 -0
  104. package/dist/server/implementation/mutations/signIn.d.ts.map +1 -0
  105. package/dist/server/implementation/mutations/signIn.js +26 -0
  106. package/dist/server/implementation/mutations/signIn.js.map +1 -0
  107. package/dist/server/implementation/mutations/signOut.d.ts +11 -0
  108. package/dist/server/implementation/mutations/signOut.d.ts.map +1 -0
  109. package/dist/server/implementation/mutations/signOut.js +24 -0
  110. package/dist/server/implementation/mutations/signOut.js.map +1 -0
  111. package/dist/server/implementation/mutations/userOAuth.d.ts +19 -0
  112. package/dist/server/implementation/mutations/userOAuth.d.ts.map +1 -0
  113. package/dist/server/implementation/mutations/userOAuth.js +84 -0
  114. package/dist/server/implementation/mutations/userOAuth.js.map +1 -0
  115. package/dist/server/implementation/mutations/verifier.d.ts +8 -0
  116. package/dist/server/implementation/mutations/verifier.d.ts.map +1 -0
  117. package/dist/server/implementation/mutations/verifier.js +19 -0
  118. package/dist/server/implementation/mutations/verifier.js.map +1 -0
  119. package/dist/server/implementation/mutations/verifierSignature.d.ts +15 -0
  120. package/dist/server/implementation/mutations/verifierSignature.d.ts.map +1 -0
  121. package/dist/server/implementation/mutations/verifierSignature.js +29 -0
  122. package/dist/server/implementation/mutations/verifierSignature.js.map +1 -0
  123. package/dist/server/implementation/mutations/verifyCodeAndSignIn.d.ts +21 -0
  124. package/dist/server/implementation/mutations/verifyCodeAndSignIn.d.ts.map +1 -0
  125. package/dist/server/implementation/mutations/verifyCodeAndSignIn.js +127 -0
  126. package/dist/server/implementation/mutations/verifyCodeAndSignIn.js.map +1 -0
  127. package/dist/server/implementation/provider.d.ts +6 -0
  128. package/dist/server/implementation/provider.d.ts.map +1 -0
  129. package/dist/server/implementation/provider.js +21 -0
  130. package/dist/server/implementation/provider.js.map +1 -0
  131. package/dist/server/implementation/rateLimit.d.ts +6 -0
  132. package/dist/server/implementation/rateLimit.d.ts.map +1 -0
  133. package/dist/server/implementation/rateLimit.js +76 -0
  134. package/dist/server/implementation/rateLimit.js.map +1 -0
  135. package/dist/server/implementation/redirects.d.ts +6 -0
  136. package/dist/server/implementation/redirects.d.ts.map +1 -0
  137. package/dist/server/implementation/redirects.js +40 -0
  138. package/dist/server/implementation/redirects.js.map +1 -0
  139. package/dist/server/implementation/refreshTokens.d.ts +40 -0
  140. package/dist/server/implementation/refreshTokens.d.ts.map +1 -0
  141. package/dist/server/implementation/refreshTokens.js +160 -0
  142. package/dist/server/implementation/refreshTokens.js.map +1 -0
  143. package/dist/server/implementation/sessions.d.ts +43 -0
  144. package/dist/server/implementation/sessions.d.ts.map +1 -0
  145. package/dist/server/implementation/sessions.js +94 -0
  146. package/dist/server/implementation/sessions.js.map +1 -0
  147. package/dist/server/implementation/signIn.d.ts +31 -0
  148. package/dist/server/implementation/signIn.d.ts.map +1 -0
  149. package/dist/server/implementation/signIn.js +148 -0
  150. package/dist/server/implementation/signIn.js.map +1 -0
  151. package/dist/server/implementation/tokens.d.ts +7 -0
  152. package/dist/server/implementation/tokens.d.ts.map +1 -0
  153. package/dist/server/implementation/tokens.js +18 -0
  154. package/dist/server/implementation/tokens.js.map +1 -0
  155. package/dist/server/implementation/types.d.ts +288 -0
  156. package/dist/server/implementation/types.d.ts.map +1 -0
  157. package/dist/server/implementation/types.js +182 -0
  158. package/dist/server/implementation/types.js.map +1 -0
  159. package/dist/server/implementation/users.d.ts +27 -0
  160. package/dist/server/implementation/users.d.ts.map +1 -0
  161. package/dist/server/implementation/users.js +181 -0
  162. package/dist/server/implementation/users.js.map +1 -0
  163. package/dist/server/implementation/utils.d.ts +17 -0
  164. package/dist/server/implementation/utils.d.ts.map +1 -0
  165. package/dist/server/implementation/utils.js +72 -0
  166. package/dist/server/implementation/utils.js.map +1 -0
  167. package/dist/server/index.d.ts +17 -0
  168. package/dist/server/index.d.ts.map +1 -0
  169. package/dist/server/index.js +54 -0
  170. package/dist/server/index.js.map +1 -0
  171. package/dist/server/oauth/authorizationUrl.d.ts +13 -0
  172. package/dist/server/oauth/authorizationUrl.d.ts.map +1 -0
  173. package/dist/server/oauth/authorizationUrl.js +91 -0
  174. package/dist/server/oauth/authorizationUrl.js.map +1 -0
  175. package/dist/server/oauth/callback.d.ts +19 -0
  176. package/dist/server/oauth/callback.d.ts.map +1 -0
  177. package/dist/server/oauth/callback.js +173 -0
  178. package/dist/server/oauth/callback.js.map +1 -0
  179. package/dist/server/oauth/checks.d.ts +52 -0
  180. package/dist/server/oauth/checks.d.ts.map +1 -0
  181. package/dist/server/oauth/checks.js +106 -0
  182. package/dist/server/oauth/checks.js.map +1 -0
  183. package/dist/server/oauth/convexAuth.d.ts +12 -0
  184. package/dist/server/oauth/convexAuth.d.ts.map +1 -0
  185. package/dist/server/oauth/convexAuth.js +137 -0
  186. package/dist/server/oauth/convexAuth.js.map +1 -0
  187. package/dist/server/oauth/lib/utils/customFetch.d.ts +9 -0
  188. package/dist/server/oauth/lib/utils/customFetch.d.ts.map +1 -0
  189. package/dist/server/oauth/lib/utils/customFetch.js +11 -0
  190. package/dist/server/oauth/lib/utils/customFetch.js.map +1 -0
  191. package/dist/server/oauth/lib/utils/providers.d.ts +3 -0
  192. package/dist/server/oauth/lib/utils/providers.d.ts.map +1 -0
  193. package/dist/server/oauth/lib/utils/providers.js +7 -0
  194. package/dist/server/oauth/lib/utils/providers.js.map +1 -0
  195. package/dist/server/oauth/providers/oauth.d.ts +43 -0
  196. package/dist/server/oauth/providers/oauth.d.ts.map +1 -0
  197. package/dist/server/oauth/providers/oauth.js +3 -0
  198. package/dist/server/oauth/providers/oauth.js.map +1 -0
  199. package/dist/server/oauth/types.d.ts +24 -0
  200. package/dist/server/oauth/types.d.ts.map +1 -0
  201. package/dist/server/oauth/types.js +5 -0
  202. package/dist/server/oauth/types.js.map +1 -0
  203. package/dist/server/provider_utils.d.ts +76 -0
  204. package/dist/server/provider_utils.d.ts.map +1 -0
  205. package/dist/server/provider_utils.js +177 -0
  206. package/dist/server/provider_utils.js.map +1 -0
  207. package/dist/server/types.d.ts +412 -0
  208. package/dist/server/types.d.ts.map +1 -0
  209. package/dist/server/types.js +2 -0
  210. package/dist/server/types.js.map +1 -0
  211. package/dist/server/utils.d.ts +3 -0
  212. package/dist/server/utils.d.ts.map +1 -0
  213. package/dist/server/utils.js +11 -0
  214. package/dist/server/utils.js.map +1 -0
  215. package/package.json +126 -0
  216. package/providers/Anonymous/package.json +6 -0
  217. package/providers/ConvexCredentials/package.json +6 -0
  218. package/providers/Email/package.json +6 -0
  219. package/providers/Password/package.json +6 -0
  220. package/providers/Phone/package.json +6 -0
  221. package/server/package.json +6 -0
  222. package/src/cli/command.ts +69 -0
  223. package/src/cli/generateKeys.ts +20 -0
  224. package/src/cli/index.ts +840 -0
  225. package/src/client/index.ts +415 -0
  226. package/src/component/_generated/api.ts +52 -0
  227. package/src/component/_generated/component.ts +586 -0
  228. package/src/component/_generated/dataModel.ts +60 -0
  229. package/src/component/_generated/server.ts +156 -0
  230. package/src/component/convex.config.ts +5 -0
  231. package/src/component/index.ts +40 -0
  232. package/src/component/public.ts +607 -0
  233. package/src/component/schema.ts +35 -0
  234. package/src/providers/Anonymous.ts +79 -0
  235. package/src/providers/ConvexCredentials.ts +108 -0
  236. package/src/providers/Email.ts +60 -0
  237. package/src/providers/Password.ts +253 -0
  238. package/src/providers/Phone.ts +46 -0
  239. package/src/server/convex_types.ts +55 -0
  240. package/src/server/cookies.ts +42 -0
  241. package/src/server/implementation/db.ts +125 -0
  242. package/src/server/implementation/index.ts +815 -0
  243. package/src/server/implementation/mutations/createAccountFromCredentials.ts +113 -0
  244. package/src/server/implementation/mutations/createVerificationCode.ts +139 -0
  245. package/src/server/implementation/mutations/index.ts +157 -0
  246. package/src/server/implementation/mutations/invalidateSessions.ts +47 -0
  247. package/src/server/implementation/mutations/modifyAccount.ts +65 -0
  248. package/src/server/implementation/mutations/refreshSession.ts +188 -0
  249. package/src/server/implementation/mutations/retrieveAccountWithCredentials.ts +87 -0
  250. package/src/server/implementation/mutations/signIn.ts +51 -0
  251. package/src/server/implementation/mutations/signOut.ts +38 -0
  252. package/src/server/implementation/mutations/userOAuth.ts +112 -0
  253. package/src/server/implementation/mutations/verifier.ts +29 -0
  254. package/src/server/implementation/mutations/verifierSignature.ts +44 -0
  255. package/src/server/implementation/mutations/verifyCodeAndSignIn.ts +205 -0
  256. package/src/server/implementation/provider.ts +38 -0
  257. package/src/server/implementation/rateLimit.ts +105 -0
  258. package/src/server/implementation/redirects.ts +58 -0
  259. package/src/server/implementation/refreshTokens.ts +221 -0
  260. package/src/server/implementation/sessions.ts +155 -0
  261. package/src/server/implementation/signIn.ts +253 -0
  262. package/src/server/implementation/tokens.ts +29 -0
  263. package/src/server/implementation/types.ts +220 -0
  264. package/src/server/implementation/users.ts +286 -0
  265. package/src/server/implementation/utils.ts +91 -0
  266. package/src/server/index.ts +74 -0
  267. package/src/server/oauth/NOTICE.txt +21 -0
  268. package/src/server/oauth/README.md +7 -0
  269. package/src/server/oauth/authorizationUrl.ts +113 -0
  270. package/src/server/oauth/callback.ts +243 -0
  271. package/src/server/oauth/checks.ts +136 -0
  272. package/src/server/oauth/convexAuth.ts +168 -0
  273. package/src/server/oauth/lib/utils/customFetch.ts +18 -0
  274. package/src/server/oauth/lib/utils/providers.ts +12 -0
  275. package/src/server/oauth/providers/oauth.ts +56 -0
  276. package/src/server/oauth/types.ts +60 -0
  277. package/src/server/provider_utils.ts +222 -0
  278. package/src/server/types.ts +470 -0
  279. package/src/server/utils.ts +12 -0
  280. package/src/test.ts +24 -0
@@ -0,0 +1,840 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "@commander-js/extra-typings";
4
+ import chalk from "chalk";
5
+ import { execSync } from "child_process";
6
+ import { config as loadEnvFile } from "dotenv";
7
+ import { existsSync, readFileSync, writeFileSync } from "fs";
8
+ import inquirer from "inquirer";
9
+ import path from "path";
10
+ import * as v from "valibot";
11
+ import { actionDescription } from "./command.js";
12
+ import { generateKeys } from "./generateKeys.js";
13
+
14
+ new Command()
15
+ .name("@robelest/convex-auth")
16
+ .description(
17
+ "Add code and set environment variables for @robelest/convex-auth.\n\n" +
18
+ "The steps are detailed here: https://labs.convex.dev/auth/setup/manual",
19
+ )
20
+ .option(
21
+ "--variables <json>",
22
+ "Configure additional variables for interactive configuration.",
23
+ )
24
+ .option("--skip-git-check", "Don't warn when running outside a Git checkout.")
25
+ .option("--allow-dirty-git-state", "Don't warn when Git state is not clean.")
26
+ .option(
27
+ "--web-server-url <url>",
28
+ "URL of web server, e.g. 'http://localhost:5173' if local",
29
+ )
30
+ .addDeploymentSelectionOptions(
31
+ actionDescription("Set environment variables on"),
32
+ )
33
+ .action(async (options) => {
34
+ await checkSourceControl(options);
35
+
36
+ const packageJson = readPackageJson();
37
+ const convexJson = readConvexJson();
38
+ const deployment = readConvexDeployment(options);
39
+ const convexFolderPath = convexJson.functions ?? "convex";
40
+
41
+ const isNextjs = !!packageJson.dependencies?.next;
42
+ const usesTypeScript = !!(
43
+ packageJson.dependencies?.typescript ||
44
+ packageJson.devDependencies?.typescript
45
+ );
46
+ const isVite = !!(
47
+ packageJson.dependencies?.vite || packageJson.devDependencies?.vite
48
+ );
49
+ const isExpo = !!(
50
+ packageJson.dependencies?.expo || packageJson.devDependencies?.expo
51
+ );
52
+ const config = {
53
+ isNextjs,
54
+ isVite,
55
+ isExpo,
56
+ usesTypeScript,
57
+ convexFolderPath,
58
+ deployment,
59
+ step: 1,
60
+ };
61
+
62
+ // Step 1: Configure SITE_URL
63
+ // We check for existing config.
64
+ // We default to localhost and port depending on framework
65
+ await configureSiteUrl(config, options.webServerUrl);
66
+
67
+ // Step 2: Configure private and public key
68
+ // We ask if we would overwrite existing keys
69
+ await configureKeys(config);
70
+
71
+ // Step 3: Change moduleResolution to "bundler"
72
+ // and turn on skipLibCheck
73
+ // Skipped if there's no tsconfig.json
74
+ await modifyTsConfig(config);
75
+
76
+ // Step 4: Configure auth.config.ts
77
+ // To avoid having to execute it we just give instructions
78
+ // if it exists already.
79
+ await configureAuthConfig(config);
80
+
81
+ // Step 5: Initialize auth.ts
82
+ // We do nothing if the file already contains the code,
83
+ // and give instructions otherwise
84
+ await initializeAuth(config);
85
+
86
+ // Step 6: Configure http.ts
87
+ // We do nothing if the file already contains the code,
88
+ // and give instructions otherwise
89
+ await configureHttp(config);
90
+
91
+ // Extra: Configure providers interactively.
92
+ if (options.variables !== undefined) {
93
+ await configureOtherVariables(config, options.variables);
94
+ } else {
95
+ logSuccess(
96
+ "You're all set. Continue by configuring your schema and frontend.",
97
+ );
98
+ }
99
+ })
100
+ .parse(process.argv);
101
+
102
+ type ProjectConfig = {
103
+ isExpo: boolean;
104
+ isNextjs: boolean;
105
+ isVite: boolean;
106
+ usesTypeScript: boolean;
107
+ convexFolderPath: string;
108
+ deployment: {
109
+ name: string | null;
110
+ type: string | null;
111
+ options: {
112
+ url?: string;
113
+ adminKey?: string;
114
+ prod?: boolean;
115
+ previewName?: string;
116
+ deploymentName?: string;
117
+ };
118
+ };
119
+ // Mutated along the way
120
+ step: number;
121
+ };
122
+
123
+ async function configureSiteUrl(config: ProjectConfig, forcedValue?: string) {
124
+ logStep(config, "Configure SITE_URL");
125
+ if (config.isExpo) {
126
+ logInfo("React Native projects don't require a SITE_URL.");
127
+ return;
128
+ }
129
+
130
+ // Default to localhost for dev and also for local backend
131
+ // this is not perfect but OK since it's just the default.
132
+ const value =
133
+ config.deployment.type === "dev" || config.deployment.type === null
134
+ ? config.isVite
135
+ ? "http://localhost:5173"
136
+ : "http://localhost:3000"
137
+ : undefined;
138
+ const description =
139
+ config.deployment.type === "dev"
140
+ ? "the URL of your local web server (e.g. http://localhost:1234)"
141
+ : "the URL where your site is hosted (e.g. https://example.com)";
142
+
143
+ await configureEnvVar(config, {
144
+ name: "SITE_URL",
145
+ default: value,
146
+ description,
147
+ validate: (input) => {
148
+ try {
149
+ new URL(input);
150
+ return true;
151
+ } catch {
152
+ return "The URL must start with http:// or https://";
153
+ }
154
+ },
155
+ forcedValue,
156
+ });
157
+ }
158
+
159
+ async function configureEnvVar(
160
+ config: ProjectConfig,
161
+ variable: {
162
+ name: string;
163
+ default?: string;
164
+ description: string;
165
+ validate?: (input: string) => true | string;
166
+ forcedValue?: string;
167
+ },
168
+ ) {
169
+ if (
170
+ variable.forcedValue &&
171
+ (variable.validate ? variable.validate(variable.forcedValue) : true)
172
+ ) {
173
+ await setEnvVar(config, variable.name, variable.forcedValue);
174
+ return;
175
+ }
176
+ const existing = await backendEnvVar(config, variable.name);
177
+ if (existing !== "") {
178
+ if (
179
+ !(await promptForConfirmation(
180
+ `The ${printDeployment(config)} already has ${variable.name} configured to ${chalk.bold(existing)}. Do you want to change it?`,
181
+ { default: false },
182
+ ))
183
+ ) {
184
+ return;
185
+ }
186
+ }
187
+ const chosenValue = await promptForInput(`Enter ${variable.description}`, {
188
+ default: variable.default,
189
+ validate: variable.validate,
190
+ });
191
+ await setEnvVar(config, variable.name, chosenValue);
192
+ }
193
+
194
+ async function configureKeys(config: ProjectConfig) {
195
+ logStep(config, "Configure private and public key");
196
+ const { JWT_PRIVATE_KEY, JWKS } = await generateKeys();
197
+ // TODO: We should just list all the 3 env vars in one command
198
+ // to speed things up, but the convex CLI doesn't quote the
199
+ // values correctly right now, so we can't.
200
+ const existingPrivateKey = await backendEnvVar(config, "JWT_PRIVATE_KEY");
201
+ const existingJwks = await backendEnvVar(config, "JWKS");
202
+ if (existingPrivateKey !== "" || existingJwks !== "") {
203
+ if (
204
+ !(await promptForConfirmation(
205
+ `The ${printDeployment(config)} already has JWT_PRIVATE_KEY or JWKS configured. Overwrite them?`,
206
+ { default: false },
207
+ ))
208
+ ) {
209
+ return;
210
+ }
211
+ }
212
+ // TODO: We should set both env vars in one command, but the convex CLI doesn't
213
+ // support setting multiple env vars.
214
+ await setEnvVar(config, "JWT_PRIVATE_KEY", JWT_PRIVATE_KEY, {
215
+ hideValue: true,
216
+ });
217
+ await setEnvVar(config, "JWKS", JWKS, { hideValue: true });
218
+ }
219
+
220
+ async function backendEnvVar(config: ProjectConfig, name: string) {
221
+ return (
222
+ execSync(`npx convex env get ${deploymentOptions(config)} ${name}`, {
223
+ stdio: "pipe",
224
+ })
225
+ .toString()
226
+ // Remove trailing newline
227
+ .slice(0, -1)
228
+ );
229
+ }
230
+
231
+ async function setEnvVar(
232
+ config: ProjectConfig,
233
+ name: string,
234
+ value: string,
235
+ options?: { hideValue: boolean },
236
+ ) {
237
+ const valueEscaped = value.replace(/"/g, '\\"');
238
+ execSync(
239
+ `npx convex env set ${deploymentOptions(config)} -- ${name} "${valueEscaped}"`,
240
+ {
241
+ stdio: options?.hideValue ? "ignore" : "inherit",
242
+ },
243
+ );
244
+ if (options?.hideValue) {
245
+ logSuccess(
246
+ `Successfully set ${chalk.bold(name)} (on ${printDeployment(config)})`,
247
+ );
248
+ }
249
+ }
250
+
251
+ function deploymentOptions(config: ProjectConfig) {
252
+ const {
253
+ deployment: {
254
+ options: { adminKey },
255
+ },
256
+ } = config;
257
+ const adminKeyOption =
258
+ adminKey !== undefined ? `--admin-key '${adminKey}' ` : "";
259
+ return adminKeyOption + deploymentNameOptions(config);
260
+ }
261
+
262
+ function deploymentNameOptions(config: ProjectConfig) {
263
+ const {
264
+ deployment: {
265
+ options: { url, prod, previewName, deploymentName },
266
+ },
267
+ } = config;
268
+ if (url) {
269
+ return `--url ${url}`;
270
+ } else if (prod) {
271
+ return "--prod";
272
+ } else if (previewName) {
273
+ return `--preview-name ${previewName}`;
274
+ } else if (deploymentName) {
275
+ return `--deployment-name ${deploymentName}`;
276
+ } else {
277
+ return "";
278
+ }
279
+ }
280
+
281
+ function printDeployment(config: ProjectConfig) {
282
+ const { name, type } = config.deployment;
283
+ return (
284
+ (type !== null ? `${chalk.bold(type)} ` : "") +
285
+ "deployment" +
286
+ (name !== null ? ` ${chalk.bold(name)}` : "")
287
+ );
288
+ }
289
+
290
+ // Match `"compilerOptions": {"`
291
+ // ignore comments after the bracket
292
+ // and capture the space between the bracket/last comment
293
+ // and the quote.
294
+ const compilerOptionsPattern =
295
+ /("compilerOptions"\s*:\s*\{(?:\s*(?:\/\*(?:[^*]|\*(?!\/))*\*\/))*(\s*))(?=")/;
296
+
297
+ const validTsConfig = `\
298
+ {
299
+ /* This TypeScript project config describes the environment that
300
+ * Convex functions run in and is used to typecheck them.
301
+ * You can modify it, but some settings required to use Convex.
302
+ */
303
+ "compilerOptions": {
304
+ /* These settings are not required by Convex and can be modified. */
305
+ "allowJs": true,
306
+ "strict": true,
307
+ "skipLibCheck": true,
308
+ "jsx": "react",
309
+
310
+ /* These compiler options are required by Convex */
311
+ "target": "ESNext",
312
+ "lib": ["ES2021", "dom", "ES2023.Array"],
313
+ "forceConsistentCasingInFileNames": true,
314
+ "allowSyntheticDefaultImports": true,
315
+ "module": "ESNext",
316
+ "moduleResolution": "Bundler",
317
+ "isolatedModules": true,
318
+ "noEmit": true
319
+ },
320
+ "include": ["./**/*"],
321
+ "exclude": ["./_generated"]
322
+ }
323
+ `;
324
+
325
+ async function modifyTsConfig(config: ProjectConfig) {
326
+ logStep(config, "Modify tsconfig file");
327
+ const projectLevelTsConfigPath = "tsconfig.json";
328
+ const tsConfigPath = path.join(config.convexFolderPath, "tsconfig.json");
329
+ if (!existsSync(tsConfigPath)) {
330
+ if (existsSync(projectLevelTsConfigPath)) {
331
+ if (config.isExpo) {
332
+ writeFileSync(tsConfigPath, validTsConfig);
333
+ logSuccess(`Added ${chalk.bold(tsConfigPath)}`);
334
+ return;
335
+ }
336
+ // else assume that the project-level tsconfig already
337
+ // has the right settings, which is true for Vite and Next.js
338
+ }
339
+ logInfo(`No ${chalk.bold(tsConfigPath)} found. Skipping.`);
340
+ return;
341
+ }
342
+ const existingTsConfig = readFileSync(tsConfigPath, "utf8");
343
+ const moduleResolutionPattern = /"moduleResolution"\s*:\s*"(\w+)"/;
344
+ const [, existingModuleResolution] =
345
+ existingTsConfig.match(moduleResolutionPattern) ?? [];
346
+ const skipLibCheckPattern = /"skipLibCheck"\s*:\s*(\w+)/;
347
+ const [, existingSkipLibCheck] =
348
+ existingTsConfig.match(skipLibCheckPattern) ?? [];
349
+ if (
350
+ /Bundler/i.test(existingModuleResolution) &&
351
+ existingSkipLibCheck === "true"
352
+ ) {
353
+ logSuccess(`The ${chalk.bold(tsConfigPath)} is already set up.`);
354
+ return;
355
+ }
356
+
357
+ if (!compilerOptionsPattern.test(existingTsConfig)) {
358
+ logInfo(
359
+ `Modify your ${chalk.bold(tsConfigPath)} to include the following:`,
360
+ );
361
+ const source = `\
362
+ {
363
+ "compilerOptions": {
364
+ "moduleResolution": "Bundler",
365
+ "skipLibCheck": true
366
+ }
367
+ }
368
+ `;
369
+ print(indent(`\n${source}\n`));
370
+ await promptForConfirmationOrExit("Ready to continue?");
371
+ }
372
+ const changedTsConfig = addCompilerOption(
373
+ addCompilerOption(
374
+ existingTsConfig,
375
+ existingModuleResolution,
376
+ moduleResolutionPattern,
377
+ '"moduleResolution": "Bundler"',
378
+ ),
379
+ existingSkipLibCheck,
380
+ skipLibCheckPattern,
381
+ '"skipLibCheck": true',
382
+ );
383
+ writeFileSync(tsConfigPath, changedTsConfig);
384
+ logSuccess(`Modified ${chalk.bold(tsConfigPath)}`);
385
+ }
386
+
387
+ function addCompilerOption(
388
+ tsconfig: string,
389
+ existingValue: string | undefined,
390
+ pattern: RegExp,
391
+ optionAndValue: string,
392
+ ) {
393
+ if (existingValue === undefined) {
394
+ return tsconfig.replace(compilerOptionsPattern, `$1${optionAndValue},$2`);
395
+ } else {
396
+ return tsconfig.replace(pattern, optionAndValue);
397
+ }
398
+ }
399
+
400
+ async function configureAuthConfig(config: ProjectConfig) {
401
+ logStep(config, "Configure auth config file");
402
+ const sourceTemplate = `\
403
+ export default {
404
+ providers: [$$
405
+ {
406
+ domain: process.env.CONVEX_SITE_URL,
407
+ applicationID: "convex",
408
+ },$$
409
+ ],
410
+ };
411
+ `;
412
+ const source = templateToSource(sourceTemplate);
413
+ const authConfigPath = path.join(config.convexFolderPath, "auth.config");
414
+ const existingConfigPath = await existingNonEmptySourcePath(authConfigPath);
415
+ if (existingConfigPath !== null) {
416
+ const existingConfig = readFileSync(existingConfigPath, "utf8");
417
+ if (doesAlreadyMatchTemplate(existingConfig, sourceTemplate)) {
418
+ logSuccess(`The ${chalk.bold(existingConfigPath)} is already set up.`);
419
+ } else {
420
+ logInfo(
421
+ `You already have a ${chalk.bold(existingConfigPath)}, make sure the \`providers\` include the following config:`,
422
+ );
423
+ print(indent(`\n${source}\n`));
424
+ await promptForConfirmationOrExit("Ready to continue?");
425
+ }
426
+ } else {
427
+ const newConfigPath = config.usesTypeScript
428
+ ? `${authConfigPath}.ts`
429
+ : `${authConfigPath}.js`;
430
+ writeFileSync(newConfigPath, source);
431
+ logSuccess(`Created ${chalk.bold(newConfigPath)}`);
432
+ }
433
+ }
434
+
435
+ async function initializeAuth(config: ProjectConfig) {
436
+ logStep(config, "Initialize auth file");
437
+ const sourceTemplate = `\
438
+ import { Auth } from "@robelest/convex-auth/component";
439
+
440
+ export const { auth, signIn, signOut, store } = Auth({$$
441
+ providers: [$$],$$
442
+ });
443
+ `;
444
+ const source = templateToSource(sourceTemplate);
445
+ const authPath = path.join(config.convexFolderPath, "auth");
446
+ const existingAuthPath = await existingNonEmptySourcePath(authPath);
447
+ if (existingAuthPath !== null) {
448
+ const existingAuth = readFileSync(existingAuthPath, "utf8");
449
+ if (doesAlreadyMatchTemplate(existingAuth, sourceTemplate)) {
450
+ logSuccess(`The ${chalk.bold(existingAuthPath)} is already set up.`);
451
+ } else {
452
+ logInfo(
453
+ `You already have a ${chalk.bold(existingAuthPath)}, make sure it initializes \`Auth\` like this:`,
454
+ );
455
+ print(indent(`\n${source}\n`));
456
+ await promptForConfirmationOrExit("Ready to continue?");
457
+ }
458
+ } else {
459
+ const newAuthPath = config.usesTypeScript
460
+ ? `${authPath}.ts`
461
+ : `${authPath}.js`;
462
+ writeFileSync(newAuthPath, source);
463
+ logSuccess(`Created ${chalk.bold(newAuthPath)}`);
464
+ }
465
+ }
466
+
467
+ async function configureHttp(config: ProjectConfig) {
468
+ logStep(config, "Configure http file");
469
+ const sourceTemplate = `\
470
+ import { httpRouter } from "convex/server";
471
+ import { auth } from "./auth";
472
+
473
+ const http = httpRouter();
474
+
475
+ auth.addHttpRoutes(http);
476
+
477
+ export default http;
478
+ `;
479
+ const source = templateToSource(sourceTemplate);
480
+ const httpPath = path.join(config.convexFolderPath, "http");
481
+ const existingHttpPath = await existingNonEmptySourcePath(httpPath);
482
+ if (existingHttpPath !== null) {
483
+ const existingHttp = readFileSync(existingHttpPath, "utf8");
484
+ if (doesAlreadyMatchTemplate(existingHttp, sourceTemplate)) {
485
+ logSuccess(`The ${chalk.bold(existingHttpPath)} is already set up.`);
486
+ } else {
487
+ logInfo(
488
+ `You already have a ${chalk.bold(existingHttpPath)}, make sure it includes the call to \`auth.addHttpRoutes\`:`,
489
+ );
490
+ print(indent(`\n${source}\n`));
491
+ await promptForConfirmationOrExit("Ready to continue?");
492
+ }
493
+ } else {
494
+ const newHttpPath = config.usesTypeScript
495
+ ? `${httpPath}.ts`
496
+ : `${httpPath}.js`;
497
+ writeFileSync(newHttpPath, source);
498
+ logSuccess(`Created ${chalk.bold(newHttpPath)}`);
499
+ }
500
+ }
501
+
502
+ const VariablesSchema = v.object({
503
+ help: v.optional(v.string()),
504
+ providers: v.array(
505
+ v.object({
506
+ name: v.string(),
507
+ help: v.optional(v.string()),
508
+ variables: v.array(
509
+ v.object({
510
+ name: v.string(),
511
+ description: v.string(),
512
+ }),
513
+ ),
514
+ }),
515
+ ),
516
+ success: v.optional(v.string()),
517
+ });
518
+
519
+ async function configureOtherVariables(config: ProjectConfig, json: string) {
520
+ const variables = v.parse(VariablesSchema, JSON.parse(json));
521
+ logStep(config, "Configure extra environment variables");
522
+ // Ex: The default setup includes sign-in with GitHub OAuth
523
+ // and sending magic links via Resend.
524
+ if (variables.help !== undefined) {
525
+ print(variables.help);
526
+ }
527
+ for (const provider of variables.providers) {
528
+ if (
529
+ !(await promptForConfirmation(
530
+ `Do you want to configure ${provider.name}?`,
531
+ ))
532
+ ) {
533
+ continue;
534
+ }
535
+ if (provider.help !== undefined) {
536
+ print(provider.help);
537
+ }
538
+ for (const variable of provider.variables) {
539
+ await configureEnvVar(config, {
540
+ name: variable.name,
541
+ description: variable.description,
542
+ });
543
+ }
544
+ }
545
+ if (variables.success !== undefined) {
546
+ logSuccess(variables.success);
547
+ }
548
+ }
549
+
550
+ function doesAlreadyMatchTemplate(existing: string, template: string) {
551
+ const regex = new RegExp(
552
+ template
553
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
554
+ .replace(/\\\$\\\$/g, ".*")
555
+ .replace(/;\n/g, ";.*"),
556
+ "s",
557
+ );
558
+ return regex.test(existing);
559
+ }
560
+
561
+ function templateToSource(template: string) {
562
+ return template.replace(/\$\$/g, "");
563
+ }
564
+
565
+ async function existingNonEmptySourcePath(path: string) {
566
+ return (await existsAndNotEmpty(`${path}.ts`))
567
+ ? `${path}.ts`
568
+ : (await existsAndNotEmpty(`${path}.js`))
569
+ ? `${path}.js`
570
+ : null;
571
+ }
572
+
573
+ async function existsAndNotEmpty(path: string) {
574
+ return existsSync(path) && readFileSync(path, "utf8").trim() !== "";
575
+ }
576
+
577
+ function logStep(config: ProjectConfig, message: string) {
578
+ if (config.step > 1) {
579
+ print();
580
+ }
581
+ logInfo(chalk.bold(`Step ${config.step++}: ${message}`));
582
+ }
583
+
584
+ async function checkSourceControl(options: {
585
+ skipGitCheck?: boolean;
586
+ allowDirtyGitState?: boolean;
587
+ }) {
588
+ if (options.allowDirtyGitState) {
589
+ return;
590
+ }
591
+ const isGit = existsSync(".git");
592
+ if (isGit) {
593
+ const gitStatus = execSync("git status --porcelain").toString();
594
+ const changedFiles = gitStatus
595
+ .split("\n")
596
+ .filter(
597
+ (line) => !/\bpackage(-lock)?.json/.test(line) && line.length > 0,
598
+ );
599
+ if (changedFiles.length > 0) {
600
+ logError(
601
+ "There are unstaged or uncommitted changes in the working directory. " +
602
+ "Please commit or stash them before proceeding.",
603
+ );
604
+ await promptForConfirmationOrExit("Continue anyway?", { default: false });
605
+ }
606
+ } else {
607
+ if (options.skipGitCheck) {
608
+ return;
609
+ }
610
+ logWarning(
611
+ "No source control detected. We strongly recommend committing the current state of your code before proceeding.",
612
+ );
613
+ await promptForConfirmationOrExit("Continue anyway?");
614
+ }
615
+ }
616
+
617
+ type PackageJSON = { __isPackageJSON: true; [key: string]: any };
618
+
619
+ function readPackageJson(): PackageJSON {
620
+ try {
621
+ const data = readFileSync("package.json", "utf8");
622
+ return JSON.parse(data);
623
+ } catch (error: any) {
624
+ logErrorAndExit(
625
+ "`@robelest/convex-auth` must be run from a project directory which " +
626
+ 'includes a valid "package.json" file. You can create one by running ' +
627
+ "`npm init`.",
628
+ error.message,
629
+ );
630
+ }
631
+ }
632
+
633
+ type ConvexJSON = { __isConvexJSON: true; [key: string]: any };
634
+
635
+ function readConvexJson(): ConvexJSON {
636
+ if (!existsSync("convex.json")) {
637
+ return {} as ConvexJSON;
638
+ }
639
+ try {
640
+ const data = readFileSync("convex.json", "utf8");
641
+ return JSON.parse(data);
642
+ } catch (error: any) {
643
+ logErrorAndExit(
644
+ "Could not parse your convex.json. Is it valid JSON?",
645
+ error.message,
646
+ );
647
+ }
648
+ }
649
+
650
+ function readConvexDeployment(options: {
651
+ url?: string;
652
+ adminKey?: string;
653
+ prod?: boolean;
654
+ previewName?: string;
655
+ deploymentName?: string;
656
+ }) {
657
+ const { adminKey, url, prod, previewName, deploymentName } = options;
658
+ const adminKeyName = adminKey ? deploymentNameFromAdminKey(adminKey) : null;
659
+ const adminKeyType = adminKey ? deploymentTypeFromAdminKey(adminKey) : null;
660
+ if (url) {
661
+ return { name: adminKeyName ?? url, type: adminKeyType, options };
662
+ } else if (prod) {
663
+ return { name: adminKeyName, type: "prod", options };
664
+ }
665
+ if (previewName) {
666
+ return { name: previewName, type: "preview", options };
667
+ }
668
+ if (deploymentName) {
669
+ return { name: deploymentName, type: adminKeyType, options };
670
+ }
671
+ if (adminKey) {
672
+ return { name: adminKeyName, type: adminKeyType, options };
673
+ }
674
+ loadEnvFile({ path: ".env.local" });
675
+ loadEnvFile();
676
+ if (process.env.CONVEX_DEPLOYMENT) {
677
+ return {
678
+ name: stripDeploymentTypePrefix(process.env.CONVEX_DEPLOYMENT),
679
+ type: getDeploymentTypeFromConfiguredDeployment(
680
+ process.env.CONVEX_DEPLOYMENT,
681
+ ),
682
+ options,
683
+ };
684
+ }
685
+
686
+ logErrorAndExit(
687
+ "Could not find a configured CONVEX_DEPLOYMENT. Did you forget to run `npx convex dev` first?",
688
+ );
689
+ }
690
+
691
+ // NOTE: CONVEX CLI DEP
692
+ // Given a deployment string like "dev:tall-forest-1234"
693
+ // returns only the slug "tall-forest-1234".
694
+ // If there's no prefix returns the original string.
695
+ export function stripDeploymentTypePrefix(deployment: string) {
696
+ return deployment.split(":").at(-1)!;
697
+ }
698
+
699
+ // NOTE: CONVEX CLI DEP
700
+ // Handling legacy CONVEX_DEPLOYMENT without type prefix as well
701
+ function getDeploymentTypeFromConfiguredDeployment(raw: string) {
702
+ const typeRaw = raw.split(":")[0];
703
+ const type =
704
+ typeRaw === "prod" || typeRaw === "dev" || typeRaw === "preview"
705
+ ? typeRaw
706
+ : null;
707
+ return type;
708
+ }
709
+
710
+ // NOTE: CONVEX CLI DEP
711
+ function deploymentNameFromAdminKey(adminKey: string) {
712
+ const parts = adminKey.split("|");
713
+ if (parts.length === 1) {
714
+ return null;
715
+ }
716
+ if (isPreviewDeployKey(adminKey)) {
717
+ // Preview deploy keys do not contain a deployment name.
718
+ return null;
719
+ }
720
+ return stripDeploymentTypePrefix(parts[0]);
721
+ }
722
+
723
+ // NOTE: CONVEX CLI DEP - but modified to not default to "prod"
724
+ //
725
+ // For current keys returns prod|dev|preview,
726
+ // for legacy keys returns "prod".
727
+ // Examples:
728
+ // "prod:deploymentName|key" -> "prod"
729
+ // "preview:deploymentName|key" -> "preview"
730
+ // "dev:deploymentName|key" -> "dev"
731
+ // "key" -> "prod"
732
+ export function deploymentTypeFromAdminKey(adminKey: string) {
733
+ const parts = adminKey.split(":");
734
+ if (parts.length === 1) {
735
+ return null;
736
+ }
737
+ return parts.at(0)!;
738
+ }
739
+
740
+ // NOTE: CONVEX CLI DEP
741
+ // Needed to differentiate a preview deploy key
742
+ // from a concrete preview deployment's deploy key.
743
+ // preview deploy key: `preview:team:project|key`
744
+ // preview deployment's deploy key: `preview:deploymentName|key`
745
+ export function isPreviewDeployKey(adminKey: string) {
746
+ const parts = adminKey.split("|");
747
+ if (parts.length === 1) {
748
+ return false;
749
+ }
750
+ const [prefix] = parts;
751
+ const prefixParts = prefix.split(":");
752
+ return prefixParts[0] === "preview" && prefixParts.length === 3;
753
+ }
754
+
755
+ async function promptForConfirmationOrExit(
756
+ message: string,
757
+ options: { default?: boolean } = {},
758
+ ) {
759
+ if (!(await promptForConfirmation(message, options))) {
760
+ process.exit(1);
761
+ }
762
+ }
763
+
764
+ async function promptForConfirmation(
765
+ message: string,
766
+ options: { default?: boolean } = {},
767
+ ) {
768
+ if (process.stdout.isTTY) {
769
+ const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
770
+ {
771
+ type: "confirm",
772
+ name: "confirmed",
773
+ message,
774
+ default: options.default ?? true,
775
+ },
776
+ ]);
777
+ return confirmed;
778
+ } else {
779
+ return options.default ?? true;
780
+ }
781
+ }
782
+
783
+ async function promptForInput(
784
+ message: string,
785
+ options: { default?: string; validate?: (input: string) => true | string },
786
+ ) {
787
+ if (process.stdout.isTTY) {
788
+ const { input } = await inquirer.prompt<{ input: string }>([
789
+ {
790
+ type: "input",
791
+ name: "input",
792
+ message,
793
+ default: options.default,
794
+ validate: options.validate,
795
+ },
796
+ ]);
797
+ return input;
798
+ } else {
799
+ if (options.default !== undefined) {
800
+ return options.default;
801
+ } else {
802
+ logErrorAndExit(
803
+ "Run this command in an interactive terminal to provide input.",
804
+ );
805
+ }
806
+ }
807
+ }
808
+
809
+ function logErrorAndExit(message: string, error?: string): never {
810
+ logError(message, error);
811
+ process.exit(1);
812
+ }
813
+
814
+ function logError(message: string, error?: string) {
815
+ print(
816
+ `${chalk.red(`✖`)} ${indent(message)}${
817
+ error !== undefined ? `\n ${chalk.grey(`Error: ${indent(error)}`)}` : ""
818
+ }`,
819
+ );
820
+ }
821
+
822
+ function logWarning(message: string) {
823
+ print(`${chalk.yellow.bold(`!`)} ${indent(message)}`);
824
+ }
825
+
826
+ function logInfo(message: string) {
827
+ print(`${chalk.blue.bold(`i`)} ${indent(message)}`);
828
+ }
829
+
830
+ function logSuccess(message: string) {
831
+ print(`${chalk.green(`✔`)} ${indent(message)}`);
832
+ }
833
+
834
+ function print(message?: string) {
835
+ process.stderr.write((message ?? "") + "\n");
836
+ }
837
+
838
+ function indent(string: string) {
839
+ return string.replace(/^/gm, " ").slice(2);
840
+ }