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