├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── bin ├── install-mcli.sh ├── install-starter-kit.sh ├── mcli ├── mcli-bundle └── test-mcli.sh ├── package.js ├── src ├── CLI.coffee ├── MeteorNoops.coffee └── log.js ├── starter-mcli-app ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── cordova-plugins │ ├── packages │ ├── platforms │ ├── release │ └── versions └── server │ ├── EchoCommand.js │ ├── FindOneCommand.js │ ├── HelloWorldCommand.js │ ├── LsCommand.js │ ├── main.js │ ├── todos-bootstrap.js │ └── todos-collection.js └── tests └── CLITest.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | versions.json 2 | # Logs 3 | logs 4 | *.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 | # Compiled binary addons (http://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directory 24 | # Deployed apps should consider commenting this line out: 25 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 26 | node_modules 27 | .build* 28 | .npm 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10.36" 4 | 5 | before_install: 6 | - "curl https://install.meteor.com | /bin/sh" 7 | - "npm install -g spacejam" 8 | 9 | script: "./bin/test-mcli.sh" 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.5 2 | 3 | * Update starter-mcli-app to meteor 1.0.3.2 4 | * Update practicalmeteor:loglevel dependency to 1.2.0_1 5 | * Update async ls example to use CLI.done() 6 | 7 | ## 1.1.4 8 | 9 | * Add support for commands with async code 10 | * Update starter-mcli-app to meteor 1.0.2.1 11 | 12 | ## 1.1.3 13 | 14 | * Update starter-mcli-app to meteor 1.0.2 15 | * Support option values with spaces in mcli and mcli-bundle 16 | * Fix install script 17 | 18 | ## 1.1.2 19 | 20 | * Update changelog for 1.1.1 and 1.1.2 21 | 22 | ## 1.1.1 23 | 24 | * Rename package to practicalmeteor:mcli 25 | * Update starter-mcli-app to meteor 1.0 26 | * Update munit and chai dependencies to latest practicalmeteor ones 27 | * Add logging using practicalmeteor:loglevel 28 | 29 | ## 1.1.0 30 | 31 | * Update required meteor version to 0.9.3 32 | * Update spacejamio:chai and spacejamio:munit dependencies to 1.9.2_1 and 2.0.0 33 | * Add this changelog 34 | 35 | ### 1.0.2 36 | 37 | * Documentation update 38 | 39 | ### 1.0.1 40 | 41 | * Documentation update 42 | 43 | ### 1.0.0 44 | 45 | * Initial release 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 LaVaina Inc. 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/practicalmeteor/meteor-mcli.svg?branch=master)](https://travis-ci.org/practicalmeteor/meteor-mcli) 2 | practicalmeteor:mcli 3 | -------------- 4 | ## Overview 5 | 6 | A meteor package and command line tools for creating and running command line / cli programs with meteor. 7 | 8 | ## Incentive 9 | 10 | To be able to reuse the same code of your meteor app in your command line programs, instead of having to create a separate node / npm code base with lot's of code duplicated from your meteor app. 11 | 12 | ## Quickstart 13 | 14 | ```bash 15 | 16 | # jq is a command-line JSON processor that the mcli command line tool depends on. 17 | # Replace the first line with your linux's distribution way of installing packages. 18 | sudo apt-get install -y jq 19 | 20 | curl https://raw.githubusercontent.com/practicalmeteor/meteor-mcli/master/bin/install-starter-kit.sh | bash 21 | 22 | cd starter-mcli-app 23 | 24 | # Run the hello-world command in the meteor command line program. 25 | mcli hello-world 26 | ``` 27 | 28 | The starter-mcli-app is a fully functional meteor command line program that you should use as a base. 29 | 30 | ## API 31 | 32 | You can include multiple commands in a single meteor command line program. To register your commands: 33 | 34 | ```javascript 35 | 36 | // defaultOptions and async are optional 37 | CLI.registerCommand(commandName, functionToExecute, defaultOptions = {}, async = false); 38 | ``` 39 | 40 | For example: 41 | 42 | ```javascript 43 | 44 | var defaultOptions = { stderr: false }; 45 | 46 | var helloWorld = function(options) { 47 | if(options.stderr) 48 | console.error("Hello world from practicalmeteor:mcli!"); 49 | else 50 | console.info("Hello world from practicalmeteor:mcli!"); 51 | }; 52 | 53 | CLI.registerCommand('hello-world', helloWorld, defaultOptions); 54 | ``` 55 | 56 | To run your command, just: 57 | 58 | ```bash 59 | 60 | # Run this from your meteor command line program folder. 61 | mcli hello-world --stderr=true 62 | ``` 63 | 64 | Or, with a meteor settings file: 65 | 66 | ```bash 67 | 68 | mcli --settings my-settings.json hello-world --stderr=true 69 | ``` 70 | 71 | ## Async Commands API 72 | 73 | Unlike a meteor webapp, which never exits and therefore your async callbacks will always execute, command line programs with async callbacks will exit, unless you tell them not to using [futures](https://github.com/laverdet/node-fibers) or mcli's simplified async API based on futures. 74 | 75 | Here is an example async [ls](https://github.com/practicalmeteor/meteor-mcli/blob/master/starter-mcli-app/server/LsCommand.js) command from the starter-mcli-app: 76 | 77 | ```javascript 78 | 79 | var child_process = Npm.require('child_process'); 80 | 81 | // When you register your command as an async one, 82 | // you need to call CLI.done() when your command has completed. 83 | var lsCommand = function(options) { 84 | 85 | var ls = child_process.spawn("ls"); 86 | 87 | ls.stdout.setEncoding("utf8"); 88 | ls.stdout.on("data", function(data){ 89 | process.stdout.write(data); 90 | }); 91 | 92 | ls.stderr.setEncoding("utf8"); 93 | ls.stderr.on("data", function(data){ 94 | process.stderr.write(data); 95 | }); 96 | 97 | // You need to wait on a child process's close event, and not on it's exit event, 98 | // to make sure it has exited and all it's output has been delivered to you. 99 | ls.on("close", function(code, signal){ 100 | // Calling CLI.done() will let CLI know your command has completed and it can exit. 101 | CLI.done(); 102 | }); 103 | }; 104 | 105 | // When registering an async command, pass in true as the last argument. 106 | CLI.registerCommand('ls', lsCommand, {}, true); 107 | ``` 108 | 109 | ## Additional Examples 110 | 111 | You have additional examples of commands, including commands that take command line arguments as well as options, in the [starter-mcli-app](https://github.com/practicalmeteor/meteor-mcli/tree/master/starter-mcli-app/server) command line program. 112 | 113 | ## Command Line Options, Defaults and Arguments 114 | 115 | practicalmeteor:mcli uses the excellent [rc](https://www.npmjs.org/package/rc) npm command line parser and configurator. 116 | 117 | When you register your command, you provide a json object with default values only for command line options you want default values for. When your command is called, it will get an options json object that will include all the options specified on the command line, as well as defaults you specified for options that were not included. 118 | 119 | Command line options can also be specified using environment variables, prefixed with your command name, i.e. for the hello-world example above, before running your program, you can: 120 | 121 | ```bash 122 | 123 | export hello_world_stderr=true 124 | ``` 125 | 126 | Note that you will need to replace '-' in command names with '_' in your environment variables. 127 | 128 | Arguments provided on the command line are stored in the 'options._' array. An example can be found [here](https://github.com/practicalmeteor/meteor-mcli/blob/master/starter-mcli-app/server/EchoCommand.js). 129 | 130 | ## Using the same mongodb in your web app and your command line program(s) 131 | 132 | In development mode, every meteor app has it's own internal mongodb that is located inside an app's .meteor/local folder. 133 | 134 | Therefore, if you want to share a mongodb between your webapp and your command line programs, you will need to use an external mongodb and export the MONGO_URL environment variable so your meteor apps can connect to it. You can get a free sandbox database from [compose.io](https://www.compose.io/) (formerly mongohq), but there are other alternatives out there. 135 | 136 | ## Executing commands in a meteor build 137 | 138 | You can execute your commands in a meteor build by appending them to the standard meteor node command line, i.e. 139 | 140 | ```bash 141 | 142 | node main.js hello-world --stderr=true 143 | ``` 144 | 145 | mcli-bundle can be used to test your commands are working in a meteor build before deployment to production, by running: 146 | 147 | ```bash 148 | 149 | mcli-bundle hello-world --stderr=true 150 | ``` 151 | 152 | It will build your meteor program, extract it to a /tmp folder, install the npm packages and run your command, as above. You can also specify a settings file: 153 | 154 | ```bash 155 | 156 | mcli-bundle --settings my-settings.json hello-world --stderr=true 157 | ``` 158 | 159 | In this case, mcli-bundle will automatically set the METEOR_SETTINGS environment variable to the contents of your settings file. 160 | 161 | ## The hard way (i.e. creating your meteor mcli app from scratch) 162 | 163 | - Create your meteor app the standard way. 164 | 165 | - Run: 166 | 167 | ```bash 168 | 169 | # Meteor command line programs cannot include the webapp package, 170 | # as well as client side packages 171 | meteor remove meteor-platform 172 | 173 | # Add the meteor core 174 | meteor add meteor 175 | 176 | # Add mongo to access mongodb collections 177 | meteor add mongo 178 | 179 | # Add application-configuration to access 3rd party services 180 | meteor add application-configuration 181 | 182 | # Add practicalmeteor:mcli 183 | meteor add practicalmeteor:mcli 184 | ``` 185 | 186 | - meteor apps expect a main function which is the entry point to the app. The meteor webapp package provides just that. In a cli program, you will need to create your own main function that calls CLI.executeCommand, as in [here](https://github.com/practicalmeteor/meteor-mcli/blob/master/starter-mcli-app/server/main.js). 187 | 188 | - Install the jq json command line processor: 189 | 190 | ```bash 191 | 192 | # Replace the first line with your linux's distribution way of installing packages. 193 | sudo apt-get install -y jq 194 | ``` 195 | 196 | - Install the mcli and mcli-bundle tools: 197 | 198 | ```bash 199 | 200 | curl https://raw.githubusercontent.com/practicalmeteor/meteor-mcli/master/bin/install-mcli.sh | bash 201 | ``` 202 | 203 | ## How it works 204 | 205 | Since in local development mode, meteor cannot accept command line arguments, the mcli tool creates or extends your meteor settings file and adds the specified command line arguments as an array to Meteor.settings.argv. The practicalmeteor:cli package will read the command line from this setting, if it exists, or the normal way (with some meteor specific manipulation) from process.argv in a meteor build. 206 | 207 | ## Changelog 208 | 209 | [CHANGELOG](https://github.com/practicalmeteor/meteor-mcli/blob/master/CHANGELOG.md) 210 | 211 | ## License 212 | [MIT](https://github.com/practicalmeteor/meteor-mcli/blob/master/LICENSE.txt) 213 | 214 | ## Contributions 215 | Are more than welcome. Would be nice to: 216 | 217 | - Connect to the internal meteor mongodb database of a running meteor app. 218 | 219 | - Start and stop the internal meteor mongodb database, if a meteor app is not running. 220 | 221 | - Have a meteor mcli app scaffolding tool. 222 | -------------------------------------------------------------------------------- /bin/install-mcli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | sudo curl -o /usr/local/bin/mcli https://raw.githubusercontent.com/practicalmeteor/meteor-mcli/master/bin/mcli 4 | sudo chmod 555 /usr/local/bin/mcli 5 | sudo curl -o /usr/local/bin/mcli-bundle https://raw.githubusercontent.com/practicalmeteor/meteor-mcli/master/bin/mcli-bundle 6 | sudo chmod 555 /usr/local/bin/mcli-bundle 7 | -------------------------------------------------------------------------------- /bin/install-starter-kit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | sudo curl -L -o /tmp/mcli.tar.gz https://github.com/practicalmeteor/meteor-mcli/archive/master.tar.gz 4 | sudo tar xf /tmp/mcli.tar.gz -C /tmp 5 | sudo mv /tmp/meteor-mcli-master/bin/mcli /tmp/meteor-mcli-master/bin/mcli-bundle /usr/local/bin 6 | sudo chmod 555 /usr/local/bin/mcli /usr/local/bin/mcli-bundle 7 | echo "mcli and mcli-bundle scripts installed in /usr/local/bin" 8 | cp -r /tmp/meteor-mcli-master/starter-mcli-app . 9 | sudo rm -rf /tmp/mcli.tar.gz /tmp/meteor-mcli-master 10 | echo "A starter meteor command line program using practicalmeteor:mcli was created in your current directory." 11 | echo "To run your new meteor command line program:" 12 | echo " cd starter-mcli-app" 13 | echo " mcli hello-world" 14 | -------------------------------------------------------------------------------- /bin/mcli: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ -z "$1" ]; then 4 | >&2 echo "Error: you need to provide at least a command name." 5 | exit 1 6 | fi 7 | 8 | argv="[" 9 | 10 | while (( "$#" )); do 11 | if [ "$1" == "--settings" ]; then 12 | if [ ! -e $2 ]; then 13 | >&2 echo "Error: The specified settings file doesn't exist." 14 | exit 1 15 | fi 16 | meteor_settings_path=$2 17 | shift 18 | else 19 | if [ "$argv" == "[" ]; then 20 | argv="$argv\"$1\"" 21 | else 22 | argv="$argv, \"$1\"" 23 | fi 24 | fi 25 | shift 26 | done 27 | 28 | argv="$argv]" 29 | 30 | if [ -n "$meteor_settings_path" ]; then 31 | jq ". + {argv: $argv }" < $meteor_settings_path > /tmp/settings.json 32 | else 33 | echo "{ \"argv\": $argv }" > /tmp/settings.json 34 | fi 35 | 36 | export METEOR_NO_WEBAPP=1 37 | 38 | meteor --once --settings /tmp/settings.json 39 | -------------------------------------------------------------------------------- /bin/mcli-bundle: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ "$1" == "--settings" ]; then 4 | if [ ! -e $2 ]; then 5 | >&2 echo "Error: The specified settings file doesn't exist." 6 | exit 1 7 | fi 8 | meteor_settings_path=$2 9 | shift 2 10 | fi 11 | 12 | if [ -z "$1" ]; then 13 | >&2 echo "Error: you need to provide at least a command name." 14 | exit 1 15 | fi 16 | 17 | if [ -n "$meteor_settings_path" ]; then 18 | export METEOR_SETTINGS=$(tr '\n' ' ' < $meteor_settings_path) 19 | fi 20 | 21 | app_name=$(basename $PWD) 22 | 23 | sudo rm -rf /tmp/${app_name}* 24 | 25 | meteor build /tmp 26 | 27 | cd /tmp 28 | 29 | tar xf $app_name.tar.gz 30 | 31 | cd /tmp/bundle 32 | 33 | (cd programs/server && npm install) 34 | 35 | export METEOR_NO_WEBAPP=1 36 | 37 | node main.js "$@" 38 | -------------------------------------------------------------------------------- /bin/test-mcli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | # Test the mcli package 4 | spacejam test-packages ./ 5 | 6 | # Test the mcli and mcli-bundle tools 7 | export PATH=/usr/local/bin:$PATH:$PWD/bin 8 | export PACKAGE_DIRS=$(dirname $PWD) 9 | cd starter-mcli-app 10 | mcli hello-world 11 | mcli echo --stderr=true I am an stderr echo 12 | mcli find-one --collection Todos 13 | # Async command 14 | mcli ls 15 | mcli-bundle find-one --collection Todos 16 | # Async command 17 | mcli-bundle ls 18 | 19 | # Test the install-starter-kit.sh script 20 | cd .. 21 | mkdir -p tmp 22 | cd tmp 23 | curl https://raw.githubusercontent.com/practicalmeteor/meteor-mcli/$TRAVIS_COMMIT/bin/install-starter-kit.sh | bash 24 | if [ $(which mcli) != "/usr/local/bin/mcli" ]; then 25 | exit 1 26 | fi 27 | if [ $(which mcli-bundle) != "/usr/local/bin/mcli-bundle" ]; then 28 | exit 1 29 | fi 30 | if [ ! -d starter-mcli-app ]; then 31 | exit 1 32 | fi 33 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Npm.depends({ 2 | 'rc': '0.5.1' 3 | }); 4 | 5 | 6 | Package.describe({ 7 | summary: "A package and tools for creating and running command line / cli programs with meteor.", 8 | name: "practicalmeteor:mcli", 9 | version: "1.1.6", 10 | git: "https://github.com/practicalmeteor/meteor-mcli.git" 11 | }); 12 | 13 | 14 | Package.onUse(function (api) { 15 | api.versionsFrom('0.9.3'); 16 | 17 | api.use([ 18 | "coffeescript", 19 | "underscore", 20 | "practicalmeteor:chai", 21 | "practicalmeteor:loglevel@1.2.0_1", 22 | 'practicalmeteor:underscore.string@2.3.3_3' 23 | ], "server"); 24 | 25 | api.addFiles(["src/log.js"], 'server'); 26 | api.addFiles(["src/MeteorNoops.coffee"], 'server'); 27 | api.add_files("src/CLI.coffee", "server"); 28 | 29 | api.export("CLI", "server"); 30 | }); 31 | 32 | 33 | Package.onTest(function(api) { 34 | api.use(["coffeescript", "practicalmeteor:mcli", "practicalmeteor:loglevel@1.2.0_1", "practicalmeteor:munit@2.1.2"], 'server'); 35 | api.add_files("tests/CLITest.coffee", "server") 36 | }); 37 | -------------------------------------------------------------------------------- /src/CLI.coffee: -------------------------------------------------------------------------------- 1 | rc = Npm.require('rc') 2 | Future = Npm.require('fibers/future'); 3 | 4 | @practical ?= {} 5 | 6 | class practical.CLI 7 | 8 | @instance: null 9 | 10 | registeredCommands: { } 11 | 12 | future: null 13 | 14 | @get:-> 15 | practical.CLI.instance ?= new CLI() 16 | 17 | constructor: -> 18 | log.debug("NODE_ENV=#{process.env.NODE_ENV}") 19 | 20 | 21 | executeCommand: -> 22 | log.debug('CLI.executeCommand()', process.argv) 23 | 24 | argv = Meteor?.settings?.argv 25 | 26 | if argv 27 | expect(argv, "Meteor.settings.argv is expected to be an array").to.be.an 'array' 28 | argv.unshift("main.js") 29 | argv.unshift("node") 30 | process.argv = argv 31 | else 32 | # In a meteor bundle, the first arg is node, the 2nd main.js, and the 3rd program.json 33 | # We need to remove program.json, so it will not be interpreted by rc as a command line argument. 34 | expect(process.argv[2], "program.json was expected at process.argv[2]").to.equal 'program.json' 35 | process.argv.splice(2, 1) 36 | 37 | # THe first arg after is always the name of the command to execute. 38 | 39 | # Meteor._debug process.argv.join(' ') 40 | 41 | expect(process.argv, "No command specified").to.have.length.above(2) 42 | 43 | commandName = process.argv[2] 44 | 45 | command = @registeredCommands[commandName] 46 | expect(command, "#{commandName} is not a registered cli command").to.to.be.an 'object' 47 | 48 | # Remove the command, so rc doesn't interpret it as a command line argument. 49 | process.argv.splice(2, 1) 50 | 51 | options = rc(commandName.replace('-', '_'), command.defaultOptions) 52 | 53 | # Execute the registered command 54 | if not command.async 55 | log.debug("Executing '#{commandName}' with options:\n", options) 56 | command.func options 57 | else 58 | log.debug("Executing async '#{commandName}' with options:\n", options) 59 | @future = new Future() 60 | command.func options, @done 61 | @future.wait() 62 | 63 | 64 | done: => 65 | log.debug("CLI.done()") 66 | expect(@future, "command is not async, cannot call done").to.be.an 'object' 67 | expect(@future.isResolved(), "done already called").to.be.false 68 | 69 | @future.return(null) 70 | 71 | 72 | # Note: defaultOptions will be mutated by actual command line options. 73 | registerCommand: (name, func, defaultOptions = {}, async = false) -> 74 | log.debug("CLI.registerCommand()") 75 | expect(name, "command name is missing").to.be.a("string") 76 | expect(func, "command function is missing").to.be.a("function") 77 | expect(defaultOptions, "command defaultOptions is not an object").to.be.a("object") 78 | 79 | log.debug("Registering '#{name}' with default options:\n", defaultOptions) 80 | 81 | @registeredCommands[name] = { func: func, defaultOptions: defaultOptions, async: async } 82 | 83 | 84 | CLI = practical.CLI.get() 85 | -------------------------------------------------------------------------------- /src/MeteorNoops.coffee: -------------------------------------------------------------------------------- 1 | if not Meteor.server 2 | # Meteor.server is not defined, creating noop implementations for all meteor ddp methods 3 | _.each(['publish', 'methods', 'call', 'apply', 'onConnection'], 4 | (name) -> 5 | Meteor[name] = -> 6 | ); 7 | -------------------------------------------------------------------------------- /src/log.js: -------------------------------------------------------------------------------- 1 | log = loglevel.createPackageLogger('practicalmeteor:mcli', 'info'); 2 | -------------------------------------------------------------------------------- /starter-mcli-app/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | -------------------------------------------------------------------------------- /starter-mcli-app/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /starter-mcli-app/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1o6yank15ruk46sau3wk 8 | -------------------------------------------------------------------------------- /starter-mcli-app/.meteor/cordova-plugins: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /starter-mcli-app/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | practicalmeteor:mcli@1.1.6 7 | mongo 8 | application-configuration 9 | meteor 10 | practicalmeteor:loglevel 11 | -------------------------------------------------------------------------------- /starter-mcli-app/.meteor/platforms: -------------------------------------------------------------------------------- 1 | browser 2 | server 3 | -------------------------------------------------------------------------------- /starter-mcli-app/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.0.5 2 | -------------------------------------------------------------------------------- /starter-mcli-app/.meteor/versions: -------------------------------------------------------------------------------- 1 | application-configuration@1.0.4 2 | base64@1.0.3 3 | binary-heap@1.0.3 4 | callback-hook@1.0.3 5 | check@1.0.5 6 | coffeescript@1.0.6 7 | ddp@1.1.0 8 | ejson@1.0.6 9 | follower-livedata@1.0.3 10 | geojson-utils@1.0.3 11 | id-map@1.0.3 12 | json@1.0.3 13 | logging@1.0.7 14 | meteor@1.1.5 15 | minimongo@1.0.7 16 | mongo@1.1.0 17 | ordered-dict@1.0.3 18 | practicalmeteor:chai@2.1.0_1 19 | practicalmeteor:loglevel@1.2.0_1 20 | practicalmeteor:mcli@1.1.6 21 | practicalmeteor:underscore.string@2.3.3_3 22 | random@1.0.3 23 | retry@1.0.3 24 | tracker@1.0.6 25 | underscore@1.0.3 26 | -------------------------------------------------------------------------------- /starter-mcli-app/server/EchoCommand.js: -------------------------------------------------------------------------------- 1 | // If called without --prefix, the default prefix will be used. 2 | // To print to stderr, set '--stderr=true' 3 | var defaultOptions = { 4 | prefix: "prefix:", 5 | stderr: false 6 | }; 7 | 8 | var echoCommand = function(options) { 9 | var string = "No string to echo was provided."; 10 | if(options._ && options._.length > 0) 11 | string = options._.join(' '); 12 | 13 | msg = options.prefix + ' ' + string; 14 | 15 | if(options.postfix) 16 | msg += ' ' + options.postfix; 17 | 18 | if(options.stderr) 19 | console.error(msg); 20 | else 21 | console.info(msg); 22 | }; 23 | 24 | CLI.registerCommand('echo', echoCommand, defaultOptions); 25 | -------------------------------------------------------------------------------- /starter-mcli-app/server/FindOneCommand.js: -------------------------------------------------------------------------------- 1 | // There is no need to provide default options, if none should be set, rc will provide all options anyway, 2 | // but we like to specify them with null values, for command documentation purposes. 3 | 4 | //var defaultOptions = { 5 | // collection: null 6 | //}; 7 | 8 | var findOneCommand = function(options) { 9 | // We use the meteor logging package, instead of console.log and console.error 10 | if( ! options.collection ) 11 | console.error('Error: --collection is missing.'); 12 | 13 | if( ! global[options.collection] ) { 14 | console.error("Error: The collection" + options.collection + " doesn't exist."); 15 | return; 16 | } 17 | 18 | doc = global[options.collection].findOne(); 19 | if(doc) 20 | console.info(JSON.stringify(doc, null, 2)); 21 | else 22 | console.error('Error: No documents found in the ' + options.collection + ' collection.'); 23 | }; 24 | 25 | CLI.registerCommand('find-one', findOneCommand); 26 | -------------------------------------------------------------------------------- /starter-mcli-app/server/HelloWorldCommand.js: -------------------------------------------------------------------------------- 1 | CLI.registerCommand('hello-world', function(options) { 2 | if(options.stderr) 3 | console.error("Hello world from practicalmeteor:mcli!"); 4 | else 5 | console.info("Hello world from practicalmeteor:mcli!"); 6 | }); 7 | -------------------------------------------------------------------------------- /starter-mcli-app/server/LsCommand.js: -------------------------------------------------------------------------------- 1 | // Example of an async command that spawns an ls child process, 2 | // waits for it to exit, and then calls CLI.done() 3 | 4 | var log = loglevel.createLogger('ls', 'debug'); 5 | 6 | var child_process = Npm.require('child_process'); 7 | 8 | // When you register your command as an async one, 9 | // you need to call CLI.done() when your command has completed. 10 | var lsCommand = function(options) { 11 | 12 | log.debug('spawning ls'); 13 | var ls = child_process.spawn("ls"); 14 | log.debug('ls spawned'); 15 | 16 | ls.stdout.setEncoding("utf8"); 17 | ls.stdout.on("data", function(data){ 18 | process.stdout.write(data); 19 | }); 20 | 21 | ls.stderr.setEncoding("utf8"); 22 | ls.stderr.on("data", function(data){ 23 | process.stderr.write(data); 24 | }); 25 | 26 | // You need to wait on a child process's close event, and not on it's exit event, 27 | // to make sure it has exited and all it's output has been delivered to you. 28 | ls.on("close", function(code, signal){ 29 | log.debug('ls closed'); 30 | // Calling CLI.done() will let CLI know your command has completed and it can exit. 31 | CLI.done(); 32 | }); 33 | }; 34 | 35 | // When registering an async command, pass in true as the last argument. 36 | CLI.registerCommand('ls', lsCommand, {}, true); 37 | -------------------------------------------------------------------------------- /starter-mcli-app/server/main.js: -------------------------------------------------------------------------------- 1 | var mainCalled = false; 2 | 3 | main = function() { 4 | if (mainCalled) { 5 | throw new Error("main was already called!"); 6 | } 7 | 8 | CLI.executeCommand(); 9 | }; 10 | -------------------------------------------------------------------------------- /starter-mcli-app/server/todos-bootstrap.js: -------------------------------------------------------------------------------- 1 | // if the database is empty on server start, create some sample data. 2 | Meteor.startup(function () { 3 | if (Lists.find().count() === 0) { 4 | var data = [ 5 | {name: "Meteor Principles", 6 | contents: [ 7 | ["Data on the Wire", "Simplicity", "Better UX", "Fun"], 8 | ["One Language", "Simplicity", "Fun"], 9 | ["Database Everywhere", "Simplicity"], 10 | ["Latency Compensation", "Better UX"], 11 | ["Full Stack Reactivity", "Better UX", "Fun"], 12 | ["Embrace the Ecosystem", "Fun"], 13 | ["Simplicity Equals Productivity", "Simplicity", "Fun"] 14 | ] 15 | }, 16 | {name: "Languages", 17 | contents: [ 18 | ["Lisp", "GC"], 19 | ["C", "Linked"], 20 | ["C++", "Objects", "Linked"], 21 | ["Python", "GC", "Objects"], 22 | ["Ruby", "GC", "Objects"], 23 | ["JavaScript", "GC", "Objects"], 24 | ["Scala", "GC", "Objects"], 25 | ["Erlang", "GC"], 26 | ["6502 Assembly", "Linked"] 27 | ] 28 | }, 29 | {name: "Favorite Scientists", 30 | contents: [ 31 | ["Ada Lovelace", "Computer Science"], 32 | ["Grace Hopper", "Computer Science"], 33 | ["Marie Curie", "Physics", "Chemistry"], 34 | ["Carl Friedrich Gauss", "Math", "Physics"], 35 | ["Nikola Tesla", "Physics"], 36 | ["Claude Shannon", "Math", "Computer Science"] 37 | ] 38 | } 39 | ]; 40 | 41 | var timestamp = (new Date()).getTime(); 42 | for (var i = 0; i < data.length; i++) { 43 | var list_id = Lists.insert({name: data[i].name}); 44 | for (var j = 0; j < data[i].contents.length; j++) { 45 | var info = data[i].contents[j]; 46 | Todos.insert({list_id: list_id, 47 | text: info[0], 48 | timestamp: timestamp, 49 | tags: info.slice(1)}); 50 | timestamp += 1; // ensure unique timestamp. 51 | } 52 | } 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /starter-mcli-app/server/todos-collection.js: -------------------------------------------------------------------------------- 1 | Lists = new Mongo.Collection("lists"); 2 | 3 | Todos = new Mongo.Collection("todos"); 4 | -------------------------------------------------------------------------------- /tests/CLITest.coffee: -------------------------------------------------------------------------------- 1 | Stubs = Munit.stubs 2 | Spies = Munit.spies 3 | 4 | log = loglevel.createPackageLogger('local-test:practicalmeteor:mcli', 'debug') 5 | 6 | describe "CLI", -> 7 | 8 | cli = null 9 | processArgv = null 10 | logSpy = null 11 | actualOptions = null 12 | parseOptionsCommand = (options)=> 13 | actualOptions = options 14 | 15 | asyncDoneArgCommand = (options, done)-> 16 | Meteor.defer -> 17 | log.debug('asyncDoneArgCommand on defer') 18 | done() 19 | 20 | asyncDoneCallCommand = (options)-> 21 | Meteor.defer -> 22 | log.debug('asyncDoneCallCommand on defer') 23 | cli.done() 24 | 25 | beforeAll -> 26 | processArgv = process.argv 27 | 28 | 29 | afterAll -> 30 | process.argv = processArgv 31 | 32 | 33 | beforeEach -> 34 | Spies.restoreAll() 35 | actualOptions = null 36 | console.info 'beforeEach' 37 | logSpy = Spies.create "logSpy", console, 'log' 38 | 39 | cli = new practical.CLI() 40 | cli.registerCommand 'hello-world', (opts) -> 41 | console.log "Hello world from practicalmeteor:mcli" 42 | 43 | cli.registerCommand 'echo', (opts) -> 44 | console.log opts.string 45 | , _.clone({string: "I am echoing the --string default"}) 46 | 47 | cli.registerCommand 'parse-options', parseOptionsCommand 48 | 49 | cli.registerCommand 'async-done-arg', asyncDoneArgCommand, {}, true 50 | 51 | cli.registerCommand 'async-done-call', asyncDoneArgCommand, {}, true 52 | 53 | it 'registerCommand - should have hello-world and echo registered', -> 54 | expect(cli.registeredCommands['hello-world']).to.be.an 'object' 55 | expect(cli.registeredCommands['hello-world']).to.have.keys ['func', 'defaultOptions', 'async'] 56 | expect(cli.registeredCommands['hello-world'].func).to.be.a 'function' 57 | expect(cli.registeredCommands['hello-world'].defaultOptions).to.be.an 'object' 58 | expect(cli.registeredCommands['hello-world'].defaultOptions).to.be.empty 59 | expect(cli.registeredCommands['hello-world'].async).to.be.false 60 | 61 | expect(cli.registeredCommands['echo']).to.be.an 'object' 62 | expect(cli.registeredCommands['echo']).to.be.to.have.keys ['func', 'defaultOptions', 'async'] 63 | expect(cli.registeredCommands['echo'].func).to.be.a 'function' 64 | expect(cli.registeredCommands['echo'].defaultOptions).to.be.an 'object' 65 | console.log cli.registeredCommands['echo'].defaultOptions 66 | expect(cli.registeredCommands['echo'].defaultOptions).to.have.key 'string' 67 | expect(cli.registeredCommands['echo'].defaultOptions.string).to.equal "I am echoing the --string default" 68 | expect(cli.registeredCommands['hello-world'].async).to.be.false 69 | 70 | 71 | expect(cli.registeredCommands['async-done-arg']).to.be.an 'object' 72 | expect(cli.registeredCommands['async-done-arg'].async).to.be.true 73 | 74 | 75 | it 'executeCommand - should execute the hello-world command', -> 76 | process.argv = ['node', 'main.js', 'program.json', 'hello-world'] 77 | cli.executeCommand() 78 | chai.assert logSpy.calledWith "Hello world from practicalmeteor:mcli" 79 | 80 | 81 | it 'executeCommand - should remove program.json and the command name from process.argv', -> 82 | process.argv = ['node', 'main.js', 'program.json', 'hello-world'] 83 | cli.executeCommand() 84 | expect(process.argv).to.have.length 2 85 | expect(process.argv[0]).to.equal 'node' 86 | expect(process.argv[1]).to.equal 'main.js' 87 | 88 | 89 | it 'executeCommand - should execute the echo command with the default string', -> 90 | process.argv = ['node', 'main.js', 'program.json', 'echo'] 91 | cli.executeCommand() 92 | chai.assert logSpy.calledWith "I am echoing the --string default" 93 | 94 | 95 | it 'executeCommand - should execute the echo command with a provided string', -> 96 | process.argv = ['node', 'main.js', 'program.json', 'echo', '--string', 'I am echoing this string'] 97 | cli.executeCommand() 98 | chai.assert logSpy.calledWith "I am echoing this string" 99 | 100 | 101 | it 'executeCommand - should execute the echo command with a string defined in env', -> 102 | process.argv = ['node', 'main.js', 'program.json', 'echo'] 103 | process.env.echo_string = 'I am echoing an env string' 104 | cli.executeCommand() 105 | chai.assert logSpy.calledWith "I am echoing an env string" 106 | 107 | 108 | it 'executeCommand - should execute the async-done-arg command and wait for it to be done', ()-> 109 | process.argv = ['node', 'main.js', 'program.json', 'async-done-arg'] 110 | cli.executeCommand() 111 | expect(cli.future.isResolved()).to.be.true 112 | 113 | 114 | it 'executeCommand - should execute the async-done-call command and wait for it to be done', ()-> 115 | process.argv = ['node', 'main.js', 'program.json', 'async-done-call'] 116 | cli.executeCommand() 117 | expect(cli.future.isResolved()).to.be.true 118 | 119 | 120 | it 'executeCommand - should use Meteor.settings.argv, if it exists', -> 121 | process.argv = processArgv 122 | Meteor.settings.argv = ['hello-world'] 123 | cli.executeCommand() 124 | chai.assert logSpy.calledWith "Hello world from practicalmeteor:mcli" 125 | expect(process.argv[0]).to.equal 'node' 126 | expect(process.argv[1]).to.equal 'main.js' 127 | expect(process.argv).to.have.length 2 128 | 129 | 130 | it 'executeCommand - should fail if no command was provided', -> 131 | process.argv = ['node', 'main.js', 'program-json'] 132 | expect(CLI.executeCommand).to.throw(Error) 133 | 134 | 135 | it 'executeCommand - should fail if command is not registered', -> 136 | process.argv = ['node', 'main.js', 'program-json', 'not-registered'] 137 | expect(CLI.executeCommand).to.throw(Error) 138 | --------------------------------------------------------------------------------