@capgo/cli 5.0.0-alpha.3 โ 5.0.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.
- package/README.md +197 -37
- package/dist/index.js +327 -65172
- package/dist/package.json +83 -0
- package/package.json +48 -61
- package/.eslintignore +0 -4
- package/.github/FUNDING.yml +0 -1
- package/.github/workflows/build.yml +0 -46
- package/.github/workflows/bump_version.yml +0 -56
- package/.github/workflows/test.yml +0 -30
- package/.prettierignore +0 -6
- package/.vscode/launch.json +0 -23
- package/.vscode/settings.json +0 -5
- package/.vscode/tasks.json +0 -42
- package/CHANGELOG.md +0 -2727
- package/build.mjs +0 -23
- package/bun.lockb +0 -0
- package/capacitor.config.ts +0 -33
- package/crypto_explained.png +0 -0
- package/eslint.config.js +0 -3
- package/renovate.json +0 -23
- package/src/api/app.ts +0 -75
- package/src/api/channels.ts +0 -142
- package/src/api/crypto.ts +0 -121
- package/src/api/devices_override.ts +0 -41
- package/src/api/update.ts +0 -12
- package/src/api/versions.ts +0 -98
- package/src/app/add.ts +0 -154
- package/src/app/debug.ts +0 -214
- package/src/app/delete.ts +0 -68
- package/src/app/info.ts +0 -87
- package/src/app/list.ts +0 -63
- package/src/app/set.ts +0 -94
- package/src/bundle/check.ts +0 -42
- package/src/bundle/cleanup.ts +0 -128
- package/src/bundle/compatibility.ts +0 -70
- package/src/bundle/decrypt.ts +0 -65
- package/src/bundle/delete.ts +0 -53
- package/src/bundle/encrypt.ts +0 -69
- package/src/bundle/list.ts +0 -43
- package/src/bundle/unlink.ts +0 -80
- package/src/bundle/upload.ts +0 -434
- package/src/bundle/zip.ts +0 -137
- package/src/channel/add.ts +0 -73
- package/src/channel/currentBundle.ts +0 -73
- package/src/channel/delete.ts +0 -51
- package/src/channel/list.ts +0 -49
- package/src/channel/set.ts +0 -171
- package/src/index.ts +0 -285
- package/src/init.ts +0 -301
- package/src/key.ts +0 -158
- package/src/login.ts +0 -66
- package/src/types/capacitor__cli.d.ts +0 -6
- package/src/types/supabase.types.ts +0 -2065
- package/src/utils.ts +0 -719
- 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/tsconfig.json +0 -39
package/src/bundle/upload.ts
DELETED
|
@@ -1,434 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
-
import process from 'node:process'
|
|
4
|
-
import type { Buffer } from 'node:buffer'
|
|
5
|
-
import AdmZip from 'adm-zip'
|
|
6
|
-
import { program } from 'commander'
|
|
7
|
-
import * as p from '@clack/prompts'
|
|
8
|
-
import { checksum as getChecksum } from '@tomasklaen/checksum'
|
|
9
|
-
import ciDetect from 'ci-info'
|
|
10
|
-
import ky from 'ky'
|
|
11
|
-
import { checkLatest } from '../api/update'
|
|
12
|
-
import { checkAppExistsAndHasPermissionOrgErr } from '../api/app'
|
|
13
|
-
import { encryptSource } from '../api/crypto'
|
|
14
|
-
import type {
|
|
15
|
-
OptionsBase,
|
|
16
|
-
} from '../utils'
|
|
17
|
-
import {
|
|
18
|
-
OrganizationPerm,
|
|
19
|
-
baseKey,
|
|
20
|
-
checKOldEncryption,
|
|
21
|
-
checkCompatibility,
|
|
22
|
-
checkPlanValid,
|
|
23
|
-
convertAppName,
|
|
24
|
-
createSupabaseClient,
|
|
25
|
-
findSavedKey,
|
|
26
|
-
formatError,
|
|
27
|
-
getAppOwner,
|
|
28
|
-
getConfig,
|
|
29
|
-
getLocalConfig,
|
|
30
|
-
getLocalDepenencies,
|
|
31
|
-
hasOrganizationPerm,
|
|
32
|
-
regexSemver,
|
|
33
|
-
requireUpdateMetadata,
|
|
34
|
-
updateOrCreateChannel,
|
|
35
|
-
updateOrCreateVersion,
|
|
36
|
-
uploadUrl,
|
|
37
|
-
useLogSnag,
|
|
38
|
-
verifyUser,
|
|
39
|
-
} from '../utils'
|
|
40
|
-
import { checkIndexPosition, searchInDirectory } from './check'
|
|
41
|
-
|
|
42
|
-
const alertMb = 20
|
|
43
|
-
|
|
44
|
-
interface Options extends OptionsBase {
|
|
45
|
-
bundle?: string
|
|
46
|
-
path?: string
|
|
47
|
-
channel?: string
|
|
48
|
-
displayIvSession?: boolean
|
|
49
|
-
external?: string
|
|
50
|
-
key?: boolean | string
|
|
51
|
-
keyData?: string
|
|
52
|
-
ivSessionKey?: string
|
|
53
|
-
bundleUrl?: boolean
|
|
54
|
-
codeCheck?: boolean
|
|
55
|
-
minUpdateVersion?: string
|
|
56
|
-
autoMinUpdateVersion?: boolean
|
|
57
|
-
ignoreMetadataCheck?: boolean
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export async function uploadBundle(appid: string, options: Options, shouldExit = true) {
|
|
61
|
-
p.intro(`Uploading`)
|
|
62
|
-
await checkLatest()
|
|
63
|
-
let { bundle, path, channel } = options
|
|
64
|
-
const { external, key, displayIvSession, autoMinUpdateVersion, ignoreMetadataCheck } = options
|
|
65
|
-
let { minUpdateVersion } = options
|
|
66
|
-
options.apikey = options.apikey || findSavedKey()
|
|
67
|
-
const snag = useLogSnag()
|
|
68
|
-
|
|
69
|
-
channel = channel || 'dev'
|
|
70
|
-
|
|
71
|
-
const config = await getConfig()
|
|
72
|
-
const localS3: boolean = (config.app.extConfig.plugins && config.app.extConfig.plugins.CapacitorUpdater
|
|
73
|
-
&& config.app.extConfig.plugins.CapacitorUpdater.localS3) === true
|
|
74
|
-
|
|
75
|
-
const checkNotifyAppReady = options.codeCheck
|
|
76
|
-
appid = appid || config?.app?.appId
|
|
77
|
-
// create bundle name format : 1.0.0-beta.x where x is a uuid
|
|
78
|
-
const uuid = randomUUID().split('-')[0]
|
|
79
|
-
bundle = bundle || config?.app?.extConfig?.plugins?.CapacitorUpdater?.version || config?.app?.package?.version || `0.0.1-beta.${uuid}`
|
|
80
|
-
// check if bundle is valid
|
|
81
|
-
if (!regexSemver.test(bundle)) {
|
|
82
|
-
p.log.error(`Your bundle name ${bundle}, is not valid it should follow semver convention : https://semver.org/`)
|
|
83
|
-
program.error('')
|
|
84
|
-
}
|
|
85
|
-
path = path || config?.app?.webDir
|
|
86
|
-
if (!options.apikey) {
|
|
87
|
-
p.log.error(`Missing API key, you need to provide a API key to upload your bundle`)
|
|
88
|
-
program.error('')
|
|
89
|
-
}
|
|
90
|
-
if (!appid || !bundle || !path) {
|
|
91
|
-
p.log.error('Missing argument, you need to provide a appid and a bundle and a path, or be in a capacitor project')
|
|
92
|
-
program.error('')
|
|
93
|
-
}
|
|
94
|
-
// check if path exist
|
|
95
|
-
if (!existsSync(path)) {
|
|
96
|
-
p.log.error(`Path ${path} does not exist, build your app first, or provide a valid path`)
|
|
97
|
-
program.error('')
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (typeof checkNotifyAppReady === 'undefined' || checkNotifyAppReady) {
|
|
101
|
-
const isPluginConfigured = searchInDirectory(path, 'notifyAppReady')
|
|
102
|
-
if (!isPluginConfigured) {
|
|
103
|
-
p.log.error(`notifyAppReady() is missing in the source code. see: https://capgo.app/docs/plugin/api/#notifyappready`)
|
|
104
|
-
program.error('')
|
|
105
|
-
}
|
|
106
|
-
const foundIndex = checkIndexPosition(path)
|
|
107
|
-
if (!foundIndex) {
|
|
108
|
-
p.log.error(`index.html is missing in the root folder or in the only folder in the root folder`)
|
|
109
|
-
program.error('')
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
p.log.info(`Upload ${appid}@${bundle} started from path "${path}" to Capgo cloud`)
|
|
114
|
-
|
|
115
|
-
const localConfig = await getLocalConfig()
|
|
116
|
-
const supabase = await createSupabaseClient(options.apikey)
|
|
117
|
-
const userId = await verifyUser(supabase, options.apikey, ['write', 'all', 'upload'])
|
|
118
|
-
// Check we have app access to this appId
|
|
119
|
-
// await checkAppExistsAndHasPermissionErr(supabase, options.apikey, appid);
|
|
120
|
-
|
|
121
|
-
const permissions = await checkAppExistsAndHasPermissionOrgErr(supabase, options.apikey, appid, OrganizationPerm.upload)
|
|
122
|
-
await checkPlanValid(supabase, userId, options.apikey, appid, true)
|
|
123
|
-
|
|
124
|
-
const updateMetadataRequired = await requireUpdateMetadata(supabase, channel)
|
|
125
|
-
|
|
126
|
-
// Check compatibility here
|
|
127
|
-
const { data: channelData, error: channelError } = await supabase
|
|
128
|
-
.from('channels')
|
|
129
|
-
.select('version ( minUpdateVersion, native_packages )')
|
|
130
|
-
.eq('name', channel)
|
|
131
|
-
.eq('app_id', appid)
|
|
132
|
-
.single()
|
|
133
|
-
|
|
134
|
-
// eslint-disable-next-line no-undef-init
|
|
135
|
-
let localDependencies: Awaited<ReturnType<typeof getLocalDepenencies>> | undefined = undefined
|
|
136
|
-
let finalCompatibility: Awaited<ReturnType<typeof checkCompatibility>>['finalCompatibility']
|
|
137
|
-
|
|
138
|
-
// We only check compatibility IF the channel exists
|
|
139
|
-
if (!channelError && channelData && channelData.version && (channelData.version as any).native_packages && !ignoreMetadataCheck) {
|
|
140
|
-
const spinner = p.spinner()
|
|
141
|
-
spinner.start(`Checking bundle compatibility with channel ${channel}`)
|
|
142
|
-
const {
|
|
143
|
-
finalCompatibility: finalCompatibilityWithChannel,
|
|
144
|
-
localDependencies: localDependenciesWithChannel,
|
|
145
|
-
} = await checkCompatibility(supabase, appid, channel)
|
|
146
|
-
|
|
147
|
-
finalCompatibility = finalCompatibilityWithChannel
|
|
148
|
-
localDependencies = localDependenciesWithChannel
|
|
149
|
-
|
|
150
|
-
if (finalCompatibility.find(x => x.localVersion !== x.remoteVersion)) {
|
|
151
|
-
p.log.error(`Your bundle is not compatible with the channel ${channel}`)
|
|
152
|
-
p.log.warn(`You can check compatibility with "npx @capgo/cli bundle compatibility"`)
|
|
153
|
-
|
|
154
|
-
if (autoMinUpdateVersion) {
|
|
155
|
-
minUpdateVersion = bundle
|
|
156
|
-
p.log.info(`Auto set min-update-version to ${minUpdateVersion}`)
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
else if (autoMinUpdateVersion) {
|
|
160
|
-
try {
|
|
161
|
-
const { minUpdateVersion: lastMinUpdateVersion } = channelData.version as any
|
|
162
|
-
if (!lastMinUpdateVersion || !regexSemver.test(lastMinUpdateVersion)) {
|
|
163
|
-
p.log.error('Invalid remote min update version, skipping auto setting compatibility')
|
|
164
|
-
program.error('')
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
minUpdateVersion = lastMinUpdateVersion
|
|
168
|
-
p.log.info(`Auto set min-update-version to ${minUpdateVersion}`)
|
|
169
|
-
}
|
|
170
|
-
catch (error) {
|
|
171
|
-
p.log.error(`Cannot auto set compatibility, invalid data ${channelData}`)
|
|
172
|
-
program.error('')
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
spinner.stop(`Bundle compatible with ${channel} channel`)
|
|
176
|
-
}
|
|
177
|
-
else if (!ignoreMetadataCheck) {
|
|
178
|
-
p.log.warn(`Channel ${channel} is new or it's your first upload with compatibility check, it will be ignored this time`)
|
|
179
|
-
localDependencies = await getLocalDepenencies()
|
|
180
|
-
|
|
181
|
-
if (autoMinUpdateVersion) {
|
|
182
|
-
minUpdateVersion = bundle
|
|
183
|
-
p.log.info(`Auto set min-update-version to ${minUpdateVersion}`)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (updateMetadataRequired && !minUpdateVersion && !ignoreMetadataCheck) {
|
|
188
|
-
p.log.error(`You need to provide a min-update-version to upload a bundle to this channel`)
|
|
189
|
-
program.error('')
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (minUpdateVersion) {
|
|
193
|
-
if (!regexSemver.test(minUpdateVersion)) {
|
|
194
|
-
p.log.error(`Your minimal version update ${minUpdateVersion}, is not valid it should follow semver convention : https://semver.org/`)
|
|
195
|
-
program.error('')
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const { data: isTrial, error: isTrialsError } = await supabase
|
|
200
|
-
.rpc('is_trial', { userid: userId })
|
|
201
|
-
.single()
|
|
202
|
-
if ((isTrial && isTrial > 0) || isTrialsError) {
|
|
203
|
-
p.log.warn(`WARNING !!\nTrial expires in ${isTrial} days`)
|
|
204
|
-
p.log.warn(`Upgrade here: ${localConfig.hostWeb}/dashboard/settings/plans`)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// check if app already exist
|
|
208
|
-
const { data: appVersion, error: appVersionError } = await supabase
|
|
209
|
-
.rpc('exist_app_versions', { appid, apikey: options.apikey, name_version: bundle })
|
|
210
|
-
.single()
|
|
211
|
-
|
|
212
|
-
if (appVersion || appVersionError) {
|
|
213
|
-
p.log.error(`Version already exists ${formatError(appVersionError)}`)
|
|
214
|
-
program.error('')
|
|
215
|
-
}
|
|
216
|
-
// make bundle safe for s3 name https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html
|
|
217
|
-
const safeBundle = bundle.replace(/[^a-zA-Z0-9-_.!*'()]/g, '__')
|
|
218
|
-
const fileName = `${safeBundle}.zip`
|
|
219
|
-
|
|
220
|
-
let sessionKey
|
|
221
|
-
let checksum = ''
|
|
222
|
-
let zipped: Buffer | null = null
|
|
223
|
-
if (!external) {
|
|
224
|
-
const zip = new AdmZip()
|
|
225
|
-
zip.addLocalFolder(path)
|
|
226
|
-
zipped = zip.toBuffer()
|
|
227
|
-
const s = p.spinner()
|
|
228
|
-
s.start(`Calculating checksum`)
|
|
229
|
-
checksum = await getChecksum(zipped, 'crc32')
|
|
230
|
-
s.stop(`Checksum: ${checksum}`)
|
|
231
|
-
if (key === false) {
|
|
232
|
-
p.log.info(`Encryption ignored`)
|
|
233
|
-
}
|
|
234
|
-
else if (key || existsSync(baseKey)) {
|
|
235
|
-
await checKOldEncryption()
|
|
236
|
-
const privateKey = typeof key === 'string' ? key : baseKey
|
|
237
|
-
let keyData = options.keyData || ''
|
|
238
|
-
// check if privateKey exist
|
|
239
|
-
if (!keyData && !existsSync(privateKey)) {
|
|
240
|
-
p.log.error(`Cannot find private key ${privateKey}`)
|
|
241
|
-
if (ciDetect.isCI)
|
|
242
|
-
program.error('')
|
|
243
|
-
|
|
244
|
-
const res = await p.confirm({ message: 'Do you want to use our public key ?' })
|
|
245
|
-
if (!res) {
|
|
246
|
-
p.log.error(`Error: Missing public key`)
|
|
247
|
-
program.error('')
|
|
248
|
-
}
|
|
249
|
-
keyData = localConfig.signKey || ''
|
|
250
|
-
}
|
|
251
|
-
await snag.track({
|
|
252
|
-
channel: 'app',
|
|
253
|
-
event: 'App encryption',
|
|
254
|
-
icon: '๐',
|
|
255
|
-
user_id: userId,
|
|
256
|
-
tags: {
|
|
257
|
-
'app-id': appid,
|
|
258
|
-
},
|
|
259
|
-
notify: false,
|
|
260
|
-
}).catch()
|
|
261
|
-
// open with fs privateKey path
|
|
262
|
-
if (!keyData) {
|
|
263
|
-
const keyFile = readFileSync(privateKey)
|
|
264
|
-
keyData = keyFile.toString()
|
|
265
|
-
}
|
|
266
|
-
// encrypt
|
|
267
|
-
p.log.info(`Encrypting your bundle`)
|
|
268
|
-
const res = encryptSource(zipped, keyData)
|
|
269
|
-
sessionKey = res.ivSessionKey
|
|
270
|
-
if (displayIvSession) {
|
|
271
|
-
p.log.info(`Your Iv Session key is ${sessionKey},
|
|
272
|
-
keep it safe, you will need it to decrypt your bundle.
|
|
273
|
-
It will be also visible in your dashboard\n`)
|
|
274
|
-
}
|
|
275
|
-
zipped = res.encryptedData
|
|
276
|
-
}
|
|
277
|
-
const mbSize = Math.floor(zipped.byteLength / 1024 / 1024)
|
|
278
|
-
if (mbSize > alertMb) {
|
|
279
|
-
p.log.warn(`WARNING !!\nThe app size is ${mbSize} Mb, this may take a while to download for users\n`)
|
|
280
|
-
p.log.info(`Learn how to optimize your assets https://capgo.app/blog/optimise-your-images-for-updates/\n`)
|
|
281
|
-
await snag.track({
|
|
282
|
-
channel: 'app-error',
|
|
283
|
-
event: 'App Too Large',
|
|
284
|
-
icon: '๐',
|
|
285
|
-
user_id: userId,
|
|
286
|
-
tags: {
|
|
287
|
-
'app-id': appid,
|
|
288
|
-
},
|
|
289
|
-
notify: false,
|
|
290
|
-
}).catch()
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
else if (external && !external.startsWith('https://')) {
|
|
294
|
-
p.log.error(`External link should should start with "https://" current is "${external}"`)
|
|
295
|
-
program.error('')
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
298
|
-
await snag.track({
|
|
299
|
-
channel: 'app',
|
|
300
|
-
event: 'App external',
|
|
301
|
-
icon: '๐ค',
|
|
302
|
-
user_id: userId,
|
|
303
|
-
tags: {
|
|
304
|
-
'app-id': appid,
|
|
305
|
-
},
|
|
306
|
-
notify: false,
|
|
307
|
-
}).catch()
|
|
308
|
-
sessionKey = options.ivSessionKey
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const hashedLocalDependencies = localDependencies
|
|
312
|
-
? new Map(localDependencies
|
|
313
|
-
.filter(a => !!a.native && a.native !== undefined)
|
|
314
|
-
.map(a => [a.name, a]))
|
|
315
|
-
: new Map()
|
|
316
|
-
|
|
317
|
-
const nativePackages = (hashedLocalDependencies.size > 0 || !options.ignoreMetadataCheck) ? Array.from(hashedLocalDependencies, ([name, value]) => ({ name, version: value.version })) : undefined
|
|
318
|
-
|
|
319
|
-
const appOwner = await getAppOwner(supabase, appid)
|
|
320
|
-
|
|
321
|
-
const versionData = {
|
|
322
|
-
bucket_id: external ? undefined : fileName,
|
|
323
|
-
user_id: appOwner,
|
|
324
|
-
name: bundle,
|
|
325
|
-
app_id: appid,
|
|
326
|
-
session_key: sessionKey,
|
|
327
|
-
external_url: external,
|
|
328
|
-
storage_provider: external ? 'external' : 'r2-direct',
|
|
329
|
-
minUpdateVersion,
|
|
330
|
-
native_packages: nativePackages,
|
|
331
|
-
checksum,
|
|
332
|
-
}
|
|
333
|
-
const { error: dbError } = await updateOrCreateVersion(supabase, versionData)
|
|
334
|
-
if (dbError) {
|
|
335
|
-
p.log.error(`Cannot add bundle ${formatError(dbError)}`)
|
|
336
|
-
program.error('')
|
|
337
|
-
}
|
|
338
|
-
if (!external && zipped) {
|
|
339
|
-
const spinner = p.spinner()
|
|
340
|
-
spinner.start(`Uploading Bundle`)
|
|
341
|
-
|
|
342
|
-
const url = await uploadUrl(supabase, appid, fileName)
|
|
343
|
-
if (!url) {
|
|
344
|
-
p.log.error(`Cannot get upload url`)
|
|
345
|
-
program.error('')
|
|
346
|
-
}
|
|
347
|
-
await ky.put(url, {
|
|
348
|
-
timeout: 60000,
|
|
349
|
-
body: zipped,
|
|
350
|
-
headers: (!localS3
|
|
351
|
-
? {
|
|
352
|
-
'Content-Type': 'application/octet-stream',
|
|
353
|
-
'Cache-Control': 'public, max-age=456789, immutable',
|
|
354
|
-
'x-amz-meta-crc32': checksum,
|
|
355
|
-
}
|
|
356
|
-
: undefined),
|
|
357
|
-
})
|
|
358
|
-
versionData.storage_provider = 'r2'
|
|
359
|
-
const { error: dbError2 } = await updateOrCreateVersion(supabase, versionData)
|
|
360
|
-
if (dbError2) {
|
|
361
|
-
p.log.error(`Cannot update bundle ${formatError(dbError2)}`)
|
|
362
|
-
program.error('')
|
|
363
|
-
}
|
|
364
|
-
spinner.stop('Bundle Uploaded ๐ช')
|
|
365
|
-
}
|
|
366
|
-
const { data: versionId } = await supabase
|
|
367
|
-
.rpc('get_app_versions', { apikey: options.apikey, name_version: bundle, appid })
|
|
368
|
-
.single()
|
|
369
|
-
|
|
370
|
-
if (versionId && hasOrganizationPerm(permissions, OrganizationPerm.write)) {
|
|
371
|
-
const { error: dbError3, data } = await updateOrCreateChannel(supabase, {
|
|
372
|
-
name: channel,
|
|
373
|
-
app_id: appid,
|
|
374
|
-
created_by: appOwner,
|
|
375
|
-
version: versionId,
|
|
376
|
-
})
|
|
377
|
-
if (dbError3) {
|
|
378
|
-
p.log.error(`Cannot set channel, the upload key is not allowed to do that, use the "all" for this. ${formatError(dbError3)}`)
|
|
379
|
-
program.error('')
|
|
380
|
-
}
|
|
381
|
-
const appidWeb = convertAppName(appid)
|
|
382
|
-
const bundleUrl = `${localConfig.hostWeb}/app/p/${appidWeb}/channel/${data.id}`
|
|
383
|
-
if (data?.public)
|
|
384
|
-
p.log.info('Your update is now available in your public channel ๐')
|
|
385
|
-
else if (data?.id)
|
|
386
|
-
p.log.info(`Link device to this bundle to try it: ${bundleUrl}`)
|
|
387
|
-
|
|
388
|
-
if (options.bundleUrl)
|
|
389
|
-
p.log.info(`Bundle url: ${bundleUrl}`)
|
|
390
|
-
}
|
|
391
|
-
else if (!versionId) {
|
|
392
|
-
p.log.warn('Cannot set bundle with upload key, use key with more rights for that')
|
|
393
|
-
program.error('')
|
|
394
|
-
}
|
|
395
|
-
else if (!hasOrganizationPerm(permissions, OrganizationPerm.write)) {
|
|
396
|
-
p.log.warn('Cannot set channel as a upload organization member')
|
|
397
|
-
}
|
|
398
|
-
await snag.track({
|
|
399
|
-
channel: 'app',
|
|
400
|
-
event: 'App Uploaded',
|
|
401
|
-
icon: 'โซ',
|
|
402
|
-
user_id: userId,
|
|
403
|
-
tags: {
|
|
404
|
-
'app-id': appid,
|
|
405
|
-
},
|
|
406
|
-
notify: false,
|
|
407
|
-
}).catch()
|
|
408
|
-
if (shouldExit) {
|
|
409
|
-
p.outro('Time to share your update to the world ๐')
|
|
410
|
-
process.exit()
|
|
411
|
-
}
|
|
412
|
-
return true
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
export async function uploadCommand(apikey: string, options: Options) {
|
|
416
|
-
try {
|
|
417
|
-
await uploadBundle(apikey, options, true)
|
|
418
|
-
}
|
|
419
|
-
catch (error) {
|
|
420
|
-
p.log.error(formatError(error))
|
|
421
|
-
program.error('')
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
export async function uploadDeprecatedCommand(apikey: string, options: Options) {
|
|
426
|
-
p.log.warn('โ ๏ธ This command is deprecated, use "npx @capgo/cli bundle upload" instead โ ๏ธ')
|
|
427
|
-
try {
|
|
428
|
-
await uploadBundle(apikey, options, true)
|
|
429
|
-
}
|
|
430
|
-
catch (error) {
|
|
431
|
-
p.log.error(formatError(error))
|
|
432
|
-
program.error('')
|
|
433
|
-
}
|
|
434
|
-
}
|
package/src/bundle/zip.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { writeFileSync } from 'node:fs'
|
|
3
|
-
import process from 'node:process'
|
|
4
|
-
import AdmZip from 'adm-zip'
|
|
5
|
-
import { program } from 'commander'
|
|
6
|
-
import * as p from '@clack/prompts'
|
|
7
|
-
import { checksum as getChecksum } from '@tomasklaen/checksum'
|
|
8
|
-
import { checkLatest } from '../api/update'
|
|
9
|
-
import type {
|
|
10
|
-
OptionsBase,
|
|
11
|
-
} from '../utils'
|
|
12
|
-
import {
|
|
13
|
-
formatError,
|
|
14
|
-
getConfig,
|
|
15
|
-
regexSemver,
|
|
16
|
-
useLogSnag,
|
|
17
|
-
} from '../utils'
|
|
18
|
-
import { checkIndexPosition, searchInDirectory } from './check'
|
|
19
|
-
|
|
20
|
-
const alertMb = 20
|
|
21
|
-
|
|
22
|
-
interface Options extends OptionsBase {
|
|
23
|
-
bundle?: string
|
|
24
|
-
path?: string
|
|
25
|
-
codeCheck?: boolean
|
|
26
|
-
name?: string
|
|
27
|
-
json?: boolean
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function zipBundle(appId: string, options: Options) {
|
|
31
|
-
let { bundle, path } = options
|
|
32
|
-
const { json } = options
|
|
33
|
-
const snag = useLogSnag()
|
|
34
|
-
if (!json)
|
|
35
|
-
await checkLatest()
|
|
36
|
-
|
|
37
|
-
const config = await getConfig()
|
|
38
|
-
appId = appId || config?.app?.appId
|
|
39
|
-
// create bundle name format : 1.0.0-beta.x where x is a uuid
|
|
40
|
-
const uuid = randomUUID().split('-')[0]
|
|
41
|
-
bundle = bundle || config?.app?.package?.version || `0.0.1-beta.${uuid}`
|
|
42
|
-
if (!json)
|
|
43
|
-
p.intro(`Zipping ${appId}@${bundle}`)
|
|
44
|
-
// check if bundle is valid
|
|
45
|
-
if (!regexSemver.test(bundle)) {
|
|
46
|
-
if (!json)
|
|
47
|
-
p.log.error(`Your bundle name ${bundle}, is not valid it should follow semver convention : https://semver.org/`)
|
|
48
|
-
else
|
|
49
|
-
console.error(formatError({ error: 'invalid_semver' }))
|
|
50
|
-
program.error('')
|
|
51
|
-
}
|
|
52
|
-
path = path || config?.app?.webDir
|
|
53
|
-
if (!appId || !bundle || !path) {
|
|
54
|
-
if (!json)
|
|
55
|
-
p.log.error('Missing argument, you need to provide a appId and a bundle and a path, or be in a capacitor project')
|
|
56
|
-
else
|
|
57
|
-
console.error(formatError({ error: 'missing_argument' }))
|
|
58
|
-
program.error('')
|
|
59
|
-
}
|
|
60
|
-
if (!json)
|
|
61
|
-
p.log.info(`Started from path "${path}"`)
|
|
62
|
-
const checkNotifyAppReady = options.codeCheck
|
|
63
|
-
if (typeof checkNotifyAppReady === 'undefined' || checkNotifyAppReady) {
|
|
64
|
-
const isPluginConfigured = searchInDirectory(path, 'notifyAppReady')
|
|
65
|
-
if (!isPluginConfigured) {
|
|
66
|
-
if (!json)
|
|
67
|
-
p.log.error(`notifyAppReady() is missing in the source code. see: https://capgo.app/docs/plugin/api/#notifyappready`)
|
|
68
|
-
else
|
|
69
|
-
console.error(formatError({ error: 'notifyAppReady_not_in_source_code' }))
|
|
70
|
-
program.error('')
|
|
71
|
-
}
|
|
72
|
-
const foundIndex = checkIndexPosition(path)
|
|
73
|
-
if (!foundIndex) {
|
|
74
|
-
if (!json)
|
|
75
|
-
p.log.error(`index.html is missing in the root folder or in the only folder in the root folder`)
|
|
76
|
-
else
|
|
77
|
-
console.error(formatError({ error: 'index_html_not_found' }))
|
|
78
|
-
program.error('')
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const zip = new AdmZip()
|
|
82
|
-
zip.addLocalFolder(path)
|
|
83
|
-
const zipped = zip.toBuffer()
|
|
84
|
-
if (!json)
|
|
85
|
-
p.log.info(`Zipped ${zipped.byteLength} bytes`)
|
|
86
|
-
const s = p.spinner()
|
|
87
|
-
if (!json)
|
|
88
|
-
s.start(`Calculating checksum`)
|
|
89
|
-
const checksum = await getChecksum(zipped, 'crc32')
|
|
90
|
-
if (!json)
|
|
91
|
-
s.stop(`Checksum: ${checksum}`)
|
|
92
|
-
const mbSize = Math.floor(zipped.byteLength / 1024 / 1024)
|
|
93
|
-
// We do not issue this warning for json
|
|
94
|
-
if (mbSize > alertMb && !json) {
|
|
95
|
-
p.log.warn(`WARNING !!\nThe app size is ${mbSize} Mb, this may take a while to download for users\n`)
|
|
96
|
-
p.log.warn(`Learn how to optimize your assets https://capgo.app/blog/optimise-your-images-for-updates/\n`)
|
|
97
|
-
await snag.track({
|
|
98
|
-
channel: 'app-error',
|
|
99
|
-
event: 'App Too Large',
|
|
100
|
-
icon: '๐',
|
|
101
|
-
tags: {
|
|
102
|
-
'app-id': appId,
|
|
103
|
-
},
|
|
104
|
-
notify: false,
|
|
105
|
-
}).catch()
|
|
106
|
-
}
|
|
107
|
-
const s2 = p.spinner()
|
|
108
|
-
const name = options.name || `${appId}_${bundle}.zip`
|
|
109
|
-
if (!json)
|
|
110
|
-
s2.start(`Saving to ${name}`)
|
|
111
|
-
writeFileSync(name, zipped)
|
|
112
|
-
if (!json)
|
|
113
|
-
s2.stop(`Saved to ${name}`)
|
|
114
|
-
|
|
115
|
-
if (options.json) {
|
|
116
|
-
const output = {
|
|
117
|
-
bundle,
|
|
118
|
-
filename: name,
|
|
119
|
-
checksum,
|
|
120
|
-
}
|
|
121
|
-
p.log.info(formatError(output))
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
await snag.track({
|
|
125
|
-
channel: 'app',
|
|
126
|
-
event: 'App zip',
|
|
127
|
-
icon: 'โซ',
|
|
128
|
-
tags: {
|
|
129
|
-
'app-id': appId,
|
|
130
|
-
},
|
|
131
|
-
notify: false,
|
|
132
|
-
}).catch()
|
|
133
|
-
|
|
134
|
-
if (!json)
|
|
135
|
-
p.outro(`Done โ
`)
|
|
136
|
-
process.exit()
|
|
137
|
-
}
|
package/src/channel/add.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import process from 'node:process'
|
|
2
|
-
import { program } from 'commander'
|
|
3
|
-
import * as p from '@clack/prompts'
|
|
4
|
-
import { checkAppExistsAndHasPermissionErr } from '../api/app'
|
|
5
|
-
import { createChannel, findUnknownVersion } from '../api/channels'
|
|
6
|
-
import type { OptionsBase } from '../utils'
|
|
7
|
-
import { createSupabaseClient, findSavedKey, getConfig, useLogSnag, verifyUser } from '../utils'
|
|
8
|
-
|
|
9
|
-
interface Options extends OptionsBase {
|
|
10
|
-
default?: boolean
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function addChannel(channelId: string, appId: string, options: Options, shouldExit = true) {
|
|
14
|
-
p.intro(`Create channel`)
|
|
15
|
-
options.apikey = options.apikey || findSavedKey()
|
|
16
|
-
const config = await getConfig()
|
|
17
|
-
appId = appId || config?.app?.appId
|
|
18
|
-
const snag = useLogSnag()
|
|
19
|
-
|
|
20
|
-
if (!options.apikey) {
|
|
21
|
-
p.log.error('Missing API key, you need to provide a API key to upload your bundle')
|
|
22
|
-
program.error('')
|
|
23
|
-
}
|
|
24
|
-
if (!appId) {
|
|
25
|
-
p.log.error('Missing argument, you need to provide a appId, or be in a capacitor project')
|
|
26
|
-
program.error('')
|
|
27
|
-
}
|
|
28
|
-
const supabase = await createSupabaseClient(options.apikey)
|
|
29
|
-
|
|
30
|
-
const userId = await verifyUser(supabase, options.apikey, ['write', 'all'])
|
|
31
|
-
// Check we have app access to this appId
|
|
32
|
-
await checkAppExistsAndHasPermissionErr(supabase, options.apikey, appId)
|
|
33
|
-
|
|
34
|
-
p.log.info(`Creating channel ${appId}#${channelId} to Capgo`)
|
|
35
|
-
try {
|
|
36
|
-
const data = await findUnknownVersion(supabase, appId)
|
|
37
|
-
if (!data) {
|
|
38
|
-
p.log.error(`Cannot find default version for channel creation, please contact Capgo support ๐คจ`)
|
|
39
|
-
program.error('')
|
|
40
|
-
}
|
|
41
|
-
await createChannel(supabase, {
|
|
42
|
-
name: channelId,
|
|
43
|
-
app_id: appId,
|
|
44
|
-
version: data.id,
|
|
45
|
-
created_by: userId,
|
|
46
|
-
})
|
|
47
|
-
p.log.success(`Channel created โ
`)
|
|
48
|
-
await snag.track({
|
|
49
|
-
channel: 'channel',
|
|
50
|
-
event: 'Create channel',
|
|
51
|
-
icon: 'โ
',
|
|
52
|
-
user_id: userId,
|
|
53
|
-
tags: {
|
|
54
|
-
'app-id': appId,
|
|
55
|
-
'channel': channelId,
|
|
56
|
-
},
|
|
57
|
-
notify: false,
|
|
58
|
-
}).catch()
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
p.log.error(`Cannot create Channel ๐`)
|
|
62
|
-
return false
|
|
63
|
-
}
|
|
64
|
-
if (shouldExit) {
|
|
65
|
-
p.outro(`Done โ
`)
|
|
66
|
-
process.exit()
|
|
67
|
-
}
|
|
68
|
-
return true
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function addChannelCommand(apikey: string, appId: string, options: Options) {
|
|
72
|
-
addChannel(apikey, appId, options, true)
|
|
73
|
-
}
|