cloudron 5.12.0 → 5.13.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.
@@ -54,6 +54,10 @@ program.command('upload')
54
54
  .option('-f, --force', 'Update existing version')
55
55
  .action(appstoreActions.upload);
56
56
 
57
+ program.command('verify-manifest')
58
+ .description('Verify if manifest is ready to be upload/published')
59
+ .action(appstoreActions.verifyManifest);
60
+
57
61
  program.command('versions')
58
62
  .alias('list')
59
63
  .description('List published versions')
@@ -0,0 +1,21 @@
1
+ const js = require('@eslint/js');
2
+ const globals = require('globals');
3
+
4
+ module.exports = [
5
+ js.configs.recommended,
6
+ {
7
+ files: ["**/*.js"],
8
+ languageOptions: {
9
+ globals: {
10
+ ...globals.node,
11
+ },
12
+ ecmaVersion: 13,
13
+ sourceType: "commonjs"
14
+ },
15
+ rules: {
16
+ semi: "error",
17
+ "prefer-const": "error"
18
+ }
19
+ }
20
+ ];
21
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudron",
3
- "version": "5.12.0",
3
+ "version": "5.13.0",
4
4
  "license": "MIT",
5
5
  "description": "Cloudron Commandline Tool",
6
6
  "main": "main.js",
@@ -17,29 +17,25 @@
17
17
  },
18
18
  "author": "Cloudron Developers <support@cloudron.io>",
19
19
  "dependencies": {
20
- "async": "^3.2.6",
21
20
  "cloudron-manifestformat": "^5.26.2",
22
- "commander": "^12.1.0",
23
- "debug": "^4.3.7",
21
+ "commander": "^13.1.0",
22
+ "debug": "^4.4.0",
24
23
  "easy-table": "^1.2.0",
25
24
  "ejs": "^3.1.10",
26
- "eventsource": "^2.0.2",
25
+ "eventsource": "^3.0.5",
27
26
  "micromatch": "^4.0.8",
28
27
  "open": "^10.1.0",
29
- "progress": "^2.0.3",
30
- "progress-stream": "^2.0.0",
31
- "readline-sync": "^1.4.10",
32
- "safetydance": "^2.4.0",
33
- "split2": "^4.2.0",
28
+ "safetydance": "^2.5.0",
34
29
  "superagent": "^10.1.1",
35
- "tar-fs": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.12.0.tgz",
36
- "underscore": "^1.13.7"
30
+ "tar-fs": "^3.0.8"
37
31
  },
38
32
  "engines": {
39
33
  "node": ">= 18.x.x"
40
34
  },
41
35
  "devDependencies": {
36
+ "@eslint/js": "^9.20.0",
37
+ "eslint": "^9.20.1",
42
38
  "expect.js": "^0.3.1",
43
- "mocha": "^10.8.2"
39
+ "mocha": "^11.1.0"
44
40
  }
45
41
  }
package/src/actions.js CHANGED
@@ -4,25 +4,22 @@ const assert = require('assert'),
4
4
  config = require('./config.js'),
5
5
  ejs = require('ejs'),
6
6
  { exit, locateManifest } = require('./helper.js'),
7
- EventSource = require('eventsource'),
7
+ { EventSource } = require('eventsource'),
8
8
  fs = require('fs'),
9
9
  https = require('https'),
10
+ LineStream = require('./line-stream.js'),
10
11
  manifestFormat = require('cloudron-manifestformat'),
11
12
  os = require('os'),
12
13
  path = require('path'),
13
- ProgressBar = require('progress'),
14
- ProgressStream = require('progress-stream'),
15
- readlineSync = require('readline-sync'),
14
+ readline = require('./readline.js'),
16
15
  safe = require('safetydance'),
17
16
  spawn = require('child_process').spawn,
18
17
  semver = require('semver'),
19
- split = require('split2'),
20
18
  superagent = require('superagent'),
21
19
  Table = require('easy-table'),
22
20
  tar = require('tar-fs'),
23
21
  timers = require('timers/promises'),
24
- zlib = require('zlib'),
25
- _ = require('underscore');
22
+ zlib = require('zlib');
26
23
 
27
24
  exports = module.exports = {
28
25
  list,
@@ -163,7 +160,7 @@ async function selectAppWithRepository(repository, options) {
163
160
 
164
161
  let index = -1;
165
162
  while (true) {
166
- index = parseInt(readlineSync.question('Choose app [0-' + (matchingApps.length-1) + ']: ', {}), 10);
163
+ index = parseInt(await readline.question('Choose app [0-' + (matchingApps.length-1) + ']: ', {}), 10);
167
164
  if (isNaN(index) || index < 0 || index > matchingApps.length-1) console.log('Invalid selection');
168
165
  else break;
169
166
  }
@@ -334,7 +331,7 @@ async function authenticate(adminFqdn, username, password, options) {
334
331
  let totpToken;
335
332
 
336
333
  const { rejectUnauthorized, askForTotpToken } = options;
337
- if (askForTotpToken) totpToken = readlineSync.question('2FA Token: ', {});
334
+ if (askForTotpToken) totpToken = await readline.question('2FA Token: ', {});
338
335
 
339
336
  const request = superagent.post(`https://${adminFqdn}/api/v1/auth/login`)
340
337
  .timeout(60000)
@@ -360,7 +357,7 @@ async function authenticate(adminFqdn, username, password, options) {
360
357
  }
361
358
 
362
359
  async function login(adminFqdn, localOptions, cmd) {
363
- if (!adminFqdn) adminFqdn = readlineSync.question('Cloudron Domain (e.g. my.example.com): ', {});
360
+ if (!adminFqdn) adminFqdn = await readline.question('Cloudron Domain (e.g. my.example.com): ', {});
364
361
  if (!adminFqdn) return exit('');
365
362
 
366
363
  if (adminFqdn.indexOf('https://') === 0) adminFqdn = adminFqdn.slice('https://'.length);
@@ -389,8 +386,8 @@ async function login(adminFqdn, localOptions, cmd) {
389
386
  }
390
387
 
391
388
  if (!token) {
392
- const username = options.username || readlineSync.question('Username: ', {});
393
- const password = options.password || readlineSync.question('Password: ', { noEchoBack: true });
389
+ const username = options.username || await readline.question('Username: ', {});
390
+ const password = options.password || await readline.question('Password: ', { noEchoBack: true });
394
391
 
395
392
  const [error, result] = await safe(authenticate(adminFqdn, username, password, { rejectUnauthorized, askForTotpToken: false }));
396
393
  if (error) return exit(`Failed to login: ${error.message}`);
@@ -458,7 +455,7 @@ async function list(localOptions, cmd) {
458
455
  t.cell('Id', detailedApp.id);
459
456
  t.cell('Location', detailedApp.fqdn);
460
457
  t.cell('Manifest Id', (detailedApp.manifest.id || 'customapp') + '@' + detailedApp.manifest.version);
461
- var prettyState;
458
+ let prettyState;
462
459
  if (detailedApp.installationState === 'installed') {
463
460
  prettyState = (detailedApp.debugMode ? 'debug' : detailedApp.runState);
464
461
  } else if (detailedApp.installationState === 'error') {
@@ -481,18 +478,19 @@ async function querySecondaryDomains(app, manifest, options) {
481
478
 
482
479
  for (const env in manifest.httpPorts) {
483
480
  const defaultDomain = (app && app.secondaryDomains && app.secondaryDomains[env]) ? app.secondaryDomains[env] : (manifest.httpPorts[env].defaultValue || '');
484
- const input = readlineSync.question(`${manifest.httpPorts[env].description} (default: "${defaultDomain}"): `, {});
481
+ const input = await readline.question(`${manifest.httpPorts[env].description} (default: "${defaultDomain}"): `, {});
485
482
  secondaryDomains[env] = await selectDomain(input, options);
486
483
  }
487
484
  return secondaryDomains;
488
485
  }
489
486
 
490
- function queryPortBindings(app, manifest) {
487
+ async function queryPortBindings(app, manifest) {
491
488
  const portBindings = {};
492
- const allPorts = _.extend({}, manifest.tcpPorts, manifest.udpPorts);
489
+ const allPorts = Object.assign({}, manifest.tcpPorts, manifest.udpPorts);
490
+
493
491
  for (const env in allPorts) {
494
492
  const defaultPort = (app && app.portBindings && app.portBindings[env]) ? app.portBindings[env] : (allPorts[env].defaultValue || '');
495
- const port = readlineSync.question(allPorts[env].description + ' (default ' + env + '=' + defaultPort + '. "x" to disable): ', {});
493
+ const port = await readline.question(allPorts[env].description + ' (default ' + env + '=' + defaultPort + '. "x" to disable): ', {});
496
494
  if (port === '') {
497
495
  portBindings[env] = defaultPort;
498
496
  } else if (isNaN(parseInt(port, 10))) {
@@ -559,7 +557,7 @@ async function install(localOptions, cmd) {
559
557
  manifest.dockerImage = image;
560
558
  }
561
559
 
562
- const location = options.location || readlineSync.question('Location: ', {});
560
+ const location = options.location || await readline.question('Location: ', {});
563
561
  if (!location) return exit('');
564
562
 
565
563
  const domainObject = await selectDomain(location, options);
@@ -604,10 +602,10 @@ async function install(localOptions, cmd) {
604
602
  ports[tmp[0]] = parseInt(tmp[1], 10);
605
603
  });
606
604
  } else {
607
- ports = queryPortBindings(null /* existing app */, manifest);
605
+ ports = await queryPortBindings(null /* existing app */, manifest);
608
606
  }
609
607
  } else { // just put in defaults
610
- const allPorts = _.extend({}, manifest.tcpPorts, manifest.udpPorts);
608
+ const allPorts = Object.assign({}, manifest.tcpPorts, manifest.udpPorts);
611
609
  for (const portName in allPorts) {
612
610
  ports[portName] = allPorts[portName].defaultValue;
613
611
  }
@@ -669,7 +667,7 @@ async function setLocation(localOptions, cmd) {
669
667
  const app = await getApp(options);
670
668
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
671
669
 
672
- if (!options.location) options.location = readlineSync.question(`Enter new location (default: ${app.fqdn}): `, { });
670
+ if (!options.location) options.location = await readline.question(`Enter new location (default: ${app.fqdn}): `, { });
673
671
  const location = options.location || app.subdomain;
674
672
 
675
673
  const domainObject = await selectDomain(location, options);
@@ -734,7 +732,7 @@ async function setLocation(localOptions, cmd) {
734
732
  ports[tmp[0]] = parseInt(tmp[1], 10);
735
733
  });
736
734
  } else {
737
- ports = queryPortBindings(app, app.manifest);
735
+ ports = await queryPortBindings(app, app.manifest);
738
736
  }
739
737
 
740
738
  for (const port in ports) {
@@ -788,10 +786,10 @@ async function update(localOptions, cmd) {
788
786
 
789
787
  if (app.error && (app.error.installationState === 'pending_install')) { // install had failed. call repair to re-install
790
788
  apiPath = `/api/v1/apps/${app.id}/repair`;
791
- data = _.extend(data, { manifest });
789
+ data = Object.assign(data, { manifest });
792
790
  } else {
793
791
  apiPath = `/api/v1/apps/${app.id}/update`;
794
- data = _.extend(data, {
792
+ data = Object.assign(data, {
795
793
  appStoreId: options.appstoreId || '', // note case change
796
794
  manifest: manifest,
797
795
  skipBackup: !options.backup,
@@ -954,13 +952,13 @@ async function logs(localOptions, cmd) {
954
952
 
955
953
  if (tail) {
956
954
  const url = `${apiPath}?access_token=${token}&lines=10&format=json`;
957
- var es = new EventSource(url, { rejectUnauthorized }); // not sure why this is needed
955
+ const es = new EventSource(url, { rejectUnauthorized }); // not sure why this is needed
958
956
 
959
- es.on('message', function (e) { // e { type, data, lastEventId }. lastEventId is the timestamp
957
+ es.addEventListener('message', function (e) { // e { type, data, lastEventId }. lastEventId is the timestamp
960
958
  logPrinter(JSON.parse(e.data));
961
959
  });
962
960
 
963
- es.on('error', function (error) {
961
+ es.addEventListener('error', function (error) {
964
962
  if (error.status === 401) return exit('Please login first');
965
963
  if (error.status === 412) exit('Logs currently not available.');
966
964
  exit(error);
@@ -974,13 +972,13 @@ async function logs(localOptions, cmd) {
974
972
  });
975
973
  req.on('error', (error) => exit(`Pipe error: ${error.message}`));
976
974
 
977
- const jsonStream = split((line) => JSON.parse(line));
978
- jsonStream
979
- .on('data', logPrinter)
975
+ const lineStream = new LineStream();
976
+ lineStream
977
+ .on('line', (line) => { logPrinter(JSON.parse(line)); } )
980
978
  .on('error', (error) => exit(`JSON parse error: ${error.message}`))
981
979
  .on('end', process.exit);
982
980
 
983
- req.pipe(jsonStream);
981
+ req.pipe(lineStream);
984
982
  }
985
983
  }
986
984
 
@@ -1100,7 +1098,7 @@ async function backupList(localOptions, cmd) {
1100
1098
  return backup;
1101
1099
  }).sort(function (a, b) { return b.creationTime - a.creationTime; });
1102
1100
 
1103
- var t = new Table();
1101
+ const t = new Table();
1104
1102
 
1105
1103
  response.body.backups.forEach(function (backup) {
1106
1104
  t.cell('Id', backup.id);
@@ -1242,9 +1240,9 @@ async function clone(localOptions, cmd) {
1242
1240
 
1243
1241
  if (!options.backup) return exit('Use --backup to specify the backup id');
1244
1242
 
1245
- const location = options.location || readlineSync.question('Cloned app location: ', {});
1243
+ const location = options.location || await readline.question('Cloned app location: ', {});
1246
1244
  const secondaryDomains = await querySecondaryDomains(app, app.manifest, options);
1247
- const ports = queryPortBindings(app, app.manifest);
1245
+ const ports = await queryPortBindings(app, app.manifest);
1248
1246
  const backupId = options.backup;
1249
1247
 
1250
1248
  const domainObject = await selectDomain(location, options);
@@ -1417,19 +1415,7 @@ function push(localDir, remote, localOptions, cmd) {
1417
1415
  if (local === '-') {
1418
1416
  localOptions._stdin = process.stdin;
1419
1417
  } else if (stat) {
1420
- const progress = new ProgressStream({ length: stat.size, time: 1000 });
1421
-
1422
- localOptions._stdin = progress;
1423
- fs.createReadStream(local).pipe(progress);
1424
-
1425
- const bar = new ProgressBar('Uploading [:bar] :percent: :etas', {
1426
- complete: '=',
1427
- incomplete: ' ',
1428
- width: 100,
1429
- total: stat.size
1430
- });
1431
-
1432
- progress.on('progress', function (p) { bar.update(p.percentage / 100); /* bar.tick(p.transferred - bar.curr); */ });
1418
+ localOptions._stdin = fs.createReadStream(local);
1433
1419
  } else {
1434
1420
  exit('local file ' + local + ' does not exist');
1435
1421
  }
@@ -9,9 +9,8 @@ const assert = require('assert'),
9
9
  { exit, locateManifest } = require('./helper.js'),
10
10
  manifestFormat = require('cloudron-manifestformat'),
11
11
  path = require('path'),
12
- readlineSync = require('readline-sync'),
12
+ readline = require('./readline.js'),
13
13
  safe = require('safetydance'),
14
- semver = require('semver'),
15
14
  superagent = require('superagent'),
16
15
  Table = require('easy-table');
17
16
 
@@ -24,6 +23,7 @@ exports = module.exports = {
24
23
  upload,
25
24
  revoke,
26
25
  approve,
26
+ verifyManifest,
27
27
 
28
28
  notify
29
29
  };
@@ -71,8 +71,8 @@ async function authenticate(options) { // maybe we can use options.token to vali
71
71
  console.log(`${webDomain} login` + ` (If you do not have one, sign up at https://${webDomain}/console.html#/register)`);
72
72
  }
73
73
 
74
- const email = options.email || readlineSync.question('Email: ', {});
75
- const password = options.password || readlineSync.question('Password: ', { noEchoBack: true });
74
+ const email = options.email || await readline.question('Email: ', {});
75
+ const password = options.password || await readline.question('Password: ', { noEchoBack: true });
76
76
 
77
77
  config.setAppStoreToken(null);
78
78
 
@@ -80,7 +80,7 @@ async function authenticate(options) { // maybe we can use options.token to vali
80
80
  if (response.statusCode === 401 && response.body.message.indexOf('TOTP') !== -1) {
81
81
  if (response.body.message === 'TOTP token missing') console.log('A 2FA TOTP Token is required for this account.');
82
82
 
83
- options.totpToken = readlineSync.question('2FA token: ', {});
83
+ options.totpToken = await readline.question('2FA token: ', {});
84
84
  options.email = email;
85
85
  options.password = password;
86
86
  options.hideBanner = true;
@@ -265,6 +265,36 @@ async function updateVersion(manifest, baseDir, options) {
265
265
  if (response.statusCode !== 204) throw new Error(`Failed to publish version: ${requestError(response)}`);
266
266
  }
267
267
 
268
+ async function verifyManifest(localOptions, cmd) {
269
+ const options = cmd.optsWithGlobals();
270
+ // try to find the manifest of this project
271
+ const manifestFilePath = locateManifest();
272
+ if (!manifestFilePath) return exit(NO_MANIFEST_FOUND_ERROR_STRING);
273
+
274
+ const result = manifestFormat.parseFile(manifestFilePath);
275
+ if (result.error) return exit(result.error.message);
276
+
277
+ const manifest = result.manifest;
278
+
279
+ const sourceDir = path.dirname(manifestFilePath);
280
+ const appConfig = config.getAppConfig(sourceDir);
281
+
282
+ // image can be passed in options for buildbot
283
+ if (options.image) {
284
+ manifest.dockerImage = options.image;
285
+ } else {
286
+ manifest.dockerImage = appConfig.dockerImage;
287
+ }
288
+
289
+ if (!manifest.dockerImage) exit('No docker image found, run `cloudron build` first');
290
+
291
+ // ensure we remove the docker hub handle
292
+ if (manifest.dockerImage.indexOf('docker.io/') === 0) manifest.dockerImage = manifest.dockerImage.slice('docker.io/'.length);
293
+
294
+ const error = manifestFormat.checkAppstoreRequirements(manifest);
295
+ if (error) return exit(error);
296
+ }
297
+
268
298
  async function upload(localOptions, cmd) {
269
299
  const options = cmd.optsWithGlobals();
270
300
  // try to find the manifest of this project
@@ -384,7 +414,7 @@ async function approve(localOptions, cmd) {
384
414
  if (safe.error) return exit(`Failed to get head of ${defaultBranch}: ${safe.error.message}`);
385
415
  latestTagSha = latestTagSha.trim();
386
416
 
387
- if (defaultBranchSha !== latestTagSha) return exit(`Latest tag ${latestTag} does not match HEAD of ${defaultBranch}`);
417
+ if (defaultBranchSha !== latestTagSha) console.warn(`Latest tag ${latestTag} does not match HEAD of ${defaultBranch}`);
388
418
  }
389
419
 
390
420
  const response = await createRequest('POST', `/api/v1/developers/apps/${appstoreId}/versions/${version}/approve`, options);
@@ -11,7 +11,6 @@ exports = module.exports = {
11
11
  };
12
12
 
13
13
  const assert = require('assert'),
14
- async = require('async'),
15
14
  crypto = require('crypto'),
16
15
  debug = require('debug')('cloudron-backup'),
17
16
  fs = require('fs'),
@@ -268,15 +267,16 @@ async function decryptDir(inDir, outDir, options) {
268
267
  const outDirAbs = path.resolve(process.cwd(), outDir);
269
268
 
270
269
  const tbd = [ '' ]; // only has paths relative to inDirAbs
271
- async.whilst((done) => done(null, tbd.length !== 0), function iteratee(whilstCallback) {
270
+ while (true) {
272
271
  const cur = tbd.pop();
273
272
  const entries = fs.readdirSync(path.join(inDirAbs, cur), { withFileTypes: true });
274
- async.eachSeries(entries, async function (entry) {
273
+
274
+ for (const entry of entries) {
275
275
  if (entry.isDirectory()) {
276
276
  tbd.push(path.join(cur, entry.name));
277
- return;
277
+ continue;
278
278
  } else if (!entry.isFile()) {
279
- return;
279
+ continue;
280
280
  }
281
281
 
282
282
  const encryptedFilePath = path.join(cur, entry.name);
@@ -298,8 +298,10 @@ async function decryptDir(inDir, outDir, options) {
298
298
  safe.fs.rmSync(outfile);
299
299
  throw new Error(`Could not decrypt ${infile}: ${decryptError.message}`);
300
300
  }
301
- }, whilstCallback);
302
- }, exit);
301
+ }
302
+
303
+ if (tbd.length === 0) break;
304
+ }
303
305
  }
304
306
 
305
307
  async function decryptFilename(filePath, options) {
@@ -11,14 +11,14 @@ exports = module.exports = {
11
11
  const assert = require('assert'),
12
12
  config = require('./config.js'),
13
13
  crypto = require('crypto'),
14
- EventSource = require('eventsource'),
14
+ { EventSource } = require('eventsource'),
15
15
  execSync = require('child_process').execSync,
16
16
  exit = require('./helper.js').exit,
17
17
  fs = require('fs'),
18
18
  helper = require('./helper.js'),
19
19
  manifestFormat = require('cloudron-manifestformat'),
20
20
  micromatch = require('micromatch'),
21
- readlineSync = require('readline-sync'),
21
+ readline = require('./readline.js'),
22
22
  superagent = require('superagent'),
23
23
  os = require('os'),
24
24
  path = require('path'),
@@ -34,7 +34,7 @@ function requestError(response) {
34
34
  }
35
35
 
36
36
  // analyzes options and merges with any existing build service config
37
- function getBuildServiceConfig(options) {
37
+ async function resolveBuildServiceConfig(options) {
38
38
  const buildService = config.getBuildServiceConfig();
39
39
  if (!buildService.type) buildService.type = 'local'; // default
40
40
 
@@ -49,7 +49,7 @@ function getBuildServiceConfig(options) {
49
49
  if (typeof options.setBuildService === 'string') {
50
50
  url = options.setBuildService;
51
51
  } else {
52
- url = readlineSync.question('Enter build service URL: ', { });
52
+ url = await readline.question('Enter build service URL: ', { });
53
53
  }
54
54
 
55
55
  if (url.indexOf('://') === -1) url = `https://${url}`;
@@ -66,11 +66,11 @@ function getBuildServiceConfig(options) {
66
66
  async function login(localOptions, cmd) {
67
67
  const options = cmd.optsWithGlobals();
68
68
 
69
- const buildServiceConfig = getBuildServiceConfig(options);
69
+ const buildServiceConfig = await resolveBuildServiceConfig(options);
70
70
 
71
71
  console.log('Build Service login' + ` (${buildServiceConfig.url}):`);
72
72
 
73
- const token = options.buildServiceToken || readlineSync.question('Token: ', {});
73
+ const token = options.buildServiceToken || await readline.question('Token: ', {});
74
74
 
75
75
  const response = await superagent.get(`${buildServiceConfig.url}/api/v1/profile`).query({ accessToken: token }).ok(() => true);
76
76
  if (response.statusCode === 401 || response.statusCode === 403) return exit(`Authentication error: ${requestError(response)}`);
@@ -93,7 +93,7 @@ async function followBuildLog(buildId, raw) {
93
93
  const es = new EventSource(`${tmp.href}api/v1/builds/${buildId}/logstream?accessToken=${config.getBuildServiceConfig().token}`);
94
94
  let prevId = null, prevWasStatus = false;
95
95
 
96
- es.on('message', function (e) {
96
+ es.addEventListener('message', function (e) {
97
97
  if (raw) return console.dir(e);
98
98
 
99
99
  const data = safe.JSON.parse(e.data);
@@ -138,15 +138,15 @@ async function followBuildLog(buildId, raw) {
138
138
  });
139
139
 
140
140
  let didConnect = false;
141
- es.once('open', () => didConnect = true);
141
+ es.addEventListener('open', () => didConnect = true, { once: true });
142
142
 
143
143
  return new Promise((resolve, reject) => {
144
- es.once('error', function (error) { // server close or network error or some interruption
144
+ es.addEventListener('error', function (error) { // server close or network error or some interruption
145
145
  if (raw) console.dir(error);
146
146
 
147
147
  es.close();
148
148
  if (didConnect) resolve(); else reject(new Error('Failed to connect'));
149
- });
149
+ }, { once: true });
150
150
  });
151
151
  }
152
152
 
@@ -315,14 +315,14 @@ async function build(localOptions, cmd) {
315
315
  const sourceDir = path.dirname(manifestFilePath);
316
316
 
317
317
  const appConfig = config.getAppConfig(sourceDir);
318
- const buildServiceConfig = getBuildServiceConfig(options);
318
+ const buildServiceConfig = await resolveBuildServiceConfig(options);
319
319
 
320
320
  let repository = appConfig.repository;
321
321
  if (!repository || options.setRepository) {
322
322
  if (typeof options.setRepository === 'string') {
323
323
  repository = options.setRepository;
324
324
  } else {
325
- repository = readlineSync.question(`Enter docker repository (e.g registry/username/${manifest.id || path.basename(sourceDir)}): `, {});
325
+ repository = await readline.question(`Enter docker repository (e.g registry/username/${manifest.id || path.basename(sourceDir)}): `, {});
326
326
  if (!repository) exit('No repository provided');
327
327
  console.log();
328
328
  }
@@ -374,7 +374,7 @@ async function push(localOptions, cmd) {
374
374
  tag = options.tag;
375
375
  }
376
376
 
377
- const buildServiceConfig = getBuildServiceConfig(options);
377
+ const buildServiceConfig = await resolveBuildServiceConfig(options);
378
378
 
379
379
  const response = await superagent.post(`${buildServiceConfig.url}/api/v1/builds/${options.id}/push`)
380
380
  .query({ accessToken: buildServiceConfig.token })
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const { Transform } = require('stream');
4
+
5
+ class LineStream extends Transform {
6
+ constructor(options) {
7
+ super();
8
+ this._buffer = '';
9
+ }
10
+
11
+ _transform(chunk, encoding, callback) {
12
+ this._buffer += chunk.toString('utf8');
13
+
14
+ const lines = this._buffer.split('\n');
15
+ this._buffer = lines.pop(); // maybe incomplete line
16
+
17
+ for (const line of lines) {
18
+ this.emit('line', line);
19
+ }
20
+
21
+ callback();
22
+ }
23
+
24
+ _flush(callback) {
25
+ if (this._buffer) this.emit('line', this.buff_bufferer);
26
+ callback();
27
+ }
28
+ }
29
+
30
+ exports = module.exports = LineStream;
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ exports = module.exports = {
4
+ question
5
+ };
6
+
7
+ const readline = require('node:readline/promises'),
8
+ { Writable } = require('node:stream');
9
+
10
+ async function question(query, options) {
11
+ const output = options.noEchoBack
12
+ ? new Writable({ write: function (chunk, encoding, callback) { callback(); } })
13
+ : process.stdout;
14
+ process.stdin.setRawMode(options.noEchoBack); // raw mode gives each keypress as opposd to line based cooked mode
15
+ const rl = readline.createInterface({ input: process.stdin, output });
16
+ if (options.noEchoBack) process.stdout.write(query);
17
+ const answer = await rl.question(query, options);
18
+ rl.close();
19
+ if (options.noEchoBack) process.stdout.write('\n');
20
+ return answer;
21
+ }
@@ -0,0 +1,198 @@
1
+ 'use strict';
2
+
3
+ exports = module.exports = {
4
+ get,
5
+ put,
6
+ post,
7
+ patch,
8
+ del,
9
+ options,
10
+ request
11
+ };
12
+
13
+ // IMPORTANT: do not require box code here . This is used by migration scripts
14
+ const assert = require('assert'),
15
+ consumers = require('node:stream/consumers'),
16
+ debug = require('debug')('box:superagent'),
17
+ fs = require('fs'),
18
+ http = require('http'),
19
+ https = require('https'),
20
+ path = require('path'),
21
+ safe = require('safetydance');
22
+
23
+ class Request {
24
+ constructor(method, url) {
25
+ assert.strictEqual(typeof url, 'string');
26
+
27
+ this.url = new URL(url);
28
+ this.options = {
29
+ method,
30
+ headers: {},
31
+ signal: null // set for timeouts
32
+ };
33
+ this.okFunc = ({ status }) => status >=200 && status <= 299;
34
+ this.timer = { timeout: 0, id: null, controller: null };
35
+ this.retryCount = 0;
36
+ this.body = null;
37
+ this.redirectCount = 5;
38
+ }
39
+
40
+ async _handleResponse(url, response) {
41
+ // const contentLength = response.headers['content-length'];
42
+ // if (!contentLength || contentLength > 5*1024*1024*1024) throw new Error(`Response size unknown or too large: ${contentLength}`);
43
+ const [consumeError, data] = await safe(consumers.buffer(response)); // have to drain response
44
+ if (consumeError) throw new Error(`Error consuming body stream: ${consumeError.message}`);
45
+ if (!response.complete) throw new Error('Incomplete response');
46
+ const contentType = response.headers['content-type'];
47
+
48
+ const result = {
49
+ url: new URL(response.headers['location'] || '', url),
50
+ status: response.statusCode,
51
+ headers: response.headers,
52
+ body: null,
53
+ text: null
54
+ };
55
+
56
+ if (contentType?.includes('application/json')) {
57
+ result.text = data.toString('utf8');
58
+ if (data.byteLength !== 0) result.body = safe.JSON.parse(result.text) || {};
59
+ } else if (contentType?.includes('application/x-www-form-urlencoded')) {
60
+ result.text = data.toString('utf8');
61
+ const searchParams = new URLSearchParams(data);
62
+ result.body = Object.fromEntries(searchParams.entries());
63
+ } else if (!contentType || contentType.startsWith('text/')) {
64
+ result.body = data;
65
+ result.text = result.body.toString('utf8');
66
+ } else {
67
+ result.body = data;
68
+ result.text = `<binary data (${data.byteLength} bytes)>`;
69
+ }
70
+
71
+ return result;
72
+ }
73
+
74
+ async _makeRequest(url) {
75
+ return new Promise((resolve, reject) => {
76
+ const proto = url.protocol === 'https:' ? https : http;
77
+ const request = proto.request(url, this.options); // ClientRequest
78
+
79
+ request.on('error', reject); // network error, dns error
80
+ request.on('response', async (response) => {
81
+ const [error, result] = await safe(this._handleResponse(url, response));
82
+ if (error) reject(error); else resolve(result);
83
+ });
84
+
85
+ if (this.body) request.write(this.body);
86
+
87
+ request.end();
88
+ });
89
+ }
90
+
91
+ async _start() {
92
+ let error;
93
+
94
+ for (let i = 0; i < this.retryCount+1; i++) {
95
+ if (this.timer.timeout) this.timer.id = setTimeout(() => this.timer.controller.abort(), this.timer.timeout);
96
+ debug(`${this.options.method} ${this.url.toString()}` + (i ? ` try ${i+1}` : ''));
97
+
98
+ let response, url = this.url;
99
+ for (let redirects = 0; redirects < this.redirectCount+1; redirects++) {
100
+ [error, response] = await safe(this._makeRequest(url));
101
+ if (error || (response.status < 300 || response.status > 399) || (this.options.method !== 'GET')) break;
102
+ url = response.url; // follow
103
+ }
104
+
105
+ if (!error && !this.okFunc({ status: response.status })) {
106
+ error = new Error(`${response.status} ${http.STATUS_CODES[response.status]}`);
107
+ Object.assign(error, response);
108
+ }
109
+
110
+ if (error) debug(`${this.options.method} ${this.url.toString()} ${error.message}`);
111
+ if (this.timer.timeout) clearTimeout(this.timer.id);
112
+ if (!error) return response;
113
+ }
114
+
115
+ throw error;
116
+ }
117
+
118
+ set(name, value) {
119
+ this.options.headers[name.toLowerCase()] = value;
120
+ return this;
121
+ }
122
+
123
+ query(data) {
124
+ Object.entries(data).forEach(([key, value]) => this.url.searchParams.append(key, value));
125
+ return this;
126
+ }
127
+
128
+ redirects(count) {
129
+ this.redirectCount = count;
130
+ return this;
131
+ }
132
+
133
+ send(data) {
134
+ const contentType = this.options.headers['content-type'];
135
+ if (!contentType || contentType === 'application/json') {
136
+ this.options.headers['content-type'] = 'application/json';
137
+ this.body = Buffer.from(JSON.stringify(data), 'utf8');
138
+ this.options.headers['content-length'] = this.body.byteLength;
139
+ } else if (contentType === 'application/x-www-form-urlencoded') {
140
+ this.body = Buffer.from((new URLSearchParams(data)).toString(), 'utf8');
141
+ this.options.headers['content-length'] = this.body.byteLength;
142
+ }
143
+ return this;
144
+ }
145
+
146
+ timeout(msecs) {
147
+ this.timer.controller = new AbortController();
148
+ this.timer.timeout = msecs;
149
+ this.options.signal = this.timer.controller.signal;
150
+ return this;
151
+ }
152
+
153
+ retry(count) {
154
+ this.retryCount = Math.max(0, count);
155
+ return this;
156
+ }
157
+
158
+ ok(func) {
159
+ this.okFunc = func;
160
+ return this;
161
+ }
162
+
163
+ disableTLSCerts() {
164
+ this.options.rejectUnauthorized = true;
165
+ return this;
166
+ }
167
+
168
+ auth(username, password) {
169
+ this.set('Authorization', 'Basic ' + btoa(`${username}:${password}`));
170
+ return this;
171
+ }
172
+
173
+ attach(name, filepath) { // this is only used in tests and thus simplistic
174
+ const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
175
+
176
+ const partHeader = Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${name}" filename="${path.basename(filepath)}"\r\n\r\n`, 'utf8');
177
+ const partData = fs.readFileSync(filepath);
178
+ const partTrailer = Buffer.from(`\r\n--${boundary}--\r\n`, 'utf8');
179
+ this.body = Buffer.concat([partHeader, partData, partTrailer]);
180
+
181
+ this.options.headers['content-type'] = `multipart/form-data; boundary=${boundary}`;
182
+ this.options.headers['content-length'] = this.body.byteLength;
183
+
184
+ return this;
185
+ }
186
+
187
+ then(onFulfilled, onRejected) {
188
+ this._start().then(onFulfilled, onRejected);
189
+ }
190
+ }
191
+
192
+ function get(url) { return new Request('GET', url); }
193
+ function put(url) { return new Request('PUT', url); }
194
+ function post(url) { return new Request('POST', url); }
195
+ function patch(url) { return new Request('PATCH', url); }
196
+ function del(url) { return new Request('DELETE', url); }
197
+ function options(url) { return new Request('OPTIONS', url); }
198
+ function request(method, url) { return new Request(method, url); }
package/test/test.js CHANGED
@@ -32,7 +32,7 @@ function md5(file) {
32
32
  function cli(args, options) {
33
33
  // https://github.com/nodejs/node-v0.x-archive/issues/9265
34
34
  options = options || { };
35
- args = util.isArray(args) ? args : args.match(/[^\s"]+|"([^"]+)"/g);
35
+ args = Array.isArray(args) ? args : args.match(/[^\s"]+|"([^"]+)"/g);
36
36
  args = args.map(function (e) { return e[0] === '"' ? e.slice(1, -1) : e; }); // remove the quotes
37
37
 
38
38
  try {