@capgo/cli 4.13.8 → 4.13.9

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 (57) hide show
  1. package/.github/FUNDING.yml +1 -0
  2. package/.github/workflows/build.yml +46 -0
  3. package/.github/workflows/bump_version.yml +56 -0
  4. package/.github/workflows/test.yml +30 -0
  5. package/.prettierignore +6 -0
  6. package/.vscode/launch.json +23 -0
  7. package/.vscode/settings.json +46 -0
  8. package/.vscode/tasks.json +42 -0
  9. package/CHANGELOG.md +2739 -0
  10. package/build.mjs +23 -0
  11. package/bun.lockb +0 -0
  12. package/capacitor.config.ts +33 -0
  13. package/crypto_explained.png +0 -0
  14. package/dist/index.js +114692 -205
  15. package/eslint.config.js +10 -0
  16. package/package.json +33 -32
  17. package/renovate.json +23 -0
  18. package/src/api/app.ts +55 -0
  19. package/src/api/channels.ts +140 -0
  20. package/src/api/crypto.ts +116 -0
  21. package/src/api/devices_override.ts +41 -0
  22. package/src/api/update.ts +13 -0
  23. package/src/api/versions.ts +101 -0
  24. package/src/app/add.ts +158 -0
  25. package/src/app/debug.ts +222 -0
  26. package/src/app/delete.ts +106 -0
  27. package/src/app/info.ts +90 -0
  28. package/src/app/list.ts +67 -0
  29. package/src/app/set.ts +94 -0
  30. package/src/bundle/check.ts +42 -0
  31. package/src/bundle/cleanup.ts +127 -0
  32. package/src/bundle/compatibility.ts +70 -0
  33. package/src/bundle/decrypt.ts +54 -0
  34. package/src/bundle/delete.ts +53 -0
  35. package/src/bundle/encrypt.ts +60 -0
  36. package/src/bundle/list.ts +43 -0
  37. package/src/bundle/unlink.ts +86 -0
  38. package/src/bundle/upload.ts +532 -0
  39. package/src/bundle/zip.ts +139 -0
  40. package/src/channel/add.ts +74 -0
  41. package/src/channel/currentBundle.ts +72 -0
  42. package/src/channel/delete.ts +52 -0
  43. package/src/channel/list.ts +49 -0
  44. package/src/channel/set.ts +178 -0
  45. package/src/index.ts +307 -0
  46. package/src/init.ts +342 -0
  47. package/src/key.ts +131 -0
  48. package/src/login.ts +70 -0
  49. package/src/types/capacitor__cli.d.ts +6 -0
  50. package/src/types/supabase.types.ts +2193 -0
  51. package/src/user/account.ts +11 -0
  52. package/src/utils.ts +956 -0
  53. package/test/chunk_convert.ts +28 -0
  54. package/test/data.ts +18769 -0
  55. package/test/test_headers_rls.ts +24 -0
  56. package/test/test_semver.ts +13 -0
  57. package/tsconfig.json +39 -0
package/src/utils.ts ADDED
@@ -0,0 +1,956 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
+ import { homedir } from 'node:os'
3
+ import { resolve } from 'node:path'
4
+ import process from 'node:process'
5
+ import type { Buffer } from 'node:buffer'
6
+ import { loadConfig } from '@capacitor/cli/dist/config'
7
+ import { program } from 'commander'
8
+ import type { SupabaseClient } from '@supabase/supabase-js'
9
+ import { createClient } from '@supabase/supabase-js'
10
+ import prettyjson from 'prettyjson'
11
+ import { LogSnag } from 'logsnag'
12
+ import * as p from '@clack/prompts'
13
+ import ky from 'ky'
14
+ import { promiseFiles } from 'node-dir'
15
+ import { findRootSync } from '@manypkg/find-root'
16
+ import type { InstallCommand, PackageManagerRunner, PackageManagerType } from '@capgo/find-package-manager'
17
+ import { findInstallCommand, findPackageManagerRunner, findPackageManagerType } from '@capgo/find-package-manager'
18
+ import type { Database } from './types/supabase.types'
19
+
20
+ export const baseKey = '.capgo_key'
21
+ export const baseKeyPub = `${baseKey}.pub`
22
+ export const defaultHost = 'https://capgo.app'
23
+ export const defaultApiHost = 'https://api.capgo.app'
24
+ export const defaultHostWeb = 'https://web.capgo.app'
25
+
26
+ export type ArrayElement<ArrayType extends readonly unknown[]> =
27
+ ArrayType extends readonly (infer ElementType)[] ? ElementType : never
28
+ export type Organization = ArrayElement<Database['public']['Functions']['get_orgs_v5']['Returns']>
29
+
30
+ export const regexSemver = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
31
+ export const formatError = (error: any) => error ? `\n${prettyjson.render(error)}` : ''
32
+
33
+ export interface OptionsBase {
34
+ apikey: string
35
+ }
36
+
37
+ export function wait(ms: number) {
38
+ return new Promise((resolve) => {
39
+ setTimeout(resolve, ms)
40
+ })
41
+ }
42
+
43
+ export async function getConfig() {
44
+ let config: Config
45
+ try {
46
+ config = await loadConfig()
47
+ }
48
+ catch (err) {
49
+ p.log.error(`No capacitor config file found, run \`cap init\` first ${formatError(err)}`)
50
+ program.error('')
51
+ }
52
+ return config
53
+ }
54
+
55
+ export async function getLocalConfig() {
56
+ try {
57
+ const config: Config = await getConfig()
58
+ const capConfig: Partial<CapgoConfig> = {
59
+ host: (config?.app?.extConfig?.plugins?.CapacitorUpdater?.localHost || defaultHost) as string,
60
+ hostWeb: (config?.app?.extConfig?.plugins?.CapacitorUpdater?.localWebHost || defaultHostWeb) as string,
61
+ }
62
+
63
+ if (config?.app?.extConfig?.plugins?.CapacitorUpdater?.localSupa && config?.app?.extConfig?.plugins?.CapacitorUpdater?.localSupaAnon) {
64
+ p.log.info('Using custom supabase instance from capacitor.config.json')
65
+ capConfig.supaKey = config?.app?.extConfig?.plugins?.CapacitorUpdater?.localSupaAnon
66
+ capConfig.supaHost = config?.app?.extConfig?.plugins?.CapacitorUpdater?.localSupa
67
+ }
68
+ return capConfig
69
+ }
70
+ catch (error) {
71
+ return {
72
+ host: defaultHost,
73
+ hostWeb: defaultHostWeb,
74
+ }
75
+ }
76
+ }
77
+
78
+ const nativeFileRegex = /([A-Za-z0-9]+)\.(java|swift|kt|scala)$/
79
+
80
+ interface CapgoConfig {
81
+ supaHost: string
82
+ supaKey: string
83
+ host: string
84
+ hostWeb: string
85
+ signKey: string
86
+ }
87
+ export async function getRemoteConfig() {
88
+ // call host + /api/get_config and parse the result as json using axios
89
+ const localConfig = await getLocalConfig()
90
+ return ky
91
+ .get(`${defaultApiHost}/private/config`)
92
+ .then(res => res.json<CapgoConfig>())
93
+ .then(data => ({ ...data, ...localConfig } as CapgoConfig))
94
+ .catch(() => {
95
+ p.log.info(`Local config ${formatError(localConfig)}`)
96
+ return localConfig
97
+ })
98
+ }
99
+
100
+ export async function createSupabaseClient(apikey: string) {
101
+ const config = await getRemoteConfig()
102
+ if (!config.supaHost || !config.supaKey) {
103
+ p.log.error('Cannot connect to server please try again later')
104
+ program.error('')
105
+ }
106
+ return createClient<Database>(config.supaHost, config.supaKey, {
107
+ auth: {
108
+ persistSession: false,
109
+ },
110
+ global: {
111
+ headers: {
112
+ capgkey: apikey,
113
+ },
114
+ },
115
+ })
116
+ }
117
+
118
+ export async function checkKey(supabase: SupabaseClient<Database>, apikey: string, keymode: Database['public']['Enums']['key_mode'][]) {
119
+ const { data: apiAccess } = await supabase
120
+ .rpc('is_allowed_capgkey', { apikey, keymode })
121
+ .single()
122
+
123
+ if (!apiAccess) {
124
+ p.log.error(`Invalid API key or insufficient permissions.`)
125
+ // create a string from keymode array with comma and space and "or" for the last one
126
+ const keymodeStr = keymode.map((k, i) => {
127
+ if (i === keymode.length - 1)
128
+ return `or ${k}`
129
+
130
+ return `${k}, `
131
+ }).join('')
132
+ p.log.error(`Your key should be: ${keymodeStr} mode.`)
133
+ program.error('')
134
+ }
135
+ }
136
+
137
+ export async function isGoodPlan(supabase: SupabaseClient<Database>, userId: string): Promise<boolean> {
138
+ const { data } = await supabase
139
+ .rpc('is_good_plan_v5', { userid: userId })
140
+ .single()
141
+ return data || false
142
+ }
143
+
144
+ export async function isPayingOrg(supabase: SupabaseClient<Database>, orgId: string): Promise<boolean> {
145
+ const { data } = await supabase
146
+ .rpc('is_paying_org', { orgid: orgId })
147
+ .single()
148
+ return data || false
149
+ }
150
+
151
+ export async function isTrialOrg(supabase: SupabaseClient<Database>, orgId: string): Promise<number> {
152
+ const { data } = await supabase
153
+ .rpc('is_trial_org', { orgid: orgId })
154
+ .single()
155
+ return data || 0
156
+ }
157
+
158
+ export async function isAllowedActionOrg(supabase: SupabaseClient<Database>, orgId: string): Promise<boolean> {
159
+ const { data } = await supabase
160
+ .rpc('is_allowed_action_org', { orgid: orgId })
161
+ .single()
162
+ return !!data
163
+ }
164
+
165
+ export async function isAllowedActionAppIdApiKey(supabase: SupabaseClient<Database>, appId: string, apikey: string): Promise<boolean> {
166
+ const { data } = await supabase
167
+ .rpc('is_allowed_action', { apikey, appid: appId })
168
+ .single()
169
+
170
+ return !!data
171
+ }
172
+
173
+ export async function isAllowedApp(supabase: SupabaseClient<Database>, apikey: string, appId: string): Promise<boolean> {
174
+ const { data } = await supabase
175
+ .rpc('is_app_owner', { apikey, appid: appId })
176
+ .single()
177
+ return !!data
178
+ }
179
+
180
+ export enum OrganizationPerm {
181
+ none = 0,
182
+ read = 1,
183
+ upload = 2,
184
+ write = 3,
185
+ admin = 4,
186
+ super_admin = 5,
187
+ }
188
+
189
+ export const hasOrganizationPerm = (perm: OrganizationPerm, required: OrganizationPerm): boolean => (perm as number) >= (required as number)
190
+
191
+ export async function isAllowedAppOrg(supabase: SupabaseClient<Database>, apikey: string, appId: string): Promise<{ okay: true, data: OrganizationPerm } | { okay: false, error: 'INVALID_APIKEY' | 'NO_APP' | 'NO_ORG' }> {
192
+ const { data, error } = await supabase
193
+ .rpc('get_org_perm_for_apikey', { apikey, app_id: appId })
194
+ .single()
195
+
196
+ if (error) {
197
+ p.log.error('Cannot get permissions for organization!')
198
+ console.error(error)
199
+ process.exit(1)
200
+ }
201
+
202
+ const ok = (data as string).includes('perm')
203
+ if (ok) {
204
+ let perm = null as (OrganizationPerm | null)
205
+
206
+ switch (data as string) {
207
+ case 'perm_none': {
208
+ perm = OrganizationPerm.none
209
+ break
210
+ }
211
+ case 'perm_read': {
212
+ perm = OrganizationPerm.read
213
+ break
214
+ }
215
+ case 'perm_upload': {
216
+ perm = OrganizationPerm.upload
217
+ break
218
+ }
219
+ case 'perm_write': {
220
+ perm = OrganizationPerm.write
221
+ break
222
+ }
223
+ case 'perm_admin': {
224
+ perm = OrganizationPerm.admin
225
+ break
226
+ }
227
+ case 'perm_owner': {
228
+ perm = OrganizationPerm.super_admin
229
+ break
230
+ }
231
+ default: {
232
+ if ((data as string).includes('invite')) {
233
+ p.log.info('Please accept/deny the organization invitation before trying to access the app')
234
+ process.exit(1)
235
+ }
236
+
237
+ p.log.error(`Invalid output when fetching organization permission. Response: ${data}`)
238
+ process.exit(1)
239
+ }
240
+ }
241
+
242
+ return {
243
+ okay: true,
244
+ data: perm,
245
+ }
246
+ }
247
+
248
+ // This means that something went wrong here
249
+ let functionError = null as 'INVALID_APIKEY' | 'NO_APP' | 'NO_ORG' | null
250
+
251
+ switch (data as string) {
252
+ case 'INVALID_APIKEY': {
253
+ functionError = 'INVALID_APIKEY'
254
+ break
255
+ }
256
+ case 'NO_APP': {
257
+ functionError = 'NO_APP'
258
+ break
259
+ }
260
+ case 'NO_ORG': {
261
+ functionError = 'NO_ORG'
262
+ break
263
+ }
264
+ default: {
265
+ p.log.error(`Invalid error when fetching organization permission. Response: ${data}`)
266
+ process.exit(1)
267
+ }
268
+ }
269
+
270
+ return {
271
+ okay: false,
272
+ error: functionError,
273
+ }
274
+ }
275
+
276
+ export async function checkPlanValid(supabase: SupabaseClient<Database>, orgId: string, apikey: string, appId?: string, warning = true) {
277
+ const config = await getRemoteConfig()
278
+
279
+ // isAllowedActionAppIdApiKey was updated in the orgs_v3 migration to work with the new system
280
+ const validPlan = await (appId ? isAllowedActionAppIdApiKey(supabase, appId, apikey) : isAllowedActionOrg(supabase, orgId))
281
+ if (!validPlan) {
282
+ p.log.error(`You need to upgrade your plan to continue to use capgo.\n Upgrade here: ${config.hostWeb}/dashboard/settings/plans\n`)
283
+ wait(100)
284
+ import('open')
285
+ .then((module) => {
286
+ module.default(`${config.hostWeb}/dashboard/settings/plans`)
287
+ })
288
+ wait(500)
289
+ program.error('')
290
+ }
291
+ const [trialDays, ispaying] = await Promise.all([
292
+ isTrialOrg(supabase, orgId),
293
+ isPayingOrg(supabase, orgId),
294
+ ])
295
+ if (trialDays > 0 && warning && !ispaying)
296
+ p.log.warn(`WARNING !!\nTrial expires in ${trialDays} days, upgrade here: ${config.hostWeb}/dashboard/settings/plans\n`)
297
+ }
298
+
299
+ export function findSavedKey(quiet = false) {
300
+ // search for key in home dir
301
+ const userHomeDir = homedir()
302
+ let key
303
+ let keyPath = `${userHomeDir}/.capgo`
304
+ if (existsSync(keyPath)) {
305
+ if (!quiet)
306
+ p.log.info(`Use global apy key ${keyPath}`)
307
+ key = readFileSync(keyPath, 'utf8').trim()
308
+ }
309
+ keyPath = `.capgo`
310
+ if (!key && existsSync(keyPath)) {
311
+ if (!quiet)
312
+ p.log.info(`Use local apy key ${keyPath}`)
313
+ key = readFileSync(keyPath, 'utf8').trim()
314
+ }
315
+ if (!key) {
316
+ p.log.error(`Cannot find API key in local folder or global, please login first with ${getPMAndCommand().runner} @capacitor/cli login`)
317
+ program.error('')
318
+ }
319
+ return key
320
+ }
321
+
322
+ async function* getFiles(dir: string): AsyncGenerator<string> {
323
+ const dirents = await readdirSync(dir, { withFileTypes: true })
324
+ for (const dirent of dirents) {
325
+ const res = resolve(dir, dirent.name)
326
+ if (dirent.isDirectory()
327
+ && !dirent.name.startsWith('.')
328
+ && !dirent.name.startsWith('node_modules')
329
+ && !dirent.name.startsWith('dist'))
330
+ yield * getFiles(res)
331
+ else
332
+ yield res
333
+ }
334
+ }
335
+
336
+ export async function findProjectType() {
337
+ // for nuxtjs check if nuxt.config.js exists
338
+ // for nextjs check if next.config.js exists
339
+ // for angular check if angular.json exists
340
+ // for sveltekit check if svelte.config.js exists
341
+ const pwd = process.cwd()
342
+ for await (const f of getFiles(pwd)) {
343
+ // find number of folder in path after pwd
344
+ if (f.includes('angular.json')) {
345
+ p.log.info('Found angular project')
346
+ return 'angular'
347
+ }
348
+ if (f.includes('nuxt.config.js')) {
349
+ p.log.info('Found nuxtjs project')
350
+ return 'nuxtjs'
351
+ }
352
+ if (f.includes('next.config.js')) {
353
+ p.log.info('Found nextjs project')
354
+ return 'nextjs'
355
+ }
356
+ if (f.includes('svelte.config.js')) {
357
+ p.log.info('Found sveltekit project')
358
+ return 'sveltekit'
359
+ }
360
+ }
361
+ return 'unknown'
362
+ }
363
+
364
+ export async function findMainFileForProjectType(projectType: string) {
365
+ if (projectType === 'angular')
366
+ return 'src/main.ts'
367
+
368
+ if (projectType === 'nuxtjs')
369
+ return 'src/main.ts'
370
+
371
+ if (projectType === 'nextjs')
372
+ return 'pages/_app.tsx'
373
+
374
+ if (projectType === 'sveltekit')
375
+ return 'src/main.ts'
376
+
377
+ return null
378
+ }
379
+
380
+ // create a function to find the right command to build the project in static mode depending on the project type
381
+
382
+ export async function findBuildCommandForProjectType(projectType: string) {
383
+ if (projectType === 'angular') {
384
+ p.log.info('Angular project detected')
385
+ return 'build'
386
+ }
387
+
388
+ if (projectType === 'nuxtjs') {
389
+ p.log.info('Nuxtjs project detected')
390
+ return 'generate'
391
+ }
392
+
393
+ if (projectType === 'nextjs') {
394
+ p.log.info('Nextjs project detected')
395
+ p.log.warn('Please make sure you have configured static export in your next.config.js: https://nextjs.org/docs/pages/building-your-application/deploying/static-exports')
396
+ p.log.warn('Please make sure you have the output: \'export\' and distDir: \'dist\' in your next.config.js')
397
+ const doContinue = await p.confirm({ message: 'Do you want to continue?' })
398
+ if (!doContinue) {
399
+ p.log.error('Aborted')
400
+ program.error('')
401
+ }
402
+ return 'build'
403
+ }
404
+
405
+ if (projectType === 'sveltekit') {
406
+ p.log.info('Sveltekit project detected')
407
+ p.log.warn('Please make sure you have the adapter-static installed: https://kit.svelte.dev/docs/adapter-static')
408
+ p.log.warn('Please make sure you have the pages: \'dist\' and assets: \'dest\', in your svelte.config.js adaptater')
409
+ const doContinue = await p.confirm({ message: 'Do you want to continue?' })
410
+ if (!doContinue) {
411
+ p.log.error('Aborted')
412
+ program.error('')
413
+ }
414
+ return 'build'
415
+ }
416
+
417
+ return 'build'
418
+ }
419
+
420
+ export async function findMainFile() {
421
+ const mainRegex = /(main|index)\.(ts|tsx|js|jsx)$/
422
+ // search for main.ts or main.js in local dir and subdirs
423
+ let mainFile = ''
424
+ const pwd = process.cwd()
425
+ const pwdL = pwd.split('/').length
426
+ for await (const f of getFiles(pwd)) {
427
+ // find number of folder in path after pwd
428
+ const folders = f.split('/').length - pwdL
429
+ if (folders <= 2 && mainRegex.test(f)) {
430
+ mainFile = f
431
+ p.log.info(`Found main file here ${f}`)
432
+ break
433
+ }
434
+ }
435
+ return mainFile
436
+ }
437
+
438
+ interface Config {
439
+ app: {
440
+ appId: string
441
+ appName: string
442
+ webDir: string
443
+ package: {
444
+ version: string
445
+ }
446
+ extConfigFilePath: string
447
+ extConfig: {
448
+ extConfig: object
449
+ plugins: {
450
+ extConfig: object
451
+ CapacitorUpdater: {
452
+ appReadyTimeout?: number
453
+ responseTimeout?: number
454
+ autoDeleteFailed?: boolean
455
+ autoDeletePrevious?: boolean
456
+ autoUpdate?: boolean
457
+ resetWhenUpdate?: boolean
458
+ updateUrl?: string
459
+ statsUrl?: string
460
+ privateKey?: string
461
+ version?: string
462
+ directUpdate?: boolean
463
+ periodCheckDelay?: number
464
+ localS3?: boolean
465
+ localHost?: string
466
+ localWebHost?: string
467
+ localSupa?: string
468
+ localSupaAnon?: string
469
+ allowModifyUrl?: boolean
470
+ defaultChannel?: string
471
+ }
472
+ }
473
+ server: {
474
+ cleartext: boolean
475
+ url: string
476
+ }
477
+ }
478
+ }
479
+ }
480
+
481
+ export async function updateOrCreateVersion(supabase: SupabaseClient<Database>, update: Database['public']['Tables']['app_versions']['Insert']) {
482
+ return supabase.from('app_versions')
483
+ .upsert(update, { onConflict: 'name,app_id' })
484
+ .eq('app_id', update.app_id)
485
+ .eq('name', update.name)
486
+ }
487
+
488
+ export async function uploadUrl(supabase: SupabaseClient<Database>, appId: string, name: string): Promise<string> {
489
+ const data = {
490
+ app_id: appId,
491
+ name,
492
+ version: 0,
493
+ }
494
+ try {
495
+ const pathUploadLink = 'private/upload_link'
496
+ const res = await supabase.functions.invoke(pathUploadLink, { body: JSON.stringify(data) })
497
+ return res.data.url
498
+ }
499
+ catch (error) {
500
+ p.log.error(`Cannot get upload url ${formatError(error)}`)
501
+ }
502
+ return ''
503
+ }
504
+
505
+ async function prepareMultipart(supabase: SupabaseClient<Database>, appId: string, name: string): Promise<{ key: string, uploadId: string, url: string } | null> {
506
+ const data = {
507
+ app_id: appId,
508
+ name,
509
+ version: 1,
510
+ }
511
+ try {
512
+ const pathUploadLink = 'private/upload_link'
513
+ const res = await supabase.functions.invoke(pathUploadLink, { body: JSON.stringify(data) })
514
+ return res.data as any
515
+ }
516
+ catch (error) {
517
+ p.log.error(`Cannot get upload url ${formatError(error)}`)
518
+ return null
519
+ }
520
+ }
521
+
522
+ async function finishMultipartDownload(key: string, uploadId: string, url: string, parts: any[]) {
523
+ const metadata = {
524
+ action: 'mpu-complete',
525
+ uploadId,
526
+ key,
527
+ }
528
+
529
+ await ky.post(url, {
530
+ json: {
531
+ parts,
532
+ },
533
+ searchParams: new URLSearchParams({ body: btoa(JSON.stringify(metadata)) }),
534
+ })
535
+
536
+ // console.log(await response.json())
537
+ }
538
+
539
+ const PART_SIZE = 10 * 1024 * 1024
540
+ export async function uploadMultipart(supabase: SupabaseClient<Database>, appId: string, name: string, data: Buffer): Promise<boolean> {
541
+ try {
542
+ const multipartPrep = await prepareMultipart(supabase, appId, name)
543
+ if (!multipartPrep) {
544
+ // Just pass the error
545
+ return false
546
+ }
547
+
548
+ const fileSize = data.length
549
+ const partCount = Math.ceil(fileSize / PART_SIZE)
550
+
551
+ const uploadPromises = Array.from({ length: partCount }, (_, index) =>
552
+ uploadPart(data, PART_SIZE, multipartPrep.url, multipartPrep.key, multipartPrep.uploadId, index))
553
+
554
+ const parts = await Promise.all(uploadPromises)
555
+
556
+ await finishMultipartDownload(multipartPrep.key, multipartPrep.uploadId, multipartPrep.url, parts)
557
+
558
+ return true
559
+ }
560
+ catch (e) {
561
+ p.log.error(`Could not upload via multipart ${formatError(e)}`)
562
+ return false
563
+ }
564
+ }
565
+
566
+ export async function deletedFailedVersion(supabase: SupabaseClient<Database>, appId: string, name: string): Promise<void> {
567
+ const data = {
568
+ app_id: appId,
569
+ name,
570
+ }
571
+ try {
572
+ const pathFailed = 'private/delete_failed_version'
573
+ const res = await supabase.functions.invoke(pathFailed, { body: JSON.stringify(data), method: 'DELETE' })
574
+ return res.data.url
575
+ }
576
+ catch (error) {
577
+ p.log.error(`Cannot delete failed version ${formatError(error)}`)
578
+ return Promise.reject(new Error('Cannot delete failed version'))
579
+ }
580
+ }
581
+
582
+ async function uploadPart(
583
+ data: Buffer,
584
+ partsize: number,
585
+ url: string,
586
+ key: string,
587
+ uploadId: string,
588
+ index: number,
589
+ ) {
590
+ const dataToUpload = data.subarray(
591
+ partsize * index,
592
+ partsize * (index + 1),
593
+ )
594
+
595
+ const metadata = {
596
+ action: 'mpu-uploadpart',
597
+ uploadId,
598
+ partNumber: index + 1,
599
+ key,
600
+ }
601
+
602
+ const response = await ky.put(url, {
603
+ body: dataToUpload,
604
+ searchParams: new URLSearchParams({ body: btoa(JSON.stringify(metadata)) }),
605
+ })
606
+
607
+ return await response.json()
608
+ }
609
+
610
+ export async function updateOrCreateChannel(supabase: SupabaseClient<Database>, update: Database['public']['Tables']['channels']['Insert']) {
611
+ // console.log('updateOrCreateChannel', update)
612
+ if (!update.app_id || !update.name || !update.created_by) {
613
+ p.log.error('missing app_id, name, or created_by')
614
+ return Promise.reject(new Error('missing app_id, name, or created_by'))
615
+ }
616
+ const { data, error } = await supabase
617
+ .from('channels')
618
+ .select('enable_progressive_deploy, secondaryVersionPercentage, secondVersion')
619
+ .eq('app_id', update.app_id)
620
+ .eq('name', update.name)
621
+ // .eq('created_by', update.created_by)
622
+ .single()
623
+
624
+ if (data && !error) {
625
+ if (data.enable_progressive_deploy) {
626
+ p.log.info('Progressive deploy is enabled')
627
+
628
+ if (data.secondaryVersionPercentage !== 1)
629
+ p.log.warn('Latest progressive deploy has not finished')
630
+
631
+ update.secondVersion = update.version
632
+ if (!data.secondVersion) {
633
+ p.log.error('missing secondVersion')
634
+ return Promise.reject(new Error('missing secondVersion'))
635
+ }
636
+ update.version = data.secondVersion
637
+ update.secondaryVersionPercentage = 0.1
638
+ p.log.info('Started new progressive upload!')
639
+
640
+ // update.version = undefined
641
+ }
642
+ return supabase
643
+ .from('channels')
644
+ .update(update)
645
+ .eq('app_id', update.app_id)
646
+ .eq('name', update.name)
647
+ // .eq('created_by', update.created_by)
648
+ .select()
649
+ .single()
650
+ }
651
+
652
+ return supabase
653
+ .from('channels')
654
+ .insert(update)
655
+ .select()
656
+ .single()
657
+ }
658
+
659
+ export function useLogSnag(): LogSnag {
660
+ const logsnag = new LogSnag({
661
+ token: process.env.CAPGO_LOGSNAG ?? 'c124f5e9d0ce5bdd14bbb48f815d5583',
662
+ project: process.env.CAPGO_LOGSNAG_PROJECT ?? 'capgo',
663
+ })
664
+ return logsnag
665
+ }
666
+
667
+ export async function getOrganization(supabase: SupabaseClient<Database>, roles: string[]): Promise<Organization> {
668
+ const { error: orgError, data: allOrganizations } = await supabase
669
+ .rpc('get_orgs_v5')
670
+
671
+ if (orgError) {
672
+ p.log.error('Cannot get the list of organizations - exiting')
673
+ p.log.error(`Error ${JSON.stringify(orgError)}`)
674
+ program.error('')
675
+ }
676
+
677
+ const adminOrgs = allOrganizations.filter(org => !!roles.find(role => role === org.role))
678
+
679
+ if (adminOrgs.length === 0) {
680
+ p.log.error(`Could not get organization with roles: ${roles.join(' or ')} because the user does not have any org`)
681
+ program.error('')
682
+ }
683
+
684
+ const organizationUidRaw = (adminOrgs.length > 1)
685
+ ? await p.select({
686
+ message: 'Please pick the organization that you want to insert to',
687
+ options: adminOrgs.map((org) => {
688
+ return { value: org.gid, label: org.name }
689
+ }),
690
+ })
691
+ : adminOrgs[0].gid
692
+
693
+ if (p.isCancel(organizationUidRaw)) {
694
+ p.log.error('Canceled organization selection, exiting')
695
+ program.error('')
696
+ }
697
+
698
+ const organizationUid = organizationUidRaw as string
699
+ const organization = allOrganizations.find(org => org.gid === organizationUid)!
700
+
701
+ p.log.info(`Using the organization "${organization.name}" as the app owner`)
702
+ return organization
703
+ }
704
+
705
+ export const convertAppName = (appName: string) => appName.replace(/\./g, '--')
706
+
707
+ export async function verifyUser(supabase: SupabaseClient<Database>, apikey: string, keymod: Database['public']['Enums']['key_mode'][] = ['all']) {
708
+ await checkKey(supabase, apikey, keymod)
709
+
710
+ const { data: dataUser, error: userIdError } = await supabase
711
+ .rpc('get_user_id', { apikey })
712
+ .single()
713
+
714
+ const userId = (dataUser || '').toString()
715
+
716
+ if (!userId || userIdError) {
717
+ p.log.error(`Cannot auth user with apikey`)
718
+ program.error('')
719
+ }
720
+ return userId
721
+ }
722
+
723
+ export async function getOrganizationId(supabase: SupabaseClient<Database>, appId: string) {
724
+ const { data, error } = await supabase.from('apps')
725
+ .select('owner_org')
726
+ .eq('app_id', appId)
727
+ .single()
728
+
729
+ if (!data || error) {
730
+ p.log.error(`Cannot get organization id for app id ${appId}`)
731
+ formatError(error)
732
+ program.error('')
733
+ }
734
+ return data.owner_org
735
+ }
736
+
737
+ export async function requireUpdateMetadata(supabase: SupabaseClient<Database>, channel: string, appId: string): Promise<boolean> {
738
+ const { data, error } = await supabase
739
+ .from('channels')
740
+ .select('disableAutoUpdate')
741
+ .eq('name', channel)
742
+ .eq('app_id', appId)
743
+ .limit(1)
744
+
745
+ if (error) {
746
+ p.log.error(`Cannot check if disableAutoUpdate is required ${formatError(error)}`)
747
+ program.error('')
748
+ }
749
+
750
+ // Channel does not exist and the default is never 'version_number'
751
+ if (data.length === 0)
752
+ return false
753
+
754
+ const { disableAutoUpdate } = (data[0])
755
+ return disableAutoUpdate === 'version_number'
756
+ }
757
+
758
+ export function getHumanDate(createdA: string | null) {
759
+ const date = new Date(createdA || '')
760
+ return date.toLocaleString()
761
+ }
762
+
763
+ let pmFetched = false
764
+ let pm: PackageManagerType = 'npm'
765
+ let pmCommand: InstallCommand = 'install'
766
+ let pmRunner: PackageManagerRunner = 'npx'
767
+ export function getPMAndCommand() {
768
+ if (pmFetched)
769
+ return { pm, command: pmCommand, installCommand: `${pm} ${pmCommand}`, runner: pmRunner }
770
+ const dir = findRootSync(process.cwd())
771
+ pm = findPackageManagerType(dir.rootDir, 'npm')
772
+ pmCommand = findInstallCommand(pm)
773
+ pmFetched = true
774
+ pmRunner = findPackageManagerRunner(dir.rootDir)
775
+ return { pm, command: pmCommand, installCommand: `${pm} ${pmCommand}`, runner: pmRunner }
776
+ }
777
+
778
+ export async function getLocalDepenencies() {
779
+ const dir = findRootSync(process.cwd())
780
+ if (!existsSync('./package.json')) {
781
+ p.log.error('Missing package.json, you need to be in a capacitor project')
782
+ program.error('')
783
+ }
784
+
785
+ let packageJson
786
+ try {
787
+ packageJson = JSON.parse(readFileSync('./package.json', 'utf8'))
788
+ }
789
+ catch (err) {
790
+ p.log.error('Invalid package.json, JSON parsing failed')
791
+ console.error('json parse error: ', err)
792
+ program.error('')
793
+ }
794
+
795
+ const { dependencies } = packageJson
796
+ if (!dependencies) {
797
+ p.log.error('Missing dependencies section in package.json')
798
+ program.error('')
799
+ }
800
+
801
+ for (const [key, value] of Object.entries(dependencies)) {
802
+ if (typeof value !== 'string') {
803
+ p.log.error(`Invalid dependency ${key}: ${value}, expected string, got ${typeof value}`)
804
+ program.error('')
805
+ }
806
+ }
807
+
808
+ if (!existsSync('./node_modules/')) {
809
+ const pm = findPackageManagerType(dir.rootDir, 'npm')
810
+ const installCmd = findInstallCommand(pm)
811
+ p.log.error(`Missing node_modules folder, please run ${pm} ${installCmd}`)
812
+ program.error('')
813
+ }
814
+
815
+ let anyInvalid = false
816
+
817
+ const dependenciesObject = await Promise.all(Object.entries(dependencies as Record<string, string>)
818
+
819
+ .map(async ([key, value]) => {
820
+ const dependencyFolderExists = existsSync(`./node_modules/${key}`)
821
+
822
+ if (!dependencyFolderExists) {
823
+ anyInvalid = true
824
+ const pm = findPackageManagerType(dir.rootDir, 'npm')
825
+ const installCmd = findInstallCommand(pm)
826
+ p.log.error(`Missing dependency ${key}, please run ${pm} ${installCmd}`)
827
+ return { name: key, version: value }
828
+ }
829
+
830
+ let hasNativeFiles = false
831
+ await promiseFiles(`./node_modules/${key}`)
832
+ .then((files) => {
833
+ if (files.find(fileName => nativeFileRegex.test(fileName)))
834
+ hasNativeFiles = true
835
+ })
836
+ .catch((error) => {
837
+ p.log.error(`Error reading node_modulses files for ${key} package`)
838
+ console.error(error)
839
+ program.error('')
840
+ })
841
+
842
+ return {
843
+ name: key,
844
+ version: value,
845
+ native: hasNativeFiles,
846
+ }
847
+ })).catch(() => [])
848
+
849
+ if (anyInvalid || dependenciesObject.find(a => a.native === undefined))
850
+ program.error('')
851
+
852
+ return dependenciesObject as { name: string, version: string, native: boolean }[]
853
+ }
854
+
855
+ export async function getRemoteDepenencies(supabase: SupabaseClient<Database>, appId: string, channel: string) {
856
+ const { data: remoteNativePackages, error } = await supabase
857
+ .from('channels')
858
+ .select(`version (
859
+ native_packages
860
+ )`)
861
+ .eq('name', channel)
862
+ .eq('app_id', appId)
863
+ .single()
864
+
865
+ if (error) {
866
+ p.log.error(`Error fetching native packages: ${error.message}`)
867
+ program.error('')
868
+ }
869
+
870
+ let castedRemoteNativePackages
871
+ try {
872
+ castedRemoteNativePackages = (remoteNativePackages as any).version.native_packages
873
+ }
874
+ catch (err) {
875
+ // If we do not do this we will get an unreadable
876
+ p.log.error(`Error parsing native packages`)
877
+ program.error('')
878
+ }
879
+
880
+ if (!castedRemoteNativePackages) {
881
+ p.log.error(`Error parsing native packages, perhaps the metadata does not exist?`)
882
+ program.error('')
883
+ }
884
+
885
+ // Check types
886
+ castedRemoteNativePackages.forEach((data: any) => {
887
+ if (typeof data !== 'object') {
888
+ p.log.error(`Invalid remote native package data: ${data}, expected object, got ${typeof data}`)
889
+ program.error('')
890
+ }
891
+
892
+ const { name, version } = data
893
+ if (!name || typeof name !== 'string') {
894
+ p.log.error(`Invalid remote native package name: ${name}, expected string, got ${typeof name}`)
895
+ program.error('')
896
+ }
897
+
898
+ if (!version || typeof version !== 'string') {
899
+ p.log.error(`Invalid remote native package version: ${version}, expected string, got ${typeof version}`)
900
+ program.error('')
901
+ }
902
+ })
903
+
904
+ const mappedRemoteNativePackages = new Map((castedRemoteNativePackages as { name: string, version: string }[])
905
+ .map(a => [a.name, a]))
906
+
907
+ return mappedRemoteNativePackages
908
+ }
909
+
910
+ export async function checkCompatibility(supabase: SupabaseClient<Database>, appId: string, channel: string) {
911
+ const dependenciesObject = await getLocalDepenencies()
912
+ const mappedRemoteNativePackages = await getRemoteDepenencies(supabase, appId, channel)
913
+
914
+ const finalDepenencies:
915
+ ({
916
+ name: string
917
+ localVersion: string
918
+ remoteVersion: string
919
+ } | {
920
+ name: string
921
+ localVersion: string
922
+ remoteVersion: undefined
923
+ } | {
924
+ name: string
925
+ localVersion: undefined
926
+ remoteVersion: string
927
+ })[] = dependenciesObject
928
+ .filter(a => !!a.native)
929
+ .map((local) => {
930
+ const remotePackage = mappedRemoteNativePackages.get(local.name)
931
+ if (remotePackage) {
932
+ return {
933
+ name: local.name,
934
+ localVersion: local.version,
935
+ remoteVersion: remotePackage.version,
936
+ }
937
+ }
938
+
939
+ return {
940
+ name: local.name,
941
+ localVersion: local.version,
942
+ remoteVersion: undefined,
943
+ }
944
+ })
945
+
946
+ const removeNotInLocal = [...mappedRemoteNativePackages]
947
+ .filter(([remoteName]) => dependenciesObject.find(a => a.name === remoteName) === undefined)
948
+ .map(([name, version]) => ({ name, localVersion: undefined, remoteVersion: version.version }))
949
+
950
+ finalDepenencies.push(...removeNotInLocal)
951
+
952
+ return {
953
+ finalCompatibility: finalDepenencies,
954
+ localDependencies: dependenciesObject,
955
+ }
956
+ }