From 4b731a1d7434c7f3f723f41f8c0c581ea3b2c959 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Wed, 31 Jul 2019 13:52:07 +0200 Subject: [PATCH 01/13] fixed typos in readme --- .vscode/settings.json | 15 +++++++++++++++ README.md | 6 +++--- 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3955eb5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "cSpell.words": [ + "bitbar", + "inno", + "kargos", + "kimai", + "pkg's" + ], + "cSpell.ignoreWords": [ + "argosbutton", + "chmod", + "executables", + "pkg" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 16424a0..44b4c3e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Command line client for [Kimai2](https://www.kimai.org/), the open source, self- To use this program you have to install Kimai2 first! -This client is still under developement. See planned features in the next section +This client is still under development. See planned features in the next section ## Current and planned features @@ -122,7 +122,7 @@ On the windows installer version settings.ini location: `C:\Users\Username\AppDa Integration settings are not asked during first run, you have to change them manually in settings.ini. If you don't use an integration, you can safely ignore it's settings. -## Developement version +## Development version ### Installation @@ -193,7 +193,7 @@ npm start start foo bar node kimai2-cmd.js start foo bar ``` -On the first run it will ask for your settings, but you can just copy settings.ini.example to settings.ini and modify it with your favourite text editor +On the first run it will ask for your settings, but you can just copy settings.ini.example to settings.ini and modify it with your favorite text editor ## Troubleshooting From 685a8762a60afef084db2c5b80ec3817ee4dfb02 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Wed, 31 Jul 2019 14:13:19 +0200 Subject: [PATCH 02/13] started working on rainmeter --- .vscode/settings.json | 1 + kimai2-cmd.js | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 3955eb5..75ce8d3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "inno", "kargos", "kimai", + "pjson", "pkg's" ], "cSpell.ignoreWords": [ diff --git a/kimai2-cmd.js b/kimai2-cmd.js index 6daa87a..40291a2 100644 --- a/kimai2-cmd.js +++ b/kimai2-cmd.js @@ -677,7 +677,7 @@ program .description(pjson.description + '. For interactive mode start without any commands. To generate settings file start in interactive mode!') .option('-v, --verbose', 'verbose, longer logging', false) .option('-i, --id', 'show id of elements when listing', false) - // .option('-r, --rainmeter', 'generate rainmeter files') + .option('-r, --rainmeter', 'generate rainmeter files') .option('-b, --argosbutton', 'argos/bitbar button output') .option('-a, --argos', 'argos/bitbar output') diff --git a/package.json b/package.json index 0ca988e..3f06cd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kimai2-cmd", - "version": "0.2.3", + "version": "0.3.0", "description": "Command line client for Kimai2", "main": "kimai2-cmd.js", "bin": "kimai2-cmd.js", From 6556d98bf3b99eaec55f0e61b3ecf3e26876b050 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Sun, 4 Aug 2019 00:38:22 +0200 Subject: [PATCH 03/13] Fixed a typo --- kimai2-cmd.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kimai2-cmd.js b/kimai2-cmd.js index 40291a2..91ca0e9 100644 --- a/kimai2-cmd.js +++ b/kimai2-cmd.js @@ -631,7 +631,7 @@ function uiAskForSettings() { settings.argos_bitbar.buttonlength = 10 const thePath = iniFullPath() - if (program.verbose) { console.log('Trying to save settings to: '.thePath) } + if (program.verbose) { console.log('Trying to save settings to: ' + thePath) } fs.writeFileSync(thePath, ini.stringify(settings)) console.log('Settings saved to ' + iniPath()) From fee581fb3a97fdc4c0f59ba36b7c68d384f7b549 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Sun, 4 Aug 2019 21:40:56 +0200 Subject: [PATCH 04/13] Added "|" to argosbutton empty output --- kimai2-cmd.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kimai2-cmd.js b/kimai2-cmd.js index 91ca0e9..3cd5fda 100644 --- a/kimai2-cmd.js +++ b/kimai2-cmd.js @@ -379,7 +379,7 @@ function printList(settings, arr, endpoint) { console.log('No active measurements') } if (program.argosbutton) { - console.log("Kimai2") + console.log("Kimai2 |") } } for (let i = 0; i < arr.length; i++) { From 8371c52bfa888db06cebdf9f903ecf8a45f3a882 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Sun, 4 Aug 2019 22:22:40 +0200 Subject: [PATCH 05/13] version number to 0.2.4, removed rainmeter --- kimai2-cmd.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kimai2-cmd.js b/kimai2-cmd.js index 3cd5fda..a51770c 100644 --- a/kimai2-cmd.js +++ b/kimai2-cmd.js @@ -677,7 +677,7 @@ program .description(pjson.description + '. For interactive mode start without any commands. To generate settings file start in interactive mode!') .option('-v, --verbose', 'verbose, longer logging', false) .option('-i, --id', 'show id of elements when listing', false) - .option('-r, --rainmeter', 'generate rainmeter files') + // .option('-r, --rainmeter', 'generate rainmeter files') .option('-b, --argosbutton', 'argos/bitbar button output') .option('-a, --argos', 'argos/bitbar output') diff --git a/package.json b/package.json index 3f06cd4..a9be240 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kimai2-cmd", - "version": "0.3.0", + "version": "0.2.4", "description": "Command line client for Kimai2", "main": "kimai2-cmd.js", "bin": "kimai2-cmd.js", From b2d6d20ab52ddb97d8554d8a15d93018a030c279 Mon Sep 17 00:00:00 2001 From: Devin Bayer Date: Tue, 6 Aug 2019 12:40:50 +0200 Subject: [PATCH 06/13] Add shebang line to kimai2-cmd.js This means it can be installed more easily. A `npm install -g` suffices. --- kimai2-cmd.js | 1 + 1 file changed, 1 insertion(+) diff --git a/kimai2-cmd.js b/kimai2-cmd.js index 6daa87a..7e255cb 100644 --- a/kimai2-cmd.js +++ b/kimai2-cmd.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node /* -------------------------------------------------------------------------- */ /* Modules */ /* -------------------------------------------------------------------------- */ From 933f734d79b79a2b7ef82bb7fc3a16f39ff5e660 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Tue, 6 Aug 2019 13:26:04 +0200 Subject: [PATCH 07/13] Added npm install to docs, bin in package.json --- README.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 44b4c3e..ed7d52c 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,14 @@ To remove: sudo rm /usr/bin/kimai ``` +### Install with npm + +If npm installed you can install it with the following command: + +``` +npm install -g infeeeee/kimai2-cmd +``` + ## Usage Two usage modes: interactive and classic ui diff --git a/package.json b/package.json index a9be240..1cd562e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.2.4", "description": "Command line client for Kimai2", "main": "kimai2-cmd.js", - "bin": "kimai2-cmd.js", + "bin": "kimai2-cmd", "scripts": { "start": "node kimai2-cmd.js", "build-nix": "pkg --out-path builds package.json", From 933c0ec0e5bf137f07c50d0aa9f31fc30b9bdade Mon Sep 17 00:00:00 2001 From: infeeeee Date: Tue, 6 Aug 2019 13:29:59 +0200 Subject: [PATCH 08/13] fixed bin in package.json --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1cd562e..cba4eda 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "version": "0.2.4", "description": "Command line client for Kimai2", "main": "kimai2-cmd.js", - "bin": "kimai2-cmd", + "bin": { + "kimai2-cmd": "kimai2-cmd.js" + }, "scripts": { "start": "node kimai2-cmd.js", "build-nix": "pkg --out-path builds package.json", @@ -41,4 +43,4 @@ "node10-macos" ] } -} +} \ No newline at end of file From 5bd0d81ca6cf3ab847cd71309b3c5ace41f24680 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Tue, 6 Aug 2019 14:21:09 +0200 Subject: [PATCH 09/13] fixed settings.ini paths --- kimai2-cmd.js | 14 +++++++++++--- package.json | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/kimai2-cmd.js b/kimai2-cmd.js index 8619a75..f8ae95f 100644 --- a/kimai2-cmd.js +++ b/kimai2-cmd.js @@ -548,8 +548,8 @@ function uiAutocompleteSelect(thelist, message) { function iniPath() { //different settings.ini path for developement and pkg and windows installer version const iniRoot = [ - path.dirname(process.execPath), - __dirname + path.dirname(process.execPath),//This is for pkg version + __dirname//This is for npm version ] if (appdata) { iniRoot.push(path.join(appdata, '/kimai2-cmd')) } @@ -647,14 +647,22 @@ function uiAskForSettings() { */ function iniFullPath() { let installDir = path.dirname(process.execPath).split("\\") + let dirArr = __dirname.split(path.sep) - //I should replace this tererible if to some registry value readin, maybe for uninstaller + //Maybe I should replace this terrible 'if' with some registry value reading if (platform == 'win32' && installDir[installDir.length - 2] == "Program Files" && installDir[installDir.length - 1] == "kimai2-cmd") { + if (program.verbose) { console.log('This is an installer based windows installation') } if (!fs.existsSync(path.join(appdata, 'kimai2-cmd'))) { fs.mkdirSync(path.join(appdata, 'kimai2-cmd')) } return path.join(appdata, 'kimai2-cmd', 'settings.ini') + } else if (dirArr[0] == 'snapshot' || dirArr[1] == 'snapshot') { + if (program.verbose) {console.log('This is a pkg version')} + //for pkg version: + return path.join(path.dirname(process.execPath), 'settings.ini') } else { + if (program.verbose) {console.log('This is an npm version')} + //For npm version: return './settings.ini' } } diff --git a/package.json b/package.json index cba4eda..fedf650 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kimai2-cmd", - "version": "0.2.4", + "version": "0.2.5", "description": "Command line client for Kimai2", "main": "kimai2-cmd.js", "bin": { From b0da65603f4665adb6da4772865794bdbbe7bca3 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Tue, 6 Aug 2019 14:28:21 +0200 Subject: [PATCH 10/13] fixed npm path again --- kimai2-cmd.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kimai2-cmd.js b/kimai2-cmd.js index f8ae95f..4c33a81 100644 --- a/kimai2-cmd.js +++ b/kimai2-cmd.js @@ -663,7 +663,7 @@ function iniFullPath() { } else { if (program.verbose) {console.log('This is an npm version')} //For npm version: - return './settings.ini' + return 'settings.ini' } } From 5023138d95b4d6954d36f6f3c04863b805246d31 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Sun, 11 Aug 2019 23:25:00 +0200 Subject: [PATCH 11/13] chenged default password in settings.ini.example --- settings.ini.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.ini.example b/settings.ini.example index 7166e03..1b3623e 100644 --- a/settings.ini.example +++ b/settings.ini.example @@ -1,7 +1,7 @@ [serversettings] kimaiurl=https://demo.kimai.org username=anna_admin -password=apipassword +password=api_kitten [argos_bitbar] kimaipath=/path/to/kimai2-cmd-macos From 5df2a7a837c2bf92ba8d97d8f803ecaa3619a9a0 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Sun, 11 Aug 2019 23:27:41 +0200 Subject: [PATCH 12/13] changed line endings? --- .gitignore | 6 +- README.md | 422 +++++------ kimai2-cmd.js | 1578 +++++++++++++++++++++--------------------- settings.ini.example | 14 +- 4 files changed, 1010 insertions(+), 1010 deletions(-) diff --git a/.gitignore b/.gitignore index 7104ebf..e72b2b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -settings.ini -node_modules/ -builds/ +settings.ini +node_modules/ +builds/ *.log \ No newline at end of file diff --git a/README.md b/README.md index ed7d52c..bbaa836 100644 --- a/README.md +++ b/README.md @@ -1,212 +1,212 @@ -# Kimai2-cmd - -Command line client for [Kimai2](https://www.kimai.org/), the open source, self-hosted time tracker. - -![interactive restart gif](assets/interactive-restart.gif) - -To use this program you have to install Kimai2 first! - -This client is still under development. See planned features in the next section - -## Current and planned features - -This client is not intended to replace the Kimai webUI, so only basic functions, starting and stopping measurements - -Commands: -- [x] Start, restart and stop measurements -- [x] List active and recent measurements -- [x] List projects and activities - -UI: -- [x] Interactive terminal UI with autocomplete -- [x] Classic terminal UI for integration - -Integration: -- [x] Portable executable for all three platforms -- [x] Installer for windows -- [ ] Generate output for Rainmeter (Windows) (Just like [kimai-cmd](https://github.com/infeeeee/kimai-cmd)) -- [x] Generate output for Argos/Kargos/Bitbar (Gnome, Kde, Mac). More info here: [kimai2-cmd-argos](https://github.com/infeeeee/kimai2-cmd-argos) - -Requests for integrations with other softwares are welcomed! Just open an issue and show an example output, what you need. - -## Installation - -Download from [releases](https://github.com/infeeeee/kimai2-cmd/releases/latest). - -You have to create an API password for your username on your Kimai installation. In Kimai: User menu (Top right corner) -> User profile -> API. - -### Notes on Windows - -Portable executable or installer available. - -Installer automatically adds the install path to the %PATH% environment variable, so you can use it from command line/powershell system wide. Sign out and in if it's not working. - -With the portable version you have to do this manually. Follow [this tutorial](https://stackoverflow.com/questions/44272416/how-to-add-a-folder-to-path-environment-variable-in-windows-10-with-screensho) or a similar one if you don't know how to do it. - -### Notes on Linux/Mac - -Portable executable only. On the following terminal examples use the file name you downloaded. - -Make the downloaded binary executable: -``` -sudo chmod +x kimai2-cmd-os -``` - -Add kimai2-cmd to path so you have to just type `kimai` to the terminal: -``` -sudo ln -s /path/to/kimai2-cmd-os /usr/bin/kimai -``` - -To remove: -``` -sudo rm /usr/bin/kimai -``` - -### Install with npm - -If npm installed you can install it with the following command: - -``` -npm install -g infeeeee/kimai2-cmd -``` - -## Usage - -Two usage modes: interactive and classic ui - -### Interactive ui - -![interactive start gif](assets/interactive-start.gif) - -If you start without any commands, you will get to the interactive UI. Use your keyboard's arrow keys for navigation. On the `Start new measurement` menu item you can search for project and activity names. - -You can exit with ctrl+c any time. - -### Classic ui - -You can find all the options in the help: - -``` -$ kimai2-cmd --help - -Usage: kimai2-cmd [options] [command] - -Command line client for Kimai2. For interactive mode start without any commands. To generate settings file start in interactive mode! - -Options: - -V, --version output the version number - -v, --verbose verbose, longer logging - -i, --id show id of elements when listing - -b, --argosbutton argos/bitbar button output - -a, --argos argos/bitbar output - -h, --help output usage information - -Commands: - start [project] [activity] start selected project and activity - restart [id] restart selected measurement - stop [id] stop all or selected measurement measurements, [id] is optional - list-active list active measurements - list-recent list recent measurements - list-projects list all projects - list-activities list all activities - url prints the url of the server -``` - -Project and activity names are case insensitive. If your project or activity name contains a space, wrap it in double or single quotes. This example starts project named `foo` with activity named `bar bar`: - -``` -kimai2-cmd start "foo" "bar bar" -``` - -### Settings and first run - -All settings stored in the settings.ini file. Place this file to the same directory as the executable. If no settings file found you will drop to the interactive UI, where you can add your settings: - -![interactive settings gif](assets/interactive-settings.gif) - -You can create your settings.ini file manually, by downloading, renaming and editing [settings.ini.example](https://github.com/infeeeee/kimai2-cmd/blob/master/settings.ini.example). - -On the windows installer version settings.ini location: `C:\Users\Username\AppData\Roaming\kimai2-cmd\settings.ini` - -Integration settings are not asked during first run, you have to change them manually in settings.ini. If you don't use an integration, you can safely ignore it's settings. - -## Development version - -### Installation - -Prerequisites: -- node js 10+ -- git - -``` -git clone https://github.com/infeeeee/kimai2-cmd -cd kimai2-cmd -npm install -``` - -### Build - -Prerequisite: globally installed [pkg](https://github.com/zeit/pkg): - -``` -npm install pkg -g -``` - -Build for current platform and architecture - -``` -npm run build-current -``` - -Build x64 executables for linux, mac on linux or on mac - -``` -npm run build-nix -``` - -About building for other platforms see pkg's documentation, or open an issue and I can build it for you. - -### Installer (Windows only) - -Prerequisite: [Inno Setup](http://www.jrsoftware.org/isinfo.php) - -- Create a windows build: `npm run build-current` -- Open `kimai2-innosetup.iss` in Inno Setup - -### Usage - -For interactive mode just simply: - -``` -npm start -``` -or -``` -node kimai2-cmd.js -``` - -For usage with options you have pass a `--` before the options if you start with `npm start`. You don't need this if you don't use options just commands - -So this two lines are equivalent, both shows the current version of kimai2-cmd: - -``` -npm start -- -V -node kimai2-cmd.js -V -``` - -This two are equivalent as well, both starts the project `foo` with the activity `bar` - -``` -npm start start foo bar -node kimai2-cmd.js start foo bar -``` - -On the first run it will ask for your settings, but you can just copy settings.ini.example to settings.ini and modify it with your favorite text editor - -## Troubleshooting - -If you find a bug open an issue! The client is not finished yet, however all implemented features should work! - -## License - +# Kimai2-cmd + +Command line client for [Kimai2](https://www.kimai.org/), the open source, self-hosted time tracker. + +![interactive restart gif](assets/interactive-restart.gif) + +To use this program you have to install Kimai2 first! + +This client is still under development. See planned features in the next section + +## Current and planned features + +This client is not intended to replace the Kimai webUI, so only basic functions, starting and stopping measurements + +Commands: +- [x] Start, restart and stop measurements +- [x] List active and recent measurements +- [x] List projects and activities + +UI: +- [x] Interactive terminal UI with autocomplete +- [x] Classic terminal UI for integration + +Integration: +- [x] Portable executable for all three platforms +- [x] Installer for windows +- [ ] Generate output for Rainmeter (Windows) (Just like [kimai-cmd](https://github.com/infeeeee/kimai-cmd)) +- [x] Generate output for Argos/Kargos/Bitbar (Gnome, Kde, Mac). More info here: [kimai2-cmd-argos](https://github.com/infeeeee/kimai2-cmd-argos) + +Requests for integrations with other softwares are welcomed! Just open an issue and show an example output, what you need. + +## Installation + +Download from [releases](https://github.com/infeeeee/kimai2-cmd/releases/latest). + +You have to create an API password for your username on your Kimai installation. In Kimai: User menu (Top right corner) -> User profile -> API. + +### Notes on Windows + +Portable executable or installer available. + +Installer automatically adds the install path to the %PATH% environment variable, so you can use it from command line/powershell system wide. Sign out and in if it's not working. + +With the portable version you have to do this manually. Follow [this tutorial](https://stackoverflow.com/questions/44272416/how-to-add-a-folder-to-path-environment-variable-in-windows-10-with-screensho) or a similar one if you don't know how to do it. + +### Notes on Linux/Mac + +Portable executable only. On the following terminal examples use the file name you downloaded. + +Make the downloaded binary executable: +``` +sudo chmod +x kimai2-cmd-os +``` + +Add kimai2-cmd to path so you have to just type `kimai` to the terminal: +``` +sudo ln -s /path/to/kimai2-cmd-os /usr/bin/kimai +``` + +To remove: +``` +sudo rm /usr/bin/kimai +``` + +### Install with npm + +If npm installed you can install it with the following command: + +``` +npm install -g infeeeee/kimai2-cmd +``` + +## Usage + +Two usage modes: interactive and classic ui + +### Interactive ui + +![interactive start gif](assets/interactive-start.gif) + +If you start without any commands, you will get to the interactive UI. Use your keyboard's arrow keys for navigation. On the `Start new measurement` menu item you can search for project and activity names. + +You can exit with ctrl+c any time. + +### Classic ui + +You can find all the options in the help: + +``` +$ kimai2-cmd --help + +Usage: kimai2-cmd [options] [command] + +Command line client for Kimai2. For interactive mode start without any commands. To generate settings file start in interactive mode! + +Options: + -V, --version output the version number + -v, --verbose verbose, longer logging + -i, --id show id of elements when listing + -b, --argosbutton argos/bitbar button output + -a, --argos argos/bitbar output + -h, --help output usage information + +Commands: + start [project] [activity] start selected project and activity + restart [id] restart selected measurement + stop [id] stop all or selected measurement measurements, [id] is optional + list-active list active measurements + list-recent list recent measurements + list-projects list all projects + list-activities list all activities + url prints the url of the server +``` + +Project and activity names are case insensitive. If your project or activity name contains a space, wrap it in double or single quotes. This example starts project named `foo` with activity named `bar bar`: + +``` +kimai2-cmd start "foo" "bar bar" +``` + +### Settings and first run + +All settings stored in the settings.ini file. Place this file to the same directory as the executable. If no settings file found you will drop to the interactive UI, where you can add your settings: + +![interactive settings gif](assets/interactive-settings.gif) + +You can create your settings.ini file manually, by downloading, renaming and editing [settings.ini.example](https://github.com/infeeeee/kimai2-cmd/blob/master/settings.ini.example). + +On the windows installer version settings.ini location: `C:\Users\Username\AppData\Roaming\kimai2-cmd\settings.ini` + +Integration settings are not asked during first run, you have to change them manually in settings.ini. If you don't use an integration, you can safely ignore it's settings. + +## Development version + +### Installation + +Prerequisites: +- node js 10+ +- git + +``` +git clone https://github.com/infeeeee/kimai2-cmd +cd kimai2-cmd +npm install +``` + +### Build + +Prerequisite: globally installed [pkg](https://github.com/zeit/pkg): + +``` +npm install pkg -g +``` + +Build for current platform and architecture + +``` +npm run build-current +``` + +Build x64 executables for linux, mac on linux or on mac + +``` +npm run build-nix +``` + +About building for other platforms see pkg's documentation, or open an issue and I can build it for you. + +### Installer (Windows only) + +Prerequisite: [Inno Setup](http://www.jrsoftware.org/isinfo.php) + +- Create a windows build: `npm run build-current` +- Open `kimai2-innosetup.iss` in Inno Setup + +### Usage + +For interactive mode just simply: + +``` +npm start +``` +or +``` +node kimai2-cmd.js +``` + +For usage with options you have pass a `--` before the options if you start with `npm start`. You don't need this if you don't use options just commands + +So this two lines are equivalent, both shows the current version of kimai2-cmd: + +``` +npm start -- -V +node kimai2-cmd.js -V +``` + +This two are equivalent as well, both starts the project `foo` with the activity `bar` + +``` +npm start start foo bar +node kimai2-cmd.js start foo bar +``` + +On the first run it will ask for your settings, but you can just copy settings.ini.example to settings.ini and modify it with your favorite text editor + +## Troubleshooting + +If you find a bug open an issue! The client is not finished yet, however all implemented features should work! + +## License + MIT \ No newline at end of file diff --git a/kimai2-cmd.js b/kimai2-cmd.js index 4c33a81..72d2c17 100644 --- a/kimai2-cmd.js +++ b/kimai2-cmd.js @@ -1,789 +1,789 @@ -#!/usr/bin/env node -/* -------------------------------------------------------------------------- */ -/* Modules */ -/* -------------------------------------------------------------------------- */ - -//builtin -const path = require('path'); -const fs = require('fs'); - -const platform = process.platform -const appdata = process.env.appdata - -//request -const request = require('request'); - -//ui -const inquirer = require('inquirer'); -const fuzzy = require('fuzzy'); -const program = require('commander'); - -// ini -const ini = require('ini'); - -//moment -const moment = require('moment'); - -//reading version number from package.json -var pjson = require('./package.json'); - -/* -------------------------------------------------------------------------- */ -/* Functions */ -/* -------------------------------------------------------------------------- */ - -/** - * Calls the kimai API - * - * @param {string} httpMethod Http method: 'GET', 'POST', 'PATCH'... - * @param {string} kimaiMethod Endpoint to call on the kimai API: timesheet, activities, timesheets/123/stop - * @param {object} serversettings Serversettings section read from ini. Only serversettings, not the full settings! - * @param {object} options All of them are optional: - * options.qs querystring - * options.reqbody request body - * @returns {object} The response body as an object - * - */ -function callKimaiApi(httpMethod, kimaiMethod, serversettings, options = false) { - //default options to false: - const qs = options.qs || false - const reqbody = options.reqbody || false - - if (program.verbose) { - console.log("calling kimai:", httpMethod, kimaiMethod, serversettings) - } - - return new Promise((resolve, reject) => { - const options = { - url: sanitizeServerUrl(serversettings.kimaiurl) + '/api/' + kimaiMethod, - headers: { - 'X-AUTH-USER': serversettings.username, - 'X-AUTH-TOKEN': serversettings.password, - }, - method: httpMethod - } - - if (qs) { - options.qs = qs - } - if (reqbody) { - options.body = JSON.stringify(reqbody) - options.headers['Content-Type'] = 'application/json' - } - - if (program.verbose) { - console.log("request options:", options) - } - - request(options, (error, response, body) => { - if (error) { - reject(error) - } - - let jsonarr = JSON.parse(response.body) - - if (program.verbose) { - console.log("Response body:", jsonarr) - } - - if (jsonarr.message) { - console.log('Server error message:') - console.log(jsonarr.code) - console.log(jsonarr.message) - reject(jsonarr.message) - } - - resolve(jsonarr) - }) - }) -} - -/** - * Interactive ui: displays the main menu - * - * @param {object} settings The full settings object read from the ini - */ -function uiMainMenu(settings) { - console.log() - inquirer - .prompt([ - { - type: 'list', - name: 'mainmenu', - message: 'Select command', - pageSize: process.stdout.rows - 1, - choices: - [ - { name: 'Restart recent measurement', value: 'restart' }, - { name: 'Start new measurement', value: 'start' }, - { name: 'Stop all active measurements', value: 'stop-all' }, - { name: 'Stop an active measurement', value: 'stop' }, - new inquirer.Separator(), - { name: 'List active measurements', value: 'list-active' }, - { name: 'List recent measurements', value: 'list-recent' }, - { name: 'List projects', value: 'list-projects' }, - { name: 'List activities', value: 'list-activities' }, - new inquirer.Separator(), - { name: 'Exit', value: 'exit' } - ] - } - ]) - .then(answers => { - if (program.verbose) { - console.log('selected answer: ' + answers.mainmenu) - } - switch (answers.mainmenu) { - case 'restart': - kimaiList(settings, 'timesheets/recent', false) - .then(res => { - return uiSelectMeasurement(res[1]) - }).then(startId => { - return kimaiRestart(settings, startId) - }) - .then(res => uiMainMenu(res[0])) - break; - case 'start': - uiKimaiStart(settings) - .then(_ => uiMainMenu(settings)) - break; - case 'stop-all': - kimaiStop(settings, false) - .then(_ => uiMainMenu(settings)) - break; - case 'stop': - kimaiList(settings, 'timesheets/active', false) - .then(res => { - return uiSelectMeasurement(res[1]) - }).then(stopId => { - return kimaiStop(settings, stopId) - }) - .then(res => uiMainMenu(res[0])) - break; - - case 'list-active': - kimaiList(settings, 'timesheets/active', true) - .then(res => uiMainMenu(res[0])) - break; - case 'list-recent': - kimaiList(settings, 'timesheets/recent', true) - .then(res => uiMainMenu(res[0])) - break; - case 'list-projects': - kimaiList(settings, 'projects', true) - .then(res => uiMainMenu(res[0])) - break; - case 'list-activities': - kimaiList(settings, 'activities', true) - .then(res => uiMainMenu(res[0])) - break; - default: - break; - } - }) -} - -/** - * Restarts a measurement - * - * @param {object} settings All settings read from ini - * @param {string} id The id of the measurement to restart - * - */ -function kimaiRestart(settings, id) { - return new Promise((resolve, reject) => { - callKimaiApi('PATCH', 'timesheets/' + id + '/restart', settings.serversettings) - .then(res => { - resolve([settings, res]) - }) - }) -} - -/** - * Interactive ui: select a project and activity and starts it - * - * @param {object} settings All settings read from ini - */ -function uiKimaiStart(settings) { - return new Promise((resolve, reject) => { - const selected = {} - kimaiList(settings, 'projects', false) - .then(res => { - // console.log(res[1]) - return uiAutocompleteSelect(res[1], 'Select project') - }) - .then(res => { - // console.log(res) - selected.projectId = res.id - return kimaiList(settings, 'activities', false, { filter: { project: res.id } }) - }) - .then(res => { - return uiAutocompleteSelect(res[1], 'Select activity') - }) - .then(res => { - selected.activityId = res.id - return kimaiStart(settings, selected.projectId, selected.activityId) - }) - .then(_ => { - resolve() - }) - }) -} - -/** - * Start a timer on the server - * - * @param {object} settings - * @param {string} project Id of project - * @param {string} activity Id of activity - */ -function kimaiStart(settings, project, activity) { - return new Promise((resolve, reject) => { - - let body = { - begin: moment().format(), - project: project, - activity: activity - } - if (program.verbose) { - console.log("kimaistart calling api:", body) - } - - callKimaiApi('POST', 'timesheets', settings.serversettings, { reqbody: body }) - .then(res => { - console.log('Started: ' + res.id) - resolve() - }) - }) - -} - -/** - * Find id of project or activity by name - * - * @param {object} settings - * @param {string} name The name to search for - * @param {string} endpoint - */ -function findId(settings, name, endpoint) { - return new Promise((resolve, reject) => { - kimaiList(settings, endpoint, false) - .then(res => { - const list = res[1] - for (let i = 0; i < list.length; i++) { - const element = list[i]; - if (element.name.toLowerCase() == name.toLowerCase()) { - resolve(element.id) - } - } - reject() - }) - }) -} - -/** - * Stops one or all current measurements. If id is empty it stops all, if given only selected - * - * @param {object} settings - * @param {string} id - */ -function kimaiStop(settings, id = false) { - return new Promise((resolve, reject) => { - if (id) { - callKimaiApi('PATCH', 'timesheets/' + id + '/stop', settings.serversettings) - .then(res => { - resolve([settings, res]) - }) - } else { - kimaiList(settings, 'timesheets/active', false) - .then(res => { - const jsonList = res[1] - return callKimaiStop(settings, jsonList) - //callKimaiStop(settings, jsonList) - }) - .then(_ => { - resolve() - }) - } - }) -} - -/** - * Supplementary function for stopping multiple running measurements - * - * @param {*} settings All settings - * @param {*} jsonList As the output of kimaiList() - * @param {*} i Counter, do not use! - */ -function callKimaiStop(settings, jsonList, i = 0) { - return new Promise((resolve, reject) => { - const element = jsonList[i]; - callKimaiApi('PATCH', 'timesheets/' + element.id + '/stop', settings.serversettings) - .then(jsl => { - console.log('Stopped: ', jsl.id) - i++ - if (i < jsonList.length) { - callKimaiStop(settings, jsonList, i) - } else { - resolve() - } - }) - }) -} - -/** - * Calls the api, lists and returns elements - * - * @param {object} settings The full settings object read from the ini - * @param {string} endpoint The endpoint to call in the api. - * @param {boolean} print If true, it prints to the terminal - * @param {object} options Options: - * options.filter: filter the query, - * @returns {array} res[0]: settings, res[1]: list of elements - */ -function kimaiList(settings, endpoint, print = false, options = false) { - const filter = options.filter || false - return new Promise((resolve, reject) => { - callKimaiApi('GET', endpoint, settings.serversettings, { qs: filter }) - .then(jsonList => { - if (print) { - printList(settings, jsonList, endpoint) - } - resolve([settings, jsonList]) - }) - .catch(msg => { - console.log("Error: " + msg) - }) - }) -} - - -/** - * Prints list to terminal - * - * @param {object} settings The full settings object read from the ini - * @param {array} arr Items to list - * @param {string} endpoint for selecting display layout - */ -function printList(settings, arr, endpoint) { - if (program.verbose) { - console.log() - if (arr.length > 1) { - console.log(arr.length + ' results:') - } else if (arr.length == 0) { - console.log('No results') - } else { - console.log('One result:') - } - } - //no result for scripts: - if (arr.length == 0) { - if (program.argos) { - console.log('No active measurements') - } - if (program.argosbutton) { - console.log("Kimai2 |") - } - } - for (let i = 0; i < arr.length; i++) { - const element = arr[i]; - - if (endpoint == 'projects' || endpoint == 'activities') { - if (program.verbose) { - console.log((i + 1) + ':', element.name, '(id:' + element.id + ')') - } else if (program.id) { - console.log(element.id + ':', element.name) - } else { - console.log(element.name) - } - - } else { //measurements - if (program.verbose) { - if (arr.length > 1) { - console.log((i + 1) + ":") - } - console.log(' Id: ' + element.id) - console.log(' Project: ' + element.project.name, '(id:' + element.project.id + ')') - console.log(' Customer: ' + element.project.customer.name, '(id:' + element.project.customer.id + ')') - console.log(' Activity: ' + element.activity.name, '(id:' + element.activity.id + ')') - console.log(' Begin: ' + element.begin) - - if (moment(element.end).isValid()) { - //finished measurements: - console.log(' Duration: ' + formattedDuration(element.begin, element.end)) - } else { - //active measurements: - console.log(' Duration: ' + formattedDuration(element.begin)) - } - - } else if (program.id) { - console.log(element.id + ':', element.project.name, '|', element.activity.name) - } else if (program.argos) { - //Argos - if (endpoint == 'timesheets/recent') { - console.log('--' + element.project.name + ',', element.activity.name, '|', 'bash=' + settings.argos_bitbar.kimaipath + ' param1=restart param2=' + element.id + ' terminal=false refresh=true') - } else if (endpoint == 'timesheets/active') { - console.log(formattedDuration(element.begin), element.project.name + ',', element.activity.name, '|', 'bash=' + settings.argos_bitbar.kimaipath + ' param1=stop param2=' + element.id + ' terminal=false refresh=true') - } - } else if (program.argosbutton) { - //Argosbutton - console.log(formattedDuration(element.begin), element.project.name + ',', element.activity.name, '| length=' + settings.argos_bitbar.buttonlength) - } else { - //Regular output - if (moment(element.end).isValid()) { - //finished measurements: - console.log(element.project.name, '|', element.activity.name) - } else { - //active measurements: - console.log(formattedDuration(element.begin), element.project.name, '|', element.activity.name) - } - } - } - } -} - -/** - * Returns duration between the two moments or between beginning and now. padded to minimum two digits. - * - * @param {moment} begin beginning moment - * @param {moment} end optional, end moment - */ -function formattedDuration(begin, end) { - let momentDuration = moment.duration(moment(end).diff(moment(begin))) - - let hrs = momentDuration.hours() - let mins = momentDuration.minutes() - - if (hrs.toString().length == 1) { - hrs = "0" + hrs - } - - if (mins.toString().length == 1) { - mins = "0" + mins - } - - return hrs + ':' + mins -} - - -/** - * Interactive ui: select measurement from a list of measurements - * @param {} thelist - */ -function uiSelectMeasurement(thelist) { - return new Promise((resolve, reject) => { - const choices = [] - for (let i = 0; i < thelist.length; i++) { - const element = thelist[i]; - choices.push({ - name: element.project.name + " | " + element.activity.name, value: element.id - }) - } - inquirer - .prompt([ - { - type: 'list', - name: 'selectMeasurement', - message: 'Select measurement', - pageSize: process.stdout.rows - 1, - choices: choices - } - ]).then(answers => { - resolve(answers.selectMeasurement) - }) - }) -} - -/** - * Returns a prompt with autocomplete - * - * @param {array} thelist The list of elements to select from - * @param {string} message Prompt message - */ -function uiAutocompleteSelect(thelist, message) { - return new Promise((resolve, reject) => { - const choices = [] - const names = [] - for (let i = 0; i < thelist.length; i++) { - const element = thelist[i]; - choices.push({ - name: element.name, id: element.id - }) - names.push(element.name) - } - inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')); - inquirer - .prompt([ - { - type: 'autocomplete', - name: 'autoSelect', - message: message, - pageSize: process.stdout.rows - 2, - source: function (answers, input) { - input = input || ''; - return new Promise((resolve, reject) => { - var fuzzyResult = fuzzy.filter(input, names); - resolve( - fuzzyResult.map(function (el) { - return el.original; - }) - ) - }) - } - } - ]).then(answers => { - let ind = names.indexOf(answers.autoSelect) - let selectedChoice = choices[ind] - // console.log(selectedChoice) - resolve(selectedChoice) - }) - }) -} - - -/** - * Finds settings file path - * - * @returns string: Path to settings.ini - * @returns false: If no settings found - */ -function iniPath() { - //different settings.ini path for developement and pkg and windows installer version - const iniRoot = [ - path.dirname(process.execPath),//This is for pkg version - __dirname//This is for npm version - ] - - if (appdata) { iniRoot.push(path.join(appdata, '/kimai2-cmd')) } - - if (program.verbose) { - console.log('Looking for settings.ini in the following places:') - console.log(iniRoot) - } - - for (let i = 0; i < iniRoot.length; i++) { - const currentIniPath = path.join(iniRoot[i], '/settings.ini') - if (fs.existsSync(currentIniPath)) { - return currentIniPath - } - } - - // no ini found so: - return false -} - -/** - * Checks if settings file exists, if not it's asks for settings - * - * @return {object} settings: all settings read from the settings file - */ -function checkSettings() { - return new Promise((resolve, reject) => { - const settingsPath = iniPath() - if (settingsPath) { - if (program.verbose) console.log("settings.ini found at: ", settingsPath) - let settings = ini.parse(fs.readFileSync(settingsPath, 'utf-8')) - resolve(settings) - } else { - console.log('Settings.ini not found') - uiAskForSettings() - .then(settings => { - resolve(settings) - }) - - } - }) -} - -/** - * Interactive ui: asks for settings than saves them - * - */ -function uiAskForSettings() { - return new Promise((resolve, reject) => { - let questions = [ - { - type: 'input', - name: 'kimaiurl', - message: "Kimai2 url:" - }, - { - type: 'input', - name: 'username', - message: "Username:" - }, - { - type: 'input', - name: 'password', - message: "API password:" - } - ] - - inquirer - .prompt(questions) - .then(answers => { - let settings = {} - settings.serversettings = answers - settings.argos_bitbar = {} - - if (platform == "darwin") { - settings.argos_bitbar.kimaipath = process.execPath - } else { - settings.argos_bitbar.kimaipath = "kimai" - } - settings.argos_bitbar.buttonlength = 10 - - const thePath = iniFullPath() - if (program.verbose) { console.log('Trying to save settings to: ' + thePath) } - - fs.writeFileSync(thePath, ini.stringify(settings)) - console.log('Settings saved to ' + iniPath()) - resolve(settings) - }); - }) -} - - -/** - * Returns the ini save path based on os and installation type, creates folder if necessary - */ -function iniFullPath() { - let installDir = path.dirname(process.execPath).split("\\") - let dirArr = __dirname.split(path.sep) - - //Maybe I should replace this terrible 'if' with some registry value reading - if (platform == 'win32' && installDir[installDir.length - 2] == "Program Files" && installDir[installDir.length - 1] == "kimai2-cmd") { - if (program.verbose) { console.log('This is an installer based windows installation') } - if (!fs.existsSync(path.join(appdata, 'kimai2-cmd'))) { - fs.mkdirSync(path.join(appdata, 'kimai2-cmd')) - } - return path.join(appdata, 'kimai2-cmd', 'settings.ini') - } else if (dirArr[0] == 'snapshot' || dirArr[1] == 'snapshot') { - if (program.verbose) {console.log('This is a pkg version')} - //for pkg version: - return path.join(path.dirname(process.execPath), 'settings.ini') - } else { - if (program.verbose) {console.log('This is an npm version')} - //For npm version: - return 'settings.ini' - } -} - - -/** - * Removes trailing slashes from url - * - * @param {string} kimaiurl Url to sanitize - */ -function sanitizeServerUrl(kimaiurl) { - return kimaiurl.replace(/\/+$/, ""); -} - -/* -------------------------------------------------------------------------- */ -/* Commander */ -/* -------------------------------------------------------------------------- */ - -program - .version(pjson.version) - .description(pjson.description + '. For interactive mode start without any commands. To generate settings file start in interactive mode!') - .option('-v, --verbose', 'verbose, longer logging', false) - .option('-i, --id', 'show id of elements when listing', false) - // .option('-r, --rainmeter', 'generate rainmeter files') - .option('-b, --argosbutton', 'argos/bitbar button output') - .option('-a, --argos', 'argos/bitbar output') - -program.command('start [project] [activity]') - .description('start selected project and activity') - .action(function (project, activity) { - const selected = {} - checkSettings() - .then(settings => { - findId(settings, project, 'projects') - .then(projectid => { - selected.projectId = projectid - return findId(settings, activity, 'activities') - }) - .then(activityid => { - selected.activityId = activityid - return kimaiStart(settings, selected.projectId, selected.activityId) - }) - }) - }) - -program.command('restart [id]') - .description('restart selected measurement') - .action(function (measurementId) { - checkSettings() - .then(settings => { - kimaiRestart(settings, measurementId) - }) - }) - -program.command('stop [id]') - .description('stop all or selected measurement measurements, [id] is optional') - .action(function (measurementId) { - checkSettings() - .then(settings => { - kimaiStop(settings, measurementId) - }) - }) - -program.command('list-active') - .description('list active measurements') - .action(function () { - checkSettings() - .then(settings => { - kimaiList(settings, 'timesheets/active', true) - }) - }) - -program.command('list-recent') - .description('list recent measurements') - .action(function () { - checkSettings() - .then(settings => { - kimaiList(settings, 'timesheets/recent', true) - }) - }) - -program.command('list-projects') - .description('list all projects') - .action(function () { - checkSettings() - .then(settings => { - kimaiList(settings, 'projects', true) - }) - }) - -program.command('list-activities') - .description('list all activities') - .action(function () { - checkSettings() - .then(settings => { - kimaiList(settings, 'activities', true) - }) - }) - -program.command('url') - .description('prints the url of the server') - .action(function () { - checkSettings() - .then(settings => { - console.log(settings.serversettings.kimaiurl) - }) - }) - -// program.command('debug') -// .description('debug snapshot filesystem. If you see this you are using a developement build') -// .action(function () { -// fs.readdir(__dirname, (err, files) => { console.log(files) }) -// }) - -program.parse(process.argv); - - -//interactive mode if no option added -if (!program.args.length) { - checkSettings() - .then(settings => { - uiMainMenu(settings) - }) -} +#!/usr/bin/env node +/* -------------------------------------------------------------------------- */ +/* Modules */ +/* -------------------------------------------------------------------------- */ + +//builtin +const path = require('path'); +const fs = require('fs'); + +const platform = process.platform +const appdata = process.env.appdata + +//request +const request = require('request'); + +//ui +const inquirer = require('inquirer'); +const fuzzy = require('fuzzy'); +const program = require('commander'); + +// ini +const ini = require('ini'); + +//moment +const moment = require('moment'); + +//reading version number from package.json +var pjson = require('./package.json'); + +/* -------------------------------------------------------------------------- */ +/* Functions */ +/* -------------------------------------------------------------------------- */ + +/** + * Calls the kimai API + * + * @param {string} httpMethod Http method: 'GET', 'POST', 'PATCH'... + * @param {string} kimaiMethod Endpoint to call on the kimai API: timesheet, activities, timesheets/123/stop + * @param {object} serversettings Serversettings section read from ini. Only serversettings, not the full settings! + * @param {object} options All of them are optional: + * options.qs querystring + * options.reqbody request body + * @returns {object} The response body as an object + * + */ +function callKimaiApi(httpMethod, kimaiMethod, serversettings, options = false) { + //default options to false: + const qs = options.qs || false + const reqbody = options.reqbody || false + + if (program.verbose) { + console.log("calling kimai:", httpMethod, kimaiMethod, serversettings) + } + + return new Promise((resolve, reject) => { + const options = { + url: sanitizeServerUrl(serversettings.kimaiurl) + '/api/' + kimaiMethod, + headers: { + 'X-AUTH-USER': serversettings.username, + 'X-AUTH-TOKEN': serversettings.password, + }, + method: httpMethod + } + + if (qs) { + options.qs = qs + } + if (reqbody) { + options.body = JSON.stringify(reqbody) + options.headers['Content-Type'] = 'application/json' + } + + if (program.verbose) { + console.log("request options:", options) + } + + request(options, (error, response, body) => { + if (error) { + reject(error) + } + + let jsonarr = JSON.parse(response.body) + + if (program.verbose) { + console.log("Response body:", jsonarr) + } + + if (jsonarr.message) { + console.log('Server error message:') + console.log(jsonarr.code) + console.log(jsonarr.message) + reject(jsonarr.message) + } + + resolve(jsonarr) + }) + }) +} + +/** + * Interactive ui: displays the main menu + * + * @param {object} settings The full settings object read from the ini + */ +function uiMainMenu(settings) { + console.log() + inquirer + .prompt([ + { + type: 'list', + name: 'mainmenu', + message: 'Select command', + pageSize: process.stdout.rows - 1, + choices: + [ + { name: 'Restart recent measurement', value: 'restart' }, + { name: 'Start new measurement', value: 'start' }, + { name: 'Stop all active measurements', value: 'stop-all' }, + { name: 'Stop an active measurement', value: 'stop' }, + new inquirer.Separator(), + { name: 'List active measurements', value: 'list-active' }, + { name: 'List recent measurements', value: 'list-recent' }, + { name: 'List projects', value: 'list-projects' }, + { name: 'List activities', value: 'list-activities' }, + new inquirer.Separator(), + { name: 'Exit', value: 'exit' } + ] + } + ]) + .then(answers => { + if (program.verbose) { + console.log('selected answer: ' + answers.mainmenu) + } + switch (answers.mainmenu) { + case 'restart': + kimaiList(settings, 'timesheets/recent', false) + .then(res => { + return uiSelectMeasurement(res[1]) + }).then(startId => { + return kimaiRestart(settings, startId) + }) + .then(res => uiMainMenu(res[0])) + break; + case 'start': + uiKimaiStart(settings) + .then(_ => uiMainMenu(settings)) + break; + case 'stop-all': + kimaiStop(settings, false) + .then(_ => uiMainMenu(settings)) + break; + case 'stop': + kimaiList(settings, 'timesheets/active', false) + .then(res => { + return uiSelectMeasurement(res[1]) + }).then(stopId => { + return kimaiStop(settings, stopId) + }) + .then(res => uiMainMenu(res[0])) + break; + + case 'list-active': + kimaiList(settings, 'timesheets/active', true) + .then(res => uiMainMenu(res[0])) + break; + case 'list-recent': + kimaiList(settings, 'timesheets/recent', true) + .then(res => uiMainMenu(res[0])) + break; + case 'list-projects': + kimaiList(settings, 'projects', true) + .then(res => uiMainMenu(res[0])) + break; + case 'list-activities': + kimaiList(settings, 'activities', true) + .then(res => uiMainMenu(res[0])) + break; + default: + break; + } + }) +} + +/** + * Restarts a measurement + * + * @param {object} settings All settings read from ini + * @param {string} id The id of the measurement to restart + * + */ +function kimaiRestart(settings, id) { + return new Promise((resolve, reject) => { + callKimaiApi('PATCH', 'timesheets/' + id + '/restart', settings.serversettings) + .then(res => { + resolve([settings, res]) + }) + }) +} + +/** + * Interactive ui: select a project and activity and starts it + * + * @param {object} settings All settings read from ini + */ +function uiKimaiStart(settings) { + return new Promise((resolve, reject) => { + const selected = {} + kimaiList(settings, 'projects', false) + .then(res => { + // console.log(res[1]) + return uiAutocompleteSelect(res[1], 'Select project') + }) + .then(res => { + // console.log(res) + selected.projectId = res.id + return kimaiList(settings, 'activities', false, { filter: { project: res.id } }) + }) + .then(res => { + return uiAutocompleteSelect(res[1], 'Select activity') + }) + .then(res => { + selected.activityId = res.id + return kimaiStart(settings, selected.projectId, selected.activityId) + }) + .then(_ => { + resolve() + }) + }) +} + +/** + * Start a timer on the server + * + * @param {object} settings + * @param {string} project Id of project + * @param {string} activity Id of activity + */ +function kimaiStart(settings, project, activity) { + return new Promise((resolve, reject) => { + + let body = { + begin: moment().format(), + project: project, + activity: activity + } + if (program.verbose) { + console.log("kimaistart calling api:", body) + } + + callKimaiApi('POST', 'timesheets', settings.serversettings, { reqbody: body }) + .then(res => { + console.log('Started: ' + res.id) + resolve() + }) + }) + +} + +/** + * Find id of project or activity by name + * + * @param {object} settings + * @param {string} name The name to search for + * @param {string} endpoint + */ +function findId(settings, name, endpoint) { + return new Promise((resolve, reject) => { + kimaiList(settings, endpoint, false) + .then(res => { + const list = res[1] + for (let i = 0; i < list.length; i++) { + const element = list[i]; + if (element.name.toLowerCase() == name.toLowerCase()) { + resolve(element.id) + } + } + reject() + }) + }) +} + +/** + * Stops one or all current measurements. If id is empty it stops all, if given only selected + * + * @param {object} settings + * @param {string} id + */ +function kimaiStop(settings, id = false) { + return new Promise((resolve, reject) => { + if (id) { + callKimaiApi('PATCH', 'timesheets/' + id + '/stop', settings.serversettings) + .then(res => { + resolve([settings, res]) + }) + } else { + kimaiList(settings, 'timesheets/active', false) + .then(res => { + const jsonList = res[1] + return callKimaiStop(settings, jsonList) + //callKimaiStop(settings, jsonList) + }) + .then(_ => { + resolve() + }) + } + }) +} + +/** + * Supplementary function for stopping multiple running measurements + * + * @param {*} settings All settings + * @param {*} jsonList As the output of kimaiList() + * @param {*} i Counter, do not use! + */ +function callKimaiStop(settings, jsonList, i = 0) { + return new Promise((resolve, reject) => { + const element = jsonList[i]; + callKimaiApi('PATCH', 'timesheets/' + element.id + '/stop', settings.serversettings) + .then(jsl => { + console.log('Stopped: ', jsl.id) + i++ + if (i < jsonList.length) { + callKimaiStop(settings, jsonList, i) + } else { + resolve() + } + }) + }) +} + +/** + * Calls the api, lists and returns elements + * + * @param {object} settings The full settings object read from the ini + * @param {string} endpoint The endpoint to call in the api. + * @param {boolean} print If true, it prints to the terminal + * @param {object} options Options: + * options.filter: filter the query, + * @returns {array} res[0]: settings, res[1]: list of elements + */ +function kimaiList(settings, endpoint, print = false, options = false) { + const filter = options.filter || false + return new Promise((resolve, reject) => { + callKimaiApi('GET', endpoint, settings.serversettings, { qs: filter }) + .then(jsonList => { + if (print) { + printList(settings, jsonList, endpoint) + } + resolve([settings, jsonList]) + }) + .catch(msg => { + console.log("Error: " + msg) + }) + }) +} + + +/** + * Prints list to terminal + * + * @param {object} settings The full settings object read from the ini + * @param {array} arr Items to list + * @param {string} endpoint for selecting display layout + */ +function printList(settings, arr, endpoint) { + if (program.verbose) { + console.log() + if (arr.length > 1) { + console.log(arr.length + ' results:') + } else if (arr.length == 0) { + console.log('No results') + } else { + console.log('One result:') + } + } + //no result for scripts: + if (arr.length == 0) { + if (program.argos) { + console.log('No active measurements') + } + if (program.argosbutton) { + console.log("Kimai2 |") + } + } + for (let i = 0; i < arr.length; i++) { + const element = arr[i]; + + if (endpoint == 'projects' || endpoint == 'activities') { + if (program.verbose) { + console.log((i + 1) + ':', element.name, '(id:' + element.id + ')') + } else if (program.id) { + console.log(element.id + ':', element.name) + } else { + console.log(element.name) + } + + } else { //measurements + if (program.verbose) { + if (arr.length > 1) { + console.log((i + 1) + ":") + } + console.log(' Id: ' + element.id) + console.log(' Project: ' + element.project.name, '(id:' + element.project.id + ')') + console.log(' Customer: ' + element.project.customer.name, '(id:' + element.project.customer.id + ')') + console.log(' Activity: ' + element.activity.name, '(id:' + element.activity.id + ')') + console.log(' Begin: ' + element.begin) + + if (moment(element.end).isValid()) { + //finished measurements: + console.log(' Duration: ' + formattedDuration(element.begin, element.end)) + } else { + //active measurements: + console.log(' Duration: ' + formattedDuration(element.begin)) + } + + } else if (program.id) { + console.log(element.id + ':', element.project.name, '|', element.activity.name) + } else if (program.argos) { + //Argos + if (endpoint == 'timesheets/recent') { + console.log('--' + element.project.name + ',', element.activity.name, '|', 'bash=' + settings.argos_bitbar.kimaipath + ' param1=restart param2=' + element.id + ' terminal=false refresh=true') + } else if (endpoint == 'timesheets/active') { + console.log(formattedDuration(element.begin), element.project.name + ',', element.activity.name, '|', 'bash=' + settings.argos_bitbar.kimaipath + ' param1=stop param2=' + element.id + ' terminal=false refresh=true') + } + } else if (program.argosbutton) { + //Argosbutton + console.log(formattedDuration(element.begin), element.project.name + ',', element.activity.name, '| length=' + settings.argos_bitbar.buttonlength) + } else { + //Regular output + if (moment(element.end).isValid()) { + //finished measurements: + console.log(element.project.name, '|', element.activity.name) + } else { + //active measurements: + console.log(formattedDuration(element.begin), element.project.name, '|', element.activity.name) + } + } + } + } +} + +/** + * Returns duration between the two moments or between beginning and now. padded to minimum two digits. + * + * @param {moment} begin beginning moment + * @param {moment} end optional, end moment + */ +function formattedDuration(begin, end) { + let momentDuration = moment.duration(moment(end).diff(moment(begin))) + + let hrs = momentDuration.hours() + let mins = momentDuration.minutes() + + if (hrs.toString().length == 1) { + hrs = "0" + hrs + } + + if (mins.toString().length == 1) { + mins = "0" + mins + } + + return hrs + ':' + mins +} + + +/** + * Interactive ui: select measurement from a list of measurements + * @param {} thelist + */ +function uiSelectMeasurement(thelist) { + return new Promise((resolve, reject) => { + const choices = [] + for (let i = 0; i < thelist.length; i++) { + const element = thelist[i]; + choices.push({ + name: element.project.name + " | " + element.activity.name, value: element.id + }) + } + inquirer + .prompt([ + { + type: 'list', + name: 'selectMeasurement', + message: 'Select measurement', + pageSize: process.stdout.rows - 1, + choices: choices + } + ]).then(answers => { + resolve(answers.selectMeasurement) + }) + }) +} + +/** + * Returns a prompt with autocomplete + * + * @param {array} thelist The list of elements to select from + * @param {string} message Prompt message + */ +function uiAutocompleteSelect(thelist, message) { + return new Promise((resolve, reject) => { + const choices = [] + const names = [] + for (let i = 0; i < thelist.length; i++) { + const element = thelist[i]; + choices.push({ + name: element.name, id: element.id + }) + names.push(element.name) + } + inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')); + inquirer + .prompt([ + { + type: 'autocomplete', + name: 'autoSelect', + message: message, + pageSize: process.stdout.rows - 2, + source: function (answers, input) { + input = input || ''; + return new Promise((resolve, reject) => { + var fuzzyResult = fuzzy.filter(input, names); + resolve( + fuzzyResult.map(function (el) { + return el.original; + }) + ) + }) + } + } + ]).then(answers => { + let ind = names.indexOf(answers.autoSelect) + let selectedChoice = choices[ind] + // console.log(selectedChoice) + resolve(selectedChoice) + }) + }) +} + + +/** + * Finds settings file path + * + * @returns string: Path to settings.ini + * @returns false: If no settings found + */ +function iniPath() { + //different settings.ini path for developement and pkg and windows installer version + const iniRoot = [ + path.dirname(process.execPath),//This is for pkg version + __dirname//This is for npm version + ] + + if (appdata) { iniRoot.push(path.join(appdata, '/kimai2-cmd')) } + + if (program.verbose) { + console.log('Looking for settings.ini in the following places:') + console.log(iniRoot) + } + + for (let i = 0; i < iniRoot.length; i++) { + const currentIniPath = path.join(iniRoot[i], '/settings.ini') + if (fs.existsSync(currentIniPath)) { + return currentIniPath + } + } + + // no ini found so: + return false +} + +/** + * Checks if settings file exists, if not it's asks for settings + * + * @return {object} settings: all settings read from the settings file + */ +function checkSettings() { + return new Promise((resolve, reject) => { + const settingsPath = iniPath() + if (settingsPath) { + if (program.verbose) console.log("settings.ini found at: ", settingsPath) + let settings = ini.parse(fs.readFileSync(settingsPath, 'utf-8')) + resolve(settings) + } else { + console.log('Settings.ini not found') + uiAskForSettings() + .then(settings => { + resolve(settings) + }) + + } + }) +} + +/** + * Interactive ui: asks for settings than saves them + * + */ +function uiAskForSettings() { + return new Promise((resolve, reject) => { + let questions = [ + { + type: 'input', + name: 'kimaiurl', + message: "Kimai2 url:" + }, + { + type: 'input', + name: 'username', + message: "Username:" + }, + { + type: 'input', + name: 'password', + message: "API password:" + } + ] + + inquirer + .prompt(questions) + .then(answers => { + let settings = {} + settings.serversettings = answers + settings.argos_bitbar = {} + + if (platform == "darwin") { + settings.argos_bitbar.kimaipath = process.execPath + } else { + settings.argos_bitbar.kimaipath = "kimai" + } + settings.argos_bitbar.buttonlength = 10 + + const thePath = iniFullPath() + if (program.verbose) { console.log('Trying to save settings to: ' + thePath) } + + fs.writeFileSync(thePath, ini.stringify(settings)) + console.log('Settings saved to ' + iniPath()) + resolve(settings) + }); + }) +} + + +/** + * Returns the ini save path based on os and installation type, creates folder if necessary + */ +function iniFullPath() { + let installDir = path.dirname(process.execPath).split("\\") + let dirArr = __dirname.split(path.sep) + + //Maybe I should replace this terrible 'if' with some registry value reading + if (platform == 'win32' && installDir[installDir.length - 2] == "Program Files" && installDir[installDir.length - 1] == "kimai2-cmd") { + if (program.verbose) { console.log('This is an installer based windows installation') } + if (!fs.existsSync(path.join(appdata, 'kimai2-cmd'))) { + fs.mkdirSync(path.join(appdata, 'kimai2-cmd')) + } + return path.join(appdata, 'kimai2-cmd', 'settings.ini') + } else if (dirArr[0] == 'snapshot' || dirArr[1] == 'snapshot') { + if (program.verbose) {console.log('This is a pkg version')} + //for pkg version: + return path.join(path.dirname(process.execPath), 'settings.ini') + } else { + if (program.verbose) {console.log('This is an npm version')} + //For npm version: + return 'settings.ini' + } +} + + +/** + * Removes trailing slashes from url + * + * @param {string} kimaiurl Url to sanitize + */ +function sanitizeServerUrl(kimaiurl) { + return kimaiurl.replace(/\/+$/, ""); +} + +/* -------------------------------------------------------------------------- */ +/* Commander */ +/* -------------------------------------------------------------------------- */ + +program + .version(pjson.version) + .description(pjson.description + '. For interactive mode start without any commands. To generate settings file start in interactive mode!') + .option('-v, --verbose', 'verbose, longer logging', false) + .option('-i, --id', 'show id of elements when listing', false) + // .option('-r, --rainmeter', 'generate rainmeter files') + .option('-b, --argosbutton', 'argos/bitbar button output') + .option('-a, --argos', 'argos/bitbar output') + +program.command('start [project] [activity]') + .description('start selected project and activity') + .action(function (project, activity) { + const selected = {} + checkSettings() + .then(settings => { + findId(settings, project, 'projects') + .then(projectid => { + selected.projectId = projectid + return findId(settings, activity, 'activities') + }) + .then(activityid => { + selected.activityId = activityid + return kimaiStart(settings, selected.projectId, selected.activityId) + }) + }) + }) + +program.command('restart [id]') + .description('restart selected measurement') + .action(function (measurementId) { + checkSettings() + .then(settings => { + kimaiRestart(settings, measurementId) + }) + }) + +program.command('stop [id]') + .description('stop all or selected measurement measurements, [id] is optional') + .action(function (measurementId) { + checkSettings() + .then(settings => { + kimaiStop(settings, measurementId) + }) + }) + +program.command('list-active') + .description('list active measurements') + .action(function () { + checkSettings() + .then(settings => { + kimaiList(settings, 'timesheets/active', true) + }) + }) + +program.command('list-recent') + .description('list recent measurements') + .action(function () { + checkSettings() + .then(settings => { + kimaiList(settings, 'timesheets/recent', true) + }) + }) + +program.command('list-projects') + .description('list all projects') + .action(function () { + checkSettings() + .then(settings => { + kimaiList(settings, 'projects', true) + }) + }) + +program.command('list-activities') + .description('list all activities') + .action(function () { + checkSettings() + .then(settings => { + kimaiList(settings, 'activities', true) + }) + }) + +program.command('url') + .description('prints the url of the server') + .action(function () { + checkSettings() + .then(settings => { + console.log(settings.serversettings.kimaiurl) + }) + }) + +// program.command('debug') +// .description('debug snapshot filesystem. If you see this you are using a developement build') +// .action(function () { +// fs.readdir(__dirname, (err, files) => { console.log(files) }) +// }) + +program.parse(process.argv); + + +//interactive mode if no option added +if (!program.args.length) { + checkSettings() + .then(settings => { + uiMainMenu(settings) + }) +} diff --git a/settings.ini.example b/settings.ini.example index 1b3623e..9358b5e 100644 --- a/settings.ini.example +++ b/settings.ini.example @@ -1,8 +1,8 @@ -[serversettings] -kimaiurl=https://demo.kimai.org -username=anna_admin -password=api_kitten - -[argos_bitbar] -kimaipath=/path/to/kimai2-cmd-macos +[serversettings] +kimaiurl=https://demo.kimai.org +username=anna_admin +password=api_kitten + +[argos_bitbar] +kimaipath=/path/to/kimai2-cmd-macos buttonlength=10 \ No newline at end of file From acd5d977f2e578166657a5859608756cdcb8df35 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Sun, 11 Aug 2019 23:56:46 +0200 Subject: [PATCH 13/13] fixed settings.inin locations --- kimai2-cmd.js | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/kimai2-cmd.js b/kimai2-cmd.js index 72d2c17..efba725 100644 --- a/kimai2-cmd.js +++ b/kimai2-cmd.js @@ -546,23 +546,17 @@ function uiAutocompleteSelect(thelist, message) { * @returns false: If no settings found */ function iniPath() { - //different settings.ini path for developement and pkg and windows installer version - const iniRoot = [ - path.dirname(process.execPath),//This is for pkg version - __dirname//This is for npm version - ] - - if (appdata) { iniRoot.push(path.join(appdata, '/kimai2-cmd')) } - if (program.verbose) { console.log('Looking for settings.ini in the following places:') console.log(iniRoot) } - for (let i = 0; i < iniRoot.length; i++) { - const currentIniPath = path.join(iniRoot[i], '/settings.ini') - if (fs.existsSync(currentIniPath)) { - return currentIniPath + for (var key in iniRoot) { + if (iniRoot.hasOwnProperty(key)) { + const currentIniPath = path.join(iniRoot[key], '/settings.ini') + if (fs.existsSync(currentIniPath)) { + return currentIniPath + } } } @@ -655,15 +649,15 @@ function iniFullPath() { if (!fs.existsSync(path.join(appdata, 'kimai2-cmd'))) { fs.mkdirSync(path.join(appdata, 'kimai2-cmd')) } - return path.join(appdata, 'kimai2-cmd', 'settings.ini') + return path.join(iniRoot.wininstaller, 'settings.ini') } else if (dirArr[0] == 'snapshot' || dirArr[1] == 'snapshot') { - if (program.verbose) {console.log('This is a pkg version')} + if (program.verbose) { console.log('This is a pkg version') } //for pkg version: - return path.join(path.dirname(process.execPath), 'settings.ini') + return path.join(iniRoot.pkg, 'settings.ini') } else { - if (program.verbose) {console.log('This is an npm version')} + if (program.verbose) { console.log('This is an npm version') } //For npm version: - return 'settings.ini' + return path.join(iniRoot.npm, 'settings.ini') } } @@ -677,6 +671,20 @@ function sanitizeServerUrl(kimaiurl) { return kimaiurl.replace(/\/+$/, ""); } +/* -------------------------------------------------------------------------- */ +/* Settings.ini locations */ +/* -------------------------------------------------------------------------- */ + +//different settings.ini path for developement and pkg and windows installer version +const iniRoot = { + pkg: path.dirname(process.execPath),//This is for pkg version + npm: __dirname//This is for npm version +} + +if (appdata) { + iniRoot.wininstaller = path.join(appdata, '/kimai2-cmd') +} + /* -------------------------------------------------------------------------- */ /* Commander */ /* -------------------------------------------------------------------------- */