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