├── .eslintignore ├── .eslintrc ├── test ├── fixtures │ ├── remove │ │ ├── projects │ │ │ ├── gilab.com │ │ │ │ └── eggjs │ │ │ │ │ └── egg │ │ │ │ │ └── .gitkeep │ │ │ └── github.com │ │ │ │ ├── DiamondYuan │ │ │ │ ├── .other │ │ │ │ └── yuque │ │ │ │ │ └── .gitkeep │ │ │ │ ├── eggjs │ │ │ │ ├── egg │ │ │ │ │ └── .gitkeep │ │ │ │ └── egg-core │ │ │ │ │ └── .gitkeep │ │ │ │ └── popomore │ │ │ │ └── projj │ │ │ │ └── .gitkeep │ │ └── .projj │ │ │ └── config.json │ ├── hook │ │ ├── .projj │ │ │ ├── cache.json │ │ │ ├── hooks │ │ │ │ ├── ls │ │ │ │ ├── run_config │ │ │ │ └── custom │ │ │ └── config.json │ │ └── github.com │ │ │ └── popomore │ │ │ ├── test1 │ │ │ └── package.json │ │ │ └── test2 │ │ │ └── package.json │ ├── importdir │ │ ├── file.js │ │ ├── repo │ │ │ └── package.json │ │ └── repo2 │ │ │ ├── package.json │ │ │ └── node_modules │ │ │ └── repo │ │ │ └── package.json │ ├── base-relative │ │ └── .projj │ │ │ └── config.json │ ├── base-tilde │ │ └── .projj │ │ │ └── config.json │ ├── find │ │ └── .projj │ │ │ └── config.json │ ├── add-change-directory │ │ └── .projj │ │ │ └── config.json │ ├── multiple-directory │ │ └── .projj │ │ │ ├── config.json │ │ │ └── cache.json │ ├── find-change-directory │ │ └── .projj │ │ │ └── config.json │ ├── alias │ │ └── .projj │ │ │ └── config.json │ ├── base-tmp │ │ └── .projj │ │ │ └── config.json │ ├── hook-add │ │ └── .projj │ │ │ ├── config.json │ │ │ └── hooks │ │ │ └── hook.js │ ├── mock_not_darwin.js │ └── mock_darwin.js ├── projj.test.js ├── projj_sync.test.js ├── projj_run.test.js ├── projj_runall.test.js ├── projj_init.test.js ├── projj_find.test.js ├── projj_import.test.js ├── projj_remove.test.js └── projj_add.test.js ├── bin └── projj.js ├── .gitignore ├── .travis.yml ├── lib ├── ssh.js ├── command │ ├── init.js │ ├── run.js │ ├── runall.js │ ├── sync.js │ ├── add.js │ ├── find.js │ ├── import.js │ └── remove.js ├── program.js ├── utils.js ├── cache.js └── base_command.js ├── appveyor.yml ├── .autod.conf.js ├── LICENSE ├── package.json ├── History.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "egg" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/remove/projects/gilab.com/eggjs/egg/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/remove/projects/github.com/DiamondYuan/.other: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/remove/projects/github.com/eggjs/egg/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/hook/.projj/cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v1" 3 | } -------------------------------------------------------------------------------- /test/fixtures/importdir/file.js: -------------------------------------------------------------------------------- 1 | // load should skip this file 2 | -------------------------------------------------------------------------------- /test/fixtures/remove/projects/github.com/DiamondYuan/yuque/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/remove/projects/github.com/eggjs/egg-core/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/remove/projects/github.com/popomore/projj/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/base-relative/.projj/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": ".." 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/base-tilde/.projj/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "~/code" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/importdir/repo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repo" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/importdir/repo2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repo" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/remove/.projj/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "../temp" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/hook/github.com/popomore/test1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test1" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/hook/github.com/popomore/test2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test2" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/importdir/repo2/node_modules/repo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repo" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/find/.projj/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "..", 3 | "run_config": true 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/hook/.projj/hooks/ls: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | console.log('buildin ls'); 4 | -------------------------------------------------------------------------------- /test/fixtures/add-change-directory/.projj/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "../../tmp", 3 | "change_directory": true 4 | } -------------------------------------------------------------------------------- /test/fixtures/multiple-directory/.projj/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": [ 3 | "../a", 4 | "../b" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /bin/projj.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const Program = require('../lib/program'); 6 | new Program().start(); 7 | -------------------------------------------------------------------------------- /test/fixtures/find-change-directory/.projj/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "..", 3 | "run_config": true, 4 | "change_directory":true 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/alias/.projj/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "../../tmp", 3 | "alias": { 4 | "github://": "https://github.com/" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/base-tmp/.projj/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "../../tmp", 3 | "hooks": { 4 | "preadd": "echo preadd", 5 | "postadd": "echo postadd" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/hook-add/.projj/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "../../tmp", 3 | "hooks": { 4 | "preadd": "hook.js pre", 5 | "postadd": "hook.js post" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | coverage/ 3 | cache.json 4 | .nyc_output/ 5 | !test/fixtures/hook/.projj/cache.json 6 | !test/fixtures/multiple-directory/.projj/cache.json 7 | .github/ -------------------------------------------------------------------------------- /test/fixtures/hook/.projj/hooks/run_config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const config = JSON.parse(process.env.PROJJ_HOOK_CONFIG); 4 | console.log('get config from env %s', config) 5 | -------------------------------------------------------------------------------- /test/fixtures/hook/.projj/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "..", 3 | "hooks": { 4 | "custom": "custom", 5 | "ls": "ls", 6 | "run_config": "run_config", 7 | "error": "exit 1" 8 | }, 9 | "run_config": true 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/hook/.projj/hooks/custom: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | 5 | const cwd = process.cwd(); 6 | const pkg = require(path.join(cwd, 'package.json')); 7 | console.log('get package name %s from %s', pkg.name, cwd); 8 | -------------------------------------------------------------------------------- /test/fixtures/mock_not_darwin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mm = require('mm'); 4 | const clipboardy = require('clipboardy'); 5 | 6 | 7 | mm(process, 'platform', 'not_darwin'); 8 | mm(clipboardy, 'write', function* () { 9 | return; 10 | }); 11 | -------------------------------------------------------------------------------- /test/fixtures/multiple-directory/.projj/cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v1", 3 | "/Users/popomore/projj/github.com/popomore/projj/test/fixtures/multiple-directory/a/github.com/popomore/projj": { 4 | "repo": "https://github.com/popomore/projj.git" 5 | } 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '10' 5 | - '12' 6 | before_install: 7 | - npm i npminstall -g 8 | install: 9 | - npminstall 10 | script: 11 | - npm run ci 12 | after_script: 13 | - npminstall codecov && codecov 14 | -------------------------------------------------------------------------------- /test/fixtures/mock_darwin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mm = require('mm'); 4 | const BaseCommand = require('../../lib/base_command'); 5 | 6 | mm(process, 'platform', 'darwin'); 7 | 8 | mm(BaseCommand.prototype, 'runScript', function* (cmd) { 9 | console.log(cmd); 10 | }); 11 | -------------------------------------------------------------------------------- /lib/ssh.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const spawn = require('child_process').spawn; 6 | 7 | const args = [ 8 | '-o', 'StrictHostKeyChecking=no', 9 | ].concat(process.argv.slice(2)); 10 | const opt = { 11 | stdio: 'inherit', 12 | }; 13 | 14 | spawn('ssh', args, opt); 15 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '10' 4 | - nodejs_version: '12' 5 | 6 | install: 7 | - ps: Install-Product node $env:nodejs_version 8 | - npm i npminstall && node_modules\.bin\npminstall 9 | 10 | test_script: 11 | - node --version 12 | - npm --version 13 | - npm run test 14 | 15 | build: off 16 | -------------------------------------------------------------------------------- /test/fixtures/hook-add/.projj/hooks/hook.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | 5 | console.log('%s hook, cwd %s', process.argv[2], process.cwd()); 6 | try { 7 | const pkg = require(path.join(process.cwd(), 'package.json')); 8 | console.log('%s hook, get package name %s', process.argv[2], pkg.name); 9 | } catch (_) { 10 | } 11 | -------------------------------------------------------------------------------- /lib/command/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BaseCommand = require('../base_command'); 4 | 5 | class InitCommand extends BaseCommand { 6 | async _run() { 7 | console.log(this.config); 8 | this.logger.info('Set base directory: %s', this.config.base.join(',')); 9 | } 10 | 11 | get description() { 12 | return 'Initialize configuration'; 13 | } 14 | 15 | } 16 | 17 | module.exports = InitCommand; 18 | -------------------------------------------------------------------------------- /.autod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | write: true, 5 | prefix: '^', 6 | test: [ 7 | 'test', 8 | ], 9 | dep: [ 10 | ], 11 | devdep: [ 12 | 'egg-ci', 13 | 'egg-bin', 14 | 'autod', 15 | 'eslint', 16 | 'eslint-config-egg', 17 | ], 18 | exclude: [ 19 | './test/fixtures', 20 | './dist', 21 | ], 22 | semver: [ 23 | "zlogger@1", 24 | ], 25 | registry: 'https://r.cnpmjs.org', 26 | }; 27 | -------------------------------------------------------------------------------- /lib/program.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const Command = require('common-bin'); 5 | 6 | class Program extends Command { 7 | constructor(rawArgv) { 8 | super(rawArgv); 9 | this.yargs.scriptName('projj'); 10 | this.usage = 'Usage: [command] [options]'; 11 | this.version = require('../package.json').version; 12 | this.load(path.join(__dirname, 'command')); 13 | } 14 | } 15 | 16 | module.exports = Program; 17 | -------------------------------------------------------------------------------- /lib/command/run.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BaseCommand = require('../base_command'); 4 | 5 | class RunCommand extends BaseCommand { 6 | 7 | async _run(cwd, [ hookName ]) { 8 | if (!hookName || !this.config.hooks[hookName]) { 9 | throw new Error(`Hook "${hookName}" don't exist`); 10 | } 11 | 12 | await this.runHook(hookName, cwd); 13 | } 14 | 15 | get description() { 16 | return 'Run hook in current directory'; 17 | } 18 | } 19 | 20 | module.exports = RunCommand; 21 | -------------------------------------------------------------------------------- /lib/command/runall.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BaseCommand = require('../base_command'); 4 | 5 | class RunCommand extends BaseCommand { 6 | 7 | async _run(cwd, [ hookName ]) { 8 | if (!hookName || !this.config.hooks[hookName]) { 9 | throw new Error(`hook "${hookName}" don't exist`); 10 | } 11 | 12 | const keys = await this.cache.getKeys(); 13 | for (const key of keys) { 14 | try { 15 | await this.runHook(hookName, key); 16 | } catch (err) { 17 | this.childLogger.error(err.message); 18 | } 19 | } 20 | } 21 | 22 | get description() { 23 | return 'Run hook in every repository'; 24 | } 25 | } 26 | 27 | module.exports = RunCommand; 28 | -------------------------------------------------------------------------------- /lib/command/sync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('mz/fs'); 4 | const BaseCommand = require('../base_command'); 5 | 6 | class SyncCommand extends BaseCommand { 7 | 8 | async _run() { 9 | const base = this.config.base; 10 | this.logger.info('Syncing cache from directory %s', base); 11 | const keys = await this.cache.getKeys(); 12 | for (const key of keys) { 13 | if (await fs.exists(key)) continue; 14 | this.childLogger.info('Remove %s that don\'t exist', key); 15 | await this.cache.remove(key); 16 | } 17 | await this.cache.dump(); 18 | } 19 | 20 | get description() { 21 | return 'Sync data from directory'; 22 | } 23 | } 24 | 25 | module.exports = SyncCommand; 26 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.generateAppleScript = dir => { 4 | const terminalCommand = `tell application "Terminal" 5 | do script "cd ${dir}" in front window 6 | end tell`.split('\n').map(line => (` -e '${line.trim()}'`)).join(''); 7 | 8 | const iTermCommand = `tell application "iTerm" 9 | tell current session of current window 10 | write text "cd ${dir}" 11 | end tell 12 | end tell`.split('\n').map(line => (` -e '${line.trim()}'`)).join(''); 13 | 14 | const currentApp = `tell application "System Events" 15 | set activeApp to name of first application process whose frontmost is true 16 | end tell`.split('\n').map(line => (` -e '${line.trim()}'`)).join(''); 17 | 18 | return `[ \`osascript ${currentApp}\` = "Terminal" ] && osascript ${terminalCommand} >/dev/null || osascript ${iTermCommand}`; 19 | }; 20 | -------------------------------------------------------------------------------- /test/projj.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const coffee = require('coffee'); 5 | const binfile = path.join(__dirname, '../bin/projj.js'); 6 | 7 | 8 | describe('test/projj.test.js', () => { 9 | 10 | it('should show help info', done => { 11 | coffee.fork(binfile, []) 12 | // .debug() 13 | .expect('stdout', /Usage: \[command] \[options]/) 14 | .expect('stdout', /init\ +Initialize configuration/) 15 | .expect('stdout', /add\ +Add repository/) 16 | .expect('stdout', /run\ +Run hook in current directory/) 17 | .expect('stdout', /runall\ +Run hook in every repository/) 18 | .expect('stdout', /import\ +Import repositories from existing directory/) 19 | .expect('code', 0) 20 | .end(done); 21 | }); 22 | 23 | it('should show version', done => { 24 | coffee.fork(binfile, [ '-v' ]) 25 | // .debug() 26 | .expect('stdout', require('../package.json').version + '\n') 27 | .expect('code', 0) 28 | .end(done); 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /test/projj_sync.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const coffee = require('coffee'); 5 | const mm = require('mm'); 6 | const rimraf = require('mz-modules/rimraf'); 7 | const fs = require('mz/fs'); 8 | 9 | const binfile = path.join(__dirname, '../bin/projj.js'); 10 | const fixtures = path.join(__dirname, 'fixtures'); 11 | const tmp = path.join(fixtures, 'tmp'); 12 | 13 | 14 | describe('test/projj_sync.test.js', () => { 15 | 16 | afterEach(mm.restore); 17 | afterEach(() => rimraf(tmp)); 18 | 19 | it('should run hook that do not exist', function* () { 20 | const home = path.join(fixtures, 'hook'); 21 | mm(process.env, 'HOME', home); 22 | 23 | const content = JSON.stringify({ 24 | [path.join(tmp, 'github.com/popomore/projj')]: {}, 25 | }); 26 | yield fs.writeFile(path.join(home, '.projj/cache.json'), content); 27 | 28 | yield coffee.fork(binfile, [ 'sync' ]) 29 | // .debug() 30 | .expect('stdout', new RegExp(`Remove ${tmp}/github.com/popomore/projj that don't exist`)) 31 | .expect('code', 0) 32 | .end(); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present popomore 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": "projj", 3 | "version": "2.1.0", 4 | "description": "Manage repository easily.", 5 | "dependencies": { 6 | "chalk": "^3.0.0", 7 | "clipboardy": "^2.1.0", 8 | "common-bin": "^2.8.3", 9 | "giturl": "^1.0.1", 10 | "inquirer": "^7.0.3", 11 | "mz": "^2.7.0", 12 | "mz-modules": "^2.1.0", 13 | "ora": "^4.0.3", 14 | "runscript": "^1.4.0", 15 | "through2": "^3.0.1", 16 | "utility": "^1.16.3", 17 | "zlogger": "^1.1.0" 18 | }, 19 | "devDependencies": { 20 | "autod": "^3.1.0", 21 | "coffee": "^5.2.2", 22 | "egg-bin": "^4.14.1", 23 | "egg-ci": "^1.13.1", 24 | "eslint": "^6.8.0", 25 | "eslint-config-egg": "^8.0.1", 26 | "mm": "^2.5.0" 27 | }, 28 | "engines": { 29 | "node": ">=10.0.0" 30 | }, 31 | "scripts": { 32 | "autod": "autod", 33 | "lint": "eslint .", 34 | "test": "npm run lint -- --fix && npm run test-local", 35 | "test-local": "egg-bin test", 36 | "cov": "egg-bin cov", 37 | "ci": "npm run lint && npm run cov" 38 | }, 39 | "ci": { 40 | "version": "10, 12", 41 | "license": { 42 | "fullname": "popomore" 43 | } 44 | }, 45 | "bin": { 46 | "projj": "bin/projj.js" 47 | }, 48 | "files": [ 49 | "bin", 50 | "lib" 51 | ], 52 | "repository": { 53 | "type": "git", 54 | "url": "git@github.com:popomore/projj.git" 55 | }, 56 | "author": "popomore ", 57 | "license": "MIT" 58 | } 59 | -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const fs = require('mz/fs'); 5 | const readJSON = require('utility').readJSON; 6 | 7 | module.exports = class Cache { 8 | constructor(options) { 9 | assert(options && options.cachePath, 'cachePath is required'); 10 | this.cachePath = options.cachePath; 11 | } 12 | 13 | async get(key) { 14 | if (!this.cache) { 15 | if (await fs.exists(this.cachePath)) { 16 | this.cache = await readJSON(this.cachePath); 17 | await this.setRepo(this.cache); 18 | } else { 19 | this.cache = {}; 20 | await this.dump(); 21 | } 22 | } 23 | return key ? this.cache[key] : this.cache; 24 | } 25 | 26 | async getKeys() { 27 | const cache = await this.get(); 28 | return Object.keys(cache).filter(key => key !== 'version'); 29 | } 30 | 31 | async set(key, value) { 32 | if (!key) return; 33 | if (!this.cache) await this.get(); 34 | 35 | this.cache[key] = value || {}; 36 | } 37 | 38 | async remove(keys) { 39 | if (!keys) return; 40 | if (!Array.isArray(keys)) keys = [ keys ]; 41 | keys.forEach(key => delete this.cache[key]); 42 | } 43 | 44 | async dump() { 45 | if (!this.cache) return; 46 | await fs.writeFile(this.cachePath, JSON.stringify(this.cache, null, 2)); 47 | } 48 | 49 | async setRepo(cache) { 50 | const keys = await this.getKeys(); 51 | for (const key of keys) { 52 | if (cache[key] && cache[key].repo) continue; 53 | const option = cache[key] = {}; 54 | const s = key.split('/'); 55 | option.repo = `git@${s[0]}:${s[1]}/${s[2]}.git`; 56 | } 57 | await this.dump(); 58 | } 59 | 60 | async upgrade() { 61 | const cache = await this.get(); 62 | switch (cache.version) { 63 | // v1 don't upgrade 64 | case 'v1': 65 | /* istanbul ignore next */ 66 | return; 67 | default: 68 | } 69 | 70 | cache.version = 'v1'; 71 | 72 | await this.dump(); 73 | } 74 | 75 | }; 76 | -------------------------------------------------------------------------------- /test/projj_run.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const coffee = require('coffee'); 5 | const mm = require('mm'); 6 | const rimraf = require('mz-modules/rimraf'); 7 | 8 | const binfile = path.join(__dirname, '../bin/projj.js'); 9 | const fixtures = path.join(__dirname, 'fixtures'); 10 | const tmp = path.join(fixtures, 'tmp'); 11 | 12 | 13 | describe('test/projj_run.test.js', () => { 14 | 15 | afterEach(mm.restore); 16 | afterEach(() => rimraf(tmp)); 17 | 18 | it('should run hook that do not exist', done => { 19 | const home = path.join(fixtures, 'hook'); 20 | mm(process.env, 'HOME', home); 21 | coffee.fork(binfile, [ 'run', 'noexist' ]) 22 | // .debug() 23 | .expect('stderr', /Hook "noexist" don't exist/) 24 | .expect('code', 1) 25 | .end(done); 26 | }); 27 | 28 | it('should run hook in cwd', done => { 29 | const home = path.join(fixtures, 'hook'); 30 | const cwd = path.join(home, 'github.com/popomore/test1'); 31 | mm(process.env, 'HOME', home); 32 | coffee.fork(binfile, [ 'run', 'custom' ], { cwd }) 33 | // .debug() 34 | .expect('stdout', new RegExp(`Run hook custom for ${home}/github.com/popomore/test1`)) 35 | .expect('stdout', new RegExp(`get package name test1 from ${home}/github.com/popomore/test1`)) 36 | .expect('code', 0) 37 | .end(done); 38 | }); 39 | 40 | it('should using buildin hook when has same name', done => { 41 | const home = path.join(fixtures, 'hook'); 42 | mm(process.env, 'HOME', home); 43 | coffee.fork(binfile, [ 'run', 'ls' ]) 44 | // .debug() 45 | .expect('stdout', /buildin ls/) 46 | .expect('code', 0) 47 | .end(done); 48 | }); 49 | 50 | it('should get hook config', done => { 51 | const home = path.join(fixtures, 'hook'); 52 | mm(process.env, 'HOME', home); 53 | coffee.fork(binfile, [ 'run', 'run_config' ]) 54 | // .debug() 55 | .expect('stdout', /get config from env true/) 56 | .expect('code', 0) 57 | .end(done); 58 | }); 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /lib/command/add.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('mz/fs'); 5 | const chalk = require('chalk'); 6 | const clipboardy = require('clipboardy'); 7 | const utils = require('../utils'); 8 | const BaseCommand = require('../base_command'); 9 | 10 | class AddCommand extends BaseCommand { 11 | 12 | async _run(_, [ repo ]) { 13 | repo = this.normalizeRepo(repo); 14 | const key = this.url2dir(repo); 15 | const base = await this.chooseBaseDirectory(); 16 | const targetPath = path.join(base, key); 17 | this.logger.info('Start adding repository %s', chalk.green(repo)); 18 | 19 | if (await fs.exists(targetPath)) { 20 | this.logger.info(`${targetPath} already exist`); 21 | await clipboardy.write(`cd ${targetPath}`); 22 | this.logger.info(chalk.green('📋 Copied to clipboard') + ', just use Ctrl+V'); 23 | return; 24 | } 25 | 26 | await this.addRepo(repo, targetPath); 27 | 28 | if (this.config.change_directory) { 29 | /* istanbul ignore next */ 30 | if (process.platform === 'darwin') { 31 | const script = utils.generateAppleScript(targetPath); 32 | this.logger.info(`Change directory to ${targetPath}`); 33 | await this.runScript(script); 34 | return; 35 | } 36 | this.logger.error('Change directory only supported in darwin'); 37 | } 38 | 39 | try { 40 | await clipboardy.write(`cd ${targetPath}`); 41 | this.logger.info(chalk.green('📋 Copied to clipboard') + ', just use Ctrl+V'); 42 | } catch (e) { 43 | this.logger.warn('Fail to copy to clipboard, error: %s', e.message); 44 | } 45 | } 46 | 47 | normalizeRepo(repo) { 48 | const alias = this.config.alias; 49 | const keys = Object.keys(alias); 50 | for (const key of keys) { 51 | // github://popomore/projj -> https://github.com/popomore/projj.git 52 | if (repo.startsWith(key)) { 53 | repo = alias[key] + repo.substring(key.length) + '.git'; 54 | break; 55 | } 56 | } 57 | return repo; 58 | } 59 | 60 | get description() { 61 | return 'Add repository'; 62 | } 63 | 64 | } 65 | 66 | module.exports = AddCommand; 67 | -------------------------------------------------------------------------------- /test/projj_runall.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const coffee = require('coffee'); 5 | const mm = require('mm'); 6 | const rimraf = require('mz-modules/rimraf'); 7 | const fs = require('mz/fs'); 8 | 9 | 10 | const binfile = path.join(__dirname, '../bin/projj.js'); 11 | const fixtures = path.join(__dirname, 'fixtures'); 12 | const tmp = path.join(fixtures, 'tmp'); 13 | 14 | 15 | describe('test/projj_runall.test.js', () => { 16 | const home = path.join(fixtures, 'hook'); 17 | const content = JSON.stringify({ 18 | [path.join(home, 'github.com/popomore/test1')]: {}, 19 | [path.join(home, 'github.com/popomore/test2')]: {}, 20 | }); 21 | 22 | before(() => fs.writeFile(path.join(home, '.projj/cache.json'), content)); 23 | afterEach(mm.restore); 24 | afterEach(() => rimraf(tmp)); 25 | 26 | it('should run hook that do not exist', done => { 27 | mm(process.env, 'HOME', home); 28 | coffee.fork(binfile, [ 'runall', 'noexist' ]) 29 | // .debug() 30 | .expect('stderr', /hook "noexist" don't exist/) 31 | .expect('code', 1) 32 | .end(done); 33 | }); 34 | 35 | it('should run hook in every repo', done => { 36 | mm(process.env, 'HOME', home); 37 | coffee.fork(binfile, [ 'runall', 'custom' ]) 38 | // .debug() 39 | .expect('stdout', new RegExp(`Run hook custom for ${home}/github.com/popomore/test1`)) 40 | .expect('stdout', new RegExp(`Run hook custom for ${home}/github.com/popomore/test2`)) 41 | .expect('stdout', new RegExp(`get package name test1 from ${home}/github.com/popomore/test1`)) 42 | .expect('stdout', new RegExp(`get package name test2 from ${home}/github.com/popomore/test2`)) 43 | .expect('code', 0) 44 | .end(done); 45 | }); 46 | 47 | it('should run all hooks if one has error', done => { 48 | mm(process.env, 'HOME', home); 49 | coffee.fork(binfile, [ 'runall', 'error' ]) 50 | // .debug() 51 | .expect('stdout', new RegExp(`Run hook error for ${home}/github.com/popomore/test1`)) 52 | .expect('stdout', new RegExp(`Run hook error for ${home}/github.com/popomore/test2`)) 53 | .expect('stderr', /Run "sh -c exit 1" error, exit code 1/) 54 | .expect('code', 0) 55 | .end(done); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /lib/command/find.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chalk = require('chalk'); 4 | const clipboardy = require('clipboardy'); 5 | const utils = require('../utils'); 6 | const BaseCommand = require('../base_command'); 7 | 8 | class FindCommand extends BaseCommand { 9 | 10 | async _run(cwd, [ repo ]) { 11 | if (!repo) { 12 | this.logger.error('Please specify the repo name:'); 13 | this.childLogger.error(chalk.white('For example:'), chalk.green('projj find', chalk.yellow('example'))); 14 | return; 15 | } 16 | const keys = await this.cache.getKeys(); 17 | let matched = keys.filter(key => key.endsWith(repo.replace(/^\/?/, '/'))); 18 | if (!matched.length) matched = keys.filter(key => key.indexOf(repo) >= 0); 19 | 20 | if (!matched.length) { 21 | this.logger.error('Can not find repo %s', chalk.yellow(repo)); 22 | return; 23 | } 24 | let key; 25 | if (matched.length === 1) { 26 | key = matched[0]; 27 | } else { 28 | const res = await this.choose(matched); 29 | key = res.key; 30 | } 31 | const dir = key; 32 | if (this.config.change_directory) { 33 | /* istanbul ignore next */ 34 | if (process.platform === 'darwin') { 35 | const script = utils.generateAppleScript(dir); 36 | this.logger.info(`Change directory to ${dir}`); 37 | await this.runScript(script); 38 | return; 39 | } 40 | this.logger.error('Change directory only supported in darwin'); 41 | } 42 | await this.copyPath(repo, dir); 43 | } 44 | 45 | async choose(choices) { 46 | return await this.prompt({ 47 | name: 'key', 48 | type: 'list', 49 | message: 'Please select the correct repo', 50 | choices, 51 | }); 52 | } 53 | 54 | async copyPath(repo, dir) { 55 | try { 56 | this.logger.info('find repo %s\'s location: %s', repo, dir); 57 | await clipboardy.write(`cd ${dir}`); 58 | this.logger.info(chalk.green('📋 Copied to clipboard') + ', just use Ctrl+V'); 59 | } catch (e) { 60 | this.logger.warn('Fail to copy to clipboard, error: %s', e.message); 61 | } 62 | } 63 | 64 | get description() { 65 | return 'Find repository'; 66 | } 67 | 68 | } 69 | 70 | module.exports = FindCommand; 71 | -------------------------------------------------------------------------------- /lib/command/import.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('mz/fs'); 5 | const ora = require('ora'); 6 | const runscript = require('runscript'); 7 | const chalk = require('chalk'); 8 | const BaseCommand = require('../base_command'); 9 | 10 | 11 | class ImportCommand extends BaseCommand { 12 | 13 | async _run(cwd, [ from ]) { 14 | let repos = []; 15 | if (from === '--cache') { 16 | const keys = await this.cache.getKeys(); 17 | for (const key of keys) { 18 | const option = await this.cache.get(key); 19 | if (option.repo) repos.push(option.repo); 20 | } 21 | await this.cache.dump(); 22 | } else { 23 | this.count = 0; 24 | this.spinner = ora('Searching ' + from).start(); 25 | repos = await this.findDirs(from); 26 | this.spinner.stop(); 27 | } 28 | 29 | const baseDir = await this.chooseBaseDirectory(); 30 | for (const repo of repos) { 31 | const key = this.url2dir(repo); 32 | const targetPath = path.join(baseDir, key); 33 | this.logger.info('Start importing repository %s', chalk.green(repo)); 34 | if (await fs.exists(targetPath)) { 35 | this.logger.warn(chalk.yellow('%s exists'), targetPath); 36 | continue; 37 | } 38 | try { 39 | await this.addRepo(repo, targetPath); 40 | } catch (_) { 41 | this.error(`Fail to clone ${repo}`); 42 | } 43 | } 44 | } 45 | 46 | async findDirs(cwd) { 47 | this.spinner.text = `Found ${chalk.cyan(this.count)}, Searching ${cwd}`; 48 | const dirs = await fs.readdir(cwd); 49 | 50 | // match the directory 51 | if (dirs.includes('.git')) { 52 | try { 53 | const { stdout } = await runscript('git config --get remote.origin.url', { stdio: 'pipe', cwd }); 54 | this.spinner.text = `Found ${chalk.cyan(this.count++)}, Searching ${cwd}`; 55 | return [ stdout.toString().slice(0, -1) ]; 56 | } catch (e) { 57 | // it contains .git, but no remote.url 58 | return []; 59 | } 60 | } 61 | 62 | // ignore node_modules 63 | if (dirs.includes('node_modules')) { 64 | return []; 65 | } 66 | 67 | let gitdir = []; 68 | for (const dir of dirs) { 69 | const subdir = path.join(cwd, dir); 70 | const stat = await fs.stat(subdir); 71 | if (!stat.isDirectory()) { 72 | continue; 73 | } 74 | const d = await this.findDirs(subdir); 75 | gitdir = gitdir.concat(d); 76 | } 77 | return gitdir; 78 | } 79 | 80 | get description() { 81 | return 'Import repositories from existing directory'; 82 | } 83 | 84 | } 85 | 86 | module.exports = ImportCommand; 87 | -------------------------------------------------------------------------------- /lib/command/remove.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const chalk = require('chalk'); 5 | const BaseCommand = require('../base_command'); 6 | const fs = require('mz/fs'); 7 | const rimraf = require('mz-modules/rimraf'); 8 | 9 | class RemoveCommand extends BaseCommand { 10 | 11 | async _run(cwd, [ repo ]) { 12 | if (!repo) { 13 | this.logger.error('Please specify the repo name:'); 14 | this.childLogger.error(chalk.white('For example:'), chalk.green('projj remove', chalk.yellow('example'))); 15 | return; 16 | } 17 | const keys = await this.cache.getKeys(); 18 | let matched = keys.filter(key => key.endsWith(repo.replace(/^\/?/, '/'))); 19 | if (!matched.length) matched = keys.filter(key => key.indexOf(repo) >= 0); 20 | if (!matched.length) { 21 | this.logger.error('Can not find repo %s', chalk.yellow(repo)); 22 | return; 23 | } 24 | let key; 25 | if (matched.length === 1) { 26 | key = matched[0]; 27 | } else { 28 | const res = await this.choose(matched); 29 | key = res.key; 30 | } 31 | this.logger.info('Do you want to remove the repository', chalk.green(key)); 32 | this.logger.info(chalk.red('Removed repository cannot be restored!')); 33 | const s = key.split('/'); 34 | const res = await this.confirm(`${s[s.length - 2]}/${s[s.length - 1]}`); 35 | if (res) { 36 | const dir = key; 37 | await rimraf(dir); 38 | const parent = path.dirname(dir); 39 | if ((await fs.readdir(parent)).length === 0) { 40 | await rimraf(parent); 41 | } 42 | await this.cache.remove(key); 43 | await this.cache.dump(); 44 | this.logger.info('Successfully remove repository', chalk.green(key)); 45 | } else { 46 | this.logger.info('Cancel remove repository', chalk.green(key)); 47 | } 48 | } 49 | 50 | 51 | async confirm(repoName) { 52 | const res = await this.prompt({ 53 | message: `Please type in the name of the repository to confirm. ${chalk.green(repoName)} \n`, 54 | name: 'userInput', 55 | }); 56 | if (res.userInput === repoName) { 57 | return true; 58 | } 59 | const continueRes = await this.prompt({ 60 | type: 'confirm', 61 | message: 'Do you want to continue?', 62 | name: 'continueToEnter', 63 | default: false, 64 | }); 65 | if (continueRes.continueToEnter) { 66 | return await this.confirm(repoName); 67 | } 68 | return false; 69 | } 70 | 71 | 72 | async choose(choices) { 73 | return await this.prompt({ 74 | name: 'key', 75 | type: 'list', 76 | message: 'Please select the correct repo', 77 | choices, 78 | }); 79 | } 80 | 81 | get description() { 82 | return 'Remove repository'; 83 | } 84 | 85 | } 86 | 87 | module.exports = RemoveCommand; 88 | -------------------------------------------------------------------------------- /test/projj_init.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('mz/fs'); 4 | const path = require('path'); 5 | const coffee = require('coffee'); 6 | const mm = require('mm'); 7 | const rimraf = require('mz-modules/rimraf'); 8 | const mkdirp = require('mz-modules/mkdirp'); 9 | const assert = require('assert'); 10 | 11 | const binfile = path.join(__dirname, '../bin/projj.js'); 12 | const fixtures = path.join(__dirname, 'fixtures'); 13 | const tmp = path.join(fixtures, 'tmp'); 14 | 15 | describe('test/projj_init.test.js', () => { 16 | 17 | afterEach(mm.restore); 18 | afterEach(() => rimraf(tmp)); 19 | 20 | it('should get base directory with relative path', done => { 21 | const home = path.join(fixtures, 'base-relative'); 22 | mm(process.env, 'HOME', home); 23 | coffee.fork(binfile, [ 'init' ]) 24 | // .debug() 25 | .expect('stdout', new RegExp(`Set base directory: ${home}\n`)) 26 | .expect('code', 0) 27 | .end(done); 28 | }); 29 | 30 | it('should get base directory with tilde', done => { 31 | const home = path.join(fixtures, 'base-tilde'); 32 | mm(process.env, 'HOME', home); 33 | coffee.fork(binfile, [ 'init' ]) 34 | // .debug() 35 | .expect('stdout', new RegExp(`Set base directory: ${home}/code\n`)) 36 | .expect('code', 0) 37 | .end(done); 38 | }); 39 | 40 | it('should set base when config don\'t exist', done => { 41 | mm(process.env, 'HOME', tmp); 42 | coffee.fork(binfile, [ 'init' ]) 43 | // .debug() 44 | .expect('stdout', /Set base directory: /) 45 | .expect('stdout', /Set base directory: \/home\n/) 46 | .expect('code', 0) 47 | .write('/home') 48 | .end(err => { 49 | assert.ifError(err); 50 | assert(fs.existsSync(path.join(tmp, '.projj/config.json'))); 51 | const content = fs.readFileSync(path.join(tmp, '.projj/config.json'), 'utf8'); 52 | assert(content === '{\n \"base\": [\n \"/home\"\n ],\n \"hooks\": {},\n \"alias\": {\n \"github://\": \"https://github.com/\"\n }\n}'); 53 | done(); 54 | }); 55 | }); 56 | 57 | it('should set base with relative path', done => { 58 | mm(process.env, 'HOME', tmp); 59 | coffee.fork(binfile, [ 'init' ]) 60 | // .debug() 61 | .expect('stdout', new RegExp(`Set base directory: ${process.cwd()}/code\n`)) 62 | .expect('code', 0) 63 | .write('code') 64 | .end(done); 65 | }); 66 | 67 | it('should upgrade', function* () { 68 | mm(process.env, 'HOME', tmp); 69 | yield mkdirp(path.join(tmp, '.projj')); 70 | yield fs.writeFile(path.join(tmp, '.projj/config.json'), `{"base":"${tmp}"}`); 71 | 72 | yield coffee.fork(binfile, [ 'init' ]) 73 | // .debug() 74 | .expect('stderr', /Upgrade cache/) 75 | .expect('code', 0) 76 | .end(); 77 | 78 | const cache = yield fs.readFile(path.join(tmp, '.projj/cache.json'), 'utf8'); 79 | assert(JSON.parse(cache).version === 'v1'); 80 | }); 81 | 82 | it('should not upgrade', function* () { 83 | mm(process.env, 'HOME', tmp); 84 | yield mkdirp(path.join(tmp, '.projj')); 85 | yield fs.writeFile(path.join(tmp, '.projj/config.json'), `{"base":"${tmp}"}`); 86 | yield fs.writeFile(path.join(tmp, '.projj/cache.json'), '{"version":"v1"}'); 87 | 88 | yield coffee.fork(binfile, [ 'init' ]) 89 | // .debug() 90 | .notExpect('stderr', /Upgrade cache/) 91 | .expect('code', 0) 92 | .end(); 93 | 94 | const cache = yield fs.readFile(path.join(tmp, '.projj/cache.json'), 'utf8'); 95 | assert(JSON.parse(cache).version === 'v1'); 96 | }); 97 | 98 | }); 99 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 2.1.0 / 2021-01-28 3 | ================== 4 | 5 | **fixes** 6 | * [[`5f2ba4a`](http://github.com/popomore/projj/commit/5f2ba4a589dd56ac1cbb2e66cb4ec30837f913f1)] - fix: redirect to find when add exists (#56) (tangyao <>) 7 | 8 | 2.0.0 / 2020-01-23 9 | ================== 10 | 11 | **features** 12 | * [[`d237eb8`](http://github.com/popomore/projj/commit/d237eb8bb617b12665d570530cd200723f32de33)] - feat: [BREAKING_CHANGE] support multiple base directory (#52) (Haoliang Gao <>) 13 | 14 | **fixes** 15 | * [[`e8b8fe4`](http://github.com/popomore/projj/commit/e8b8fe4ccb1064698c6da3cbf121942f1071bdb6)] - fix: update help infomation for sync (#51) (Haoliang Gao <>) 16 | 17 | **others** 18 | * [[`906bf04`](http://github.com/popomore/projj/commit/906bf04a68c5d26757bb3eb2a0e1ad67ae042b1b)] - deps: update dependencies (#53) (Haoliang Gao <>) 19 | 20 | 1.8.0 / 2019-02-20 21 | ================== 22 | 23 | **features** 24 | * [[`76762aa`](http://github.com/popomore/projj/commit/76762aa2e44c6bf573fc3f58287b41d87bafea63)] - feat: support remove command (#40) (DiamondYuan <<541832074@qq.com>>) 25 | * [[`b45883a`](http://github.com/popomore/projj/commit/b45883a8e4c2c143ca8a9704e2270299edc70289)] - feat: support alias (#42) (TZ | 天猪 <>) 26 | 27 | 1.7.0 / 2019-01-02 28 | ================== 29 | 30 | **features** 31 | * [[`1771ca5`](http://github.com/popomore/projj/commit/1771ca59300dba7a69108ef70e0f5dc957281c7d)] - feat: add should support change directory (#39) (TZ | 天猪 <>) 32 | 33 | 1.6.0 / 2018-12-28 34 | ================== 35 | 36 | **features** 37 | * [[`444dee9`](http://github.com/popomore/projj/commit/444dee99bc8341d817105bce2c135f7b3af7a6e0)] - feat: change dir when config change_directory true and platform is darwin (#34) (DiamondYuan <<541832074@qq.com>>) 38 | 39 | **fixes** 40 | * [[`cbdd73d`](http://github.com/popomore/projj/commit/cbdd73d0890891bd9e4a42f092324720b8979672)] - fix: fix error when iterm is not installed (#35) (DiamondYuan <<541832074@qq.com>>) 41 | 42 | **others** 43 | * [[`077bbda`](http://github.com/popomore/projj/commit/077bbda7775fc5d345884f735e958fb6d2cf8e7a)] - docs: add document for change_directory (#37) (Haoliang Gao <>) 44 | 45 | 1.5.0 / 2018-12-14 46 | ================== 47 | 48 | **features** 49 | * [[`4cbe392`](http://github.com/popomore/projj/commit/4cbe39294823ca95fe15627e7e1b73a9b256b1b7)] - feat: add hints to the find command when input empty repo name (#33) (DiamondYuan <<541832074@qq.com>>) 50 | 51 | 1.4.1 / 2018-05-02 52 | ================== 53 | 54 | **fixes** 55 | * [[`c1ce741`](http://github.com/popomore/projj/commit/c1ce741348c8e8d6f0815d424bb49e30e7c7a26f)] - fix: format git repo when load cache (#30) (Haoliang Gao <>) 56 | 57 | 1.4.0 / 2018-04-28 58 | ================== 59 | 60 | **features** 61 | * [[`543567f`](http://github.com/popomore/projj/commit/543567ff09f298c82333fd26ed0b2fc8d92117ff)] - feat: import --cache (#29) (Haoliang Gao <>) 62 | 63 | 1.3.0 / 2017-02-28 64 | ================== 65 | 66 | * feat: add command sync (#28) 67 | * refactor: split cache to cache.js (#27) 68 | 69 | 1.2.1 / 2017-02-22 70 | ================== 71 | 72 | * fix: should continue run hook when one hook error (#26) 73 | 74 | 1.2.0 / 2017-02-20 75 | ================== 76 | 77 | * feat: add find command (#25) 78 | 79 | 1.1.2 / 2017-02-06 80 | ================== 81 | 82 | * fix: import also run add hook (#23) 83 | 84 | 1.1.1 / 2017-02-06 85 | ================== 86 | 87 | * fix: skip host checking when git clone (#21) 88 | 89 | 1.1.0 / 2017-02-06 90 | ================== 91 | 92 | * refactor: move commands to one directory (#19) 93 | * feat: support projj import (#16) 94 | 95 | 1.0.0 / 2017-02-04 96 | ================== 97 | 98 | * docs: add readme (#13) 99 | * refactor: more info when run hook (#12) 100 | 101 | 1.0.0-alpha.4 / 2017-02-04 102 | ========================== 103 | 104 | * refactor: improve console (#11) 105 | * feat: pass hook config to hook command (#10) 106 | * feat: use buildin hook first (#8) 107 | * feat: add runall command (#7) 108 | 109 | 1.0.0-alpha.3 / 2017-02-04 110 | ========================== 111 | 112 | * feat: add run command (#6) 113 | * feat: use config.json instead of config and hooks (#5) 114 | * fix: add command help info (#4) 115 | 116 | 1.0.0-alpha.2 / 2017-02-03 117 | ========================== 118 | 119 | * fix: miss pkg.bin 120 | 121 | 1.0.0-alpha.1 / 2017-02-03 122 | ========================= 123 | 124 | * init version 125 | 126 | -------------------------------------------------------------------------------- /test/projj_find.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const coffee = require('coffee'); 5 | const mm = require('mm'); 6 | const fs = require('mz/fs'); 7 | const rimraf = require('mz-modules/rimraf'); 8 | 9 | const binfile = path.join(__dirname, '../bin/projj.js'); 10 | const fixtures = path.join(__dirname, 'fixtures'); 11 | const tmp = path.join(fixtures, 'tmp'); 12 | 13 | 14 | describe('test/projj_find.test.js', () => { 15 | 16 | afterEach(mm.restore); 17 | afterEach(() => rimraf(tmp)); 18 | 19 | it('should run script when changeDirectory is true and platform is darwin', function* () { 20 | const home = path.join(fixtures, 'find-change-directory'); 21 | mm(process.env, 'HOME', home); 22 | yield makeConfig(home); 23 | 24 | yield coffee.fork(binfile, [ 'find', 'egg' ]) 25 | // .debug() 26 | .beforeScript(path.join(__dirname, 'fixtures/mock_darwin.js')) 27 | .expect('stdout', new RegExp(`Change directory to ${home}/github.com/eggjs/egg`)) 28 | // .expect('stdout', new RegExp(`cd ${home}/github.com/eggjs/egg`)) 29 | .expect('code', 0) 30 | .end(); 31 | }); 32 | 33 | it('should show warn when changeDirectory is true and platform is not darwin', function* () { 34 | const home = path.join(fixtures, 'find-change-directory'); 35 | mm(process.env, 'HOME', home); 36 | yield makeConfig(home); 37 | 38 | yield coffee.fork(binfile, [ 'find', 'egg' ]) 39 | .beforeScript(path.join(__dirname, 'fixtures/mock_not_darwin.js')) 40 | .expect('stderr', new RegExp('Change directory only supported in darwin')) 41 | .expect('stdout', new RegExp(`find repo egg's location: ${home}/github.com/eggjs/egg`)) 42 | .expect('code', 0) 43 | .end(); 44 | }); 45 | 46 | it('should to prompt if the input is empty', function* () { 47 | const home = path.join(fixtures, 'find'); 48 | mm(process.env, 'HOME', home); 49 | 50 | yield coffee.fork(binfile, [ 'find', '' ]) 51 | .expect('stderr', new RegExp('Please specify the repo name:')) 52 | .expect('stderr', new RegExp('For example: projj find example')) 53 | .expect('code', 0) 54 | .end(); 55 | }); 56 | 57 | it('should find endsWith egg', function* () { 58 | const home = path.join(fixtures, 'find'); 59 | mm(process.env, 'HOME', home); 60 | yield makeConfig(home); 61 | 62 | yield coffee.fork(binfile, [ 'find', 'egg' ]) 63 | // .debug() 64 | .expect('stdout', new RegExp(`find repo egg's location: ${home}/github.com/eggjs/egg`)) 65 | .expect('code', 0) 66 | .end(); 67 | }); 68 | 69 | it('should find endsWith /egg', function* () { 70 | const home = path.join(fixtures, 'find'); 71 | mm(process.env, 'HOME', home); 72 | yield makeConfig(home); 73 | 74 | yield coffee.fork(binfile, [ 'find', '/egg' ]) 75 | // .debug() 76 | .expect('stdout', new RegExp(`find repo /egg's location: ${home}/github.com/eggjs/egg`)) 77 | .expect('code', 0) 78 | .end(); 79 | }); 80 | 81 | it('should find match eggjs/autod', function* () { 82 | const home = path.join(fixtures, 'find'); 83 | mm(process.env, 'HOME', home); 84 | yield makeConfig(home); 85 | 86 | yield coffee.fork(binfile, [ 'find', 'eggjs/autod' ]) 87 | // .debug() 88 | .expect('stdout', new RegExp(`find repo eggjs/autod's location: ${home}/gitlab.com/eggjs/autod-egg`)) 89 | .expect('code', 0) 90 | .end(); 91 | }); 92 | 93 | it('should find two matching file with egg-core', function* () { 94 | const home = path.join(fixtures, 'find'); 95 | mm(process.env, 'HOME', home); 96 | yield makeConfig(home); 97 | 98 | yield coffee.fork(binfile, [ 'find', 'egg-core' ]) 99 | .write('\n') 100 | // .debug() 101 | .expect('stdout', new RegExp('Please select the correct repo')) 102 | .expect('stdout', new RegExp(`find repo egg-core's location: ${home}/github.com/eggjs/egg-core`)) 103 | .expect('code', 0) 104 | .end(); 105 | }); 106 | 107 | it('should find nothing with eggggg', function* () { 108 | const home = path.join(fixtures, 'find'); 109 | mm(process.env, 'HOME', home); 110 | yield makeConfig(home); 111 | 112 | yield coffee.fork(binfile, [ 'find', 'eggggg' ]) 113 | // .debug() 114 | .expect('stderr', new RegExp('Can not find repo eggggg')) 115 | .expect('code', 0) 116 | .end(); 117 | }); 118 | }); 119 | 120 | function* makeConfig(cwd) { 121 | const config = { 122 | [path.join(cwd, 'github.com/eggjs/egg')]: { 123 | repo: 'git@github.com:eggjs/egg.git', 124 | }, 125 | [path.join(cwd, 'github.com/eggjs/egg-core')]: { 126 | repo: 'git@github.com:eggjs/egg-core.git', 127 | }, 128 | [path.join(cwd, 'gitlab.com/eggjs/egg-core')]: { 129 | repo: 'git@gitlab.com:eggjs/egg-core.git', 130 | }, 131 | [path.join(cwd, 'gitlab.com/eggjs/autod-egg')]: { 132 | repo: 'git@gitlab.com:eggjs/autod-egg.git', 133 | }, 134 | version: 'v1', 135 | }; 136 | yield fs.writeFile(path.join(cwd, '.projj/cache.json'), JSON.stringify(config)); 137 | } 138 | -------------------------------------------------------------------------------- /test/projj_import.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const coffee = require('coffee'); 5 | const mm = require('mm'); 6 | const rimraf = require('mz-modules/rimraf'); 7 | const runscript = require('runscript'); 8 | const mkdirp = require('mz-modules/mkdirp'); 9 | const fs = require('mz/fs'); 10 | 11 | const binfile = path.join(__dirname, '../bin/projj.js'); 12 | const fixtures = path.join(__dirname, 'fixtures'); 13 | const tmp = path.join(fixtures, 'tmp'); 14 | const repo = path.join(fixtures, 'importdir/repo'); 15 | const home = path.join(fixtures, 'base-tmp'); 16 | const target = path.join(tmp, 'github.com/popomore/projj'); 17 | 18 | 19 | describe('test/projj_import.test.js', () => { 20 | 21 | beforeEach(() => { 22 | mm(process.env, 'HOME', home); 23 | }); 24 | afterEach(mm.restore); 25 | afterEach(() => rimraf(tmp)); 26 | afterEach(() => rimraf(path.join(repo, '.git'))); 27 | 28 | describe('when origin url exists', () => { 29 | before(function* () { 30 | yield runscript('git init && git remote add origin https://github.com/popomore/projj.git', { 31 | cwd: repo, 32 | }); 33 | }); 34 | 35 | it('should import from it', function* () { 36 | yield coffee.fork(binfile, [ 'import', path.join(fixtures, 'importdir') ]) 37 | // .debug() 38 | .expect('stdout', /importing repository https:\/\/github.com\/popomore\/projj.git/) 39 | .expect('stdout', new RegExp(`Cloning into ${target}`)) 40 | .expect('stdout', /preadd/) 41 | .expect('stdout', /postadd/) 42 | .expect('code', 0) 43 | .end(); 44 | }); 45 | }); 46 | 47 | describe('when origin url is unknown', () => { 48 | before(function* () { 49 | yield runscript('git init && git remote add origin https://unknown.com/popomore/projj.git', { 50 | cwd: repo, 51 | }); 52 | }); 53 | 54 | it('should fail to clone', function* () { 55 | yield coffee.fork(binfile, [ 'import', path.join(fixtures, 'importdir') ]) 56 | // .debug() 57 | .expect('stdout', /importing repository https:\/\/unknown.com\/popomore\/projj.git/) 58 | .expect('stderr', /Fail to clone https:\/\/unknown.com\/popomore\/projj.git/) 59 | .expect('code', 0) 60 | .end(); 61 | }); 62 | }); 63 | 64 | describe('when no origin url', () => { 65 | before(function* () { 66 | yield runscript('git init', { 67 | cwd: repo, 68 | }); 69 | }); 70 | 71 | it('should fail to clone', function* () { 72 | yield coffee.fork(binfile, [ 'import', path.join(fixtures, 'importdir') ]) 73 | // .debug() 74 | .notExpect('stdout', /importing repository https:\/\/unknown.com\/popomore\/projj.git/) 75 | .expect('code', 0) 76 | .end(); 77 | }); 78 | }); 79 | 80 | describe('when target exists', () => { 81 | before(function* () { 82 | yield runscript('git init && git remote add origin https://github.com/popomore/projj.git', { 83 | cwd: repo, 84 | }); 85 | }); 86 | before(() => mkdirp(target)); 87 | 88 | it('should ignore', function* () { 89 | yield coffee.fork(binfile, [ 'import', path.join(fixtures, 'importdir') ]) 90 | // .debug() 91 | .expect('stdout', /importing repository https:\/\/github.com\/popomore\/projj.git/) 92 | .expect('stderr', new RegExp(`${target} exists`)) 93 | .expect('code', 0) 94 | .end(); 95 | }); 96 | }); 97 | 98 | describe('when repo under node_modules', () => { 99 | const repo = path.join(fixtures, 'importdir/repo2/node_modules/repo'); 100 | before(function* () { 101 | yield runscript('git init && git remote add origin https://github.com/popomore/projj-hooks.git', { 102 | cwd: repo, 103 | }); 104 | }); 105 | after(() => rimraf(path.join(repo, '.git'))); 106 | 107 | it('should ignore', function* () { 108 | yield coffee.fork(binfile, [ 'import', path.join(fixtures, 'importdir') ]) 109 | // .debug() 110 | .notExpect('stdout', /importing repository https:\/\/github.com\/popomore\/projj-hooks.git/) 111 | .expect('code', 0) 112 | .end(); 113 | }); 114 | }); 115 | 116 | describe('when --cache', () => { 117 | const cacheFile = path.join(home, '.projj/cache.json'); 118 | 119 | before(function* () { 120 | yield fs.writeFile(cacheFile, JSON.stringify({ 121 | [path.join(tmp, 'github.com/popomore/projj')]: { 122 | repo: 'https://github.com/popomore/projj.git', 123 | }, 124 | })); 125 | }); 126 | after(function* () { 127 | yield rimraf(cacheFile); 128 | }); 129 | 130 | it('should import from it', function* () { 131 | yield coffee.fork(binfile, [ 'import', '--cache' ]) 132 | // .debug() 133 | .expect('stdout', /importing repository https:\/\/github.com\/popomore\/projj.git/) 134 | .expect('stdout', new RegExp(`Cloning into ${target}`)) 135 | .expect('stdout', /preadd/) 136 | .expect('stdout', /postadd/) 137 | .expect('code', 0) 138 | .end(); 139 | }); 140 | }); 141 | 142 | }); 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Projj 2 | 3 | Manage repository easily. 4 | 5 | [![NPM version][npm-image]][npm-url] 6 | [![build status][travis-image]][travis-url] 7 | [![Test coverage][codecov-image]][codecov-url] 8 | [![David deps][david-image]][david-url] 9 | [![Known Vulnerabilities][snyk-image]][snyk-url] 10 | [![npm download][download-image]][download-url] 11 | 12 | [npm-image]: https://img.shields.io/npm/v/projj.svg?style=flat-square 13 | [npm-url]: https://npmjs.org/package/projj 14 | [travis-image]: https://img.shields.io/travis/popomore/projj.svg?style=flat-square 15 | [travis-url]: https://travis-ci.org/popomore/projj 16 | [codecov-image]: https://codecov.io/gh/popomore/projj/branch/master/graph/badge.svg 17 | [codecov-url]: https://codecov.io/gh/popomore/projj 18 | [david-image]: https://img.shields.io/david/popomore/projj.svg?style=flat-square 19 | [david-url]: https://david-dm.org/popomore/projj 20 | [snyk-image]: https://snyk.io/test/npm/projj/badge.svg?style=flat-square 21 | [snyk-url]: https://snyk.io/test/npm/projj 22 | [download-image]: https://img.shields.io/npm/dm/projj.svg?style=flat-square 23 | [download-url]: https://npmjs.org/package/projj 24 | 25 | ## Why? 26 | 27 | How do you manage git repository? 28 | 29 | Maybe you create a directory and clone to it. However if you want to clone repository that has same name? Or Do something in every directory like `clean`? 30 | 31 | `Projj` provide a structure making it easy. 32 | 33 | ``` 34 | $BASE 35 | |- github.com 36 | | `- popomore 37 | | `- projj 38 | `- gitlab.com 39 | `- popomore 40 | `- projj 41 | ``` 42 | 43 | And you can `DO` everything in repository by [Hook](#hook). 44 | 45 | ## Feature 46 | 47 | - ✔︎ Add repository using `projj add` 48 | - ✔︎ Command Hook 49 | - ✘ Buildin Hook 50 | - ✔︎ Custom Hook 51 | - ✔︎ Run Hook in All Repositories 52 | - ✔︎ Git Support 53 | 54 | ## Installation 55 | 56 | Install `projj` globally. 57 | 58 | ```bash 59 | $ npm i projj -g 60 | ``` 61 | 62 | ## Usage 63 | 64 | ### Initialize 65 | 66 | ```bash 67 | $ projj init 68 | ``` 69 | 70 | Set base directory which repositories will be cloned to, default is `~/projj`. 71 | 72 | You can change base directory in `~/.projj/config.json`. 73 | 74 | ### Add Repository 75 | 76 | ```bash 77 | $ projj add git@github.com:popomore/projj.git 78 | ``` 79 | 80 | it's just like `git clone`, but the repository will be cached by projj. You can find all repositories in `~/.projj/cache.json` 81 | 82 | also support alias which could config at `alias` of `~/.projj/config.json`: 83 | 84 | ```bash 85 | $ projj add github://popomore/projj 86 | ``` 87 | 88 | ### Importing 89 | 90 | If you have some repositories in `~/code`, projj can import by `projj import ~/code`. 91 | 92 | Or projj can import repositories from `cache.json` when you change laptop by `projj import --cache` 93 | 94 | ### Find Repository 95 | 96 | projj provide a easy way to find the location of your repositories. 97 | 98 | ```bash 99 | $ projj find [repo] 100 | ``` 101 | 102 | You can set `change_directory` in `~/.projj/config.json` to change directory automatically. 103 | 104 | ### Sync 105 | 106 | `projj sync` will check the repository in cache.json whether exists, the repository will be removed from cache if not exist. 107 | 108 | ## Hook 109 | 110 | Hook is flexible when manage repositories. 111 | 112 | ### Command Hook 113 | 114 | When run command like `projj add`, hook will be run. `preadd` that run before `projj add`, and `postadd` that run after `projj add`. 115 | 116 | Config hook in `~/.projj/config.json` 117 | 118 | ```json 119 | { 120 | "hooks": { 121 | "postadd": "cat package.json" 122 | } 123 | } 124 | ``` 125 | 126 | Then will show the content of the package of repository. 127 | 128 | **Only support `add` now** 129 | 130 | ### Define Hook 131 | 132 | You can define own hook. 133 | 134 | ```json 135 | { 136 | "hooks": { 137 | "hook_name": "command" 138 | } 139 | } 140 | ``` 141 | 142 | For Example, define a hook to show package. 143 | 144 | ```json 145 | { 146 | "hooks": { 147 | "show_package": "cat package.json" 148 | } 149 | } 150 | ``` 151 | 152 | Then you can use `projj run show_package` to run the hook in current directory. 153 | 154 | `Command` can be used in `$PATH`, so you can use global node_modules like `npm`. 155 | 156 | ```json 157 | { 158 | "hooks": { 159 | "npm_install": "npm install" 160 | } 161 | } 162 | ``` 163 | 164 | ### Write Hook 165 | 166 | Write a command 167 | 168 | ```js 169 | // clean 170 | #!/usr/bin/env node 171 | 172 | 'use strict'; 173 | 174 | const cp = require('child_process'); 175 | const cwd = process.cwd(); 176 | const config = JSON.parse(process.env.PROJJ_HOOK_CONFIG); 177 | if (config.node_modules === true) { 178 | cp.spawn('rm', [ '-rf', 'node_modules' ]); 179 | } 180 | ``` 181 | 182 | You can get `PROJJ_HOOK_CONFIG` from `projj` if you have defined in `~/.projj/config.json`. 183 | 184 | ```json 185 | { 186 | "hooks": { 187 | "clean": "clean" 188 | }, 189 | "clean": { 190 | "node_modules": true 191 | } 192 | } 193 | ``` 194 | 195 | ### Run Hook 196 | 197 | `projj run clean` in current directory. 198 | 199 | `projj runall clean` in every repositories from `cache.json` 200 | 201 | ## License 202 | 203 | [MIT](LICENSE) 204 | -------------------------------------------------------------------------------- /test/projj_remove.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const coffee = require('coffee'); 5 | const mm = require('mm'); 6 | const rimraf = require('mz-modules/rimraf'); 7 | const binfile = path.join(__dirname, '../bin/projj.js'); 8 | const fixtures = path.join(__dirname, 'fixtures'); 9 | const fs = require('mz/fs'); 10 | const home = path.join(fixtures, 'remove'); 11 | const runscript = require('runscript'); 12 | const assert = require('assert'); 13 | const projects = path.join(home, 'projects'); 14 | const tempProject = path.join(home, 'temp'); 15 | const catchPath = path.join(home, '.projj/cache.json'); 16 | 17 | describe('test/projj_remove.test.js', () => { 18 | beforeEach(function* () { 19 | mm(process.env, 'HOME', home); 20 | const content = JSON.stringify({ 21 | 'github.com/popomore/projj': {}, 22 | 'github.com/eggjs/egg': {}, 23 | 'github.com/eggjs/egg-core': {}, 24 | 'github.com/eggjs/autod-egg': {}, 25 | 'gitlab.com/eggjs/egg': {}, 26 | 'github.com/DiamondYuan/yuque': {}, 27 | }); 28 | yield runscript(`cp -r ${projects} ${tempProject}`); 29 | yield fs.writeFile(catchPath, content); 30 | }); 31 | 32 | afterEach(() => { 33 | afterEach(() => rimraf(tempProject)); 34 | afterEach(() => rimraf(catchPath)); 35 | }); 36 | 37 | it('should to prompt if the input is empty', done => { 38 | coffee.fork(binfile, [ 'remove', '' ]) 39 | .expect('stderr', new RegExp('Please specify the repo name:')) 40 | .expect('stderr', new RegExp('For example: projj remove example')) 41 | .expect('code', 0) 42 | .end(done); 43 | }); 44 | 45 | it('if there are other files in the folder, the folder will not be deleted.', done => { 46 | coffee.fork(binfile, [ 'remove', 'yuque' ]) 47 | .waitForPrompt() 48 | .expect('stdout', new RegExp(`Do you want to remove the repository ${tempProject}/github.com/DiamondYuan/yuque`)) 49 | .expect('stdout', new RegExp('Removed repository cannot be restored!')) 50 | .expect('stdout', new RegExp('Please type in the name of the repository to confirm. DiamondYuan/yuque')) 51 | .write('DiamondYuan/yuque\n') 52 | .expect('stdout', new RegExp(`Successfully remove repository ${tempProject}/github.com/DiamondYuan/yuque`)) 53 | .expect('code', 0) 54 | .end(err => { 55 | assert.ifError(err); 56 | assert(fs.existsSync(path.join(tempProject, 'github.com/DiamondYuan'))); 57 | done(); 58 | }); 59 | }); 60 | 61 | it('if no other files are in the folder, the folder will be deleted.', done => { 62 | const folder = path.join(tempProject, 'github.com/popomore'); 63 | coffee.fork(binfile, [ 'remove', 'projj' ]) 64 | .expect('stdout', new RegExp(`Do you want to remove the repository ${tempProject}/github.com/popomore/projj`)) 65 | .expect('stdout', new RegExp('Removed repository cannot be restored!')) 66 | .expect('stdout', new RegExp('Please type in the name of the repository to confirm. popomore/projj')) 67 | .write('popomore/projj') 68 | .expect('code', 0) 69 | .end(err => { 70 | assert.ifError(err); 71 | assert(fs.existsSync(folder) === false); 72 | done(); 73 | }); 74 | }); 75 | 76 | it('should update cache that do not exist', done => { 77 | coffee.fork(binfile, [ 'remove', 'autod-egg' ]) 78 | .expect('stdout', new RegExp(`Do you want to remove the repository ${tempProject}/github.com/eggjs/autod-egg`)) 79 | .expect('stdout', new RegExp('Removed repository cannot be restored!')) 80 | .expect('stdout', new RegExp('Please type in the name of the repository to confirm. eggjs/autod-egg')) 81 | .write('eggjs/autod-egg') 82 | .expect('code', 0) 83 | .end(err => { 84 | assert.ifError(err); 85 | const cache = fs.readFileSync(catchPath); 86 | assert(JSON.parse(cache.toString())['github.com/eggjs/autod-egg'] === undefined); 87 | done(); 88 | }); 89 | }); 90 | 91 | it('could retry if the input is incorrect', done => { 92 | coffee.fork(binfile, [ 'remove', 'autod-egg' ]) 93 | .waitForPrompt() 94 | .expect('stdout', new RegExp(`Do you want to remove the repository ${tempProject}/github.com/eggjs/autod-egg`)) 95 | .expect('stdout', new RegExp('Removed repository cannot be restored!')) 96 | .expect('stdout', new RegExp('Please type in the name of the repository to confirm. eggjs/autod-egg')) 97 | .write('eggjs/egg\n') 98 | .expect('stdout', new RegExp('Do you want to continue')) 99 | .write('Y\n') 100 | .write('eggjs/autod-egg') 101 | .expect('code', 0) 102 | .end(done); 103 | }); 104 | 105 | it('could cancel if the input is incorrect', done => { 106 | coffee.fork(binfile, [ 'remove', 'autod-egg' ]) 107 | // .debug() 108 | .waitForPrompt() 109 | .expect('stdout', new RegExp(`Do you want to remove the repository ${tempProject}/github.com/eggjs/autod-egg`)) 110 | .expect('stdout', new RegExp('Removed repository cannot be restored!')) 111 | .expect('stdout', new RegExp('Please type in the name of the repository to confirm. eggjs/autod-egg')) 112 | .write('eggjs/egg\n') 113 | .expect('stdout', new RegExp('Do you want to continue')) 114 | .write('\n') 115 | .expect('stdout', new RegExp(`Cancel remove repository ${tempProject}/github.com/eggjs/autod-egg`)) 116 | .expect('code', 0) 117 | .end(done); 118 | }); 119 | 120 | it('should find two matching file with egg', done => { 121 | coffee.fork(binfile, [ 'remove', 'egg' ]) 122 | // .debug() 123 | .expect('stdout', new RegExp('Please select the correct repo')) 124 | .write('\n') 125 | .expect('stdout', new RegExp(`Do you want to remove the repository ${tempProject}/github.com/eggjs/egg`)) 126 | // .expect('code', 0) 127 | .end(done); 128 | }); 129 | 130 | it('should find nothing with eggggg', done => { 131 | coffee.fork(binfile, [ 'remove', 'eggggg' ]) 132 | .expect('stderr', new RegExp('Can not find repo eggggg')) 133 | .expect('code', 0) 134 | .end(done); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /test/projj_add.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const coffee = require('coffee'); 6 | const mm = require('mm'); 7 | const { rimraf, mkdirp } = require('mz-modules'); 8 | const assert = require('assert'); 9 | 10 | const binfile = path.join(__dirname, '../bin/projj.js'); 11 | const fixtures = path.join(__dirname, 'fixtures'); 12 | const tmp = path.join(fixtures, 'tmp'); 13 | 14 | describe('test/projj_add.test.js', () => { 15 | 16 | afterEach(mm.restore); 17 | beforeEach(() => rimraf(tmp)); 18 | afterEach(() => rimraf(tmp)); 19 | 20 | it('should add a git repo', done => { 21 | const home = path.join(fixtures, 'base-tmp'); 22 | const cachePath = path.join(home, '.projj/cache.json'); 23 | const repo = 'https://github.com/popomore/projj.git'; 24 | const target = path.join(tmp, 'github.com/popomore/projj'); 25 | mm(process.env, 'HOME', home); 26 | 27 | fs.writeFileSync(cachePath, JSON.stringify({ 28 | 'github.com/popomore/test1': {}, 29 | 'github.com/popomore/test2': { repo: 'https://github.com/popomore/projj.git' }, 30 | })); 31 | 32 | coffee.fork(binfile, [ 'add', repo ]) 33 | // .debug() 34 | .expect('stdout', new RegExp(`Start adding repository ${repo}`)) 35 | .expect('stdout', new RegExp(`Cloning into ${target}`)) 36 | .expect('code', 0) 37 | .end(err => { 38 | assert.ifError(err); 39 | assert(fs.existsSync(path.join(target, 'package.json'))); 40 | 41 | const cache = JSON.parse(fs.readFileSync(cachePath)); 42 | assert(cache[path.join(tmp, 'github.com/popomore/projj')]); 43 | assert(cache[path.join(tmp, 'github.com/popomore/projj')].repo === 'https://github.com/popomore/projj.git'); 44 | assert(cache[path.join(tmp, 'github.com/popomore/test1')].repo === 'git@github.com:popomore/test1.git'); 45 | assert(cache[path.join(tmp, 'github.com/popomore/test2')].repo === 'https://github.com/popomore/projj.git'); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should add a git repo with alias', done => { 51 | const home = path.join(fixtures, 'alias'); 52 | const cachePath = path.join(home, '.projj/cache.json'); 53 | const repo = 'github://popomore/projj'; 54 | const target = path.join(tmp, 'github.com/popomore/projj'); 55 | mm(process.env, 'HOME', home); 56 | 57 | coffee.fork(binfile, [ 'add', repo ]) 58 | // .debug() 59 | .expect('stdout', new RegExp('Start adding repository https://github.com/popomore/projj.git')) 60 | .expect('stdout', new RegExp(`Cloning into ${target}`)) 61 | .expect('code', 0) 62 | .end(err => { 63 | assert.ifError(err); 64 | assert(fs.existsSync(path.join(target, 'package.json'))); 65 | 66 | const cache = JSON.parse(fs.readFileSync(cachePath)); 67 | assert(cache[path.join(tmp, 'github.com/popomore/projj')]); 68 | assert(cache[path.join(tmp, 'github.com/popomore/projj')].repo === 'https://github.com/popomore/projj.git'); 69 | done(); 70 | }); 71 | }); 72 | 73 | it('should throw when target exists', function* () { 74 | const home = path.join(fixtures, 'base-tmp'); 75 | const repo = 'https://github.com/popomore/projj.git'; 76 | const target = path.join(tmp, 'github.com/popomore/projj'); 77 | mm(process.env, 'HOME', home); 78 | yield mkdirp(target); 79 | 80 | yield coffee.fork(binfile, [ 'add', repo ]) 81 | // .debug() 82 | .expect('stdout', new RegExp(`${target} already exist`)) 83 | .expect('stdout', /Copied to clipboard/) 84 | .expect('code', 0) 85 | .end(); 86 | }); 87 | 88 | it('should run hook', done => { 89 | const home = path.join(fixtures, 'hook-add'); 90 | const repo = 'https://github.com/popomore/test.git'; 91 | const target = path.join(tmp, 'github.com/popomore/test'); 92 | mm(process.env, 'HOME', home); 93 | coffee.fork(binfile, [ 'add', repo ]) 94 | // .debug() 95 | .expect('stdout', new RegExp(`pre hook, cwd ${process.cwd()}`)) 96 | .expect('stdout', new RegExp(`post hook, cwd ${target}`)) 97 | .expect('stdout', /pre hook, get package name projj/) 98 | .expect('stdout', /post hook, get package name spm-bump/) 99 | .expect('code', 0) 100 | .end(done); 101 | }); 102 | 103 | it('should run script when changeDirectory is true and platform is darwin', done => { 104 | const home = path.join(fixtures, 'add-change-directory'); 105 | const repo = 'https://github.com/popomore/projj.git'; 106 | const target = path.join(tmp, 'github.com/popomore/projj'); 107 | mm(process.env, 'HOME', home); 108 | 109 | coffee.fork(binfile, [ 'add', repo ]) 110 | // .debug() 111 | .beforeScript(path.join(__dirname, 'fixtures/mock_darwin.js')) 112 | .expect('stdout', new RegExp(`Start adding repository ${repo}`)) 113 | .expect('stdout', new RegExp(`Cloning into ${target}`)) 114 | .expect('stdout', new RegExp(`Change directory to ${target}`)) 115 | .notExpect('stdout', /Copied to clipboard/) 116 | .expect('code', 0) 117 | .end(done); 118 | }); 119 | 120 | it('should run script when changeDirectory is true and platform is not darwin', done => { 121 | const home = path.join(fixtures, 'add-change-directory'); 122 | const repo = 'https://github.com/popomore/projj.git'; 123 | const target = path.join(tmp, 'github.com/popomore/projj'); 124 | mm(process.env, 'HOME', home); 125 | 126 | coffee.fork(binfile, [ 'add', repo ]) 127 | // .debug() 128 | .beforeScript(path.join(__dirname, 'fixtures/mock_not_darwin.js')) 129 | .expect('stdout', new RegExp(`Start adding repository ${repo}`)) 130 | .expect('stdout', new RegExp(`Cloning into ${target}`)) 131 | .expect('stdout', /Copied to clipboard/) 132 | .expect('stderr', new RegExp('Change directory only supported in darwin')) 133 | .expect('code', 0) 134 | .end(done); 135 | }); 136 | 137 | it('should add a git repo when multiple directory', function* () { 138 | const home = path.join(fixtures, 'multiple-directory'); 139 | const repo = 'https://github.com/popomore/projj.git'; 140 | const target = path.join(home, 'a/github.com/popomore/projj'); 141 | mm(process.env, 'HOME', home); 142 | 143 | yield coffee.fork(binfile, [ 'add', repo ]) 144 | // .debug() 145 | .waitForPrompt(false) 146 | .write('\n') 147 | .expect('code', 0) 148 | .expect('stdout', new RegExp(`Start adding repository ${repo}`)) 149 | .expect('stdout', new RegExp(`Cloning into ${target}`)) 150 | .end(); 151 | 152 | assert(fs.existsSync(path.join(target, 'package.json'))); 153 | 154 | const cachePath = path.join(home, '.projj/cache.json'); 155 | const cache = JSON.parse(fs.readFileSync(cachePath)); 156 | assert(cache[target].repo === 'https://github.com/popomore/projj.git'); 157 | 158 | yield rimraf(path.join(home, 'a')); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /lib/base_command.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const inquirer = require('inquirer'); 4 | const path = require('path'); 5 | const fs = require('mz/fs'); 6 | const mkdirp = require('mz-modules/mkdirp'); 7 | const BaseCommand = require('common-bin'); 8 | const ConsoleLogger = require('zlogger'); 9 | const chalk = require('chalk'); 10 | const runscript = require('runscript'); 11 | const through = require('through2'); 12 | const giturl = require('giturl'); 13 | const readJSON = require('utility').readJSON; 14 | const Cache = require('./cache'); 15 | const PROMPT = Symbol('prompt'); 16 | 17 | const configDir = path.join(process.env.HOME, '.projj'); 18 | const configPath = path.join(configDir, 'config.json'); 19 | const cachePath = path.join(configDir, 'cache.json'); 20 | const consoleLogger = new ConsoleLogger({ 21 | time: false, 22 | }); 23 | 24 | const defaults = { 25 | base: `${process.env.HOME}/projj`, 26 | hooks: {}, 27 | alias: { 28 | 'github://': 'https://github.com/', 29 | }, 30 | }; 31 | 32 | class Command extends BaseCommand { 33 | 34 | constructor(rawArgv) { 35 | super(rawArgv); 36 | this.logger = new ConsoleLogger({ 37 | prefix: chalk.green('✔︎ '), 38 | time: false, 39 | }); 40 | this.childLogger = new ConsoleLogger({ 41 | prefix: ' ', 42 | time: false, 43 | stdout: colorStream(process.stdout), 44 | stderr: colorStream(process.stderr), 45 | }); 46 | this.cache = new Cache({ cachePath }); 47 | } 48 | 49 | async run({ cwd, rawArgv }) { 50 | try { 51 | await this.init(); 52 | await this._run(cwd, rawArgv); 53 | consoleLogger.info('✨ Done'); 54 | } catch (err) { 55 | this.error(err.message); 56 | // this.logger.error(err.stack); 57 | process.exit(1); 58 | } 59 | } 60 | 61 | async init() { 62 | await this.loadConfig(); 63 | 64 | const cache = await this.cache.get(); 65 | 66 | if (!cache.version) { 67 | this.logger.warn('Upgrade cache'); 68 | const baseDir = await this.chooseBaseDirectory(); 69 | const keys = await this.cache.getKeys(); 70 | for (const key of keys) { 71 | if (path.isAbsolute(key)) continue; 72 | const value = cache[key]; 73 | await this.cache.remove([ key ]); 74 | await this.cache.set(path.join(baseDir, key), value); 75 | } 76 | 77 | await this.cache.upgrade(); 78 | } 79 | } 80 | 81 | async loadConfig() { 82 | await mkdirp(configDir); 83 | const configExists = await fs.exists(configPath); 84 | let config; 85 | if (configExists) { 86 | config = await readJSON(configPath); 87 | config = resolveConfig(config, defaults); 88 | // ignore when base has been defined in ~/.projj/config 89 | if (config.base) { 90 | this.config = config; 91 | return; 92 | } 93 | } 94 | 95 | const question = { 96 | type: 'input', 97 | name: 'base', 98 | message: 'Set base directory:', 99 | default: defaults.base, 100 | }; 101 | const { base } = await this.prompt([ question ]); 102 | this.config = resolveConfig({ base }, defaults); 103 | await fs.writeFile(configPath, JSON.stringify(this.config, null, 2)); 104 | } 105 | 106 | async runHook(name, cacheKey) { 107 | if (!this.config.hooks[name]) return; 108 | const hook = this.config.hooks[name]; 109 | const env = { 110 | PATH: `${configDir}/hooks:${process.env.PATH}`, 111 | PROJJ_HOOK_NAME: name, 112 | }; 113 | if (this.config[name]) { 114 | env.PROJJ_HOOK_CONFIG = JSON.stringify(this.config[name]); 115 | } 116 | const opt = { 117 | env: Object.assign({}, process.env, env), 118 | }; 119 | 120 | const cwd = cacheKey; 121 | if (cwd && (await fs.exists(cwd))) opt.cwd = cwd; 122 | 123 | this.logger.info('Run hook %s for %s', chalk.green(name), cacheKey); 124 | await this.runScript(hook, opt); 125 | } 126 | 127 | async prompt(questions) { 128 | if (!this[PROMPT]) { 129 | // create a self contained inquirer module. 130 | this[PROMPT] = inquirer.createPromptModule(); 131 | const promptMapping = this[PROMPT].prompts; 132 | for (const key of Object.keys(promptMapping)) { 133 | const Clz = promptMapping[key]; 134 | // extend origin prompt instance to emit event 135 | promptMapping[key] = class CustomPrompt extends Clz { 136 | /* istanbul ignore next */ 137 | static get name() { return Clz.name; } 138 | run() { 139 | process.send && process.send({ type: 'prompt', name: this.opt.name }); 140 | process.emit('message', { type: 'prompt', name: this.opt.name }); 141 | return super.run(); 142 | } 143 | }; 144 | } 145 | } 146 | return this[PROMPT](questions); 147 | } 148 | 149 | async runScript(cmd, opt) { 150 | const stdout = through(); 151 | stdout.pipe(this.childLogger.stdout, { end: false }); 152 | opt = Object.assign({}, { 153 | stdio: 'pipe', 154 | stdout, 155 | }, opt); 156 | try { 157 | await runscript(cmd, opt); 158 | } catch (err) { 159 | const stderr = err.stdio.stderr; 160 | if (stderr) { 161 | this.childLogger.info(stderr.toString()); 162 | } 163 | throw err; 164 | } 165 | } 166 | 167 | error(msg) { 168 | consoleLogger.error(chalk.red('✘ ' + msg)); 169 | } 170 | 171 | // https://github.com/popomore/projj.git 172 | // => $BASE/github.com/popomore/projj 173 | url2dir(url) { 174 | url = giturl.parse(url); 175 | return url.replace(/https?:\/\//, ''); 176 | } 177 | 178 | async addRepo(repo, cacheKey) { 179 | // preadd hook 180 | await this.runHook('preadd', cacheKey); 181 | 182 | const targetPath = cacheKey; 183 | this.logger.info('Cloning into %s', chalk.green(targetPath)); 184 | 185 | const env = Object.assign({ 186 | GIT_SSH: path.join(__dirname, 'ssh.js'), 187 | }, process.env); 188 | await this.runScript(`git clone ${repo} ${targetPath} > /dev/null`, { 189 | env, 190 | }); 191 | // add this repository to cache.json 192 | await this.cache.set(cacheKey, { repo }); 193 | await this.cache.dump(); 194 | 195 | // preadd hook 196 | await this.runHook('postadd', cacheKey); 197 | } 198 | 199 | async chooseBaseDirectory() { 200 | const baseDirectories = this.config.base; 201 | if (baseDirectories.length === 1) return baseDirectories[0]; 202 | 203 | const question = { 204 | type: 'list', 205 | name: 'base', 206 | message: 'Choose base directory', 207 | choices: baseDirectories, 208 | }; 209 | const { base } = await this.prompt([ question ]); 210 | return base; 211 | } 212 | } 213 | 214 | module.exports = Command; 215 | 216 | function resolveConfig(config, defaults) { 217 | config = Object.assign({}, defaults, config); 218 | if (!Array.isArray(config.base)) { 219 | config.base = [ config.base ]; 220 | } 221 | config.base = config.base.map(baseDir => { 222 | switch (baseDir[0]) { 223 | case '.': 224 | return path.join(path.dirname(configPath), baseDir); 225 | case '~': 226 | return baseDir.replace('~', process.env.HOME); 227 | case '/': 228 | return baseDir; 229 | default: 230 | return path.join(process.cwd(), baseDir); 231 | } 232 | }); 233 | 234 | return config; 235 | } 236 | 237 | function colorStream(stream) { 238 | const s = through(function(buf, _, done) { 239 | done(null, chalk.gray(buf.toString())); 240 | }); 241 | s.pipe(stream); 242 | return s; 243 | } 244 | --------------------------------------------------------------------------------