instant-cli 0.22.177 → 0.22.178

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 (213) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/__tests__/e2e/cli.e2e.test.ts +3 -3
  3. package/__tests__/e2e/helpers.ts +1 -1
  4. package/__tests__/effectHelpers.ts +45 -0
  5. package/__tests__/mergeSchema.test.ts +2 -2
  6. package/dist/commands/claim.d.ts +6 -0
  7. package/dist/commands/claim.d.ts.map +1 -0
  8. package/dist/commands/claim.js +22 -0
  9. package/dist/commands/claim.js.map +1 -0
  10. package/dist/commands/explorer.d.ts +6 -0
  11. package/dist/commands/explorer.d.ts.map +1 -0
  12. package/dist/commands/explorer.js +13 -0
  13. package/dist/commands/explorer.js.map +1 -0
  14. package/dist/commands/info.d.ts +3 -0
  15. package/dist/commands/info.d.ts.map +1 -0
  16. package/dist/commands/info.js +24 -0
  17. package/dist/commands/info.js.map +1 -0
  18. package/dist/commands/init.d.ts +5 -0
  19. package/dist/commands/init.d.ts.map +1 -0
  20. package/dist/commands/init.js +39 -0
  21. package/dist/commands/init.js.map +1 -0
  22. package/dist/commands/initWithoutFiles.d.ts +6 -0
  23. package/dist/commands/initWithoutFiles.d.ts.map +1 -0
  24. package/dist/commands/initWithoutFiles.js +64 -0
  25. package/dist/commands/initWithoutFiles.js.map +1 -0
  26. package/dist/commands/login.d.ts +9 -0
  27. package/dist/commands/login.d.ts.map +1 -0
  28. package/dist/commands/login.js +52 -0
  29. package/dist/commands/login.js.map +1 -0
  30. package/dist/commands/logout.d.ts +4 -0
  31. package/dist/commands/logout.d.ts.map +1 -0
  32. package/dist/commands/logout.js +21 -0
  33. package/dist/commands/logout.js.map +1 -0
  34. package/dist/commands/pull.d.ts +6 -0
  35. package/dist/commands/pull.d.ts.map +1 -0
  36. package/dist/commands/pull.js +16 -0
  37. package/dist/commands/pull.js.map +1 -0
  38. package/dist/commands/push.d.ts +6 -0
  39. package/dist/commands/push.d.ts.map +1 -0
  40. package/dist/commands/push.js +20 -0
  41. package/dist/commands/push.js.map +1 -0
  42. package/dist/commands/query.d.ts +7 -0
  43. package/dist/commands/query.d.ts.map +1 -0
  44. package/dist/commands/query.js +52 -0
  45. package/dist/commands/query.js.map +1 -0
  46. package/dist/context/authToken.d.ts +30 -0
  47. package/dist/context/authToken.d.ts.map +1 -0
  48. package/dist/context/authToken.js +86 -0
  49. package/dist/context/authToken.js.map +1 -0
  50. package/dist/context/currentApp.d.ts +37 -0
  51. package/dist/context/currentApp.d.ts.map +1 -0
  52. package/dist/context/currentApp.js +204 -0
  53. package/dist/context/currentApp.js.map +1 -0
  54. package/dist/context/globalOpts.d.ts +11 -0
  55. package/dist/context/globalOpts.d.ts.map +1 -0
  56. package/dist/context/globalOpts.js +13 -0
  57. package/dist/context/globalOpts.js.map +1 -0
  58. package/dist/context/platformApi.d.ts +19 -0
  59. package/dist/context/platformApi.d.ts.map +1 -0
  60. package/dist/context/platformApi.js +24 -0
  61. package/dist/context/platformApi.js.map +1 -0
  62. package/dist/context/projectInfo.d.ts +29 -0
  63. package/dist/context/projectInfo.d.ts.map +1 -0
  64. package/dist/context/projectInfo.js +149 -0
  65. package/dist/context/projectInfo.js.map +1 -0
  66. package/dist/errors.d.ts +10 -0
  67. package/dist/errors.d.ts.map +1 -0
  68. package/dist/errors.js +6 -0
  69. package/dist/errors.js.map +1 -0
  70. package/dist/index.d.ts +41 -7
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +169 -1781
  73. package/dist/index.js.map +1 -1
  74. package/dist/layer.d.ts +23 -0
  75. package/dist/layer.d.ts.map +1 -0
  76. package/dist/layer.js +68 -0
  77. package/dist/layer.js.map +1 -0
  78. package/dist/lib/createApp.d.ts +12 -0
  79. package/dist/lib/createApp.d.ts.map +1 -0
  80. package/dist/lib/createApp.js +15 -0
  81. package/dist/lib/createApp.js.map +1 -0
  82. package/dist/lib/handleEnv.d.ts +7 -0
  83. package/dist/lib/handleEnv.d.ts.map +1 -0
  84. package/dist/lib/handleEnv.js +91 -0
  85. package/dist/lib/handleEnv.js.map +1 -0
  86. package/dist/lib/http.d.ts +32 -0
  87. package/dist/lib/http.d.ts.map +1 -0
  88. package/dist/lib/http.js +67 -0
  89. package/dist/lib/http.js.map +1 -0
  90. package/dist/lib/login.d.ts +13 -0
  91. package/dist/lib/login.d.ts.map +1 -0
  92. package/dist/lib/login.js +39 -0
  93. package/dist/lib/login.js.map +1 -0
  94. package/dist/lib/pullPerms.d.ts +7 -0
  95. package/dist/lib/pullPerms.d.ts.map +1 -0
  96. package/dist/lib/pullPerms.js +41 -0
  97. package/dist/lib/pullPerms.js.map +1 -0
  98. package/dist/lib/pullSchema.d.ts +12 -0
  99. package/dist/lib/pullSchema.d.ts.map +1 -0
  100. package/dist/lib/pullSchema.js +62 -0
  101. package/dist/lib/pullSchema.js.map +1 -0
  102. package/dist/lib/pushPerms.d.ts +13 -0
  103. package/dist/lib/pushPerms.d.ts.map +1 -0
  104. package/dist/lib/pushPerms.js +54 -0
  105. package/dist/lib/pushPerms.js.map +1 -0
  106. package/dist/lib/pushSchema.d.ts +53 -0
  107. package/dist/lib/pushSchema.d.ts.map +1 -0
  108. package/dist/lib/pushSchema.js +160 -0
  109. package/dist/lib/pushSchema.js.map +1 -0
  110. package/dist/lib/ui.d.ts +16 -0
  111. package/dist/lib/ui.d.ts.map +1 -0
  112. package/dist/lib/ui.js +22 -0
  113. package/dist/lib/ui.js.map +1 -0
  114. package/dist/logging.d.ts +4 -0
  115. package/dist/logging.d.ts.map +1 -0
  116. package/dist/logging.js +17 -0
  117. package/dist/logging.js.map +1 -0
  118. package/dist/old.d.ts +14 -0
  119. package/dist/old.d.ts.map +1 -0
  120. package/dist/old.js +417 -0
  121. package/dist/old.js.map +1 -0
  122. package/dist/program.d.ts +3 -0
  123. package/dist/program.d.ts.map +1 -0
  124. package/dist/program.js +3 -0
  125. package/dist/program.js.map +1 -0
  126. package/dist/renderSchemaPlan.d.ts +3 -3
  127. package/dist/renderSchemaPlan.d.ts.map +1 -1
  128. package/dist/renderSchemaPlan.js +2 -14
  129. package/dist/renderSchemaPlan.js.map +1 -1
  130. package/dist/ui/index.d.ts +4 -3
  131. package/dist/ui/index.d.ts.map +1 -1
  132. package/dist/ui/index.js +2 -2
  133. package/dist/ui/index.js.map +1 -1
  134. package/dist/ui/lib.js +1 -0
  135. package/dist/ui/lib.js.map +1 -1
  136. package/dist/util/findConfigCandidates.d.ts +1 -1
  137. package/dist/util/findConfigCandidates.d.ts.map +1 -1
  138. package/dist/util/findConfigCandidates.js +1 -3
  139. package/dist/util/findConfigCandidates.js.map +1 -1
  140. package/dist/util/fs.d.ts +1 -1
  141. package/dist/util/fs.d.ts.map +1 -1
  142. package/dist/util/fs.js.map +1 -1
  143. package/dist/util/getAuthPaths.d.ts.map +1 -1
  144. package/dist/util/getAuthPaths.js.map +1 -1
  145. package/dist/util/isHeadlessEnvironment.d.ts +3 -1
  146. package/dist/util/isHeadlessEnvironment.d.ts.map +1 -1
  147. package/dist/util/isHeadlessEnvironment.js.map +1 -1
  148. package/dist/util/loadConfig.d.ts +1 -1
  149. package/dist/util/loadConfig.d.ts.map +1 -1
  150. package/dist/util/loadConfig.js +2 -2
  151. package/dist/util/loadConfig.js.map +1 -1
  152. package/dist/util/mergeSchema.d.ts +9 -1
  153. package/dist/util/mergeSchema.d.ts.map +1 -1
  154. package/dist/util/mergeSchema.js +4 -0
  155. package/dist/util/mergeSchema.js.map +1 -1
  156. package/dist/util/renamePrompt.d.ts +2 -1
  157. package/dist/util/renamePrompt.d.ts.map +1 -1
  158. package/dist/util/renamePrompt.js +1 -1
  159. package/dist/util/renamePrompt.js.map +1 -1
  160. package/package.json +17 -7
  161. package/src/commands/claim.ts +31 -0
  162. package/src/commands/explorer.ts +21 -0
  163. package/src/commands/info.ts +34 -0
  164. package/src/commands/init.ts +58 -0
  165. package/src/commands/initWithoutFiles.ts +107 -0
  166. package/src/commands/login.ts +76 -0
  167. package/src/commands/logout.ts +23 -0
  168. package/src/commands/pull.ts +23 -0
  169. package/src/commands/push.ts +25 -0
  170. package/src/commands/query.ts +61 -0
  171. package/src/context/authToken.ts +149 -0
  172. package/src/context/currentApp.ts +277 -0
  173. package/src/context/globalOpts.ts +22 -0
  174. package/src/context/platformApi.ts +35 -0
  175. package/src/context/projectInfo.ts +215 -0
  176. package/src/errors.ts +7 -0
  177. package/src/index.ts +428 -0
  178. package/src/layer.ts +155 -0
  179. package/src/lib/createApp.ts +28 -0
  180. package/src/lib/handleEnv.ts +115 -0
  181. package/src/lib/http.ts +148 -0
  182. package/src/lib/login.ts +54 -0
  183. package/src/lib/pullPerms.ts +50 -0
  184. package/src/lib/pullSchema.ts +95 -0
  185. package/src/lib/pushPerms.ts +80 -0
  186. package/src/lib/pushSchema.ts +240 -0
  187. package/src/lib/ui.ts +36 -0
  188. package/src/logging.ts +32 -0
  189. package/src/old.js +495 -0
  190. package/src/program.ts +3 -0
  191. package/src/renderSchemaPlan.ts +6 -18
  192. package/src/ui/index.ts +4 -3
  193. package/src/util/findConfigCandidates.ts +1 -2
  194. package/src/util/fs.ts +1 -1
  195. package/src/util/getAuthPaths.ts +1 -0
  196. package/src/util/isHeadlessEnvironment.ts +1 -1
  197. package/src/util/loadConfig.ts +3 -6
  198. package/src/util/{mergeSchema.js → mergeSchema.ts} +26 -16
  199. package/src/util/renamePrompt.ts +2 -1
  200. package/tsconfig.build.json +20 -0
  201. package/tsconfig.json +15 -5
  202. package/vitest.config.ts +2 -1
  203. package/dist/util/packageManager.d.ts +0 -3
  204. package/dist/util/packageManager.d.ts.map +0 -1
  205. package/dist/util/packageManager.js +0 -70
  206. package/dist/util/packageManager.js.map +0 -1
  207. package/dist/util/promptOk.d.ts +0 -4
  208. package/dist/util/promptOk.d.ts.map +0 -1
  209. package/dist/util/promptOk.js +0 -18
  210. package/dist/util/promptOk.js.map +0 -1
  211. package/src/index.js +0 -2333
  212. package/src/util/packageManager.js +0 -78
  213. package/src/util/promptOk.ts +0 -26
package/src/index.js DELETED
@@ -1,2333 +0,0 @@
1
- // @ts-check
2
- import {
3
- generatePermsTypescriptFile,
4
- apiSchemaToInstantSchemaDef,
5
- generateSchemaTypescriptFile,
6
- diffSchemas,
7
- convertTxSteps,
8
- validateSchema,
9
- SchemaValidationError,
10
- PlatformApi,
11
- collectSystemCatalogIdentNames,
12
- buildAutoRenameSelector,
13
- } from '@instantdb/platform';
14
- import version from './version.js';
15
- import { mkdir, writeFile, readFile, unlink } from 'fs/promises';
16
- import path, { join } from 'path';
17
- import { randomUUID } from 'crypto';
18
- import jsonDiff from 'json-diff';
19
- import JSON5 from 'json5';
20
- import chalk from 'chalk';
21
- import { program, Option } from 'commander';
22
- import boxen from 'boxen';
23
- import { loadConfig } from './util/loadConfig.js';
24
- import { findProjectDir } from './util/projectDir.js';
25
- import openInBrowser from 'open';
26
- import terminalLink from 'terminal-link';
27
- import { exec } from 'child_process';
28
- import { promisify } from 'util';
29
- import {
30
- detectPackageManager,
31
- getInstallCommand,
32
- } from './util/packageManager.js';
33
- import { pathExists, readJsonFile } from './util/fs.js';
34
- import prettier from 'prettier';
35
- import {
36
- CancelSchemaError,
37
- groupSteps,
38
- renderSchemaPlan,
39
- } from './renderSchemaPlan.js';
40
- import { getAuthPaths } from './util/getAuthPaths.js';
41
- import { renderUnwrap } from './ui/lib.js';
42
- import { UI } from './ui/index.js';
43
- import { deferred } from './ui/lib.js';
44
- import { promptOk } from './util/promptOk.js';
45
- import { ResolveRenamePrompt } from './util/renamePrompt.js';
46
- import { loadEnv } from './util/loadEnv.js';
47
- import { isHeadlessEnvironment } from './util/isHeadlessEnvironment.js';
48
- import {
49
- getSchemaReadCandidates,
50
- getPermsReadCandidates,
51
- getSchemaPathToWrite,
52
- getPermsPathToWrite,
53
- } from './util/findConfigCandidates.js';
54
- import { mergeSchema } from './util/mergeSchema.js';
55
-
56
- const execAsync = promisify(exec);
57
-
58
- loadEnv();
59
-
60
- const dev = Boolean(process.env.INSTANT_CLI_DEV);
61
- const verbose = Boolean(process.env.INSTANT_CLI_VERBOSE);
62
-
63
- // logs
64
-
65
- function warn(firstArg, ...rest) {
66
- console.warn(chalk.yellow('[warning]') + ' ' + firstArg, ...rest);
67
- }
68
-
69
- function error(firstArg, ...rest) {
70
- console.error(chalk.red('[error]') + ' ' + firstArg, ...rest);
71
- }
72
-
73
- // json response
74
-
75
- const toJson = (data) => JSON.stringify(data, null, 2);
76
-
77
- // consts
78
-
79
- const potentialEnvs = {
80
- catchall: 'INSTANT_APP_ID',
81
- next: 'NEXT_PUBLIC_INSTANT_APP_ID',
82
- svelte: 'PUBLIC_INSTANT_APP_ID',
83
- vite: 'VITE_INSTANT_APP_ID',
84
- expo: 'EXPO_PUBLIC_INSTANT_APP_ID',
85
- nuxt: 'NUXT_PUBLIC_INSTANT_APP_ID',
86
- bun: 'BUN_PUBLIC_INSTANT_APP_ID',
87
- };
88
-
89
- const potentialAdminTokenEnvs = {
90
- default: 'INSTANT_APP_ADMIN_TOKEN',
91
- short: 'INSTANT_ADMIN_TOKEN',
92
- };
93
-
94
- async function detectEnvType({ pkgDir }) {
95
- const packageJSON = await getPackageJson(pkgDir);
96
- if (!packageJSON) {
97
- return 'catchall';
98
- }
99
- if (packageJSON.dependencies?.next) {
100
- return 'next';
101
- }
102
- if (packageJSON.devDependencies?.svelte) {
103
- return 'svelte';
104
- }
105
- if (packageJSON.devDependencies?.vite) {
106
- return 'vite';
107
- }
108
- if (packageJSON.dependencies?.expo) {
109
- return 'expo';
110
- }
111
- if (packageJSON.dependencies?.nuxt) {
112
- return 'nuxt';
113
- }
114
- if (packageJSON.dependencies?.['@types/bun']) {
115
- return 'bun';
116
- }
117
- return 'catchall';
118
- }
119
-
120
- const instantDashOrigin = dev
121
- ? 'http://localhost:3000'
122
- : 'https://instantdb.com';
123
-
124
- const instantBackendOrigin =
125
- process.env.INSTANT_CLI_API_URI ||
126
- (dev ? 'http://localhost:8888' : 'https://api.instantdb.com');
127
-
128
- const PUSH_PULL_OPTIONS = new Set(['schema', 'perms', 'all']);
129
-
130
- function convertArgToBagWithErrorLogging(arg) {
131
- if (!arg) {
132
- return { ok: true, bag: 'all' };
133
- } else if (PUSH_PULL_OPTIONS.has(arg.trim().toLowerCase())) {
134
- return { ok: true, bag: arg };
135
- } else {
136
- error(
137
- `${chalk.red(arg)} is not valid. Must be one of ${chalk.green(Array.from(PUSH_PULL_OPTIONS).join(', '))}`,
138
- );
139
- return { ok: false };
140
- }
141
- }
142
-
143
- function convertPushPullToCurrentFormat(arg, opts) {
144
- const { ok, bag } = convertArgToBagWithErrorLogging(arg);
145
- if (!ok) return { ok: false };
146
- return { ok: true, bag, opts };
147
- }
148
-
149
- async function packageDirectoryWithErrorLogging() {
150
- const projectInfo = await findProjectDir();
151
- if (!projectInfo) {
152
- error(
153
- "Couldn't find your root directory. Is there a package.json or deno.json file?",
154
- );
155
- return;
156
- }
157
- return projectInfo;
158
- }
159
-
160
- // cli
161
-
162
- // Header -- this shows up in every command
163
- const logoChalk = chalk.bold('instant-cli');
164
- const versionChalk = chalk.dim(`${version.trim()}`);
165
- const headerChalk = `${logoChalk} ${versionChalk} ` + '\n';
166
-
167
- // Help Footer -- this only shows up in help commands
168
- const helpFooterChalk =
169
- '\n' +
170
- chalk.dim.bold('Want to learn more?') +
171
- '\n' +
172
- `Check out the docs: ${chalk.blueBright.underline('https://instantdb.com/docs')}
173
- Join the Discord: ${chalk.blueBright.underline('https://discord.com/invite/VU53p7uQcE')}
174
- `.trim();
175
-
176
- program.addHelpText('after', helpFooterChalk);
177
-
178
- program.addHelpText('beforeAll', headerChalk);
179
-
180
- function getLocalAndGlobalOptions(cmd, helper) {
181
- const mixOfLocalAndGlobal = helper.visibleOptions(cmd);
182
- const localOptionsFromMix = mixOfLocalAndGlobal.filter(
183
- (option) => !option.__global,
184
- );
185
- const globalOptionsFromMix = mixOfLocalAndGlobal.filter(
186
- (option) => option.__global,
187
- );
188
- const globalOptions = helper.visibleGlobalOptions(cmd);
189
-
190
- return [localOptionsFromMix, globalOptionsFromMix.concat(globalOptions)];
191
- }
192
-
193
- // custom `formatHelp`
194
- // original: https://github.com/tj/commander.js/blob/master/lib/help.js
195
- function formatHelp(cmd, helper) {
196
- const termWidth = helper.padWidth(cmd, helper);
197
- const helpWidth = helper.helpWidth || 80;
198
- const itemIndentWidth = 2;
199
- const itemSeparatorWidth = 2; // between term and description
200
- function formatItem(term, description) {
201
- if (description) {
202
- const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`;
203
- return helper.wrap(
204
- fullText,
205
- helpWidth - itemIndentWidth,
206
- termWidth + itemSeparatorWidth,
207
- );
208
- }
209
- return term;
210
- }
211
- function formatList(textArray) {
212
- return textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth));
213
- }
214
-
215
- // Usage
216
- let output = [`${helper.commandUsage(cmd)}`, ''];
217
-
218
- // Description
219
- const commandDescription = helper.commandDescription(cmd);
220
- if (commandDescription.length > 0) {
221
- output = output.concat([helper.wrap(commandDescription, helpWidth, 0), '']);
222
- }
223
-
224
- // Arguments
225
- const argumentList = helper.visibleArguments(cmd).map((argument) => {
226
- return formatItem(
227
- helper.argumentTerm(argument),
228
- helper.argumentDescription(argument),
229
- );
230
- });
231
- if (argumentList.length > 0) {
232
- output = output.concat([
233
- chalk.dim.bold('Arguments'),
234
- formatList(argumentList),
235
- '',
236
- ]);
237
- }
238
- const [visibleOptions, visibleGlobalOptions] = getLocalAndGlobalOptions(
239
- cmd,
240
- helper,
241
- );
242
-
243
- // Options
244
- const optionList = visibleOptions.map((option) => {
245
- return formatItem(
246
- helper.optionTerm(option),
247
- helper.optionDescription(option),
248
- );
249
- });
250
- if (optionList.length > 0) {
251
- output = output.concat([
252
- chalk.dim.bold('Options'),
253
- formatList(optionList),
254
- '',
255
- ]);
256
- }
257
- // Commands
258
- const commandList = helper.visibleCommands(cmd).map((cmd) => {
259
- return formatItem(
260
- helper.subcommandTerm(cmd),
261
- helper.subcommandDescription(cmd),
262
- );
263
- });
264
- if (commandList.length > 0) {
265
- output = output.concat([
266
- chalk.dim.bold('Commands'),
267
- formatList(commandList),
268
- '',
269
- ]);
270
- }
271
-
272
- if (this.showGlobalOptions) {
273
- const globalOptionList = visibleGlobalOptions.map((option) => {
274
- return formatItem(
275
- helper.optionTerm(option),
276
- helper.optionDescription(option),
277
- );
278
- });
279
- if (globalOptionList.length > 0) {
280
- output = output.concat([
281
- chalk.dim.bold('Global Options'),
282
- formatList(globalOptionList),
283
- '',
284
- ]);
285
- }
286
- }
287
-
288
- return output.join('\n');
289
- }
290
-
291
- program.configureHelp({
292
- showGlobalOptions: true,
293
- formatHelp,
294
- });
295
-
296
- function globalOption(flags, description, argParser) {
297
- const opt = new Option(flags, description);
298
- if (argParser) {
299
- opt.argParser(argParser);
300
- }
301
- // @ts-ignore
302
- // __global does not exist on `Option`,
303
- // but we use it in `getLocalAndGlobalOptions`, to produce
304
- // our own custom list of local and global options.
305
- // For more info, see the original PR:
306
- // https://github.com/instantdb/instant/pull/505
307
- opt.__global = true;
308
- return opt;
309
- }
310
-
311
- function warnDeprecation(oldCmd, newCmd) {
312
- warn(
313
- chalk.yellow('`instant-cli ' + oldCmd + '` is deprecated.') +
314
- ' Use ' +
315
- chalk.green('`instant-cli ' + newCmd + '`') +
316
- ' instead.' +
317
- '\n',
318
- );
319
- }
320
-
321
- program
322
- .name('instant-cli')
323
- .addOption(globalOption('-t --token <token>', 'Auth token override'))
324
- .addOption(globalOption('-y --yes', "Answer 'yes' to all prompts"))
325
- .addOption(globalOption('--env <file>', 'Use a specific .env file'))
326
- .addOption(
327
- globalOption('-v --version', 'Print the version number', () => {
328
- console.log(version);
329
- process.exit(0);
330
- }),
331
- )
332
- .addHelpOption(globalOption('-h --help', 'Print the help text for a command'))
333
- .usage(`<command> ${chalk.dim('[options] [args]')}`);
334
-
335
- program
336
- .command('login')
337
- .description('Log into your account')
338
- .option('-p --print', 'Prints the auth token into the console.')
339
- .option(
340
- '--headless',
341
- 'Print the login URL instead of trying to open the browser',
342
- )
343
- .action(async (opts) => {
344
- console.log("Let's log you in!");
345
- await login(opts);
346
- });
347
-
348
- program
349
- .command('logout')
350
- .description('Log out of your Instant account')
351
- .action(async () => {
352
- await logout();
353
- });
354
-
355
- program
356
- .command('info')
357
- .description('Display CLI version and login status')
358
- .action(async () => {
359
- console.log(`CLI Version: ${version}`);
360
- // Use allowAdminToken=false to skip admin tokens from env vars
361
- // This ensures we use the user's login token, not an app's admin token
362
- const token = await readConfigAuthToken(false);
363
- if (!token) {
364
- console.log('Not logged in.');
365
- return;
366
- }
367
- const meRes = await fetchJson({
368
- method: 'GET',
369
- path: '/dash/me',
370
- debugName: 'Get user info',
371
- errorMessage: 'Failed to get user info.',
372
- command: 'info',
373
- authToken: token,
374
- });
375
- if (meRes.ok) {
376
- console.log(`Logged in as: ${meRes.data.user.email}`);
377
- } else {
378
- console.log('Not logged in.');
379
- }
380
- });
381
-
382
- program
383
- .command('init')
384
- .description('Set up a new project.')
385
- .option(
386
- '-a --app <app-id>',
387
- 'If you have an existing app ID, we can pull schema and perms from there.',
388
- )
389
- .option(
390
- '-p --package <react|react-native|core|admin|solid|svelte>',
391
- 'Which package to automatically install if there is not one installed already.',
392
- )
393
- .option('--title <title>', 'Title for the created app')
394
- .action(handleInit);
395
-
396
- program
397
- .command('init-without-files')
398
- .description('Generate a new app id and admin token pair without any files.')
399
- .option('--title <title>', 'Title for the created app.')
400
- .option(
401
- '--org-id <org-id>',
402
- 'Organization id for app. Cannot be used with --temp flag.',
403
- )
404
- .option(
405
- '--temp',
406
- 'Create a temporary app which will automatically delete itself after >24 hours.',
407
- )
408
- .action(handleInitWithoutFiles);
409
-
410
- // Note: Nov 20, 2024
411
- // We can eventually delete this,
412
- // once we know most people use the new pull and push commands
413
- program
414
- .command('push-schema', { hidden: true })
415
- .argument('[app-id]')
416
- .description('Push schema to production.')
417
- .option(
418
- '--skip-check-types',
419
- "Don't check types on the server when pushing schema",
420
- )
421
- .action(async (appIdOrName, opts) => {
422
- warnDeprecation('push-schema', 'push schema');
423
- await handlePush('schema', { app: appIdOrName, ...opts });
424
- });
425
-
426
- // Note: Nov 20, 2024
427
- // We can eventually delete this,
428
- // once we know most people use the new pull and push commands
429
- program
430
- .command('push-perms', { hidden: true })
431
- .argument('[app-id]')
432
- .description('Push perms to production.')
433
- .action(async (appIdOrName) => {
434
- warnDeprecation('push-perms', 'push perms');
435
- await handlePush('perms', { app: appIdOrName });
436
- });
437
-
438
- program
439
- .command('push')
440
- .argument(
441
- '[schema|perms|all]',
442
- 'Which configuration to push. Defaults to `all`',
443
- )
444
- .option(
445
- '-a --app <app-id>',
446
- 'App ID to push to. Defaults to *_INSTANT_APP_ID in .env',
447
- )
448
- .option(
449
- '--skip-check-types',
450
- "Don't check types on the server when pushing schema",
451
- )
452
- .option(
453
- '--rename [renames...]',
454
- 'List of full attribute names separated by a ":"\n Example:`push --rename posts.author:posts.creator stores.owner:stores.manager`',
455
- )
456
- .option(
457
- '-p --package <react|react-native|core|admin|solid|svelte>',
458
- 'Which package to automatically install if there is not one installed already.',
459
- )
460
- .description('Push schema and perm files to production.')
461
- .addHelpText(
462
- 'after',
463
- `
464
- Environment Variables:
465
- INSTANT_SCHEMA_FILE_PATH Override schema file location (default: instant.schema.ts)
466
- INSTANT_PERMS_FILE_PATH Override perms file location (default: instant.perms.ts)
467
- `,
468
- )
469
- .action(async function (arg, inputOpts) {
470
- const ret = convertPushPullToCurrentFormat(arg, inputOpts);
471
- if (!ret.ok) return process.exit(1);
472
- const { bag, opts } = ret;
473
- await handlePush(bag, opts);
474
- });
475
-
476
- // Note: Nov 20, 2024
477
- // We can eventually delete this,
478
- // once we know most people use the new pull and push commands
479
- program
480
- .command('pull-schema', { hidden: true })
481
- .argument('[app-id]')
482
- .description('Generate instant.schema.ts from production')
483
- .action(async (appIdOrName) => {
484
- warnDeprecation('pull-schema', 'pull schema');
485
- await handlePull('schema', { app: appIdOrName });
486
- });
487
-
488
- // Note: Nov 20, 2024
489
- // We can eventually delete this,
490
- // once we know most people use the new pull and push commands
491
- program
492
- .command('pull-perms', { hidden: true })
493
- .argument('[app-id]')
494
- .description('Generate instant.perms.ts from production.')
495
- .action(async (appIdOrName) => {
496
- warnDeprecation('pull-perms', 'pull perms');
497
- await handlePull('perms', { app: appIdOrName });
498
- });
499
-
500
- program
501
- .command('pull')
502
- .argument(
503
- '[schema|perms|all]',
504
- 'Which configuration to push. Defaults to `all`',
505
- )
506
- .option(
507
- '-a --app <app-id>',
508
- 'App ID to pull from. Defaults to *_INSTANT_APP_ID in .env',
509
- )
510
- .option(
511
- '-p --package <react|react-native|core|admin|solid|svelte>',
512
- 'Which package to automatically install if there is not one installed already.',
513
- )
514
- .option(
515
- '--experimental-type-preservation',
516
- "[Experimental] Preserve manual type changes like `status: i.json<'online' | 'offline'>()` when doing `instant-cli pull schema`",
517
- )
518
- .description('Pull schema and perm files from production.')
519
- .addHelpText(
520
- 'after',
521
- `
522
- Environment Variables:
523
- INSTANT_SCHEMA_FILE_PATH Override schema file location (default: instant.schema.ts)
524
- INSTANT_PERMS_FILE_PATH Override perms file location (default: instant.perms.ts)
525
- `,
526
- )
527
- .action(async function (arg, inputOpts) {
528
- const ret = convertPushPullToCurrentFormat(arg, inputOpts);
529
- if (!ret.ok) return process.exit(1);
530
- const { bag, opts } = ret;
531
- await handlePull(bag, opts);
532
- });
533
-
534
- program
535
- .command('claim')
536
- .description('Transfer a tempoary app into your Instant account')
537
- .action(async function () {
538
- const authToken = await readConfigAuthToken(false);
539
- if (!authToken) {
540
- console.error(
541
- `Please log in first with ${chalk.bgGray.white('instant-cli login')} to claim an app`,
542
- );
543
- process.exit(1);
544
- }
545
-
546
- const envResult = detectAppIdAndAdminTokenFromEnvWithErrorLogging();
547
- if (!envResult.ok) return process.exit(1);
548
-
549
- if (!envResult.appId) {
550
- error('No app ID found in environment variables.');
551
- return process.exit(1);
552
- }
553
-
554
- if (!envResult.adminToken) {
555
- error('No admin token found in environment variables.');
556
- return process.exit(1);
557
- }
558
-
559
- const appId = envResult.appId.value;
560
- const adminToken = envResult.adminToken.value;
561
-
562
- console.log(`Found ${chalk.green(envResult.appId.envName)}: ${appId}`);
563
-
564
- await claimEphemeralApp(appId, adminToken, authToken);
565
- });
566
-
567
- program
568
- .command('explorer')
569
- .description('Opens the Explorer in your browser')
570
- .option(
571
- '-a --app <app-id>',
572
- 'App ID to open the explorer to. Defaults to *_INSTANT_APP_ID in .env',
573
- )
574
- .action(async function (opts) {
575
- console.log('Opening Explorer...');
576
- const pkgAndAuthInfo = await getOrPromptPackageAndAuthInfoWithErrorLogging(
577
- {},
578
- );
579
- const { ok, appId } = await getOrCreateAppAndWriteToEnv(
580
- pkgAndAuthInfo,
581
- opts,
582
- );
583
- if (!ok) {
584
- return process.exit(1);
585
- }
586
- openInBrowser(`${instantDashOrigin}/dash?s=main&app=${appId}&t=explorer`);
587
- });
588
-
589
- program
590
- .command('query')
591
- .argument('<query>', 'InstaQL query as JSON/JSON5')
592
- .option(
593
- '-a --app <app-id>',
594
- 'App ID to query. Defaults to *_INSTANT_APP_ID in .env',
595
- )
596
- .option('--admin', 'Run the query as admin (bypasses permissions)')
597
- .option('--as-email <email>', 'Run the query as a specific user by email')
598
- .option('--as-guest', 'Run the query as an unauthenticated guest')
599
- .option(
600
- '--as-token <refresh-token>',
601
- 'Run the query as a user identified by refresh token',
602
- )
603
- .description('Run an InstaQL query against your app.')
604
- .action(async function (queryArg, opts) {
605
- await handleQuery(queryArg, opts);
606
- });
607
-
608
- program.parse(process.argv);
609
-
610
- async function handleInit(opts) {
611
- const pkgAndAuthInfo =
612
- await getOrPromptPackageAndAuthInfoWithErrorLogging(opts);
613
- if (!pkgAndAuthInfo) return process.exit(1);
614
- const { ok, appId } = await getOrCreateAppAndWriteToEnv(pkgAndAuthInfo, opts);
615
- if (!ok) {
616
- return process.exit(1);
617
- }
618
-
619
- // Create schema file if it doesn't exist
620
- // or ask to push if local schema exists
621
- const localSchemaExists = await readLocalSchemaFile();
622
- if (!localSchemaExists) {
623
- await pull('schema', appId, pkgAndAuthInfo);
624
- } else {
625
- const doSchemaPush = await promptOk(
626
- {
627
- promptText: 'Found local schema. Push it to the new app?',
628
- inline: true,
629
- },
630
- program.opts(),
631
- );
632
- if (doSchemaPush) {
633
- await push('schema', appId, opts);
634
- }
635
- }
636
-
637
- // Create perms file if it doesn't exist
638
- // or ask to push if local perms exists
639
- const localPermsExists = await readLocalPermsFile();
640
- if (!localPermsExists) {
641
- await pull('perms', appId, pkgAndAuthInfo);
642
- } else {
643
- const doPermsPush = await promptOk(
644
- {
645
- promptText: 'Found local perms. Push it to the new app?',
646
- inline: true,
647
- },
648
- program.opts(),
649
- );
650
- if (doPermsPush) {
651
- await push('perms', appId, opts);
652
- }
653
- }
654
- }
655
-
656
- async function handleInitWithoutFiles(opts) {
657
- try {
658
- if (!opts?.title) {
659
- throw new Error(
660
- 'Title is required for creating a new app without local files.',
661
- );
662
- }
663
-
664
- if (opts.title.startsWith('-')) {
665
- throw new Error(
666
- `Invalid title: "${opts.title}". Title cannot be a flag.`,
667
- );
668
- }
669
-
670
- if (opts?.temp && opts?.orgId) {
671
- throw new Error('Cannot use --temp and --org-id flags together.');
672
- }
673
-
674
- let result;
675
- if (opts?.temp) {
676
- result = await createEphemeralApp(opts.title);
677
- } else {
678
- const authToken = await readConfigAuthToken(false);
679
- if (!authToken) {
680
- throw new Error(
681
- `Please log in first with 'instant-cli login' before running this command.`,
682
- );
683
- }
684
- result = await createApp(opts.title, opts.orgId, authToken);
685
- }
686
-
687
- console.error(`${chalk.green('Successfully created new app!')}\n`);
688
-
689
- console.log(
690
- toJson({
691
- app: result,
692
- error: null,
693
- }),
694
- );
695
- } catch (error) {
696
- console.error(`${chalk.red('Failed to create app.')}\n`);
697
-
698
- console.log(
699
- toJson({
700
- app: null,
701
- error: { message: error.message },
702
- }),
703
- );
704
- process.exit(1);
705
- }
706
- }
707
-
708
- async function detectAppIdQuietly(opts) {
709
- const fromOpts = await detectAppIdFromOptsWithErrorLogging(opts);
710
- if (!fromOpts.ok) return fromOpts;
711
- if (fromOpts.appId) {
712
- return { ok: true, appId: fromOpts.appId };
713
- }
714
- const fromEnv = detectAppIdFromEnvWithErrorLogging();
715
- if (!fromEnv.ok) return fromEnv;
716
- if (fromEnv.found) {
717
- return { ok: true, appId: fromEnv.found.value };
718
- }
719
- return { ok: true };
720
- }
721
-
722
- async function handleQuery(queryArg, opts) {
723
- const contexts = [
724
- opts.admin,
725
- opts.asEmail,
726
- opts.asGuest,
727
- opts.asToken,
728
- ].filter(Boolean);
729
- if (contexts.length > 1) {
730
- error(
731
- 'Please specify exactly one context: --admin, --as-email <email>, --as-guest, or --as-token <refresh-token>',
732
- );
733
- return process.exit(1);
734
- }
735
-
736
- const { ok, appId } = await detectAppIdQuietly(opts);
737
- if (!ok) return process.exit(1);
738
- if (!appId) {
739
- error(
740
- 'No app ID detected. Please specify one with --app or set up with `instant-cli init`',
741
- );
742
- return process.exit(1);
743
- }
744
-
745
- let query;
746
- try {
747
- query = JSON5.parse(queryArg);
748
- } catch {
749
- error('Invalid JSON query argument.');
750
- return process.exit(1);
751
- }
752
-
753
- const headers = { 'app-id': appId };
754
- if (opts.asEmail) {
755
- headers['as-email'] = opts.asEmail;
756
- } else if (opts.asGuest) {
757
- headers['as-guest'] = 'true';
758
- } else if (opts.asToken) {
759
- headers['as-token'] = opts.asToken;
760
- }
761
-
762
- const res = await fetchJson({
763
- method: 'POST',
764
- path: '/admin/query',
765
- body: { query, 'inference?': true },
766
- headers,
767
- debugName: 'Query',
768
- errorMessage: 'Failed to run query.',
769
- command: 'query',
770
- });
771
-
772
- if (!res.ok) return process.exit(1);
773
-
774
- console.log(JSON.stringify(res.data, null, 2));
775
- }
776
-
777
- async function handlePush(bag, opts) {
778
- const pkgAndAuthInfo = await enforcePackageAndAuthInfoWithErrorLogging(opts);
779
- if (!pkgAndAuthInfo) return process.exit(1);
780
- const { ok, appId } = await detectAppWithErrorLogging(opts);
781
- if (!ok) return process.exit(1);
782
- if (!appId) {
783
- error(
784
- 'No app ID detected. Please specify one with --app or set up with `instant-cli init`',
785
- );
786
- return;
787
- }
788
- await push(bag, appId, opts);
789
- }
790
-
791
- async function handlePull(bag, opts) {
792
- const pkgAndAuthInfo = await enforcePackageAndAuthInfoWithErrorLogging(opts);
793
- if (!pkgAndAuthInfo) return process.exit(1);
794
- const { ok, appId } = await detectAppWithErrorLogging(opts);
795
- if (!ok) {
796
- return process.exit(1);
797
- }
798
- if (!appId) {
799
- error(
800
- 'No app ID detected. Please specify one with --app or set up with `instant-cli init`',
801
- );
802
- return;
803
- }
804
- await pull(bag, appId, { ...pkgAndAuthInfo, ...opts });
805
- }
806
-
807
- async function push(bag, appId, opts) {
808
- if (bag === 'schema' || bag === 'all') {
809
- const { ok } = await pushSchema(appId, opts);
810
- if (!ok) return process.exit(1);
811
- }
812
- if (bag === 'perms' || bag === 'all') {
813
- const { ok } = await pushPerms(appId);
814
- if (!ok) return process.exit(1);
815
- }
816
- }
817
-
818
- function printDotEnvInfo(envType, appId) {
819
- console.log(`\nPicked app ${chalk.green(appId)}!\n`);
820
- console.log(
821
- `To use this app automatically from now on, update your ${chalk.green('`.env`')} file:`,
822
- );
823
- const picked = potentialEnvs[envType];
824
- const rest = { ...potentialEnvs };
825
- delete rest[envType];
826
- console.log(` ${chalk.green(picked)}=${appId}`);
827
- const otherEnvs = Object.values(rest);
828
- otherEnvs.sort();
829
- const otherEnvStr = otherEnvs.map((x) => ' ' + chalk.green(x)).join('\n');
830
- console.log(`Alternative names: \n${otherEnvStr} \n`);
831
- console.log(terminalLink('Dashboard:', appDashUrl(appId)) + '\n');
832
- }
833
-
834
- async function handleEnvFile(pkgAndAuthInfo, { appId, appToken }) {
835
- const { pkgDir } = pkgAndAuthInfo;
836
- const envType = await detectEnvType(pkgAndAuthInfo);
837
- const envName = potentialEnvs[envType];
838
-
839
- const envFile = program.optsWithGlobals().env ?? '.env';
840
- const hasEnvFile = await pathExists(join(pkgDir, envFile));
841
- if (hasEnvFile) {
842
- printDotEnvInfo(envType, appId);
843
- return;
844
- }
845
- console.log(
846
- `\nLooks like you don't have a ${chalk.green(`\`${envFile}\``)} file yet.`,
847
- );
848
- console.log(
849
- `If we set ${chalk.green(envName)} & ${chalk.green('INSTANT_APP_ADMIN_TOKEN')}, we can remember the app that you chose for all future commands.`,
850
- );
851
-
852
- const saveExtraInfo =
853
- envFile !== '.env' ? chalk.green(' (will create `' + envFile + '`)') : '';
854
-
855
- const ok = await promptOk(
856
- {
857
- inline: true,
858
- promptText: 'Want us to create this env file for you?' + saveExtraInfo,
859
- modifyOutput: (a) => a,
860
- },
861
- program.opts(),
862
- true,
863
- );
864
- if (!ok) {
865
- console.log(
866
- `No .env file created. You can always set ${chalk.green('`' + envName + '`')} later. \n`,
867
- );
868
- return;
869
- }
870
- const content =
871
- [
872
- [envName, appId],
873
- ['INSTANT_APP_ADMIN_TOKEN', appToken],
874
- ]
875
- .map(([k, v]) => `${k}=${v}`)
876
- .join('\n') + '\n';
877
- await writeFile(join(pkgDir, envFile), content, 'utf-8');
878
- if (envFile !== '.env') {
879
- console.log(`Created ${chalk.green(envFile)}!`);
880
- } else {
881
- console.log(`Created ${chalk.green('.env')} file!`);
882
- }
883
- }
884
-
885
- async function getOrCreateAppAndWriteToEnv(pkgAndAuthInfo, opts) {
886
- const ret = await detectOrCreateAppWithErrorLogging(opts);
887
- if (!ret.ok) return ret;
888
- const { appId, appToken, source } = ret;
889
- if (source === 'created' || source === 'imported') {
890
- await handleEnvFile(pkgAndAuthInfo, { appId, appToken });
891
- }
892
- return ret;
893
- }
894
-
895
- async function pull(bag, appId, pkgAndAuthInfo) {
896
- if (bag === 'schema' || bag === 'all') {
897
- const { ok } = await pullSchema(appId, pkgAndAuthInfo);
898
- if (!ok) return process.exit(1);
899
- }
900
- if (bag === 'perms' || bag === 'all') {
901
- const { ok } = await pullPerms(appId, pkgAndAuthInfo);
902
- if (!ok) return process.exit(1);
903
- }
904
- }
905
-
906
- async function login(options) {
907
- const registerRes = await fetchJson({
908
- method: 'POST',
909
- path: '/dash/cli/auth/register',
910
- debugName: 'Login register',
911
- errorMessage: 'Failed to register login.',
912
- noAuth: true,
913
- command: 'login',
914
- });
915
-
916
- if (!registerRes.ok) {
917
- return process.exit(1);
918
- }
919
-
920
- const { secret, ticket } = registerRes.data;
921
-
922
- console.log();
923
-
924
- if (isHeadlessEnvironment(options)) {
925
- console.log(
926
- `Open this URL in a browser to log in:\n ${instantDashOrigin}/dash?ticket=${ticket}\n`,
927
- );
928
- } else {
929
- const ok = await promptOk(
930
- {
931
- promptText: `This will open instantdb.com in your browser, OK to proceed?`,
932
- },
933
- program.opts(),
934
- /*defaultAnswer=*/ true,
935
- );
936
-
937
- if (!ok) return;
938
- openInBrowser(`${instantDashOrigin}/dash?ticket=${ticket}`);
939
- }
940
-
941
- console.log('Waiting for authentication...');
942
- const authTokenRes = await waitForAuthToken({ secret });
943
- if (!authTokenRes) {
944
- return process.exit(1);
945
- }
946
-
947
- const { token, email } = authTokenRes;
948
-
949
- if (options.print) {
950
- console.log(chalk.red('[Do not share] Your Instant auth token:', token));
951
- } else {
952
- await saveConfigAuthToken(token);
953
- console.log(chalk.green(`Successfully logged in as ${email}!`));
954
- }
955
- return token;
956
- }
957
-
958
- async function logout() {
959
- const { authConfigFilePath } = getAuthPaths();
960
-
961
- try {
962
- await unlink(authConfigFilePath);
963
- console.log(chalk.green('Successfully logged out from Instant!'));
964
- return true;
965
- } catch (error) {
966
- if (error.code === 'ENOENT') {
967
- console.log(chalk.green('You were already logged out!'));
968
- } else {
969
- error('Failed to logout: ' + error.message);
970
- }
971
- return false;
972
- }
973
- }
974
-
975
- const packageAliasAndFullNames = {
976
- react: '@instantdb/react',
977
- 'react-native': '@instantdb/react-native',
978
- core: '@instantdb/core',
979
- admin: '@instantdb/admin',
980
- solid: '@instantdb/solidjs',
981
- svelte: '@instantdb/svelte',
982
- };
983
-
984
- async function getOrInstallInstantModuleWithErrorLogging(pkgDir, opts) {
985
- const pkgJson = await getPackageJSONWithErrorLogging(pkgDir);
986
- if (!pkgJson) {
987
- return;
988
- }
989
- console.log('Checking for an Instant SDK...');
990
- const instantModuleName = await getInstantModuleName(pkgJson);
991
- if (instantModuleName) {
992
- console.log(
993
- `Found ${chalk.green(instantModuleName)} in your package.json.`,
994
- );
995
- return instantModuleName;
996
- }
997
- console.log(
998
- "Couldn't find an Instant SDK in your package.json, let's install one!",
999
- );
1000
-
1001
- let moduleName;
1002
- if (opts.package) {
1003
- moduleName = packageAliasAndFullNames[opts.package];
1004
- } else {
1005
- if (program.optsWithGlobals()?.yes) {
1006
- console.error(
1007
- '--yes was provided without a package specificaion and no Instant SDK was found',
1008
- );
1009
- process.exit(1);
1010
- }
1011
- moduleName = await renderUnwrap(
1012
- new UI.Select({
1013
- promptText: 'Which package would you like to use?',
1014
- options: [
1015
- { label: '@instantdb/react', value: '@instantdb/react' },
1016
- {
1017
- label: '@instantdb/react-native',
1018
- value: '@instantdb/react-native',
1019
- },
1020
- { label: '@instantdb/core', value: '@instantdb/core' },
1021
- { label: '@instantdb/admin', value: '@instantdb/admin' },
1022
- { label: '@instantdb/solidjs', value: '@instantdb/solidjs' },
1023
- { label: '@instantdb/svelte', value: '@instantdb/svelte' },
1024
- ],
1025
- }),
1026
- );
1027
- }
1028
-
1029
- const packageManager = await detectPackageManager(pkgDir);
1030
-
1031
- const packagesToInstall = [moduleName];
1032
- if (moduleName === '@instantdb/react-native') {
1033
- packagesToInstall.push(
1034
- 'react-native-get-random-values',
1035
- '@react-native-async-storage/async-storage',
1036
- );
1037
- }
1038
-
1039
- const installCommand = getInstallCommand(
1040
- packageManager,
1041
- packagesToInstall.join(' '),
1042
- );
1043
-
1044
- await renderUnwrap(
1045
- new UI.Spinner({
1046
- promise: execAsync(installCommand, pkgDir),
1047
- errorText: `Failed to install ${packagesToInstall.join(', ')} using ${packageManager}.`,
1048
- workingText: `Installing ${packagesToInstall.join(', ')} using ${packageManager}...`,
1049
- doneText: `Installed ${packagesToInstall.join(', ')} using ${packageManager}.`,
1050
- }),
1051
- );
1052
-
1053
- return moduleName;
1054
- }
1055
-
1056
- async function promptCreateApp(opts) {
1057
- const id = randomUUID();
1058
- const token = randomUUID();
1059
-
1060
- let _title;
1061
- if (opts?.title) {
1062
- _title = opts.title;
1063
- } else {
1064
- _title = await renderUnwrap(
1065
- new UI.TextInput({
1066
- prompt: 'What would you like to call it?',
1067
- placeholder: 'My cool app',
1068
- }),
1069
- ).catch(() => null);
1070
- }
1071
-
1072
- const title = _title?.trim();
1073
-
1074
- if (!title) {
1075
- error('No name provided.');
1076
- return { ok: false };
1077
- }
1078
-
1079
- const res = await fetchJson({
1080
- debugName: 'Fetching orgs',
1081
- method: 'GET',
1082
- path: '/dash',
1083
- errorMessage: 'Failed to fetch apps.',
1084
- command: 'init',
1085
- });
1086
- if (!res.ok) {
1087
- return { ok: false };
1088
- }
1089
-
1090
- const allowedOrgs = res.data.orgs.filter((org) => org.role !== 'app-member');
1091
-
1092
- let org_id = opts.org;
1093
-
1094
- if (!org_id && allowedOrgs.length) {
1095
- const choices = [{ label: '(No organization)', value: null }];
1096
- for (const org of allowedOrgs) {
1097
- choices.push({ label: org.title, value: org.id });
1098
- }
1099
- const choice = await renderUnwrap(
1100
- new UI.Select({
1101
- promptText: 'Would you like to create the app in an organization?',
1102
- options: choices,
1103
- }),
1104
- );
1105
- if (choice) {
1106
- org_id = choice;
1107
- }
1108
- }
1109
-
1110
- const app = { id, title, admin_token: token, org_id };
1111
- const appRes = await fetchJson({
1112
- method: 'POST',
1113
- path: '/dash/apps',
1114
- debugName: 'App create',
1115
- errorMessage: 'Failed to create app.',
1116
- body: app,
1117
- command: 'init',
1118
- });
1119
-
1120
- if (!appRes.ok) return { ok: false };
1121
- return {
1122
- ok: true,
1123
- appId: id,
1124
- appTitle: title,
1125
- appToken: token,
1126
- source: 'created',
1127
- };
1128
- }
1129
-
1130
- async function promptImportAppOrCreateApp() {
1131
- const res = await fetchJson({
1132
- debugName: 'Fetching apps',
1133
- method: 'GET',
1134
- path: '/dash',
1135
- errorMessage: 'Failed to fetch apps.',
1136
- command: 'init',
1137
- });
1138
- if (!res.ok) {
1139
- return { ok: false };
1140
- }
1141
-
1142
- const result = await renderUnwrap(
1143
- new UI.AppSelector({
1144
- allowEphemeral: true,
1145
- allowCreate: true,
1146
- startingMenuIndex: 2,
1147
- api: {
1148
- getDash: () => res.data,
1149
- createEphemeralApp,
1150
- getAppsForOrg: async (orgId) => {
1151
- const orgsRes = await fetchJson({
1152
- debugName: 'Fetching org apps',
1153
- method: 'GET',
1154
- path: `/dash/orgs/${orgId}`,
1155
- errorMessage: 'Failed to fetch apps.',
1156
- command: 'init',
1157
- });
1158
- if (!orgsRes.ok) {
1159
- throw new Error('Failed to fetch org apps');
1160
- }
1161
- return { apps: orgsRes.data.apps };
1162
- },
1163
- createApp,
1164
- },
1165
- }),
1166
- );
1167
-
1168
- if (result.approach === 'import') {
1169
- trackAppImport(result.appId);
1170
- }
1171
-
1172
- return {
1173
- ok: true,
1174
- appId: result.appId,
1175
- appToken: result.adminToken,
1176
- source: result.approach === 'import' ? 'imported' : 'created',
1177
- };
1178
- }
1179
-
1180
- async function createApp(title, orgId, authToken) {
1181
- const id = randomUUID();
1182
- const token = randomUUID();
1183
- const app = { id, title, admin_token: token, org_id: orgId };
1184
- const appRes = await fetchJson({
1185
- method: 'POST',
1186
- path: '/dash/apps',
1187
- debugName: 'App create',
1188
- errorMessage: 'Failed to create app.',
1189
- body: app,
1190
- command: 'init',
1191
- authToken,
1192
- });
1193
- if (!appRes.ok) throw new Error('Failed to create app');
1194
- return { appId: id, adminToken: token };
1195
- }
1196
-
1197
- async function createEphemeralApp(title) {
1198
- const api = new PlatformApi({ apiURI: instantBackendOrigin });
1199
- const { app } = await api.createTemporaryApp({ title });
1200
- return { appId: app.id, adminToken: app.adminToken };
1201
- }
1202
-
1203
- /**
1204
- * Fire-and-forget tracking for when a user imports/links an existing app.
1205
- * Captures user info if authenticated.
1206
- */
1207
- function trackAppImport(appId) {
1208
- fetchJson({
1209
- method: 'POST',
1210
- path: `/dash/apps/${appId}/track-import`,
1211
- debugName: 'Track import',
1212
- errorMessage: '',
1213
- noLogError: true,
1214
- command: 'init',
1215
- }).catch(() => {});
1216
- }
1217
-
1218
- async function detectAppWithErrorLogging(opts) {
1219
- const fromOpts = await detectAppIdFromOptsWithErrorLogging(opts);
1220
- if (!fromOpts.ok) return fromOpts;
1221
- if (fromOpts.appId) {
1222
- trackAppImport(fromOpts.appId);
1223
- return { ok: true, appId: fromOpts.appId, source: 'opts' };
1224
- }
1225
- const fromEnv = detectAppIdFromEnvWithErrorLogging();
1226
- if (!fromEnv.ok) return fromEnv;
1227
- if (fromEnv.found) {
1228
- const { envName, value } = fromEnv.found;
1229
- console.log(`Found ${chalk.green(envName)}: ${value}`);
1230
- return { ok: true, appId: value, source: 'env' };
1231
- }
1232
- return { ok: true };
1233
- }
1234
-
1235
- async function detectOrCreateAppWithErrorLogging(opts) {
1236
- const detected = await detectAppWithErrorLogging(opts);
1237
- if (!detected.ok) return detected;
1238
- if (detected.appId) {
1239
- return detected;
1240
- }
1241
- let action;
1242
- if (program.optsWithGlobals().yes) {
1243
- action = 'create';
1244
- if (!opts?.title) {
1245
- console.error(
1246
- chalk.red(`Title is required when using --yes and no app is linked`),
1247
- );
1248
- process.exit(1);
1249
- }
1250
- const app = await createApp(opts.title);
1251
-
1252
- return { ok: true, appId: app.appId, source: 'created' };
1253
- } else {
1254
- console.log();
1255
- return await promptImportAppOrCreateApp();
1256
- }
1257
- }
1258
-
1259
- async function writeTypescript(path, content, encoding) {
1260
- const prettierConfig = await prettier.resolveConfig(path);
1261
- const formattedCode = await prettier.format(content, {
1262
- ...prettierConfig,
1263
- parser: 'typescript',
1264
- });
1265
- return await writeFile(path, formattedCode, encoding);
1266
- }
1267
-
1268
- async function getInstantModuleName(pkgJson) {
1269
- const deps = pkgJson.dependencies || {};
1270
- const devDeps = pkgJson.devDependencies || {};
1271
- const instantModuleName = [
1272
- '@instantdb/react',
1273
- '@instantdb/react-native',
1274
- '@instantdb/core',
1275
- '@instantdb/admin',
1276
- '@instantdb/solidjs',
1277
- '@instantdb/svelte',
1278
- ].find((name) => deps[name] || devDeps[name]);
1279
- return instantModuleName;
1280
- }
1281
-
1282
- async function getPackageJson(pkgDir) {
1283
- return await readJsonFile(join(pkgDir, 'package.json'));
1284
- }
1285
-
1286
- async function getPackageJSONWithErrorLogging(pkgDir) {
1287
- const pkgJson = await getPackageJson(pkgDir);
1288
- if (!pkgJson) {
1289
- error(`Couldn't find a packge.json file in: ${pkgDir}. Please add one.`);
1290
- return;
1291
- }
1292
- return pkgJson;
1293
- }
1294
-
1295
- async function enforcePackageAndAuthInfoWithErrorLogging(_opts) {
1296
- const projectInfo = await packageDirectoryWithErrorLogging();
1297
- if (!projectInfo) {
1298
- return;
1299
- }
1300
- const { dir: pkgDir, type: projectType } = projectInfo;
1301
-
1302
- // Deno projects don't have package.json or node_modules
1303
- if (projectType === 'deno') {
1304
- const authToken = await readConfigAuthTokenWithErrorLogging();
1305
- if (!authToken) {
1306
- return;
1307
- }
1308
- return {
1309
- pkgDir,
1310
- projectType,
1311
- instantModuleName: '@instantdb/core',
1312
- authToken,
1313
- };
1314
- }
1315
-
1316
- const pkgJson = await getPackageJSONWithErrorLogging(pkgDir);
1317
- if (!pkgJson) {
1318
- return;
1319
- }
1320
- const instantModuleName = await getInstantModuleName(pkgJson);
1321
- if (!instantModuleName) {
1322
- error("We couldn't find an Instant SDK. Install one, or run `init`");
1323
- }
1324
- const authToken = await readConfigAuthTokenWithErrorLogging();
1325
- if (!authToken) {
1326
- return;
1327
- }
1328
- return { pkgDir, projectType, instantModuleName, authToken };
1329
- }
1330
-
1331
- async function getOrPromptPackageAndAuthInfoWithErrorLogging(opts) {
1332
- const projectInfo = await packageDirectoryWithErrorLogging();
1333
- if (!projectInfo) {
1334
- return;
1335
- }
1336
- const { dir: pkgDir, type: projectType } = projectInfo;
1337
-
1338
- // Deno projects don't have package.json or node_modules
1339
- if (projectType === 'deno') {
1340
- const authToken = await readAuthTokenOrLoginWithErrorLogging();
1341
- if (!authToken) {
1342
- return;
1343
- }
1344
- return {
1345
- pkgDir,
1346
- projectType,
1347
- instantModuleName: '@instantdb/core',
1348
- authToken,
1349
- };
1350
- }
1351
-
1352
- const instantModuleName = await getOrInstallInstantModuleWithErrorLogging(
1353
- pkgDir,
1354
- opts,
1355
- );
1356
- if (!instantModuleName) {
1357
- return;
1358
- }
1359
- const authToken = await readAuthTokenOrLoginWithErrorLogging();
1360
- if (!authToken) {
1361
- return;
1362
- }
1363
- return { pkgDir, projectType, instantModuleName, authToken };
1364
- }
1365
-
1366
- async function pullSchema(
1367
- appId,
1368
- { pkgDir, instantModuleName, experimentalTypePreservation },
1369
- ) {
1370
- console.log('Pulling schema...');
1371
-
1372
- const pullRes = await fetchJson({
1373
- path: `/dash/apps/${appId}/schema/pull`,
1374
- debugName: 'Schema pull',
1375
- errorMessage: 'Failed to pull schema.',
1376
- command: 'pull',
1377
- });
1378
-
1379
- if (!pullRes.ok) return pullRes;
1380
-
1381
- if (
1382
- !countEntities(pullRes.data.schema.refs) &&
1383
- !countEntities(pullRes.data.schema.blobs)
1384
- ) {
1385
- console.log('Schema is empty. Skipping.');
1386
- return { ok: true };
1387
- }
1388
-
1389
- const prev = await readLocalSchemaFile();
1390
- const shortSchemaPath = getSchemaPathToWrite(prev?.path);
1391
- const schemaPath = join(pkgDir, shortSchemaPath);
1392
-
1393
- if (prev) {
1394
- const shouldContinue = await promptOk(
1395
- {
1396
- promptText: `This will overwrite your local ${shortSchemaPath} file, OK to proceed?`,
1397
- modifyOutput: UI.modifiers.yPadding,
1398
- inline: true,
1399
- },
1400
- program.opts(),
1401
- );
1402
- console.log();
1403
-
1404
- if (!shouldContinue) return { ok: true };
1405
- }
1406
-
1407
- let newSchemaContent = generateSchemaTypescriptFile(
1408
- prev?.schema,
1409
- apiSchemaToInstantSchemaDef(pullRes.data.schema),
1410
- instantModuleName,
1411
- );
1412
-
1413
- if (prev && experimentalTypePreservation) {
1414
- try {
1415
- const oldSchemaContent = await readFile(prev.path, 'utf-8');
1416
- newSchemaContent = mergeSchema(oldSchemaContent, newSchemaContent);
1417
- } catch (e) {
1418
- warn(
1419
- 'Failed to merge schema with existing file. Overwriting instead.',
1420
- e,
1421
- );
1422
- }
1423
- }
1424
-
1425
- await writeTypescript(schemaPath, newSchemaContent, 'utf-8');
1426
-
1427
- console.log('✅ Wrote schema to ' + shortSchemaPath);
1428
-
1429
- return { ok: true };
1430
- }
1431
-
1432
- async function pullPerms(appId, { pkgDir, instantModuleName }) {
1433
- console.log('Pulling perms...');
1434
-
1435
- const pullRes = await fetchJson({
1436
- path: `/dash/apps/${appId}/perms/pull`,
1437
- debugName: 'Perms pull',
1438
- errorMessage: 'Failed to pull perms.',
1439
- command: 'pull',
1440
- });
1441
-
1442
- if (!pullRes.ok) return pullRes;
1443
- const prev = await readLocalPermsFile();
1444
- const shortPermsPath = getPermsPathToWrite(prev?.path);
1445
- const permsPath = join(pkgDir, shortPermsPath);
1446
-
1447
- if (prev) {
1448
- const shouldContinue = await promptOk(
1449
- {
1450
- promptText: `This will overwrite your local ${shortPermsPath} file, OK to proceed?`,
1451
- modifyOutput: UI.modifiers.yPadding,
1452
- inline: true,
1453
- },
1454
- program.opts(),
1455
- );
1456
- console.log();
1457
-
1458
- if (!shouldContinue) return { ok: true };
1459
- }
1460
- await writeTypescript(
1461
- permsPath,
1462
- generatePermsTypescriptFile(pullRes.data.perms || {}, instantModuleName),
1463
- 'utf-8',
1464
- );
1465
-
1466
- console.log('✅ Wrote permissions to ' + shortPermsPath);
1467
-
1468
- return { ok: true };
1469
- }
1470
-
1471
- function indexingJobCompletedActionMessage(job) {
1472
- if (job.job_type === 'check-data-type') {
1473
- return `setting type of ${job.attr_name} to ${job.checked_data_type}`;
1474
- }
1475
- if (job.job_type === 'remove-data-type') {
1476
- return `removing type from ${job.attr_name}`;
1477
- }
1478
- if (job.job_type === 'index') {
1479
- return `adding index to ${job.attr_name}`;
1480
- }
1481
- if (job.job_type === 'remove-index') {
1482
- return `removing index from ${job.attr_name}`;
1483
- }
1484
- if (job.job_type === 'unique') {
1485
- return `adding uniqueness constraint to ${job.attr_name}`;
1486
- }
1487
- if (job.job_type === 'remove-unique') {
1488
- return `removing uniqueness constraint from ${job.attr_name}`;
1489
- }
1490
- if (job.job_type === 'required') {
1491
- return `adding required constraint to ${job.attr_name}`;
1492
- }
1493
- if (job.job_type === 'remove-required') {
1494
- return `removing required constraint from ${job.attr_name}`;
1495
- }
1496
- return `unexpected job type ${job.job_type} - please ping us on discord with this job id (${job.id})`;
1497
- }
1498
-
1499
- function truncate(s, maxLen) {
1500
- if (s.length > maxLen) {
1501
- return `${s.substr(0, maxLen - 3)}...`;
1502
- }
1503
- return s;
1504
- }
1505
-
1506
- function formatSamples(triples_samples) {
1507
- return triples_samples.slice(0, 3).map((t) => {
1508
- return { ...t, value: truncate(JSON.stringify(t.value), 32) };
1509
- });
1510
- }
1511
-
1512
- function createUrl(triple, job) {
1513
- const urlParams = new URLSearchParams({
1514
- s: 'main',
1515
- app: job.app_id,
1516
- t: 'explorer',
1517
- ns: job.attr_name.split('.')[0],
1518
- where: JSON.stringify(['id', triple.entity_id]),
1519
- });
1520
- const url = new URL(instantDashOrigin);
1521
- url.pathname = '/dash';
1522
- url.search = urlParams.toString();
1523
- return url;
1524
- }
1525
-
1526
- function padCell(value, width) {
1527
- const trimmed = value.length > width ? value.substring(0, width) : value;
1528
- return trimmed + ' '.repeat(width - trimmed.length);
1529
- }
1530
-
1531
- function indexingJobCompletedMessage(job) {
1532
- const actionMessage = indexingJobCompletedActionMessage(job);
1533
- if (job.job_status === 'canceled') {
1534
- return `Canceled ${actionMessage} before it could finish.`;
1535
- }
1536
- if (job.job_status === 'completed') {
1537
- return `Finished ${actionMessage}.`;
1538
- }
1539
- if (job.job_status === 'errored') {
1540
- if (job.invalid_triples_sample?.length) {
1541
- const [etype, label] = job.attr_name.split('.');
1542
- const samples = formatSamples(job.invalid_triples_sample);
1543
- const longestValue = samples.reduce(
1544
- (acc, { value }) => Math.max(acc, value.length),
1545
- label.length,
1546
- );
1547
-
1548
- const columns = [
1549
- { header: 'namespace', width: 15, getValue: () => etype },
1550
- {
1551
- header: 'id',
1552
- width: 37,
1553
- getValue: (triple) =>
1554
- terminalLink(triple.entity_id, createUrl(triple, job).toString(), {
1555
- fallback: () => triple.entity_id,
1556
- }),
1557
- },
1558
- {
1559
- header: label,
1560
- width: longestValue + 2,
1561
- getValue: (triple) => triple.value,
1562
- },
1563
- { header: 'type', width: 8, getValue: (triple) => triple.json_type },
1564
- ];
1565
-
1566
- let msg = `${chalk.red('INVALID DATA')} ${actionMessage}.\n`;
1567
- if (job.invalid_unique_value) {
1568
- msg += ` Found multiple entities with value ${truncate(JSON.stringify(job.invalid_unique_value), 64)}.\n`;
1569
- }
1570
- if (job.error === 'triple-too-large-error') {
1571
- msg += ` Some of the existing data is too large to index.\n`;
1572
- }
1573
-
1574
- msg += ` First few examples:\n`;
1575
- msg += ` ${columns.map((col) => chalk.bold(padCell(col.header, col.width))).join(' | ')}\n`;
1576
- msg += ` ${columns.map((col) => '-'.repeat(col.width)).join('-|-')}\n`;
1577
-
1578
- for (const triple of samples) {
1579
- const cells = columns.map((col) =>
1580
- padCell(col.getValue(triple), col.width),
1581
- );
1582
- msg += ` ${cells.join(' | ')}\n`;
1583
- }
1584
- return msg;
1585
- }
1586
- return `Error ${actionMessage}.`;
1587
- }
1588
- }
1589
-
1590
- function joinInSentence(items) {
1591
- if (items.length === 0) {
1592
- return '';
1593
- }
1594
- if (items.length === 1) {
1595
- return items[0];
1596
- }
1597
- if (items.length === 2) {
1598
- return `${items[0]} and ${items[1]}`;
1599
- }
1600
- return `${items.slice(0, -1).join(', ')}, and ${items[items.length - 1]}`;
1601
- }
1602
-
1603
- function jobGroupDescription(jobs) {
1604
- const actions = new Set();
1605
- const jobActions = {
1606
- 'check-data-type': 'updating types',
1607
- 'remove-data-type': 'updating types',
1608
- index: 'updating indexes',
1609
- 'remove-index': 'updating indexes',
1610
- unique: 'updating uniqueness constraints',
1611
- 'remove-unique': 'updating uniqueness constraints',
1612
- required: 'making attributes required',
1613
- 'remove-required': 'making attributes optional',
1614
- };
1615
- for (const job of jobs) {
1616
- actions.add(jobActions[job.job_type]);
1617
- }
1618
- return joinInSentence([...actions].sort()) || 'updating schema';
1619
- }
1620
-
1621
- async function waitForIndexingJobsToFinish(appId, data) {
1622
- const spinnerDefferedPromise = deferred();
1623
- const spinner = new UI.Spinner({
1624
- promise: spinnerDefferedPromise.promise,
1625
- });
1626
- const spinnerRenderPromise = renderUnwrap(spinner);
1627
-
1628
- const groupId = data['group-id'];
1629
- let jobs = data.jobs;
1630
- let waitMs = 20;
1631
- let lastUpdatedAt = new Date(0);
1632
-
1633
- const completedIds = new Set();
1634
-
1635
- const errorMessages = [];
1636
-
1637
- while (true) {
1638
- let stillRunning = false;
1639
- let updated = false;
1640
- let workEstimateTotal = 0;
1641
- let workCompletedTotal = 0;
1642
-
1643
- for (const job of jobs) {
1644
- const updatedAt = new Date(job.updated_at);
1645
- if (updatedAt > lastUpdatedAt) {
1646
- updated = true;
1647
- lastUpdatedAt = updatedAt;
1648
- }
1649
- if (job.job_status === 'waiting' || job.job_status === 'processing') {
1650
- stillRunning = true;
1651
- // Default estimate to high value to prevent % from jumping around
1652
- workEstimateTotal += job.work_estimate ?? 50000;
1653
- workCompletedTotal += job.work_completed ?? 0;
1654
- } else {
1655
- if (!completedIds.has(job.id)) {
1656
- completedIds.add(job.id);
1657
- const msg = indexingJobCompletedMessage(job);
1658
- if (msg) {
1659
- if (job.job_status === 'errored') {
1660
- spinner.addMessage(msg);
1661
- errorMessages.push(msg);
1662
- } else {
1663
- spinner.addMessage(msg);
1664
- }
1665
- }
1666
- }
1667
- }
1668
- }
1669
- if (!stillRunning) {
1670
- break;
1671
- }
1672
- if (workEstimateTotal) {
1673
- const percent = Math.floor(
1674
- (workCompletedTotal / workEstimateTotal) * 100,
1675
- );
1676
- spinner.updateText(`${jobGroupDescription(jobs)} ${percent}%`);
1677
- }
1678
- waitMs = updated ? 1 : Math.min(10000, waitMs * 2);
1679
- await sleep(waitMs);
1680
- const res = await fetchJson({
1681
- debugName: 'Check indexing status',
1682
- method: 'GET',
1683
- path: `/dash/apps/${appId}/indexing-jobs/group/${groupId}`,
1684
- errorMessage: 'Failed to check indexing status.',
1685
- command: 'push',
1686
- });
1687
- if (!res.ok) {
1688
- break;
1689
- }
1690
- jobs = res.data.jobs;
1691
- }
1692
-
1693
- spinnerDefferedPromise.resolve(null);
1694
-
1695
- await spinnerRenderPromise;
1696
-
1697
- // Log errors at the end so that they're easier to see.
1698
- if (errorMessages.length) {
1699
- for (const msg of errorMessages) {
1700
- console.log(msg);
1701
- }
1702
- console.log(chalk.red('Some steps failed while updating schema.'));
1703
- process.exit(1);
1704
- }
1705
- }
1706
-
1707
- const resolveRenames = async (created, promptData, extraInfo) => {
1708
- const answer = await renderUnwrap(
1709
- new ResolveRenamePrompt(
1710
- created,
1711
- promptData,
1712
- extraInfo,
1713
- UI.modifiers.piped([
1714
- (out) =>
1715
- boxen(out, {
1716
- dimBorder: true,
1717
- padding: {
1718
- left: 1,
1719
- right: 1,
1720
- },
1721
- }),
1722
- UI.modifiers.vanishOnComplete,
1723
- ]),
1724
- ),
1725
- );
1726
- return answer;
1727
- };
1728
-
1729
- async function pushSchema(appId, opts) {
1730
- const res = await readLocalSchemaFileWithErrorLogging();
1731
- if (!res) return { ok: false };
1732
- const { schema } = res;
1733
-
1734
- const pulledSchemaResponse = await fetchJson({
1735
- method: 'GET',
1736
- path: `/dash/apps/${appId}/schema/pull`,
1737
- debugName: 'Schema plan',
1738
- errorMessage: 'Failed to get old schema.',
1739
- command: 'push',
1740
- });
1741
-
1742
- if (!pulledSchemaResponse.ok) return pulledSchemaResponse;
1743
-
1744
- const currentAttrs = pulledSchemaResponse.data['attrs'];
1745
- const currentApiSchema = pulledSchemaResponse.data['schema'];
1746
- const oldSchema = apiSchemaToInstantSchemaDef(currentApiSchema, {
1747
- disableTypeInference: true,
1748
- });
1749
-
1750
- const systemCatalogIdentNames = collectSystemCatalogIdentNames(currentAttrs);
1751
-
1752
- try {
1753
- validateSchema(schema, systemCatalogIdentNames);
1754
- } catch (error) {
1755
- if (error instanceof SchemaValidationError) {
1756
- console.error(chalk.red('Invalid schema:', error.message));
1757
- } else {
1758
- console.error('Unexpected error:', error);
1759
- }
1760
- return { ok: false };
1761
- }
1762
-
1763
- const renames = opts.rename && Array.isArray(opts.rename) ? opts.rename : [];
1764
- const renameSelector = program.optsWithGlobals().yes
1765
- ? buildAutoRenameSelector(renames)
1766
- : resolveRenames;
1767
-
1768
- const diffResult = await diffSchemas(
1769
- oldSchema,
1770
- schema,
1771
- renameSelector,
1772
- systemCatalogIdentNames,
1773
- );
1774
-
1775
- if (currentAttrs === undefined) {
1776
- throw new Error("Couldn't get current schema from server");
1777
- }
1778
-
1779
- const txSteps = convertTxSteps(diffResult, currentAttrs);
1780
-
1781
- if (txSteps.length === 0) {
1782
- console.log(chalk.bgGray('No schema changes to apply!'));
1783
- return { ok: true };
1784
- }
1785
-
1786
- let wantsToPush = false;
1787
- try {
1788
- const groupedSteps = groupSteps(diffResult);
1789
- const lines = renderSchemaPlan(groupedSteps, currentAttrs);
1790
- if (program.optsWithGlobals().yes) {
1791
- console.log('Applying schema changes...');
1792
- console.log(lines.join('\n'));
1793
- }
1794
- wantsToPush = await promptOk(
1795
- {
1796
- promptText: 'Push these changes?',
1797
- yesText: 'Push',
1798
- noText: 'Cancel',
1799
- modifyOutput: (output) => {
1800
- let both = lines.join('\n') + '\n\n' + output;
1801
- return boxen(both, {
1802
- dimBorder: true,
1803
- padding: {
1804
- left: 1,
1805
- right: 1,
1806
- },
1807
- });
1808
- },
1809
- },
1810
- program.opts(),
1811
- );
1812
- } catch (error) {
1813
- if (error instanceof CancelSchemaError) {
1814
- console.info('Schema migration cancelled!');
1815
- }
1816
- return { ok: false };
1817
- }
1818
-
1819
- if (verbose) {
1820
- console.log(txSteps);
1821
- }
1822
-
1823
- if (wantsToPush) {
1824
- const applyRes = await fetchJson({
1825
- method: 'POST',
1826
- path: `/dash/apps/${appId}/schema/steps/apply`,
1827
- debugName: 'Schema apply',
1828
- errorMessage: 'Failed to update schema.',
1829
- body: {
1830
- steps: txSteps,
1831
- },
1832
- command: 'push',
1833
- });
1834
- console.log(chalk.green('Schema updated!'));
1835
- if (!applyRes.ok) return applyRes;
1836
-
1837
- if (applyRes.data['indexing-jobs']) {
1838
- await waitForIndexingJobsToFinish(appId, applyRes.data['indexing-jobs']);
1839
- }
1840
- } else {
1841
- console.info('Schema migration cancelled!');
1842
- }
1843
-
1844
- return { ok: true };
1845
- }
1846
-
1847
- async function claimEphemeralApp(appId, adminToken, authToken) {
1848
- const res = await fetchJson({
1849
- method: 'POST',
1850
- body: {
1851
- app_id: appId,
1852
- token: adminToken,
1853
- },
1854
- path: `/dash/apps/ephemeral/${appId}/claim`,
1855
- debugName: 'Claim ephemeral app',
1856
- errorMessage: 'Failed to claim ephemeral app.',
1857
- command: 'claim',
1858
- authToken,
1859
- });
1860
-
1861
- if (!res.ok) return res;
1862
-
1863
- console.log(chalk.green('App claimed!'));
1864
- return { ok: true };
1865
- }
1866
-
1867
- async function pushPerms(appId) {
1868
- const res = await readLocalPermsFileWithErrorLogging();
1869
- if (!res) {
1870
- return { ok: true };
1871
- }
1872
-
1873
- console.log('Planning perms...');
1874
-
1875
- const prodPerms = await fetchJson({
1876
- path: `/dash/apps/${appId}/perms/pull`,
1877
- debugName: 'Perms pull',
1878
- errorMessage: 'Failed to pull perms.',
1879
- command: 'push',
1880
- });
1881
-
1882
- if (!prodPerms.ok) return prodPerms;
1883
-
1884
- const diffedStr = jsonDiff.diffString(
1885
- prodPerms.data.perms || {},
1886
- res.perms || {},
1887
- );
1888
- if (!diffedStr.length) {
1889
- console.log('No perms changes detected. Skipping.');
1890
- return { ok: true };
1891
- }
1892
-
1893
- const okPush = await promptOk(
1894
- {
1895
- promptText: 'Push these changes to your perms?',
1896
- modifyOutput: (output) => {
1897
- let both = diffedStr + '\n' + output;
1898
- return boxen(both, {
1899
- dimBorder: true,
1900
- padding: {
1901
- left: 1,
1902
- right: 1,
1903
- },
1904
- });
1905
- },
1906
- },
1907
- program.opts(),
1908
- );
1909
- if (!okPush) return { ok: true };
1910
-
1911
- const permsRes = await fetchJson({
1912
- method: 'POST',
1913
- path: `/dash/apps/${appId}/rules`,
1914
- debugName: 'Schema apply',
1915
- errorMessage: 'Failed to update schema.',
1916
- body: {
1917
- code: res.perms,
1918
- },
1919
- command: 'push',
1920
- });
1921
-
1922
- if (!permsRes.ok) return permsRes;
1923
-
1924
- console.log(chalk.green('Permissions updated!'));
1925
-
1926
- return { ok: true };
1927
- }
1928
-
1929
- async function waitForAuthToken({ secret }) {
1930
- for (let i = 1; i <= 120; i++) {
1931
- await sleep(1000);
1932
- const authCheckRes = await fetchJson({
1933
- method: 'POST',
1934
- debugName: 'Auth check',
1935
- errorMessage: 'Failed to check auth status.',
1936
- path: '/dash/cli/auth/check',
1937
- body: { secret },
1938
- noAuth: true,
1939
- noLogError: true,
1940
- command: 'login',
1941
- });
1942
- if (authCheckRes.ok) {
1943
- return authCheckRes.data;
1944
- }
1945
- if (authCheckRes.data?.hint.errors?.[0]?.issue === 'waiting-for-user') {
1946
- continue;
1947
- }
1948
- error('Failed to authenticate ');
1949
- prettyPrintJSONErr(authCheckRes.data);
1950
- return;
1951
- }
1952
- error('Timed out waiting for authentication');
1953
- return null;
1954
- }
1955
-
1956
- // resources
1957
-
1958
- /**
1959
- * Fetches JSON data from a specified path using the POST method.
1960
- *
1961
- * @param {Object} options
1962
- * @param {string} options.debugName
1963
- * @param {string} options.errorMessage
1964
- * @param {string} options.path
1965
- * @param {'POST' | 'GET'} [options.method]
1966
- * @param {Object} [options.body=undefined]
1967
- * @param {boolean} [options.noAuth]
1968
- * @param {boolean} [options.noLogError]
1969
- * @param {string} [options.command] - The CLI command being executed (e.g., 'push', 'pull', 'login')
1970
- * @param {string} [options.authToken] - Optional auth token to use instead of reading from config
1971
- * @param {Record<string, string>} [options.headers] - Extra headers to include in the request
1972
- * @returns {Promise<{ ok: boolean; data: any }>}
1973
- */
1974
- async function fetchJson({
1975
- debugName,
1976
- errorMessage,
1977
- path,
1978
- body,
1979
- method,
1980
- noAuth,
1981
- noLogError,
1982
- command,
1983
- authToken: providedAuthToken,
1984
- headers: extraHeaders,
1985
- }) {
1986
- const withAuth = !noAuth;
1987
- const withErrorLogging = !noLogError;
1988
- let authToken = null;
1989
- if (withAuth) {
1990
- authToken =
1991
- providedAuthToken ?? (await readConfigAuthTokenWithErrorLogging());
1992
- if (!authToken) {
1993
- return { ok: false, data: undefined };
1994
- }
1995
- }
1996
- const timeoutMs = 1000 * 60 * 5; // 5 minutes
1997
-
1998
- try {
1999
- const res = await fetch(`${instantBackendOrigin}${path}`, {
2000
- method: method ?? 'GET',
2001
- headers: {
2002
- ...(withAuth ? { Authorization: `Bearer ${authToken}` } : {}),
2003
- 'Content-Type': 'application/json',
2004
- 'X-Instant-Source': 'instant-cli',
2005
- 'X-Instant-Version': version,
2006
- ...(command ? { 'X-Instant-Command': command } : {}),
2007
- ...extraHeaders,
2008
- },
2009
- body: body ? JSON.stringify(body) : undefined,
2010
- signal: AbortSignal.timeout(timeoutMs),
2011
- });
2012
-
2013
- if (verbose) {
2014
- console.log(debugName, 'response:', res.status, res.statusText);
2015
- }
2016
-
2017
- let data;
2018
- try {
2019
- data = await res.json();
2020
- } catch {
2021
- data = null;
2022
- }
2023
- if (verbose && data) {
2024
- console.log(debugName, 'json:', JSON.stringify(data, null, 2));
2025
- }
2026
- if (!res.ok) {
2027
- if (withErrorLogging) {
2028
- error(errorMessage);
2029
- prettyPrintJSONErr(data);
2030
- }
2031
- return { ok: false, data };
2032
- }
2033
-
2034
- return { ok: true, data };
2035
- } catch (err) {
2036
- if (withErrorLogging) {
2037
- if (err.name === 'AbortError') {
2038
- error(
2039
- `Timeout: It took more than ${timeoutMs / 60000} minutes to get the result.`,
2040
- );
2041
- } else {
2042
- error(`Error: type: ${err.name}, message: ${err.message}`);
2043
- }
2044
- }
2045
- return { ok: false, data: null };
2046
- }
2047
- }
2048
-
2049
- function prettyPrintJSONErr(data) {
2050
- if (data?.message) {
2051
- error(data.message);
2052
- }
2053
- if (Array.isArray(data?.hint?.errors)) {
2054
- for (const err of data.hint.errors) {
2055
- error(`${err.in ? err.in.join('->') + ': ' : ''}${err.message}`);
2056
- }
2057
- }
2058
- if (!data) {
2059
- error('Failed to parse error response');
2060
- }
2061
- }
2062
-
2063
- async function readLocalPermsFile() {
2064
- const readCandidates = getPermsReadCandidates();
2065
- const res = await loadConfig({
2066
- sources: readCandidates,
2067
- merge: false,
2068
- });
2069
- if (!res.config) return;
2070
- const relativePath = path.relative(process.cwd(), res.sources[0]);
2071
- return { path: relativePath, perms: res.config };
2072
- }
2073
-
2074
- async function readLocalPermsFileWithErrorLogging() {
2075
- const res = await readLocalPermsFile();
2076
- if (!res) {
2077
- error(
2078
- `We couldn't find your ${chalk.yellow('`instant.perms.ts`')} file. Make sure it's in the root directory. (Hint: You can use an INSTANT_PERMS_FILE_PATH environment variable to specify it.)`,
2079
- );
2080
- }
2081
- return res;
2082
- }
2083
-
2084
- async function readLocalSchemaFile() {
2085
- const readCandidates = getSchemaReadCandidates();
2086
- const res = await loadConfig({
2087
- sources: readCandidates,
2088
- merge: false,
2089
- });
2090
- if (!res.config) return;
2091
- const relativePath = path.relative(process.cwd(), res.sources[0]);
2092
- return { path: relativePath, schema: res.config };
2093
- }
2094
-
2095
- async function readInstantConfigFile() {
2096
- return (
2097
- await loadConfig({
2098
- sources: [
2099
- // load from `instant.config.xx`
2100
- {
2101
- files: 'instant.config',
2102
- extensions: ['ts', 'mts', 'cts', 'js', 'mjs', 'cjs', 'json'],
2103
- },
2104
- ],
2105
- // if false, the only the first matched will be loaded
2106
- // if true, all matched will be loaded and deep merged
2107
- merge: false,
2108
- })
2109
- ).config;
2110
- }
2111
-
2112
- async function readLocalSchemaFileWithErrorLogging() {
2113
- const res = await readLocalSchemaFile();
2114
-
2115
- if (!res) {
2116
- error(
2117
- `We couldn't find your ${chalk.yellow('`instant.schema.ts`')} file. Make sure it's in the root directory. (Hint: You can use an INSTANT_SCHEMA_FILE_PATH environment variable to specify it.)`,
2118
- );
2119
- return;
2120
- }
2121
-
2122
- if (res.schema?.constructor?.name !== 'InstantSchemaDef') {
2123
- error("We couldn't find your schema export.");
2124
- error(
2125
- 'In your ' +
2126
- chalk.green('`instant.schema.ts`') +
2127
- ' file, make sure you ' +
2128
- chalk.green('`export default schema`'),
2129
- );
2130
- return;
2131
- }
2132
-
2133
- return res;
2134
- }
2135
-
2136
- async function readConfigAuthToken(allowAdminToken = true) {
2137
- const options = program.opts();
2138
- if (typeof options.token === 'string') {
2139
- return options.token;
2140
- }
2141
-
2142
- if (process.env.INSTANT_CLI_AUTH_TOKEN) {
2143
- return process.env.INSTANT_CLI_AUTH_TOKEN;
2144
- }
2145
-
2146
- if (allowAdminToken) {
2147
- const adminTokenNames = Object.values(potentialAdminTokenEnvs);
2148
- for (const envName of adminTokenNames) {
2149
- const token = process.env[envName];
2150
- if (token) {
2151
- return token;
2152
- }
2153
- }
2154
- }
2155
-
2156
- const authToken = await readFile(
2157
- getAuthPaths().authConfigFilePath,
2158
- 'utf-8',
2159
- ).catch(() => null);
2160
-
2161
- if (authToken) {
2162
- return authToken;
2163
- }
2164
-
2165
- return null;
2166
- }
2167
-
2168
- export async function readConfigAuthTokenWithErrorLogging() {
2169
- const token = await readConfigAuthToken();
2170
- if (!token) {
2171
- error(
2172
- `Looks like you are not logged in. Please log in with ${chalk.green('`instant-cli login`')}`,
2173
- );
2174
- }
2175
- return token;
2176
- }
2177
-
2178
- async function readAuthTokenOrLoginWithErrorLogging() {
2179
- const token = await readConfigAuthToken();
2180
- if (token) return token;
2181
- console.log(`Looks like you are not logged in...`);
2182
- console.log(`Let's log in!`);
2183
- return await login({});
2184
- }
2185
-
2186
- async function saveConfigAuthToken(authToken) {
2187
- const authPaths = getAuthPaths();
2188
-
2189
- await mkdir(authPaths.appConfigDirPath, {
2190
- recursive: true,
2191
- });
2192
-
2193
- return writeFile(authPaths.authConfigFilePath, authToken, 'utf-8');
2194
- }
2195
-
2196
- // utils
2197
-
2198
- function sleep(ms) {
2199
- return new Promise((resolve) => setTimeout(resolve, ms));
2200
- }
2201
-
2202
- function countEntities(o) {
2203
- return Object.keys(o).length;
2204
- }
2205
-
2206
- function sortedEntries(o) {
2207
- return Object.entries(o).sort(([a], [b]) => a.localeCompare(b));
2208
- }
2209
-
2210
- function capitalizeFirstLetter(string) {
2211
- return string.charAt(0).toUpperCase() + string.slice(1);
2212
- }
2213
-
2214
- // attr helpers
2215
- function identEtype(ident) {
2216
- return ident[1];
2217
- }
2218
-
2219
- function identLabel(ident) {
2220
- return ident[2];
2221
- }
2222
-
2223
- function identName(ident) {
2224
- return `${identEtype(ident)}.${identLabel(ident)}`;
2225
- }
2226
-
2227
- function attrFwdLabel(attr) {
2228
- return attr['forward-identity']?.[2];
2229
- }
2230
-
2231
- function attrFwdEtype(attr) {
2232
- return attr['forward-identity']?.[1];
2233
- }
2234
-
2235
- function attrRevLabel(attr) {
2236
- return attr['reverse-identity']?.[2];
2237
- }
2238
-
2239
- function attrRevEtype(attr) {
2240
- return attr['reverse-identity']?.[1];
2241
- }
2242
-
2243
- function attrFwdName(attr) {
2244
- return `${attrFwdEtype(attr)}.${attrFwdLabel(attr)}`;
2245
- }
2246
-
2247
- function attrRevName(attr) {
2248
- if (attr['reverse-identity']) {
2249
- return `${attrRevEtype(attr)}.${attrRevLabel(attr)}`;
2250
- }
2251
- }
2252
-
2253
- // templates and constants
2254
-
2255
- export const rels = {
2256
- 'many-false': ['many', 'many'],
2257
- 'one-true': ['one', 'one'],
2258
- 'many-true': ['many', 'one'],
2259
- 'one-false': ['one', 'many'],
2260
- };
2261
-
2262
- function isUUID(uuid) {
2263
- const uuidRegex =
2264
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2265
- return uuidRegex.test(uuid);
2266
- }
2267
-
2268
- async function detectAppIdFromOptsWithErrorLogging(opts) {
2269
- if (!opts.app) return { ok: true };
2270
- const appId = opts.app;
2271
- const config = await readInstantConfigFile();
2272
- const nameMatch = config?.apps?.[appId];
2273
- const namedAppId = nameMatch?.id && isUUID(nameMatch.id) ? nameMatch : null;
2274
- const uuidAppId = appId && isUUID(appId) ? appId : null;
2275
-
2276
- if (nameMatch && !namedAppId) {
2277
- error(`Expected \`${appId}\` to point to a UUID, but got ${nameMatch.id}.`);
2278
- return { ok: false };
2279
- }
2280
- if (!namedAppId && !uuidAppId) {
2281
- error(`Expected App ID to be a UUID, but got: ${chalk.red(appId)}`);
2282
- return { ok: false };
2283
- }
2284
- return { ok: true, appId: namedAppId || uuidAppId };
2285
- }
2286
-
2287
- function detectAppIdFromEnvWithErrorLogging() {
2288
- const found = Object.keys(potentialEnvs)
2289
- .map((type) => {
2290
- const envName = potentialEnvs[type];
2291
- const value = process.env[envName];
2292
- return { type, envName, value };
2293
- })
2294
- .find(({ value }) => !!value);
2295
- if (found && !isUUID(found.value)) {
2296
- error(
2297
- `Found ${chalk.green('`' + found.envName + '`')} but it's not a valid UUID.`,
2298
- );
2299
- return { ok: false, found };
2300
- }
2301
- return { ok: true, found };
2302
- }
2303
-
2304
- function detectAppIdAndAdminTokenFromEnvWithErrorLogging() {
2305
- const appIdResult = Object.keys(potentialEnvs)
2306
- .map((type) => {
2307
- const envName = potentialEnvs[type];
2308
- const value = process.env[envName];
2309
- return { type, envName, value };
2310
- })
2311
- .find(({ value }) => !!value);
2312
-
2313
- const adminTokenResult = Object.keys(potentialAdminTokenEnvs)
2314
- .map((type) => {
2315
- const envName = potentialAdminTokenEnvs[type];
2316
- const value = process.env[envName];
2317
- return { type, envName, value };
2318
- })
2319
- .find(({ value }) => !!value);
2320
-
2321
- if (appIdResult && !isUUID(appIdResult.value)) {
2322
- error(
2323
- `Found ${chalk.green('`' + appIdResult.envName + '`')} but it's not a valid UUID.`,
2324
- );
2325
- return { ok: false, appId: appIdResult, adminToken: adminTokenResult };
2326
- }
2327
-
2328
- return { ok: true, appId: appIdResult, adminToken: adminTokenResult };
2329
- }
2330
-
2331
- function appDashUrl(id) {
2332
- return `${instantDashOrigin}/dash?s=main&t=home&app=${id}`;
2333
- }