├── .gitignore ├── LICENSE ├── cli.js ├── cli ├── add.js ├── connect.js ├── deinit.js ├── help.js ├── init.js └── install.js ├── lib ├── deinit.js ├── init.js └── install.js ├── package.json ├── publish-dep.sh ├── readme.md ├── scripts └── publish-dep.sh └── tests ├── deinit_test.js ├── helpers.js ├── init_test.js └── test_package ├── .gitignore └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 EverythingStays 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 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const fs = require('fs') 4 | const args = process.argv 5 | const command = args[2] 6 | 7 | const config = { 8 | app_dir: process.env.HOME + '/.stay' 9 | } 10 | 11 | try { 12 | fs.accessSync(config.app_dir, fs.F_OK) 13 | } catch (e) { 14 | const mkdirp = require('mkdirp') 15 | mkdirp.sync(config.app_dir) 16 | } 17 | 18 | if (!command) { 19 | require('./cli/help')(args, config) 20 | process.exit(1) 21 | } 22 | 23 | try { 24 | require('./cli/' + command)(args, config) 25 | } catch (Err) { 26 | if (Err.code === 'MODULE_NOT_FOUND') { 27 | console.log('Command "' + command + '" not found') 28 | } else { 29 | throw Err 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cli/add.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | module.exports = (args, config) => { 3 | var module = args[3] 4 | const name = module.split('@')[0] 5 | const version = module.split('@')[1] 6 | if (!version) { 7 | console.log('You need to provide "module@hash" to install it') 8 | console.log('Example: "es add lodash@hash"') 9 | process.exit(1) 10 | } 11 | 12 | var pkg = require(process.cwd() + '/package.json') 13 | if (pkg.esDependencies) { 14 | if (pkg.esDependencies[name]) { 15 | console.log('Updating ' + name + ' to ' + version) 16 | } else { 17 | console.log('Adding ' + name + '@' + version) 18 | } 19 | pkg.esDependencies[name] = version 20 | } else { 21 | pkg.esDependencies = { 22 | [name]: version 23 | } 24 | } 25 | fs.writeFileSync(process.cwd() + '/package.json', JSON.stringify(pkg, null, 2)) 26 | } 27 | -------------------------------------------------------------------------------- /cli/connect.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | const fs = require('fs') 3 | 4 | module.exports = (args, config) => { 5 | const host = args[3] 6 | if (!host) { 7 | console.log('You need to provide "host" to be able to connect to node') 8 | console.log('Example: "stay connect 192.168.2.1"') 9 | process.exit(1) 10 | } 11 | const hostname = host.split(':')[0] 12 | const port = host.split(':')[1] 13 | if (!hostname || !port) { 14 | console.log('You need to format the "host" as hostname:port') 15 | console.log('You gave me "' + hostname + ':' + port + '"') 16 | process.exit(1) 17 | } 18 | 19 | const url = 'http://' + hostname + ':' + port 20 | 21 | fetch(url + '/health') 22 | .then((res) => { 23 | if (res.status === 200) { 24 | console.log('node is responding! Adding') 25 | var nodes = [] 26 | try { 27 | nodes = require(config.app_dir + '/nodes.json') 28 | } catch (err) { 29 | if (err.code === 'MODULE_NOT_FOUND') { 30 | fs.writeFileSync(config.app_dir + '/nodes.json', JSON.stringify([], null, 2)) 31 | } else { 32 | throw new Error(err) 33 | } 34 | } 35 | if (nodes.indexOf(url) !== -1) { 36 | console.log('Already exists! Nothing to do...') 37 | process.exit(1) 38 | } 39 | nodes.push(url) 40 | fs.writeFileSync(config.app_dir + '/nodes.json', JSON.stringify(nodes, null, 2)) 41 | console.log('Added!') 42 | } else { 43 | console.log('node did not respond to request. Are you sure its reachable at "' + url + '" ?') 44 | } 45 | }).catch((err) => { console.log(err) }) 46 | } 47 | -------------------------------------------------------------------------------- /cli/deinit.js: -------------------------------------------------------------------------------- 1 | const deinit = require('../lib/deinit') 2 | 3 | module.exports = (args) => { 4 | deinit(process.cwd()) 5 | console.log('Removed scripts, don\'t forget to commit your changes') 6 | process.exit(0) 7 | } 8 | -------------------------------------------------------------------------------- /cli/help.js: -------------------------------------------------------------------------------- 1 | const printCmdHelp = (name, desc) => { 2 | console.log() 3 | console.log(' stay ' + name) 4 | console.log(' ' + desc) 5 | } 6 | 7 | module.exports = (args, config) => { 8 | const pkg = require('../package.json') 9 | const version = pkg.version 10 | const description = pkg.description 11 | console.log('stay - ' + description + ' - [' + version + ']') 12 | console.log('Usage: stay [command] [arguments]') 13 | printCmdHelp('init', 'Adds script hook for publishing a module with `npm publish`') 14 | printCmdHelp('deinit', 'Removes the script hook') 15 | printCmdHelp('install', 'Installs dependencies from IPFS') 16 | printCmdHelp('add module@version', 'Adds a dependency to `package.json`') 17 | console.log(' `module` is the module name') 18 | console.log(' `version` is the modules hash outputted from `npm publish`') 19 | printCmdHelp('connect multiaddr', 'Adds a node to pin to after publishing') 20 | console.log(' `multiaddr` is the address outputted from running `ipfs id`') 21 | console.log(' Example: /ip4/10.13.0.5/tcp/4001/ipfs/QmPimdxPFy5K1VdCVfe8JsUd6DpdmKQDTdYCmCfReMQ7YY') 22 | console.log('') 23 | console.log('Source: https://github.com/everythingstays/stay-cli') 24 | console.log('Report a bug: https://github.com/everythingstays/stay-cli/issues/new') 25 | console.log('') 26 | } 27 | -------------------------------------------------------------------------------- /cli/init.js: -------------------------------------------------------------------------------- 1 | const init = require('../lib/init') 2 | 3 | module.exports = (args) => { 4 | init(process.cwd()) 5 | console.log('Installed scripts, don\'t forget to commit your changes') 6 | process.exit(0) 7 | } 8 | -------------------------------------------------------------------------------- /cli/install.js: -------------------------------------------------------------------------------- 1 | const install = require('../lib/install') 2 | 3 | module.exports = (args) => { 4 | console.log('Installing dependencies') 5 | install(process.cwd(), () => { 6 | console.log('Installed dependencies, running npm install...') 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /lib/deinit.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const join = require('path').join 3 | 4 | module.exports = (path_to_uninstall) => { 5 | var pkg = JSON.parse(fs.readFileSync((join(path_to_uninstall, '/package.json')))) 6 | if (pkg.scripts.preinstall === './get-deps.sh') { 7 | delete pkg.scripts.preinstall 8 | fs.unlinkSync(join(path_to_uninstall, '/get-deps.sh')) 9 | } 10 | 11 | if (pkg.scripts.prepublish === './publish-dep.sh') { 12 | delete pkg.scripts.prepublish 13 | fs.unlinkSync(join(path_to_uninstall, '/publish-dep.sh')) 14 | } 15 | 16 | fs.writeFileSync(join(path_to_uninstall, '/package.json'), JSON.stringify(pkg, null, 2)) 17 | } 18 | -------------------------------------------------------------------------------- /lib/init.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const join = require('path').join 3 | 4 | module.exports = (path_to_install) => { 5 | var pkg = JSON.parse(fs.readFileSync((join(path_to_install, '/package.json')))) 6 | if (pkg.scripts.prepublish) { 7 | throw new Error('scripts.prepublish already filled out. Aborting') 8 | } 9 | fs.writeFileSync(join(path_to_install, '/publish-dep.sh'), fs.readFileSync(join(__dirname, '../scripts/publish-dep.sh'))) 10 | 11 | fs.chmodSync(path_to_install + '/publish-dep.sh', '755') 12 | 13 | pkg.scripts.prepublish = './publish-dep.sh' 14 | fs.writeFileSync(path_to_install + '/package.json', JSON.stringify(pkg, null, 2)) 15 | } 16 | -------------------------------------------------------------------------------- /lib/install.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var join = require('path').join 3 | var execSync = require('child_process').execSync 4 | 5 | const downloadPackage = (path, name, hash) => { 6 | // TODO Ok, this is bad, I know. But apparently recursive, async code is difficult. 7 | // If you see this and want to improve it, please! 8 | // First step, get rid of all the synchronous code, it blocks I/O 9 | // console.log(name + ' ' + hash) 10 | execSync('ipfs get ' + hash + ' --output ' + join(path, 'node_modules', name), {stdio: []}) 11 | } 12 | 13 | const notInstalledDependencies = (path) => { 14 | var not_installed_yet = {} 15 | const dirs = fs.readdirSync(path).filter((file) => { 16 | return fs.statSync(join(path, file)).isDirectory() && file.split('')[0] !== '.' 17 | }) 18 | dirs.forEach((directory) => { 19 | const deps = readDeps(join(path, directory)) 20 | if (deps) { 21 | Object.keys(deps).forEach((key) => { 22 | const name = key 23 | const hash = deps[key] 24 | if (dirs.indexOf(name) === -1) { 25 | not_installed_yet[name] = hash 26 | } 27 | }) 28 | } 29 | }) 30 | const deps_installed = dirs.length 31 | const deps_left = Object.keys(not_installed_yet).length 32 | process.stdout.write(deps_left + ' left to install, have ' + deps_installed + ' installed so far\r') 33 | return not_installed_yet 34 | } 35 | 36 | const downloadAllDependencies = (path, deps) => { 37 | Object.keys(deps).forEach((key) => { 38 | const name = key 39 | const hash = deps[key] 40 | downloadPackage(path, name, hash) 41 | }) 42 | const new_deps = notInstalledDependencies(join(path, 'node_modules')) 43 | if (Object.keys(new_deps).length === 0) { 44 | return 45 | } 46 | return downloadAllDependencies(path, new_deps) 47 | } 48 | 49 | const readDeps = (path) => { 50 | const pkg = JSON.parse(fs.readFileSync(join(path, 'package.json'))) 51 | return pkg.esDependencies 52 | } 53 | 54 | module.exports = (path, callback) => { 55 | console.log('Getting dependencies from IPFS') 56 | const pkg = JSON.parse(fs.readFileSync(join(path, 'package.json'))) 57 | if (!pkg.esDependencies) { 58 | throw new Error('No esDependencies found in package.json') 59 | } 60 | downloadAllDependencies(path, readDeps(path)) 61 | callback() 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stay-cli", 3 | "version": "0.1.1", 4 | "description": "CLI for installing and publishing packages to IPFS easily", 5 | "main": "cli.js", 6 | "scripts": { 7 | "test": "mocha tests", 8 | "test:watch": "npm test -- --growl --watch" 9 | }, 10 | "bin": { 11 | "stay": "./cli.js" 12 | }, 13 | "keywords": [ 14 | "ipfs", 15 | "cli", 16 | "everythingstays" 17 | ], 18 | "author": "Victor Bjelkholm (https://www.github.com/victorbjelkholm)", 19 | "license": "MIT", 20 | "dependencies": { 21 | "mkdirp": "0.5.1", 22 | "node-fetch": "1.4.1" 23 | }, 24 | "devDependencies": { 25 | "mocha": "2.4.5" 26 | }, 27 | "esDependencies": { 28 | "mkdirp": "QmNe2eWvbapjxn5FSVFZS1NxvgFUNwQcg5UYZPMLYRVKmK", 29 | "node-fetch": "QmaAhfyydWWTvtCrTjyo28EmWmr9shidd4GiKqqmknPWNN" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /publish-dep.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # 400 - Missing hash and/or peer_id 4 | # 403 - Not allowed 5 | # 413 - Too big 6 | # 200 - Ok! 7 | 8 | 9 | if ipfs &>/dev/null; then 10 | echo "## Publishing dependency" 11 | 12 | mv node_modules .node_modules 2>/dev/null 13 | 14 | HASH=$(ipfs add . -r | tail -n 1 | cut -d ' ' -f 2) 15 | 16 | mv .node_modules node_modules 2>/dev/null 17 | 18 | echo "Published as $HASH, pinning in your nodes..." 19 | 20 | PEER=$(ipfs id --format '') 21 | 22 | cat ~/.stay/nodes.json | jq -rc '.[]' | while read host; do 23 | address="$host/api/pin/add/$HASH/$PEER" 24 | status=$(curl -X POST --silent $address) 25 | case "$status" in 26 | "400") echo "$host - Application Error: Missing the hash and/or peer_id" 27 | ;; 28 | "403") echo "$host - You do not have access to pinning at this node" 29 | ;; 30 | "413") echo "$host - The module was too big to pin!" 31 | ;; 32 | "200") echo "$host - Pinned!" 33 | ;; 34 | *) echo "Weird status code $status for $host" 35 | ;; 36 | esac 37 | done 38 | else 39 | echo "## Could not publish dependency to IPFS, doing the good'ol 'fetch from npm registry' way" 40 | echo "Either 'ipfs' doesn't exists in PATH or you haven't run 'ipfs daemon' before running the command" 41 | exit 0 42 | fi 43 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## everythingstays-cli 2 | 3 | CLI for installing and publishing packages to IPFS easily 4 | 5 | ## Requirements 6 | 7 | * NodeJS version 4 or higher 8 | * NPM version 2 or higher 9 | * IPFS version 0.3.11 or higher 10 | 11 | NOTE: This is the minimum requirements that has been tested, it might work on lower versions. If it does/does not, [please let us know](https://github.com/EverythingStays/stay-cli/issues/new). 12 | 13 | ## Installation 14 | 15 | `npm install -g stay-cli` 16 | 17 | Now `stay` is in your PATH 18 | 19 | ### Commands 20 | 21 | `stay init` 22 | 23 | Installs shell-scripts in your repository and package.json to run on `npm install` and `npm publish` 24 | 25 | `stay deinit` 26 | 27 | Removes the shell-scripts 28 | 29 | `stay install` 30 | 31 | Install dependencies from the `esDependencies` key in the `package.json` 32 | 33 | `stay add module@version` 34 | 35 | Adds a module to your `package.json` 36 | 37 | `stay connect multiaddr` 38 | 39 | Adds a node that will pin your published module when running `npm publish`. 40 | 41 | If you have `ipfs daemon` running on a instance, you can run `ipfs id` to get a list of addresses that the daemon is listening to. 42 | 43 | Use a public address like this: `stay connect /ip4/10.13.0.5/tcp/4001/ipfs/QmPimdxPFy5K1VdCVfe8JsUd6DpdmKQDTdYCmCfReMQ7YY` 44 | 45 | ### How to publish packages? 46 | 47 | * Run `stay init` in your repository 48 | * Commit your changes 49 | * Run `npm publish` 50 | * Write down the hash somewhere for people to use 51 | 52 | ### How to install packages? 53 | 54 | * Run `stay init` in your repository 55 | * Publish the dependencies you need, you'll get a hash back when publishing. 56 | * Copy `dependencies` from `package.json` to `esDependencies` in the same file, replacing the version with the hash from `npm publish` in the dependency publish 57 | * Commit your changes 58 | * Run `stay install` 59 | 60 | -------------------------------------------------------------------------------- /scripts/publish-dep.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # 400 - Missing hash and/or peer_id 4 | # 403 - Not allowed 5 | # 413 - Too big 6 | # 200 - Ok! 7 | 8 | 9 | if echo $npm_config_argv | grep -q "install"; then 10 | echo "Running at install step, skipping" 11 | else 12 | if ipfs &>/dev/null; then 13 | echo "## Publishing dependency" 14 | 15 | mv node_modules .node_modules 2>/dev/null 16 | 17 | HASH=$(ipfs add . -r | tail -n 1 | cut -d ' ' -f 2) 18 | 19 | mv .node_modules node_modules 2>/dev/null 20 | 21 | echo "Published as $HASH" 22 | 23 | PEER=$(ipfs id --format '') 24 | 25 | if [ -f ~/.stay/nodes.json ]; then 26 | cat ~/.stay/nodes.json | jq -rc '.[]' | while read host; do 27 | address="$host/api/pin/add/$HASH/$PEER" 28 | status=$(curl -X POST --silent $address) 29 | case "$status" in 30 | "400") echo "$host - Application Error: Missing the hash and/or peer_id" 31 | ;; 32 | "403") echo "$host - You do not have access to pinning at this node" 33 | ;; 34 | "413") echo "$host - The module was too big to pin!" 35 | ;; 36 | "200") echo "$host - Pinned!" 37 | ;; 38 | *) echo "Weird status code $status for $host" 39 | ;; 40 | esac 41 | done 42 | else 43 | echo "You don't have any saved nodes in ~/.stay/nodes.json, skip pinning" 44 | fi 45 | else 46 | echo "## Could not publish dependency to IPFS, doing the good'ol 'fetch from npm registry' way" 47 | echo "Either 'ipfs' doesn't exists in PATH or you haven't run 'ipfs daemon' before running the command" 48 | exit 0 49 | fi 50 | fi 51 | -------------------------------------------------------------------------------- /tests/deinit_test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | const assert = require('assert') 3 | const uninstall = require('../lib/deinit') 4 | const join = require('path').join 5 | 6 | const helpers = require('./helpers') 7 | 8 | describe('Deinit Lib', () => { 9 | const package_path = join(__dirname, '/test_package') 10 | it('Uninstall scripts', () => { 11 | helpers.writeInstalledPkgJSON(package_path) 12 | uninstall(package_path) 13 | const pkg = helpers.readPkgJson(package_path) 14 | assert.deepStrictEqual(pkg, helpers.notInstalledPkg) 15 | }) 16 | it('Does not uninstall if scripts are not from stay', () => { 17 | helpers.writeInstalledPkgJSONWithOtherScripts(package_path) 18 | uninstall(package_path) 19 | const pkg = helpers.readPkgJson(package_path) 20 | assert.deepStrictEqual(pkg, helpers.otherScriptsPkg) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const join = require('path').join 3 | 4 | const notInstalledPkg = { 5 | name: 'TestPackage', 6 | version: '0.0.1', 7 | scripts: {} 8 | } 9 | const otherScriptsPkg = { 10 | name: 'TestPackage', 11 | version: '0.0.1', 12 | scripts: { 13 | prepublish: 'another-something' 14 | } 15 | } 16 | 17 | const installedPkg = { 18 | name: 'TestPackage', 19 | version: '0.0.1', 20 | scripts: { 21 | prepublish: './publish-dep.sh' 22 | } 23 | } 24 | 25 | module.exports = { 26 | notInstalledPkg: notInstalledPkg, 27 | installedPkg: installedPkg, 28 | otherScriptsPkg: otherScriptsPkg, 29 | writeNotInstalledPkgJSON: (package_path) => { 30 | fs.writeFileSync(join(package_path, 'package.json'), JSON.stringify(notInstalledPkg, null, 2)) 31 | }, 32 | writeInstalledPkgJSON: (package_path) => { 33 | fs.writeFileSync(join(package_path, 'package.json'), JSON.stringify(installedPkg, null, 2)) 34 | }, 35 | writeInstalledPkgJSONWithOtherScripts: (package_path) => { 36 | fs.writeFileSync(join(package_path, 'package.json'), JSON.stringify(otherScriptsPkg, null, 2)) 37 | }, 38 | readPkgJson: (package_path) => { 39 | return JSON.parse(fs.readFileSync(join(package_path, '/package.json'))) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/init_test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | const assert = require('assert') 3 | const install = require('../lib/init') 4 | const join = require('path').join 5 | 6 | const helpers = require('./helpers') 7 | 8 | describe('Init Lib', () => { 9 | const package_path = join(__dirname, '/test_package') 10 | beforeEach(() => { 11 | helpers.writeNotInstalledPkgJSON(package_path) 12 | }) 13 | it('Install scripts', () => { 14 | helpers.writeNotInstalledPkgJSON(package_path) 15 | install(package_path) 16 | const pkg = helpers.readPkgJson(package_path) 17 | assert.strictEqual(pkg.scripts.prepublish, './publish-dep.sh') 18 | }) 19 | it('Does not overwrite existing scripts', () => { 20 | helpers.writeInstalledPkgJSONWithOtherScripts(package_path) 21 | var error = null 22 | try { 23 | install(package_path) 24 | } catch (err) { 25 | error = err 26 | } 27 | const pkg = helpers.readPkgJson(package_path) 28 | assert.notEqual(pkg.scripts.prepublish, './publish-dep.sh') 29 | assert(error) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/test_package/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !package.json 4 | !.gitignore 5 | 6 | !*/ 7 | -------------------------------------------------------------------------------- /tests/test_package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TestPackage", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "prepublish": "another-something" 6 | } 7 | } --------------------------------------------------------------------------------