├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── img ├── cover.png └── happy.png ├── index.js ├── package.json └── src ├── analyze.js ├── build.js ├── helpers.js ├── index.js ├── lint.js ├── publish.js ├── pull.js ├── push.js ├── save.js └── test.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.paypal.me/franciscopresencia/19 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Temporary folder 9 | /temp 10 | 11 | # Mac temporal file 12 | .DS_Store 13 | 14 | # SASS Cache 15 | .sass-cache 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # Yarn Integrity file 64 | .yarn-integrity 65 | 66 | # dotenv environment variables file 67 | .env 68 | 69 | # next.js build output 70 | .next 71 | 72 | # This is a library so don't include it 73 | package-lock.json 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Francisco Presencia 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Happy 2 | 3 | Happy simplifies your day-to-day git workflow: 4 | 5 | ```bash 6 | $ happy 7 | $ happy "Move the dates to ISO 8601" 8 | $ happy "Quick hot fix" --now 9 | ``` 10 | 11 | screenshot 12 | 13 | _happy_ analyzes your project to find the appropriate npm scripts to run and then commits and deploys those changes with git. 14 | 15 | ## Getting started 16 | 17 | First install it globally: 18 | 19 | ```bash 20 | npm install happy -g 21 | ``` 22 | 23 | Then you can run it in your console, either with just `happy` or with `happy "Message"`. Run `happy --help` anytime: 24 | 25 | ```bash 26 | $ happy --help 27 | 28 | Happy simplifies your day-to-day git workflow. 29 | 30 | Usage 31 | $ happy 32 | $ happy "Message here" --now 33 | $ happy "Message here" --publish patch 34 | 35 | Options 36 | --now Skip build, lint and tests to deploy the changes *now* 37 | --publish VERSION Publish your package to NPM with "np VERSION --yolo" 38 | --patch Alias for --publish patch 39 | --minor Alias for --publish minor 40 | --major Alias for --publish major 41 | 42 | Examples 43 | $ happy 44 | ✔ Building project 45 | ↓ Linting 46 | ✔ Testing project 47 | ✔ Saving changes 48 | ✔ Downloading latest 49 | ✔ Uploading changes 50 | 51 | $ happy "Move the dates to ISO 8601" 52 | ✔ Building project 53 | ↓ Linting 54 | ✔ Testing project 55 | ✔ Saving changes 56 | ↓ Downloading latest 57 | ✔ Uploading changes 58 | 59 | $ happy --now 60 | ✔ Saving changes 61 | ↓ Downloading latest 62 | ✔ Uploading changes 63 | ``` 64 | 65 | 66 | ## What it does 67 | 68 | It makes sure your project is ready to deploy, and then deploy it. For this, these are the steps: 69 | 70 | - ["Building project"](#building-project): run `npm run build` *if* the `"build"` script is found in your `package.json`. 71 | - ["Linting"](#linting): run `npm run lint` *if* the `"lint"` script is found in the project `package.json`. 72 | - ["Testing project"](#testing-project): run `npm test` *if* the `"test"` script is found in the project `package.json`. 73 | - ["Saving changes"](#saving-changes): add all of the files with git, equivalent to `git add . && git commit -m "Saved on $TIME"`. Provide a message for a custom git message. 74 | - ["Downloading latest"](#downloading-latest): git pull 75 | - ["Uploading changes"](#uploading-changes): git push 76 | - ["Publish to npm"](#publish-to-npm): _only_ if the `--publish` flag is passed, publish it to npm. 77 | 78 | 79 | 80 | ### Building project 81 | 82 | Run the `npm run build` script *if* this script is found in your `package.json` configuration. Example: 83 | 84 | ```json 85 | { 86 | "scripts": { 87 | "build": "rollup -c" 88 | } 89 | } 90 | ``` 91 | 92 | This step will be **skipped** if: 93 | - The script `"build"` is not found in the project `package.json`. 94 | - The flag `--now` was passed. 95 | 96 | 97 | 98 | ### Linting 99 | 100 | Run the `npm run lint` script *if* this script is found in your `package.json` configuration. Example: 101 | 102 | ```json 103 | { 104 | "scripts": { 105 | "lint": "eslint" 106 | } 107 | } 108 | ``` 109 | 110 | This step will be **skipped** if: 111 | - The script `"lint"` is not found in the project `package.json`. 112 | - The flag `--now` was passed. 113 | 114 | 115 | 116 | ### Testing project 117 | 118 | Run the `npm test` script *if* this script is found in your `package.json` configuration. Example: 119 | 120 | ```json 121 | { 122 | "scripts": { 123 | "test": "jest" 124 | } 125 | } 126 | ``` 127 | 128 | The test script will also set the environment variable CI=true to avoid [some common issues](https://stackoverflow.com/a/56917151/938236). 129 | 130 | This step will be **skipped** if: 131 | - The script `"test"` is not found in the project `package.json`. 132 | - The flag `--now` was passed. 133 | 134 | 135 | 136 | ### Saving Changes 137 | 138 | This is the equivalent of _adding_ and _commiting_ the changed files to Git. The message for the commit is the string that you pass: 139 | 140 | ```bash 141 | happy "Added that new cool feature" 142 | ``` 143 | 144 | When no string is provided, it will save the changes with a generic commit with the current timestamp like: 145 | 146 | ``` 147 | Saved on 2020-08-13T10:20:00Z 148 | ``` 149 | 150 | This step will be **skipped** if: 151 | - There are no changes to add or commit. 152 | - The changes were already commited. 153 | 154 | 155 | 156 | ### Downloading latest 157 | 158 | Try to pull the latest changes from the remote repo to combine them locally. It will exit if there's a problem with the merge so that you can merge it manually. 159 | 160 | > This step might take longer than the others since it talks to your git server. 161 | 162 | This step will be **skipped** if: 163 | - There were no changes in the remote repo (you are up to date). 164 | 165 | This step will **throw an error** if: 166 | - The origin is not set. 167 | 168 | > TODO: ask/fix the origin if it's not set 169 | 170 | 171 | 172 | ### Uploading changes 173 | 174 | Take all of your changes and upload them to the `origin` that is set in your project. This is specially useful when combined with e.g. Heroku, and you set heroku as the origin, since it will also deploy the full website. 175 | 176 | This step takes longer than the others since it's talking to your git server. 177 | 178 | This step will be **skipped** if: 179 | - There were no changes in the local repo. 180 | 181 | 182 | 183 | ### Publish to npm 184 | 185 | > You need to have the library `np` installed for this, please do `npm i np -g` 186 | 187 | Add a `--publish VERSION` flag to publish the current package to npm with [np](https://github.com/sindresorhus/np#readme): 188 | 189 | ```bash 190 | happy --publish patch 191 | happy --publish minor 192 | happy --publish major 193 | 194 | happy --publish 5.0.0 195 | ``` 196 | 197 | As an alias, you can do with just `--patch`, `--minor` or `--major` instead: 198 | 199 | ```bash 200 | happy --patch 201 | happy --minor 202 | happy --major 203 | 204 | happy --publish 5.0.0 205 | ``` 206 | 207 | 208 | -------------------------------------------------------------------------------- /img/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/happy/bf2778822ccd221196289111a2d6677f62c84b72/img/cover.png -------------------------------------------------------------------------------- /img/happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/happy/bf2778822ccd221196289111a2d6677f62c84b72/img/happy.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import listr from "listr"; 4 | import meow from "meow"; 5 | 6 | import { 7 | analyze, 8 | build, 9 | lint, 10 | publish, 11 | pull, 12 | push, 13 | save, 14 | test, 15 | } from "./src/index.js"; 16 | 17 | const { flags, input } = meow( 18 | ` 19 | Usage 20 | $ happy 21 | $ happy "Message here" 22 | 23 | Options 24 | --now Skip building, linting and testing to deploy it now 25 | --publish VERSION Publish your package to NPM with "np VERSION --yolo" 26 | --patch Alias for --publish patch 27 | --minor Alias for --publish minor 28 | --major Alias for --publish major 29 | 30 | Examples 31 | $ happy 32 | ✔ Building project 33 | ↓ Linting 34 | ✔ Testing project 35 | ✔ Saving changes 36 | ✔ Downloading latest 37 | ✔ Uploading changes 38 | 39 | $ happy "Move the dates to ISO 8601" 40 | ✔ Building project 41 | ↓ Linting 42 | ✔ Testing project 43 | ✔ Saving changes 44 | ✔ Downloading latest 45 | ✔ Uploading changes 46 | `, 47 | { 48 | importMeta: import.meta, 49 | flags: { 50 | now: { 51 | type: "boolean", 52 | alias: "n", 53 | }, 54 | publish: { 55 | type: "string", 56 | alias: "p", 57 | }, 58 | patch: { 59 | type: "boolean", 60 | }, 61 | minor: { 62 | type: "boolean", 63 | }, 64 | major: { 65 | type: "boolean", 66 | }, 67 | }, 68 | } 69 | ); 70 | 71 | const action = [save, pull, push]; 72 | 73 | if (!flags.now) { 74 | action.unshift(build, lint, test); 75 | } 76 | 77 | if (flags.patch && !flags.publish) { 78 | flags.publish = "patch"; 79 | } 80 | if (flags.minor && !flags.publish) { 81 | flags.publish = "minor"; 82 | } 83 | if (flags.major && !flags.publish) { 84 | flags.publish = "major"; 85 | } 86 | if (flags.publish) { 87 | action.push(publish); 88 | } 89 | 90 | const tasks = new listr(action.map((task) => task({ flags, input }))); 91 | 92 | try { 93 | const ctx = await analyze(); 94 | await tasks.run(ctx); 95 | } catch (error) { 96 | console.log("Failed..."); 97 | setTimeout(() => { 98 | console.log(error.stdout?.trim?.(), "\n\n", error.message?.trim?.()); 99 | }, 1000); 100 | } 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happy", 3 | "version": "1.0.2", 4 | "description": "Happy simplifies your day-to-day git workflow", 5 | "author": "Francisco Presencia (https://francisco.io/)", 6 | "funding": { 7 | "url": "https://www.paypal.me/franciscopresencia/19" 8 | }, 9 | "license": "MIT", 10 | "scripts": {}, 11 | "homepage": "https://github.com/franciscop/happy", 12 | "repository": "https://github.com:franciscop/happy.git", 13 | "bugs": "https://github.com/franciscop/happy/issues", 14 | "keywords": [ 15 | "happy", 16 | "command", 17 | "cli", 18 | "git", 19 | "save", 20 | "deploy" 21 | ], 22 | "bin": { 23 | "happy": "index.js" 24 | }, 25 | "main": "index.js", 26 | "type": "module", 27 | "dependencies": { 28 | "atocha": "^2.0.0", 29 | "cross-env": "^7.0.2", 30 | "files": "^2.2.2", 31 | "listr": "^0.14.3", 32 | "meow": "^11.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/analyze.js: -------------------------------------------------------------------------------- 1 | // Analyze the project and find the right script for each thing 2 | import { exists, read } from "files"; 3 | 4 | export default async (ctx) => { 5 | if (!(await exists("package.json"))) return {}; 6 | const pkg = await read("package.json"); 7 | return { pkg: JSON.parse(pkg) }; 8 | }; 9 | -------------------------------------------------------------------------------- /src/build.js: -------------------------------------------------------------------------------- 1 | import cmd from "atocha"; 2 | import { stderrok } from "./helpers.js"; 3 | 4 | export default (cli) => ({ 5 | title: "Building project", 6 | skip: async (ctx) => { 7 | if (!ctx.pkg) return true; 8 | if (!ctx.pkg.scripts.build) return true; 9 | }, 10 | task: async () => cmd("npm run build").catch(stderrok), 11 | }); 12 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | // Accept these errors 2 | export const wtf = (err) => { 3 | if (/branch\s+master\s+-> FETCH_HEAD/.test(err.message)) return; 4 | if (/master -> master/.test(err.message)) return; 5 | if (/Everything up-to-date/.test(err.message)) return; 6 | throw err; 7 | }; 8 | 9 | // Only throw if the error is not 0 10 | export const stderrok = (error) => { 11 | // 0 or undefined should be ignored 12 | if (!error.code) return; 13 | throw error; 14 | }; 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import analyze from "./analyze.js"; 2 | import build from "./build.js"; 3 | import lint from "./lint.js"; 4 | import publish from "./publish.js"; 5 | import pull from "./pull.js"; 6 | import push from "./push.js"; 7 | import save from "./save.js"; 8 | import test from "./test.js"; 9 | 10 | export { analyze, build, lint, publish, pull, push, save, test }; 11 | -------------------------------------------------------------------------------- /src/lint.js: -------------------------------------------------------------------------------- 1 | import cmd from "atocha"; 2 | import { stderrok } from "./helpers.js"; 3 | 4 | const ci = "export CI=true || set CI=true&&"; 5 | 6 | export default (cli) => ({ 7 | title: "Linting", 8 | skip: async (ctx) => { 9 | if (!ctx.pkg) return true; 10 | if (!ctx.pkg.scripts.lint && !ctx.pkg.scripts.linter) return true; 11 | }, 12 | task: async (ctx) => { 13 | if (ctx.pkg.scripts.lint) { 14 | return await cmd(`${ci} npm run lint`).catch(stderrok); 15 | } 16 | if (ctx.pkg.scripts.linter) { 17 | return await cmd(`${ci} npm run linter`).catch(stderrok); 18 | } 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/publish.js: -------------------------------------------------------------------------------- 1 | import cmd from "atocha"; 2 | 3 | export default (cli) => ({ 4 | title: "Publish to npm", 5 | skip: async () => { 6 | if (!cli.flags.publish) return true; 7 | // 5.0.0 8 | if (!/^\d+\.\d+\.\d+$/.test(await cmd(`np --version`))) { 9 | throw new Error('Need `np` installed, please run "npm install -g np"'); 10 | } 11 | return false; 12 | }, 13 | task: async () => 14 | await cmd(`np ${cli.flags.publish} --yolo --no-release-draft`), 15 | }); 16 | -------------------------------------------------------------------------------- /src/pull.js: -------------------------------------------------------------------------------- 1 | import cmd from "atocha"; 2 | import { wtf } from "./helpers.js"; 3 | 4 | export default (cli) => ({ 5 | title: "Downloading latest", 6 | skip: async () => { 7 | const status = await cmd(`git status`); 8 | const ahead = /Your branch is ahead of/.test(status); 9 | if (ahead) return true; 10 | const updated = /Your branch is up to date with/.test(status); 11 | if (updated) return true; 12 | }, 13 | task: async () => await cmd(`git pull origin master`).catch(wtf), 14 | }); 15 | -------------------------------------------------------------------------------- /src/push.js: -------------------------------------------------------------------------------- 1 | import cmd from "atocha"; 2 | import { wtf } from "./helpers.js"; 3 | 4 | export default (cli) => ({ 5 | title: "Uploading changes", 6 | skip: async () => { 7 | const status = await cmd(`git status`); 8 | const hasCommited = /Your branch is ahead of/.test(status); 9 | if (!hasCommited) return true; 10 | }, 11 | task: async () => await cmd(`git push`).catch(wtf), 12 | }); 13 | -------------------------------------------------------------------------------- /src/save.js: -------------------------------------------------------------------------------- 1 | import cmd from "atocha"; 2 | import { stderrok } from "./helpers.js"; 3 | 4 | // ISO 8601 without milliseconds (which is still ISO 8601) 5 | const time = () => new Date().toISOString().replace(/\.[0-9]{3}/, ""); 6 | 7 | export default (cli) => ({ 8 | title: "Saving changes", 9 | skip: async () => { 10 | const status = await cmd(`git status`); 11 | const hasAdded = /untracked files present/i.test(status); 12 | const hasEdited = /Changes not staged for commit/i.test(status); 13 | const hasUncommited = /Changes to be committed/i.test(status); 14 | if (!hasAdded && !hasEdited && !hasUncommited) return true; 15 | }, 16 | task: async () => { 17 | const message = cli.input[0] || `Saved on ${time()}`; 18 | await cmd(`git add . -A`).catch(stderrok); 19 | return await cmd(`git commit -m "${message}"`).catch(stderrok); 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | import cmd from "atocha"; 2 | 3 | import { stderrok } from "./helpers.js"; 4 | 5 | const ci = "export CI=true || set CI=true&&"; 6 | 7 | export default (cli) => ({ 8 | title: "Testing project", 9 | skip: async (ctx) => { 10 | if (!ctx.pkg) return true; 11 | if (!ctx.pkg.scripts.test) return true; 12 | }, 13 | task: async () => cmd(`${ci} npm run test`).catch(stderrok), 14 | }); 15 | --------------------------------------------------------------------------------