hello-lights 0.3.1 → 0.3.2

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.
@@ -0,0 +1,116 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [master]
6
+ push:
7
+ branches: [master]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-node@v4
20
+ with:
21
+ node-version: 22
22
+
23
+ - name: Install system dependencies
24
+ run: sudo apt-get update && sudo apt-get install -y libusb-1.0-0-dev libudev-dev
25
+
26
+ - run: npm ci
27
+
28
+ - run: npm run lint
29
+
30
+ - run: npm run coverage
31
+
32
+ - run: npm run coverage:text
33
+
34
+ - run: npm run build:web
35
+
36
+ - name: Upload web artifact
37
+ if: github.event_name == 'push'
38
+ uses: actions/upload-pages-artifact@v3
39
+ with:
40
+ path: web
41
+
42
+ deploy:
43
+ needs: build
44
+ if: github.event_name == 'push'
45
+ runs-on: ubuntu-latest
46
+
47
+ permissions:
48
+ pages: write
49
+ id-token: write
50
+
51
+ environment:
52
+ name: github-pages
53
+ url: ${{ steps.deploy.outputs.page_url }}
54
+
55
+ steps:
56
+ - name: Deploy to GitHub Pages
57
+ id: deploy
58
+ uses: actions/deploy-pages@v4
59
+
60
+ publish:
61
+ needs: build
62
+ if: github.event_name == 'push' && !contains(github.event.head_commit.message, '[skip ci]')
63
+ runs-on: ubuntu-latest
64
+
65
+ permissions:
66
+ contents: write
67
+ id-token: write
68
+
69
+ steps:
70
+ - uses: actions/checkout@v4
71
+ with:
72
+ token: ${{ secrets.GITHUB_TOKEN }}
73
+
74
+ - uses: actions/setup-node@v4
75
+ with:
76
+ node-version: 22
77
+ registry-url: https://registry.npmjs.org
78
+
79
+ - name: Install system dependencies
80
+ run: sudo apt-get update && sudo apt-get install -y libusb-1.0-0-dev libudev-dev
81
+
82
+ - run: npm ci
83
+
84
+ - run: npm install -g npm@latest
85
+
86
+ - name: Determine version bump type
87
+ id: bump
88
+ run: |
89
+ COMMIT_MSG="${{ github.event.head_commit.message }}"
90
+ if echo "$COMMIT_MSG" | grep -qiE '\[major\]|BREAKING CHANGE'; then
91
+ echo "type=major" >> $GITHUB_OUTPUT
92
+ elif echo "$COMMIT_MSG" | grep -qi '\[minor\]'; then
93
+ echo "type=minor" >> $GITHUB_OUTPUT
94
+ else
95
+ echo "type=patch" >> $GITHUB_OUTPUT
96
+ fi
97
+
98
+ - name: Bump version
99
+ id: version
100
+ run: |
101
+ git config user.name "github-actions[bot]"
102
+ git config user.email "github-actions[bot]@users.noreply.github.com"
103
+ npm version ${{ steps.bump.outputs.type }} -m "v%s [skip ci]"
104
+ echo "new_version=$(node -p 'require("./package.json").version')" >> $GITHUB_OUTPUT
105
+
106
+ - name: Push version bump
107
+ run: git push --follow-tags
108
+
109
+ - name: Publish to npm
110
+ run: npm publish --provenance
111
+
112
+ - name: Create GitHub Release
113
+ uses: softprops/action-gh-release@v2
114
+ with:
115
+ tag_name: v${{ steps.version.outputs.new_version }}
116
+ generate_release_notes: true
package/README.md CHANGED
@@ -1,9 +1,7 @@
1
1
  # hello-lights
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/hello-lights.svg)](https://www.npmjs.com/package/hello-lights)
4
- [![Build Status](https://travis-ci.org/jordao76/hello-lights.svg)](https://travis-ci.org/jordao76/hello-lights)
5
- [![Dependency Status](https://david-dm.org/jordao76/hello-lights.svg)](https://david-dm.org/jordao76/hello-lights)
6
- [![devDependency Status](https://david-dm.org/jordao76/hello-lights/dev-status.svg)](https://david-dm.org/jordao76/hello-lights#info=devDependencies)
4
+ [![CI](https://github.com/jordao76/hello-lights/actions/workflows/ci.yml/badge.svg)](https://github.com/jordao76/hello-lights/actions/workflows/ci.yml)
7
5
  [![License](http://img.shields.io/:license-mit-blue.svg)](https://github.com/jordao76/hello-lights/blob/master/LICENSE.md)
8
6
 
9
7
  > Commands to control a traffic light
@@ -39,6 +37,52 @@ Check out the available commands [here](https://jordao76.github.io/hello-lights)
39
37
 
40
38
  For the documentation look [here](https://jordao76.github.io/hello-lights/doc/index.html).
41
39
 
40
+ ## Development
41
+
42
+ Install dependencies:
43
+
44
+ ```sh
45
+ $ npm install
46
+ ```
47
+
48
+ ### npm scripts
49
+
50
+ | Script | Description |
51
+ |---|---|
52
+ | `npm test` | Run all tests (generates PEG parsers first) |
53
+ | `npm run lint` | Lint source and test files |
54
+ | `npm run coverage` | Run tests with coverage instrumentation |
55
+ | `npm run coverage:text` | Print a text coverage summary to the terminal |
56
+ | `npm run coverage:open` | Generate and open an HTML coverage report |
57
+ | `npm run build:doc` | Generate JSDoc documentation into `web/doc/` |
58
+ | `npm run build:web` | Build all web assets (PEG parsers, browserify bundle, docs) |
59
+ | `npm run doc` | Build and open the documentation in the browser |
60
+ | `npm run web` | Build and open the browser demo |
61
+ | `npm run mocha-grep <pattern>` | Run only tests matching a pattern |
62
+ | `npm run cli` | Run the CLI locally |
63
+
64
+ ### CLI
65
+
66
+ Run the CLI locally with `npm run cli`:
67
+
68
+ ```sh
69
+ $ npm run cli -- exec bounce 300 # execute a command
70
+ $ npm run cli -- exec-file ./cmds.clj # execute commands from a file
71
+ $ npm run cli -- repl # start an interactive REPL
72
+ $ npm run cli -- serve # start the HTTP server on port 9000
73
+ $ npm run cli -- serve --port 3000 # start the HTTP server on a custom port
74
+ ```
75
+
76
+ Use `--help` for the full list of options, including `--serial-num` to target a specific device and `--selector multi` to control multiple traffic lights at once.
77
+
78
+ ## CI
79
+
80
+ The CI workflow runs on every push and pull request to `master`. It has three jobs:
81
+
82
+ - **Build** -- lints, runs tests with coverage, and builds all web assets.
83
+ - **Deploy** -- on push to `master`, deploys the `web/` directory to GitHub Pages.
84
+ - **Publish** -- on push to `master`, bumps the package version, publishes to npm with provenance via trusted publishing, and creates a GitHub Release. The version bump type is determined from the commit message: `[major]` or `BREAKING CHANGE` for major, `[minor]` for minor, and patch by default.
85
+
42
86
  ## License
43
87
 
44
88
  Licensed under the [MIT license](https://github.com/jordao76/hello-lights/blob/master/LICENSE.md).
package/cli.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ require('./lib/cli').parse();
3
+
@@ -0,0 +1,127 @@
1
+ const chalk = require('chalk');
2
+ const path = require('path');
3
+ const {Commander} = require('..');
4
+ const {MetaFormatter, CodeFormatter} = require('..').commands;
5
+
6
+ /////////////////////////////////////////////////////////////////
7
+
8
+ const logger = {
9
+ log: (...args) => {
10
+ console.log(chalk.gray(...args));
11
+ },
12
+ error: (...args) => {
13
+ console.error(chalk.red(...args));
14
+ }
15
+ };
16
+
17
+ /////////////////////////////////////////////////////////////////
18
+
19
+ class ChalkCodeFormatter extends CodeFormatter {
20
+
21
+ formatCommand(text) {
22
+ return chalk.blue(text);
23
+ }
24
+
25
+ formatIdentifier(text) {
26
+ if (['red', 'yellow', 'green'].indexOf(text) >= 0) return chalk[text](text);
27
+ return chalk.blue(text);
28
+ }
29
+
30
+ formatVariable(text) {
31
+ return chalk.green(text);
32
+ }
33
+
34
+ formatNumber(text) {
35
+ return chalk.magenta(text);
36
+ }
37
+
38
+ formatString(text) {
39
+ return chalk.cyan(text);
40
+ }
41
+
42
+ formatComment(text) {
43
+ return chalk.gray(text);
44
+ }
45
+
46
+ }
47
+
48
+ class ChalkMetaFormatter extends MetaFormatter {
49
+
50
+ constructor() {
51
+ super(new ChalkCodeFormatter());
52
+ }
53
+
54
+ formatName(name) {
55
+ return chalk.yellow(name);
56
+ }
57
+
58
+ formatParam(param) {
59
+ return chalk.cyan(super.formatParam(param));
60
+ }
61
+
62
+ formatReturn($return) {
63
+ return chalk.magenta(super.formatReturn($return));
64
+ }
65
+
66
+ formatInlineCode(code) {
67
+ return chalk.cyan(`${code.trim()}`);
68
+ }
69
+
70
+ }
71
+
72
+ /////////////////////////////////////////////////////////////////
73
+
74
+ function resolveDeviceManager(options) {
75
+ const clewareDevicePath = '../devices/cleware-switch1';
76
+ let devicePath;
77
+ if (options.devicePath) {
78
+ devicePath = path.resolve(options.devicePath);
79
+ } else {
80
+ // for now, it is always the case that: options.deviceType === 'cleware'
81
+ devicePath = clewareDevicePath;
82
+ }
83
+ const {Manager} = require(devicePath);
84
+ return Manager;
85
+ }
86
+
87
+ /////////////////////////////////////////////////////////////////
88
+
89
+ function resolveCommander(options) {
90
+ return Commander[options.selector]({
91
+ logger,
92
+ formatter: new ChalkMetaFormatter(),
93
+ manager: resolveDeviceManager(options),
94
+ serialNum: options.serialNum
95
+ });
96
+ }
97
+
98
+ /////////////////////////////////////////////////////////////////
99
+
100
+ function define(yargs) {
101
+ yargs
102
+ .option('device-type', {
103
+ alias: 'd',
104
+ describe: 'device type to use',
105
+ choices: ['cleware'],
106
+ default: 'cleware',
107
+ hidden: true }) // un-hide when more than one option exists
108
+ .option('device-path', {
109
+ alias: 'p',
110
+ describe: 'device type path to use, overrides --device',
111
+ normalize: true,
112
+ hidden: true }) // hide advanced option
113
+ .option('serial-num', {
114
+ alias: 'n',
115
+ describe: 'serial number of device to use (only for "single" selector)' })
116
+ .option('selector', {
117
+ alias: 's',
118
+ describe: 'selector type to use',
119
+ choices: ['single', 'multi'],
120
+ default: 'single' });
121
+ }
122
+
123
+ /////////////////////////////////////////////////////////////////
124
+
125
+ module.exports = {define, resolveCommander};
126
+
127
+ /////////////////////////////////////////////////////////////////
@@ -0,0 +1,42 @@
1
+ /////////////////////////////////////////////////////////////////
2
+
3
+ const commanderOptions = require('./commander-options');
4
+
5
+ /////////////////////////////////////////////////////////////////
6
+
7
+ async function exec(options, cmd, cdr = []) {
8
+ let commander = commanderOptions.resolveCommander(options);
9
+ cdr.unshift(cmd);
10
+ let command = cdr.join(' ');
11
+ await commander.run(command);
12
+ commander.close();
13
+ }
14
+
15
+ /////////////////////////////////////////////////////////////////
16
+
17
+ const commandSpec = {
18
+
19
+ command: 'exec <cmd>',
20
+ describe: 'executes a command',
21
+
22
+ builder: yargs =>
23
+ yargs.positional('cmd', { describe: 'command to execute' }),
24
+
25
+ handler: argv =>
26
+ exec(argv, argv.cmd, argv._.slice(1)) // argv._ includes 'exec' at index 0
27
+
28
+ };
29
+
30
+ /////////////////////////////////////////////////////////////////
31
+
32
+ function define(yargs) {
33
+ yargs
34
+ .command(commandSpec)
35
+ .example('$0 exec bounce 300', '# executes the `bounce 300` command');
36
+ };
37
+
38
+ /////////////////////////////////////////////////////////////////
39
+
40
+ module.exports = {define};
41
+
42
+ /////////////////////////////////////////////////////////////////
@@ -0,0 +1,40 @@
1
+ /////////////////////////////////////////////////////////////////
2
+
3
+ const commanderOptions = require('./commander-options');
4
+
5
+ /////////////////////////////////////////////////////////////////
6
+
7
+ async function execFile(options, filePath) {
8
+ let commander = commanderOptions.resolveCommander(options);
9
+ await commander.runFile(filePath);
10
+ commander.close();
11
+ }
12
+
13
+ /////////////////////////////////////////////////////////////////
14
+
15
+ const commandSpec = {
16
+
17
+ command: 'exec-file <file-path>',
18
+ describe: 'executes commands in a file',
19
+
20
+ builder: yargs =>
21
+ yargs.positional('file-path', { describe: 'file to execute' }),
22
+
23
+ handler: argv =>
24
+ execFile(argv, argv.filePath)
25
+
26
+ };
27
+
28
+ /////////////////////////////////////////////////////////////////
29
+
30
+ function define(yargs) {
31
+ yargs
32
+ .command(commandSpec)
33
+ .example('$0 exec-file ./my-file.clj', '# executes the file');
34
+ };
35
+
36
+ /////////////////////////////////////////////////////////////////
37
+
38
+ module.exports = {define};
39
+
40
+ /////////////////////////////////////////////////////////////////
@@ -0,0 +1 @@
1
+ module.exports = require('./yargs');
@@ -0,0 +1,127 @@
1
+ /////////////////////////////////////////////////////////////////
2
+
3
+ const repl = require('repl');
4
+
5
+ /////////////////////////////////////////////////////////////////
6
+
7
+ class CommanderRepl {
8
+
9
+ constructor(commander) {
10
+ this.commander = commander;
11
+ this.multiline = false;
12
+ this.logger = this.commander.logger;
13
+ this.manager = this.commander.manager;
14
+ }
15
+
16
+ formatCommandNames() {
17
+ let names = this.commander.commandNames;
18
+ let parts = [' '];
19
+ names.forEach((name, i) => {
20
+ parts.push(` ${name}`);
21
+ if ((i + 1) % 8 === 0) parts.push('\n ');
22
+ });
23
+ return parts.join('');
24
+ }
25
+
26
+ help(commandName) {
27
+ if (commandName === undefined) {
28
+ this.logger.log([
29
+ `Commands for the traffic light`,
30
+ `> help`,
31
+ `> help [command name]`,
32
+ `> check device` + (this.supportsNewDevice() ? '\n> new device' : ''),
33
+ `> exit | quit`,
34
+ `> [command]`,
35
+ `> { [... multi line command] }`,
36
+ ` available commands:`,
37
+ this.formatCommandNames()
38
+ ].join('\n'));
39
+ } else {
40
+ this.commander.help(commandName);
41
+ }
42
+ }
43
+
44
+ execute(text, context, filename, callback) {
45
+ text = text.trim();
46
+ let match;
47
+ if (!text) {
48
+ } else if (text === 'cancel') {
49
+ this.commander.cancel();
50
+ } else if (text === 'help') {
51
+ this.help();
52
+ } else if (match = text.match(/^help\s+(.+)/)) { // eslint-disable-line no-cond-assign
53
+ this.help(match[1]);
54
+ } else if (text === 'exit' || text === 'quit') {
55
+ this.commander.cancel();
56
+ this.commander.close();
57
+ this.logger.log('Bye');
58
+ process.exit(0);
59
+ } else if (text === 'check device') {
60
+ this.commander.logInfo();
61
+ } else if (this.supportsNewDevice() && text === 'new device') {
62
+ this.newDevice();
63
+ } else if (text.startsWith('{')) {
64
+ this.multiline = true;
65
+ return this.execute(text.replace('{', ''), context, filename, callback);
66
+ } else if (this.multiline && text.endsWith('}')) {
67
+ this.multiline = false;
68
+ this.commander.run(text.replace('}', ''));
69
+ } else if (this.multiline) {
70
+ return callback(new repl.Recoverable());
71
+ } else {
72
+ this.commander.run(text);
73
+ }
74
+ return callback();
75
+ }
76
+
77
+ launch() {
78
+ this.help();
79
+ let server = repl.start({
80
+ prompt: '> ',
81
+ eval: (...args) => this.execute(...args)
82
+ });
83
+ server.on('exit', () => process.exit(0));
84
+ }
85
+
86
+ supportsNewDevice() {
87
+ return !!this.manager.newDevice;
88
+ }
89
+ newDevice() {
90
+ this.manager.newDevice();
91
+ }
92
+
93
+ }
94
+
95
+ /////////////////////////////////////////////////////////////////
96
+
97
+ const commanderOptions = require('./commander-options');
98
+
99
+ /////////////////////////////////////////////////////////////////
100
+
101
+ function main(options) {
102
+ let commander = commanderOptions.resolveCommander(options);
103
+ let commanderRepl = new CommanderRepl(commander);
104
+ commanderRepl.launch();
105
+ }
106
+
107
+ /////////////////////////////////////////////////////////////////
108
+
109
+ const commandSpec = {
110
+ command: 'repl',
111
+ describe: 'starts the REPL',
112
+ handler: main
113
+ };
114
+
115
+ /////////////////////////////////////////////////////////////////
116
+
117
+ function define(yargs) {
118
+ yargs
119
+ .command(commandSpec)
120
+ .example('$0 repl', '# starts a REPL');
121
+ };
122
+
123
+ /////////////////////////////////////////////////////////////////
124
+
125
+ module.exports = {define, CommanderRepl};
126
+
127
+ /////////////////////////////////////////////////////////////////
@@ -0,0 +1,72 @@
1
+ /////////////////////////////////////////////////////////////////
2
+
3
+ const path = require('path');
4
+ const commanderOptions = require('../commander-options');
5
+
6
+ /////////////////////////////////////////////////////////////////
7
+
8
+ function createApp(commander) {
9
+ const express = require('express');
10
+ const app = express();
11
+
12
+ app.use(express.static(path.join(__dirname, 'public')));
13
+ app.post('/run', (req, res) => {
14
+ let commandStr = '';
15
+ req.on('data', data => commandStr += data);
16
+ req.on('end', () => {
17
+ commander.run(commandStr);
18
+ res.statusCode = 202; // accepted
19
+ res.end();
20
+ });
21
+ });
22
+
23
+ return app;
24
+ }
25
+
26
+ function serve(options) {
27
+ const commander = commanderOptions.resolveCommander(options);
28
+ const app = createApp(commander);
29
+ const server = app.listen(options.port, () => console.log(`Server listening on port ${options.port}`));
30
+ server.on('error', err => {
31
+ if (err.code === 'EADDRINUSE') {
32
+ console.error(`Error: port ${options.port} is already in use`);
33
+ } else {
34
+ console.error(`Error: ${err.message}`);
35
+ }
36
+ process.exit(1);
37
+ });
38
+ }
39
+
40
+ /////////////////////////////////////////////////////////////////
41
+
42
+ const commandSpec = {
43
+
44
+ command: 'serve',
45
+ describe: 'starts the HTTP server for remote light control',
46
+
47
+ builder: yargs =>
48
+ yargs.option('port', {
49
+ alias: 'P',
50
+ describe: 'port to listen on',
51
+ default: 9000,
52
+ type: 'number'
53
+ }),
54
+
55
+ handler: argv => serve(argv)
56
+
57
+ };
58
+
59
+ /////////////////////////////////////////////////////////////////
60
+
61
+ function define(yargs) {
62
+ yargs
63
+ .command(commandSpec)
64
+ .example('$0 serve', '# starts the server on port 9000')
65
+ .example('$0 serve --port 3000', '# starts the server on port 3000');
66
+ }
67
+
68
+ /////////////////////////////////////////////////////////////////
69
+
70
+ module.exports = {define, createApp};
71
+
72
+ /////////////////////////////////////////////////////////////////
@@ -0,0 +1,42 @@
1
+ <!doctype html>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <title>Hello Lights</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ <script src="main.js"></script>
9
+ </head>
10
+
11
+ <body>
12
+
13
+ <div class="container">
14
+
15
+ <div class="header">
16
+ <h2>hello-lights</h2>
17
+ </div>
18
+
19
+ <div class="command">
20
+ <div>
21
+ <textarea id="command" name="command" spellcheck="false"></textarea>
22
+ </div>
23
+ </div>
24
+
25
+ <div class="toolbar">
26
+ <div class="buttons">
27
+ <button id="run">Run</button>
28
+ <button id="cancel">Cancel</button>
29
+ <button id="reset">Reset</button>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="footer">
34
+ By <a href="https://github.com/jordao76">Jordão</a>.
35
+ Check it out on <a href="https://github.com/jordao76/hello-lights">GitHub</a>.
36
+ </div>
37
+
38
+ </div>
39
+
40
+ </body>
41
+
42
+ </html>