├── .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 |
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 |
--------------------------------------------------------------------------------