├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── assets ├── interactive-restart.gif ├── interactive-settings.gif ├── interactive-start.gif ├── kimai-icon-192x192.ico └── kimai-icon-192x192.png ├── kimai2-cmd.js ├── kimai2-innosetup.iss ├── package-lock.json ├── package.json └── settings.ini.example /.gitignore: -------------------------------------------------------------------------------- 1 | settings.ini 2 | settings.ini.old 3 | node_modules/ 4 | builds/ 5 | *.log -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "bitbar", 4 | "inno", 5 | "kargos", 6 | "kimai", 7 | "pjson", 8 | "pkg's" 9 | ], 10 | "cSpell.ignoreWords": [ 11 | "argosbutton", 12 | "chmod", 13 | "executables", 14 | "pkg" 15 | ] 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 infeeeee 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **⚠️ UNMAINTAINED ⚠️** 2 | 3 | PRs will be accepted, but issues won't be fixed. 4 | 5 | Alternative maintained clients: 6 | - [Kimai-console](https://github.com/kevinpapst/kimai2-console): Another cli app 7 | - [Kemai](https://github.com/AlexandrePTJ/kemai): Graphical client 8 | 9 | # Kimai2-cmd 10 | 11 | Command line client for [Kimai2](https://www.kimai.org/), the open source, self-hosted time tracker. 12 | 13 | ![interactive restart gif](assets/interactive-restart.gif) 14 | 15 | To use this program you have to install Kimai2 first! 16 | 17 | ## Features 18 | 19 | This client is not intended to replace the Kimai webUI, so only basic functions, starting and stopping measurements 20 | 21 | Commands: 22 | - Start, restart and stop measurements 23 | - List active and recent measurements 24 | - List projects and activities 25 | 26 | UI: 27 | - Interactive terminal UI with autocomplete 28 | - Classic terminal UI for integration 29 | 30 | Integration: 31 | - Portable executable for all three platforms 32 | - Installer for Windows 33 | - Generate output for Rainmeter (Windows). More info here: [kimai2-cmd-rainmeter](https://github.com/infeeeee/kimai2-cmd-rainmeter) 34 | - Generate output for Argos/Kargos/Bitbar (Gnome, Kde, Mac). More info here: [kimai2-cmd-argos](https://github.com/infeeeee/kimai2-cmd-argos) 35 | 36 | Requests for integrations with other softwares are welcomed! Just open an issue and show an example output, what you need. 37 | 38 | ## Installation 39 | 40 | Download from [releases](https://github.com/infeeeee/kimai2-cmd/releases/latest). 41 | 42 | You have to create an API password for your username on your Kimai installation. In Kimai: User menu (Top right corner) -> Edit -> API. 43 | 44 | ### Notes on Windows 45 | 46 | Portable executable or installer available. 47 | 48 | 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. 49 | 50 | 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. 51 | 52 | ### Notes on Linux/Mac 53 | 54 | Portable executable only. On the following terminal examples use the file name you downloaded. 55 | 56 | Make the downloaded binary executable: 57 | ``` 58 | sudo chmod +x kimai2-cmd-os 59 | ``` 60 | 61 | Add kimai2-cmd to path so you have to just type `kimai` to the terminal: 62 | ``` 63 | sudo ln -s /path/to/kimai2-cmd-os /usr/bin/kimai 64 | ``` 65 | 66 | To remove: 67 | ``` 68 | sudo rm /usr/bin/kimai 69 | ``` 70 | 71 | ### Install with npm 72 | 73 | If npm installed you can install it with the following command: 74 | 75 | ``` 76 | npm install -g infeeeee/kimai2-cmd 77 | ``` 78 | 79 | ## Usage 80 | 81 | Two usage modes: interactive and classic ui 82 | 83 | ### Interactive ui 84 | 85 | ![interactive start gif](assets/interactive-start.gif) 86 | 87 | 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. 88 | 89 | You can exit with ctrl+c any time. 90 | 91 | ### Classic ui 92 | 93 | You can find all the options in the help: 94 | 95 | ``` 96 | $ kimai2-cmd --help 97 | 98 | Usage: kimai2-cmd [options] [command] 99 | 100 | Command line client for Kimai2. For interactive mode start without any commands. To generate settings file start in interactive mode! 101 | 102 | Options: 103 | -V, --version output the version number 104 | -v, --verbose verbose, longer logging 105 | -i, --id show id of elements when listing 106 | -b, --argosbutton argos/bitbar button output 107 | -a, --argos argos/bitbar output 108 | -h, --help output usage information 109 | 110 | Commands: 111 | start [project] [activity] start selected project and activity 112 | restart [id] restart selected measurement 113 | stop [id] [description] stop all or selected measurement measurements, [id] is optional, [description] is optional but needs [id] 114 | rainmeter update rainmeter skin 115 | list-active list active measurements 116 | list-recent list recent measurements 117 | list-projects list all projects 118 | list-activities list all activities 119 | url prints the url of the server 120 | ``` 121 | 122 | 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`: 123 | 124 | ``` 125 | kimai2-cmd start "foo" "bar bar" 126 | ``` 127 | 128 | ### Settings and first run 129 | 130 | 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: 131 | 132 | ![interactive settings gif](assets/interactive-settings.gif) 133 | 134 | 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). 135 | 136 | On the windows installer version settings.ini location: `C:\Users\Username\AppData\Roaming\kimai2-cmd\settings.ini` 137 | 138 | You can also store the settings.ini file in custom location, just export the full path to the `KIMAI_CONFIG` variable, something like this: 139 | 140 | ```bash 141 | export KIMAI_CONFIG=$XDG_CONFIG_HOME/kimai2/settings.ini 142 | ``` 143 | 144 | 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. 145 | 146 | ## Development version 147 | 148 | ### Installation 149 | 150 | Prerequisites: 151 | - node js 10+ 152 | - git 153 | 154 | ``` 155 | git clone https://github.com/infeeeee/kimai2-cmd 156 | cd kimai2-cmd 157 | npm i 158 | ``` 159 | 160 | ### Build 161 | 162 | Prerequisite: globally installed [pkg](https://github.com/zeit/pkg): 163 | 164 | ``` 165 | npm i pkg -g 166 | ``` 167 | 168 | Build for current platform and architecture 169 | 170 | ``` 171 | npm run build-current 172 | ``` 173 | 174 | Build x64 executables for linux, mac on linux or on mac 175 | 176 | ``` 177 | npm run build-nix 178 | ``` 179 | 180 | About building for other platforms see pkg's documentation, or open an issue and I can build it for you. 181 | 182 | ### Installer (Windows only) 183 | 184 | Prerequisite: [Inno Setup](http://www.jrsoftware.org/isinfo.php) 185 | 186 | - Create a windows build: `npm run build-current` 187 | - Open `kimai2-innosetup.iss` in Inno Setup 188 | 189 | ### Usage 190 | 191 | For interactive mode just simply: 192 | 193 | ``` 194 | npm start 195 | ``` 196 | 197 | For usage with options you have pass a `--` before the options. You don't need this if you don't use options just commands: 198 | 199 | ``` 200 | npm start -- -V 201 | npm start start foo bar 202 | ``` 203 | 204 | 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 205 | 206 | ## Troubleshooting 207 | 208 | If you find a bug open an issue here! 209 | 210 | ## License 211 | 212 | MIT 213 | -------------------------------------------------------------------------------- /assets/interactive-restart.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infeeeee/kimai2-cmd/874dee64b987643c59cea665be93ad9a1969a449/assets/interactive-restart.gif -------------------------------------------------------------------------------- /assets/interactive-settings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infeeeee/kimai2-cmd/874dee64b987643c59cea665be93ad9a1969a449/assets/interactive-settings.gif -------------------------------------------------------------------------------- /assets/interactive-start.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infeeeee/kimai2-cmd/874dee64b987643c59cea665be93ad9a1969a449/assets/interactive-start.gif -------------------------------------------------------------------------------- /assets/kimai-icon-192x192.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infeeeee/kimai2-cmd/874dee64b987643c59cea665be93ad9a1969a449/assets/kimai-icon-192x192.ico -------------------------------------------------------------------------------- /assets/kimai-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infeeeee/kimai2-cmd/874dee64b987643c59cea665be93ad9a1969a449/assets/kimai-icon-192x192.png -------------------------------------------------------------------------------- /kimai2-cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* -------------------------------------------------------------------------- */ 4 | /* Modules */ 5 | /* -------------------------------------------------------------------------- */ 6 | 7 | //builtin 8 | const path = require('path'); 9 | const fs = require('fs'); 10 | 11 | const platform = process.platform 12 | const appdata = process.env.appdata 13 | const userprofile = process.env.userprofile 14 | 15 | //request 16 | const request = require('request'); 17 | 18 | //ui 19 | const inquirer = require('inquirer'); 20 | const fuzzy = require('fuzzy'); 21 | const program = require('commander'); 22 | 23 | // ini 24 | const ini = require('ini'); 25 | 26 | //moment 27 | const moment = require('moment'); 28 | 29 | //reading version number from package.json 30 | var pjson = require('./package.json'); 31 | const { 32 | config 33 | } = require('process'); 34 | 35 | /* -------------------------------------------------------------------------- */ 36 | /* Functions */ 37 | /* -------------------------------------------------------------------------- */ 38 | 39 | /** 40 | * Calls the kimai API 41 | * 42 | * @param {string} httpMethod Http method: 'GET', 'POST', 'PATCH'... 43 | * @param {string} kimaiMethod Endpoint to call on the kimai API: timesheet, activities, timesheets/123/stop 44 | * @param {object} serversettings Serversettings section read from ini. Only serversettings, not the full settings! 45 | * @param {object} options All of them are optional: 46 | * options.qs querystring 47 | * options.reqbody request body 48 | * @returns {object} The response body as an object 49 | * 50 | */ 51 | function callKimaiApi(httpMethod, kimaiMethod, serversettings, options = false) { 52 | //default options to false: 53 | const qs = options.qs || false 54 | const reqbody = options.reqbody || false 55 | 56 | debug("---") 57 | debug("calling kimai: " + " " + httpMethod + " " + kimaiMethod + " " + serversettings) 58 | 59 | return new Promise((resolve, reject) => { 60 | const options = { 61 | url: sanitizeServerUrl(serversettings.kimaiurl) + '/api/' + kimaiMethod, 62 | headers: { 63 | 'X-AUTH-USER': serversettings.username, 64 | 'X-AUTH-TOKEN': serversettings.password, 65 | }, 66 | method: httpMethod 67 | } 68 | 69 | if (qs) { 70 | options.qs = qs 71 | } 72 | if (reqbody) { 73 | options.body = JSON.stringify(reqbody) 74 | options.headers['Content-Type'] = 'application/json' 75 | } 76 | 77 | if (typeof serversettings.ca_cert_file !== 'undefined' && serversettings.ca_cert_file !== '') { 78 | if (fs.existsSync(serversettings.ca_cert_file)) { 79 | options.agentOptions = { 80 | ca: fs.readFileSync(serversettings.ca_cert_file) 81 | }; 82 | } else { 83 | console.error(`Configured CA cert file ${serversettings.ca_cert_file} does not exist or is not accessible.`); 84 | } 85 | } 86 | 87 | debug("request options: " + options.body) 88 | 89 | request(options, (error, response, body) => { 90 | if (error) { 91 | reject(error) 92 | } 93 | 94 | let jsonarr = JSON.parse(response.body) 95 | 96 | debug("Response body: " + jsonarr) 97 | 98 | if (jsonarr.message) { 99 | console.log('Server error message:') 100 | console.log(jsonarr.code) 101 | console.log(jsonarr.message) 102 | reject(jsonarr.message) 103 | } 104 | 105 | resolve(jsonarr) 106 | }) 107 | }) 108 | } 109 | 110 | /** 111 | * Interactive ui: displays the main menu 112 | * 113 | * @param {object} settings The full settings object read from the ini 114 | */ 115 | function uiMainMenu(settings) { 116 | console.log() 117 | inquirer 118 | .prompt([{ 119 | type: 'list', 120 | name: 'mainmenu', 121 | message: 'Select command', 122 | pageSize: process.stdout.rows - 1, 123 | choices: [{ 124 | name: 'Restart recent measurement', 125 | value: 'restart' 126 | }, 127 | { 128 | name: 'Start new measurement', 129 | value: 'start' 130 | }, 131 | { 132 | name: 'Stop all active measurements', 133 | value: 'stop-all' 134 | }, 135 | { 136 | name: 'Stop an active measurement', 137 | value: 'stop' 138 | }, 139 | new inquirer.Separator(), 140 | { 141 | name: 'List active measurements', 142 | value: 'list-active' 143 | }, 144 | { 145 | name: 'List recent measurements', 146 | value: 'list-recent' 147 | }, 148 | { 149 | name: 'List projects', 150 | value: 'list-projects' 151 | }, 152 | { 153 | name: 'List activities', 154 | value: 'list-activities' 155 | }, 156 | new inquirer.Separator(), 157 | { 158 | name: 'Exit', 159 | value: 'exit' 160 | } 161 | ] 162 | }]) 163 | .then(answers => { 164 | 165 | debug('selected answer: ' + answers.mainmenu) 166 | 167 | switch (answers.mainmenu) { 168 | case 'restart': 169 | kimaiList(settings, 'timesheets/recent', false) 170 | .then(res => { 171 | return uiSelectMeasurement(res[1]) 172 | }).then(startId => { 173 | return kimaiRestart(settings, startId) 174 | }) 175 | .then(res => uiMainMenu(res[0])) 176 | break; 177 | case 'start': 178 | uiKimaiStart(settings) 179 | .then(_ => uiMainMenu(settings)) 180 | break; 181 | case 'stop-all': 182 | kimaiStop(settings, false) 183 | .then(_ => uiMainMenu(settings)) 184 | break; 185 | case 'stop': 186 | const selected = {} 187 | kimaiList(settings, 'timesheets/active', false) 188 | .then(res => { 189 | if (res[1].length > 0) { 190 | return uiSelectMeasurement(res[1]) 191 | } 192 | }) 193 | .then(stopId => { 194 | selected.id = stopId; 195 | 196 | // Only ask for the description if a specific measurement has been selected 197 | return selected.id ? uiEnterDescription() : undefined 198 | }) 199 | .then(res => { 200 | // only set the description if one has been prompted and entered 201 | if (res && res.enterDescription) { 202 | return kimaiSetDescription(settings, selected.id, res.enterDescription) 203 | } 204 | }) 205 | .then(res => { 206 | return kimaiStop(settings, selected.id) 207 | }) 208 | .then(res => uiMainMenu(res[0])) 209 | break; 210 | 211 | case 'list-active': 212 | kimaiList(settings, 'timesheets/active', true) 213 | .then(res => uiMainMenu(res[0])) 214 | break; 215 | case 'list-recent': 216 | kimaiList(settings, 'timesheets/recent', true) 217 | .then(res => uiMainMenu(res[0])) 218 | break; 219 | case 'list-projects': 220 | kimaiList(settings, 'projects', true) 221 | .then(res => uiMainMenu(res[0])) 222 | break; 223 | case 'list-activities': 224 | kimaiList(settings, 'activities', true) 225 | .then(res => uiMainMenu(res[0])) 226 | break; 227 | default: 228 | break; 229 | } 230 | }) 231 | } 232 | 233 | /** 234 | * Restarts a measurement 235 | * 236 | * @param {object} settings All settings read from ini 237 | * @param {string} id The id of the measurement to restart 238 | * 239 | */ 240 | function kimaiRestart(settings, id) { 241 | return new Promise((resolve, reject) => { 242 | callKimaiApi('PATCH', 'timesheets/' + id + '/restart', settings.serversettings) 243 | .then(res => { 244 | resolve([settings, res]) 245 | }) 246 | }) 247 | } 248 | 249 | /** 250 | * Returns the date according to settings 251 | * 252 | * @param {object} settings All settings read from ini 253 | * @returns {string} The date according to settings 254 | */ 255 | function kimaiServerTime(settings) { 256 | if (settings.serversettings.servertime) { 257 | callKimaiApi('GET', 'config/i18n', settings.serversettings) 258 | .then(res => { 259 | debug("servertime: " + res.now) 260 | debug("localtime: " + moment().format()) 261 | return res.now 262 | }) 263 | } else { 264 | debug("localtime: " + moment().format()) 265 | return moment().format() 266 | } 267 | } 268 | 269 | /** 270 | * Interactive ui: select a project and activity and starts it 271 | * 272 | * @param {object} settings All settings read from ini 273 | */ 274 | function uiKimaiStart(settings) { 275 | return new Promise((resolve, reject) => { 276 | const selected = {} 277 | kimaiList(settings, 'projects', false) 278 | .then(res => { 279 | return uiAutocompleteSelect(res[1], 'Select project') 280 | }) 281 | .then(res => { 282 | selected.projectId = res.id 283 | return kimaiList(settings, 'activities', false, { 284 | filter: { 285 | project: res.id 286 | } 287 | }) 288 | }) 289 | .then(res => { 290 | return uiAutocompleteSelect(res[1], 'Select activity') 291 | }) 292 | .then(res => { 293 | selected.activityId = res.id 294 | return kimaiStart(settings, selected.projectId, selected.activityId) 295 | }) 296 | .then(_ => { 297 | resolve() 298 | }) 299 | }) 300 | } 301 | 302 | /** 303 | * Start a timer on the server 304 | * 305 | * @param {object} settings 306 | * @param {string} project Id of project 307 | * @param {string} activity Id of activity 308 | */ 309 | function kimaiStart(settings, project, activity) { 310 | return new Promise((resolve, reject) => { 311 | 312 | let body = { 313 | project: project, 314 | activity: activity 315 | } 316 | 317 | // select client or server time according to settings 318 | body.begin = kimaiServerTime(settings) 319 | 320 | debug("kimaistart calling api: " + body) 321 | 322 | callKimaiApi('POST', 'timesheets', settings.serversettings, { 323 | reqbody: body 324 | }) 325 | .then(res => { 326 | console.log('Started: ' + res.id) 327 | resolve() 328 | }) 329 | }) 330 | } 331 | 332 | /** 333 | * Find id of project or activity by name 334 | * 335 | * @param {object} settings 336 | * @param {string} name The name to search for 337 | * @param {string} endpoint 338 | */ 339 | function findId(settings, name, endpoint) { 340 | return new Promise((resolve, reject) => { 341 | kimaiList(settings, endpoint, false) 342 | .then(res => { 343 | const list = res[1] 344 | for (let i = 0; i < list.length; i++) { 345 | const element = list[i]; 346 | if (element.name.toLowerCase() == name.toLowerCase()) { 347 | resolve(element.id) 348 | } 349 | } 350 | reject() 351 | }) 352 | }) 353 | } 354 | 355 | /** 356 | * Stops one or all current measurements. If id is empty it stops all, if given only selected 357 | * 358 | * @param {object} settings 359 | * @param {string} id 360 | */ 361 | function kimaiStop(settings, id = false) { 362 | return new Promise((resolve, reject) => { 363 | if (id) { 364 | callKimaiApi('PATCH', 'timesheets/' + id + '/stop', settings.serversettings) 365 | .then(res => { 366 | resolve([settings, res]) 367 | }) 368 | } else { 369 | kimaiList(settings, 'timesheets/active', false) 370 | .then(res => { 371 | if (res[1].length > 0) { 372 | const jsonList = res[1] 373 | return callKimaiStop(settings, jsonList) 374 | } else { 375 | console.log('No active measurements') 376 | resolve([settings]) 377 | } 378 | }) 379 | .then(_ => { 380 | resolve() 381 | }) 382 | } 383 | }) 384 | } 385 | 386 | /** 387 | * Supplementary function for stopping multiple running measurements 388 | * 389 | * @param {*} settings All settings 390 | * @param {*} jsonList As the output of kimaiList() 391 | * @param {*} i Counter, do not use! 392 | */ 393 | function callKimaiStop(settings, jsonList, i = 0) { 394 | return new Promise((resolve, reject) => { 395 | const element = jsonList[i]; 396 | callKimaiApi('PATCH', 'timesheets/' + element.id + '/stop', settings.serversettings) 397 | .then(jsl => { 398 | console.log('Stopped: ', jsl.id) 399 | i++ 400 | if (i < jsonList.length) { 401 | callKimaiStop(settings, jsonList, i) 402 | } else { 403 | resolve() 404 | } 405 | }) 406 | }) 407 | } 408 | 409 | /** 410 | * Calls the api, lists and returns elements 411 | * 412 | * @param {object} settings The full settings object read from the ini 413 | * @param {string} endpoint The endpoint to call in the api. 414 | * @param {boolean} print If true, it prints to the terminal 415 | * @param {object} options Options: 416 | * options.filter: filter the query, 417 | * @returns {array} res[0]: settings, res[1]: list of elements 418 | */ 419 | function kimaiList(settings, endpoint, print = false, options = false) { 420 | const filter = options.filter || false 421 | return new Promise((resolve, reject) => { 422 | callKimaiApi('GET', endpoint, settings.serversettings, { 423 | qs: filter 424 | }) 425 | .then(jsonList => { 426 | if (print) { 427 | printList(settings, jsonList, endpoint) 428 | } 429 | resolve([settings, jsonList]) 430 | }) 431 | .catch(msg => { 432 | console.log("Error: " + msg) 433 | }) 434 | }) 435 | } 436 | 437 | 438 | /** 439 | * Prints list to terminal 440 | * 441 | * @param {object} settings The full settings object read from the ini 442 | * @param {array} arr Items to list 443 | * @param {string} endpoint for selecting display layout 444 | */ 445 | function printList(settings, arr, endpoint) { 446 | 447 | if (arr.length > 1) { 448 | debug(arr.length + ' results:') 449 | } else if (arr.length == 0) { 450 | debug('No results') 451 | } else { 452 | debug('One result:') 453 | } 454 | 455 | //no result for scripts: 456 | if (arr.length == 0) { 457 | if (program.argos) { 458 | console.log('No active measurements') 459 | } 460 | if (program.argosbutton) { 461 | console.log("Kimai2 |") 462 | } 463 | } 464 | for (let i = 0; i < arr.length; i++) { 465 | const element = arr[i]; 466 | 467 | if (endpoint == 'projects' || endpoint == 'activities') { 468 | if (program.verbose) { 469 | console.log((i + 1) + ':', element.name, '(id:' + element.id + ')') 470 | } else if (program.id) { 471 | console.log(element.id + ':', element.name) 472 | } else { 473 | console.log(element.name) 474 | } 475 | 476 | } else { //measurements 477 | if (program.verbose) { 478 | if (arr.length > 1) { 479 | console.log((i + 1) + ":") 480 | } 481 | console.log(' Id: ' + element.id) 482 | console.log(' Project: ' + element.project.name, '(id:' + element.project.id + ')') 483 | console.log(' Customer: ' + element.project.customer.name, '(id:' + element.project.customer.id + ')') 484 | console.log(' Activity: ' + element.activity.name, '(id:' + element.activity.id + ')') 485 | console.log(' Begin: ' + element.begin) 486 | 487 | if (moment(element.end).isValid()) { 488 | //finished measurements: 489 | console.log(' Duration: ' + formattedDuration(element.begin, element.end)) 490 | } else { 491 | //active measurements: 492 | console.log(' Duration: ' + formattedDuration(element.begin)) 493 | } 494 | 495 | } else if (program.id) { 496 | console.log(element.id + ':', element.project.name, '|', element.activity.name) 497 | } else if (program.argos) { 498 | //Argos 499 | if (endpoint == 'timesheets/recent') { 500 | console.log('--' + element.project.name + ',', element.activity.name, '|', 'bash=' + settings.argos_bitbar.kimaipath + ' param1=restart param2=' + element.id + ' terminal=false refresh=true') 501 | } else if (endpoint == 'timesheets/active') { 502 | 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') 503 | } 504 | } else if (program.argosbutton) { 505 | //Argosbutton 506 | console.log(formattedDuration(element.begin), element.project.name + ',', element.activity.name, '| length=' + settings.argos_bitbar.buttonlength) 507 | } else { 508 | //Regular output 509 | if (moment(element.end).isValid()) { 510 | //finished measurements: 511 | console.log(element.project.name, '|', element.activity.name) 512 | } else { 513 | //active measurements: 514 | console.log(formattedDuration(element.begin), element.project.name, '|', element.activity.name) 515 | } 516 | } 517 | } 518 | } 519 | } 520 | 521 | /** 522 | * Returns duration between the two moments or between beginning and now. padded to minimum two digits. 523 | * 524 | * @param {moment} begin beginning moment 525 | * @param {moment} end optional, end moment 526 | * @param {boolean} returnArray optional, returns array if true, returns formatted text if false 527 | */ 528 | function formattedDuration(begin, end, returnArray = false) { 529 | let momentDuration = moment.duration(moment(end).diff(moment(begin))) 530 | 531 | let hrs = momentDuration.hours().toString() 532 | let mins = momentDuration.minutes().toString() 533 | 534 | if (hrs.length == 1) { 535 | hrs = "0" + hrs 536 | } 537 | 538 | if (mins.length == 1) { 539 | mins = "0" + mins 540 | } 541 | 542 | if (returnArray) { 543 | return [hrs, mins] 544 | } else { 545 | return hrs + ':' + mins 546 | } 547 | } 548 | 549 | 550 | /** 551 | * Interactive ui: select measurement from a list of measurements 552 | * @param {} thelist 553 | */ 554 | function uiSelectMeasurement(thelist) { 555 | return new Promise((resolve, reject) => { 556 | const choices = [] 557 | if (thelist.length == 0) { 558 | reject() 559 | } 560 | for (let i = 0; i < thelist.length; i++) { 561 | const element = thelist[i]; 562 | choices.push({ 563 | name: element.project.name + " | " + element.activity.name, 564 | value: element.id 565 | }) 566 | } 567 | inquirer 568 | .prompt([{ 569 | type: 'list', 570 | name: 'selectMeasurement', 571 | message: 'Select measurement', 572 | pageSize: process.stdout.rows - 1, 573 | choices: choices 574 | }]).then(answers => { 575 | resolve(answers.selectMeasurement) 576 | }) 577 | }) 578 | } 579 | 580 | /** 581 | * Interactive UI: Prompt the user to enter a description for a measurement. 582 | */ 583 | function uiEnterDescription() { 584 | return new Promise((resolve, reject) => { 585 | inquirer 586 | .prompt({ 587 | type: 'input', 588 | name: 'enterDescription', 589 | message: 'Description: ' 590 | }).then(answer => { 591 | resolve(answer); 592 | }); 593 | }); 594 | } 595 | /** 596 | * Returns a prompt with autocomplete 597 | * 598 | * @param {array} thelist The list of elements to select from 599 | * @param {string} message Prompt message 600 | */ 601 | function uiAutocompleteSelect(thelist, message) { 602 | return new Promise((resolve, reject) => { 603 | const choices = [] 604 | const names = [] 605 | for (let i = 0; i < thelist.length; i++) { 606 | const element = thelist[i]; 607 | choices.push({ 608 | name: element.name, 609 | id: element.id 610 | }) 611 | names.push(element.name) 612 | } 613 | inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')); 614 | inquirer 615 | .prompt([{ 616 | type: 'autocomplete', 617 | name: 'autoSelect', 618 | message: message, 619 | pageSize: process.stdout.rows - 2, 620 | source: function (answers, input) { 621 | input = input || ''; 622 | return new Promise((resolve, reject) => { 623 | var fuzzyResult = fuzzy.filter(input, names); 624 | resolve( 625 | fuzzyResult.map(function (el) { 626 | return el.original; 627 | }) 628 | ) 629 | }) 630 | } 631 | }]).then(answers => { 632 | let ind = names.indexOf(answers.autoSelect) 633 | let selectedChoice = choices[ind] 634 | // console.log(selectedChoice) 635 | resolve(selectedChoice) 636 | }) 637 | }) 638 | } 639 | 640 | 641 | /** 642 | * Finds settings file path 643 | * 644 | * @returns string: Path to settings.ini 645 | * @returns false: If no settings found 646 | */ 647 | function iniPath() { 648 | 649 | // Check path in environment variable 650 | if (process.env.KIMAI_CONFIG) { 651 | const envIniPath = process.env.KIMAI_CONFIG 652 | debug('Found in KIMAI_CONFIG envvar: ' + envIniPath) 653 | if (fs.existsSync(envIniPath)) { 654 | return envIniPath 655 | } else { 656 | debug('KIMAI_CONFIG variable malformed') 657 | } 658 | } else { 659 | debug('No environment variable found') 660 | } 661 | 662 | debug('Looking for settings.ini in the following places:') 663 | debug(iniRoot) 664 | 665 | for (var key in iniRoot) { 666 | if (iniRoot.hasOwnProperty(key)) { 667 | const currentIniPath = path.join(iniRoot[key], '/settings.ini') 668 | if (fs.existsSync(currentIniPath)) { 669 | return currentIniPath 670 | } 671 | } 672 | } 673 | 674 | // no ini found so: 675 | return false 676 | } 677 | 678 | /** 679 | * Checks if settings file exists, if not it's asks for settings 680 | * 681 | * @return {object} settings: all settings read from the settings file 682 | */ 683 | function checkSettings() { 684 | return new Promise((resolve, reject) => { 685 | 686 | const settingsPath = iniPath() 687 | if (settingsPath) { 688 | debug("settings.ini found at: " + settingsPath) 689 | let settings = ini.parse(fs.readFileSync(settingsPath, 'utf-8')) 690 | resolve(settings) 691 | } else { 692 | console.log('Settings.ini not found') 693 | uiAskForSettings() 694 | .then(settings => { 695 | resolve(settings) 696 | }) 697 | 698 | } 699 | }) 700 | } 701 | 702 | /** 703 | * Prints to console if verbose 704 | * @param {string} msg 705 | */ 706 | function debug(msg) { 707 | if (program.verbose) console.log(msg) 708 | } 709 | 710 | /** 711 | * Interactive ui: asks for settings than saves them 712 | * 713 | */ 714 | function uiAskForSettings() { 715 | return new Promise((resolve, reject) => { 716 | let questions = [{ 717 | type: 'input', 718 | name: 'kimaiurl', 719 | message: "Kimai2 url:" 720 | }, 721 | { 722 | type: 'input', 723 | name: 'username', 724 | message: "Username:" 725 | }, 726 | { 727 | type: 'input', 728 | name: 'password', 729 | message: "API password:" 730 | }, 731 | { 732 | type: 'confirm', 733 | name: 'custom_ca', 734 | message: "Do you want to specify a custom certificate authority?", 735 | default: false 736 | }, 737 | { 738 | type: 'input', 739 | name: 'ca_cert_file', 740 | message: 'Path to CA cert file:', 741 | validate: function(input) { 742 | const result = fs.existsSync(input) 743 | if (result === false) 744 | { 745 | console.log('\nFile does not exist or is not accessible.') 746 | } 747 | return result; 748 | }, 749 | default: null, 750 | when: function(hash) { 751 | return hash.custom_ca; 752 | } 753 | } 754 | ] 755 | 756 | inquirer 757 | .prompt(questions) 758 | .then(answers => { 759 | let settings = {} 760 | settings.serversettings = answers 761 | 762 | //defaults servertime to false 763 | settings.serversettings.servertime = false 764 | 765 | //argos/bitbar settings 766 | settings.argos_bitbar = {} 767 | 768 | if (platform == "darwin") { 769 | settings.argos_bitbar.kimaipath = process.execPath 770 | } else { 771 | settings.argos_bitbar.kimaipath = "kimai" 772 | } 773 | settings.argos_bitbar.buttonlength = 10 774 | 775 | // rainmeter settings 776 | settings.rainmeter = {} 777 | 778 | if (userprofile) { 779 | settings.rainmeter.skindir = path.join(userprofile, "Documents\\Rainmeter\\Skins\\kimai2-cmd-rainmeter\\kimai2") 780 | } else { 781 | settings.rainmeter.skindir = "" 782 | } 783 | settings.rainmeter.meterstyle = "styleProjects" 784 | 785 | const thePath = iniFullPath() 786 | debug('Trying to save settings to: ' + thePath) 787 | 788 | fs.writeFileSync(thePath, ini.stringify(settings)) 789 | console.log('Settings saved to ' + iniPath()) 790 | resolve(settings) 791 | }); 792 | }) 793 | } 794 | 795 | /** 796 | * Sets the 'description' field of a measurement. Works on both running and stopped measurements. 797 | * @param {object} settings all settings read from the settings file 798 | * @param {string} id measurement id 799 | * @param {*} description the description that shall be applied to the measurement 800 | */ 801 | function kimaiSetDescription(settings, id, description) { 802 | return new Promise((resolve, reject) => { 803 | 804 | let body = { 805 | description: description 806 | } 807 | 808 | debug("kimaiSetDescription calling api: " + body) 809 | 810 | callKimaiApi('PATCH', 'timesheets/' + id, settings.serversettings, { 811 | reqbody: body 812 | }) 813 | .then(res => { 814 | console.log('Set description for: ' + res.id) 815 | resolve() 816 | }) 817 | }) 818 | } 819 | 820 | /** 821 | * Returns the ini save path based on os and installation type, creates folder if necessary 822 | */ 823 | function iniFullPath() { 824 | let installDir = path.dirname(process.execPath).split("\\") 825 | let dirArr = __dirname.split(path.sep) 826 | 827 | //Maybe I should replace this terrible 'if' with some registry value reading 828 | if (platform == 'win32' && installDir[installDir.length - 2] == "Program Files" && installDir[installDir.length - 1] == "kimai2-cmd") { 829 | debug('This is an installer based windows installation') 830 | 831 | if (!fs.existsSync(path.join(appdata, 'kimai2-cmd'))) { 832 | fs.mkdirSync(path.join(appdata, 'kimai2-cmd')) 833 | } 834 | return path.join(iniRoot.wininstaller, 'settings.ini') 835 | } else if (dirArr[0] == 'snapshot' || dirArr[1] == 'snapshot') { 836 | debug('This is a pkg version') 837 | 838 | //for pkg version: 839 | return path.join(iniRoot.pkg, 'settings.ini') 840 | } else { 841 | debug('This is an npm version') 842 | 843 | //For npm version: 844 | return path.join(iniRoot.npm, 'settings.ini') 845 | } 846 | } 847 | 848 | 849 | /** 850 | * Removes trailing slashes from url 851 | * 852 | * @param {string} kimaiurl Url to sanitize 853 | */ 854 | function sanitizeServerUrl(kimaiurl) { 855 | return kimaiurl.replace(/\/+$/, ""); 856 | } 857 | 858 | /** 859 | * Replace all occurenies of chars in string 860 | * 861 | * @param {string} search regex to search for 862 | * @param {string} replacement replacement string 863 | * 864 | */ 865 | String.prototype.replaceAll = function (search, replacement) { 866 | var target = this; 867 | return target.replace(new RegExp(search, 'g'), replacement); 868 | }; 869 | 870 | /* -------------------------------- Rainmeter ------------------------------- */ 871 | 872 | const rainmeterVars = {} 873 | rainmeterVars.Variables = {} 874 | const rainmeterRaw = {} 875 | const rainmeterData = {} 876 | 877 | /** 878 | * Updates rainmeter files 879 | * 880 | * @return {object} settings: all settings read from the settings file 881 | */ 882 | function updateRainmeter(settings) { 883 | kimaiList(settings, 'timesheets/recent', false) 884 | .then(res => { 885 | rainmeterRaw.recent = res[1] 886 | return kimaiList(settings, 'timesheets/active', false) 887 | }) 888 | .then(res => { 889 | // active measurement. Rainmeter only supports one active measurement. 890 | rainmeterVars.Variables.serverUrl = settings.serversettings.kimaiurl 891 | rainmeterVars.Variables.activeRecording = (res[1].length) ? res[1][0].project.name + ' - ' + res[1][0].activity.name : "No active recording" 892 | rainmeterVars.Variables.activeHrs = (res[1].length) ? formattedDuration(res[1][0].begin, undefined, true)[0] : "" 893 | rainmeterVars.Variables.activeMins = (res[1].length) ? formattedDuration(res[1][0].begin, undefined, true)[1] : "" 894 | rainmeterVars.Variables.activeRunning = (res[1].length) ? "1" : "0" 895 | 896 | //Add first id as default 897 | rainmeterVars.Variables.measurementid = rainmeterRaw.recent[0].id 898 | 899 | if (res[1].length) { 900 | rainmeterVars.Variables.startHidden = 1 901 | rainmeterVars.Variables.stopHidden = 0 902 | } else { 903 | rainmeterVars.Variables.startHidden = 0 904 | rainmeterVars.Variables.stopHidden = 1 905 | } 906 | 907 | //recent measurements 908 | for (let i = 0; i < rainmeterRaw.recent.length; i++) { 909 | let currMeter = {} 910 | currMeter.Meter = 'String' 911 | currMeter.MeterStyle = settings.rainmeter.meterstyle 912 | currMeter.DynamicVariables = '1' 913 | currMeter.Hidden = "#MenuVis#" 914 | currMeter.Text = rainmeterRaw.recent[i].project.name + ' - ' + rainmeterRaw.recent[i].activity.name 915 | currMeter.leftmouseupaction = ini.unsafe('[!SetVariable measurementid "' + rainmeterRaw.recent[i].id + '"][!UpdateMeasure MeasureStart][!CommandMeasure MeasureStart "Run"]') 916 | 917 | rainmeterData["MeterRecent" + i] = currMeter 918 | } 919 | 920 | let rainmeterVarPath = path.join(settings.rainmeter.skindir, 'kimaiVars.inc') 921 | let rainmeterDataPath = path.join(settings.rainmeter.skindir, 'kimaiData.inc') 922 | 923 | // stringify wraps spec character, rainmeter doesn't like that 924 | let rainmeterDataIni = ini.stringify(rainmeterData).replaceAll('\\\\#', '#').replaceAll('"\\[', '[').replaceAll('\]"', ']').replaceAll('\\\\"', '"') 925 | 926 | // write rainmeter files 927 | fs.writeFileSync(rainmeterVarPath, ini.stringify(rainmeterVars), { 928 | encoding: 'utf16le' 929 | }) 930 | fs.writeFileSync(rainmeterDataPath, rainmeterDataIni, { 931 | encoding: 'utf16le' 932 | }) 933 | 934 | debug("Rainmeter files:") 935 | debug(rainmeterVarPath, rainmeterDataPath) 936 | debug("rainmeter data:") 937 | debug(rainmeterVars) 938 | debug(rainmeterDataIni) 939 | 940 | }) 941 | } 942 | 943 | /* -------------------------------------------------------------------------- */ 944 | /* Settings.ini locations */ 945 | /* -------------------------------------------------------------------------- */ 946 | 947 | //different settings.ini path for developement and pkg and windows installer version 948 | const iniRoot = { 949 | pkg: path.dirname(process.execPath), //This is for pkg version 950 | npm: __dirname //This is for npm version 951 | } 952 | 953 | if (appdata) { 954 | iniRoot.wininstaller = path.join(appdata, '/kimai2-cmd') 955 | } 956 | 957 | /* -------------------------------------------------------------------------- */ 958 | /* Commander */ 959 | /* -------------------------------------------------------------------------- */ 960 | 961 | program 962 | .version(pjson.version) 963 | .description(pjson.description + '. For interactive mode start without any commands. To generate settings file start in interactive mode!') 964 | .option('-v, --verbose', 'verbose, longer logging', false) 965 | .option('-i, --id', 'show id of elements when listing', false) 966 | .option('-b, --argosbutton', 'argos/bitbar button output') 967 | .option('-a, --argos', 'argos/bitbar output') 968 | 969 | program.command('start [project] [activity]') 970 | .description('start selected project and activity') 971 | .action(function (project, activity) { 972 | const selected = {} 973 | checkSettings() 974 | .then(settings => { 975 | findId(settings, project, 'projects') 976 | .then(projectid => { 977 | selected.projectId = projectid 978 | return findId(settings, activity, 'activities') 979 | }) 980 | .then(activityid => { 981 | selected.activityId = activityid 982 | return kimaiStart(settings, selected.projectId, selected.activityId) 983 | }) 984 | }) 985 | }) 986 | 987 | program.command('restart [id]') 988 | .description('restart selected measurement') 989 | .action(function (measurementId) { 990 | checkSettings() 991 | .then(settings => { 992 | kimaiRestart(settings, measurementId) 993 | }) 994 | }) 995 | 996 | program.command('stop [id] [description]') 997 | .description('stop all or selected measurement measurements, [id] is optional, [description] is optional but needs [id]') 998 | .action(function (measurementId, description) { 999 | checkSettings() 1000 | .then(settings => { 1001 | kimaiSetDescription(settings, measurementId, description) 1002 | kimaiStop(settings, measurementId) 1003 | }) 1004 | }) 1005 | 1006 | program.command('rainmeter') 1007 | .description('update rainmeter skin') 1008 | .action(function () { 1009 | checkSettings() 1010 | .then(settings => { 1011 | updateRainmeter(settings) 1012 | }) 1013 | }) 1014 | 1015 | program.command('list-active') 1016 | .description('list active measurements') 1017 | .action(function () { 1018 | checkSettings() 1019 | .then(settings => { 1020 | kimaiList(settings, 'timesheets/active', true) 1021 | }) 1022 | }) 1023 | 1024 | program.command('list-recent') 1025 | .description('list recent measurements') 1026 | .action(function () { 1027 | checkSettings() 1028 | .then(settings => { 1029 | kimaiList(settings, 'timesheets/recent', true) 1030 | }) 1031 | }) 1032 | 1033 | program.command('list-projects') 1034 | .description('list all projects') 1035 | .action(function () { 1036 | checkSettings() 1037 | .then(settings => { 1038 | kimaiList(settings, 'projects', true) 1039 | }) 1040 | }) 1041 | 1042 | program.command('list-activities') 1043 | .description('list all activities') 1044 | .action(function () { 1045 | checkSettings() 1046 | .then(settings => { 1047 | kimaiList(settings, 'activities', true) 1048 | }) 1049 | }) 1050 | 1051 | program.command('url') 1052 | .description('prints the url of the server') 1053 | .action(function () { 1054 | checkSettings() 1055 | .then(settings => { 1056 | console.log(settings.serversettings.kimaiurl) 1057 | }) 1058 | }) 1059 | 1060 | program.command('config') 1061 | .description('configure kimai2-cmd') 1062 | .action(function() { 1063 | checkSettings() 1064 | .then(settings => { 1065 | uiAskForSettings() 1066 | }) 1067 | }) 1068 | 1069 | // program.command('debug') 1070 | // .description('debug snapshot filesystem. If you see this you are using a developement build') 1071 | // .action(function () { 1072 | // fs.readdir(__dirname, (err, files) => { console.log(files) }) 1073 | // }) 1074 | 1075 | program.parse(process.argv); 1076 | 1077 | 1078 | //interactive mode if no option added 1079 | if (!program.args.length) { 1080 | checkSettings() 1081 | .then(settings => { 1082 | uiMainMenu(settings) 1083 | }) 1084 | } 1085 | -------------------------------------------------------------------------------- /kimai2-innosetup.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Script Studio Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | [Setup] 5 | ; NOTE: The value of AppId uniquely identifies this application. 6 | ; Do not use the same AppId value in installers for other applications. 7 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 8 | AppId={{A10BF7B2-6641-4B06-9C68-268B649FCE57} 9 | AppName=kimai2-cmd 10 | AppVersion=1.3.1 11 | AppPublisher=infeeeee 12 | AppPublisherURL=https://github.com/infeeeee/kimai2-cmd 13 | AppSupportURL=https://github.com/infeeeee/kimai2-cmd 14 | AppUpdatesURL=https://github.com/infeeeee/kimai2-cmd 15 | DefaultDirName={commonpf}\kimai2-cmd 16 | DefaultGroupName=kimai2-cmd 17 | AllowNoIcons=yes 18 | LicenseFile={#SourcePath}\LICENSE 19 | OutputDir={#SourcePath}\builds 20 | OutputBaseFilename=kimai2-cmd-setup 21 | SetupIconFile={#SourcePath}\assets\kimai-icon-192x192.ico 22 | Compression=lzma 23 | SolidCompression=yes 24 | ArchitecturesInstallIn64BitMode=x64 25 | ArchitecturesAllowed=x64 26 | 27 | [Tasks] 28 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 29 | Name: "createini"; Description: "Create settings.ini during installation"; GroupDescription: "Server settings"; Flags: checkedonce 30 | 31 | [Files] 32 | Source: "builds\kimai2-cmd.exe"; DestDir: "{app}"; Flags: ignoreversion 33 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 34 | Source: "settings.ini.example"; DestDir: "{userappdata}\kimai2-cmd"; DestName: "settings.ini"; Flags: onlyifdoesntexist uninsneveruninstall recursesubdirs; Tasks: createini 35 | 36 | [Icons] 37 | Name: "{group}\kimai2-cmd"; Filename: "{app}\kimai2-cmd.exe" 38 | Name: "{group}\{cm:UninstallProgram,kimai2-cmd}"; Filename: "{uninstallexe}" 39 | Name: "{commondesktop}\kimai2-cmd"; Filename: "{app}\kimai2-cmd.exe"; Tasks: desktopicon 40 | 41 | [Run] 42 | Filename: "{app}\kimai2-cmd.exe"; Description: "{cm:LaunchProgram,kimai2-cmd}"; Flags: nowait postinstall skipifsilent 43 | 44 | [INI] 45 | Filename: "{userappdata}\kimai2-cmd\settings.ini"; Section: "serversettings"; Key: "kimaiurl"; String: "{code:GetKimaiUrl}"; Tasks: createini 46 | Filename: "{userappdata}\kimai2-cmd\settings.ini"; Section: "serversettings"; Key: "username"; String: "{code:GetUserName}"; Tasks: createini 47 | Filename: "{userappdata}\kimai2-cmd\settings.ini"; Section: "serversettings"; Key: "password"; String: "{code:GetPassword}"; Tasks: createini 48 | Filename: "{userappdata}\kimai2-cmd\settings.ini"; Section: "serversettings"; Key: "servertime"; String: "false"; Tasks: createini 49 | Filename: "{userappdata}\kimai2-cmd\settings.ini"; Section: "rainmeter"; Key: "skindir"; String: "{code:GetRainmeterPath}"; Tasks: createini 50 | Filename: "{userappdata}\kimai2-cmd\settings.ini"; Section: "rainmeter"; Key: "meterstyle"; String: "styleProjects"; Tasks: createini 51 | 52 | [Registry] 53 | Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \ 54 | ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; \ 55 | Check: NeedsAddPath(ExpandConstant('{app}')) 56 | 57 | [Code] 58 | var AuthPage: TInputQueryWizardPage; 59 | 60 | procedure InitializeWizard; 61 | begin 62 | AuthPage := CreateInputQueryPage(wpSelectTasks, 63 | 'Kimai2 settings', 'Please enter your server url and account information', 64 | 'You can modify this later in AppData\Roaming\kimai2-cmd\settings.ini'); 65 | AuthPage.Add('Kimai2 url:', False); 66 | AuthPage.Add('Username:', False); 67 | AuthPage.Add('API password:', False); 68 | AuthPage.Add('Skin folder', False); 69 | AuthPage.Values[3] := ExpandConstant('{userdocs}') + '\Rainmeter\Skins\kimai2-cmd-rainmeter\kimai2'; 70 | 71 | end; 72 | 73 | function ShouldSkipPage(PageID: Integer): Boolean; 74 | begin 75 | { Skip pages that shouldn't be shown } 76 | if (PageID = AuthPage.ID) and ( not WizardIsTaskSelected('createini')) then 77 | Result := True 78 | else 79 | Result := False; 80 | end; 81 | 82 | 83 | function AuthForm_NextButtonClick(Page: TWizardPage): Boolean; 84 | begin 85 | Result := True; 86 | end; 87 | 88 | function GetKimaiUrl(Param: String): string; 89 | begin 90 | result := AuthPage.Values[0]; 91 | end; 92 | 93 | function GetUserName(Param: String): string; 94 | begin 95 | result := AuthPage.Values[1]; 96 | end; 97 | 98 | function GetPassword(Param: String): string; 99 | begin 100 | result := AuthPage.Values[2]; 101 | end; 102 | 103 | function GetRainmeterPath(Param: String): string; 104 | begin 105 | result := AuthPage.Values[3]; 106 | end; 107 | 108 | function NeedsAddPath(Param: string): boolean; 109 | var 110 | OrigPath: string; 111 | begin 112 | if not RegQueryStringValue(HKEY_LOCAL_MACHINE, 113 | 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 114 | 'Path', OrigPath) 115 | then begin 116 | Result := True; 117 | exit; 118 | end; 119 | { look for the path with leading and trailing semicolon } 120 | { Pos() returns 0 if not found } 121 | Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0; 122 | end; 123 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kimai2-cmd", 3 | "version": "1.3.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ajv": { 8 | "version": "6.12.6", 9 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 10 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 11 | "requires": { 12 | "fast-deep-equal": "^3.1.1", 13 | "fast-json-stable-stringify": "^2.0.0", 14 | "json-schema-traverse": "^0.4.1", 15 | "uri-js": "^4.2.2" 16 | }, 17 | "dependencies": { 18 | "fast-deep-equal": { 19 | "version": "3.1.3", 20 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 21 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 22 | } 23 | } 24 | }, 25 | "ansi-escapes": { 26 | "version": "3.2.0", 27 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", 28 | "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" 29 | }, 30 | "ansi-regex": { 31 | "version": "3.0.1", 32 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", 33 | "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==" 34 | }, 35 | "ansi-styles": { 36 | "version": "3.2.1", 37 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 38 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 39 | "requires": { 40 | "color-convert": "^1.9.0" 41 | } 42 | }, 43 | "asn1": { 44 | "version": "0.2.4", 45 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 46 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 47 | "requires": { 48 | "safer-buffer": "~2.1.0" 49 | } 50 | }, 51 | "assert-plus": { 52 | "version": "1.0.0", 53 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 54 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 55 | }, 56 | "asynckit": { 57 | "version": "0.4.0", 58 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 59 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 60 | }, 61 | "aws-sign2": { 62 | "version": "0.7.0", 63 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 64 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 65 | }, 66 | "aws4": { 67 | "version": "1.8.0", 68 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 69 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" 70 | }, 71 | "bcrypt-pbkdf": { 72 | "version": "1.0.2", 73 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 74 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 75 | "requires": { 76 | "tweetnacl": "^0.14.3" 77 | } 78 | }, 79 | "caseless": { 80 | "version": "0.12.0", 81 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 82 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 83 | }, 84 | "chalk": { 85 | "version": "2.4.2", 86 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 87 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 88 | "requires": { 89 | "ansi-styles": "^3.2.1", 90 | "escape-string-regexp": "^1.0.5", 91 | "supports-color": "^5.3.0" 92 | } 93 | }, 94 | "chardet": { 95 | "version": "0.7.0", 96 | "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", 97 | "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" 98 | }, 99 | "cli-cursor": { 100 | "version": "2.1.0", 101 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", 102 | "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", 103 | "requires": { 104 | "restore-cursor": "^2.0.0" 105 | } 106 | }, 107 | "cli-width": { 108 | "version": "2.2.0", 109 | "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", 110 | "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" 111 | }, 112 | "color-convert": { 113 | "version": "1.9.3", 114 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 115 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 116 | "requires": { 117 | "color-name": "1.1.3" 118 | } 119 | }, 120 | "color-name": { 121 | "version": "1.1.3", 122 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 123 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 124 | }, 125 | "combined-stream": { 126 | "version": "1.0.8", 127 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 128 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 129 | "requires": { 130 | "delayed-stream": "~1.0.0" 131 | } 132 | }, 133 | "commander": { 134 | "version": "2.20.0", 135 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", 136 | "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" 137 | }, 138 | "core-util-is": { 139 | "version": "1.0.2", 140 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 141 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 142 | }, 143 | "dashdash": { 144 | "version": "1.14.1", 145 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 146 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 147 | "requires": { 148 | "assert-plus": "^1.0.0" 149 | } 150 | }, 151 | "delayed-stream": { 152 | "version": "1.0.0", 153 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 154 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 155 | }, 156 | "ecc-jsbn": { 157 | "version": "0.1.2", 158 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 159 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 160 | "requires": { 161 | "jsbn": "~0.1.0", 162 | "safer-buffer": "^2.1.0" 163 | } 164 | }, 165 | "escape-string-regexp": { 166 | "version": "1.0.5", 167 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 168 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 169 | }, 170 | "extend": { 171 | "version": "3.0.2", 172 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 173 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 174 | }, 175 | "external-editor": { 176 | "version": "3.0.3", 177 | "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", 178 | "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", 179 | "requires": { 180 | "chardet": "^0.7.0", 181 | "iconv-lite": "^0.4.24", 182 | "tmp": "^0.0.33" 183 | } 184 | }, 185 | "extsprintf": { 186 | "version": "1.3.0", 187 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 188 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 189 | }, 190 | "fast-json-stable-stringify": { 191 | "version": "2.0.0", 192 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 193 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 194 | }, 195 | "figures": { 196 | "version": "2.0.0", 197 | "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", 198 | "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", 199 | "requires": { 200 | "escape-string-regexp": "^1.0.5" 201 | } 202 | }, 203 | "forever-agent": { 204 | "version": "0.6.1", 205 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 206 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 207 | }, 208 | "form-data": { 209 | "version": "2.3.3", 210 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 211 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 212 | "requires": { 213 | "asynckit": "^0.4.0", 214 | "combined-stream": "^1.0.6", 215 | "mime-types": "^2.1.12" 216 | } 217 | }, 218 | "fuzzy": { 219 | "version": "0.1.3", 220 | "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", 221 | "integrity": "sha1-THbsL/CsGjap3M+aAN+GIweNTtg=" 222 | }, 223 | "getpass": { 224 | "version": "0.1.7", 225 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 226 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 227 | "requires": { 228 | "assert-plus": "^1.0.0" 229 | } 230 | }, 231 | "har-schema": { 232 | "version": "2.0.0", 233 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 234 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 235 | }, 236 | "har-validator": { 237 | "version": "5.1.3", 238 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 239 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 240 | "requires": { 241 | "ajv": "^6.5.5", 242 | "har-schema": "^2.0.0" 243 | } 244 | }, 245 | "has-flag": { 246 | "version": "3.0.0", 247 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 248 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 249 | }, 250 | "http-signature": { 251 | "version": "1.2.0", 252 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 253 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 254 | "requires": { 255 | "assert-plus": "^1.0.0", 256 | "jsprim": "^1.2.2", 257 | "sshpk": "^1.7.0" 258 | } 259 | }, 260 | "iconv-lite": { 261 | "version": "0.4.24", 262 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 263 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 264 | "requires": { 265 | "safer-buffer": ">= 2.1.2 < 3" 266 | } 267 | }, 268 | "ini": { 269 | "version": "1.3.6", 270 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.6.tgz", 271 | "integrity": "sha512-IZUoxEjNjubzrmvzZU4lKP7OnYmX72XRl3sqkfJhBKweKi5rnGi5+IUdlj/H1M+Ip5JQ1WzaDMOBRY90Ajc5jg==" 272 | }, 273 | "inquirer": { 274 | "version": "6.4.1", 275 | "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.4.1.tgz", 276 | "integrity": "sha512-/Jw+qPZx4EDYsaT6uz7F4GJRNFMRdKNeUZw3ZnKV8lyuUgz/YWRCSUAJMZSVhSq4Ec0R2oYnyi6b3d4JXcL5Nw==", 277 | "requires": { 278 | "ansi-escapes": "^3.2.0", 279 | "chalk": "^2.4.2", 280 | "cli-cursor": "^2.1.0", 281 | "cli-width": "^2.0.0", 282 | "external-editor": "^3.0.3", 283 | "figures": "^2.0.0", 284 | "lodash": "^4.17.11", 285 | "mute-stream": "0.0.7", 286 | "run-async": "^2.2.0", 287 | "rxjs": "^6.4.0", 288 | "string-width": "^2.1.0", 289 | "strip-ansi": "^5.1.0", 290 | "through": "^2.3.6" 291 | } 292 | }, 293 | "inquirer-autocomplete-prompt": { 294 | "version": "1.0.1", 295 | "resolved": "https://registry.npmjs.org/inquirer-autocomplete-prompt/-/inquirer-autocomplete-prompt-1.0.1.tgz", 296 | "integrity": "sha512-Y4V6ifAu9LNrNjcEtYq8YUKhrgmmufUn5fsDQqeWgHY8rEO6ZAQkNUiZtBm2kw2uUQlC9HdgrRCHDhTPPguH5A==", 297 | "requires": { 298 | "ansi-escapes": "^3.0.0", 299 | "chalk": "^2.0.0", 300 | "figures": "^2.0.0", 301 | "run-async": "^2.3.0" 302 | } 303 | }, 304 | "is-fullwidth-code-point": { 305 | "version": "2.0.0", 306 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 307 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" 308 | }, 309 | "is-promise": { 310 | "version": "2.1.0", 311 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", 312 | "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" 313 | }, 314 | "is-typedarray": { 315 | "version": "1.0.0", 316 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 317 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 318 | }, 319 | "isstream": { 320 | "version": "0.1.2", 321 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 322 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 323 | }, 324 | "jsbn": { 325 | "version": "0.1.1", 326 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 327 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 328 | }, 329 | "json-schema-traverse": { 330 | "version": "0.4.1", 331 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 332 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 333 | }, 334 | "json-stringify-safe": { 335 | "version": "5.0.1", 336 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 337 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 338 | }, 339 | "jsprim": { 340 | "version": "1.4.2", 341 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", 342 | "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", 343 | "requires": { 344 | "assert-plus": "1.0.0", 345 | "extsprintf": "1.3.0", 346 | "json-schema": "0.4.0", 347 | "verror": "1.10.0" 348 | }, 349 | "dependencies": { 350 | "json-schema": { 351 | "version": "0.4.0", 352 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", 353 | "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" 354 | } 355 | } 356 | }, 357 | "lodash": { 358 | "version": "4.17.21", 359 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 360 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 361 | }, 362 | "mime-db": { 363 | "version": "1.40.0", 364 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 365 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" 366 | }, 367 | "mime-types": { 368 | "version": "2.1.24", 369 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 370 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", 371 | "requires": { 372 | "mime-db": "1.40.0" 373 | } 374 | }, 375 | "mimic-fn": { 376 | "version": "1.2.0", 377 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", 378 | "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" 379 | }, 380 | "moment": { 381 | "version": "2.29.4", 382 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", 383 | "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" 384 | }, 385 | "mute-stream": { 386 | "version": "0.0.7", 387 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", 388 | "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" 389 | }, 390 | "oauth-sign": { 391 | "version": "0.9.0", 392 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 393 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 394 | }, 395 | "onetime": { 396 | "version": "2.0.1", 397 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", 398 | "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", 399 | "requires": { 400 | "mimic-fn": "^1.0.0" 401 | } 402 | }, 403 | "os-tmpdir": { 404 | "version": "1.0.2", 405 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 406 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" 407 | }, 408 | "performance-now": { 409 | "version": "2.1.0", 410 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 411 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 412 | }, 413 | "psl": { 414 | "version": "1.1.33", 415 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.33.tgz", 416 | "integrity": "sha512-LTDP2uSrsc7XCb5lO7A8BI1qYxRe/8EqlRvMeEl6rsnYAqDOl8xHR+8lSAIVfrNaSAlTPTNOCgNjWcoUL3AZsw==" 417 | }, 418 | "punycode": { 419 | "version": "2.1.1", 420 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 421 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 422 | }, 423 | "qs": { 424 | "version": "6.5.3", 425 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", 426 | "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" 427 | }, 428 | "request": { 429 | "version": "2.88.0", 430 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 431 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 432 | "requires": { 433 | "aws-sign2": "~0.7.0", 434 | "aws4": "^1.8.0", 435 | "caseless": "~0.12.0", 436 | "combined-stream": "~1.0.6", 437 | "extend": "~3.0.2", 438 | "forever-agent": "~0.6.1", 439 | "form-data": "~2.3.2", 440 | "har-validator": "~5.1.0", 441 | "http-signature": "~1.2.0", 442 | "is-typedarray": "~1.0.0", 443 | "isstream": "~0.1.2", 444 | "json-stringify-safe": "~5.0.1", 445 | "mime-types": "~2.1.19", 446 | "oauth-sign": "~0.9.0", 447 | "performance-now": "^2.1.0", 448 | "qs": "~6.5.2", 449 | "safe-buffer": "^5.1.2", 450 | "tough-cookie": "~2.4.3", 451 | "tunnel-agent": "^0.6.0", 452 | "uuid": "^3.3.2" 453 | } 454 | }, 455 | "restore-cursor": { 456 | "version": "2.0.0", 457 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", 458 | "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", 459 | "requires": { 460 | "onetime": "^2.0.0", 461 | "signal-exit": "^3.0.2" 462 | } 463 | }, 464 | "run-async": { 465 | "version": "2.3.0", 466 | "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", 467 | "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", 468 | "requires": { 469 | "is-promise": "^2.1.0" 470 | } 471 | }, 472 | "rxjs": { 473 | "version": "6.5.2", 474 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", 475 | "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", 476 | "requires": { 477 | "tslib": "^1.9.0" 478 | } 479 | }, 480 | "safe-buffer": { 481 | "version": "5.1.2", 482 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 483 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 484 | }, 485 | "safer-buffer": { 486 | "version": "2.1.2", 487 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 488 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 489 | }, 490 | "signal-exit": { 491 | "version": "3.0.2", 492 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 493 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 494 | }, 495 | "sshpk": { 496 | "version": "1.16.1", 497 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 498 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 499 | "requires": { 500 | "asn1": "~0.2.3", 501 | "assert-plus": "^1.0.0", 502 | "bcrypt-pbkdf": "^1.0.0", 503 | "dashdash": "^1.12.0", 504 | "ecc-jsbn": "~0.1.1", 505 | "getpass": "^0.1.1", 506 | "jsbn": "~0.1.0", 507 | "safer-buffer": "^2.0.2", 508 | "tweetnacl": "~0.14.0" 509 | } 510 | }, 511 | "string-width": { 512 | "version": "2.1.1", 513 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 514 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 515 | "requires": { 516 | "is-fullwidth-code-point": "^2.0.0", 517 | "strip-ansi": "^4.0.0" 518 | }, 519 | "dependencies": { 520 | "strip-ansi": { 521 | "version": "4.0.0", 522 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 523 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 524 | "requires": { 525 | "ansi-regex": "^3.0.0" 526 | } 527 | } 528 | } 529 | }, 530 | "strip-ansi": { 531 | "version": "5.2.0", 532 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 533 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 534 | "requires": { 535 | "ansi-regex": "^4.1.0" 536 | }, 537 | "dependencies": { 538 | "ansi-regex": { 539 | "version": "4.1.1", 540 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", 541 | "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" 542 | } 543 | } 544 | }, 545 | "supports-color": { 546 | "version": "5.5.0", 547 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 548 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 549 | "requires": { 550 | "has-flag": "^3.0.0" 551 | } 552 | }, 553 | "through": { 554 | "version": "2.3.8", 555 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 556 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 557 | }, 558 | "tmp": { 559 | "version": "0.0.33", 560 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", 561 | "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", 562 | "requires": { 563 | "os-tmpdir": "~1.0.2" 564 | } 565 | }, 566 | "tough-cookie": { 567 | "version": "2.4.3", 568 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 569 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 570 | "requires": { 571 | "psl": "^1.1.24", 572 | "punycode": "^1.4.1" 573 | }, 574 | "dependencies": { 575 | "punycode": { 576 | "version": "1.4.1", 577 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 578 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 579 | } 580 | } 581 | }, 582 | "tslib": { 583 | "version": "1.10.0", 584 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", 585 | "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" 586 | }, 587 | "tunnel-agent": { 588 | "version": "0.6.0", 589 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 590 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 591 | "requires": { 592 | "safe-buffer": "^5.0.1" 593 | } 594 | }, 595 | "tweetnacl": { 596 | "version": "0.14.5", 597 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 598 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 599 | }, 600 | "uri-js": { 601 | "version": "4.2.2", 602 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 603 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 604 | "requires": { 605 | "punycode": "^2.1.0" 606 | } 607 | }, 608 | "uuid": { 609 | "version": "3.3.2", 610 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 611 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 612 | }, 613 | "verror": { 614 | "version": "1.10.0", 615 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 616 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 617 | "requires": { 618 | "assert-plus": "^1.0.0", 619 | "core-util-is": "1.0.2", 620 | "extsprintf": "^1.2.0" 621 | } 622 | } 623 | } 624 | } 625 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kimai2-cmd", 3 | "version": "1.3.1", 4 | "description": "Command line client for Kimai2", 5 | "main": "kimai2-cmd.js", 6 | "bin": { 7 | "kimai2-cmd": "kimai2-cmd.js" 8 | }, 9 | "scripts": { 10 | "start": "node kimai2-cmd.js", 11 | "build-nix": "pkg --out-path builds package.json", 12 | "build-current": "pkg --targets node10 --out-path builds kimai2-cmd.js", 13 | "copy-exe-to-rainmeter": "copy .\\builds\\kimai2-cmd.exe %userprofile%\\Documents\\Rainmeter\\Skins\\kimai2-cmd-rainmeter\\@Resources\\kimai2-cmd\\" 14 | }, 15 | "keywords": [ 16 | "kimai2", 17 | "command-line", 18 | "timetracker", 19 | "terminal" 20 | ], 21 | "author": "infeeeee", 22 | "license": "MIT", 23 | "dependencies": { 24 | "commander": "^2.20.0", 25 | "fuzzy": "^0.1.3", 26 | "ini": "^1.3.6", 27 | "inquirer": "^6.4.1", 28 | "inquirer-autocomplete-prompt": "^1.0.1", 29 | "moment": "^2.29.4", 30 | "request": "^2.88.0" 31 | }, 32 | "devDependencies": {}, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/infeeeee/kimai2-cmd.git" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/infeeeee/kimai2-cmd/issues" 39 | }, 40 | "homepage": "https://github.com/infeeeee/kimai2-cmd#readme", 41 | "pkg": { 42 | "targets": [ 43 | "node10-linux", 44 | "node10-macos" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /settings.ini.example: -------------------------------------------------------------------------------- 1 | [serversettings] 2 | kimaiurl=https://demo.kimai.org 3 | username=anna_admin 4 | password=api_kitten 5 | servertime=false 6 | 7 | [argos_bitbar] 8 | kimaipath=/path/to/kimai2-cmd-macos 9 | buttonlength=10 10 | 11 | [rainmeter] 12 | skindir=C:\Users\username\Documents\Rainmeter\Skins\kimai2-cmd-rainmeter\kimai2 13 | meterstyle=styleProjects --------------------------------------------------------------------------------