@capgo/cli 0.9.0 → 0.11.1

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/src/bin/upload.ts CHANGED
@@ -1,66 +1,11 @@
1
1
  import AdmZip from 'adm-zip';
2
- import axios, { AxiosResponse } from 'axios';
3
- import prettyjson from 'prettyjson';
4
2
  import { program } from 'commander';
3
+ import { randomUUID } from 'crypto';
5
4
  import cliProgress from 'cli-progress';
6
- import axiosRetry from 'axios-retry';
7
- import { host, hostWeb, supaAnon, hostSupa, getConfig } from './utils';
8
-
9
- axiosRetry(axios, { retries: 5, retryDelay: axiosRetry.exponentialDelay });
10
- const oneMb = 1048576; // size of one mb in bytes
11
- const maxMb = 30;
12
- const alertMb = 25;
13
- // enum string format
14
- enum UploadMode {
15
- uft8 = 'utf8',
16
- base64 = 'base64',
17
- hex = 'hex',
18
- binary = 'binary'
19
- }
20
- interface uploadPayload {
21
- version: string
22
- appid: string
23
- fileName?: string
24
- channel: string
25
- format: UploadMode
26
- app: string,
27
- isMultipart: boolean,
28
- chunk: number,
29
- totalChunks: number,
30
- }
31
- interface uploadExternal {
32
- version: string
33
- appid: string
34
- channel: string
35
- external: string
36
- }
37
-
38
- interface ResApi {
39
- fileName: string
40
- }
41
-
42
- const formatDefault = UploadMode.binary;
43
- const chuckNumber = (l: number, divider: number) => l < divider ? 1 : Math.floor(l / divider)
44
- const chuckSize = (l: number, divider: number) => Math.floor(l / chuckNumber(l, divider))
45
-
46
- const mbConvert = {
47
- 'base64': (l: number) => Math.floor((l * 4) / 3),
48
- 'hex': (l: number) => Math.floor(l * 2),
49
- 'binary': (l: number) => l,
50
- 'utf8': (l: number) => l,
51
- }
52
-
53
- const sendToBack = async (data: uploadPayload | uploadExternal, apikey: string): Promise<AxiosResponse<ResApi>> => axios({
54
- method: 'POST',
55
- url: `${hostSupa}/upload`,
56
- data,
57
- validateStatus: () => true,
58
- headers: {
59
- 'Content-Type': 'application/json',
60
- 'apikey': apikey,
61
- authorization: `Bearer ${supaAnon}`
62
- }
63
- })
5
+ import {
6
+ host, hostWeb, getConfig, createSupabaseClient,
7
+ updateOrCreateChannel, updateOrCreateVersion, formatError, findSavedKey
8
+ } from './utils';
64
9
 
65
10
  interface Options {
66
11
  version: string
@@ -68,17 +13,17 @@ interface Options {
68
13
  apikey: string
69
14
  channel?: string
70
15
  external?: string
71
- format?: UploadMode
72
16
  }
17
+
18
+ const maxMb = 30;
19
+ const alertMb = 25;
20
+
73
21
  export const uploadVersion = async (appid: string, options: Options) => {
74
22
  let { version, path, channel } = options;
75
- const { apikey, external, format } = options;
23
+ const { external } = options;
24
+ const apikey = options.apikey || findSavedKey()
76
25
  channel = channel || 'dev';
77
26
  const config = await getConfig();
78
- let formatType = formatDefault;
79
- if (format && format in UploadMode) {
80
- formatType = format;
81
- }
82
27
  appid = appid || config?.app?.appId
83
28
  version = version || config?.app?.package?.version
84
29
  path = path || config?.app?.webDir
@@ -89,85 +34,116 @@ export const uploadVersion = async (appid: string, options: Options) => {
89
34
  program.error("Missing argument, you need to provide a appid and a version and a path, or be in a capacitor project");
90
35
  }
91
36
  console.log(`Upload ${appid}@${version} started from path "${path}" to Capgo cloud`);
92
- if (external) {
93
- try {
94
- const res = await sendToBack({
95
- version,
96
- appid,
97
- channel,
98
- external
99
- }, apikey)
100
- if (res.status !== 200) {
101
- program.error(`Server Error \n${prettyjson.render(res?.data || "")}`);
102
- }
103
- } catch (err) {
104
- if (axios.isAxiosError(err) && err.response) {
105
- program.error(`Network Error \n${prettyjson.render(err.response?.data)}`);
106
- } else {
107
- program.error(`Unknow error \n${prettyjson.render(err)}`);
108
- }
37
+
38
+ const supabase = createSupabaseClient(apikey)
39
+ const multibar = new cliProgress.MultiBar({
40
+ clearOnComplete: false,
41
+ hideCursor: true
42
+ }, cliProgress.Presets.shades_grey);
43
+
44
+ // add bars
45
+ const b1 = multibar.create(7, 0, {
46
+ format: 'Uploading: [{bar}] {percentage}% | ETA: {eta}s | {value}/{total} Part'
47
+ }, cliProgress.Presets.shades_grey);
48
+ b1.start(7, 0, {
49
+ speed: "N/A"
50
+ });
51
+ // checking if user has access rights before uploading
52
+ const { data: apiAccess, error: apiAccessError } = await supabase
53
+ .rpc('is_allowed_capgkey', { apikey, keymode: ['upload', 'write', 'all'], app_id: appid })
54
+
55
+ if (!apiAccess || apiAccessError) {
56
+ multibar.stop()
57
+ program.error(`Invalid API key or insufisant rights ${formatError(apiAccessError)}`);
58
+ }
59
+ b1.increment();
60
+
61
+ // checking if user has access rights before uploading
62
+ const { data: versionExist, error: versionExistError } = await supabase
63
+ .rpc('exist_app_versions', { apikey, version_name: version, appid })
64
+
65
+ if (versionExist || versionExistError) {
66
+ multibar.stop()
67
+ program.error(`This app version already exist ${formatError(versionExistError)}`);
68
+ }
69
+ b1.increment();
70
+
71
+ const { data, error: userIdError } = await supabase
72
+ .rpc<string>('get_user_id', { apikey })
73
+
74
+ const userId = data ? data.toString() : '';
75
+ if (!userId || userIdError) {
76
+ multibar.stop()
77
+ program.error(`Cannot verify user ${formatError(userIdError)}`);
78
+ }
79
+ b1.increment();
80
+
81
+ const { data: app, error: dbError0 } = await supabase
82
+ .rpc<string>('exist_app', { appid, apikey })
83
+ if (!app || dbError0) {
84
+ multibar.stop()
85
+ program.error(`Cannot find app ${appid} in your account \n${formatError(dbError0)}`)
86
+ }
87
+ b1.increment();
88
+
89
+ if (!external) {
90
+ const zip = new AdmZip();
91
+ zip.addLocalFolder(path);
92
+ const zipped = zip.toBuffer();
93
+ const mbSize = Math.floor(zipped.byteLength / 1024 / 1024);
94
+ const filePath = `apps/${userId}/${appid}/versions`
95
+ const fileName = randomUUID()
96
+ b1.increment();
97
+ if (mbSize > maxMb) {
98
+ multibar.stop()
99
+ program.error(`The app is too big, the limit is ${maxMb} Mb, your is ${mbSize} Mb`);
109
100
  }
110
- } else {
111
- const b1 = new cliProgress.SingleBar({
112
- format: 'Uploading: [{bar}] {percentage}% | ETA: {eta}s | {value}/{total} Mb'
113
- }, cliProgress.Presets.shades_grey);
114
- try {
115
- const zip = new AdmZip();
116
- zip.addLocalFolder(path);
117
- const zipped = zip.toBuffer();
118
- const appData = zipped.toString(formatType);
119
- // split appData in chunks and send them sequentially with axios
120
- // console.log('appData size', appData.length)
121
- const zippedSize = appData.length;
122
- const mbSize = Math.floor(zippedSize / mbConvert[formatType](oneMb));
123
- // console.log('mbSize', zippedSize, mbSize, mbConvert[formatType](oneMb))
124
- const chunkSize = chuckNumber(zippedSize, mbConvert[formatType](oneMb)) > 1
125
- ? chuckSize(zippedSize, mbConvert[formatType](oneMb)) : zippedSize;
126
- if (mbSize > maxMb) {
127
- program.error(`The app is too big, the limit is ${maxMb} Mb, your is ${mbSize} Mb`);
128
- }
129
- if (mbSize > alertMb) {
130
- console.log(`WARNING !!\nThe app size is ${mbSize} Mb, the limit is ${maxMb} Mb`);
131
- }
132
- const chunks = [];
133
- for (let i = 0; i < appData.length; i += chunkSize) {
134
- chunks.push(appData.slice(i, i + chunkSize));
135
- }
136
- b1.start(chunks.length, 0, {
137
- speed: "N/A"
138
- });
139
- let fileName
140
- for (let i = 0; i < chunks.length; i += 1) {
141
- const response = await sendToBack({
142
- version,
143
- appid,
144
- fileName,
145
- channel,
146
- format: formatType,
147
- app: chunks[i],
148
- isMultipart: chunks.length > 1,
149
- chunk: i + 1,
150
- totalChunks: chunks.length,
151
- }, apikey)
152
- if (response.status !== 200 || !response.data.fileName) {
153
- b1.stop();
154
- program.error(`Server Error \n${prettyjson.render(response?.data || "")}`);
155
- }
156
- b1.update(i + 1)
157
- const data: ResApi = response.data as ResApi
158
- fileName = data.fileName
159
- }
160
- b1.stop();
161
- } catch (err) {
162
- b1.stop();
163
- if (axios.isAxiosError(err) && err.response) {
164
- program.error(`Network Error \n${prettyjson.render(err.response?.data)}`);
165
- } else {
166
- program.error(`Unknow error \n${prettyjson.render(err)}`);
167
- }
101
+ if (mbSize > alertMb) {
102
+ multibar.log(`WARNING !!\nThe app size is ${mbSize} Mb, the limit is ${maxMb} Mb\n`);
168
103
  }
169
- }
170
104
 
105
+ const { error: upError } = await supabase.storage
106
+ .from(filePath)
107
+ .upload(fileName, zipped, {
108
+ contentType: 'application/zip',
109
+ })
110
+ if (upError) {
111
+ multibar.stop()
112
+ program.error(`Cannot upload ${formatError(upError)}`)
113
+ }
114
+ } else if (external && !external.startsWith('https://')) {
115
+ multibar.stop()
116
+ program.error(`External link should should start with "https://" current is "${external}"`)
117
+ }
118
+ b1.increment();
119
+ const fileName = randomUUID()
120
+ const { data: versionData, error: dbError } = await updateOrCreateVersion(supabase, {
121
+ bucket_id: external ? undefined : fileName,
122
+ user_id: userId,
123
+ name: version,
124
+ app_id: appid,
125
+ external_url: external,
126
+ }, apikey)
127
+ if (dbError) {
128
+ multibar.stop()
129
+ program.error(`Cannot add version ${formatError(dbError)}`)
130
+ }
131
+ b1.increment();
132
+ if (versionData && versionData.length) {
133
+ const { error: dbError3 } = await updateOrCreateChannel(supabase, {
134
+ name: channel,
135
+ app_id: appid,
136
+ created_by: userId,
137
+ version: versionData[0].id,
138
+ }, apikey)
139
+ if (dbError3) {
140
+ multibar.log('Cannot set version with upload key, use key with more rights for that\n');
141
+ }
142
+ } else {
143
+ multibar.log('Cannot set version with upload key, use key with more rights for that\n');
144
+ }
145
+ b1.increment();
146
+ multibar.stop()
171
147
  console.log("App uploaded to server")
172
148
  console.log(`Try it in mobile app: ${host}/app_mobile`)
173
149
  console.log(`Or set the channel ${channel} as public here: ${hostWeb}/app/package/${appid}`)
package/src/bin/utils.ts CHANGED
@@ -1,19 +1,48 @@
1
1
  import { loadConfig } from '@capacitor/cli/dist/config';
2
2
  import { program } from 'commander';
3
+ import { createClient, SupabaseClient } from '@supabase/supabase-js'
4
+ import prettyjson from 'prettyjson';
5
+ import fs from 'fs'
6
+ import os from 'os'
7
+ import { definitions } from './types_supabase';
3
8
 
4
9
  export const host = 'https://capgo.app';
5
10
  export const hostWeb = 'https://web.capgo.app';
6
- export const hostSupa = 'https://xvwzpoazmxkqosrdewyv.functions.supabase.co';
7
- // export const hostSupa = 'https://aucsybvnhavogdmzwtcw.functions.supabase.co';
11
+ // export const hostSupa = 'https://xvwzpoazmxkqosrdewyv.functions.supabase.co';
12
+ export const hostSupa = 'https://aucsybvnhavogdmzwtcw.supabase.co';
8
13
 
9
14
  // For local test purposes
10
15
  // export const host = 'http://localhost:3334';
11
16
 
12
17
  /* eslint-disable */
13
- export const supaAnon = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNzgwNTAwOSwiZXhwIjoxOTUzMzgxMDA5fQ.8tgID1d4jodPwuo_fz4KHN4o1XKB9fnqyt0_GaJSj-w'
14
- // export const supaAnon = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImF1Y3N5YnZuaGF2b2dkbXp3dGN3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTE4NDE4MjMsImV4cCI6MTk2NzQxNzgyM30.AYzjGigPxlTw4eEkCjoGfYph8WRU3QXgIDcMWptQyAQ'
18
+ // export const supaAnon = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNzgwNTAwOSwiZXhwIjoxOTUzMzgxMDA5fQ.8tgID1d4jodPwuo_fz4KHN4o1XKB9fnqyt0_GaJSj-w'
19
+ export const supaAnon = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImF1Y3N5YnZuaGF2b2dkbXp3dGN3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTM1ODcxNjgsImV4cCI6MTk2OTE2MzE2OH0.8FKKJqiGgoVA3p9GH5wvnbWkWywIxVLqQyZFhupZ7C4'
15
20
  /* eslint-enable */
16
21
 
22
+ export const createSupabaseClient = (apikey: string) => createClient(hostSupa, supaAnon, {
23
+ headers: {
24
+ capgkey: apikey,
25
+ }
26
+ })
27
+
28
+ export const findSavedKey = () => {
29
+ // search for key in home dir
30
+ const userHomeDir = os.homedir();
31
+ let keyPath = `${userHomeDir}/.capgo`;
32
+ if (fs.existsSync(keyPath)) {
33
+ const key = fs.readFileSync(keyPath, 'utf8');
34
+ return key.trim();
35
+ }
36
+ keyPath = `.capgo`;
37
+ if (fs.existsSync(keyPath)) {
38
+ const key = fs.readFileSync(keyPath, 'utf8');
39
+ return key.trim();
40
+ }
41
+ return null
42
+ }
43
+
44
+ export const formatError = (error: any) => error ? `\n${prettyjson.render(error)}` : ''
45
+
17
46
  interface Config {
18
47
  app: {
19
48
  appId: string;
@@ -32,4 +61,47 @@ export const getConfig = async () => {
32
61
  program.error("No capacitor config file found, run `cap init` first");
33
62
  }
34
63
  return config;
64
+ }
65
+
66
+ export const updateOrCreateVersion = async (supabase: SupabaseClient, update: Partial<definitions['app_versions']>, apikey: string) => {
67
+ // console.log('updateOrCreateVersion', update, apikey)
68
+ const { data, error } = await supabase
69
+ .rpc<string>('exist_app_versions', { appid: update.app_id, name_version: update.name, apikey })
70
+ if (data && !error) {
71
+ update.deleted = false
72
+ return supabase
73
+ .from<definitions['app_versions']>('app_versions')
74
+ .update(update, { returning: "minimal" })
75
+ .eq('app_id', update.app_id)
76
+ .eq('name', update.name)
77
+ }
78
+ // console.log('create Version', data, error)
79
+
80
+ return supabase
81
+ .from<definitions['app_versions']>('app_versions')
82
+ .insert(update, { returning: "minimal" })
83
+
84
+ }
85
+
86
+ export const updateOrCreateChannel = async (supabase: SupabaseClient, update: Partial<definitions['channels']>, apikey: string) => {
87
+ // console.log('updateOrCreateChannel', update)
88
+ if (!update.app_id || !update.name || !update.created_by) {
89
+ console.error('missing app_id, name, or created_by')
90
+ return Promise.reject(new Error('missing app_id, name, or created_by'))
91
+ }
92
+ const { data, error } = await supabase
93
+ .rpc<string>('exist_channel', { appid: update.app_id, name_channel: update.name, apikey })
94
+ if (data && !error) {
95
+ return supabase
96
+ .from<definitions['channels']>('channels')
97
+ .update(update, { returning: "minimal" })
98
+ .eq('app_id', update.app_id)
99
+ .eq('name', update.name)
100
+ .eq('created_by', update.created_by)
101
+ }
102
+ // console.log('create Channel', data, error)
103
+
104
+ return supabase
105
+ .from<definitions['channels']>('channels')
106
+ .insert(update, { returning: "minimal" })
35
107
  }
@@ -0,0 +1,26 @@
1
+ import { createClient } from '@supabase/supabase-js'
2
+
3
+ const supaUrl = 'https://aucsybvnhavogdmzwtcw.supabase.co'
4
+ const apikey = '***'
5
+ const anonKey = '***'
6
+ const init = async () => {
7
+ const supabase = createClient(supaUrl, anonKey, {
8
+ headers: {
9
+ capgkey: '***',
10
+ }
11
+ })
12
+ const { data: userId } = await supabase
13
+ .rpc('get_user_id', { apikey })
14
+ console.log('userId', userId)
15
+ const apps = await supabase.from('apps')
16
+ .select()
17
+ .eq('app_id', 'ee.forgr.captime')
18
+ console.log('apps', apps.data)
19
+ // try to find one app
20
+ }
21
+
22
+ init()
23
+ // (is_app_shared(uid(), app_id) OR is_allowed_apikey((((current_setting('request.headers'::text, true))::json ->> 'capgkey'::text))::character varying, '{read}'::key_mode[]))
24
+ // is_allowed_apikey(((current_setting('request.headers'::text, true))::json ->> 'capgkey'::text), '{read}'::key_mode[])
25
+
26
+ // (((current_setting('request.headers'::text, true))::json ->> 'capgkey'::text) = 'dKC9teP4zHh7Lr5ak7hErgYn385C'::text)