cloudron 5.11.11 → 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.
package/bin/cloudron CHANGED
@@ -170,7 +170,7 @@ program.command('install')
170
170
  .option('-a, --alias-domains [domain,...]', 'Alias domains')
171
171
  .option('--appstore-id <appid[@version]>', 'Use app from the store')
172
172
  .option('--no-sso', 'Disable Cloudron SSO [false]')
173
- .option('--debug [cmd]', 'Enable debug mode')
173
+ .option('--debug [cmd...]', 'Enable debug mode', false)
174
174
  .option('--readonly', 'Mount filesystem readonly. Default is read/write in debug mode.')
175
175
  .action(actions.install);
176
176
 
@@ -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.11.11",
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
  }
@@ -243,13 +240,7 @@ async function waitForTask(taskId, options) {
243
240
  const response = await createRequest('GET', `/api/v1/tasks/${taskId}`, options);
244
241
  if (response.statusCode !== 200) throw new Error(`Failed to get task: ${requestError(response)}`);
245
242
 
246
- // TODO remove later, for now keep old behavior on if `pending` is missing
247
- if (typeof response.body.pending === 'undefined') {
248
- // note: for queued tasks, 'active' returns false
249
- if (response.body.error || response.body.percent === 100) return response.body; // task errored or done
250
- } else {
251
- if (!response.body.pending && !response.body.active) return response.body;
252
- }
243
+ if (!response.body.active) return response.body;
253
244
 
254
245
  let message = response.body.message || '';
255
246
 
@@ -340,7 +331,7 @@ async function authenticate(adminFqdn, username, password, options) {
340
331
  let totpToken;
341
332
 
342
333
  const { rejectUnauthorized, askForTotpToken } = options;
343
- if (askForTotpToken) totpToken = readlineSync.question('2FA Token: ', {});
334
+ if (askForTotpToken) totpToken = await readline.question('2FA Token: ', {});
344
335
 
345
336
  const request = superagent.post(`https://${adminFqdn}/api/v1/auth/login`)
346
337
  .timeout(60000)
@@ -366,7 +357,7 @@ async function authenticate(adminFqdn, username, password, options) {
366
357
  }
367
358
 
368
359
  async function login(adminFqdn, localOptions, cmd) {
369
- 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): ', {});
370
361
  if (!adminFqdn) return exit('');
371
362
 
372
363
  if (adminFqdn.indexOf('https://') === 0) adminFqdn = adminFqdn.slice('https://'.length);
@@ -395,8 +386,8 @@ async function login(adminFqdn, localOptions, cmd) {
395
386
  }
396
387
 
397
388
  if (!token) {
398
- const username = options.username || readlineSync.question('Username: ', {});
399
- 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 });
400
391
 
401
392
  const [error, result] = await safe(authenticate(adminFqdn, username, password, { rejectUnauthorized, askForTotpToken: false }));
402
393
  if (error) return exit(`Failed to login: ${error.message}`);
@@ -464,7 +455,7 @@ async function list(localOptions, cmd) {
464
455
  t.cell('Id', detailedApp.id);
465
456
  t.cell('Location', detailedApp.fqdn);
466
457
  t.cell('Manifest Id', (detailedApp.manifest.id || 'customapp') + '@' + detailedApp.manifest.version);
467
- var prettyState;
458
+ let prettyState;
468
459
  if (detailedApp.installationState === 'installed') {
469
460
  prettyState = (detailedApp.debugMode ? 'debug' : detailedApp.runState);
470
461
  } else if (detailedApp.installationState === 'error') {
@@ -487,18 +478,19 @@ async function querySecondaryDomains(app, manifest, options) {
487
478
 
488
479
  for (const env in manifest.httpPorts) {
489
480
  const defaultDomain = (app && app.secondaryDomains && app.secondaryDomains[env]) ? app.secondaryDomains[env] : (manifest.httpPorts[env].defaultValue || '');
490
- const input = readlineSync.question(`${manifest.httpPorts[env].description} (default: "${defaultDomain}"): `, {});
481
+ const input = await readline.question(`${manifest.httpPorts[env].description} (default: "${defaultDomain}"): `, {});
491
482
  secondaryDomains[env] = await selectDomain(input, options);
492
483
  }
493
484
  return secondaryDomains;
494
485
  }
495
486
 
496
- function queryPortBindings(app, manifest) {
487
+ async function queryPortBindings(app, manifest) {
497
488
  const portBindings = {};
498
- const allPorts = _.extend({}, manifest.tcpPorts, manifest.udpPorts);
489
+ const allPorts = Object.assign({}, manifest.tcpPorts, manifest.udpPorts);
490
+
499
491
  for (const env in allPorts) {
500
492
  const defaultPort = (app && app.portBindings && app.portBindings[env]) ? app.portBindings[env] : (allPorts[env].defaultValue || '');
501
- 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): ', {});
502
494
  if (port === '') {
503
495
  portBindings[env] = defaultPort;
504
496
  } else if (isNaN(parseInt(port, 10))) {
@@ -510,16 +502,6 @@ function queryPortBindings(app, manifest) {
510
502
  return portBindings;
511
503
  }
512
504
 
513
- // this function can fail on many levels
514
- function parseDebugCommand(cmd) {
515
- // hack for commander
516
- if (typeof cmd !== 'string' || cmd === '') return [ '/bin/bash', '-c', 'echo "Debug mode. Use cloudron exec to debug. Sleeping" && sleep 100000' ];
517
-
518
- if (cmd == 'default') return null; // another hack
519
-
520
- return cmd.split(' '); // yet another hack
521
- }
522
-
523
505
  async function downloadManifest(appstoreId) {
524
506
  const [ id, version ] = appstoreId.split('@');
525
507
 
@@ -575,7 +557,7 @@ async function install(localOptions, cmd) {
575
557
  manifest.dockerImage = image;
576
558
  }
577
559
 
578
- const location = options.location || readlineSync.question('Location: ', {});
560
+ const location = options.location || await readline.question('Location: ', {});
579
561
  if (!location) return exit('');
580
562
 
581
563
  const domainObject = await selectDomain(location, options);
@@ -620,10 +602,10 @@ async function install(localOptions, cmd) {
620
602
  ports[tmp[0]] = parseInt(tmp[1], 10);
621
603
  });
622
604
  } else {
623
- ports = queryPortBindings(null /* existing app */, manifest);
605
+ ports = await queryPortBindings(null /* existing app */, manifest);
624
606
  }
625
607
  } else { // just put in defaults
626
- const allPorts = _.extend({}, manifest.tcpPorts, manifest.udpPorts);
608
+ const allPorts = Object.assign({}, manifest.tcpPorts, manifest.udpPorts);
627
609
  for (const portName in allPorts) {
628
610
  ports[portName] = allPorts[portName].defaultValue;
629
611
  }
@@ -646,10 +628,11 @@ async function install(localOptions, cmd) {
646
628
  // the sso only applies for apps which allow optional sso
647
629
  if (manifest.optionalSso) data.sso = options.sso;
648
630
 
649
- if (options.debug) {
631
+ if (options.debug) { // 'true' when no args. otherwise, array
632
+ const debugCmd = options.debug === true ? [ '/bin/bash', '-c', 'echo "Repair mode. Use the webterminal or cloudron exec to repair. Sleeping" && sleep infinity' ] : options.debug;
650
633
  data.debugMode = {
651
634
  readonlyRootfs: options.readonly ? true : false,
652
- cmd: parseDebugCommand(options.debug)
635
+ cmd: debugCmd
653
636
  };
654
637
  data.memoryLimit = -1;
655
638
  options.wait = false; // in debug mode, health check never succeeds
@@ -684,7 +667,7 @@ async function setLocation(localOptions, cmd) {
684
667
  const app = await getApp(options);
685
668
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
686
669
 
687
- 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}): `, { });
688
671
  const location = options.location || app.subdomain;
689
672
 
690
673
  const domainObject = await selectDomain(location, options);
@@ -749,7 +732,7 @@ async function setLocation(localOptions, cmd) {
749
732
  ports[tmp[0]] = parseInt(tmp[1], 10);
750
733
  });
751
734
  } else {
752
- ports = queryPortBindings(app, app.manifest);
735
+ ports = await queryPortBindings(app, app.manifest);
753
736
  }
754
737
 
755
738
  for (const port in ports) {
@@ -803,10 +786,10 @@ async function update(localOptions, cmd) {
803
786
 
804
787
  if (app.error && (app.error.installationState === 'pending_install')) { // install had failed. call repair to re-install
805
788
  apiPath = `/api/v1/apps/${app.id}/repair`;
806
- data = _.extend(data, { manifest });
789
+ data = Object.assign(data, { manifest });
807
790
  } else {
808
791
  apiPath = `/api/v1/apps/${app.id}/update`;
809
- data = _.extend(data, {
792
+ data = Object.assign(data, {
810
793
  appStoreId: options.appstoreId || '', // note case change
811
794
  manifest: manifest,
812
795
  skipBackup: !options.backup,
@@ -835,10 +818,12 @@ async function debug(args, localOptions, cmd) {
835
818
  const app = await getApp(options);
836
819
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
837
820
 
821
+ const debugCmd = args.length === 0 ? [ '/bin/bash', '-c', 'echo "Repair mode. Use the webterminal or cloudron exec to repair. Sleeping" && sleep infinity' ] : args;
822
+
838
823
  const data = {
839
824
  debugMode: options.disable ? null : {
840
825
  readonlyRootfs: options.readonly ? true : false,
841
- cmd: parseDebugCommand(args.join(' ').trim())
826
+ cmd: debugCmd
842
827
  }
843
828
  };
844
829
 
@@ -967,13 +952,13 @@ async function logs(localOptions, cmd) {
967
952
 
968
953
  if (tail) {
969
954
  const url = `${apiPath}?access_token=${token}&lines=10&format=json`;
970
- 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
971
956
 
972
- 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
973
958
  logPrinter(JSON.parse(e.data));
974
959
  });
975
960
 
976
- es.on('error', function (error) {
961
+ es.addEventListener('error', function (error) {
977
962
  if (error.status === 401) return exit('Please login first');
978
963
  if (error.status === 412) exit('Logs currently not available.');
979
964
  exit(error);
@@ -987,13 +972,13 @@ async function logs(localOptions, cmd) {
987
972
  });
988
973
  req.on('error', (error) => exit(`Pipe error: ${error.message}`));
989
974
 
990
- const jsonStream = split((line) => JSON.parse(line));
991
- jsonStream
992
- .on('data', logPrinter)
975
+ const lineStream = new LineStream();
976
+ lineStream
977
+ .on('line', (line) => { logPrinter(JSON.parse(line)); } )
993
978
  .on('error', (error) => exit(`JSON parse error: ${error.message}`))
994
979
  .on('end', process.exit);
995
980
 
996
- req.pipe(jsonStream);
981
+ req.pipe(lineStream);
997
982
  }
998
983
  }
999
984
 
@@ -1113,7 +1098,7 @@ async function backupList(localOptions, cmd) {
1113
1098
  return backup;
1114
1099
  }).sort(function (a, b) { return b.creationTime - a.creationTime; });
1115
1100
 
1116
- var t = new Table();
1101
+ const t = new Table();
1117
1102
 
1118
1103
  response.body.backups.forEach(function (backup) {
1119
1104
  t.cell('Id', backup.id);
@@ -1255,9 +1240,9 @@ async function clone(localOptions, cmd) {
1255
1240
 
1256
1241
  if (!options.backup) return exit('Use --backup to specify the backup id');
1257
1242
 
1258
- const location = options.location || readlineSync.question('Cloned app location: ', {});
1243
+ const location = options.location || await readline.question('Cloned app location: ', {});
1259
1244
  const secondaryDomains = await querySecondaryDomains(app, app.manifest, options);
1260
- const ports = queryPortBindings(app, app.manifest);
1245
+ const ports = await queryPortBindings(app, app.manifest);
1261
1246
  const backupId = options.backup;
1262
1247
 
1263
1248
  const domainObject = await selectDomain(location, options);
@@ -1430,19 +1415,7 @@ function push(localDir, remote, localOptions, cmd) {
1430
1415
  if (local === '-') {
1431
1416
  localOptions._stdin = process.stdin;
1432
1417
  } else if (stat) {
1433
- const progress = new ProgressStream({ length: stat.size, time: 1000 });
1434
-
1435
- localOptions._stdin = progress;
1436
- fs.createReadStream(local).pipe(progress);
1437
-
1438
- const bar = new ProgressBar('Uploading [:bar] :percent: :etas', {
1439
- complete: '=',
1440
- incomplete: ' ',
1441
- width: 100,
1442
- total: stat.size
1443
- });
1444
-
1445
- progress.on('progress', function (p) { bar.update(p.percentage / 100); /* bar.tick(p.transferred - bar.curr); */ });
1418
+ localOptions._stdin = fs.createReadStream(local);
1446
1419
  } else {
1447
1420
  exit('local file ' + local + ' does not exist');
1448
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 {