hello-lights 0.3.6 → 0.3.8
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/README.md +6 -2
- package/lib/cli/commander-options.js +11 -3
- package/lib/cli/repl-command.js +12 -12
- package/lib/cli/serve/index.js +4 -4
- package/lib/commander.js +11 -3
- package/lib/index.js +3 -1
- package/lib/rest-commander.js +126 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,9 +34,11 @@ $ hello-lights exec-file ./cmds.clj # execute commands from a file
|
|
|
34
34
|
$ hello-lights repl # start an interactive REPL
|
|
35
35
|
$ hello-lights serve # start the HTTP server on port 9000
|
|
36
36
|
$ hello-lights serve --port 3000 # start the HTTP server on a custom port
|
|
37
|
+
$ hello-lights --selector http exec bounce 300 # execute via a remote server
|
|
38
|
+
$ hello-lights --selector http --host http://myserver:3000 repl # REPL via a remote server
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
Use `--help` for the full list of options, including `--serial-num` to target a specific device
|
|
41
|
+
Use `--help` for the full list of options, including `--serial-num` to target a specific device, `--selector multi` to control multiple traffic lights at once, and `--selector http` to send commands to a remote hello-lights server.
|
|
40
42
|
|
|
41
43
|
### Library
|
|
42
44
|
|
|
@@ -95,9 +97,11 @@ $ npm run cli -- exec-file ./cmds.clj # execute commands from a file
|
|
|
95
97
|
$ npm run cli -- repl # start an interactive REPL
|
|
96
98
|
$ npm run cli -- serve # start the HTTP server on port 9000
|
|
97
99
|
$ npm run cli -- serve --port 3000 # start the HTTP server on a custom port
|
|
100
|
+
$ npm run cli -- --selector http exec bounce 300 # execute via a remote server
|
|
101
|
+
$ npm run cli -- --selector http --host http://myserver:3000 repl # REPL via a remote server
|
|
98
102
|
```
|
|
99
103
|
|
|
100
|
-
Use `--help` for the full list of options, including `--serial-num` to target a specific device
|
|
104
|
+
Use `--help` for the full list of options, including `--serial-num` to target a specific device, `--selector multi` to control multiple traffic lights at once, and `--selector http` to send commands to a remote hello-lights server.
|
|
101
105
|
|
|
102
106
|
## HTTP Server REST API
|
|
103
107
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const {Commander} = require('..');
|
|
3
|
+
const {Commander, RestCommander} = require('..');
|
|
4
4
|
const {MetaFormatter, CodeFormatter} = require('..').commands;
|
|
5
5
|
|
|
6
6
|
/////////////////////////////////////////////////////////////////
|
|
@@ -87,6 +87,9 @@ function resolveDeviceManager(options) {
|
|
|
87
87
|
/////////////////////////////////////////////////////////////////
|
|
88
88
|
|
|
89
89
|
function resolveCommander(options) {
|
|
90
|
+
if (options.selector === 'http') {
|
|
91
|
+
return new RestCommander({host: options.host, logger});
|
|
92
|
+
}
|
|
90
93
|
return Commander[options.selector]({
|
|
91
94
|
logger,
|
|
92
95
|
formatter: new ChalkMetaFormatter(),
|
|
@@ -116,8 +119,13 @@ function define(yargs) {
|
|
|
116
119
|
.option('selector', {
|
|
117
120
|
alias: 's',
|
|
118
121
|
describe: 'selector type to use',
|
|
119
|
-
choices: ['single', 'multi'],
|
|
120
|
-
default: 'single' })
|
|
122
|
+
choices: ['single', 'multi', 'http'],
|
|
123
|
+
default: 'single' })
|
|
124
|
+
.option('host', {
|
|
125
|
+
alias: 'H',
|
|
126
|
+
describe: 'server URL for --selector http',
|
|
127
|
+
default: 'http://localhost:9000',
|
|
128
|
+
type: 'string' });
|
|
121
129
|
}
|
|
122
130
|
|
|
123
131
|
/////////////////////////////////////////////////////////////////
|
package/lib/cli/repl-command.js
CHANGED
|
@@ -13,8 +13,8 @@ class CommanderRepl {
|
|
|
13
13
|
this.manager = this.commander.manager;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
formatCommandNames() {
|
|
17
|
-
let names = this.commander.
|
|
16
|
+
async formatCommandNames() {
|
|
17
|
+
let names = await this.commander.fetchCommandNames();
|
|
18
18
|
let parts = [' '];
|
|
19
19
|
names.forEach((name, i) => {
|
|
20
20
|
parts.push(` ${name}`);
|
|
@@ -23,7 +23,7 @@ class CommanderRepl {
|
|
|
23
23
|
return parts.join('');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
help(commandName) {
|
|
26
|
+
async help(commandName) {
|
|
27
27
|
if (commandName === undefined) {
|
|
28
28
|
this.logger.log([
|
|
29
29
|
`Commands for the traffic light`,
|
|
@@ -34,30 +34,30 @@ class CommanderRepl {
|
|
|
34
34
|
`> [command]`,
|
|
35
35
|
`> { [... multi line command] }`,
|
|
36
36
|
` available commands:`,
|
|
37
|
-
this.formatCommandNames()
|
|
37
|
+
await this.formatCommandNames()
|
|
38
38
|
].join('\n'));
|
|
39
39
|
} else {
|
|
40
|
-
this.commander.help(commandName);
|
|
40
|
+
await this.commander.help(commandName);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
execute(text, context, filename, callback) {
|
|
44
|
+
async execute(text, context, filename, callback) {
|
|
45
45
|
text = text.trim();
|
|
46
46
|
let match;
|
|
47
47
|
if (!text) {
|
|
48
48
|
} else if (text === 'cancel') {
|
|
49
|
-
this.commander.cancel();
|
|
49
|
+
await this.commander.cancel();
|
|
50
50
|
} else if (text === 'help') {
|
|
51
|
-
this.help();
|
|
51
|
+
await this.help();
|
|
52
52
|
} else if (match = text.match(/^help\s+(.+)/)) { // eslint-disable-line no-cond-assign
|
|
53
|
-
this.help(match[1]);
|
|
53
|
+
await this.help(match[1]);
|
|
54
54
|
} else if (text === 'exit' || text === 'quit') {
|
|
55
|
-
this.commander.cancel();
|
|
55
|
+
await this.commander.cancel();
|
|
56
56
|
this.commander.close();
|
|
57
57
|
this.logger.log('Bye');
|
|
58
58
|
process.exit(0);
|
|
59
59
|
} else if (text === 'check device') {
|
|
60
|
-
this.commander.logInfo();
|
|
60
|
+
await this.commander.logInfo();
|
|
61
61
|
} else if (this.supportsNewDevice() && text === 'new device') {
|
|
62
62
|
this.newDevice();
|
|
63
63
|
} else if (text.startsWith('{')) {
|
|
@@ -84,7 +84,7 @@ class CommanderRepl {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
supportsNewDevice() {
|
|
87
|
-
return !!this.manager.newDevice;
|
|
87
|
+
return !!this.manager && !!this.manager.newDevice;
|
|
88
88
|
}
|
|
89
89
|
newDevice() {
|
|
90
90
|
this.manager.newDevice();
|
package/lib/cli/serve/index.js
CHANGED
|
@@ -18,8 +18,8 @@ function createApp(commander) {
|
|
|
18
18
|
res.sendStatus(202);
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
app.post('/cancel', (req, res) => {
|
|
22
|
-
commander.cancel();
|
|
21
|
+
app.post('/cancel', async (req, res) => {
|
|
22
|
+
await commander.cancel();
|
|
23
23
|
res.sendStatus(200);
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -28,8 +28,8 @@ function createApp(commander) {
|
|
|
28
28
|
res.sendStatus(202);
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
app.get('/commands', (req, res) => {
|
|
32
|
-
res.json(commander.
|
|
31
|
+
app.get('/commands', async (req, res) => {
|
|
32
|
+
res.json(await commander.fetchCommandNames());
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
app.get('/commands/:name', (req, res) => {
|
package/lib/commander.js
CHANGED
|
@@ -89,7 +89,7 @@ class Commander {
|
|
|
89
89
|
/**
|
|
90
90
|
* Cancels any currently executing command.
|
|
91
91
|
*/
|
|
92
|
-
cancel() {
|
|
92
|
+
async cancel() {
|
|
93
93
|
this.interpreter.cancel();
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -248,11 +248,19 @@ class Commander {
|
|
|
248
248
|
return this.interpreter.commands;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Fetches all supported command names.
|
|
253
|
+
* @returns {Promise<string[]>} A promise that resolves with the command names.
|
|
254
|
+
*/
|
|
255
|
+
async fetchCommandNames() {
|
|
256
|
+
return this.interpreter.commandNames;
|
|
257
|
+
}
|
|
258
|
+
|
|
251
259
|
/**
|
|
252
260
|
* Logs the help info for the given command name.
|
|
253
261
|
* @param {string} commandName - Name of the command to log help info.
|
|
254
262
|
*/
|
|
255
|
-
help(commandName) {
|
|
263
|
+
async help(commandName) {
|
|
256
264
|
let command = this.interpreter.lookup(commandName);
|
|
257
265
|
if (!command) {
|
|
258
266
|
this.logger.error(`Command not found: "${commandName}"`);
|
|
@@ -264,7 +272,7 @@ class Commander {
|
|
|
264
272
|
/**
|
|
265
273
|
* Logs information about known traffic lights.
|
|
266
274
|
*/
|
|
267
|
-
logInfo() {
|
|
275
|
+
async logInfo() {
|
|
268
276
|
this.selector.logInfo(this.logger);
|
|
269
277
|
}
|
|
270
278
|
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const {Commander} = require('./commander');
|
|
2
|
+
const {RestCommander} = require('./rest-commander');
|
|
2
3
|
|
|
3
4
|
module.exports = {
|
|
4
5
|
commands: require('./commands'),
|
|
@@ -6,5 +7,6 @@ module.exports = {
|
|
|
6
7
|
physical: require('./physical'),
|
|
7
8
|
devices: require('./devices'),
|
|
8
9
|
selectors: require('./selectors'),
|
|
9
|
-
Commander
|
|
10
|
+
Commander,
|
|
11
|
+
RestCommander
|
|
10
12
|
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
////////////////////////////////////////////////
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
|
|
6
|
+
////////////////////////////////////////////////
|
|
7
|
+
|
|
8
|
+
function request(baseUrl, method, path, body) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
let url = new URL(path, baseUrl);
|
|
11
|
+
let client = url.protocol === 'https:' ? https : http;
|
|
12
|
+
let options = {
|
|
13
|
+
method,
|
|
14
|
+
hostname: url.hostname,
|
|
15
|
+
port: url.port,
|
|
16
|
+
path: url.pathname + url.search,
|
|
17
|
+
headers: {}
|
|
18
|
+
};
|
|
19
|
+
if (body != null) {
|
|
20
|
+
options.headers['Content-Type'] = 'text/plain';
|
|
21
|
+
}
|
|
22
|
+
let req = client.request(options, (res) => {
|
|
23
|
+
let data = '';
|
|
24
|
+
res.on('data', chunk => data += chunk);
|
|
25
|
+
res.on('end', () => resolve({statusCode: res.statusCode, body: data}));
|
|
26
|
+
});
|
|
27
|
+
req.on('error', reject);
|
|
28
|
+
if (body != null) req.write(body);
|
|
29
|
+
req.end();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
////////////////////////////////////////////////
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A Commander client that controls a traffic light via a remote REST API.
|
|
37
|
+
* Communicates with a server started by the `serve` command.
|
|
38
|
+
*/
|
|
39
|
+
class RestCommander {
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates a new RestCommander instance.
|
|
43
|
+
* @param {object} [options] - Options.
|
|
44
|
+
* @param {string} [options.host='http://localhost:9000'] - The base URL of
|
|
45
|
+
* the remote server (e.g. `http://localhost:9000`).
|
|
46
|
+
* @param {object} [options.logger=console] - A Console-like object for logging.
|
|
47
|
+
*/
|
|
48
|
+
constructor({host = 'http://localhost:9000', logger = console} = {}) {
|
|
49
|
+
this.host = host.replace(/\/+$/, ''); // strip trailing slashes
|
|
50
|
+
this.logger = logger;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Executes a command on the remote server (fire-and-forget).
|
|
55
|
+
* @param {string} command - Command to execute.
|
|
56
|
+
* @param {boolean} [reset=false] - Whether to reset the traffic light first.
|
|
57
|
+
*/
|
|
58
|
+
async run(command, reset = false) {
|
|
59
|
+
let path = reset ? '/run?reset=true' : '/run';
|
|
60
|
+
await request(this.host, 'POST', path, command);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Cancels any currently executing command on the remote server.
|
|
65
|
+
*/
|
|
66
|
+
async cancel() {
|
|
67
|
+
await request(this.host, 'POST', '/cancel');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Executes definition-only commands on the remote server.
|
|
72
|
+
* @param {string} command - Command with definitions to execute.
|
|
73
|
+
*/
|
|
74
|
+
async runDefinitions(command) {
|
|
75
|
+
await request(this.host, 'POST', '/definitions', command);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Fetches all available command names from the remote server.
|
|
80
|
+
* @returns {Promise<string[]>} Array of command names.
|
|
81
|
+
*/
|
|
82
|
+
async fetchCommandNames() {
|
|
83
|
+
let res = await request(this.host, 'GET', '/commands');
|
|
84
|
+
return JSON.parse(res.body);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Logs the help info for the given command name from the remote server.
|
|
89
|
+
* @param {string} commandName - Name of the command.
|
|
90
|
+
*/
|
|
91
|
+
async help(commandName) {
|
|
92
|
+
let res = await request(this.host, 'GET', `/commands/${encodeURIComponent(commandName)}`);
|
|
93
|
+
if (res.statusCode === 404) {
|
|
94
|
+
this.logger.error(res.body);
|
|
95
|
+
} else {
|
|
96
|
+
this.logger.log(res.body);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Logs information about known traffic lights from the remote server.
|
|
102
|
+
*/
|
|
103
|
+
async logInfo() {
|
|
104
|
+
let res = await request(this.host, 'GET', '/info');
|
|
105
|
+
let devicesInfo = JSON.parse(res.body);
|
|
106
|
+
if (devicesInfo.length === 0) {
|
|
107
|
+
this.logger.log('No devices found');
|
|
108
|
+
} else {
|
|
109
|
+
this.logger.log('Known devices:');
|
|
110
|
+
devicesInfo.forEach(info =>
|
|
111
|
+
this.logger.log(`device ${info.serialNum}: ${info.status}`));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Closes this instance. No-op for REST client.
|
|
117
|
+
*/
|
|
118
|
+
close() {}
|
|
119
|
+
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
////////////////////////////////////////////////
|
|
123
|
+
|
|
124
|
+
module.exports = {RestCommander};
|
|
125
|
+
|
|
126
|
+
////////////////////////////////////////////////
|