cloudron 5.8.2 → 5.10.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/bin/cloudron CHANGED
@@ -109,16 +109,6 @@ program.command('clone')
109
109
  .option('--location <domain>', 'Subdomain or full domain')
110
110
  .action(actions.clone);
111
111
 
112
- program.command('configure')
113
- .description('Change location of an app')
114
- .option('--app <id/location>', 'App id or location')
115
- .option('--no-wait', 'Wait for healthcheck to succeed [false]')
116
- .option('-p, --port-bindings [PORT=port,...]', 'Query port bindings')
117
- .option('-l, --location <location>', 'Location')
118
- .option('-s, --secondary-domains [DOMAIN=domain,...]', 'Query/Set secondary domains')
119
- .option('-a, --alias-domains [domain,...]', 'Alias domains')
120
- .action(actions.configure);
121
-
122
112
  program.command('debug [cmd...]')
123
113
  .description('Put app in debug mode and run [cmd] as entrypoint. If cmd is "default" the main app entrypoint is run.')
124
114
  .option('--app <id/location>', 'App id or location')
@@ -275,6 +265,17 @@ program.command('restart')
275
265
  .option('--no-wait', 'Wait for healthcheck to succeed [false]')
276
266
  .action(actions.restart);
277
267
 
268
+ program.command('set-location')
269
+ .description('Set the location of an app')
270
+ .option('--app <id/location>', 'App id or location')
271
+ .option('--no-wait', 'Wait for healthcheck to succeed [false]')
272
+ .option('-p, --port-bindings [PORT=port,...]', 'Query port bindings')
273
+ .option('-l, --location <location>', 'Location')
274
+ .option('-s, --secondary-domains [DOMAIN=domain,...]', 'Query/Set secondary domains')
275
+ .option('-a, --alias-domains [domain,...]', 'Alias domains')
276
+ .alias('configure') // deprecated
277
+ .action(actions.setLocation);
278
+
278
279
  program.command('start')
279
280
  .description('Start an installed application')
280
281
  .option('--app <id/location>', 'App id or location')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudron",
3
- "version": "5.8.2",
3
+ "version": "5.10.0",
4
4
  "license": "MIT",
5
5
  "description": "Cloudron Commandline Tool",
6
6
  "main": "main.js",
package/src/actions.js CHANGED
@@ -31,7 +31,7 @@ exports = module.exports = {
31
31
  logout,
32
32
  open,
33
33
  install,
34
- configure,
34
+ setLocation,
35
35
  debug,
36
36
  update,
37
37
  uninstall,
@@ -677,7 +677,7 @@ async function install(localOptions, cmd) {
677
677
  }
678
678
  }
679
679
 
680
- async function configure(localOptions, cmd) {
680
+ async function setLocation(localOptions, cmd) {
681
681
  const options = cmd.optsWithGlobals();
682
682
 
683
683
  try {
@@ -873,18 +873,11 @@ async function repair(localOptions, cmd) {
873
873
  const app = await getApp(options);
874
874
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
875
875
 
876
- const { manifestFilePath } = await getManifest('' /* appStoreId */);
877
-
878
- let dockerImage = options.image;
879
- if (!dockerImage) {
880
- const sourceDir = path.dirname(manifestFilePath);
881
- dockerImage = config.getAppConfig(sourceDir).dockerImage;
882
- }
883
-
884
- if (!dockerImage) return exit('No image found, please run `cloudron build` first or use --image');
876
+ const data = {};
877
+ if (options.image) data.dockerImage = options.image;
885
878
 
886
879
  const request = createRequest('POST', `/api/v1/apps/${app.id}/repair`, options);
887
- const response = await request.send({ dockerImage });
880
+ const response = await request.send(data);
888
881
  if (response.statusCode !== 202) return exit(`Failed to set repair mode: ${requestError(response)}`);
889
882
 
890
883
  process.stdout.write('\n => ' + 'Waiting for app to be repaired ');
@@ -11,8 +11,7 @@ const assert = require('assert'),
11
11
  readlineSync = require('readline-sync'),
12
12
  safe = require('safetydance'),
13
13
  superagent = require('superagent'),
14
- Table = require('easy-table'),
15
- util = require('util');
14
+ Table = require('easy-table');
16
15
 
17
16
  exports = module.exports = {
18
17
  login,
@@ -29,83 +28,79 @@ exports = module.exports = {
29
28
 
30
29
  const NO_MANIFEST_FOUND_ERROR_STRING = 'No CloudronManifest.json found';
31
30
 
31
+ function requestError(response) {
32
+ if (response.statusCode === 401) return 'Invalid token. Use cloudron appstore login again.';
33
+
34
+ return `${response.statusCode} message: ${response.body.message || JSON.stringify(response.body)}`; // body is sometimes just a string like in 401
35
+ }
36
+
37
+ function createRequest(method, apiPath) {
38
+ let url = `${config.appStoreOrigin()}${apiPath}`;
39
+ if (url.includes('?')) url += '&'; else url += '?';
40
+ url += `accessToken=${config.appStoreToken()}`;
41
+ const request = superagent(method, url);
42
+ request.retry(3);
43
+ request.ok(() => true);
44
+ return request;
45
+ }
46
+
32
47
  function createUrl(api) {
33
48
  return config.appStoreOrigin() + api;
34
49
  }
35
50
 
36
51
  // the app argument allows us in the future to get by name or id
37
- function getAppstoreId(appstoreId, callback) {
38
- if (appstoreId) {
39
- var parts = appstoreId.split('@');
40
-
41
- return callback(null, parts[0], parts[1]);
42
- }
52
+ async function getAppstoreId(appstoreId) {
53
+ if (appstoreId) return appstoreId.split('@');
43
54
 
44
- var manifestFilePath = locateManifest();
45
-
46
- if (!manifestFilePath) return callback('No CloudronManifest.json found');
55
+ const manifestFilePath = locateManifest();
56
+ if (!manifestFilePath) throw new Error('No CloudronManifest.json found');
47
57
 
48
- var manifest = safe.JSON.parse(safe.fs.readFileSync(manifestFilePath));
49
- if (!manifest) callback(util.format('Unable to read manifest %s. Error: %s', manifestFilePath, safe.error));
58
+ const manifest = safe.JSON.parse(safe.fs.readFileSync(manifestFilePath));
59
+ if (!manifest) throw new Error(`Unable to read manifest ${manifestFilePath}. Error: ${safe.error.message}`);
50
60
 
51
- return callback(null, manifest.id, manifest.version);
61
+ return [manifest.id, manifest.version];
52
62
  }
53
63
 
54
- // takes a function returning a superagent request instance and will reauthenticate in case the token is invalid
55
- function superagentEnd(requestFactory, callback) {
56
- requestFactory().end(function (error, result) {
57
- if (error && !error.response) return callback(error);
58
- if (result.statusCode === 401) return authenticate({ error: true }, superagentEnd.bind(null, requestFactory, callback));
59
- if (result.statusCode === 403) return callback(new Error(result.type === 'application/javascript' ? JSON.stringify(result.body) : result.text));
60
- callback(error, result);
61
- });
62
- }
63
-
64
- function authenticate(options, callback) {
64
+ async function authenticate(options) {
65
65
  if (!options.hideBanner) {
66
66
  const webDomain = config.appStoreOrigin().replace('https://api.', '');
67
67
  console.log(`${webDomain} login` + ` (If you do not have one, sign up at https://${webDomain}/console.html#/register)`);
68
68
  }
69
69
 
70
- var email = options.email || readlineSync.question('Email: ', {});
71
- var password = options.password || readlineSync.question('Password: ', { noEchoBack: true });
70
+ const email = options.email || readlineSync.question('Email: ', {});
71
+ const password = options.password || readlineSync.question('Password: ', { noEchoBack: true });
72
72
 
73
73
  config.setAppStoreToken(null);
74
74
 
75
- superagent.post(createUrl('/api/v1/login')).auth(email, password).send({ totpToken: options.totpToken }).end(function (error, result) {
76
- if (error && !error.response) exit(error);
77
-
78
- if (result.statusCode === 401 && result.body.message.indexOf('TOTP') !== -1) {
79
- if (result.body.message === 'TOTP token missing') console.log('A 2FA TOTP Token is required for this account.');
80
-
81
- options.totpToken = readlineSync.question('2FA token: ', {});
82
- options.email = email;
83
- options.password = password;
84
- options.hideBanner = true;
75
+ const response = await superagent.post(createUrl('/api/v1/login')).auth(email, password).send({ totpToken: options.totpToken }).ok(() => true);
76
+ if (response.statusCode === 401 && response.body.message.indexOf('TOTP') !== -1) {
77
+ if (response.body.message === 'TOTP token missing') console.log('A 2FA TOTP Token is required for this account.');
85
78
 
86
- return authenticate(options, callback);
87
- }
79
+ options.totpToken = readlineSync.question('2FA token: ', {});
80
+ options.email = email;
81
+ options.password = password;
82
+ options.hideBanner = true;
88
83
 
89
- if (result.statusCode !== 200) {
90
- console.log('Login failed.');
84
+ return await authenticate(options); // try again with top set
85
+ }
91
86
 
92
- options.hideBanner = true;
93
- options.email = '';
94
- options.password = '';
87
+ if (response.statusCode !== 200) {
88
+ console.log('Login failed.');
95
89
 
96
- return authenticate(options, callback);
97
- }
90
+ options.hideBanner = true;
91
+ options.email = '';
92
+ options.password = '';
98
93
 
99
- config.setAppStoreToken(result.body.accessToken);
94
+ return await authenticate(options);
95
+ }
100
96
 
101
- console.log('Login successful.');
97
+ config.setAppStoreToken(response.body.accessToken);
102
98
 
103
- if (typeof callback === 'function') callback();
104
- });
99
+ console.log('Login successful.');
105
100
  }
106
101
 
107
- function login(options) {
108
- authenticate(options);
102
+ async function login(options) {
103
+ await authenticate(options);
109
104
  }
110
105
 
111
106
  function logout() {
@@ -113,93 +108,67 @@ function logout() {
113
108
  console.log('Done.');
114
109
  }
115
110
 
116
- function info(options) {
117
- getAppstoreId(options.appstoreId, function (error, id, version) {
118
- if (error) exit(error);
119
-
120
- superagentEnd(function () {
121
- return superagent.get(createUrl('/api/v1/developers/apps/' + id + '/versions/' + version)).query({ accessToken: config.appStoreToken() });
122
- }, function (error, result) {
123
- if (error && !error.response) exit(util.format('Failed to list apps: %s', error.message));
124
- if (result.statusCode !== 200) exit(util.format('Failed to list apps: %s message: %s', result.statusCode, result.text));
125
-
126
- var manifest = result.body.manifest;
127
- console.log('id: %s', manifest.id);
128
- console.log('title: %s', manifest.title);
129
- console.log('tagline: %s', manifest.tagline);
130
- console.log('description: %s', manifest.description);
131
- console.log('website: %s', manifest.website);
132
- console.log('contactEmail: %s', manifest.contactEmail);
133
- });
134
- });
135
- }
111
+ async function info(options) {
112
+ const [id, version] = await getAppstoreId(options.appstoreId);
136
113
 
137
- function listVersions(options) {
138
- getAppstoreId(options.appstoreId, function (error, id) {
139
- if (error) exit(error);
114
+ const response = await createRequest('GET', `/api/v1/developers/apps/${id}/versions/${version}`);
115
+ if (response.statusCode !== 200) throw new Error(`Failed to list domains: ${requestError(response)}`);
140
116
 
141
- superagentEnd(function () {
142
- return superagent.get(createUrl('/api/v1/developers/apps/' + id + '/versions')).query({ accessToken: config.appStoreToken() });
143
- }, function (error, result) {
144
- if (error && !error.response) exit(util.format('Failed to list versions: %s', error.message));
145
- if (result.statusCode === 404) exit('This app is not listed in appstore');
146
- if (result.statusCode !== 200) exit(util.format('Failed to list versions: %s message: %s', result.statusCode, result.text));
117
+ const manifest = response.body.manifest;
118
+ console.log('id: %s', manifest.id);
119
+ console.log('title: %s', manifest.title);
120
+ console.log('tagline: %s', manifest.tagline);
121
+ console.log('description: %s', manifest.description);
122
+ console.log('website: %s', manifest.website);
123
+ console.log('contactEmail: %s', manifest.contactEmail);
124
+ }
147
125
 
148
- if (result.body.versions.length === 0) return console.log('No versions found.');
126
+ async function listVersions(options) {
127
+ const [id] = await getAppstoreId(options.appstoreId);
149
128
 
150
- if (options.raw) return console.log(JSON.stringify(result.body.versions, null, 2));
129
+ const response = await createRequest('GET', `/api/v1/developers/apps/${id}/versions`);
130
+ if (response.statusCode !== 200) throw new Error(`Failed to list domains: ${requestError(response)}`);
151
131
 
152
- var versions = result.body.versions.reverse();
132
+ if (response.body.versions.length === 0) return console.log('No versions found.');
153
133
 
154
- // var manifest = versions[0].manifest;
155
- var t = new Table();
134
+ if (options.raw) return console.log(JSON.stringify(response.body.versions, null, 2));
156
135
 
157
- versions.forEach(function (version) {
158
- t.cell('Version', version.manifest.version);
159
- t.cell('Creation Date', version.creationDate);
160
- t.cell('Image', version.manifest.dockerImage);
161
- t.cell('Publish state', version.publishState);
162
- t.newRow();
163
- });
136
+ const versions = response.body.versions.reverse();
137
+ const t = new Table();
164
138
 
165
- console.log();
166
- console.log(t.toString());
167
- });
139
+ versions.forEach(function (version) {
140
+ t.cell('Version', version.manifest.version);
141
+ t.cell('Creation Date', version.creationDate);
142
+ t.cell('Image', version.manifest.dockerImage);
143
+ t.cell('Publish state', version.publishState);
144
+ t.newRow();
168
145
  });
146
+
147
+ console.log();
148
+ console.log(t.toString());
169
149
  }
170
150
 
171
- function addApp(manifest, baseDir, callback) {
172
- assert(typeof manifest === 'object');
173
- assert(typeof baseDir === 'string');
174
- assert(typeof callback === 'function');
175
-
176
- superagentEnd(function () {
177
- return superagent.post(createUrl('/api/v1/developers/apps'))
178
- .query({ accessToken: config.appStoreToken() })
179
- .send({ id: manifest.id });
180
- }, function (error, result) {
181
- if (error && !error.response) return exit(util.format('Failed to create app: %s', error.message));
182
- if (result.statusCode !== 201 && result.statusCode !== 409) {
183
- return exit(util.format('Failed to create app: %s message: %s', result.statusCode, result.text));
184
- }
185
-
186
- if (result.statusCode === 201) {
187
- console.log('New application added to the appstore with id %s.', manifest.id);
188
- }
189
-
190
- callback();
191
- });
151
+ async function addApp(manifest, baseDir) {
152
+ assert.strictEqual(typeof manifest, 'object');
153
+ assert.strictEqual(typeof baseDir, 'string');
154
+
155
+ const request = createRequest('POST', '/api/v1/developers/apps');
156
+ request.send({ id: manifest.id });
157
+ const response = await request;
158
+ if (response.statusCode === 409) return; // already exists
159
+ if (response.statusCode !== 201) return exit(`Failed to add app: ${requestError(response)}`);
192
160
  }
193
161
 
194
162
  function parseChangelog(file, version) {
195
- var changelog = '';
196
- var data = safe.fs.readFileSync(file, 'utf8');
163
+ let changelog = '';
164
+ const data = safe.fs.readFileSync(file, 'utf8');
197
165
  if (!data) return null;
198
- var lines = data.split('\n');
166
+ const lines = data.split('\n');
199
167
 
200
168
  version = version.replace(/-.*/, ''); // remove any prerelease
201
169
 
202
- for (var i = 0; i < lines.length; i++) {
170
+ let i;
171
+ for (i = 0; i < lines.length; i++) {
203
172
  if (lines[i] === '[' + version + ']') break;
204
173
  }
205
174
 
@@ -213,226 +182,177 @@ function parseChangelog(file, version) {
213
182
  return changelog;
214
183
  }
215
184
 
216
- function addVersion(manifest, baseDir, callback) {
185
+ async function addVersion(manifest, baseDir) {
217
186
  assert.strictEqual(typeof manifest, 'object');
218
187
  assert.strictEqual(typeof baseDir, 'string');
219
188
 
220
- var iconFilePath = null;
189
+ let iconFilePath = null;
221
190
  if (manifest.icon) {
222
- var iconFile = manifest.icon; // backward compat
191
+ let iconFile = manifest.icon; // backward compat
223
192
  if (iconFile.slice(0, 7) === 'file://') iconFile = iconFile.slice(7);
224
193
 
225
194
  iconFilePath = path.isAbsolute(iconFile) ? iconFile : path.join(baseDir, iconFile);
226
- if (!fs.existsSync(iconFilePath)) return callback(new Error('icon not found at ' + iconFilePath));
195
+ if (!fs.existsSync(iconFilePath)) throw new Error('icon not found at ' + iconFilePath);
227
196
  }
228
197
 
229
198
  if (manifest.description.slice(0, 7) === 'file://') {
230
- var descriptionFilePath = manifest.description.slice(7);
199
+ let descriptionFilePath = manifest.description.slice(7);
231
200
  descriptionFilePath = path.isAbsolute(descriptionFilePath) ? descriptionFilePath : path.join(baseDir, descriptionFilePath);
232
201
  manifest.description = safe.fs.readFileSync(descriptionFilePath, 'utf8');
233
- if (!manifest.description && safe.error) return callback(new Error('Could not read/parse description ' + safe.error.message));
234
- if (!manifest.description) return callback(new Error('Description cannot be empty'));
202
+ if (!manifest.description && safe.error) throw(new Error('Could not read/parse description ' + safe.error.message));
203
+ if (!manifest.description) throw new Error('Description cannot be empty');
235
204
  }
236
205
 
237
206
  if (manifest.postInstallMessage && manifest.postInstallMessage.slice(0, 7) === 'file://') {
238
- var postInstallFilePath = manifest.postInstallMessage.slice(7);
207
+ let postInstallFilePath = manifest.postInstallMessage.slice(7);
239
208
  postInstallFilePath = path.isAbsolute(postInstallFilePath) ? postInstallFilePath : path.join(baseDir, postInstallFilePath);
240
209
  manifest.postInstallMessage = safe.fs.readFileSync(postInstallFilePath, 'utf8');
241
- if (!manifest.postInstallMessage && safe.error) return callback(new Error('Could not read/parse postInstall ' + safe.error.message));
242
- if (!manifest.postInstallMessage) return callback(new Error('PostInstall file specified but it is empty'));
210
+ if (!manifest.postInstallMessage && safe.error) throw(new Error('Could not read/parse postInstall ' + safe.error.message));
211
+ if (!manifest.postInstallMessage) throw new Error('PostInstall file specified but it is empty');
243
212
  }
244
213
 
245
214
  if (manifest.changelog.slice(0, 7) === 'file://') {
246
- var changelogPath = manifest.changelog.slice(7);
215
+ let changelogPath = manifest.changelog.slice(7);
247
216
  changelogPath = path.isAbsolute(changelogPath) ? changelogPath : path.join(baseDir, changelogPath);
248
217
  manifest.changelog = parseChangelog(changelogPath, manifest.version);
249
- if (!manifest.changelog) return callback(new Error('Bad changelog format or missing changelog for this version'));
218
+ if (!manifest.changelog) throw new Error('Bad changelog format or missing changelog for this version');
250
219
  }
251
220
 
252
- superagentEnd(function () {
253
- var req = superagent.post(createUrl('/api/v1/developers/apps/' + manifest.id + '/versions'));
254
- req.query({ accessToken: config.appStoreToken() });
255
- if (iconFilePath) req.attach('icon', iconFilePath);
256
- req.attach('manifest', Buffer.from(JSON.stringify(manifest)), 'manifest');
257
- return req;
258
- }, function (error, result) {
259
- if (error && !error.response) return callback(new Error(util.format('Failed to publish version: %s', error.message)));
260
- if (result.statusCode === 409) return callback('This version already exists. Use --force to overwrite.');
261
- if (result.statusCode !== 204) return callback(new Error(util.format('Failed to publish version (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message : result.text)));
262
-
263
- callback();
264
- });
221
+ const request = createRequest('POST', `/api/v1/developers/apps/${manifest.id}/versions`);
222
+ if (iconFilePath) request.attach('icon', iconFilePath);
223
+ request.attach('manifest', Buffer.from(JSON.stringify(manifest)), 'manifest');
224
+ const response = await request;
225
+ if (response.statusCode === 409) throw new Error('This version already exists. Use --force to overwrite.');
226
+ if (response.statusCode !== 204) throw new Error(`Failed to publish version: ${requestError(response)}`);
265
227
  }
266
228
 
267
- function updateVersion(manifest, baseDir, callback) {
229
+ async function updateVersion(manifest, baseDir) {
268
230
  assert.strictEqual(typeof manifest, 'object');
269
231
  assert.strictEqual(typeof baseDir, 'string');
270
232
 
271
- var iconFilePath = null;
233
+ let iconFilePath = null;
272
234
  if (manifest.icon) {
273
- var iconFile = manifest.icon; // backward compat
235
+ let iconFile = manifest.icon; // backward compat
274
236
  if (iconFile.slice(0, 7) === 'file://') iconFile = iconFile.slice(7);
275
237
 
276
238
  iconFilePath = path.isAbsolute(iconFile) ? iconFile : path.join(baseDir, iconFile);
277
- if (!fs.existsSync(iconFilePath)) return callback(new Error('icon not found at ' + iconFilePath));
239
+ if (!fs.existsSync(iconFilePath)) throw new Error('icon not found at ' + iconFilePath);
278
240
  }
279
241
 
280
242
  if (manifest.description.slice(0, 7) === 'file://') {
281
- var descriptionFilePath = manifest.description.slice(7);
243
+ let descriptionFilePath = manifest.description.slice(7);
282
244
  descriptionFilePath = path.isAbsolute(descriptionFilePath) ? descriptionFilePath : path.join(baseDir, descriptionFilePath);
283
245
  manifest.description = safe.fs.readFileSync(descriptionFilePath, 'utf8');
284
- if (!manifest.description) return callback(new Error('Could not read description ' + safe.error.message));
246
+ if (!manifest.description) throw new Error('Could not read description ' + safe.error.message);
285
247
  }
286
248
 
287
249
  if (manifest.postInstallMessage && manifest.postInstallMessage.slice(0, 7) === 'file://') {
288
- var postInstallFilePath = manifest.postInstallMessage.slice(7);
250
+ let postInstallFilePath = manifest.postInstallMessage.slice(7);
289
251
  postInstallFilePath = path.isAbsolute(postInstallFilePath) ? postInstallFilePath : path.join(baseDir, postInstallFilePath);
290
252
  manifest.postInstallMessage = safe.fs.readFileSync(postInstallFilePath, 'utf8');
291
- if (!manifest.postInstallMessage) return callback(new Error('Could not read/parse postInstall ' + safe.error.message));
253
+ if (!manifest.postInstallMessage) throw new Error('Could not read/parse postInstall ' + safe.error.message);
292
254
  }
293
255
 
294
256
  if (manifest.changelog.slice(0, 7) === 'file://') {
295
- var changelogPath = manifest.changelog.slice(7);
257
+ let changelogPath = manifest.changelog.slice(7);
296
258
  changelogPath = path.isAbsolute(changelogPath) ? changelogPath : path.join(baseDir, changelogPath);
297
259
  manifest.changelog = parseChangelog(changelogPath, manifest.version);
298
- if (!manifest.changelog) return callback(new Error('Could not read changelog or missing version changes'));
260
+ if (!manifest.changelog) throw new Error('Could not read changelog or missing version changes');
299
261
  }
300
262
 
301
- superagentEnd(function () {
302
- var req = superagent.put(createUrl('/api/v1/developers/apps/' + manifest.id + '/versions/' + manifest.version));
303
- req.query({ accessToken: config.appStoreToken() });
304
- if (iconFilePath) req.attach('icon', iconFilePath);
305
- req.attach('manifest', Buffer.from(JSON.stringify(manifest)), 'manifest');
306
- return req;
307
- }, function (error, result) {
308
- if (error && !error.response) return callback(new Error(util.format('Failed to publish version: %s', error.message)));
309
- if (result.statusCode !== 204) {
310
- return callback(new Error(util.format('Failed to publish version (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message : result.text)));
311
- }
312
-
313
- callback();
314
- });
263
+ const request = createRequest('PUT', `/api/v1/developers/apps/${manifest.id}/versions/${manifest.version}`);
264
+ if (iconFilePath) request.attach('icon', iconFilePath);
265
+ request.attach('manifest', Buffer.from(JSON.stringify(manifest)), 'manifest');
266
+ const response = await request;
267
+ if (response.statusCode !== 204) throw new Error(`Failed to publish version: ${requestError(response)}`);
315
268
  }
316
269
 
317
- function delVersion(manifest, force) {
318
- assert(typeof manifest === 'object');
319
- assert(typeof force === 'boolean');
270
+ async function delVersion(manifest, force) {
271
+ assert.strictEqual(typeof manifest, 'object');
272
+ assert.strictEqual(typeof force, 'boolean');
320
273
 
321
274
  if (!force) {
322
- console.log('This will delete the version %s of app %s from the appstore!', manifest.version, manifest.id);
323
- var reallyDelete = readlineSync.question(util.format('Really do this? [y/N]: '), {});
275
+ console.log(`This will delete the version ${manifest.version} of app ${manifest.id} from the appstore!`);
276
+ const reallyDelete = readlineSync.question('Really do this? [y/N]: ', {});
324
277
  if (reallyDelete.toUpperCase() !== 'Y') exit();
325
278
  }
326
279
 
327
- superagentEnd(function () {
328
- return superagent.del(createUrl('/api/v1/developers/apps/' + manifest.id + '/versions/' + manifest.version)).query({ accessToken: config.appStoreToken() });
329
- }, function (error, result) {
330
- if (error && !error.response) return exit(util.format('Failed to unpublish version: %s', error.message));
331
- if (result.statusCode !== 204) exit(util.format('Failed to unpublish version (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message : result.text));
280
+ const response = await createRequest('DEL', `/api/v1/developers/apps/${manifest.id}/versions/${manifest.version}`);
281
+ if (response.statusCode !== 204) return exit(`Failed to unpublish version: ${requestError(response)}`);
332
282
 
333
- console.log('version unpublished.');
334
- });
283
+ console.log('version unpublished.');
335
284
  }
336
285
 
337
- function revokeVersion(appstoreId, version) {
286
+ async function revokeVersion(appstoreId, version) {
338
287
  assert.strictEqual(typeof appstoreId, 'string');
339
288
  assert.strictEqual(typeof version, 'string');
340
289
 
341
- superagentEnd(function () {
342
- return superagent.post(createUrl('/api/v1/developers/apps/' + appstoreId + '/versions/' + version + '/revoke'))
343
- .query({ accessToken: config.appStoreToken() })
344
- .send({ });
345
- }, function (error, result) {
346
- if (error && !error.response) return exit(util.format('Failed to revoke version: %s', error.message));
347
- if (result.statusCode !== 200) exit(util.format('Failed to revoke version (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message : result.text));
290
+ const response = await createRequest('POST', `/api/v1/developers/apps/${appstoreId}/versions/${version}/revoke`);
291
+ if (response.statusCode !== 200) return exit(`Failed to revoke version: ${requestError(response)}`);
348
292
 
349
- console.log('version revoked.');
350
- });
293
+ console.log('version revoked.');
351
294
  }
352
295
 
353
- function approveVersion(appstoreId, version) {
296
+ async function approveVersion(appstoreId, version) {
354
297
  assert.strictEqual(typeof appstoreId, 'string');
355
298
  assert.strictEqual(typeof version, 'string');
356
299
 
357
- superagentEnd(function () {
358
- return superagent.post(createUrl('/api/v1/developers/apps/' + appstoreId + '/versions/' + version + '/approve'))
359
- .query({ accessToken: config.appStoreToken() })
360
- .send({ });
361
- }, function (error, result) {
362
- if (error && !error.response) return exit(util.format('Failed to approve version: %s', error.message));
363
- if (result.statusCode !== 200) exit(util.format('Failed to approve version (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message : result.text));
364
-
365
- console.log('Approved.');
366
- console.log('');
367
-
368
- superagentEnd(function () {
369
- return superagent.get(createUrl('/api/v1/developers/apps/' + appstoreId + '/versions/' + version)).query({ accessToken: config.appStoreToken() });
370
- }, function (error, result) {
371
- if (error && !error.response) exit(util.format('Failed to list apps: %s', error.message));
372
- if (result.statusCode !== 200) exit(util.format('Failed to list apps: %s message: %s', result.statusCode, result.text));
373
-
374
- console.log('Changelog for forum update: ' + result.body.manifest.forumUrl);
375
- console.log('');
376
- console.log('[' + version + ']');
377
- console.log(result.body.manifest.changelog);
378
- console.log('');
379
- });
380
- });
381
- }
300
+ const response = await createRequest('POST', `/api/v1/developers/apps/${appstoreId}/versions/${version}/approve`);
301
+ if (response.statusCode !== 200) return exit(`Failed to approve version: ${requestError(response)}`);
302
+
303
+ console.log('Approved.');
304
+ console.log('');
382
305
 
306
+ const response2 = await createRequest('GET', `/api/v1/developers/apps/${appstoreId}/versions/${version}`);
307
+ if (response2.statusCode !== 200) return exit(`Failed to list apps: ${requestError(response)}`);
383
308
 
384
- function delApp(appId, force) {
385
- assert(typeof appId === 'string');
386
- assert(typeof force === 'boolean');
309
+ console.log('Changelog for forum update: ' + response2.body.manifest.forumUrl);
310
+ console.log('');
311
+ console.log('[' + version + ']');
312
+ console.log(response2.body.manifest.changelog);
313
+ console.log('');
314
+ }
315
+
316
+ async function delApp(appId, force) {
317
+ assert.strictEqual(typeof appId, 'string');
318
+ assert.strictEqual(typeof force, 'boolean');
387
319
 
388
320
  if (!force) {
389
321
  console.log('This will delete app %s from the appstore!', appId);
390
- var reallyDelete = readlineSync.question(util.format('Really do this? [y/N]: '), {});
391
- if (reallyDelete.toUpperCase() !== 'Y') exit();
322
+ const reallyDelete = readlineSync.question('Really do this? [y/N]: ', {});
323
+ if (reallyDelete.toUpperCase() !== 'Y') return exit();
392
324
  }
393
325
 
394
- superagentEnd(function () {
395
- return superagent.del(createUrl('/api/v1/developers/apps/' + appId)).query({ accessToken: config.appStoreToken() });
396
- }, function (error, result) {
397
- if (error && !error.response) return exit(util.format('Failed to unpublish app: %s', error.message));
398
- if (result.statusCode !== 204) exit(util.format('Failed to unpublish app (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message : result.text));
326
+ const response = await createRequest('DEL', `/api/v1/developers/apps/${appId}`);
327
+ if (response.statusCode !== 204) exit(`Failed to unpublish app : ${requestError(response)}`);
399
328
 
400
- console.log('App unpublished.');
401
- });
329
+ console.log('App unpublished.');
402
330
  }
403
331
 
404
- function submitAppForReview(manifest, callback) {
405
- assert(typeof manifest === 'object');
406
- assert(typeof callback === 'function');
407
-
408
- superagentEnd(function () {
409
- return superagent.post(createUrl('/api/v1/developers/apps/' + manifest.id + '/versions/' + manifest.version + '/submit'))
410
- .query({ accessToken: config.appStoreToken() })
411
- .send({ });
412
- }, function (error, result) {
413
- if (error && !error.response) exit(util.format('Failed to submit app for review: %s', error.message));
414
- if (result.statusCode === 404) {
415
- console.log('No version %s found. Please use %s first.', manifest.version, 'cloudron appstore upload');
416
- exit('Failed to submit app for review.');
417
- }
418
- if (result.statusCode !== 200) return exit(util.format('Failed to submit app (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message : result.text));
419
-
420
- console.log('App submitted for review.');
421
- console.log('You will receive an email when approved.');
422
-
423
- callback();
424
- });
332
+ async function submitAppForReview(manifest) {
333
+ assert.strictEqual(typeof manifest, 'object');
334
+
335
+ const response = await createRequest('POST', `/api/v1/developers/apps/${manifest.id}/versions/${manifest.version}/submit`);
336
+ if (response.statusCode === 404) {
337
+ console.log(`No version ${manifest.version} found. Please use 'cloudron apsptore upload' first`);
338
+ return exit('Failed to submit app for review.');
339
+ }
340
+
341
+ if (response.statusCode !== 200) return exit(`Failed to submit app: ${requestError(response)}`);
342
+
343
+ console.log('App submitted for review.');
344
+ console.log('You will receive an email when approved.');
425
345
  }
426
346
 
427
- function upload(options) {
347
+ async function upload(options) {
428
348
  // try to find the manifest of this project
429
- var manifestFilePath = locateManifest();
349
+ const manifestFilePath = locateManifest();
430
350
  if (!manifestFilePath) return exit(NO_MANIFEST_FOUND_ERROR_STRING);
431
351
 
432
- var result = manifestFormat.parseFile(manifestFilePath);
352
+ const result = manifestFormat.parseFile(manifestFilePath);
433
353
  if (result.error) return exit(result.error.message);
434
354
 
435
- let manifest = result.manifest;
355
+ const manifest = result.manifest;
436
356
 
437
357
  const sourceDir = path.dirname(manifestFilePath);
438
358
  const appConfig = config.getAppConfig(sourceDir);
@@ -446,19 +366,16 @@ function upload(options) {
446
366
  // ensure we remove the docker hub handle
447
367
  if (manifest.dockerImage.indexOf('docker.io/') === 0) manifest.dockerImage = manifest.dockerImage.slice('docker.io/'.length);
448
368
 
449
- var error = manifestFormat.checkAppstoreRequirements(manifest);
369
+ const error = manifestFormat.checkAppstoreRequirements(manifest);
450
370
  if (error) return exit(error);
451
371
 
452
372
  // ensure the app is known on the appstore side
453
- addApp(manifest, path.dirname(manifestFilePath), function () {
454
- console.log(`Uploading ${manifest.id}@${manifest.version} (dockerImage: ${manifest.dockerImage}) for testing`);
455
-
456
- var func = options.force ? updateVersion : addVersion;
373
+ const baseDir = path.dirname(manifestFilePath);
374
+ await addApp(manifest, baseDir);
375
+ console.log(`Uploading ${manifest.id}@${manifest.version} (dockerImage: ${manifest.dockerImage}) for testing`);
457
376
 
458
- func(manifest, path.dirname(manifestFilePath), function (error) {
459
- if (error) return exit(error);
460
- });
461
- });
377
+ const [error2] = await safe(options.force ? updateVersion(manifest, baseDir) : addVersion(manifest, baseDir));
378
+ if (error2) return exit(error2);
462
379
  }
463
380
 
464
381
  function submit() {
@@ -474,69 +391,55 @@ function submit() {
474
391
  submitAppForReview(manifest, exit);
475
392
  }
476
393
 
477
- function unpublish(options) {
478
- getAppstoreId(options.appstoreId, function (error, id, version) {
479
- if (error) exit(error);
394
+ async function unpublish(options) {
395
+ const [id, version] = await getAppstoreId(options.appstoreId);
480
396
 
481
- if (!version) {
482
- console.log('Unpublishing ' + options.app);
483
- delApp(options.app, !!options.force);
484
- return;
485
- }
397
+ if (!version) {
398
+ console.log(`Unpublishing ${options.appstoreId}`);
399
+ await delApp(options.app, !!options.force);
400
+ return;
401
+ }
486
402
 
487
- console.log('Unpublishing ' + id + '@' + version);
488
- delVersion(id, !!options.force);
489
- });
403
+ console.log(`Unpublishing ${id}@${version}`);
404
+ await delVersion(id, !!options.force);
490
405
  }
491
406
 
492
- function revoke(options) {
493
- getAppstoreId(options.appstoreId, function (error, id, version) {
494
- if (error) return exit(error);
495
-
496
- if (!version) return exit('--appstore-id must be of the format id@version');
407
+ async function revoke(options) {
408
+ const [id, version] = await getAppstoreId(options.appstoreId);
409
+ if (!version) return exit('--appstore-id must be of the format id@version');
497
410
 
498
- console.log('Revoking ' + id + '@' + version);
499
- revokeVersion(id, version);
500
- });
411
+ console.log(`Revoking ${id}@${version}`);
412
+ await revokeVersion(id, version);
501
413
  }
502
414
 
503
- function approve(options) {
504
- getAppstoreId(options.appstoreId, function (error, id, version) {
505
- if (error) return exit(error);
415
+ async function approve(options) {
416
+ const [id, version] = await getAppstoreId(options.appstoreId);
506
417
 
507
- if (!version) return exit('--appstore-id must be of the format id@version');
418
+ if (!version) return exit('--appstore-id must be of the format id@version');
508
419
 
509
- console.log('Approving ' + id + '@' + version);
420
+ console.log(`Approving ${id}@${version}`);
510
421
 
511
- approveVersion(id, version);
512
- });
422
+ await approveVersion(id, version);
513
423
  }
514
424
 
515
425
  // TODO currently no pagination, only needed once we have users with more than 100 apps
516
- function listPublishedApps(options) {
517
- superagentEnd(function () {
518
- return superagent.get(createUrl('/api/v1/developers/apps?per_page=100'))
519
- .query({ accessToken: config.appStoreToken() })
520
- .send({ });
521
- }, function (error, result) {
522
- if (error && !error.response) return exit(util.format('Failed to get list of published apps: %s', error.message));
523
- if (result.statusCode !== 200) return exit(util.format('Failed to get list of published apps (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message : result.text));
524
-
525
- if (result.body.apps.length === 0) return console.log('No apps published.');
526
-
527
- var t = new Table();
528
-
529
- result.body.apps.forEach(function (app) {
530
- t.cell('Id', app.id);
531
- t.cell('Title', app.manifest.title);
532
- t.cell('Latest Version', app.manifest.version);
533
- t.cell('Publish State', app.publishState);
534
- t.cell('Creation Date', new Date(app.creationDate));
535
- if (options.image) t.cell('Image', app.manifest.dockerImage);
536
- t.newRow();
537
- });
538
-
539
- console.log();
540
- console.log(t.toString());
426
+ async function listPublishedApps(options) {
427
+ const response = await createRequest('GET', '/api/v1/developers/apps?per_page=100');
428
+ if (response.statusCode !== 200) return exit(`Failed to get list of published apps: ${requestError(response)}`);
429
+ if (response.body.apps.length === 0) return console.log('No apps published.');
430
+
431
+ const t = new Table();
432
+
433
+ response.body.apps.forEach(function (app) {
434
+ t.cell('Id', app.id);
435
+ t.cell('Title', app.manifest.title);
436
+ t.cell('Latest Version', app.manifest.version);
437
+ t.cell('Publish State', app.publishState);
438
+ t.cell('Creation Date', new Date(app.creationDate));
439
+ if (options.image) t.cell('Image', app.manifest.dockerImage);
440
+ t.newRow();
541
441
  });
442
+
443
+ console.log();
444
+ console.log(t.toString());
542
445
  }
@@ -323,7 +323,7 @@ function build(options) {
323
323
  url = readlineSync.question('Enter build service URL: ', { });
324
324
  }
325
325
 
326
- if (!url.startsWith('https://')) url = `https://${url}`;
326
+ if (url.indexOf('://') === -1) url = `https://${url}`;
327
327
  buildService.url = url;
328
328
  }
329
329