├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── bin └── cli.js ├── package.json └── src ├── client.js ├── daemon.js ├── launch.js ├── log.js └── port.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": ["transform-runtime", "closure-elimination"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb-base", 4 | "env": { 5 | "node": true 6 | }, 7 | "rules": { 8 | "consistent-return": 0, 9 | "no-console": 0, 10 | "no-param-reassign": 0, 11 | "prefer-template": 0, 12 | "semi": ["error", "never"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | node_modules 3 | npm-debug.log* 4 | lib 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "4" 5 | - "5" 6 | 7 | install: 8 | - npm install 9 | 10 | script: 11 | - npm run lint 12 | - npm run build 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Jhen-Jie Hong 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remote Redux Dispatch CLI [![NPM version](http://img.shields.io/npm/v/redux-dispatch-cli.svg?style=flat)](https://www.npmjs.com/package/redux-dispatch-cli) 2 | 3 | > A CLI tool for Redux remote dispatch. Used in [remote-redux-devtools](https://github.com/zalmoxisus/remote-redux-devtools) 4 | 5 | ## Screenshot 6 | 7 | ![Screenshot](https://cloud.githubusercontent.com/assets/3001525/14658566/56aaecc0-06c7-11e6-8f21-f3503c3faf24.gif) 8 | 9 | ## Installation 10 | 11 | ```bash 12 | $ npm install -g redux-dispatch-cli 13 | ``` 14 | 15 | ## Usage 16 | 17 | Use `redux-dispatch` or `rrd` command. 18 | 19 | ```bash 20 | # Connect to remotedev-server 21 | $ redux-dispatch connect --hostname --port 22 | 23 | # Show instance list 24 | $ redux-dispatch ls-instance 25 | 26 | # Select instance 27 | $ redux-dispatch select 28 | 29 | # Sync currently selected instance states to all instances 30 | $ redux-dispatch sync 31 | 32 | # Dispatch action 33 | $ redux-dispatch action "{ type: 'ACTION', ... }" 34 | 35 | # Start daemon (`connect` can also start daemon) 36 | $ redux-dispatch start 37 | # Restart daemon 38 | $ redux-dispatch restart 39 | # Stop daemon 40 | $ redux-dispatch stop 41 | # Check daemon status 42 | $ redux-dispatch status 43 | ``` 44 | 45 | Run `redux-dispatch --help` or `redux-dispatch --help` for more information. 46 | 47 | ## Steps 48 | 49 | #### Connect to [remotedev-server](https://github.com/zalmoxisus/remotedev-server) (hostname default: `localhost`) 50 | 51 | ```bash 52 | $ rrd connect --hostname --port 53 | ``` 54 | 55 | It will create a daemon, the daemon process will exit when `$HOME/.remotedev_d_port` is removed. 56 | 57 | #### Show available instances 58 | 59 | ```bash 60 | $ rrd ls-instance 61 | ``` 62 | 63 | Make sure have instance can dispatch action. 64 | 65 | #### Select a instance (default: `auto`) 66 | 67 | ```bash 68 | $ rrd select 69 | ``` 70 | 71 | #### Dispatch action 72 | 73 | ```bash 74 | $ rrd action "{ type: 'ACTION', a: 1 }" 75 | ``` 76 | 77 | ## Credits 78 | 79 | * Remote store create from [zalmoxisus/remotedev-app](https://github.com/zalmoxisus/remotedev-app) 80 | * Daemon inspired by [mantoni/eslint_d.js](https://github.com/mantoni/eslint_d.js) 81 | * Used [chentsulin/react-native-counter-ios-android](https://github.com/chentsulin/react-native-counter-ios-android) as a example of screenshot 82 | 83 | ## License 84 | 85 | [MIT](LICENSE.md) 86 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var client = require('../lib/client') 4 | 5 | function noArg(yargs) { 6 | return yargs 7 | } 8 | 9 | require('yargs') 10 | .usage('Usage: $0 [options]') 11 | .command('connect', 'Connect to remotedev-server', function(yargs) { 12 | return yargs 13 | .option('hostname', { 14 | alias: 'host', 15 | default: 'localhost' 16 | }) 17 | .option('port', { 18 | alias: 'p', 19 | default: '8000' 20 | }) 21 | .option('secure', { 22 | alias: 's', 23 | default: false 24 | }) 25 | }, function(argv) { 26 | client.connect({ 27 | hostname: argv.host, 28 | port: argv.port, 29 | secure: !!argv.secure 30 | }).then(log => console.log(log)) 31 | }) 32 | .command('ls-instance', 'Show instance list', noArg, function() { 33 | client.lsInstance().then(log => console.log(log)) 34 | }) 35 | .command('select', 'Select instance', noArg, function(argv) { 36 | client.select(argv._[1] || 'auto').then(log => console.log(log)) 37 | }) 38 | .command('sync', 'Sync', noArg, function(argv) { 39 | client.sync().then(log => console.log(log)) 40 | }) 41 | .command('action', 'Dispatch action', noArg, function(argv) { 42 | if (!argv._[1]) return 43 | const getAction = new Function('return ' + argv._[1]) 44 | client.action(getAction()).then(log => console.log(log)) 45 | }) 46 | .command('start', 'Start daemon (`connect` can also start daemon)', noArg, function() { 47 | client.start().then(log => console.log(log)) 48 | }) 49 | .command('restart', 'Restart daemon', noArg, function() { 50 | client.restart().then(log => console.log(log)) 51 | }) 52 | .command('stop', 'Stop daemon', noArg, function() { 53 | client.stop().then(log => console.log(log)) 54 | }) 55 | .command('status', 'Check daemon status', noArg, function() { 56 | client.status().then(log => console.log(log)) 57 | }) 58 | .help('h') 59 | .alias('h', 'help') 60 | .detectLocale(false) 61 | .argv 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-dispatch-cli", 3 | "version": "0.1.2", 4 | "description": "A CLI tool for Redux remote dispatch. Used in remote-redux-devtools", 5 | "bin": { 6 | "redux-dispatch": "bin/cli.js", 7 | "rrd": "bin/cli.js" 8 | }, 9 | "files": [ 10 | "bin", 11 | "lib" 12 | ], 13 | "scripts": { 14 | "lint": "eslint src", 15 | "build": "babel --out-dir lib src", 16 | "prepublish": "npm run lint && npm run build" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/jhen0409/redux-dispatch-cli.git" 21 | }, 22 | "keywords": [ 23 | "remote", 24 | "redux", 25 | "devtools", 26 | "react", 27 | "remotedev" 28 | ], 29 | "author": "Jhen ", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/jhen0409/redux-dispatch-cli/issues" 33 | }, 34 | "homepage": "https://github.com/jhen0409/redux-dispatch-cli#readme", 35 | "dependencies": { 36 | "babel-runtime": "^6.6.1", 37 | "koa": "^2.0.0", 38 | "koa-bodyparser": "^3.0.0", 39 | "koa-logger": "^2.0.0", 40 | "koa-router": "^7.0.1", 41 | "node-fetch": "^1.5.1", 42 | "node-watch": "^0.3.5", 43 | "remotedev-app": "^0.3.8", 44 | "yargs": "^4.6.0" 45 | }, 46 | "devDependencies": { 47 | "babel-cli": "^6.7.5", 48 | "babel-core": "^6.7.6", 49 | "babel-eslint": "^6.0.2", 50 | "babel-plugin-closure-elimination": "^1.0.6", 51 | "babel-plugin-transform-runtime": "^6.7.5", 52 | "babel-preset-es2015": "^6.6.0", 53 | "babel-preset-stage-0": "^6.5.0", 54 | "eslint": "^2.8.0", 55 | "eslint-config-airbnb-base": "^1.0.3", 56 | "eslint-plugin-import": "^1.5.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import { RUNNING, NOT_RUNNING, ALREADY_STARTED, LAUNCH_SUCCESS, LAUNCH_FAILED } from './log' 2 | import * as portfile from './port' 3 | import launchDaemon from './launch' 4 | import fetch from 'node-fetch' 5 | import net from 'net' 6 | 7 | let port 8 | let host 9 | 10 | function readPort() { 11 | port = portfile.read() 12 | host = `http://localhost:${port}` 13 | } 14 | 15 | readPort() 16 | 17 | function check() { 18 | return new Promise(resolve => { 19 | if (!port) return resolve(false) 20 | const socket = net.connect(port, () => { 21 | socket.end() 22 | resolve(true) 23 | }) 24 | socket.on('error', () => resolve(false)) 25 | }) 26 | } 27 | 28 | function get(url) { 29 | return fetch(url).then(res => res.text()) 30 | } 31 | 32 | function post(url, body) { 33 | return fetch(url, { 34 | method: 'POST', 35 | body: JSON.stringify(body), 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | }, 39 | }).then(res => res.text()) 40 | } 41 | 42 | export async function connect(options) { 43 | const running = await check() 44 | if (!running) { 45 | const isRunning = await launchDaemon() 46 | readPort() 47 | if (!isRunning) return LAUNCH_FAILED 48 | } 49 | return post(`${host}/connect`, options) 50 | } 51 | 52 | export async function lsInstance() { 53 | const running = await check() 54 | if (!running) return NOT_RUNNING 55 | return get(`${host}/ls-instance`) 56 | } 57 | 58 | export async function select(instance) { 59 | const running = await check() 60 | if (!running) return NOT_RUNNING 61 | return post(`${host}/select`, { instance }) 62 | } 63 | 64 | export async function sync() { 65 | const running = await check() 66 | if (!running) return NOT_RUNNING 67 | return post(`${host}/sync`) 68 | } 69 | 70 | export async function action(obj) { 71 | const running = await check() 72 | if (!running) return NOT_RUNNING 73 | return post(`${host}/action`, obj) 74 | } 75 | 76 | export async function start() { 77 | const running = await check() 78 | if (running) return ALREADY_STARTED 79 | 80 | const isRunning = await launchDaemon() 81 | readPort() 82 | if (!isRunning) return LAUNCH_FAILED 83 | return LAUNCH_SUCCESS 84 | } 85 | 86 | export async function restart() { 87 | const running = await check() 88 | if (!running) return NOT_RUNNING 89 | 90 | portfile.unlink() 91 | const isRunning = await launchDaemon() 92 | readPort() 93 | if (!isRunning) return LAUNCH_FAILED 94 | return LAUNCH_SUCCESS 95 | } 96 | 97 | export async function stop() { 98 | const running = await check() 99 | if (!running) return NOT_RUNNING 100 | return post(`${host}/stop`) 101 | } 102 | 103 | export async function status() { 104 | const running = await check() 105 | if (!running) return NOT_RUNNING 106 | return RUNNING 107 | } 108 | -------------------------------------------------------------------------------- /src/daemon.js: -------------------------------------------------------------------------------- 1 | import Koa from 'koa' 2 | import body from 'koa-bodyparser' 3 | import logger from 'koa-logger' 4 | import Router from 'koa-router' 5 | import * as portfile from './port' 6 | import { NO_CONNECTION, NOT_FOUND_INSTANCE, NOT_SELECT_INSTANCE, NO_TYPE } from './log' 7 | import { 8 | createRemoteStore, 9 | updateStoreInstance, 10 | enableSync, 11 | } from 'remotedev-app/lib/store/createRemoteStore' 12 | 13 | let selectedInstance 14 | let instances 15 | let store 16 | let shouldSync 17 | 18 | const app = new Koa() 19 | app.use(logger()) 20 | app.use(body()) 21 | 22 | const router = new Router() 23 | const haveConnection = async (ctx, next) => { 24 | if (!store) { 25 | ctx.body = NO_CONNECTION 26 | return 27 | } 28 | await next() 29 | } 30 | 31 | router.post('/connect', async ctx => { 32 | const { hostname, port, secure } = ctx.request.body 33 | selectedInstance = 'auto' 34 | instances = {} 35 | shouldSync = false 36 | store = createRemoteStore({ 37 | hostname, 38 | port, 39 | secure, 40 | autoReconnect: true, 41 | autoReconnectOptions: { 42 | delay: 3E3, 43 | randomness: 1E3, 44 | }, 45 | }, (instance, name, toRemove) => { 46 | if (toRemove) { 47 | delete instances[instance] 48 | store.liftedStore.deleteInstance(instance) 49 | if (selectedInstance === instance) { 50 | selectedInstance = 'auto' 51 | updateStoreInstance('auto') 52 | shouldSync = false 53 | return 54 | } 55 | } else { 56 | instances[instance] = name || instance 57 | } 58 | }, selectedInstance) 59 | ctx.body = '' 60 | }) 61 | 62 | router.get('/ls-instance', haveConnection, async ctx => { 63 | let output = 'Name\t\tInstanceKey\n' 64 | Object.keys(instances).forEach(key => { 65 | const selected = selectedInstance === key 66 | output += `${instances[key]}\t\t${key}` + (selected ? '\t(Selected)' : '') 67 | output += '\n' 68 | }) 69 | output += `\nShould sync: ${shouldSync ? 'Yes' : 'No'}` 70 | ctx.body = output 71 | }) 72 | 73 | router.post('/select', haveConnection, async ctx => { 74 | const { instance } = ctx.request.body 75 | if (Object.keys(instances).concat('auto').indexOf(instance) === -1) { 76 | ctx.body = NOT_FOUND_INSTANCE 77 | return 78 | } 79 | updateStoreInstance(instance || 'auto') 80 | selectedInstance = instance || 'auto' 81 | shouldSync = false 82 | ctx.body = '' 83 | }) 84 | 85 | router.post('/sync', haveConnection, async ctx => { 86 | if (selectedInstance !== 'auto') { 87 | shouldSync = true 88 | enableSync(shouldSync) 89 | ctx.body = '' 90 | return 91 | } 92 | ctx.body = NOT_SELECT_INSTANCE 93 | }) 94 | 95 | router.post('/action', haveConnection, async ctx => { 96 | const { type } = ctx.request.body 97 | if (!type || typeof type !== 'string') { 98 | ctx.body = NO_TYPE 99 | return 100 | } 101 | store.dispatch(ctx.request.body) 102 | ctx.body = '' 103 | }) 104 | 105 | router.post('/stop', () => process.exit()) 106 | 107 | app.use(router.routes()) 108 | app.use(router.allowedMethods()) 109 | 110 | portfile.unlink() 111 | setTimeout(() => { 112 | const port = app.listen(0).address().port 113 | console.log(`[Daemon] listening port ${port}`) 114 | console.log('[Daemon] Will save port to `$HOME/.remotedev_d_port` file') 115 | portfile.write(port) 116 | portfile.watchExists() 117 | }, 500) 118 | 119 | process.on('exit', () => portfile.unlink()) 120 | process.on('SIGTERM', process.exit) 121 | process.on('SIGINT', process.exit) 122 | -------------------------------------------------------------------------------- /src/launch.js: -------------------------------------------------------------------------------- 1 | import net from 'net' 2 | import { spawn } from 'child_process' 3 | import * as portfile from './port' 4 | 5 | // Check server is available 6 | function check() { 7 | return new Promise(resolve => { 8 | const port = portfile.read() 9 | if (!port) return resolve(false) 10 | const socket = net.connect(port, () => { 11 | socket.end() 12 | resolve(true) 13 | }) 14 | socket.on('error', () => resolve(false)) 15 | }) 16 | } 17 | 18 | const delay = t => new Promise(resolve => setTimeout(resolve, t)) 19 | 20 | async function wait() { 21 | let running = false 22 | let total = 0 23 | await delay(500) 24 | while (!running) { 25 | running = await check() 26 | if (running) return true 27 | 28 | await delay(100) 29 | total += 100 30 | if (total >= 5E3) return false 31 | } 32 | } 33 | 34 | export default () => { 35 | const daemon = require.resolve('./daemon') 36 | spawn('node', [daemon], { 37 | detached: true, 38 | stdio: ['ignore', 'ignore', 'ignore'], 39 | }).unref() 40 | return wait() 41 | } 42 | -------------------------------------------------------------------------------- /src/log.js: -------------------------------------------------------------------------------- 1 | export const RUNNING = 'Daemon is running' 2 | export const NOT_RUNNING = 'Daemon is not running' 3 | export const ALREADY_STARTED = 'Daemon already started' 4 | export const LAUNCH_SUCCESS = 'Daemon launch success' 5 | export const LAUNCH_FAILED = 'Daemon launch failed' 6 | 7 | export const NO_CONNECTION = 'No remotedev-server connection' 8 | export const NOT_FOUND_INSTANCE = 'Instance not found' 9 | export const NOT_SELECT_INSTANCE = 'Please select a instance' 10 | export const NO_TYPE = 'No `type` property' 11 | -------------------------------------------------------------------------------- /src/port.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import watch from 'node-watch' 4 | 5 | const homeEnv = process.platform === 'win32' ? 'USERPROFILE' : 'HOME' 6 | const portFile = path.join(process.env[homeEnv], '.remotedev_d_port') 7 | let isWatching = false 8 | 9 | export function write(port) { 10 | fs.writeFileSync(portFile, String(port)) 11 | } 12 | 13 | export function read() { 14 | if (!fs.existsSync(portFile)) return null 15 | return Number(fs.readFileSync(portFile, 'utf8')) 16 | } 17 | 18 | export function unlink() { 19 | if (fs.existsSync(portFile)) { 20 | fs.unlinkSync(portFile) 21 | } 22 | } 23 | 24 | export function watchExists() { 25 | if (isWatching) return 26 | isWatching = true 27 | watch(portFile, file => { 28 | if (!fs.existsSync(file)) { 29 | process.exit() 30 | } 31 | }) 32 | } 33 | --------------------------------------------------------------------------------