@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/.cz.toml +1 -1
- package/.eslintignore +1 -0
- package/CHANGELOG.md +34 -0
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/bin/add.ts +71 -36
- package/src/bin/delete.ts +122 -25
- package/src/bin/index.ts +9 -1
- package/src/bin/login.ts +23 -0
- package/src/bin/set.ts +47 -28
- package/src/bin/types_supabase.ts +2867 -0
- package/src/bin/upload.ts +118 -142
- package/src/bin/utils.ts +76 -4
- package/test/test_headers_rls.ts +26 -0
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 {
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|