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 +1 -1
- package/bin/cloudron-appstore +4 -0
- package/eslint.config.js +21 -0
- package/package.json +9 -13
- package/src/actions.js +40 -67
- package/src/appstore-actions.js +36 -6
- package/src/backup-tools.js +9 -7
- package/src/build-actions.js +13 -13
- package/src/line-stream.js +30 -0
- package/src/readline.js +21 -0
- package/src/superagent.js +198 -0
- package/test/test.js +1 -1
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
|
|
package/bin/cloudron-appstore
CHANGED
|
@@ -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')
|
package/eslint.config.js
ADDED
|
@@ -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.
|
|
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": "^
|
|
23
|
-
"debug": "^4.
|
|
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": "^
|
|
25
|
+
"eventsource": "^3.0.5",
|
|
27
26
|
"micromatch": "^4.0.8",
|
|
28
27
|
"open": "^10.1.0",
|
|
29
|
-
"
|
|
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": "
|
|
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": "^
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 ||
|
|
399
|
-
const password = options.password ||
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ||
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
789
|
+
data = Object.assign(data, { manifest });
|
|
807
790
|
} else {
|
|
808
791
|
apiPath = `/api/v1/apps/${app.id}/update`;
|
|
809
|
-
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:
|
|
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
|
-
|
|
955
|
+
const es = new EventSource(url, { rejectUnauthorized }); // not sure why this is needed
|
|
971
956
|
|
|
972
|
-
es.
|
|
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.
|
|
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
|
|
991
|
-
|
|
992
|
-
.on('
|
|
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(
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
}
|
package/src/appstore-actions.js
CHANGED
|
@@ -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
|
-
|
|
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 ||
|
|
75
|
-
const password = options.password ||
|
|
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 =
|
|
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)
|
|
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);
|
package/src/backup-tools.js
CHANGED
|
@@ -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
|
-
|
|
270
|
+
while (true) {
|
|
272
271
|
const cur = tbd.pop();
|
|
273
272
|
const entries = fs.readdirSync(path.join(inDirAbs, cur), { withFileTypes: true });
|
|
274
|
-
|
|
273
|
+
|
|
274
|
+
for (const entry of entries) {
|
|
275
275
|
if (entry.isDirectory()) {
|
|
276
276
|
tbd.push(path.join(cur, entry.name));
|
|
277
|
-
|
|
277
|
+
continue;
|
|
278
278
|
} else if (!entry.isFile()) {
|
|
279
|
-
|
|
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
|
-
}
|
|
302
|
-
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (tbd.length === 0) break;
|
|
304
|
+
}
|
|
303
305
|
}
|
|
304
306
|
|
|
305
307
|
async function decryptFilename(filePath, options) {
|
package/src/build-actions.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
69
|
+
const buildServiceConfig = await resolveBuildServiceConfig(options);
|
|
70
70
|
|
|
71
71
|
console.log('Build Service login' + ` (${buildServiceConfig.url}):`);
|
|
72
72
|
|
|
73
|
-
const token = options.buildServiceToken ||
|
|
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.
|
|
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.
|
|
141
|
+
es.addEventListener('open', () => didConnect = true, { once: true });
|
|
142
142
|
|
|
143
143
|
return new Promise((resolve, reject) => {
|
|
144
|
-
es.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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;
|
package/src/readline.js
ADDED
|
@@ -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 =
|
|
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 {
|