@coderich/sandman 0.0.4 → 0.0.6
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/index.js +5 -1
- package/package.json +3 -2
- package/src/ConfigClient.js +118 -19
- package/src/FetchService.js +37 -56
- package/src/ReadlineService.js +85 -0
- package/src/Sandman.js +22 -156
- package/src/app.js +0 -5
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderich/sandman",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
"dev": "coderich-dev"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@coderich/config": "2.2.
|
|
18
|
+
"@coderich/config": "2.2.5",
|
|
19
19
|
"@coderich/util": "2.1.3",
|
|
20
20
|
"chokidar": "4.0.3",
|
|
21
|
+
"lodash.clonedeep": "4.5.0",
|
|
21
22
|
"lodash.merge": "4.6.2"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
package/src/ConfigClient.js
CHANGED
|
@@ -1,25 +1,124 @@
|
|
|
1
|
-
const FS = require('fs');
|
|
2
1
|
const Path = require('path');
|
|
2
|
+
const Chokidar = require('chokidar');
|
|
3
3
|
const Config = require('@coderich/config');
|
|
4
|
+
const Util = require('@coderich/util');
|
|
5
|
+
|
|
6
|
+
const dataSymbol = Symbol('dataSymbol');
|
|
4
7
|
|
|
5
8
|
module.exports = class ConfigClient extends Config {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
9
|
+
#configDir; #mergeData = {};
|
|
10
|
+
|
|
11
|
+
constructor(configDir) {
|
|
12
|
+
super();
|
|
13
|
+
this.#configDir = configDir;
|
|
14
|
+
this.mergeDir();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get(key, ...args) {
|
|
18
|
+
const data = super.get(key, ...args);
|
|
19
|
+
const flatData = key === undefined ? Util.flatten(data) : Util.flatten({ [key]: data });
|
|
20
|
+
const apiKeys = Array.from(new Set(Object.keys(flatData).map(k => k.substring(0, Math.max(k.indexOf('.request'), 0))).filter(Boolean)));
|
|
21
|
+
|
|
22
|
+
apiKeys.forEach((apiKey) => {
|
|
23
|
+
Reflect.ownKeys(this.#mergeData).forEach((mergeKey) => {
|
|
24
|
+
if (mergeKey === dataSymbol) flatData[apiKey] = { ...this.#mergeData[dataSymbol], ...flatData[apiKey] };
|
|
25
|
+
else if (apiKey.startsWith(mergeKey)) flatData[apiKey] = { ...this.#mergeData[mergeKey][dataSymbol], ...flatData[apiKey] };
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const unflatData = Util.unflatten(flatData);
|
|
30
|
+
const rawData = key === undefined ? unflatData : Util.get(unflatData, key);
|
|
31
|
+
super.set(dataSymbol, rawData);
|
|
32
|
+
return super.get(dataSymbol);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
mergeDir(dir = this.#configDir) {
|
|
36
|
+
return this.merge(Config.parseDir(dir, (...args) => this.#ignore(...args)));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
watch(dir = this.#configDir) {
|
|
40
|
+
const watcher = Chokidar.watch(dir, {
|
|
41
|
+
awaitWriteFinish: true,
|
|
42
|
+
ignoreInitial: true,
|
|
43
|
+
ignored: filepath => this.#ignore(this.#normalizeWatcherPath(filepath)),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
watcher.on('all', (event, path) => {
|
|
47
|
+
const { key } = this.#normalizeWatcherPath(path);
|
|
48
|
+
|
|
49
|
+
if (['add', 'change'].includes(event)) {
|
|
50
|
+
const api = Config.parseFile(path);
|
|
51
|
+
if (key) this.set(key, api);
|
|
52
|
+
else this.merge(api); // index.yaml
|
|
53
|
+
// if (api.request) this.emit('save', { key, api });
|
|
54
|
+
} else if (['unlink', 'unlinkDir'].includes(event)) {
|
|
55
|
+
this.del(key);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#ignore({ name, filepath, paths }) {
|
|
61
|
+
if (name.startsWith('.')) return true;
|
|
62
|
+
|
|
63
|
+
if (name.startsWith('+')) {
|
|
64
|
+
const path = paths.slice(0, -1).join('.');
|
|
65
|
+
const request = Util.flatten(Config.parseFile(filepath));
|
|
66
|
+
if (path) this.#mergeData[path] = { [dataSymbol]: request };
|
|
67
|
+
else this.#mergeData[dataSymbol] = request;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return false;
|
|
24
72
|
}
|
|
73
|
+
|
|
74
|
+
#normalizeWatcherPath = (filepath) => {
|
|
75
|
+
const parsed = Path.parse(filepath);
|
|
76
|
+
const folder = filepath.substring(this.#configDir.length + 1, filepath.length - parsed.ext.length);
|
|
77
|
+
const paths = folder.split('/').filter(el => el && el !== 'index');
|
|
78
|
+
const key = paths.join('.');
|
|
79
|
+
return { ...parsed, filepath, paths, key };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// exports.decorateRequest = (mergeData, key, request) => {
|
|
83
|
+
// const toMerge = key.split('.').reduce((prev, k, i, arr) => {
|
|
84
|
+
// const $key = arr.slice(0, i).join('.');
|
|
85
|
+
// return Merge({}, prev, mergeData[$key]?.request);
|
|
86
|
+
// }, Merge({}, mergeData.request));
|
|
87
|
+
|
|
88
|
+
// return Merge({}, toMerge, request);
|
|
89
|
+
// };
|
|
90
|
+
|
|
91
|
+
// #get(key, ...rest) {
|
|
92
|
+
// const { config } = this.#configClient.toObject();
|
|
93
|
+
// const value = get(config, key);
|
|
94
|
+
|
|
95
|
+
// if (value?.request) {
|
|
96
|
+
// const $request = FetchService.decorateRequest(this.#mergeData, key, value.request);
|
|
97
|
+
// const request = this.#configClient.resolve({ vars: value.request.vars }).set(resolveSymbol, $request).get(resolveSymbol);
|
|
98
|
+
// this.#configClient.del(resolveSymbol);
|
|
99
|
+
// return Merge({}, value, { request });
|
|
100
|
+
// }
|
|
101
|
+
|
|
102
|
+
// return this.#configClient.get(key, ...rest);
|
|
103
|
+
// }
|
|
104
|
+
|
|
105
|
+
// mergeConfigDir(dir) {
|
|
106
|
+
// const ignored = (parsed) => {
|
|
107
|
+
// if (parsed.name.startsWith('.')) return true;
|
|
108
|
+
// const stat = FS.statSync(Path.join(parsed.dir, `${parsed.name}${parsed.ext}`));
|
|
109
|
+
// if (stat?.isDirectory()) return false;
|
|
110
|
+
// return !['.yml', '.yaml'].includes(parsed.ext.toLowerCase());
|
|
111
|
+
// };
|
|
112
|
+
|
|
113
|
+
// const arr = Config.dirPaths(dir, ignored);
|
|
114
|
+
|
|
115
|
+
// const yaml = arr.reduce((prev, { paths, data }) => {
|
|
116
|
+
// const path = paths.join('.');
|
|
117
|
+
// if (!path.length) return prev.concat(data);
|
|
118
|
+
// const indented = data.split('\n').map(line => (line.trim() ? ` ${line}` : line)).join('\n');
|
|
119
|
+
// return prev.concat(`${path}:\n${indented}`);
|
|
120
|
+
// }, '');
|
|
121
|
+
|
|
122
|
+
// return this.merge(Config.parseYaml(yaml));
|
|
123
|
+
// }
|
|
25
124
|
};
|
package/src/FetchService.js
CHANGED
|
@@ -1,64 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
return new Promise((resolve, reject) => {
|
|
7
|
-
const { url, ...params } = exports.normalizeRequest({ ...req });
|
|
8
|
-
|
|
9
|
-
fetch(url, params).then(async (res) => {
|
|
10
|
-
const ct = res.headers.get('content-type') || '';
|
|
11
|
-
const data = await (ct.includes('application/json') ? res.json() : res.text());
|
|
12
|
-
return { res, data };
|
|
13
|
-
}).then(resolve).catch(reject);
|
|
1
|
+
exports.fetch = (request) => {
|
|
2
|
+
return fetch(request).then(async (response) => {
|
|
3
|
+
const ct = response.headers.get('content-type') || '';
|
|
4
|
+
const data = await (ct.includes('application/json') ? response.json() : response.text());
|
|
5
|
+
return { response, data };
|
|
14
6
|
});
|
|
15
7
|
};
|
|
16
8
|
|
|
17
9
|
exports.normalizeRequest = (req) => {
|
|
18
|
-
req.path ??= '';
|
|
19
|
-
req.
|
|
20
|
-
req.url += req.path;
|
|
21
|
-
req.url = Object.entries(req.params).reduce((url, [key, value]) => { url.searchParams.append(key, value); return url; }, new URL(req.url)).toString();
|
|
10
|
+
req.path ??= ''; req.method ??= 'get'; req.headers ??= {}; req.params ??= {};
|
|
11
|
+
req.url = Object.entries(req.params).reduce((url, [key, value]) => { url.searchParams.append(key, value); return url; }, new URL(`${req.url}${req.path}`)).toString();
|
|
22
12
|
req.headers = Object.entries(req.headers).reduce((prev, [key, value]) => Object.assign(prev, { [key.toLowerCase()]: value }), {});
|
|
23
13
|
const [contentType] = req.headers['content-type']?.split(';') || [];
|
|
24
|
-
const { data } = req; delete req.data;
|
|
25
|
-
if (data == null) return req;
|
|
26
14
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
break;
|
|
15
|
+
if (req.data) {
|
|
16
|
+
switch (contentType) {
|
|
17
|
+
case 'application/json': {
|
|
18
|
+
req.body = JSON.stringify(req.data);
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
case 'application/x-www-form-urlencoded': {
|
|
22
|
+
req.body = new URLSearchParams(req.data).toString();
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
case 'multipart/form-data': {
|
|
26
|
+
delete req.headers['content-type'];
|
|
27
|
+
req.body = new FormData();
|
|
28
|
+
Object.entries(req.data).forEach(([key, value]) => req.body.append(key, value));
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
default: {
|
|
32
|
+
req.body = req.data;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
48
35
|
}
|
|
49
36
|
}
|
|
50
37
|
|
|
51
|
-
|
|
38
|
+
delete req.data; delete req.params;
|
|
39
|
+
const { url, ...params } = req;
|
|
40
|
+
return new Request(url, params);
|
|
52
41
|
};
|
|
53
42
|
|
|
54
|
-
exports.
|
|
55
|
-
const toMerge = key.split('.').reduce((prev, k, i, arr) => {
|
|
56
|
-
const $key = arr.slice(0, i).join('.');
|
|
57
|
-
return Merge({}, prev, mergeData[$key]?.request);
|
|
58
|
-
}, Merge({}, mergeData.request));
|
|
59
|
-
|
|
60
|
-
return Merge({}, toMerge, request);
|
|
61
|
-
};
|
|
43
|
+
exports.shq = s => `'${String(s).replace(/'/g, "'\\''")}'`;
|
|
62
44
|
|
|
63
45
|
exports.toCURL = (request, { pretty = true, redactAuth = false } = {}) => {
|
|
64
46
|
if (!request) return '<no request>';
|
|
@@ -89,18 +71,17 @@ exports.toCURL = (request, { pretty = true, redactAuth = false } = {}) => {
|
|
|
89
71
|
// body (skip for GET)
|
|
90
72
|
if (data != null && !/^GET$/i.test(method)) {
|
|
91
73
|
if (typeof data === 'string') {
|
|
92
|
-
parts.push('--data-raw', shq(data));
|
|
74
|
+
parts.push('--data-raw', exports.shq(data));
|
|
93
75
|
} else if (data instanceof URLSearchParams) {
|
|
94
|
-
parts.push('-H', shq('Content-Type: application/x-www-form-urlencoded'));
|
|
95
|
-
parts.push('--data', shq(data.toString()));
|
|
76
|
+
parts.push('-H', exports.shq('Content-Type: application/x-www-form-urlencoded'));
|
|
77
|
+
parts.push('--data', exports.shq(data.toString()));
|
|
96
78
|
} else if (typeof data === 'object') {
|
|
97
|
-
// JSON by default
|
|
98
79
|
const hasCT = Object.keys(hdrs).some(h => /^content-type$/i.test(h));
|
|
99
|
-
if (!hasCT) parts.push('-H', shq('Content-Type: application/json'));
|
|
100
|
-
parts.push('--data-raw', shq(JSON.stringify(data)));
|
|
80
|
+
if (!hasCT) parts.push('-H', exports.shq('Content-Type: application/json'));
|
|
81
|
+
parts.push('--data-raw', exports.shq(JSON.stringify(data)));
|
|
101
82
|
}
|
|
102
83
|
}
|
|
103
84
|
|
|
104
|
-
parts.push(shq(full.toString()));
|
|
85
|
+
parts.push(exports.shq(full.toString()));
|
|
105
86
|
return pretty ? parts.join(' \\\n ') : parts.join(' ');
|
|
106
87
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const Readline = require('readline');
|
|
2
|
+
const { get, flatten } = require('@coderich/util');
|
|
3
|
+
|
|
4
|
+
exports.createInterface = (cli, configClient) => {
|
|
5
|
+
const captureInfo = {
|
|
6
|
+
line: '',
|
|
7
|
+
lastToken: '',
|
|
8
|
+
candidates: [],
|
|
9
|
+
tabCounter: 0,
|
|
10
|
+
candidateIndex: -1,
|
|
11
|
+
captureCandidates: false,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const readline = Readline.createInterface({
|
|
15
|
+
input: process.stdin,
|
|
16
|
+
output: process.stdout,
|
|
17
|
+
terminal: true,
|
|
18
|
+
completer: (line) => {
|
|
19
|
+
// Show available CLI commands
|
|
20
|
+
if (!line) return [Object.keys(cli), line];
|
|
21
|
+
|
|
22
|
+
const tokens = line.split(' ');
|
|
23
|
+
const lastToken = tokens.at(-1);
|
|
24
|
+
const paths = lastToken.split('.');
|
|
25
|
+
const path = paths.at(-1);
|
|
26
|
+
|
|
27
|
+
// Specific request.data selector
|
|
28
|
+
if (lastToken.startsWith('.')) {
|
|
29
|
+
const api = configClient.get(tokens.at(-2));
|
|
30
|
+
if (!api?.request) return [[], path];
|
|
31
|
+
const dataPath = ['request'].concat(paths.slice(1, -1)).join('.');
|
|
32
|
+
const data = get(api, dataPath, {});
|
|
33
|
+
return [Object.keys(data).filter(k => k.toLowerCase().startsWith(path.toLowerCase())), path];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//
|
|
37
|
+
const flatKeys = Object.keys(flatten(configClient.get()));
|
|
38
|
+
|
|
39
|
+
// These keys follow the typing of the user
|
|
40
|
+
const startsWithCandidates = Array.from(new Set(flatKeys.map((flatKey) => {
|
|
41
|
+
return flatKey.split('.').slice(0, paths.length).join('.');
|
|
42
|
+
}))).filter((c) => {
|
|
43
|
+
return c.toLowerCase().startsWith(lastToken.toLowerCase());
|
|
44
|
+
}); // .map(p => p.split('.').at(-1)); // Here!
|
|
45
|
+
|
|
46
|
+
// These are shortcut keys to requests
|
|
47
|
+
const requestKeyCandidates = Array.from(new Set(flatKeys.map((flatKey) => {
|
|
48
|
+
const keys = flatKey.split('.');
|
|
49
|
+
const index = keys.indexOf('request');
|
|
50
|
+
return index && flatKey.split('.').slice(0, index).join('.');
|
|
51
|
+
// const typedPath = keys.slice(0, paths.length - 1).join('.');
|
|
52
|
+
// const autocompletePath = keys.slice(paths.length - 1, index).join('.');
|
|
53
|
+
// return index > 0 && lastToken.toLowerCase().startsWith(typedPath.toLowerCase()) && autocompletePath;
|
|
54
|
+
}).filter(Boolean))).filter((c) => {
|
|
55
|
+
return c.toLowerCase().includes(lastToken.toLowerCase());
|
|
56
|
+
// return c.toLowerCase().includes(path.toLowerCase());
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const candidates = Array.from(new Set(startsWithCandidates.concat(requestKeyCandidates)));
|
|
60
|
+
if (captureInfo.captureCandidates) { captureInfo.candidates = candidates; captureInfo.line = line; captureInfo.lastToken = lastToken; captureInfo.candidateIndex = -1; }
|
|
61
|
+
if (candidates.length === 1 && candidates[0] === lastToken) return [[], lastToken];
|
|
62
|
+
return [candidates, lastToken];
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
process.stdin.on('keypress', (ch, key) => {
|
|
67
|
+
if (key && key.name === 'tab') captureInfo.tabCounter++; else captureInfo.tabCounter = 0;
|
|
68
|
+
captureInfo.captureCandidates = captureInfo.tabCounter === 2;
|
|
69
|
+
|
|
70
|
+
if (key && key.name === 'escape') {
|
|
71
|
+
readline.line = '';
|
|
72
|
+
Readline.cursorTo(process.stdout, 0);
|
|
73
|
+
Readline.clearLine(process.stdout, 0);
|
|
74
|
+
readline.prompt();
|
|
75
|
+
} else if (captureInfo.tabCounter > 2 && captureInfo.candidates.length) {
|
|
76
|
+
const candidate = captureInfo.candidates[captureInfo.candidateIndex = ++captureInfo.candidateIndex % captureInfo.candidates.length];
|
|
77
|
+
const value = captureInfo.line.replace(captureInfo.lastToken, candidate);
|
|
78
|
+
readline.line = value;
|
|
79
|
+
readline.cursor = value.length;
|
|
80
|
+
readline.prompt(true);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return readline;
|
|
85
|
+
};
|
package/src/Sandman.js
CHANGED
|
@@ -1,27 +1,17 @@
|
|
|
1
|
-
const Path = require('path');
|
|
2
|
-
const Merge = require('lodash.merge');
|
|
3
|
-
const Readline = require('readline');
|
|
4
|
-
const Chokidar = require('chokidar');
|
|
5
1
|
const EventEmitter = require('events');
|
|
6
|
-
const
|
|
2
|
+
const ReadlineService = require('./ReadlineService');
|
|
7
3
|
const FetchService = require('./FetchService');
|
|
8
4
|
const ConfigClient = require('./ConfigClient');
|
|
9
5
|
|
|
10
|
-
const resolveSymbol = Symbol('resolve');
|
|
11
|
-
|
|
12
6
|
module.exports = class Sandman extends EventEmitter {
|
|
13
|
-
#configClient; #
|
|
14
|
-
#captureCandidates = false; #candidates = []; #tabCounter = 0; #candidateIndex; #line; #lastToken;
|
|
7
|
+
#configClient; #options; #readline; #cli; #cliCounter = 0;
|
|
15
8
|
|
|
16
9
|
constructor(configDir, options) {
|
|
17
10
|
super();
|
|
18
|
-
this.#configDir = configDir;
|
|
19
11
|
this.#options = options;
|
|
20
|
-
this.#configClient = new ConfigClient(
|
|
21
|
-
this.#createInterface();
|
|
22
|
-
this.#createWatcher();
|
|
12
|
+
this.#configClient = new ConfigClient(configDir);
|
|
23
13
|
this.#createCLI();
|
|
24
|
-
this.#
|
|
14
|
+
this.#readline = ReadlineService.createInterface(this.#cli, this.#configClient);
|
|
25
15
|
|
|
26
16
|
this.#readline.on('line', async (line) => {
|
|
27
17
|
if (!line) return this.#readline.prompt();
|
|
@@ -30,6 +20,8 @@ module.exports = class Sandman extends EventEmitter {
|
|
|
30
20
|
const value = await Promise.resolve(this.#cli[info.cmd](...info.args)).catch(e => e);
|
|
31
21
|
return this.emit(cmd, value);
|
|
32
22
|
});
|
|
23
|
+
|
|
24
|
+
this.#prompt();
|
|
33
25
|
}
|
|
34
26
|
|
|
35
27
|
cli() {
|
|
@@ -45,50 +37,38 @@ module.exports = class Sandman extends EventEmitter {
|
|
|
45
37
|
}
|
|
46
38
|
|
|
47
39
|
#run(key) {
|
|
48
|
-
const api = this.#get(key, {});
|
|
40
|
+
const api = this.#configClient.get(key, {});
|
|
49
41
|
if (!api?.request) return this.emit('error', { key, error: `Request "${key}" Not Found` });
|
|
50
|
-
this.emit('
|
|
42
|
+
this.emit('api', { api, key });
|
|
43
|
+
const request = FetchService.normalizeRequest(api.request);
|
|
44
|
+
this.emit('request', { request, api, key });
|
|
51
45
|
|
|
52
|
-
return FetchService.fetch(
|
|
53
|
-
this.emit('response', {
|
|
54
|
-
return {
|
|
46
|
+
return FetchService.fetch(request).then(({ response, data }) => {
|
|
47
|
+
this.emit('response', { response, api, key, data });
|
|
48
|
+
return { response, api, key, data };
|
|
55
49
|
}).catch((error) => {
|
|
56
50
|
this.emit('error', { key, api, error });
|
|
57
51
|
return Promise.reject(error);
|
|
58
52
|
}).then((results) => {
|
|
59
|
-
return results.
|
|
53
|
+
return results.response.ok ? results : Promise.reject(results);
|
|
60
54
|
});
|
|
61
55
|
}
|
|
62
56
|
|
|
63
57
|
#prompt() {
|
|
64
58
|
this.#readline.setPrompt(this.#configClient.get('prompt'));
|
|
65
|
-
this.#readline.prompt();
|
|
59
|
+
this.#readline.prompt(true);
|
|
66
60
|
return this;
|
|
67
61
|
}
|
|
68
62
|
|
|
69
|
-
#get(key, ...rest) {
|
|
70
|
-
const { config } = this.#configClient.toObject();
|
|
71
|
-
const value = get(config, key);
|
|
72
|
-
|
|
73
|
-
if (value?.request) {
|
|
74
|
-
const $request = FetchService.decorateRequest(this.#mergeData, key, value.request);
|
|
75
|
-
const request = this.#configClient.set(resolveSymbol, $request).get(resolveSymbol);
|
|
76
|
-
this.#configClient.del(resolveSymbol);
|
|
77
|
-
return Merge({}, value, { request });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return this.#configClient.get(key, ...rest);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
63
|
#createCLI() {
|
|
84
64
|
const self = this;
|
|
85
65
|
|
|
86
66
|
this.#cli = new Proxy({
|
|
87
67
|
run: (...args) => this.#run(...args),
|
|
88
|
-
get: (...args) => this.#get(...args),
|
|
68
|
+
get: (...args) => this.#configClient.get(...args),
|
|
89
69
|
set: (key = '', value = null) => this.#configClient.set(key, value),
|
|
90
70
|
del: (key = '') => this.#configClient.del(key),
|
|
91
|
-
curl: key => FetchService.toCURL(this.#get(key, {}).request),
|
|
71
|
+
curl: key => FetchService.toCURL(this.#configClient.get(key, {}).request),
|
|
92
72
|
quit: () => process.exit(),
|
|
93
73
|
}, {
|
|
94
74
|
get(obj, prop, receiver) {
|
|
@@ -96,9 +76,12 @@ module.exports = class Sandman extends EventEmitter {
|
|
|
96
76
|
|
|
97
77
|
if (typeof value === 'function') {
|
|
98
78
|
return (...args) => {
|
|
79
|
+
self.#cliCounter++;
|
|
99
80
|
self.#readline.pause();
|
|
100
|
-
const result = value(
|
|
101
|
-
Promise.resolve().
|
|
81
|
+
const result = value.apply(this, args);
|
|
82
|
+
Promise.resolve(result).catch(() => null).finally(() => setImmediate(() => {
|
|
83
|
+
if (--self.#cliCounter === 0) self.#prompt();
|
|
84
|
+
}));
|
|
102
85
|
return result;
|
|
103
86
|
};
|
|
104
87
|
}
|
|
@@ -107,121 +90,4 @@ module.exports = class Sandman extends EventEmitter {
|
|
|
107
90
|
},
|
|
108
91
|
});
|
|
109
92
|
}
|
|
110
|
-
|
|
111
|
-
#createInterface() {
|
|
112
|
-
this.#readline = Readline.createInterface({
|
|
113
|
-
input: process.stdin,
|
|
114
|
-
output: process.stdout,
|
|
115
|
-
terminal: true,
|
|
116
|
-
completer: (line) => {
|
|
117
|
-
// Show available CLI commands
|
|
118
|
-
if (!line) return [Object.keys(this.#cli), line];
|
|
119
|
-
|
|
120
|
-
const tokens = line.split(' ');
|
|
121
|
-
const lastToken = tokens.at(-1);
|
|
122
|
-
const paths = lastToken.split('.');
|
|
123
|
-
const path = paths.at(-1);
|
|
124
|
-
|
|
125
|
-
// Specific request.data selector
|
|
126
|
-
if (lastToken.startsWith('.')) {
|
|
127
|
-
const api = this.#get(tokens.at(-2));
|
|
128
|
-
if (!api?.request) return [[], path];
|
|
129
|
-
const dataPath = ['request'].concat(paths.slice(1, -1)).join('.');
|
|
130
|
-
const data = get(api, dataPath, {});
|
|
131
|
-
return [Object.keys(data).filter(k => k.toLowerCase().startsWith(path.toLowerCase())), path];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
const flatKeys = Object.keys(flatten(this.#configClient.get()));
|
|
136
|
-
|
|
137
|
-
// These keys follow the typing of the user
|
|
138
|
-
const startsWithCandidates = Array.from(new Set(flatKeys.map((flatKey) => {
|
|
139
|
-
return flatKey.split('.').slice(0, paths.length).join('.');
|
|
140
|
-
}))).filter((c) => {
|
|
141
|
-
return c.toLowerCase().startsWith(lastToken.toLowerCase());
|
|
142
|
-
}); // .map(p => p.split('.').at(-1)); // Here!
|
|
143
|
-
|
|
144
|
-
// These are shortcut keys to requests
|
|
145
|
-
const requestKeyCandidates = Array.from(new Set(flatKeys.map((flatKey) => {
|
|
146
|
-
const keys = flatKey.split('.');
|
|
147
|
-
const index = keys.indexOf('request');
|
|
148
|
-
return index && flatKey.split('.').slice(0, index).join('.');
|
|
149
|
-
// const typedPath = keys.slice(0, paths.length - 1).join('.');
|
|
150
|
-
// const autocompletePath = keys.slice(paths.length - 1, index).join('.');
|
|
151
|
-
// return index > 0 && lastToken.toLowerCase().startsWith(typedPath.toLowerCase()) && autocompletePath;
|
|
152
|
-
}).filter(Boolean))).filter((c) => {
|
|
153
|
-
return c.toLowerCase().includes(lastToken.toLowerCase());
|
|
154
|
-
// return c.toLowerCase().includes(path.toLowerCase());
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const candidates = Array.from(new Set(startsWithCandidates.concat(requestKeyCandidates)));
|
|
158
|
-
if (this.#captureCandidates) { this.#candidates = candidates; this.#line = line; this.#lastToken = lastToken; this.#candidateIndex = -1; }
|
|
159
|
-
if (candidates.length === 1 && candidates[0] === lastToken) return [[], lastToken];
|
|
160
|
-
return [candidates, lastToken];
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
process.stdin.on('keypress', (ch, key) => {
|
|
165
|
-
if (key && key.name === 'tab') this.#tabCounter++; else this.#tabCounter = 0;
|
|
166
|
-
this.#captureCandidates = this.#tabCounter === 2;
|
|
167
|
-
|
|
168
|
-
if (key && key.name === 'escape') {
|
|
169
|
-
this.#readline.line = '';
|
|
170
|
-
Readline.cursorTo(process.stdout, 0);
|
|
171
|
-
Readline.clearLine(process.stdout, 0);
|
|
172
|
-
this.#readline.prompt();
|
|
173
|
-
} else if (this.#tabCounter > 2 && this.#candidates.length) {
|
|
174
|
-
const candidate = this.#candidates[this.#candidateIndex = ++this.#candidateIndex % this.#candidates.length];
|
|
175
|
-
const value = this.#line.replace(this.#lastToken, candidate);
|
|
176
|
-
this.#readline.line = value;
|
|
177
|
-
this.#readline.cursor = value.length;
|
|
178
|
-
this.#readline.prompt(true);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
#createWatcher() {
|
|
184
|
-
this.#watcher = Chokidar.watch(this.#configDir, {
|
|
185
|
-
awaitWriteFinish: true,
|
|
186
|
-
ignoreInitial: true,
|
|
187
|
-
ignored: filepath => this.#ignore(this.#normalizeWatcherPath(filepath)),
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
this.#watcher.on('all', (event, path) => {
|
|
191
|
-
const { key } = this.#normalizeWatcherPath(path);
|
|
192
|
-
|
|
193
|
-
if (['add', 'change'].includes(event)) {
|
|
194
|
-
const api = ConfigClient.parseFile(path);
|
|
195
|
-
if (key) this.#configClient.set(key, api);
|
|
196
|
-
else this.#configClient.merge(api); // index.yaml
|
|
197
|
-
if (api.request) this.emit('save', { key, api });
|
|
198
|
-
this.#prompt();
|
|
199
|
-
} else if (['unlink', 'unlinkDir'].includes(event)) {
|
|
200
|
-
this.#configClient.del(key);
|
|
201
|
-
this.#prompt();
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
#ignore({ name, filepath, paths }) {
|
|
207
|
-
if (name.startsWith('.')) return true;
|
|
208
|
-
|
|
209
|
-
if (name.startsWith('+')) {
|
|
210
|
-
const request = ConfigClient.parseFile(filepath);
|
|
211
|
-
const key = paths.slice(0, -1).join('.');
|
|
212
|
-
if (key) this.#mergeData[key] = { request };
|
|
213
|
-
else this.#mergeData.request = request;
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
#normalizeWatcherPath = (filepath) => {
|
|
221
|
-
const parsed = Path.parse(filepath);
|
|
222
|
-
const folder = filepath.substring(this.#configDir.length + 1, filepath.length - parsed.ext.length);
|
|
223
|
-
const paths = folder.split('/').filter(el => el && el !== 'index');
|
|
224
|
-
const key = paths.join('.');
|
|
225
|
-
return { ...parsed, filepath, paths, key };
|
|
226
|
-
};
|
|
227
93
|
};
|