├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── bin └── timetracking ├── lib └── README.md ├── package-lock.json ├── package.json ├── src ├── core │ ├── task-status.ts │ ├── task.ts │ └── timetracking.ts └── main.ts ├── test └── test.js ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### GIS ### 3 | *.gpx 4 | *.kml 5 | 6 | ### Node ### 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (http://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Typescript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | 67 | ### VisualStudioCode ### 68 | .vscode/* 69 | !.vscode/settings.json 70 | !.vscode/tasks.json 71 | !.vscode/launch.json 72 | !.vscode/extensions.json 73 | .history 74 | 75 | ### Windows ### 76 | # Windows thumbnail cache files 77 | Thumbs.db 78 | ehthumbs.db 79 | ehthumbs_vista.db 80 | 81 | # Folder config file 82 | Desktop.ini 83 | 84 | # Recycle Bin used on file shares 85 | $RECYCLE.BIN/ 86 | 87 | # Windows Installer files 88 | *.cab 89 | *.msi 90 | *.msm 91 | *.msp 92 | 93 | # Windows shortcuts 94 | *.lnk 95 | 96 | 97 | ### Others ### 98 | lib/**/* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | ### GIS ### 3 | *.gpx 4 | *.kml 5 | 6 | ### Node ### 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (http://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Typescript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | 67 | ### VisualStudioCode ### 68 | .vscode/* 69 | !.vscode/settings.json 70 | !.vscode/tasks.json 71 | !.vscode/launch.json 72 | !.vscode/extensions.json 73 | .history 74 | 75 | ### Windows ### 76 | # Windows thumbnail cache files 77 | Thumbs.db 78 | ehthumbs.db 79 | ehthumbs_vista.db 80 | 81 | # Folder config file 82 | Desktop.ini 83 | 84 | # Recycle Bin used on file shares 85 | $RECYCLE.BIN/ 86 | 87 | # Windows Installer files 88 | *.cab 89 | *.msi 90 | *.msm 91 | *.msp 92 | 93 | # Windows shortcuts 94 | *.lnk 95 | 96 | 97 | ### NPM ### 98 | tsconfig.json 99 | tslint.json 100 | /src/**/* 101 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [v1.3.0](https://github.com/mvmjacobs/timetracking/releases/tag/v1.3.0) | 2019-02-03 3 | 4 | ### Improvements 5 | * **test**: Add initial unit test coverage. ([#22](https://github.com/mvmjacobs/timetracking/issues/22)) ([58255fb1](https://github.com/mvmjacobs/timetracking/commit/58255fb1)) 6 | 7 | 8 | ## [v1.2.2](https://github.com/mvmjacobs/timetracking/releases/tag/v1.2.2) | 2017-12-11 9 | 10 | ### Bug Fixes 11 | * **pause**: Fixed the command that was finishing the task instead of pausing. ([#23](https://github.com/mvmjacobs/timetracking/issues/23)) ([074242e](https://github.com/mvmjacobs/timetracking/commit/074242e)) 12 | 13 | 14 | ## [v1.2.1](https://github.com/mvmjacobs/timetracking/releases/tag/v1.2.1) | 2017-10-24 15 | 16 | ### Bug Fixes 17 | * **finish|pause**: Fixed commands when the `-t` option is not enter. ([#21](https://github.com/mvmjacobs/timetracking/issues/21)) ([beefa0d](https://github.com/mvmjacobs/timetracking/commit/beefa0d)) 18 | 19 | 20 | ## [v1.2.0](https://github.com/mvmjacobs/timetracking/releases/tag/v1.2.0) | 2017-10-23 21 | 22 | ### Improvements 23 | * **config**: Default date format in config file was changed to `MM/dd/yyyy`. ([#19](https://github.com/mvmjacobs/timetracking/issues/19)) ([96d9474](https://github.com/mvmjacobs/timetracking/commit/96d9474)) 24 | * **start**: Now the task description is informed from an option. [See here](https://github.com/mvmjacobs/timetracking#start) to know how to use it. ([#16](https://github.com/mvmjacobs/timetracking/issues/16)) ([f7cbc67](https://github.com/mvmjacobs/timetracking/commit/f7cbc67)) 25 | * **finish|pause**: Now you can stop a task with date and time. [See here](https://github.com/mvmjacobs/timetracking#pause) to know how. ([#18](https://github.com/mvmjacobs/timetracking/issues/18)) ([81f4c69](https://github.com/mvmjacobs/timetracking/commit/81f4c69), [40ced82](https://github.com/mvmjacobs/timetracking/commit/40ced82)) 26 | 27 | ### Bug Fixes 28 | * **finish|pause**: Fix console message when the task has already been stopped. ([#20](https://github.com/mvmjacobs/timetracking/issues/20)) ([d2fb57c](https://github.com/mvmjacobs/timetracking/commit/d2fb57c)) 29 | 30 | 31 | ## [v1.1.1](https://github.com/mvmjacobs/timetracking/releases/tag/v1.1.1) | 2017-09-04 32 | 33 | ### Improvements 34 | * **list**: Now displays the task status. ([#14](https://github.com/mvmjacobs/timetracking/issues/14)) ([61d25ce](https://github.com/mvmjacobs/timetracking/commit/61d25ce)) 35 | 36 | ### Bug Fixes 37 | * **add**: Fix expression to validate `[time_spent]` parameter. ([#13](https://github.com/mvmjacobs/timetracking/issues/13)) ([9a495f1](https://github.com/mvmjacobs/timetracking/commit/9a495f1)) 38 | 39 | 40 | ## [v1.1.0](https://github.com/mvmjacobs/timetracking/releases/tag/v1.1.0) | 2017-09-04 41 | 42 | ### Features 43 | * **add**: New command! Now you can add tasks running the command `add`. [Here](https://github.com/mvmjacobs/timetracking/tree/master#add) you can see how to use. ([#8](https://github.com/mvmjacobs/timetracking/issues/8)) ([a1f5a6](https://github.com/mvmjacobs/timetracking/commit/a1f5a6)) 44 | * **pause**: Now `task` parameter is optional, you can pause all tasks in progress. ([#4](https://github.com/mvmjacobs/timetracking/issues/4)) ([4ab557f](https://github.com/mvmjacobs/timetracking/commit/4ab557f)) 45 | * **finish**: Now `task` parameter is optional, you can complete all tasks in progress. ([#4](https://github.com/mvmjacobs/timetracking/issues/4)) ([4ab557f](https://github.com/mvmjacobs/timetracking/commit/4ab557f)) 46 | 47 | 48 | ## [v1.0.1](https://github.com/mvmjacobs/timetracking/releases/tag/v1.0.1) | 2017-09-02 49 | 50 | ### Bug Fixes 51 | * Now when you type `tm` command will show help section. 52 | * Tasks list now displays the hours in `HH:mm` format instead `H:mm`. 53 | * Date format used is the same as the one saved in the settings file. 54 | 55 | 56 | # [v1.0.0](https://github.com/mvmjacobs/timetracking/releases/tag/v1.0.0) | 2017-08-27 57 | 58 | * Initial release. 59 | * Basic options: start, pause, finish, list. 60 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Want to contribute? 2 | 3 | Found a bug or have an idea to improvement? First [open an issue](https://github.com/mvmjacobs/timetracking/issues) and we will discuss about it. 4 | 5 | But if you want to resolve an issue, first make sure the issue is not tagged with `committed` neither `in progress`. After that, follow the instructions below to make a pull request. 6 | 7 | ### How to make a clean pull request 8 | 9 | - Create a personal fork of the project on Github. 10 | - Clone the fork on your local machine. Your remote repo on Github is called `origin`. 11 | - Add the original repository as a remote called `upstream`. 12 | ``` 13 | $ git remote add upstream https://github.com/mvmjacobs/timetracking.git 14 | ``` 15 | - If you created your fork a while ago be sure to pull upstream changes into your local repository. 16 | - Create a new branch to work on! Branch from `dev-z.x.y` (current version number) if it exists, else from `master`. 17 | - Implement/fix your feature, comment your code. 18 | - Add or change the documentation as needed. 19 | - Use [keywords](https://help.github.com/articles/closing-issues-using-keywords/) in your commit message. To bug fixes use `(fixes #n)`, to improvements and new features use `(resolves #n)`. (When `n` is the issue number). 20 | - Squash your commits into a single commit with git's [interactive rebase](https://help.github.com/articles/interactive-rebase). Create a new branch if necessary. 21 | - Push your branch to your fork on Github, the remote `origin`. 22 | - From your fork open a pull request in the correct branch. Target the project's `dev-z.x.y` (current version number) branch if there is one, else go for `master` 23 | - Once the pull request is approved and merged you can pull the changes from `upstream` to your local repo and delete 24 | your extra branch(es). 25 | 26 | And last but not least: Always write your commit messages in the present tense. Your commit message should describe what the commit, when applied, does to the code – not what you did to the code. 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Marcos Vinicius Jacobs 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 | # Timetracking :hourglass: 2 | [![npm version](https://img.shields.io/npm/v/timetracking.svg)](https://www.npmjs.com/package/timetracking) 3 | [![license](https://img.shields.io/github/license/mvmjacobs/timetracking.svg)](https://github.com/mvmjacobs/timetracking/blob/master/LICENSE.md) 4 | [![npm downloads](https://img.shields.io/npm/dt/timetracking.svg)](https://www.npmjs.com/package/timetracking) 5 | 6 | **This project is not maintained anymore :(** 7 | 8 | A simple command line app to track your time. It was inspired by [Timet](https://github.com/fabiorogeriosj/timet) and [Time tracker](https://github.com/danibram/time-tracker-cli). 9 | 10 | 11 | ## What's new? 12 | [**Here**](https://github.com/mvmjacobs/timetracking/blob/master/CHANGELOG.md) you can find a complete list of improvements and fixes made in the current version. 13 | 14 | 15 | ## Installation 16 | ``` 17 | $ npm install timetracking -g 18 | ``` 19 | Now you can start to call `timetracking` or `tm` command. 20 | 21 | The data and the default config are stored inside `~/.config/configstore/timetracking.json`. 22 | 23 | 24 | ## How to use 25 | [Get started in 5 minutes](https://github.com/mvmjacobs/timetracking/wiki). 26 | 27 | 28 | ## How to contribute 29 | Found a bug? Wrote a cool code? Do you want to help with the documentation? Great! [Learn how to contribute](https://github.com/mvmjacobs/timetracking/blob/master/CONTRIBUTING.md) or [open an issue](https://github.com/mvmjacobs/timetracking/issues) and help make the app even better. 30 | 31 | 32 | ## License 33 | This repository is licensed under the [MIT License](https://github.com/mvmjacobs/timetracking/blob/master/LICENSE.md). 34 | -------------------------------------------------------------------------------- /bin/timetracking: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | require('../lib/main.js') 5 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | ## Read This! 2 | 3 | **These files are not meant to be edited by hand.** 4 | 5 | Files in this folder are generated automatically when generating the build. 6 | 7 | If you need to make modifications, the respective files should be changed within the repository's top-level `src` directory. 8 | 9 | Running `tsc` will then appropriately update the files in this directory. 10 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timetracking", 3 | "version": "1.3.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/lodash": { 8 | "version": "4.14.88", 9 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.88.tgz", 10 | "integrity": "sha512-S+7dONqCP8kXqOPCPxra7Vw8FndWS8pNfVkClMSzRCPMjXkA3BaI3ybMBCiSd4y+r1vC49UEjNthnm+fO8J/dw==", 11 | "dev": true 12 | }, 13 | "@types/node": { 14 | "version": "8.0.58", 15 | "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.58.tgz", 16 | "integrity": "sha512-V746iUU7eHNdzQipoACuguDlVhC7IHK8CES1jSkuFt352wwA84BCWPXaGekBd7R5XdNK5ReHONDVKxlL9IreAw==" 17 | }, 18 | "colors": { 19 | "version": "1.1.2", 20 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", 21 | "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" 22 | }, 23 | "commander": { 24 | "version": "2.12.2", 25 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", 26 | "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==" 27 | }, 28 | "configstore": { 29 | "version": "3.1.1", 30 | "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.1.tgz", 31 | "integrity": "sha512-5oNkD/L++l0O6xGXxb1EWS7SivtjfGQlRyxJsYgE0Z495/L81e2h4/d3r969hoPXuFItzNOKMtsXgYG4c7dYvw==", 32 | "requires": { 33 | "dot-prop": "4.2.0", 34 | "graceful-fs": "4.1.11", 35 | "make-dir": "1.1.0", 36 | "unique-string": "1.0.0", 37 | "write-file-atomic": "2.3.0", 38 | "xdg-basedir": "3.0.0" 39 | } 40 | }, 41 | "crypto-random-string": { 42 | "version": "1.0.0", 43 | "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", 44 | "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" 45 | }, 46 | "dot-prop": { 47 | "version": "4.2.0", 48 | "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", 49 | "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", 50 | "requires": { 51 | "is-obj": "1.0.1" 52 | } 53 | }, 54 | "graceful-fs": { 55 | "version": "4.1.11", 56 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", 57 | "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" 58 | }, 59 | "imurmurhash": { 60 | "version": "0.1.4", 61 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 62 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" 63 | }, 64 | "is-obj": { 65 | "version": "1.0.1", 66 | "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", 67 | "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" 68 | }, 69 | "lodash": { 70 | "version": "4.17.4", 71 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 72 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 73 | }, 74 | "make-dir": { 75 | "version": "1.1.0", 76 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.1.0.tgz", 77 | "integrity": "sha512-0Pkui4wLJ7rxvmfUvs87skoEaxmu0hCUApF8nonzpl7q//FWp9zu8W61Scz4sd/kUiqDxvUhtoam2efDyiBzcA==", 78 | "requires": { 79 | "pify": "3.0.0" 80 | } 81 | }, 82 | "moment": { 83 | "version": "2.19.4", 84 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.4.tgz", 85 | "integrity": "sha512-1xFTAknSLfc47DIxHDUbnJWC+UwgWxATmymaxIPQpmMh7LBm7ZbwVEsuushqwL2GYZU0jie4xO+TK44hJPjNSQ==" 86 | }, 87 | "pify": { 88 | "version": "3.0.0", 89 | "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", 90 | "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" 91 | }, 92 | "signal-exit": { 93 | "version": "3.0.2", 94 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 95 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 96 | }, 97 | "typescript": { 98 | "version": "2.6.2", 99 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz", 100 | "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=", 101 | "dev": true 102 | }, 103 | "unique-string": { 104 | "version": "1.0.0", 105 | "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", 106 | "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", 107 | "requires": { 108 | "crypto-random-string": "1.0.0" 109 | } 110 | }, 111 | "write-file-atomic": { 112 | "version": "2.3.0", 113 | "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", 114 | "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", 115 | "requires": { 116 | "graceful-fs": "4.1.11", 117 | "imurmurhash": "0.1.4", 118 | "signal-exit": "3.0.2" 119 | } 120 | }, 121 | "xdg-basedir": { 122 | "version": "3.0.0", 123 | "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", 124 | "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timetracking", 3 | "version": "1.3.0", 4 | "description": "A simple command line app to track your time.", 5 | "keywords": [ 6 | "timetracking", 7 | "time", 8 | "tracker" 9 | ], 10 | "author": "@mvmjacobs", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/mvmjacobs/timetracking" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/mvmjacobs/timetracking/issues" 18 | }, 19 | "main": "lib/main.js", 20 | "types": "lib/main", 21 | "bin": { 22 | "tm": "./bin/timetracking", 23 | "timetracking": "./bin/timetracking" 24 | }, 25 | "scripts": { 26 | "build": "tsc", 27 | "test": "tsc && mocha", 28 | "pack-build": "tsc && npm pack && move ./*.tgz ./build/", 29 | "install-build-dev": "npm run pack-build && npm install ./build/timetracking-1.3.0.tgz -g" 30 | }, 31 | "devDependencies": { 32 | "@types/lodash": "^4.14.88", 33 | "mocha": "^5.2.0", 34 | "typescript": "^2.6.2" 35 | }, 36 | "dependencies": { 37 | "@types/node": "^8.0.58", 38 | "colors": "^1.1.2", 39 | "commander": "^2.12.2", 40 | "configstore": "^3.1.1", 41 | "lodash": "^4.17.4", 42 | "moment": "^2.19.4" 43 | } 44 | } -------------------------------------------------------------------------------- /src/core/task-status.ts: -------------------------------------------------------------------------------- 1 | export enum TaskStatus { 2 | IN_PROGRESS, 3 | PAUSED, 4 | FINISHED 5 | } 6 | -------------------------------------------------------------------------------- /src/core/task.ts: -------------------------------------------------------------------------------- 1 | // import 3th party packages 2 | import * as _ from 'lodash/array'; 3 | import * as moment from 'moment'; 4 | 5 | // import models 6 | import { TaskStatus } from './task-status'; 7 | 8 | export class Task { 9 | public name: string; 10 | public description: string; 11 | public status: TaskStatus; 12 | public log: any[]; 13 | 14 | constructor(name: string, values: any) { 15 | this.name = name; 16 | this.description = values ? values.description : ''; 17 | this.status = values ? values.status : TaskStatus.IN_PROGRESS; 18 | this.log = values ? values.log : []; 19 | } 20 | 21 | public setDescription(description: string): void { 22 | this.description = description ? description : this.description ? this.description : ''; 23 | } 24 | 25 | public setStatus(status: TaskStatus): void { 26 | this.status = status; 27 | } 28 | 29 | public start(description: string): boolean { 30 | let lastTime = _.last(this.log); 31 | if (lastTime && lastTime.start && !lastTime.stop) { 32 | console.log('This task already has been started.'); 33 | return false; 34 | } 35 | this.log.push({ 36 | start: moment().format(), 37 | }); 38 | this.setDescription(description); 39 | this.setStatus(TaskStatus.IN_PROGRESS); 40 | return true; 41 | } 42 | 43 | public stop(status: TaskStatus, timestamp?: moment.Moment): boolean { 44 | let lastTime = _.last(this.log); 45 | if (this.status === status) { 46 | let msg = status === TaskStatus.FINISHED ? 'completed' : 'paused'; 47 | console.log('This task already has been %s.', msg); 48 | return false; 49 | } 50 | if (lastTime && !lastTime.stop) { 51 | this.log[this.log.length - 1].stop = (timestamp || moment()).format(); 52 | } 53 | this.setStatus(status); 54 | return true; 55 | } 56 | 57 | public add(date: string, dateFormat: string, hour: number, min: number): boolean { 58 | let stop = moment(date, dateFormat); 59 | stop.add(hour, 'h'); 60 | stop.add(min, 'm'); 61 | this.log.unshift({ 62 | start: moment(date, dateFormat).format(), 63 | stop: stop.format() 64 | }); 65 | return true; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/core/timetracking.ts: -------------------------------------------------------------------------------- 1 | // import 3th party packages 2 | import * as colors from 'colors'; 3 | import * as Configstore from 'configstore'; 4 | import * as _ from 'lodash'; 5 | import * as moment from 'moment'; 6 | 7 | // import models 8 | import { Task } from './task'; 9 | import { TaskStatus } from './task-status'; 10 | 11 | export class Timetracking { 12 | public config: any; 13 | private configStore: Configstore; 14 | private tasks: Task[]; 15 | 16 | constructor(configStore) { 17 | this.configStore = configStore; 18 | this.tasks = configStore.all.tasks ? configStore.all.tasks : []; 19 | this.config = configStore.all.config ? configStore.all.config : {}; 20 | } 21 | 22 | public start(taskName: string, description: string, pauseOthers: boolean): void { 23 | if (pauseOthers) { 24 | if (this.tasks && this.tasks.length > 0) { 25 | this.tasks.forEach((t, i = 0) => { 26 | if (t.name !== taskName && t.status === TaskStatus.IN_PROGRESS) { 27 | let tsk = this.getTask(t.name); 28 | if (tsk.stop(TaskStatus.PAUSED)) { 29 | this.tasks[i] = tsk; 30 | } 31 | } 32 | i++; 33 | }); 34 | } 35 | } 36 | let task = this.getTask(taskName); 37 | if (task.start(description)) { 38 | let idx = _.findIndex(this.tasks, ['name', task.name]); 39 | if (idx === -1) { 40 | this.tasks.push(task); 41 | } else { 42 | this.tasks[idx] = task; 43 | } 44 | if (this.updateTasks()) { 45 | console.log('Task %s started.', task.name); 46 | } 47 | } 48 | } 49 | 50 | public stop(taskName: string, status: TaskStatus, timestamp?: string): void { 51 | if (!this.tasks || this.tasks.length === 0) { 52 | console.log('There are no tasks added yet.'); 53 | return; 54 | } 55 | if (!taskName) { 56 | this.tasks.forEach((t, idx = 0) => { 57 | if (t.status === TaskStatus.IN_PROGRESS) { 58 | let task = this.getTask(t.name); 59 | if (task.stop(status)) { 60 | this.tasks[idx] = task; 61 | } 62 | } 63 | idx++; 64 | }); 65 | } else { 66 | let idx = _.findIndex(this.tasks, ['name', taskName]); 67 | if (idx === -1) { 68 | console.log('Task %s not found.', taskName); 69 | return; 70 | } 71 | let task = this.getTask(taskName); 72 | let dateFormat = this.getConfigDateFormat(); 73 | let fullDate = moment(); 74 | timestamp = timestamp || fullDate.format(dateFormat); 75 | let isFullDate = timestamp.includes('/'); 76 | if (isFullDate) { 77 | if (!moment(timestamp, dateFormat).isValid()) { 78 | console.log('Time it is not in a valid format.'); 79 | return; 80 | } 81 | } else { 82 | if (!this.timeIsValid(timestamp, ['([0-9]{1,3})\:+([0-5]{1}[0-9]{1})'])) { 83 | console.log('Time it is not in a valid format.'); 84 | return; 85 | } 86 | fullDate.hour(+timestamp.split(':')[0]); 87 | fullDate.minute(+timestamp.split(':')[1]); 88 | } 89 | let lastTime = _.last(task.log); 90 | if (moment(lastTime.start) > fullDate) { 91 | console.log('The time entered must be greater than the start time of the task.'); 92 | return; 93 | } 94 | if (task.stop(status, fullDate)) { 95 | this.tasks[idx] = task; 96 | } else { 97 | // If the task could not be stopped, quit the command 98 | return; 99 | } 100 | } 101 | if (this.updateTasks()) { 102 | let msg = status === TaskStatus.FINISHED ? 'completed' : 'paused'; 103 | if (taskName) { 104 | console.log('Task %s has been %s.', taskName, msg); 105 | } else { 106 | console.log('All tasks in progress have been %s.', msg); 107 | } 108 | } 109 | } 110 | 111 | public list(date: string): void { 112 | if (!this.tasks || this.tasks.length === 0) { 113 | console.log('There are no tasks added yet.'); 114 | return; 115 | } 116 | let dateFormat = this.config && this.config.date_format ? this.config.date_format.toUpperCase() : 'MM/DD/YYYY'; 117 | if (date === undefined) { 118 | date = moment().format(dateFormat); 119 | } else { 120 | if (!moment(date, dateFormat).isValid()) { 121 | console.log('Date it is not in a valid format.'); 122 | return; 123 | } 124 | } 125 | let timings: any[] = []; 126 | let beginTotal = moment(); 127 | let diffTotal = moment(); 128 | let hours; 129 | let min; 130 | this.tasks.forEach((t) => { 131 | let times = _.filter(t.log, (l) => { 132 | return moment(l.start).format(dateFormat) === moment(date, dateFormat).format(dateFormat); 133 | }); 134 | if (times && times.length > 0) { 135 | let beginTask = moment(); 136 | let diffTask = moment(); 137 | times.forEach((time) => { 138 | diffTask.add(moment(time.stop).diff(moment(time.start), 'ms')); 139 | diffTotal.add(moment(time.stop).diff(moment(time.start), 'ms')); 140 | }); 141 | hours = diffTask.diff(beginTask, 'hours'); 142 | min = moment.utc(moment(diffTask, 'HH:mm:ss').diff(moment(beginTask, 'HH:mm:ss'))).format('mm'); 143 | timings.push({ name: t.name, time: ((hours < 10 ? '0' : '') + hours) + ':' + min, status: this.formatStatus(t.status) }); 144 | } 145 | }); 146 | hours = diffTotal.diff(beginTotal, 'hours'); 147 | min = moment.utc(moment(diffTotal, 'HH:mm:ss').diff(moment(beginTotal, 'HH:mm:ss'))).format('mm'); 148 | console.log(''); 149 | console.log(' %s %s ', colors.bgGreen(' ' + ((hours < 10 ? '0' : '') + hours) + ':' + min + ' '), colors.inverse(' DATE: ' + date + ' ')); 150 | console.log(colors.white(' %s | %s | %s'), 'TIME', 'STATUS', 'TASK'); 151 | if (!timings || timings.length === 0) { 152 | console.log(colors.grey(' --- | --- | ---')); 153 | } else { 154 | timings.forEach((time) => { 155 | console.log(colors.grey(' %s | %s | %s'), time.time, time.status + (' '.repeat(11 - time.status.length)), time.name); 156 | }); 157 | } 158 | } 159 | 160 | public add(taskName: string, timeSpent: string, date: string) { 161 | let dateFormat = this.config && this.config.date_format ? this.config.date_format.toUpperCase() + ' h:mm' : 'MM/DD/YYYY h:mm'; 162 | if (date === undefined) { 163 | date = moment().format(dateFormat); 164 | } else { 165 | if (!moment(date, dateFormat).isValid()) { 166 | console.log('Date it is not in a valid format.'); 167 | return; 168 | } 169 | } 170 | if (!this.timeIsValid(timeSpent)) { 171 | console.log('Time spent it is not in a valid format.'); 172 | return; 173 | } 174 | let isFullHour = timeSpent.indexOf(':') > 0; 175 | let hour: number; 176 | let min: number; 177 | if (isFullHour) { 178 | let split = timeSpent.split(':'); 179 | hour = +split[0]; 180 | min = +split[1]; 181 | } else { 182 | let valueOfSubstr: number = +timeSpent.substring(0, timeSpent.length - 1); 183 | hour = timeSpent.indexOf('h') > -1 ? valueOfSubstr : 0; 184 | min = timeSpent.indexOf('m') > -1 ? valueOfSubstr : 0; 185 | } 186 | let idx = _.findIndex(this.tasks, ['name', taskName]); 187 | let task = this.getTaskByIndex(taskName, idx); 188 | if (!task.add(date, dateFormat, hour, min)) { 189 | return; 190 | } 191 | if (idx === -1) { 192 | task.setStatus(TaskStatus.FINISHED); 193 | this.tasks.push(task); 194 | } else { 195 | this.tasks[idx] = task; 196 | } 197 | if (this.updateTasks()) { 198 | if (idx === -1) { 199 | console.log('Task %s added.', task.name); 200 | } else { 201 | console.log('The entered time was added in the task %s.', task.name); 202 | } 203 | } 204 | } 205 | 206 | private getTask(key: string): Task { 207 | return new Task(key, _.find(this.tasks, ['name', key])); 208 | } 209 | 210 | private getTaskByIndex(key: string, idx: number) { 211 | if (idx === -1) { 212 | return new Task(key, undefined); 213 | } 214 | return new Task(key, this.tasks[idx]); 215 | } 216 | 217 | private updateTasks(): boolean { 218 | try { 219 | this.configStore.set('tasks', this.tasks); 220 | return true; 221 | } catch (error) { 222 | console.log('An error occurred.'); 223 | return false; 224 | } 225 | } 226 | 227 | private timeIsValid(time: string, regList?: string[]): boolean { 228 | if (!time || time.length > 5) { 229 | return false; 230 | } 231 | regList = regList || ['([0-9]{1,3})\:+([0-5]{1}[0-9]{1})', '([0-9]{1,3}\m)', '([0-9]{1,3}\h)']; 232 | let i = 0; 233 | let value: any; 234 | for (i; i < regList.length; i++) { 235 | if (new RegExp(regList[i]).test(time)) { 236 | value = new RegExp(regList[i]).exec(time); 237 | break; 238 | } 239 | } 240 | if (i >= regList.length || (value == null || value[0] !== time)) { 241 | return false; 242 | } 243 | return true; 244 | } 245 | 246 | private formatStatus(status: TaskStatus): string { 247 | switch (status) { 248 | case TaskStatus.IN_PROGRESS: 249 | return 'In Progress'; 250 | case TaskStatus.PAUSED: 251 | return 'Paused'; 252 | case TaskStatus.FINISHED: 253 | return 'Finished'; 254 | default: 255 | return ''; 256 | } 257 | } 258 | 259 | private getConfigDateFormat(): string { 260 | return this.config && this.config.date_format ? this.config.date_format.toUpperCase() + ' h:mm' : 'MM/DD/YYYY h:mm'; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | // import 3th party packages 2 | import * as program from 'commander'; 3 | const Configstore = require('configstore'); 4 | const pkg = require('../package.json'); 5 | 6 | // import models 7 | import { TaskStatus } from './core/task-status'; 8 | import { Timetracking } from './core/timetracking'; 9 | 10 | const config = new Configstore(pkg.name, { 11 | config: { 12 | date_format: 'MM/dd/yyyy', 13 | pause_others_on_start: true 14 | }, 15 | tasks: [] 16 | }); 17 | const timetracking = new Timetracking(config); 18 | 19 | program 20 | .command('start ') 21 | .description('Start a task.') 22 | .alias('s') 23 | .option('-d, --description ', 'Set task description.') 24 | .option('-y', 'Pause others taks in progress.') 25 | .option('-n', 'Do not pause others taks in progress.') 26 | .action((task, options) => { 27 | let description = ''; 28 | if (options.description && typeof options.description !== 'function') { 29 | description = options.description; 30 | } 31 | timetracking.start(task, description, !options.N && (options.Y || timetracking.config.pause_others_on_start)); 32 | }); 33 | 34 | program 35 | .command('finish [task]') 36 | .description('Finish a task. If you do not inform the [task] all tasks in progress will be completed.') 37 | .alias('f') 38 | .option('-t ', 'Set date|hour of finish.') 39 | .action((task, options) => { 40 | let date = ''; 41 | if (options.T && typeof options.T !== 'function') { 42 | if (!task) { 43 | console.log('To use this option you must enter a task.'); 44 | return; 45 | } 46 | date = options.T; 47 | } 48 | timetracking.stop(task, TaskStatus.FINISHED, date); 49 | }); 50 | 51 | program 52 | .command('pause [task]') 53 | .description('Pause a task. If you do not inform the [task] all tasks in progress will be paused.') 54 | .alias('p') 55 | .option('-t ', 'Set date|hour of pause.') 56 | .action((task, options) => { 57 | let date = ''; 58 | if (options.T && typeof options.T !== 'function') { 59 | if (!task) { 60 | console.log('To use this option you must enter a task.'); 61 | return; 62 | } 63 | date = options.T; 64 | } 65 | timetracking.stop(task, TaskStatus.PAUSED, date); 66 | }); 67 | 68 | program 69 | .command('list [date]') 70 | .description('Resume time of the taks. You can pass the date on format configured (' + (config && config.all.config ? config.all.config.date_format : 'MM/dd/yyyy') + ').') 71 | .alias('l') 72 | .action((date) => { 73 | timetracking.list(date); 74 | }); 75 | 76 | program 77 | .command('add [date]') 78 | .description('Add a task with a specific time spent and on a specific date. You can pass the date on format configured (' + (config && config.all.config ? config.all.config.date_format : 'MM/dd/yyyy') + ').') 79 | .alias('a') 80 | .action((task, timeSpent, date) => { 81 | timetracking.add(task, timeSpent, date); 82 | }); 83 | 84 | program 85 | .version(pkg.version) 86 | .parse(process.argv); 87 | 88 | if (!program.args.length) { 89 | program.help(); 90 | } 91 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const time = require('../lib/core/timetracking'); 3 | const taskStatus = require('../lib/core/task-status'); 4 | 5 | const Configstore = require('configstore'); 6 | const pkg = require('../package.json'); 7 | 8 | const TEST_NAMESPACE = `${pkg.name}_test`; 9 | 10 | const config = new Configstore(TEST_NAMESPACE, { 11 | config: { 12 | date_format: 'MM/dd/yyyy', 13 | pause_others_on_start: true 14 | }, 15 | tasks: [] 16 | }); 17 | 18 | const timetracking = new time.Timetracking(config); 19 | 20 | describe(`${pkg.name} - ${pkg.description}`, () => { 21 | 22 | before(() => { 23 | config.set('tasks', []); 24 | }); 25 | 26 | it(':: start a new task', () => { 27 | timetracking.start('new task', 'this is a new task', true); 28 | let tasks = config.get('tasks'); 29 | assert.equal(tasks.length, 1); 30 | assert.equal(tasks[0].status, 0) 31 | }); 32 | 33 | it(':: list all tasks by creation date', () => { 34 | timetracking.list(new Date()); 35 | }); 36 | 37 | it(':: pause task', () => { 38 | let date = new Date(); 39 | let formattedDate = `${date.getHours()}:${date.getMinutes().toString().length == 1 ? '0' + date.getMinutes(): date.getMinutes()}`; 40 | timetracking.stop('new task', taskStatus.TaskStatus.PAUSED, formattedDate); 41 | let tasks = config.get('tasks'); 42 | assert.equal(tasks.length, 1); 43 | assert.equal(tasks[0].status, 1) 44 | }); 45 | 46 | it(':: stop task', () => { 47 | let date = new Date(); 48 | let formattedDate = `${date.getHours()}:${date.getMinutes().toString().length == 1 ? '0' + date.getMinutes(): date.getMinutes()}`; 49 | timetracking.stop('new task', taskStatus.TaskStatus.FINISHED, formattedDate); 50 | let tasks = config.get('tasks'); 51 | assert.equal(tasks.length, 1); 52 | assert.equal(tasks[0].status, 2) 53 | }); 54 | 55 | after(() => { 56 | config.set('tasks', []); 57 | }); 58 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": [ 5 | "es6" 6 | ], 7 | "module": "commonjs", 8 | "noImplicitAny": false, 9 | "removeComments": true, 10 | "declaration": true, 11 | "outDir": "./lib", 12 | "strict": true, 13 | "types": [ 14 | "node" 15 | ] 16 | }, 17 | "include": [ 18 | "src/**/*" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "lib", 23 | "**/*.spec.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "ordered-imports": [ 9 | true 10 | ], 11 | "indent": [ 12 | true, 13 | "tabs", 14 | 2 15 | ], 16 | "member-access": [ 17 | true, 18 | "check-accessor" 19 | ], 20 | "curly": true, 21 | "no-console": [ 22 | false 23 | ], 24 | "quotemark": [ 25 | true, 26 | "single" 27 | ], 28 | "trailing-comma": [ 29 | false 30 | ], 31 | "no-consecutive-blank-lines": true, 32 | "eofline": true, 33 | "prefer-const": false, 34 | "no-var-keyword": true, 35 | "ban-types": false, 36 | "no-var-requires": false, 37 | "max-line-length": [ 38 | false 39 | ] 40 | }, 41 | "rulesDirectory": [] 42 | } --------------------------------------------------------------------------------