@capgo/cli 4.13.2 → 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.
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/.github/FUNDING.yml +0 -1
- package/.github/workflows/autofix.yml +0 -25
- package/.github/workflows/build.yml +0 -46
- package/.github/workflows/bump_version.yml +0 -56
- package/.github/workflows/check_posix_paths.yml +0 -229
- package/.github/workflows/test.yml +0 -30
- package/.prettierignore +0 -6
- package/.vscode/launch.json +0 -23
- package/.vscode/settings.json +0 -46
- package/.vscode/tasks.json +0 -42
- package/CHANGELOG.md +0 -3392
- package/build.mjs +0 -21
- package/bun.lockb +0 -0
- package/bunfig.toml +0 -2
- package/capacitor.config.ts +0 -33
- package/crypto_explained.png +0 -0
- package/eslint.config.js +0 -10
- package/renovate.json +0 -23
- package/src/api/app.ts +0 -55
- package/src/api/channels.ts +0 -163
- package/src/api/crypto.ts +0 -116
- package/src/api/devices_override.ts +0 -41
- package/src/api/update.ts +0 -13
- package/src/api/versions.ts +0 -101
- package/src/app/add.ts +0 -157
- package/src/app/debug.ts +0 -258
- package/src/app/delete.ts +0 -110
- package/src/app/info.ts +0 -99
- package/src/app/list.ts +0 -67
- package/src/app/set.ts +0 -96
- package/src/bundle/check.ts +0 -42
- package/src/bundle/cleanup.ts +0 -123
- package/src/bundle/compatibility.ts +0 -70
- package/src/bundle/decrypt.ts +0 -54
- package/src/bundle/delete.ts +0 -52
- package/src/bundle/encrypt.ts +0 -60
- package/src/bundle/list.ts +0 -42
- package/src/bundle/unlink.ts +0 -88
- package/src/bundle/upload.ts +0 -552
- package/src/bundle/zip.ts +0 -145
- package/src/channel/add.ts +0 -80
- package/src/channel/currentBundle.ts +0 -72
- package/src/channel/delete.ts +0 -57
- package/src/channel/list.ts +0 -49
- package/src/channel/set.ts +0 -179
- package/src/config/index.ts +0 -156
- package/src/index.ts +0 -310
- package/src/init.ts +0 -495
- package/src/key.ts +0 -135
- package/src/login.ts +0 -70
- package/src/types/capacitor__cli.d.ts +0 -6
- package/src/types/supabase.types.ts +0 -2123
- package/src/user/account.ts +0 -11
- package/src/utils.ts +0 -1076
- package/test/VerifyZip.java +0 -83
- package/test/check-posix-paths.js +0 -21
- package/test/chunk_convert.ts +0 -28
- package/test/data.ts +0 -18769
- package/test/test_headers_rls.ts +0 -24
- package/test/test_semver.ts +0 -13
- package/test/test_upload/app.js +0 -3
- package/test/test_upload/assets/check-posix-paths.js +0 -21
- package/test/test_upload/index.html +0 -0
- package/test/test_zip_swift/Package.resolved +0 -24
- package/test/test_zip_swift/Package.swift +0 -29
- package/test/test_zip_swift/Sources/main.swift +0 -80
- package/tsconfig.json +0 -39
package/src/bundle/upload.ts
DELETED
|
@@ -1,552 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
-
import { exit } from 'node:process'
|
|
4
|
-
import type { Buffer } from 'node:buffer'
|
|
5
|
-
import { program } from 'commander'
|
|
6
|
-
import { checksum as getChecksum } from '@tomasklaen/checksum'
|
|
7
|
-
import ciDetect from 'ci-info'
|
|
8
|
-
import type LogSnag from 'logsnag'
|
|
9
|
-
import { S3Client } from '@bradenmacdonald/s3-lite-client'
|
|
10
|
-
import ky, { HTTPError } from 'ky'
|
|
11
|
-
import { confirm as confirmC, intro, log, outro, spinner as spinnerC } from '@clack/prompts'
|
|
12
|
-
import { encryptSource } from '../api/crypto'
|
|
13
|
-
import { type OptionsBase, OrganizationPerm, baseKeyPub, checkChecksum, checkCompatibility, checkPlanValid, convertAppName, createSupabaseClient, deletedFailedVersion, findSavedKey, formatError, getConfig, getLocalConfig, getLocalDepenencies, getOrganizationId, getPMAndCommand, hasOrganizationPerm, readPackageJson, regexSemver, updateOrCreateChannel, updateOrCreateVersion, uploadMultipart, uploadUrl, useLogSnag, verifyUser, zipFile } from '../utils'
|
|
14
|
-
import { checkAppExistsAndHasPermissionOrgErr } from '../api/app'
|
|
15
|
-
import { checkLatest } from '../api/update'
|
|
16
|
-
import type { CapacitorConfig } from '../config'
|
|
17
|
-
import { checkIndexPosition, searchInDirectory } from './check'
|
|
18
|
-
|
|
19
|
-
interface Options extends OptionsBase {
|
|
20
|
-
bundle?: string
|
|
21
|
-
path?: string
|
|
22
|
-
channel?: string
|
|
23
|
-
displayIvSession?: boolean
|
|
24
|
-
external?: string
|
|
25
|
-
key?: boolean | string
|
|
26
|
-
keyData?: string
|
|
27
|
-
ivSessionKey?: string
|
|
28
|
-
s3Region?: string
|
|
29
|
-
s3Apikey?: string
|
|
30
|
-
s3Apisecret?: string
|
|
31
|
-
s3BucketName?: string
|
|
32
|
-
s3Port?: number
|
|
33
|
-
s3SSL?: boolean
|
|
34
|
-
s3Endpoint?: string
|
|
35
|
-
bundleUrl?: boolean
|
|
36
|
-
codeCheck?: boolean
|
|
37
|
-
minUpdateVersion?: string
|
|
38
|
-
autoMinUpdateVersion?: boolean
|
|
39
|
-
ignoreMetadataCheck?: boolean
|
|
40
|
-
ignoreChecksumCheck?: boolean
|
|
41
|
-
timeout?: number
|
|
42
|
-
multipart?: boolean
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const alertMb = 20
|
|
46
|
-
const UPLOAD_TIMEOUT = 120000
|
|
47
|
-
|
|
48
|
-
type SupabaseType = Awaited<ReturnType<typeof createSupabaseClient>>
|
|
49
|
-
type pmType = ReturnType<typeof getPMAndCommand>
|
|
50
|
-
type localConfigType = Awaited<ReturnType<typeof getLocalConfig>>
|
|
51
|
-
|
|
52
|
-
async function getBundle(config: CapacitorConfig, options: Options) {
|
|
53
|
-
const pkg = await readPackageJson()
|
|
54
|
-
// create bundle name format : 1.0.0-beta.x where x is a uuid
|
|
55
|
-
const bundle = options.bundle
|
|
56
|
-
|| config?.plugins?.CapacitorUpdater?.version
|
|
57
|
-
|| pkg?.version
|
|
58
|
-
|| `0.0.1-beta.${randomUUID().split('-')[0]}`
|
|
59
|
-
|
|
60
|
-
if (!regexSemver.test(bundle)) {
|
|
61
|
-
log.error(`Your bundle name ${bundle}, is not valid it should follow semver convention : https://semver.org/`)
|
|
62
|
-
program.error('')
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return bundle
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function getApikey(options: Options) {
|
|
69
|
-
const apikey = options.apikey || findSavedKey()
|
|
70
|
-
if (!apikey) {
|
|
71
|
-
log.error(`Missing API key, you need to provide a API key to upload your bundle`)
|
|
72
|
-
program.error('')
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return apikey
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function getAppIdAndPath(appId: string | undefined, options: Options, config: CapacitorConfig) {
|
|
79
|
-
const finalAppId = appId || config?.appId
|
|
80
|
-
const path = options.path || config?.webDir
|
|
81
|
-
|
|
82
|
-
if (!finalAppId || !path) {
|
|
83
|
-
log.error('Missing argument, you need to provide a appid and a path (--path), or be in a capacitor project')
|
|
84
|
-
program.error('')
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (!existsSync(path)) {
|
|
88
|
-
log.error(`Path ${path} does not exist, build your app first, or provide a valid path`)
|
|
89
|
-
program.error('')
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return { appid: finalAppId, path }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function checkNotifyAppReady(options: Options, path: string) {
|
|
96
|
-
const checkNotifyAppReady = options.codeCheck
|
|
97
|
-
|
|
98
|
-
if (typeof checkNotifyAppReady === 'undefined' || checkNotifyAppReady) {
|
|
99
|
-
const isPluginConfigured = searchInDirectory(path, 'notifyAppReady')
|
|
100
|
-
if (!isPluginConfigured) {
|
|
101
|
-
log.error(`notifyAppReady() is missing in the source code. see: https://capgo.app/docs/plugin/api/#notifyappready`)
|
|
102
|
-
program.error('')
|
|
103
|
-
}
|
|
104
|
-
const foundIndex = checkIndexPosition(path)
|
|
105
|
-
if (!foundIndex) {
|
|
106
|
-
log.error(`index.html is missing in the root folder or in the only folder in the root folder`)
|
|
107
|
-
program.error('')
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function verifyCompatibility(supabase: SupabaseType, pm: pmType, options: Options, channel: string, appid: string, bundle: string) {
|
|
113
|
-
// Check compatibility here
|
|
114
|
-
const ignoreMetadataCheck = options.ignoreMetadataCheck
|
|
115
|
-
const autoMinUpdateVersion = options.autoMinUpdateVersion
|
|
116
|
-
let minUpdateVersion = options.minUpdateVersion
|
|
117
|
-
|
|
118
|
-
const { data: channelData, error: channelError } = await supabase
|
|
119
|
-
.from('channels')
|
|
120
|
-
.select('disableAutoUpdate, version ( minUpdateVersion, native_packages )')
|
|
121
|
-
.eq('name', channel)
|
|
122
|
-
.eq('app_id', appid)
|
|
123
|
-
.single()
|
|
124
|
-
|
|
125
|
-
const updateMetadataRequired = !!channelData && channelData.disableAutoUpdate === 'version_number'
|
|
126
|
-
|
|
127
|
-
// eslint-disable-next-line no-undef-init
|
|
128
|
-
let localDependencies: Awaited<ReturnType<typeof getLocalDepenencies>> | undefined = undefined
|
|
129
|
-
let finalCompatibility: Awaited<ReturnType<typeof checkCompatibility>>['finalCompatibility']
|
|
130
|
-
|
|
131
|
-
// We only check compatibility IF the channel exists
|
|
132
|
-
if (!channelError && channelData && channelData.version && (channelData.version as any).native_packages && !ignoreMetadataCheck) {
|
|
133
|
-
const spinner = spinnerC()
|
|
134
|
-
spinner.start(`Checking bundle compatibility with channel ${channel}`)
|
|
135
|
-
const {
|
|
136
|
-
finalCompatibility: finalCompatibilityWithChannel,
|
|
137
|
-
localDependencies: localDependenciesWithChannel,
|
|
138
|
-
} = await checkCompatibility(supabase, appid, channel)
|
|
139
|
-
|
|
140
|
-
finalCompatibility = finalCompatibilityWithChannel
|
|
141
|
-
localDependencies = localDependenciesWithChannel
|
|
142
|
-
|
|
143
|
-
if (finalCompatibility.find(x => x.localVersion !== x.remoteVersion)) {
|
|
144
|
-
spinner.stop(`Bundle NOT compatible with ${channel} channel`)
|
|
145
|
-
log.warn(`You can check compatibility with "${pm.runner} @capgo/cli bundle compatibility"`)
|
|
146
|
-
|
|
147
|
-
if (autoMinUpdateVersion) {
|
|
148
|
-
minUpdateVersion = bundle
|
|
149
|
-
log.info(`Auto set min-update-version to ${minUpdateVersion}`)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
else if (autoMinUpdateVersion) {
|
|
153
|
-
try {
|
|
154
|
-
const { minUpdateVersion: lastMinUpdateVersion } = channelData.version as any
|
|
155
|
-
if (!lastMinUpdateVersion || !regexSemver.test(lastMinUpdateVersion)) {
|
|
156
|
-
log.error('Invalid remote min update version, skipping auto setting compatibility')
|
|
157
|
-
program.error('')
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
minUpdateVersion = lastMinUpdateVersion
|
|
161
|
-
spinner.stop(`Auto set min-update-version to ${minUpdateVersion}`)
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
log.error(`Cannot auto set compatibility, invalid data ${channelData}`)
|
|
165
|
-
program.error('')
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
spinner.stop(`Bundle compatible with ${channel} channel`)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
else if (!ignoreMetadataCheck) {
|
|
173
|
-
log.warn(`Channel ${channel} is new or it's your first upload with compatibility check, it will be ignored this time`)
|
|
174
|
-
localDependencies = await getLocalDepenencies()
|
|
175
|
-
|
|
176
|
-
if (autoMinUpdateVersion) {
|
|
177
|
-
minUpdateVersion = bundle
|
|
178
|
-
log.info(`Auto set min-update-version to ${minUpdateVersion}`)
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (updateMetadataRequired && !minUpdateVersion && !ignoreMetadataCheck) {
|
|
183
|
-
log.error(`You need to provide a min-update-version to upload a bundle to this channel`)
|
|
184
|
-
program.error('')
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (minUpdateVersion) {
|
|
188
|
-
if (!regexSemver.test(minUpdateVersion)) {
|
|
189
|
-
log.error(`Your minimal version update ${minUpdateVersion}, is not valid it should follow semver convention : https://semver.org/`)
|
|
190
|
-
program.error('')
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const hashedLocalDependencies = localDependencies
|
|
195
|
-
? new Map(localDependencies
|
|
196
|
-
.filter(a => !!a.native && a.native !== undefined)
|
|
197
|
-
.map(a => [a.name, a]))
|
|
198
|
-
: new Map()
|
|
199
|
-
|
|
200
|
-
const nativePackages = (hashedLocalDependencies.size > 0 || !options.ignoreMetadataCheck) ? Array.from(hashedLocalDependencies, ([name, value]) => ({ name, version: value.version })) : undefined
|
|
201
|
-
|
|
202
|
-
return { nativePackages, minUpdateVersion }
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async function checkTrial(supabase: SupabaseType, orgId: string, localConfig: localConfigType) {
|
|
206
|
-
const { data: isTrial, error: isTrialsError } = await supabase
|
|
207
|
-
.rpc('is_trial_org', { orgid: orgId })
|
|
208
|
-
.single()
|
|
209
|
-
if ((isTrial && isTrial > 0) || isTrialsError) {
|
|
210
|
-
// TODO: Come back to this to fix for orgs v3
|
|
211
|
-
log.warn(`WARNING !!\nTrial expires in ${isTrial} days`)
|
|
212
|
-
log.warn(`Upgrade here: ${localConfig.hostWeb}/dashboard/settings/plans?oid=${orgId}`)
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async function checkVersionExists(supabase: SupabaseType, appid: string, bundle: string) {
|
|
217
|
-
// check if app already exist
|
|
218
|
-
// apikey is sooo legacy code, current prod does not use it
|
|
219
|
-
// TODO: remove apikey and create a new function who not need it
|
|
220
|
-
const { data: appVersion, error: appVersionError } = await supabase
|
|
221
|
-
.rpc('exist_app_versions', { appid, apikey: '', name_version: bundle })
|
|
222
|
-
.single()
|
|
223
|
-
|
|
224
|
-
if (appVersion || appVersionError) {
|
|
225
|
-
log.error(`Version already exists ${formatError(appVersionError)}`)
|
|
226
|
-
program.error('')
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async function prepareBundleFile(path: string, options: Options, localConfig: localConfigType, snag: LogSnag, orgId: string, appid: string) {
|
|
231
|
-
let sessionKey
|
|
232
|
-
let checksum = ''
|
|
233
|
-
let zipped: Buffer | null = null
|
|
234
|
-
const key = options.key
|
|
235
|
-
|
|
236
|
-
zipped = await zipFile(path)
|
|
237
|
-
const s = spinnerC()
|
|
238
|
-
s.start(`Calculating checksum`)
|
|
239
|
-
checksum = await getChecksum(zipped, 'crc32')
|
|
240
|
-
s.stop(`Checksum: ${checksum}`)
|
|
241
|
-
// key should be undefined or a string if false it should ingore encryption
|
|
242
|
-
if (!key) {
|
|
243
|
-
log.info(`Encryption ignored`)
|
|
244
|
-
}
|
|
245
|
-
else if (key || existsSync(baseKeyPub)) {
|
|
246
|
-
const publicKey = typeof key === 'string' ? key : baseKeyPub
|
|
247
|
-
let keyData = options.keyData || ''
|
|
248
|
-
// check if publicKey exist
|
|
249
|
-
if (!keyData && !existsSync(publicKey)) {
|
|
250
|
-
log.error(`Cannot find public key ${publicKey}`)
|
|
251
|
-
if (ciDetect.isCI) {
|
|
252
|
-
log.error('Cannot ask if user wants to use capgo public key on the cli')
|
|
253
|
-
program.error('')
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const res = await confirmC({ message: 'Do you want to use our public key ?' })
|
|
257
|
-
if (!res) {
|
|
258
|
-
log.error(`Error: Missing public key`)
|
|
259
|
-
program.error('')
|
|
260
|
-
}
|
|
261
|
-
keyData = localConfig.signKey || ''
|
|
262
|
-
}
|
|
263
|
-
await snag.track({
|
|
264
|
-
channel: 'app',
|
|
265
|
-
event: 'App encryption',
|
|
266
|
-
icon: '🔑',
|
|
267
|
-
user_id: orgId,
|
|
268
|
-
tags: {
|
|
269
|
-
'app-id': appid,
|
|
270
|
-
},
|
|
271
|
-
notify: false,
|
|
272
|
-
}).catch()
|
|
273
|
-
// open with fs publicKey path
|
|
274
|
-
if (!keyData) {
|
|
275
|
-
const keyFile = readFileSync(publicKey)
|
|
276
|
-
keyData = keyFile.toString()
|
|
277
|
-
}
|
|
278
|
-
// encrypt
|
|
279
|
-
log.info(`Encrypting your bundle`)
|
|
280
|
-
const res = encryptSource(zipped, keyData)
|
|
281
|
-
sessionKey = res.ivSessionKey
|
|
282
|
-
if (options.displayIvSession) {
|
|
283
|
-
log.info(`Your Iv Session key is ${sessionKey},
|
|
284
|
-
keep it safe, you will need it to decrypt your bundle.
|
|
285
|
-
It will be also visible in your dashboard\n`)
|
|
286
|
-
}
|
|
287
|
-
zipped = res.encryptedData
|
|
288
|
-
}
|
|
289
|
-
const mbSize = Math.floor((zipped?.byteLength ?? 0) / 1024 / 1024)
|
|
290
|
-
if (mbSize > alertMb) {
|
|
291
|
-
log.warn(`WARNING !!\nThe app size is ${mbSize} Mb, this may take a while to download for users\n`)
|
|
292
|
-
log.info(`Learn how to optimize your assets https://capgo.app/blog/optimise-your-images-for-updates/\n`)
|
|
293
|
-
await snag.track({
|
|
294
|
-
channel: 'app-error',
|
|
295
|
-
event: 'App Too Large',
|
|
296
|
-
icon: '🚛',
|
|
297
|
-
user_id: orgId,
|
|
298
|
-
tags: {
|
|
299
|
-
'app-id': appid,
|
|
300
|
-
},
|
|
301
|
-
notify: false,
|
|
302
|
-
}).catch()
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return { zipped, sessionKey, checksum }
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function uploadBundleToCapgoCloud(supabase: SupabaseType, appid: string, bundle: string, orgId: string, zipped: Buffer, options: Options) {
|
|
309
|
-
const spinner = spinnerC()
|
|
310
|
-
spinner.start(`Uploading Bundle`)
|
|
311
|
-
const startTime = performance.now()
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
if (options.multipart !== undefined && options.multipart) {
|
|
315
|
-
log.info(`Uploading bundle as multipart`)
|
|
316
|
-
await uploadMultipart(supabase, appid, bundle, zipped, orgId)
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
const url = await uploadUrl(supabase, appid, bundle)
|
|
320
|
-
if (!url) {
|
|
321
|
-
log.error(`Cannot get upload url`)
|
|
322
|
-
program.error('')
|
|
323
|
-
}
|
|
324
|
-
await ky.put(url, {
|
|
325
|
-
timeout: options.timeout || UPLOAD_TIMEOUT,
|
|
326
|
-
retry: 5,
|
|
327
|
-
body: zipped,
|
|
328
|
-
})
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
catch (errorUpload) {
|
|
332
|
-
const endTime = performance.now()
|
|
333
|
-
const uploadTime = ((endTime - startTime) / 1000).toFixed(2)
|
|
334
|
-
spinner.stop(`Failed to upload bundle ( after ${uploadTime} seconds)`)
|
|
335
|
-
log.error(`Cannot upload bundle ( try again with --multipart option) ${formatError(errorUpload)}`)
|
|
336
|
-
if (errorUpload instanceof HTTPError) {
|
|
337
|
-
const body = await errorUpload.response.text()
|
|
338
|
-
log.error(`Response: ${formatError(body)}`)
|
|
339
|
-
}
|
|
340
|
-
// call delete version on path /delete_failed_version to delete the version
|
|
341
|
-
await deletedFailedVersion(supabase, appid, bundle)
|
|
342
|
-
program.error('')
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const endTime = performance.now()
|
|
346
|
-
const uploadTime = ((endTime - startTime) / 1000).toFixed(2)
|
|
347
|
-
spinner.stop(`Bundle Uploaded 💪 (${uploadTime} seconds)`)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
async function setVersionInChannel(
|
|
351
|
-
supabase: SupabaseType,
|
|
352
|
-
apikey: string,
|
|
353
|
-
displayBundleUrl: boolean,
|
|
354
|
-
bundle: string,
|
|
355
|
-
channel: string,
|
|
356
|
-
userId: string,
|
|
357
|
-
orgId: string,
|
|
358
|
-
appid: string,
|
|
359
|
-
localConfig: localConfigType,
|
|
360
|
-
permissions: OrganizationPerm,
|
|
361
|
-
) {
|
|
362
|
-
const { data: versionId } = await supabase
|
|
363
|
-
.rpc('get_app_versions', { apikey, name_version: bundle, appid })
|
|
364
|
-
.single()
|
|
365
|
-
|
|
366
|
-
if (versionId && hasOrganizationPerm(permissions, OrganizationPerm.write)) {
|
|
367
|
-
const { error: dbError3, data } = await updateOrCreateChannel(supabase, {
|
|
368
|
-
name: channel,
|
|
369
|
-
app_id: appid,
|
|
370
|
-
created_by: userId,
|
|
371
|
-
version: versionId,
|
|
372
|
-
owner_org: orgId,
|
|
373
|
-
})
|
|
374
|
-
if (dbError3) {
|
|
375
|
-
log.error(`Cannot set channel, the upload key is not allowed to do that, use the "all" for this. ${formatError(dbError3)}`)
|
|
376
|
-
program.error('')
|
|
377
|
-
}
|
|
378
|
-
const appidWeb = convertAppName(appid)
|
|
379
|
-
const bundleUrl = `${localConfig.hostWeb}/app/p/${appidWeb}/channel/${data.id}`
|
|
380
|
-
if (data?.public)
|
|
381
|
-
log.info('Your update is now available in your public channel 🎉')
|
|
382
|
-
else if (data?.id)
|
|
383
|
-
log.info(`Link device to this bundle to try it: ${bundleUrl}`)
|
|
384
|
-
|
|
385
|
-
if (displayBundleUrl) {
|
|
386
|
-
log.info(`Bundle url: ${bundleUrl}`)
|
|
387
|
-
}
|
|
388
|
-
else if (!versionId) {
|
|
389
|
-
log.warn('Cannot set bundle with upload key, use key with more rights for that')
|
|
390
|
-
program.error('')
|
|
391
|
-
}
|
|
392
|
-
else if (!hasOrganizationPerm(permissions, OrganizationPerm.write)) {
|
|
393
|
-
log.warn('Cannot set channel as a upload organization member')
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
export async function uploadBundle(preAppid: string, options: Options, shouldExit = true) {
|
|
399
|
-
intro(`Uploading`)
|
|
400
|
-
const pm = getPMAndCommand()
|
|
401
|
-
await checkLatest()
|
|
402
|
-
|
|
403
|
-
const { s3Region, s3Apikey, s3Apisecret, s3BucketName, s3Endpoint, s3Port, s3SSL } = options
|
|
404
|
-
|
|
405
|
-
const apikey = getApikey(options)
|
|
406
|
-
const extConfig = await getConfig()
|
|
407
|
-
const { appid, path } = getAppIdAndPath(preAppid, options, extConfig.config)
|
|
408
|
-
const bundle = await getBundle(extConfig.config, options)
|
|
409
|
-
const channel = options.channel || 'dev'
|
|
410
|
-
const snag = useLogSnag()
|
|
411
|
-
|
|
412
|
-
checkNotifyAppReady(options, path)
|
|
413
|
-
|
|
414
|
-
log.info(`Upload ${appid}@${bundle} started from path "${path}" to Capgo cloud`)
|
|
415
|
-
|
|
416
|
-
const localConfig = await getLocalConfig()
|
|
417
|
-
const supabase = await createSupabaseClient(apikey)
|
|
418
|
-
const userId = await verifyUser(supabase, apikey, ['write', 'all', 'upload'])
|
|
419
|
-
// Check we have app access to this appId
|
|
420
|
-
const permissions = await checkAppExistsAndHasPermissionOrgErr(supabase, apikey, appid, OrganizationPerm.upload)
|
|
421
|
-
|
|
422
|
-
// Now if it does exist we will fetch the org id
|
|
423
|
-
const orgId = await getOrganizationId(supabase, appid)
|
|
424
|
-
await checkPlanValid(supabase, orgId, apikey, appid, true)
|
|
425
|
-
await checkTrial(supabase, orgId, localConfig)
|
|
426
|
-
const { nativePackages, minUpdateVersion } = await verifyCompatibility(supabase, pm, options, channel, appid, bundle)
|
|
427
|
-
await checkVersionExists(supabase, appid, bundle)
|
|
428
|
-
|
|
429
|
-
if (options.external && !options.external.startsWith('https://')) {
|
|
430
|
-
log.error(`External link should should start with "https://" current is "${external}"`)
|
|
431
|
-
program.error('')
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const versionData = {
|
|
435
|
-
// bucket_id: external ? undefined : fileName,
|
|
436
|
-
name: bundle,
|
|
437
|
-
app_id: appid,
|
|
438
|
-
session_key: undefined as undefined | string,
|
|
439
|
-
external_url: options.external,
|
|
440
|
-
storage_provider: options.external ? 'external' : 'r2-direct',
|
|
441
|
-
minUpdateVersion,
|
|
442
|
-
native_packages: nativePackages,
|
|
443
|
-
owner_org: orgId,
|
|
444
|
-
user_id: userId,
|
|
445
|
-
checksum: undefined as undefined | string,
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
let zipped: Buffer | null = null
|
|
449
|
-
if (!options.external) {
|
|
450
|
-
const { zipped: _zipped, sessionKey, checksum } = await prepareBundleFile(path, options, localConfig, snag, orgId, appid)
|
|
451
|
-
versionData.session_key = sessionKey
|
|
452
|
-
versionData.checksum = checksum
|
|
453
|
-
zipped = _zipped
|
|
454
|
-
if (!options.ignoreChecksumCheck) {
|
|
455
|
-
await checkChecksum(supabase, appid, channel, checksum)
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
else {
|
|
459
|
-
await snag.track({
|
|
460
|
-
channel: 'app',
|
|
461
|
-
event: 'App external',
|
|
462
|
-
icon: '📤',
|
|
463
|
-
user_id: orgId,
|
|
464
|
-
tags: {
|
|
465
|
-
'app-id': appid,
|
|
466
|
-
},
|
|
467
|
-
notify: false,
|
|
468
|
-
}).catch()
|
|
469
|
-
versionData.session_key = options.ivSessionKey
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const { error: dbError } = await updateOrCreateVersion(supabase, versionData)
|
|
473
|
-
if (dbError) {
|
|
474
|
-
log.error(`Cannot add bundle ${formatError(dbError)}`)
|
|
475
|
-
program.error('')
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (zipped && (s3BucketName || s3Endpoint || s3Region || s3Apikey || s3Apisecret || s3Port || s3SSL)) {
|
|
479
|
-
if (!s3BucketName || !s3Endpoint || !s3Region || !s3Apikey || !s3Apisecret || !s3Port) {
|
|
480
|
-
log.error('Missing argument, for S3 upload you need to provide a bucket name, endpoint, region, port, API key, and API secret')
|
|
481
|
-
program.error('')
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
log.info('Uploading to S3')
|
|
485
|
-
const s3Client = new S3Client({
|
|
486
|
-
endPoint: s3Endpoint,
|
|
487
|
-
region: s3Region,
|
|
488
|
-
port: s3Port,
|
|
489
|
-
useSSL: s3SSL,
|
|
490
|
-
bucket: s3BucketName,
|
|
491
|
-
credentials: {
|
|
492
|
-
accessKeyId: s3Apikey,
|
|
493
|
-
secretAccessKey: s3Apisecret,
|
|
494
|
-
},
|
|
495
|
-
})
|
|
496
|
-
const fileName = `${appid}-${bundle}`
|
|
497
|
-
const encodeFileName = encodeURIComponent(fileName)
|
|
498
|
-
await s3Client.putObject(fileName, zipped)
|
|
499
|
-
versionData.external_url = `https://${s3Endpoint}/${encodeFileName}`
|
|
500
|
-
versionData.storage_provider = 'external'
|
|
501
|
-
}
|
|
502
|
-
else if (zipped) {
|
|
503
|
-
await uploadBundleToCapgoCloud(supabase, appid, bundle, orgId, zipped, options)
|
|
504
|
-
|
|
505
|
-
versionData.storage_provider = 'r2'
|
|
506
|
-
const { error: dbError2 } = await updateOrCreateVersion(supabase, versionData)
|
|
507
|
-
if (dbError2) {
|
|
508
|
-
log.error(`Cannot update bundle ${formatError(dbError2)}`)
|
|
509
|
-
program.error('')
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
await setVersionInChannel(supabase, apikey, !!options.bundleUrl, bundle, channel, userId, orgId, appid, localConfig, permissions)
|
|
514
|
-
|
|
515
|
-
await snag.track({
|
|
516
|
-
channel: 'app',
|
|
517
|
-
event: 'App Uploaded',
|
|
518
|
-
icon: '⏫',
|
|
519
|
-
user_id: orgId,
|
|
520
|
-
tags: {
|
|
521
|
-
'app-id': appid,
|
|
522
|
-
},
|
|
523
|
-
notify: false,
|
|
524
|
-
}).catch()
|
|
525
|
-
if (shouldExit) {
|
|
526
|
-
outro('Time to share your update to the world 🌍')
|
|
527
|
-
exit()
|
|
528
|
-
}
|
|
529
|
-
return true
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
export async function uploadCommand(appid: string, options: Options) {
|
|
533
|
-
try {
|
|
534
|
-
await uploadBundle(appid, options, true)
|
|
535
|
-
}
|
|
536
|
-
catch (error) {
|
|
537
|
-
log.error(formatError(error))
|
|
538
|
-
program.error('')
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
export async function uploadDeprecatedCommand(appid: string, options: Options) {
|
|
543
|
-
const pm = getPMAndCommand()
|
|
544
|
-
log.warn(`⚠️ This command is deprecated, use "${pm.runner} @capgo/cli bundle upload" instead ⚠️`)
|
|
545
|
-
try {
|
|
546
|
-
await uploadBundle(appid, options, true)
|
|
547
|
-
}
|
|
548
|
-
catch (error) {
|
|
549
|
-
log.error(formatError(error))
|
|
550
|
-
program.error('')
|
|
551
|
-
}
|
|
552
|
-
}
|
package/src/bundle/zip.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { writeFileSync } from 'node:fs'
|
|
3
|
-
import { exit } from 'node:process'
|
|
4
|
-
import { program } from 'commander'
|
|
5
|
-
import { checksum as getChecksum } from '@tomasklaen/checksum'
|
|
6
|
-
import { intro, log, outro, spinner } from '@clack/prompts'
|
|
7
|
-
import { checkLatest } from '../api/update'
|
|
8
|
-
import type {
|
|
9
|
-
OptionsBase,
|
|
10
|
-
} from '../utils'
|
|
11
|
-
import {
|
|
12
|
-
formatError,
|
|
13
|
-
getConfig,
|
|
14
|
-
readPackageJson,
|
|
15
|
-
regexSemver,
|
|
16
|
-
useLogSnag,
|
|
17
|
-
zipFile,
|
|
18
|
-
} from '../utils'
|
|
19
|
-
import { checkIndexPosition, searchInDirectory } from './check'
|
|
20
|
-
|
|
21
|
-
const alertMb = 20
|
|
22
|
-
|
|
23
|
-
interface Options extends OptionsBase {
|
|
24
|
-
bundle?: string
|
|
25
|
-
path?: string
|
|
26
|
-
codeCheck?: boolean
|
|
27
|
-
name?: string
|
|
28
|
-
json?: boolean
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function zipBundle(appId: string, options: Options) {
|
|
32
|
-
try {
|
|
33
|
-
let { bundle, path } = options
|
|
34
|
-
const { json } = options
|
|
35
|
-
const snag = useLogSnag()
|
|
36
|
-
if (!json)
|
|
37
|
-
await checkLatest()
|
|
38
|
-
|
|
39
|
-
const extConfig = await getConfig()
|
|
40
|
-
appId = appId || extConfig?.config?.appId
|
|
41
|
-
// create bundle name format : 1.0.0-beta.x where x is a uuid
|
|
42
|
-
const uuid = randomUUID().split('-')[0]
|
|
43
|
-
const pack = await readPackageJson()
|
|
44
|
-
bundle = bundle || pack?.version || `0.0.1-beta.${uuid}`
|
|
45
|
-
if (!json)
|
|
46
|
-
intro(`Zipping ${appId}@${bundle}`)
|
|
47
|
-
// check if bundle is valid
|
|
48
|
-
if (bundle && !regexSemver.test(bundle)) {
|
|
49
|
-
if (!json)
|
|
50
|
-
log.error(`Your bundle name ${bundle}, is not valid it should follow semver convention : https://semver.org/`)
|
|
51
|
-
else
|
|
52
|
-
console.error(formatError({ error: 'invalid_semver' }))
|
|
53
|
-
program.error('')
|
|
54
|
-
}
|
|
55
|
-
path = path || extConfig?.config?.webDir
|
|
56
|
-
if (!appId || !bundle || !path) {
|
|
57
|
-
if (!json)
|
|
58
|
-
log.error('Missing argument, you need to provide a appId and a bundle and a path, or be in a capacitor project')
|
|
59
|
-
else
|
|
60
|
-
console.error(formatError({ error: 'missing_argument' }))
|
|
61
|
-
program.error('')
|
|
62
|
-
}
|
|
63
|
-
if (!json)
|
|
64
|
-
log.info(`Started from path "${path}"`)
|
|
65
|
-
const checkNotifyAppReady = options.codeCheck
|
|
66
|
-
if (typeof checkNotifyAppReady === 'undefined' || checkNotifyAppReady) {
|
|
67
|
-
const isPluginConfigured = searchInDirectory(path, 'notifyAppReady')
|
|
68
|
-
if (!isPluginConfigured) {
|
|
69
|
-
if (!json)
|
|
70
|
-
log.error(`notifyAppReady() is missing in the source code. see: https://capgo.app/docs/plugin/api/#notifyappready`)
|
|
71
|
-
else
|
|
72
|
-
console.error(formatError({ error: 'notifyAppReady_not_in_source_code' }))
|
|
73
|
-
program.error('')
|
|
74
|
-
}
|
|
75
|
-
const foundIndex = checkIndexPosition(path)
|
|
76
|
-
if (!foundIndex) {
|
|
77
|
-
if (!json)
|
|
78
|
-
log.error(`index.html is missing in the root folder or in the only folder in the root folder`)
|
|
79
|
-
else
|
|
80
|
-
console.error(formatError({ error: 'index_html_not_found' }))
|
|
81
|
-
program.error('')
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
const zipped = await zipFile(path)
|
|
85
|
-
if (!json)
|
|
86
|
-
log.info(`Zipped ${zipped.byteLength} bytes`)
|
|
87
|
-
const s = spinner()
|
|
88
|
-
if (!json)
|
|
89
|
-
s.start(`Calculating checksum`)
|
|
90
|
-
const checksum = await getChecksum(zipped, 'crc32')
|
|
91
|
-
if (!json)
|
|
92
|
-
s.stop(`Checksum: ${checksum}`)
|
|
93
|
-
const mbSize = Math.floor(zipped.byteLength / 1024 / 1024)
|
|
94
|
-
// We do not issue this warning for json
|
|
95
|
-
if (mbSize > alertMb && !json) {
|
|
96
|
-
log.warn(`WARNING !!\nThe app size is ${mbSize} Mb, this may take a while to download for users\n`)
|
|
97
|
-
log.warn(`Learn how to optimize your assets https://capgo.app/blog/optimise-your-images-for-updates/\n`)
|
|
98
|
-
await snag.track({
|
|
99
|
-
channel: 'app-error',
|
|
100
|
-
event: 'App Too Large',
|
|
101
|
-
icon: '🚛',
|
|
102
|
-
tags: {
|
|
103
|
-
'app-id': appId,
|
|
104
|
-
},
|
|
105
|
-
notify: false,
|
|
106
|
-
}).catch()
|
|
107
|
-
}
|
|
108
|
-
const s2 = spinner()
|
|
109
|
-
const name = options.name || `${appId}_${bundle}.zip`
|
|
110
|
-
if (!json)
|
|
111
|
-
s2.start(`Saving to ${name}`)
|
|
112
|
-
writeFileSync(name, zipped)
|
|
113
|
-
if (!json)
|
|
114
|
-
s2.stop(`Saved to ${name}`)
|
|
115
|
-
|
|
116
|
-
await snag.track({
|
|
117
|
-
channel: 'app',
|
|
118
|
-
event: 'App zip',
|
|
119
|
-
icon: '⏫',
|
|
120
|
-
tags: {
|
|
121
|
-
'app-id': appId,
|
|
122
|
-
},
|
|
123
|
-
notify: false,
|
|
124
|
-
}).catch()
|
|
125
|
-
|
|
126
|
-
if (!json)
|
|
127
|
-
outro(`Done ✅`)
|
|
128
|
-
|
|
129
|
-
if (json) {
|
|
130
|
-
const output = {
|
|
131
|
-
bundle,
|
|
132
|
-
filename: name,
|
|
133
|
-
checksum,
|
|
134
|
-
}
|
|
135
|
-
// Keep the console log and stringify for user who parse the output
|
|
136
|
-
// eslint-disable-next-line no-console
|
|
137
|
-
console.log(JSON.stringify(output, null, 2))
|
|
138
|
-
}
|
|
139
|
-
exit()
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
log.error(formatError(error))
|
|
143
|
-
program.error('')
|
|
144
|
-
}
|
|
145
|
-
}
|