@capgo/cli 0.8.6 → 0.11.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/src/bin/upload.ts CHANGED
@@ -1,75 +1,29 @@
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 {
6
+ host, hostWeb, getConfig, createSupabaseClient,
7
+ updateOrCreateChannel, updateOrCreateVersion, formatError, findSavedKey
8
+ } from './utils';
9
9
 
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 {
10
+ interface Options {
22
11
  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 ? 1 : 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,
12
+ path: string
13
+ apikey: string
14
+ channel?: string
15
+ external?: string
42
16
  }
43
17
 
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
- }
18
+ const maxMb = 30;
19
+ const alertMb = 25;
58
20
 
59
- export const uploadVersion = async (appid, options) => {
21
+ export const uploadVersion = async (appid: string, options: Options) => {
60
22
  let { version, path, channel } = options;
61
- const { apikey, external, format } = options;
23
+ const { external } = options;
24
+ const apikey = options.apikey || findSavedKey()
62
25
  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
- }
26
+ const config = await getConfig();
73
27
  appid = appid || config?.app?.appId
74
28
  version = version || config?.app?.package?.version
75
29
  path = path || config?.app?.webDir
@@ -80,94 +34,106 @@ export const uploadVersion = async (appid, options) => {
80
34
  program.error("Missing argument, you need to provide a appid and a version and a path, or be in a capacitor project");
81
35
  }
82
36
  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
- }
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
+ const { data, error: userIdError } = await supabase
62
+ .rpc<string>('get_user_id', { apikey })
63
+
64
+ const userId = data ? data.toString() : '';
65
+ if (!userId || userIdError) {
66
+ multibar.stop()
67
+ program.error(`Cannot verify user ${formatError(userIdError)}`);
68
+ }
69
+ b1.increment();
70
+
71
+ const { data: app, error: dbError0 } = await supabase
72
+ .rpc<string>('exist_app', { appid, apikey })
73
+ if (!app || dbError0) {
74
+ multibar.stop()
75
+ program.error(`Cannot find app ${appid} in your account \n${formatError(dbError0)}`)
76
+ }
77
+ b1.increment();
78
+
79
+ if (!external) {
80
+ const zip = new AdmZip();
81
+ zip.addLocalFolder(path);
82
+ const zipped = zip.toBuffer();
83
+ const mbSize = Math.floor(zipped.byteLength / 1024 / 1024);
84
+ const filePath = `apps/${userId}/${appid}/versions`
85
+ const fileName = randomUUID()
86
+ b1.increment();
87
+ if (mbSize > maxMb) {
88
+ multibar.stop()
89
+ program.error(`The app is too big, the limit is ${maxMb} Mb, your is ${mbSize} Mb`);
90
+ }
91
+ if (mbSize > alertMb) {
92
+ multibar.log(`WARNING !!\nThe app size is ${mbSize} Mb, the limit is ${maxMb} Mb\n`);
93
+ }
94
+
95
+ const { error: upError } = await supabase.storage
96
+ .from(filePath)
97
+ .upload(fileName, zipped, {
98
+ contentType: 'application/zip',
100
99
  })
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
- }
100
+ if (upError) {
101
+ multibar.stop()
102
+ program.error(`Cannot upload ${formatError(upError)}`)
110
103
  }
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
- }
104
+ } else if (external && !external.startsWith('https://')) {
105
+ multibar.stop()
106
+ program.error(`External link should should start with "https://" current is "${external}"`)
107
+ }
108
+ b1.increment();
109
+ const fileName = randomUUID()
110
+ const { data: versionData, error: dbError } = await updateOrCreateVersion(supabase, {
111
+ bucket_id: external ? undefined : fileName,
112
+ user_id: userId,
113
+ name: version,
114
+ app_id: appid,
115
+ external_url: external,
116
+ }, apikey)
117
+ if (dbError) {
118
+ multibar.stop()
119
+ program.error(`Cannot add version ${formatError(dbError)}`)
120
+ }
121
+ b1.increment();
122
+ if (versionData && versionData.length) {
123
+ const { error: dbError3 } = await updateOrCreateChannel(supabase, {
124
+ name: channel,
125
+ app_id: appid,
126
+ created_by: userId,
127
+ version: versionData[0].id,
128
+ }, apikey)
129
+ if (dbError3) {
130
+ multibar.log('Cannot set version with upload key, use key with more rights for that\n');
168
131
  }
132
+ } else {
133
+ multibar.log('Cannot set version with upload key, use key with more rights for that\n');
169
134
  }
170
-
135
+ b1.increment();
136
+ multibar.stop()
171
137
  console.log("App uploaded to server")
172
138
  console.log(`Try it in mobile app: ${host}/app_mobile`)
173
139
  console.log(`Or set the channel ${channel} as public here: ${hostWeb}/app/package/${appid}`)
package/src/bin/utils.ts CHANGED
@@ -1,12 +1,107 @@
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 fs from 'fs'
6
+ import os from 'os'
7
+ import { definitions } from './types_supabase';
8
+
2
9
  export const host = 'https://capgo.app';
3
10
  export const hostWeb = 'https://web.capgo.app';
4
- export const hostUpload = 'https://xvwzpoazmxkqosrdewyv.functions.supabase.co/upload';
11
+ // export const hostSupa = 'https://xvwzpoazmxkqosrdewyv.functions.supabase.co';
12
+ export const hostSupa = 'https://aucsybvnhavogdmzwtcw.supabase.co';
13
+
5
14
  // For local test purposes
6
15
  // export const host = 'http://localhost:3334';
7
- // export const hostUpload = 'http://localhost:54321/functions/v1/upload';
8
- /* eslint-enable */
9
16
 
10
17
  /* eslint-disable */
11
- export const supaAnon = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNzgwNTAwOSwiZXhwIjoxOTUzMzgxMDA5fQ.8tgID1d4jodPwuo_fz4KHN4o1XKB9fnqyt0_GaJSj-w'
18
+ // export const supaAnon = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNzgwNTAwOSwiZXhwIjoxOTUzMzgxMDA5fQ.8tgID1d4jodPwuo_fz4KHN4o1XKB9fnqyt0_GaJSj-w'
19
+ export const supaAnon = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImF1Y3N5YnZuaGF2b2dkbXp3dGN3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTM1ODcxNjgsImV4cCI6MTk2OTE2MzE2OH0.8FKKJqiGgoVA3p9GH5wvnbWkWywIxVLqQyZFhupZ7C4'
12
20
  /* eslint-enable */
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
+
46
+ interface Config {
47
+ app: {
48
+ appId: string;
49
+ appName: string;
50
+ webDir: string;
51
+ package: {
52
+ version: string;
53
+ };
54
+ };
55
+ }
56
+ export const getConfig = async () => {
57
+ let config: Config;
58
+ try {
59
+ config = await loadConfig();
60
+ } catch (err) {
61
+ program.error("No capacitor config file found, run `cap init` first");
62
+ }
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" })
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)
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
  }