@capgo/cli 4.12.14 → 4.13.3

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