@capgo/cli 4.11.5 → 4.12.0

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.
@@ -1,53 +1,19 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
  import { existsSync, readFileSync } from 'node:fs'
3
- import process from 'node:process'
4
3
  import type { Buffer } from 'node:buffer'
5
- import { performance } from 'node:perf_hooks'
6
- import { program } from 'commander'
4
+ import process from 'node:process'
7
5
  import * as p from '@clack/prompts'
6
+ import { program } from 'commander'
8
7
  import { checksum as getChecksum } from '@tomasklaen/checksum'
9
8
  import ciDetect from 'ci-info'
9
+ import type LogSnag from 'logsnag'
10
10
  import ky, { HTTPError } from 'ky'
11
- import {
12
- PutObjectCommand,
13
- S3Client,
14
- } from '@aws-sdk/client-s3'
15
- import { checkLatest } from '../api/update'
16
- import { checkAppExistsAndHasPermissionOrgErr } from '../api/app'
17
11
  import { encryptSource } from '../api/crypto'
18
- import type {
19
- OptionsBase,
20
- } from '../utils'
21
- import {
22
- OrganizationPerm,
23
- baseKeyPub,
24
- checkCompatibility,
25
- checkPlanValid,
26
- convertAppName,
27
- createSupabaseClient,
28
- deletedFailedVersion,
29
- findSavedKey,
30
- formatError,
31
- getConfig,
32
- getLocalConfig,
33
- getLocalDepenencies,
34
- getOrganizationId,
35
- getPMAndCommand,
36
- hasOrganizationPerm,
37
- regexSemver,
38
- requireUpdateMetadata,
39
- updateOrCreateChannel,
40
- updateOrCreateVersion,
41
- uploadMultipart,
42
- uploadUrl,
43
- useLogSnag,
44
- verifyUser,
45
- zipFile,
46
- } from '../utils'
12
+ import { type OptionsBase, OrganizationPerm, baseKeyPub, checkChecksum, checkCompatibility, checkPlanValid, convertAppName, createSupabaseClient, deletedFailedVersion, findSavedKey, formatError, getConfig, getLocalConfig, getLocalDepenencies, getOrganizationId, getPMAndCommand, hasOrganizationPerm, regexSemver, updateOrCreateChannel, updateOrCreateVersion, uploadMultipart, uploadUrl, useLogSnag, verifyUser, zipFile } from '../utils'
13
+ import { checkAppExistsAndHasPermissionOrgErr } from '../api/app'
14
+ import { checkLatest } from '../api/update'
47
15
  import { checkIndexPosition, searchInDirectory } from './check'
48
16
 
49
- const alertMb = 20
50
-
51
17
  interface Options extends OptionsBase {
52
18
  bundle?: string
53
19
  path?: string
@@ -66,73 +32,64 @@ interface Options extends OptionsBase {
66
32
  minUpdateVersion?: string
67
33
  autoMinUpdateVersion?: boolean
68
34
  ignoreMetadataCheck?: boolean
35
+ ignoreChecksumCheck?: boolean
69
36
  timeout?: number
70
37
  multipart?: boolean
71
38
  }
72
39
 
40
+ const alertMb = 20
73
41
  const UPLOAD_TIMEOUT = 120000
74
42
 
75
- export async function uploadBundle(appid: string, options: Options, shouldExit = true) {
76
- const pm = getPMAndCommand()
77
- p.intro(`Uploading`)
78
- await checkLatest()
79
- let { bundle, path, channel } = options
80
- const { external, key, displayIvSession, autoMinUpdateVersion, ignoreMetadataCheck } = options
81
- let { minUpdateVersion } = options
82
- const { s3Region, s3Apikey, s3Apisecret, s3BucketName } = options
83
- let useS3 = false
84
- let s3Client
85
- if (s3Region && s3Apikey && s3Apisecret && s3BucketName) {
86
- p.log.info('Uploading to S3')
87
- useS3 = true
88
- s3Client = new S3Client({
89
- region: s3Region,
90
- credentials: {
91
- accessKeyId: s3Apikey,
92
- secretAccessKey: s3Apisecret,
93
- },
94
- })
95
- }
96
-
97
- options.apikey = options.apikey || findSavedKey()
98
- const snag = useLogSnag()
99
-
100
- channel = channel || 'dev'
101
-
102
- const config = await getConfig()
43
+ type ConfigType = Awaited<ReturnType<typeof getConfig>>
44
+ type SupabaseType = Awaited<ReturnType<typeof createSupabaseClient>>
45
+ type pmType = ReturnType<typeof getPMAndCommand>
46
+ type localConfigType = Awaited<ReturnType<typeof getLocalConfig>>
103
47
 
104
- const checkNotifyAppReady = options.codeCheck
105
- appid = appid || config?.app?.appId
48
+ function getBundle(config: ConfigType, options: Options) {
106
49
  // create bundle name format : 1.0.0-beta.x where x is a uuid
107
- const uuid = randomUUID().split('-')[0]
108
- bundle = bundle || config?.app?.extConfig?.plugins?.CapacitorUpdater?.version || config?.app?.package?.version || `0.0.1-beta.${uuid}`
109
- // check if bundle is valid
50
+ const bundle = options.bundle
51
+ || config?.app?.extConfig?.plugins?.CapacitorUpdater?.version
52
+ || config?.app?.package?.version
53
+ || `0.0.1-beta.${randomUUID().split('-')[0]}`
54
+
110
55
  if (!regexSemver.test(bundle)) {
111
56
  p.log.error(`Your bundle name ${bundle}, is not valid it should follow semver convention : https://semver.org/`)
112
57
  program.error('')
113
58
  }
114
- path = path || config?.app?.webDir
115
- if (!options.apikey) {
59
+
60
+ return bundle
61
+ }
62
+
63
+ function getApikey(options: Options) {
64
+ const apikey = options.apikey || findSavedKey()
65
+ if (!apikey) {
116
66
  p.log.error(`Missing API key, you need to provide a API key to upload your bundle`)
117
67
  program.error('')
118
68
  }
119
- if (!appid || !path) {
69
+
70
+ return apikey
71
+ }
72
+
73
+ function getAppIdAndPath(appId: string | undefined, options: Options, config: ConfigType) {
74
+ const finalAppId = appId || config?.app?.appId
75
+ const path = options.path || config?.app?.webDir
76
+
77
+ if (!finalAppId || !path) {
120
78
  p.log.error('Missing argument, you need to provide a appid and a path (--path), or be in a capacitor project')
121
79
  program.error('')
122
80
  }
123
- // if one S3 variable is set, check that all are set
124
- if (s3BucketName || s3Region || s3Apikey || s3Apisecret) {
125
- if (!s3BucketName || !s3Region || !s3Apikey || !s3Apisecret) {
126
- p.log.error('Missing argument, for S3 upload you need to provide a bucket name, region, API key, and API secret')
127
- program.error('')
128
- }
129
- }
130
- // check if path exist
81
+
131
82
  if (!existsSync(path)) {
132
83
  p.log.error(`Path ${path} does not exist, build your app first, or provide a valid path`)
133
84
  program.error('')
134
85
  }
135
86
 
87
+ return { appid: finalAppId, path }
88
+ }
89
+
90
+ function checkNotifyAppReady(options: Options, path: string) {
91
+ const checkNotifyAppReady = options.codeCheck
92
+
136
93
  if (typeof checkNotifyAppReady === 'undefined' || checkNotifyAppReady) {
137
94
  const isPluginConfigured = searchInDirectory(path, 'notifyAppReady')
138
95
  if (!isPluginConfigured) {
@@ -145,29 +102,23 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
145
102
  program.error('')
146
103
  }
147
104
  }
105
+ }
148
106
 
149
- p.log.info(`Upload ${appid}@${bundle} started from path "${path}" to Capgo cloud`)
150
-
151
- const localConfig = await getLocalConfig()
152
- const supabase = await createSupabaseClient(options.apikey)
153
- const userId = await verifyUser(supabase, options.apikey, ['write', 'all', 'upload'])
154
- // Check we have app access to this appId
155
- const permissions = await checkAppExistsAndHasPermissionOrgErr(supabase, options.apikey, appid, OrganizationPerm.upload)
156
-
157
- // Now if it does exist we will fetch the org id
158
- const orgId = await getOrganizationId(supabase, appid)
159
- await checkPlanValid(supabase, orgId, options.apikey, appid, true)
160
-
161
- const updateMetadataRequired = await requireUpdateMetadata(supabase, channel, appid)
162
-
107
+ async function verifyCompatibility(supabase: SupabaseType, pm: pmType, options: Options, channel: string, appid: string, bundle: string) {
163
108
  // Check compatibility here
109
+ const ignoreMetadataCheck = options.ignoreMetadataCheck
110
+ const autoMinUpdateVersion = options.autoMinUpdateVersion
111
+ let minUpdateVersion = options.minUpdateVersion
112
+
164
113
  const { data: channelData, error: channelError } = await supabase
165
114
  .from('channels')
166
- .select('version ( minUpdateVersion, native_packages )')
115
+ .select('disableAutoUpdate, version ( minUpdateVersion, native_packages )')
167
116
  .eq('name', channel)
168
117
  .eq('app_id', appid)
169
118
  .single()
170
119
 
120
+ const updateMetadataRequired = !!channelData && channelData.disableAutoUpdate === 'version_number'
121
+
171
122
  // eslint-disable-next-line no-undef-init
172
123
  let localDependencies: Awaited<ReturnType<typeof getLocalDepenencies>> | undefined = undefined
173
124
  let finalCompatibility: Awaited<ReturnType<typeof checkCompatibility>>['finalCompatibility']
@@ -185,7 +136,7 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
185
136
  localDependencies = localDependenciesWithChannel
186
137
 
187
138
  if (finalCompatibility.find(x => x.localVersion !== x.remoteVersion)) {
188
- p.log.error(`Your bundle is not compatible with the channel ${channel}`)
139
+ spinner.stop(`Bundle NOT compatible with ${channel} channel`)
189
140
  p.log.warn(`You can check compatibility with "${pm.runner} @capgo/cli bundle compatibility"`)
190
141
 
191
142
  if (autoMinUpdateVersion) {
@@ -202,14 +153,16 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
202
153
  }
203
154
 
204
155
  minUpdateVersion = lastMinUpdateVersion
205
- p.log.info(`Auto set min-update-version to ${minUpdateVersion}`)
156
+ spinner.stop(`Auto set min-update-version to ${minUpdateVersion}`)
206
157
  }
207
158
  catch (error) {
208
159
  p.log.error(`Cannot auto set compatibility, invalid data ${channelData}`)
209
160
  program.error('')
210
161
  }
211
162
  }
212
- spinner.stop(`Bundle compatible with ${channel} channel`)
163
+ else {
164
+ spinner.stop(`Bundle compatible with ${channel} channel`)
165
+ }
213
166
  }
214
167
  else if (!ignoreMetadataCheck) {
215
168
  p.log.warn(`Channel ${channel} is new or it's your first upload with compatibility check, it will be ignored this time`)
@@ -233,223 +186,172 @@ export async function uploadBundle(appid: string, options: Options, shouldExit =
233
186
  }
234
187
  }
235
188
 
189
+ const hashedLocalDependencies = localDependencies
190
+ ? new Map(localDependencies
191
+ .filter(a => !!a.native && a.native !== undefined)
192
+ .map(a => [a.name, a]))
193
+ : new Map()
194
+
195
+ const nativePackages = (hashedLocalDependencies.size > 0 || !options.ignoreMetadataCheck) ? Array.from(hashedLocalDependencies, ([name, value]) => ({ name, version: value.version })) : undefined
196
+
197
+ return { nativePackages, minUpdateVersion }
198
+ }
199
+
200
+ async function checkTrial(supabase: SupabaseType, orgId: string, localConfig: localConfigType) {
236
201
  const { data: isTrial, error: isTrialsError } = await supabase
237
202
  .rpc('is_trial_org', { orgid: orgId })
238
203
  .single()
239
204
  if ((isTrial && isTrial > 0) || isTrialsError) {
240
- // TODO: Come back to this to fix for orgs v3
205
+ // TODO: Come back to this to fix for orgs v3
241
206
  p.log.warn(`WARNING !!\nTrial expires in ${isTrial} days`)
242
207
  p.log.warn(`Upgrade here: ${localConfig.hostWeb}/dashboard/settings/plans?oid=${orgId}`)
243
208
  }
209
+ }
244
210
 
211
+ async function checkVersionExists(supabase: SupabaseType, appid: string, bundle: string) {
245
212
  // check if app already exist
213
+ // apikey is sooo legacy code, current prod does not use it
246
214
  const { data: appVersion, error: appVersionError } = await supabase
247
- .rpc('exist_app_versions', { appid, apikey: options.apikey, name_version: bundle })
215
+ .rpc('exist_app_versions', { appid, apikey: '', name_version: bundle })
248
216
  .single()
249
217
 
250
218
  if (appVersion || appVersionError) {
251
219
  p.log.error(`Version already exists ${formatError(appVersionError)}`)
252
220
  program.error('')
253
221
  }
222
+ }
254
223
 
224
+ async function prepareBundleFile(path: string, options: Options, localConfig: localConfigType, snag: LogSnag, orgId: string, appid: string) {
255
225
  let sessionKey
256
226
  let checksum = ''
257
227
  let zipped: Buffer | null = null
258
- if (!external && useS3 === false) {
259
- zipped = await zipFile(path)
260
- const s = p.spinner()
261
- s.start(`Calculating checksum`)
262
- checksum = await getChecksum(zipped, 'crc32')
263
- s.stop(`Checksum: ${checksum}`)
264
- // key should be undefined or a string if false it should ingore encryption
265
- if (key === false) {
266
- p.log.info(`Encryption ignored`)
267
- }
268
- else if (key || existsSync(baseKeyPub)) {
269
- const publicKey = typeof key === 'string' ? key : baseKeyPub
270
- let keyData = options.keyData || ''
271
- // check if publicKey exist
272
- if (!keyData && !existsSync(publicKey)) {
273
- p.log.error(`Cannot find public key ${publicKey}`)
274
- if (ciDetect.isCI)
275
- program.error('')
276
-
277
- const res = await p.confirm({ message: 'Do you want to use our public key ?' })
278
- if (!res) {
279
- p.log.error(`Error: Missing public key`)
280
- program.error('')
281
- }
282
- keyData = localConfig.signKey || ''
228
+ const key = options.key
229
+
230
+ zipped = await zipFile(path)
231
+ const s = p.spinner()
232
+ s.start(`Calculating checksum`)
233
+ checksum = await getChecksum(zipped, 'crc32')
234
+ s.stop(`Checksum: ${checksum}`)
235
+ // key should be undefined or a string if false it should ingore encryption
236
+ if (!key) {
237
+ p.log.info(`Encryption ignored`)
238
+ }
239
+ else if (key || existsSync(baseKeyPub)) {
240
+ const publicKey = typeof key === 'string' ? key : baseKeyPub
241
+ let keyData = options.keyData || ''
242
+ // check if publicKey exist
243
+ if (!keyData && !existsSync(publicKey)) {
244
+ p.log.error(`Cannot find public key ${publicKey}`)
245
+ if (ciDetect.isCI) {
246
+ p.log.error('Cannot ask if user wants to use capgo public key on the cli')
247
+ program.error('')
283
248
  }
284
- await snag.track({
285
- channel: 'app',
286
- event: 'App encryption',
287
- icon: '🔑',
288
- user_id: orgId,
289
- tags: {
290
- 'app-id': appid,
291
- },
292
- notify: false,
293
- }).catch()
294
- // open with fs publicKey path
295
- if (!keyData) {
296
- const keyFile = readFileSync(publicKey)
297
- keyData = keyFile.toString()
249
+
250
+ const res = await p.confirm({ message: 'Do you want to use our public key ?' })
251
+ if (!res) {
252
+ p.log.error(`Error: Missing public key`)
253
+ program.error('')
298
254
  }
299
- // encrypt
300
- p.log.info(`Encrypting your bundle`)
301
- const res = encryptSource(zipped, keyData)
302
- sessionKey = res.ivSessionKey
303
- if (displayIvSession) {
304
- p.log.info(`Your Iv Session key is ${sessionKey},
255
+ keyData = localConfig.signKey || ''
256
+ }
257
+ await snag.track({
258
+ channel: 'app',
259
+ event: 'App encryption',
260
+ icon: '🔑',
261
+ user_id: orgId,
262
+ tags: {
263
+ 'app-id': appid,
264
+ },
265
+ notify: false,
266
+ }).catch()
267
+ // open with fs publicKey path
268
+ if (!keyData) {
269
+ const keyFile = readFileSync(publicKey)
270
+ keyData = keyFile.toString()
271
+ }
272
+ // encrypt
273
+ p.log.info(`Encrypting your bundle`)
274
+ const res = encryptSource(zipped, keyData)
275
+ sessionKey = res.ivSessionKey
276
+ if (options.displayIvSession) {
277
+ p.log.info(`Your Iv Session key is ${sessionKey},
305
278
  keep it safe, you will need it to decrypt your bundle.
306
279
  It will be also visible in your dashboard\n`)
307
- }
308
- zipped = res.encryptedData
309
- }
310
- const mbSize = Math.floor(zipped.byteLength / 1024 / 1024)
311
- if (mbSize > alertMb) {
312
- p.log.warn(`WARNING !!\nThe app size is ${mbSize} Mb, this may take a while to download for users\n`)
313
- p.log.info(`Learn how to optimize your assets https://capgo.app/blog/optimise-your-images-for-updates/\n`)
314
- await snag.track({
315
- channel: 'app-error',
316
- event: 'App Too Large',
317
- icon: '🚛',
318
- user_id: orgId,
319
- tags: {
320
- 'app-id': appid,
321
- },
322
- notify: false,
323
- }).catch()
324
280
  }
281
+ zipped = res.encryptedData
325
282
  }
326
- else if (external && !external.startsWith('https://')) {
327
- p.log.error(`External link should should start with "https://" current is "${external}"`)
328
- program.error('')
329
- }
330
- else {
331
- if (useS3) {
332
- zipped = await zipFile(path)
333
- const s = p.spinner()
334
- s.start(`Calculating checksum`)
335
- checksum = await getChecksum(zipped, 'crc32')
336
- s.stop(`Checksum: ${checksum}`)
337
- }
283
+ const mbSize = Math.floor((zipped?.byteLength ?? 0) / 1024 / 1024)
284
+ if (mbSize > alertMb) {
285
+ p.log.warn(`WARNING !!\nThe app size is ${mbSize} Mb, this may take a while to download for users\n`)
286
+ p.log.info(`Learn how to optimize your assets https://capgo.app/blog/optimise-your-images-for-updates/\n`)
338
287
  await snag.track({
339
- channel: 'app',
340
- event: 'App external',
341
- icon: '📤',
288
+ channel: 'app-error',
289
+ event: 'App Too Large',
290
+ icon: '🚛',
342
291
  user_id: orgId,
343
292
  tags: {
344
293
  'app-id': appid,
345
294
  },
346
295
  notify: false,
347
296
  }).catch()
348
- sessionKey = options.ivSessionKey
349
297
  }
350
298
 
351
- const hashedLocalDependencies = localDependencies
352
- ? new Map(localDependencies
353
- .filter(a => !!a.native && a.native !== undefined)
354
- .map(a => [a.name, a]))
355
- : new Map()
356
-
357
- const nativePackages = (hashedLocalDependencies.size > 0 || !options.ignoreMetadataCheck) ? Array.from(hashedLocalDependencies, ([name, value]) => ({ name, version: value.version })) : undefined
299
+ return { zipped, sessionKey, checksum }
300
+ }
358
301
 
359
- const versionData = {
360
- // bucket_id: external ? undefined : fileName,
361
- name: bundle,
362
- app_id: appid,
363
- session_key: sessionKey,
364
- external_url: external,
365
- storage_provider: external ? 'external' : 'r2-direct',
366
- minUpdateVersion,
367
- native_packages: nativePackages,
368
- owner_org: orgId,
369
- user_id: userId,
370
- checksum,
371
- }
372
- const { error: dbError } = await updateOrCreateVersion(supabase, versionData)
373
- if (dbError) {
374
- p.log.error(`Cannot add bundle ${formatError(dbError)}`)
375
- program.error('')
376
- }
377
- if (!external && zipped) {
378
- const spinner = p.spinner()
379
- spinner.start(`Uploading Bundle`)
380
- const startTime = performance.now()
302
+ async function uploadBundleToCapgoCloud(supabase: SupabaseType, appid: string, bundle: string, orgId: string, zipped: Buffer, options: Options) {
303
+ const spinner = p.spinner()
304
+ spinner.start(`Uploading Bundle`)
305
+ const startTime = performance.now()
381
306
 
382
- try {
383
- if (options.multipart !== undefined && options.multipart) {
384
- p.log.info(`Uploading bundle as multipart`)
385
- await uploadMultipart(supabase, appid, bundle, zipped, orgId)
386
- }
387
- else {
388
- const url = await uploadUrl(supabase, appid, bundle)
389
- if (!url) {
390
- p.log.error(`Cannot get upload url`)
391
- program.error('')
392
- }
393
- await ky.put(url, {
394
- timeout: options.timeout || UPLOAD_TIMEOUT,
395
- retry: 5,
396
- body: zipped,
397
- })
398
- }
307
+ try {
308
+ if (options.multipart !== undefined && options.multipart) {
309
+ p.log.info(`Uploading bundle as multipart`)
310
+ await uploadMultipart(supabase, appid, bundle, zipped, orgId)
399
311
  }
400
- catch (errorUpload) {
401
- const endTime = performance.now()
402
- const uploadTime = ((endTime - startTime) / 1000).toFixed(2)
403
- spinner.stop(`Failed to upload bundle ( after ${uploadTime} seconds)`)
404
- p.log.error(`Cannot upload bundle ( try again with --multipart option) ${formatError(errorUpload)}`)
405
- if (errorUpload instanceof HTTPError) {
406
- const body = await errorUpload.response.text()
407
- p.log.error(`Response: ${formatError(body)}`)
312
+ else {
313
+ const url = await uploadUrl(supabase, appid, bundle)
314
+ if (!url) {
315
+ p.log.error(`Cannot get upload url`)
316
+ program.error('')
408
317
  }
409
- // call delete version on path /delete_failed_version to delete the version
410
- await deletedFailedVersion(supabase, appid, bundle)
411
- program.error('')
412
- }
413
-
414
- versionData.storage_provider = 'r2'
415
- const { error: dbError2 } = await updateOrCreateVersion(supabase, versionData)
416
- if (dbError2) {
417
- p.log.error(`Cannot update bundle ${formatError(dbError2)}`)
418
- program.error('')
318
+ await ky.put(url, {
319
+ timeout: options.timeout || UPLOAD_TIMEOUT,
320
+ retry: 5,
321
+ body: zipped,
322
+ })
419
323
  }
420
- const endTime = performance.now()
421
- const uploadTime = ((endTime - startTime) / 1000).toFixed(2)
422
- spinner.stop(`Bundle Uploaded 💪 (${uploadTime} seconds)`)
423
324
  }
424
- else if (useS3 && zipped && s3Client) {
425
- const spinner = p.spinner()
426
- spinner.start(`Uploading Bundle`)
427
-
428
- const fileName = `${appid}-${bundle}`
429
- const encodeFileName = encodeURIComponent(fileName)
430
- const command: PutObjectCommand = new PutObjectCommand({
431
- Bucket: s3BucketName,
432
- Key: fileName,
433
- Body: zipped,
434
- })
435
- const startTime = performance.now()
436
- const response = await s3Client.send(command)
437
- if (response.$metadata.httpStatusCode !== 200) {
438
- p.log.error(`Cannot upload to S3`)
439
- program.error('')
440
- }
441
-
442
- versionData.storage_provider = 'external'
443
- versionData.external_url = `https://${s3BucketName}.s3.amazonaws.com/${encodeFileName}`
444
- const { error: dbError2 } = await updateOrCreateVersion(supabase, versionData)
445
- if (dbError2) {
446
- p.log.error(`Cannot update bundle ${formatError(dbError2)}`)
447
- program.error('')
448
- }
325
+ catch (errorUpload) {
449
326
  const endTime = performance.now()
450
327
  const uploadTime = ((endTime - startTime) / 1000).toFixed(2)
451
- spinner.stop(`Bundle Uploaded 💪 (${uploadTime} seconds)`)
328
+ spinner.stop(`Failed to upload bundle ( after ${uploadTime} seconds)`)
329
+ p.log.error(`Cannot upload bundle ( try again with --multipart option) ${formatError(errorUpload)}`)
330
+ if (errorUpload instanceof HTTPError) {
331
+ const body = await errorUpload.response.text()
332
+ p.log.error(`Response: ${formatError(body)}`)
333
+ }
334
+ // call delete version on path /delete_failed_version to delete the version
335
+ await deletedFailedVersion(supabase, appid, bundle)
336
+ program.error('')
452
337
  }
338
+
339
+ const endTime = performance.now()
340
+ const uploadTime = ((endTime - startTime) / 1000).toFixed(2)
341
+ spinner.stop(`Bundle Uploaded 💪 (${uploadTime} seconds)`)
342
+ }
343
+
344
+ async function setVersionInChannel(
345
+ supabase: SupabaseType,
346
+ options: Options,
347
+ bundle: string,
348
+ channel: string,
349
+ userId: string,
350
+ orgId: string,
351
+ appid: string,
352
+ localConfig: localConfigType,
353
+ permissions: OrganizationPerm,
354
+ ) {
453
355
  const { data: versionId } = await supabase
454
356
  .rpc('get_app_versions', { apikey: options.apikey, name_version: bundle, appid })
455
357
  .single()
@@ -473,16 +375,121 @@ It will be also visible in your dashboard\n`)
473
375
  else if (data?.id)
474
376
  p.log.info(`Link device to this bundle to try it: ${bundleUrl}`)
475
377
 
476
- if (options.bundleUrl)
378
+ if (options.bundleUrl) {
477
379
  p.log.info(`Bundle url: ${bundleUrl}`)
380
+ }
381
+ else if (!versionId) {
382
+ p.log.warn('Cannot set bundle with upload key, use key with more rights for that')
383
+ program.error('')
384
+ }
385
+ else if (!hasOrganizationPerm(permissions, OrganizationPerm.write)) {
386
+ p.log.warn('Cannot set channel as a upload organization member')
387
+ }
388
+ }
389
+ }
390
+
391
+ export async function uploadBundle(preAppid: string, options: Options, shouldExit = true) {
392
+ p.intro(`Uploading`)
393
+ const pm = getPMAndCommand()
394
+ await checkLatest()
395
+
396
+ const { s3Region, s3Apikey, s3Apisecret, s3BucketName } = options
397
+
398
+ if (s3BucketName || s3Region || s3Apikey || s3Apisecret) {
399
+ if (!s3BucketName || !s3Region || !s3Apikey || !s3Apisecret) {
400
+ p.log.error('Missing argument, for S3 upload you need to provide a bucket name, region, API key, and API secret')
401
+ program.error('')
402
+ }
478
403
  }
479
- else if (!versionId) {
480
- p.log.warn('Cannot set bundle with upload key, use key with more rights for that')
404
+
405
+ if (s3Region && s3Apikey && s3Apisecret && s3BucketName) {
406
+ p.log.info('Uploading to S3')
407
+ // const s3Client = new S3Client({
408
+ // region: s3Region,
409
+ // credentials: {
410
+ // accessKeyId: s3Apikey,
411
+ // secretAccessKey: s3Apisecret,
412
+ // },
413
+ // })
414
+ p.log.error('S3 upload is not available we have currenly an issue with it')
481
415
  program.error('')
416
+ // todo: figure out s3 upload
417
+ return
482
418
  }
483
- else if (!hasOrganizationPerm(permissions, OrganizationPerm.write)) {
484
- p.log.warn('Cannot set channel as a upload organization member')
419
+
420
+ const apikey = getApikey(options)
421
+ const config = await getConfig()
422
+ const { appid, path } = getAppIdAndPath(preAppid, options, config)
423
+ const bundle = getBundle(config, options)
424
+ const channel = options.channel || 'dev'
425
+ const snag = useLogSnag()
426
+
427
+ checkNotifyAppReady(options, path)
428
+
429
+ p.log.info(`Upload ${appid}@${bundle} started from path "${path}" to Capgo cloud`)
430
+
431
+ const localConfig = await getLocalConfig()
432
+ const supabase = await createSupabaseClient(apikey)
433
+ const userId = await verifyUser(supabase, apikey, ['write', 'all', 'upload'])
434
+ // Check we have app access to this appId
435
+ const permissions = await checkAppExistsAndHasPermissionOrgErr(supabase, apikey, appid, OrganizationPerm.upload)
436
+
437
+ // Now if it does exist we will fetch the org id
438
+ const orgId = await getOrganizationId(supabase, appid)
439
+ await checkPlanValid(supabase, orgId, options.apikey, appid, true)
440
+ await checkTrial(supabase, orgId, localConfig)
441
+ const { nativePackages, minUpdateVersion } = await verifyCompatibility(supabase, pm, options, channel, appid, bundle)
442
+ await checkVersionExists(supabase, appid, bundle)
443
+
444
+ if (options.external && !options.external.startsWith('https://')) {
445
+ p.log.error(`External link should should start with "https://" current is "${external}"`)
446
+ program.error('')
485
447
  }
448
+
449
+ const versionData = {
450
+ // bucket_id: external ? undefined : fileName,
451
+ name: bundle,
452
+ app_id: appid,
453
+ session_key: undefined as undefined | string,
454
+ external_url: options.external,
455
+ storage_provider: options.external ? 'external' : 'r2-direct',
456
+ minUpdateVersion,
457
+ native_packages: nativePackages,
458
+ owner_org: orgId,
459
+ user_id: userId,
460
+ checksum: undefined as undefined | string,
461
+ }
462
+
463
+ let zipped: Buffer | null = null
464
+ if (!options.external) {
465
+ const { zipped: _zipped, sessionKey, checksum } = await prepareBundleFile(path, options, localConfig, snag, orgId, appid)
466
+ versionData.session_key = sessionKey
467
+ versionData.checksum = checksum
468
+ zipped = _zipped
469
+ if (!options.ignoreChecksumCheck) {
470
+ await checkChecksum(supabase, appid, channel, checksum)
471
+ }
472
+ }
473
+
474
+ const { error: dbError } = await updateOrCreateVersion(supabase, versionData)
475
+ if (dbError) {
476
+ p.log.error(`Cannot add bundle ${formatError(dbError)}`)
477
+ program.error('')
478
+ }
479
+
480
+ if (zipped) {
481
+ await uploadBundleToCapgoCloud(supabase, appid, bundle, orgId, zipped, options)
482
+
483
+ versionData.storage_provider = 'r2'
484
+ const { error: dbError2 } = await updateOrCreateVersion(supabase, versionData)
485
+ if (dbError2) {
486
+ p.log.error(`Cannot update bundle ${formatError(dbError2)}`)
487
+ program.error('')
488
+ }
489
+ }
490
+
491
+ await setVersionInChannel(supabase, options, bundle, channel, userId, orgId, appid, localConfig, permissions)
492
+
486
493
  await snag.track({
487
494
  channel: 'app',
488
495
  event: 'App Uploaded',