├── .gitattributes ├── templates ├── dot-gitattributes ├── espruino │ └── scripts │ │ ├── repl.js │ │ └── upload.js ├── main.js └── dot-gitignore ├── .jshintrc ├── .travis.yml ├── lib ├── core │ ├── colors-theme.js │ ├── file.js │ ├── ports.js │ └── cli-helpers.js ├── commands │ ├── new.js │ └── devices.js ├── devices.js └── new.js ├── .gitignore ├── scripts └── coverage_combine.js ├── bin └── thingssdk.js ├── test ├── commands │ ├── helper.js │ ├── test-devices.js │ └── test-new.js └── unit │ ├── test-file.js │ └── test-ports.js ├── LICENSE.md ├── package.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /templates/dot-gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6, 3 | "node": true, 4 | "strict": true, 5 | "browser": true, 6 | "mocha": true 7 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | 5 | install: 6 | - npm install --dev 7 | 8 | script: 9 | - npm run test:coverage 10 | 11 | after_success: 12 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /templates/espruino/scripts/repl.js: -------------------------------------------------------------------------------- 1 | const espruinoStrategy = require('thingssdk-espruino-strategy'); 2 | 3 | const devices = require('../devices.json').devices; 4 | 5 | espruinoStrategy.utils.filterDevices(devices) 6 | .forEach(espruinoStrategy.repl); 7 | -------------------------------------------------------------------------------- /lib/core/colors-theme.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const colors = require("colors"); 4 | colors.setTheme({ 5 | info: "green", 6 | help: "cyan", 7 | warn: "yellow", 8 | debug: "blue", 9 | error: "red" 10 | }); 11 | 12 | module.exports = colors; -------------------------------------------------------------------------------- /templates/main.js: -------------------------------------------------------------------------------- 1 | let isOn = false; 2 | const interval = 500; // 500 milliseconds = 0.5 seconds 3 | 4 | /** 5 | * The `main` function gets executed when the board is initialized. 6 | * Development: npm run dev 7 | * Production: npm run deploy 8 | */ 9 | function main() { 10 | setInterval(() => { 11 | isOn = !isOn; // Flips the state on or off 12 | digitalWrite(D2, isOn); // D2 is the blue LED on the ESP8266 boards 13 | }, interval); 14 | } 15 | -------------------------------------------------------------------------------- /lib/commands/new.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const {createApplication} = require('../new'); 4 | const colors = require('../core/colors-theme'); 5 | const {onError, onFinish} = require('../core/cli-helpers'); 6 | 7 | /** 8 | * Yargs required exports 9 | */ 10 | 11 | exports.command = "new "; 12 | 13 | exports.describe = 'create an applicaiton at a given path'; 14 | 15 | exports.builder = { 16 | runtime: { 17 | default: "espruino" 18 | } 19 | }; 20 | 21 | exports.handler = function (argv) { 22 | createApplication(argv).then(onFinish('Project successfully created')).catch(onError); 23 | }; -------------------------------------------------------------------------------- /lib/commands/devices.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {createDevicesJSON} = require('../devices'); 4 | const colors = require('../core/colors-theme'); 5 | const {onError, onFinish} = require('../core/cli-helpers'); 6 | 7 | /** 8 | * Yargs required exports 9 | */ 10 | 11 | exports.command = "devices"; 12 | 13 | exports.describe = 'create a devices.json in current directory'; 14 | 15 | exports.builder = { 16 | runtime: { 17 | default: "espruino" 18 | } 19 | }; 20 | 21 | exports.handler = function(argv)  { 22 | const {port, baud_rate, runtime, destinationPath} = argv; 23 | const deviceJSONOptions = { port, baud_rate, runtime, destinationPath: process.cwd() }; 24 | createDevicesJSON(deviceJSONOptions).then(onFinish("devices.json successfully created")).catch(onError); 25 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directories 27 | node_modules 28 | jspm_packages 29 | 30 | # Typings for VSC 31 | typings 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # IDE/Editors 40 | .idea 41 | .vscode 42 | 43 | # tmp directory 44 | tmp -------------------------------------------------------------------------------- /scripts/coverage_combine.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const istanbul = require('istanbul'), 7 | collector = new istanbul.Collector(), 8 | reporter = new istanbul.Reporter(), 9 | sync = false; 10 | 11 | const coveragePath = "./coverage"; 12 | 13 | fs.readdir(coveragePath, (err, files) => { 14 | files 15 | .filter(file => path.extname(file) === ".json") 16 | .map(file => path.join(coveragePath, file)) 17 | .map(filePath => fs.readFileSync(filePath, "utf-8")) 18 | .map(fileContents => JSON.parse(fileContents)) 19 | .forEach(json => collector.add(json)); 20 | reporter.addAll(['lcov']); 21 | reporter.write(collector, sync, function () { 22 | console.log('All reports generated'); 23 | }); 24 | }); -------------------------------------------------------------------------------- /bin/thingssdk.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const cliPackage = require("../package.json"); 5 | 6 | const argv = require("yargs") 7 | .version(cliPackage.version) 8 | .commandDir('../lib/commands') 9 | .option("runtime", { 10 | alias: "r", 11 | describe: "Runtime for the device", 12 | }) 13 | .option("baud_rate", { 14 | alias: "b", 15 | describe: "Baud rate for the device" 16 | }) 17 | .option("port", { 18 | alias: "p", 19 | describe: "Serial port for the device" 20 | }) 21 | .demand(1) 22 | .strict() 23 | .help() 24 | .argv; 25 | -------------------------------------------------------------------------------- /test/commands/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const rmdir = require('rimraf'); 5 | 6 | function prepCommand(command, cliArgs, path_to = "") { 7 | cliArgs[0] = path_to + cliArgs[0]; 8 | if (process.env.running_under_istanbul) { 9 | let cmd = "istanbul"; 10 | if (process.platform === 'win32') cmd = `${cmd}.cmd`; 11 | cliArgs.unshift(cliArgs[0]); 12 | cliArgs[1] = "--"; 13 | cliArgs = ['cover', '--report=none', '--print=none', '--include-pid'].concat(cliArgs); 14 | command = path.join(path_to, "node_modules", ".bin", cmd); 15 | } 16 | return { command, cliArgs }; 17 | } 18 | 19 | function cleanTmp(done) { 20 | rmdir('tmp', function (error) { 21 | done(); 22 | }); 23 | } 24 | 25 | module.exports = { 26 | prepCommand, 27 | cleanTmp 28 | }; -------------------------------------------------------------------------------- /templates/espruino/scripts/upload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const env = process.argv[2]; 3 | 4 | if(!env) throw Error('Environment not present. Please use development or production.'); 5 | 6 | const path = require('path'); 7 | const deployer = require('thingssdk-deployer')(); 8 | 9 | const espruinoStrategy = require('thingssdk-espruino-strategy'); 10 | 11 | const iotPackageJson = require('../package.json'); 12 | const devices = require('../devices.json').devices; 13 | const scriptsDir = path.dirname(require.main.filename); 14 | const payload = { 15 | entry: path.join(__dirname, '..', iotPackageJson.main), 16 | buildDir: path.join(scriptsDir, '..', 'build'), 17 | env 18 | }; 19 | 20 | deployer.prepare(devices, payload); 21 | 22 | deployer.use('espruino', espruinoStrategy.build); 23 | deployer.use('espruino', espruinoStrategy.upload); 24 | 25 | deployer.deploy(); -------------------------------------------------------------------------------- /lib/core/file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require("fs"); 3 | 4 | const NO_SUCH_FILE_OR_DIRECTORY_ERROR_CODE = 'ENOENT'; 5 | 6 | function write(path, contents) { 7 | fs.writeFileSync(path, contents); 8 | } 9 | 10 | function copy(from, to) { 11 | write(to, fs.readFileSync(from)); 12 | } 13 | 14 | function isDirectoryEmpty(path) { 15 | return new Promise((resolve, reject) => { 16 | fs.readdir(path, (err, files) => { 17 | /* istanbul ignore if */ 18 | if (err && err.code !== NO_SUCH_FILE_OR_DIRECTORY_ERROR_CODE) { 19 | reject(err); 20 | } else { 21 | const noFilesPresent = !files || !files.length; 22 | resolve(noFilesPresent, path); 23 | } 24 | }); 25 | }); 26 | } 27 | 28 | module.exports = { 29 | write, 30 | copy, 31 | isDirectoryEmpty 32 | }; -------------------------------------------------------------------------------- /templates/dot-gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (http://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directories 34 | node_modules 35 | jspm_packages 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | # ThingsSDK 44 | devices.json 45 | build/* -------------------------------------------------------------------------------- /lib/core/ports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const serialport = require("serialport"); 4 | const {printLoading} = require('./cli-helpers'); 5 | 6 | function getPorts() { 7 | let clearDevicePrompt; 8 | return new Promise( 9 | (resolve, reject) => { 10 | function portResolver(err, ports) { 11 | if (err) reject(err); 12 | const portNames = ports.map((port) => port.comName); 13 | if (portNames.length > 0) { 14 | if (clearDevicePrompt) clearDevicePrompt(); 15 | resolve(portNames); 16 | } else { 17 | clearDevicePrompt = clearDevicePrompt || printLoading("Plug your device in", ".", 3); 18 | serialport.list(portResolver); 19 | } 20 | } 21 | serialport.list(portResolver); 22 | }); 23 | } 24 | 25 | module.exports = { 26 | getPorts 27 | }; -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 thingsSDK 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thingssdk-cli", 3 | "version": "1.3.0", 4 | "description": "Generator for JavaScript microcontroller projects", 5 | "main": "bin/thingssdk.js", 6 | "bin": { 7 | "thingssdk": "./bin/thingssdk.js" 8 | }, 9 | "scripts": { 10 | "test": "mocha --recursive --timeout 5000 --exit", 11 | "combine-coverage": "node ./scripts/coverage_combine", 12 | "test:coverage": "rimraf coverage && istanbul cover --report=none --print=none --include-pid node_modules/mocha/bin/_mocha -- --recursive --timeout 5000 --exit && npm run combine-coverage" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/thingsSDK/thingssdk-cli.git" 17 | }, 18 | "keywords": [ 19 | "JavaScript", 20 | "IoT", 21 | "ESP8266", 22 | "Espruino" 23 | ], 24 | "author": "Andrew Chalkley ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/thingsSDK/thingssdk-cli/issues" 28 | }, 29 | "homepage": "https://github.com/thingsSDK/thingssdk-cli#readme", 30 | "dependencies": { 31 | "colors": "^1.2.1", 32 | "inquirer": "^5.2.0", 33 | "mkdirp": "^0.5.1", 34 | "serialport": "^6.1.1", 35 | "yargs": "11.0.0" 36 | }, 37 | "devDependencies": { 38 | "chai": "^4.1.2", 39 | "istanbul": "^0.4.5", 40 | "mocha": "^5.0.5", 41 | "proxyquire": "^2.0.1", 42 | "rimraf": "^2.6.2", 43 | "suppose": "^0.6.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/unit/test-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require("chai").expect; 4 | const path = require("path"); 5 | const mkdirp = require("mkdirp"); 6 | const fs = require("fs"); 7 | 8 | const {isDirectoryEmpty} = require("../../lib/core/file"); 9 | 10 | describe("function isDirectoryEmpty()", () => { 11 | it("should throw an error if given no path", (done) => { 12 | isDirectoryEmpty().catch(err => { 13 | expect(err).to.be.a('Error'); 14 | done(); 15 | }); 16 | 17 | }); 18 | 19 | it("should return false if any files are present in the given directory", (done) => { 20 | /** 21 | An arbitrary directory that we know isn't empty 22 | */ 23 | const thisDirectory = path.resolve(__dirname); 24 | 25 | /** 26 | Initialize actual here so that we can update it with 27 | the async results of isDirectoryEmpty 28 | */ 29 | let actual; 30 | const expected = false; 31 | 32 | /** 33 | asyncResult is passed to the callback via 34 | isDirectoryEmpty internal logic 35 | */ 36 | const callback = (asyncResult) => { 37 | actual = asyncResult; 38 | expect(actual).to.equal(expected); 39 | done(); 40 | }; 41 | 42 | isDirectoryEmpty(thisDirectory).then(callback); 43 | }); 44 | 45 | it("should return true if the given directory is empty", (done) => { 46 | const emptyDirectory = path.resolve(__dirname, "empty"); 47 | mkdirp(emptyDirectory); 48 | 49 | /** 50 | Initialize actual here so that we can update it with 51 | the async results of isDirectoryEmpty 52 | */ 53 | let actual; 54 | const expected = true; 55 | 56 | /** 57 | isDirectoryEmpty internal logic 58 | asyncResult is passed to the callback via 59 | */ 60 | const callback = (asyncResult) => { 61 | actual = asyncResult; 62 | expect(actual).to.equal(expected); 63 | 64 | /** 65 | Destroy the empty directory we made for this test 66 | before we finish 67 | */ 68 | fs.rmdir(emptyDirectory, done); 69 | }; 70 | 71 | isDirectoryEmpty(emptyDirectory).then(callback); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/unit/test-ports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire'); 5 | 6 | 7 | describe("function getPorts()", () => { 8 | let getPorts; 9 | let error; 10 | let ports; 11 | before(() => { 12 | getPorts = proxyquire('../../lib/core/ports', { 13 | 'serialport': { 14 | list: (callback) => { 15 | callback(error, ports); 16 | } 17 | } 18 | }).getPorts; 19 | }); 20 | 21 | it("should return a list of serveral ports", done => { 22 | error = null; 23 | ports = [{ comName: "COM3" }, { comName: "COM7" }] 24 | getPorts().then(ports => { 25 | assert.deepEqual(ports, ["COM3", "COM7"]); 26 | done(); 27 | }).catch(err => { 28 | assert.isNull(err); 29 | done(); 30 | }); 31 | }); 32 | 33 | it("should return a list of port if there's only one port", done => { 34 | error = null; 35 | ports = [{ comName: "COM7" }]; 36 | getPorts().then(ports => { 37 | assert.deepEqual(ports, ["COM7"]); 38 | done(); 39 | }).catch(err => { 40 | assert.isNull(err); 41 | done(); 42 | }); 43 | }); 44 | 45 | it("should return an error", done => { 46 | const errorMessage = "Opps some serial port error"; 47 | error = new Error(errorMessage); 48 | ports = null; 49 | getPorts().then(ports => { 50 | assert.isNull(ports); 51 | done(); 52 | }).catch(err => { 53 | assert.equal(err.message, errorMessage); 54 | done(); 55 | }); 56 | }); 57 | 58 | it("should eventually return ports once they're plugged in", done => { 59 | //Initially there's no ports [], then COM7 get's plugged in 60 | const calls = [[], [{ comName: "COM7" }]]; 61 | let callBackTime = 0; 62 | const getPorts = proxyquire('../../lib/core/ports', { 63 | 'serialport': { 64 | list: (callback) => { 65 | const ports = calls.shift(); 66 | setTimeout(() => callback(null, ports), callBackTime+=1200); 67 | } 68 | } 69 | }).getPorts; 70 | 71 | getPorts().then(ports => { 72 | assert.deepEqual(ports, ["COM7"], 'Ports were not what was expected'); 73 | done(); 74 | }).catch(err => { 75 | assert.isNull(err); 76 | done(); 77 | }); 78 | }); 79 | }); -------------------------------------------------------------------------------- /lib/core/cli-helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const colors = require('../core/colors-theme'); 3 | 4 | /** 5 | * CLI based functions only 6 | */ 7 | 8 | const readLine = require("readline").createInterface({ 9 | input: process.stdin, 10 | output: process.stdout 11 | }); 12 | 13 | 14 | function askQuestion(question) { 15 | return new Promise((resolve, reject) => { 16 | readLine.question(question, answer => { 17 | readLine.close(); 18 | resolve(answer); 19 | }); 20 | }); 21 | } 22 | 23 | function clearLine() { 24 | const CLEAR_LINE = new Buffer('1b5b304b', 'hex').toString(); 25 | const MOVE_LEFT = new Buffer('1b5b3130303044', 'hex').toString(); 26 | process.stdout.write(MOVE_LEFT + CLEAR_LINE); 27 | process.stdout.write(""); 28 | } 29 | 30 | function printLoading(message, loadingChar, count) { 31 | let i = 0; 32 | let interval = setInterval(() => { 33 | if (i === 0) { 34 | process.stdout.write(message); 35 | } 36 | process.stdout.write(loadingChar); 37 | if (i > count) { 38 | clearLine(); 39 | process.stdout.write(message); 40 | i = 0; 41 | } 42 | i++; 43 | }, 400); 44 | return () => { 45 | clearInterval(interval); 46 | clearLine(); 47 | }; 48 | } 49 | 50 | function cleanAnswer(answer) { 51 | return answer.toLowerCase().trim().charAt(0); 52 | } 53 | 54 | function shouldOverwrite(destinationPath) { 55 | const question = `Files already exist at ${destinationPath}.\nWould you like to overwrite the existing files?\nType y or n: `; 56 | 57 | return askQuestion(question) 58 | .then(cleanAnswer) 59 | .then(answer => { 60 | if (answer === 'y') { 61 | console.log(colors.info("You answered yes. Overwriting existing project files.")); 62 | return true; 63 | } 64 | else if (answer === 'n') { 65 | console.log(colors.warn("No project files were changed. Aborting new project creation.")); 66 | return false; 67 | } 68 | else { 69 | throw "I don't understand your input. No project files were changed. Aborting new project creation."; 70 | } 71 | }); 72 | } 73 | 74 | function onError(err) { 75 | console.error(colors.error(err)); 76 | process.exit(1); 77 | } 78 | 79 | function onFinish(message) { 80 | return () => { 81 | console.log(colors.info(message)); 82 | process.exit(0); 83 | }; 84 | } 85 | 86 | module.exports = { 87 | shouldOverwrite, 88 | printLoading, 89 | onError, 90 | onFinish 91 | }; -------------------------------------------------------------------------------- /test/commands/test-devices.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const mkdirp = require('mkdirp'); 5 | const proxyquire = require('proxyquire'); 6 | const suppose = require("suppose"); 7 | 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | 11 | const {prepCommand, cleanTmp} = require('./helper'); 12 | const {createDevicesJSON: devicesCommand} = require('../../lib/devices'); 13 | 14 | 15 | describe("thingssdk devices", () => { 16 | describe("with valid arguments", () => { 17 | const checkDevicesPath = "tmp/devices-json-check"; 18 | const jsonPath = path.join(checkDevicesPath, "devices.json"); 19 | 20 | const validArguments = { 21 | port: "COM7", 22 | baud_rate: "115200", 23 | runtime: "espruino", 24 | destinationPath: checkDevicesPath 25 | }; 26 | 27 | const otherValidArguments = { 28 | port: "COM3", 29 | runtime: "espruino", 30 | baud_rate: 9600, 31 | destinationPath: checkDevicesPath 32 | }; 33 | 34 | const validArgumentsWithoutPortAndBaudRate = { 35 | runtime: "espruino", 36 | destinationPath: checkDevicesPath 37 | }; 38 | 39 | before(done => { 40 | cleanTmp(() => { 41 | mkdirp(checkDevicesPath, (err) => { 42 | done(); 43 | }); 44 | }); 45 | }); 46 | 47 | it("should create a `devices.json` in the correct folder", done => { 48 | mkdirp(checkDevicesPath, (err) => { 49 | devicesCommand(validArguments).then(() => { 50 | const devices = JSON.parse(fs.readFileSync(jsonPath)); 51 | const expectedJson = { 52 | devices: { 53 | COM7: { 54 | runtime: "espruino", 55 | baud_rate: 115200 56 | } 57 | } 58 | }; 59 | assert.deepEqual(devices, expectedJson, "devices.json didn't match expectedJson"); 60 | done(); 61 | }); 62 | }); 63 | }); 64 | 65 | 66 | it("should create a `devices.json` in the correct folder with different params", done => { 67 | mkdirp(checkDevicesPath, (err) => { 68 | devicesCommand(otherValidArguments).then(() => { 69 | const devices = JSON.parse(fs.readFileSync(jsonPath)); 70 | const expectedJson = { 71 | devices: { 72 | COM3: { 73 | runtime: "espruino", 74 | baud_rate: 9600 75 | } 76 | } 77 | }; 78 | assert.deepEqual(devices, expectedJson, "devices.json didn't match expectedJson"); 79 | done(); 80 | }); 81 | }); 82 | }); 83 | 84 | it("should exit correctly when correct aguments are passed", () => { 85 | process.chdir('tmp'); 86 | let commandOutput; 87 | 88 | const {handler : cliDevicesCommand} = proxyquire('../../lib/commands/devices', 89 | { 90 | '../core/cli-helpers': { 91 | onFinish: (message) => { 92 | commandOutput = message; 93 | } 94 | } 95 | }); 96 | 97 | cliDevicesCommand(validArguments); 98 | assert.equal(commandOutput, 'devices.json successfully created'); 99 | process.chdir('../'); 100 | }); 101 | 102 | it("should ask for port and baud rate if missing from arguments", done => { 103 | const {createDevicesJSON: devicesCommand} = proxyquire('../../lib/devices', { 104 | "./core/ports": { 105 | getPorts: () => { 106 | return new Promise((resolve, reject) => { 107 | resolve(["COM7", "COM18"]); 108 | }); 109 | } 110 | }, 111 | "inquirer": { 112 | prompt: questions => { 113 | return new Promise((resolve, reject) => { 114 | const answers = { 115 | port: "COM7", 116 | baud_rate: 115200 117 | }; 118 | resolve(answers); 119 | }); 120 | } 121 | } 122 | }); 123 | 124 | devicesCommand(validArgumentsWithoutPortAndBaudRate).then(() => { 125 | const devices = JSON.parse(fs.readFileSync(jsonPath)); 126 | const expectedJson = { 127 | devices: { 128 | COM7: { 129 | runtime: "espruino", 130 | baud_rate: 115200 131 | } 132 | } 133 | }; 134 | assert.deepEqual(devices, expectedJson, "devices.json didn't match expectedJson"); 135 | done(); 136 | }); 137 | 138 | }); 139 | 140 | after(cleanTmp); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # thingsSDK CLI 2 | 3 | [![Build Status](https://travis-ci.org/thingsSDK/thingssdk-cli.svg?branch=master)](https://travis-ci.org/thingsSDK/thingssdk-cli) 4 | [![codecov](https://codecov.io/gh/thingsSDK/thingssdk-cli/branch/master/graph/badge.svg)](https://codecov.io/gh/thingsSDK/thingssdk-cli) 5 | [![Dependency Status](https://david-dm.org/thingssdk/thingssdk-cli.svg)](https://david-dm.org/thingssdk/thingssdk-cli) 6 | [![devDependency Status](https://david-dm.org/thingssdk/thingssdk-cli/dev-status.svg)](https://david-dm.org/thingssdk/thingssdk-cli#info=devDependencies) 7 | 8 | thingsSDK CLI is a command line utility for generating and managing modern projects for JavaScript microcontroller runtimes. 9 | 10 | Initial support is for Espruino with hopes to support others like Kinoma in the future. 11 | 12 | ## Install CLI 13 | 14 | ```bash 15 | $ npm install thingssdk-cli -g 16 | ``` 17 | 18 | Note that this project uses [serialport](https://github.com/EmergingTechnologyAdvisors/node-serialport), which compiles to binary. You might need to install some prerequesites depending on your operating system. 19 | 20 | ## Prerequisites 21 | Make sure prior to trying to push a project to your device, you flash the device with the Espruino Runtime with [Flasher.js](https://github.com/thingsSDK/flasher.js/releases). 22 | 23 | ## Usage 24 | 25 | Plug your device in first and make sure you have the necessary drivers installed. 26 | 27 | ### New Project 28 | 29 | Next to create a new project use the `new` command like so: 30 | 31 | ```bash 32 | $ thingssdk new path/to/project_name 33 | ``` 34 | 35 | You'll be prompted to enter plug your device in if you haven't already and then select the device's serial port and baud rate. 36 | 37 | If you know your device's port and baud rate already, use the `port` and `baud_rate` options: 38 | 39 | ```bash 40 | $ thingssdk new path/to/project_name --port=COM3 --baud_rate=115200 41 | ``` 42 | 43 | ### Getting Started with Your New Project 44 | 45 | Your new project will now be found at `path/to/project_name`. You'll need to then install the dependencies. 46 | 47 | ```bash 48 | $ npm install 49 | ``` 50 | 51 | `dependencies` in the new project `package.json` should be deployed to the device, `devDependancies` are what are used for your development workflow. 52 | 53 | A `devices.json` file is created in the root of your new project. An entry is placed in your `.gitignore` because serial ports from computer to computer and developer to developer will differ. 54 | 55 | ### Deploying it to Your Device 56 | 57 | To run the "Hello, world" sample project to your device(s) run the npm script `dev`. 58 | 59 | ```bash 60 | $ npm run dev 61 | ``` 62 | 63 | An interactive REPL will launch and you can interact with your code and debug your program. Once you're happy you can use `delpoy` to upload and __save__ your code to the device. 64 | 65 | ```bash 66 | $ npm run deploy 67 | ``` 68 | 69 | The "Hello, world" script can be found in `main.js`. This script gets uploaded to your device and blinks the blue LED on the `ESP8266` board. It uses the `devices.json` file to know which devices to deploy the code to. 70 | 71 | Your JavaScript program must implement a `main` function in order to be ran when the board is initialized. 72 | 73 | ### Creating a `devices.json` file 74 | 75 | To overwrite the current devices.json or create a new devices.json file in your project directory run the following command for an interactive prompt: 76 | 77 | ```bash 78 | $ thingssdk devices 79 | ``` 80 | 81 | Or with the flags `port` and `baud_rate` if you know them already. 82 | 83 | ``` 84 | $ thingssdk devices --port=COM3 --baud_rate=115200 85 | ``` 86 | 87 | This will generate a `devices.json` like this: 88 | 89 | ```javascript 90 | { 91 | "devices": { 92 | "COM3": { 93 | "baud_rate": 115200, 94 | "runtime": "espruino" 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | ### Warning for Unix users: ~/ 101 | Due to cross-platform compatibility issues, `~` does not resolve to your home directory on Unix systems. For example, suppose: 102 | 103 | ```bash 104 | $ pwd 105 | /home//some/subdirectory 106 | ``` 107 | 108 | Running 109 | ```bash 110 | $ thingssdk new ~/path/to/project_name 111 | ``` 112 | 113 | Would produce the following result: 114 | 115 | ```bash 116 | $ ls ~/path/to/project_name 117 | ls: cannot access '/home//path/to/project_name': No such file or directory 118 | 119 | $ ls ~/some/subdirectory/~/path/to/project_name 120 | main.js package.json scripts 121 | ``` 122 | 123 | This is probably not your intended behavior! So `thingssdk` throws an Error for paths beginning with `~`, and a warning for paths containing `~` elsewhere. 124 | -------------------------------------------------------------------------------- /lib/devices.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const inquirer = require("inquirer"); 3 | const path = require("path"); 4 | const {write} = require('./core/file'); 5 | const {getPorts} = require('./core/ports'); 6 | 7 | /** 8 | * @name devicesObject 9 | * 10 | * @description 11 | * Helper function ran to ask user for Port 12 | * 13 | * @param {String} port 14 | * @param {String} baud_rate 15 | * @param {String} runtime The proper key for runtime + version 16 | * 17 | * @returns {object} device configuration object 18 | * 19 | * @example 20 | * ```js 21 | * { 22 | * "devices": { 23 | * "/dev/cu.SLAB_USBtoUART": { 24 | * "baud_rate": 115200, 25 | * "runtime": "espruino" 26 | * } 27 | * } 28 | * } 29 | * ``` 30 | */ 31 | function devicesObject(port, baud_rate, runtime) { 32 | baud_rate = parseInt(baud_rate); 33 | const devices = {}; 34 | devices[port] = { baud_rate, runtime }; 35 | return { devices }; 36 | } 37 | 38 | /** 39 | * @name askForPort 40 | * @requires getPorts 41 | * 42 | * @description 43 | * Helper function ran to ask user for Port 44 | * 45 | * @param {object} answers partial device configuration object 46 | * 47 | * @returns {object} partial device configuration object with port property 48 | * defined. 49 | */ 50 | function askForPort(answers) { 51 | return getPorts().then((ports) => { 52 | const portQuestion = [ 53 | { 54 | type: 'list', 55 | name: 'port', 56 | message: 'Select a port:', 57 | choices: ports, 58 | default: ports[0] 59 | } 60 | ]; 61 | return inquirer 62 | .prompt(portQuestion) 63 | .then(promptAnswers => Object.assign({},answers,promptAnswers)) 64 | }); 65 | } 66 | 67 | /** 68 | * @name askForBaudRate 69 | * 70 | * @description 71 | * Helper function ran to ask user for baud rate 72 | * 73 | * @param {object} answers partial device configuration object 74 | * 75 | * @returns {object} partial device configuration object with baud_rate property 76 | * defined. 77 | */ 78 | function askForBaudRate(answers) { 79 | const baudRateQuestion = [ 80 | { 81 | type: 'list', 82 | name: 'baud_rate', 83 | message: 'Select the baud rate:', 84 | choices: ['9600', '115200'], 85 | default: '115200' 86 | } 87 | ]; 88 | return inquirer 89 | .prompt(baudRateQuestion) 90 | .then(promptAnswers => Object.assign({},answers,promptAnswers)) 91 | } 92 | 93 | /** 94 | * @name askUserForPortAndBaudRate 95 | * 96 | * @description 97 | * Helper function ran to ask user for both port and baud rate, then 98 | * return the devices configuration object. 99 | * 100 | * @param {String} runtime The proper key for runtime + version 101 | * 102 | * @returns {object} complete device configuration object 103 | */ 104 | function askUserForPortAndBaudRate(runtime) { 105 | const answers = {}; 106 | return askForPort() 107 | .then(answers => { 108 | return askForBaudRate(answers); 109 | }) 110 | .then(answers => { 111 | return devicesObject(answers.port, answers.baud_rate, runtime); 112 | }); 113 | } 114 | 115 | /** 116 | * @name createDevicesJSON 117 | * @requires askUserForPortAndBaudRate 118 | * @requires askForPort 119 | * @requires askForBaudRate 120 | * @requires devicesObject 121 | * @requires write 122 | * 123 | * @description 124 | * Asks the user for any unspecified configuration details, then 125 | * writes the configuration to the devices.json file in the IoT project 126 | * 127 | * @param {Object} options The four configuration options passed to the yargs 128 | * command line. 129 | * 130 | * ```js options = { 131 | * port, 132 | * baud_rate, 133 | * path, 134 | * runtime 135 | * } 136 | * ``` 137 | */ 138 | function createDevicesJSON(options) { 139 | const {port, baud_rate, runtime, destinationPath} = options; 140 | let devicePromise; 141 | 142 | if (typeof port === 'undefined' && typeof baud_rate === 'undefined') { 143 | devicePromise = askUserForPortAndBaudRate(runtime); 144 | } 145 | else if(typeof port === 'undefined') { 146 | devicePromise = askForPort({}).then(answers => devicesObject(answers.port, baud_rate, runtime)); 147 | } 148 | else if(typeof baud_rate === 'undefined') { 149 | devicePromise = askForBaudRate({}).then(answers => devicesObject(port, answers.baud_rate, runtime)); 150 | } else { 151 | devicePromise = new Promise((resolve) => { 152 | resolve(devicesObject(port, baud_rate, runtime)); 153 | }); 154 | } 155 | 156 | return devicePromise.then(devices => { 157 | write(path.join(destinationPath, "devices.json"), JSON.stringify(devices, null, 2)); 158 | }); 159 | } 160 | 161 | module.exports = { 162 | createDevicesJSON 163 | }; 164 | -------------------------------------------------------------------------------- /lib/new.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | const mkdirp = require("mkdirp"); 5 | 6 | const {createDevicesJSON} = require('./devices'); 7 | const {write, copy, isDirectoryEmpty} = require('./core/file'); 8 | const colors = require('./core/colors-theme'); 9 | const {shouldOverwrite} = require('./core/cli-helpers'); 10 | 11 | /** 12 | * @name checkForTilde 13 | * 14 | * @description 15 | * Helper function to check the destination path and display any needed warnings 16 | * 17 | * @param {String} destinationPath 18 | * @throws {Error} Bad Path 19 | */ 20 | function checkForTilde(destinationPath) { 21 | /** 22 | If the first part of the path contains a ~, 23 | we'll assume the user intends for their project to install 24 | relative to their home directory on a Unix machine and throw an error. 25 | */ 26 | if (destinationPath.split('/')[0].includes('~')) { 27 | console.log(colors.error(` 28 | Uh-oh, check your desired project path! 29 | It looks like you made a reference to your home directory: ~/ 30 | Due to cross platform compatibility with non-Unix systems, that causes some problems. 31 | Try again using absolute paths like /Users//path/to/project 32 | `)); 33 | throw new Error(`Bad path, we're sorry...`); 34 | } 35 | 36 | /** 37 | If any other part of the path contains a ~, 38 | we'll just give them a helpful warning. 39 | */ 40 | if (destinationPath.includes('~')) { 41 | console.log(colors.warn(` 42 | Be careful, it looks like your project path contains a "~". 43 | If you remove this directory, be very careful about running "rm -rf \\~" 44 | You could accidentally destroy your home directory! 45 | `)); 46 | } 47 | } 48 | 49 | /** 50 | * @name createApplication 51 | * @requires isDirectoryEmpty 52 | * @requires shouldOverwrite 53 | * @requires createFiles 54 | * @requires projectCreated 55 | * 56 | * @description 57 | * Checks if the desired project folder is empty, then creates the project files 58 | * if it is, then displays basic instructions after project has been successfully 59 | * created. 60 | * 61 | * @param {Object} options The four configuration options passed to the yargs 62 | * command line. 63 | * 64 | * ```js options = { 65 | * port, 66 | * baud_rate, 67 | * path, 68 | * runtime 69 | * } 70 | * ``` 71 | */ 72 | function createApplication(options) { 73 | const {path: destinationPath, runtime} = options; 74 | return isDirectoryEmpty(destinationPath) 75 | .then(isEmpty => { 76 | if (isEmpty) { 77 | return true; 78 | } else { 79 | return shouldOverwrite(destinationPath); 80 | } 81 | }) 82 | .then(proceed => { 83 | if (proceed) { 84 | return createFiles(options).then(() => { 85 | projectCreated(destinationPath); 86 | }); 87 | } 88 | }); 89 | } 90 | 91 | /** 92 | * @name projectCreated 93 | * 94 | * @description 95 | * Helper function to display instructions after project is successfully created 96 | * 97 | * @param {String} destinationPath 98 | */ 99 | function projectCreated(destinationPath) { 100 | const successMessage = `To install the project dependencies: 101 | cd ${destinationPath} && npm install 102 | To upload to your device: 103 | Development: 104 | npm run dev 105 | Production: 106 | npm run deploy`; 107 | 108 | console.log(colors.help(successMessage)); 109 | } 110 | 111 | /** 112 | * @name makeDirectory 113 | * 114 | * @description 115 | * Helper function to promisify mkdir 116 | * 117 | * @param {String} directory The name of the desired new directory 118 | */ 119 | function makeDirectory(directory) { 120 | return new Promise((resolve, reject) => { 121 | mkdirp(directory, err => { 122 | if (err) reject(err); 123 | else resolve(); 124 | }); 125 | }); 126 | } 127 | 128 | /** 129 | * @name createFiles 130 | * @requires checkForTilde 131 | * @requires makeDirectory 132 | * @requires createPackageJSON 133 | * @requires createDevicesJSON 134 | * 135 | * @description 136 | * Selects the correct template files for the IoT project, creates the new project 137 | * folder, and copies the template files into the new project folder. Configures 138 | * and writes the package.json and devices.json files. 139 | * 140 | * @param {Object} options The four configuration options passed to the yargs 141 | * command line. 142 | * 143 | * ```js options = { 144 | * port, 145 | * baud_rate, 146 | * path, 147 | * runtime 148 | * } 149 | * ``` 150 | */ 151 | function createFiles(options) { 152 | const {port, baud_rate, path: destinationPath, runtime} = options; 153 | const app_name = path.basename(path.resolve(destinationPath)); 154 | const templatesPath = path.join(__dirname, "..", "templates"); 155 | const scriptPath = path.join(templatesPath, runtime, "scripts"); 156 | 157 | checkForTilde(destinationPath); 158 | 159 | return makeDirectory(path.join(destinationPath, 'scripts')) 160 | .then(() => makeDirectory(path.join(destinationPath, 'build'))) 161 | .then(() => { 162 | /* Copy templates */ 163 | fs.readdir(scriptPath, (err, files) => { 164 | if (err) throw err; 165 | files.forEach(file => copy(path.join(scriptPath, file), path.join(destinationPath, "scripts", file))); 166 | }); 167 | 168 | /* Create package.json for project */ 169 | const pkg = createPackageJSON(app_name, runtime); 170 | write(path.join(destinationPath, "package.json"), JSON.stringify(pkg, null, 2)); 171 | copy(path.join(templatesPath, 'main.js'), path.join(destinationPath, 'main.js')); 172 | copy(path.join(templatesPath, 'dot-gitignore'), path.join(destinationPath, '.gitignore')); 173 | copy(path.join(templatesPath, 'dot-gitattributes'), path.join(destinationPath, '.gitattributes')); 174 | 175 | /* Create devices.json and finish */ 176 | const deviceJSONOptions = { port, baud_rate, runtime, destinationPath }; 177 | return createDevicesJSON(deviceJSONOptions); 178 | }); 179 | } 180 | 181 | /** 182 | * @name createPackageJSON 183 | * 184 | * @description 185 | * Returns the package.json object for the IoT project 186 | * 187 | * @param {String} app_name The name for the IoT project 188 | * @param {String} runtime Key string asociated with the required runtime env 189 | * desired from the RUNTIMES object. 190 | * 191 | * @returns {Object} The package.json object for the new project 192 | */ 193 | function createPackageJSON(app_name, runtime) { 194 | const strategy = `thingssdk-${runtime}-strategy`; 195 | const strategyVersions = { 196 | espruino: "~1.0.3" 197 | }; 198 | const pkg = { 199 | name: app_name, 200 | version: '0.0.0', 201 | private: true, 202 | main: 'main.js', 203 | scripts: { 204 | dev: "node ./scripts/upload development && npm run repl", 205 | deploy: "node ./scripts/upload production", 206 | repl: "node ./scripts/repl", 207 | postinstall: "rimraf node_modules/bluetooth-hci-socket" 208 | }, 209 | devDependencies: { 210 | "thingssdk-deployer": "~1.0.1", 211 | [strategy]: strategyVersions[runtime] 212 | } 213 | }; 214 | 215 | return pkg; 216 | } 217 | 218 | module.exports = { 219 | createApplication 220 | }; 221 | -------------------------------------------------------------------------------- /test/commands/test-new.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const mkdirp = require('mkdirp'); 5 | const {createApplication: newCommand} = require("../../lib/new"); 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const suppose = require("suppose"); 9 | const {prepCommand, cleanTmp} = require('./helper'); 10 | 11 | describe("thingssdk new", () => { 12 | const projectPath = "tmp/folder/to/project_name"; 13 | 14 | const validArguments = { 15 | port: "COM7", 16 | baud_rate: "115200", 17 | runtime: "espruino", 18 | path: projectPath 19 | }; 20 | 21 | describe("with valid arguments", () => { 22 | before(cleanTmp); 23 | 24 | it("should create a devices.json", done => { 25 | newCommand(validArguments).then(() => { 26 | const devices = JSON.parse(fs.readFileSync(path.join(projectPath, "devices.json"))); 27 | const expectedJson = { 28 | devices: { 29 | COM7: { 30 | runtime: "espruino", 31 | baud_rate: 115200 32 | } 33 | } 34 | }; 35 | assert.deepEqual(devices, expectedJson, "devices.json didn't match expectedJson"); 36 | done(); 37 | }); 38 | }); 39 | 40 | it("should create a properly structured package.json", done => { 41 | const pkgJSON = JSON.parse(fs.readFileSync(path.join(projectPath, "package.json"))); 42 | const expectedJson = { 43 | name: "project_name", 44 | version: '0.0.0', 45 | private: true, 46 | main: 'main.js', 47 | scripts: { 48 | dev: "node ./scripts/upload development && npm run repl", 49 | deploy: "node ./scripts/upload production", 50 | repl: "node ./scripts/repl", 51 | postinstall: "rimraf node_modules/bluetooth-hci-socket" 52 | }, 53 | devDependencies: { 54 | "thingssdk-deployer": "~1.0.1", 55 | "thingssdk-espruino-strategy": "~1.0.3" 56 | } 57 | }; 58 | assert.deepEqual(pkgJSON, expectedJson, "package.json didn't match expectedJson"); 59 | done(); 60 | }); 61 | 62 | it("should copy the correct files", done => { 63 | const templatesPath = path.join(__dirname, "..", "..", "templates"); 64 | const files = [ 65 | { 66 | source: path.join(templatesPath, "dot-gitignore"), 67 | dest: path.join(projectPath, ".gitignore") 68 | }, 69 | { 70 | source: path.join(templatesPath, "dot-gitattributes"), 71 | dest: path.join(projectPath, ".gitattributes") 72 | }, 73 | { 74 | source: path.join(templatesPath, "main.js"), 75 | dest: path.join(projectPath, "main.js") 76 | }, 77 | { 78 | source: path.join(templatesPath, validArguments.runtime, "scripts", "upload.js"), 79 | dest: path.join(projectPath, "scripts", "upload.js") 80 | }, 81 | 82 | ]; 83 | files.forEach(file => { 84 | const fileSourceContents = fs.readFileSync(file.source, "utf-8"); 85 | const fileDestinationContents = fs.readFileSync(file.dest, "utf-8"); 86 | assert.equal(fileSourceContents, fileDestinationContents, `file contetnts for ${file.dest} didn't match ${file.source}`); 87 | }); 88 | done(); 89 | }); 90 | 91 | }); 92 | 93 | describe("when folder already exists", () => { 94 | const mainPath = path.join(projectPath, 'main.js'); 95 | const newFileContents = "console.log('hello world')"; 96 | let command = "node"; 97 | let cliArgs = [`bin/thingssdk.js`, `new`, projectPath, `--port=${validArguments.port}`, `--baud_rate=${validArguments.baud_rate}`]; 98 | 99 | before(done => { 100 | const preparedCommand = prepCommand(command, cliArgs); 101 | command = preparedCommand.command; 102 | cliArgs = preparedCommand.cliArgs; 103 | done(); 104 | }); 105 | 106 | beforeEach(done => { 107 | cleanTmp(() => { 108 | newCommand(validArguments).then(() => { 109 | fs.writeFileSync(mainPath, newFileContents); 110 | done(); 111 | }); 112 | }); 113 | }); 114 | 115 | it("should replace files if y", done => { 116 | assert.equal(fs.readFileSync(mainPath, "utf-8"), newFileContents); 117 | suppose(command, cliArgs) 118 | .when('Type y or n: ').respond('y\n') 119 | .on('error', function (err) { 120 | console.log(err.message); 121 | }) 122 | .end(function (code) { 123 | assert.equal(code, 0, "process exit code"); 124 | assert.notEqual(fs.readFileSync(mainPath, "utf-8"), newFileContents); 125 | done(); 126 | }); 127 | }); 128 | 129 | it("should abort if n", done => { 130 | assert.equal(fs.readFileSync(mainPath, "utf-8"), newFileContents); 131 | suppose(command, cliArgs) 132 | .when('Type y or n: ').respond('n\n') 133 | .on('error', function (err) { 134 | console.log(err.message); 135 | }) 136 | .end(function (code) { 137 | assert.equal(code, 0, "process exit code"); 138 | assert.equal(fs.readFileSync(mainPath, "utf-8"), newFileContents); 139 | done(); 140 | }); 141 | }); 142 | 143 | it("should abort and error if y or n not pressent", done => { 144 | assert.equal(fs.readFileSync(mainPath, "utf-8"), newFileContents); 145 | suppose(command, cliArgs) 146 | .when('Type y or n: ').respond('please don\'t\n') 147 | .on('error', function (err) { 148 | console.log(err.message); 149 | }) 150 | .end(function (code) { 151 | assert.equal(code, 1, "process exit code"); 152 | assert.equal(fs.readFileSync(mainPath, "utf-8"), newFileContents); 153 | done(); 154 | }); 155 | }); 156 | }); 157 | 158 | describe("if arguments with tildes in the path are passed", () => { 159 | it("should error if tilde is used at the start of path and no project created", done => { 160 | const examplePath = "~/example"; 161 | newCommand({ 162 | path: examplePath, 163 | port: "COM7", 164 | baud_rate: "115200", 165 | runtime: "espruino" 166 | }).catch((err) => { 167 | assert.isNotNull(err); 168 | // package.json shouldn't be there, i.e. project not created 169 | assert.throws(() => fs.readFileSync(path.join(examplePath, "package.json"))); 170 | done(); 171 | }); 172 | }); 173 | 174 | it("should create project if it's in the middle of the path", done => { 175 | const examplePath = "tmp/~/example"; 176 | newCommand({ 177 | path: examplePath, 178 | port: "COM7", 179 | baud_rate: "115200", 180 | runtime: "espruino" 181 | }).then(() => { 182 | // Check that the package.json is created i.e. project created 183 | const packageJSON = JSON.parse(fs.readFileSync(path.join(examplePath, "package.json"))); 184 | assert.equal(packageJSON.name, "example"); 185 | done(); 186 | }); 187 | }); 188 | }); 189 | 190 | after(cleanTmp); 191 | }); 192 | --------------------------------------------------------------------------------