@capgo/cli 4.13.2 → 4.13.4

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