diff --git a/src/core/commands/config.js b/src/core/commands/config.js index 766f80b..e86f9d9 100755 --- a/src/core/commands/config.js +++ b/src/core/commands/config.js @@ -1,14 +1,13 @@ import {Command} from 'commander'; -import Config from '../file-config.js'; import Fs from '../filesystem.js'; -let config = new Config(process.cwd()); - -function cfgCmd() { +function cfgCmd(configLoader) { const cfgCmd = new Command('config', 'edit the configuration file in your default editor') .option('-l, --local', 'edit the local configuration file') .action((options, program) => { +let config = configLoader(); + if (program.opts().local) { config.assertLocalConfig(); } diff --git a/src/core/config.js b/src/core/config.js index 4cd3cb7..c53a460 100755 --- a/src/core/config.js +++ b/src/core/config.js @@ -10,7 +10,7 @@ const defaults = { token: false, project: false, from: "1970-01-01", - to: dayjs().format(), + to: dayjs().add(1, 'day').format('YYYY-MM-DD'), iids: false, closed: false, milestone: false, diff --git a/src/core/file-config.js b/src/core/file-config.js index cb58488..02969a0 100755 --- a/src/core/file-config.js +++ b/src/core/file-config.js @@ -2,11 +2,10 @@ import fs from 'fs'; import path from 'path'; import os from 'os'; import Config from './config.js'; -import { load as yamlLoad } from 'js-yaml'; +import { load as yamlLoad, dump as yamlDump } from 'js-yaml'; import hash from 'hash-sum'; import Fs from './filesystem.js'; import envPaths from 'env-paths'; -import Cli from './cli.js'; const readYaml = file => yamlLoad(fs.readFileSync(file, 'utf8')); @@ -18,12 +17,16 @@ class FileConfig extends Config { /** * construct * @param workDir + * @param load set to false to skip parsing the config files, e.g. when + * recovering from a broken config to still resolve its paths */ - constructor(workDir) { + constructor(workDir, recover) { super(); - this.assertGlobalConfig(); + this.assertGlobalConfig(recover); this.workDir = workDir; - this.data = Object.assign(this.data, this.localExists() ? this.parseLocal() : this.parseGlobal()); + if(!recover) { + this.data = Object.assign(this.data, this.localExists() ? this.parseLocal() : this.parseGlobal()); + } if (!fs.existsSync(this.frameDir)) fs.mkdirSync(this.frameDir, { recursive: true }); this.cache = { delete: this._cacheDelete, @@ -41,7 +44,8 @@ class FileConfig extends Config { try { return readYaml(this.global); } catch (e) { - Cli.error(`Error parsing configuration: "${this.global}"`, e); + e.message = `Error parsing configuration: "${this.global}": ${e.message}`; + throw e; } } @@ -50,24 +54,27 @@ class FileConfig extends Config { * @returns {Object} */ parseLocal() { + let local; try { - let local = Object.assign({extend: true}, readYaml(this.local)); - - if (local.extend === true) { - let global = this.parseGlobal(); - local = Object.assign(global ? global : {}, local); - } else if (local.extend) { - try { - local = Object.assign(readYaml(local.extend), local); - } catch (e) { - Cli.error(`Error parsing configuration: "${local.extend}"`, e); - } - } - - return local; + local = Object.assign({extend: true}, readYaml(this.local)); } catch (e) { - Cli.error(`Error parsing configuration: "${this.local}"`, e); + e.message = `Error parsing configuration: "${this.local}": ${e.message}`; + throw e; } + + if (local.extend === true) { + let global = this.parseGlobal(); + local = Object.assign(global ? global : {}, local); + } else if (local.extend) { + try { + local = Object.assign(readYaml(local.extend), local); + } catch (e) { + e.message = `Error parsing configuration: "${local.extend}": ${e.message}`; + throw e; + } + } + + return local; } localExists() { @@ -85,7 +92,7 @@ class FileConfig extends Config { } } - assertGlobalConfig() { + assertGlobalConfig(recover) { if(!fs.existsSync(this.globalDir) && fs.existsSync(this.oldGlobalDir)) { fs.renameSync(this.oldGlobalDir, this.globalDir); } @@ -94,7 +101,7 @@ class FileConfig extends Config { if (!fs.existsSync(this.globalDir)) fs.mkdirSync(this.globalDir, { recursive: true }); if (!fs.existsSync(this.cacheDir)) fs.mkdirSync(this.cacheDir, { recursive: true }); - if (!fs.existsSync(this.global)) fs.appendFileSync(this.global, ''); + if (recover && !fs.existsSync(this.global)) fs.appendFileSync(this.global, yamlDump(this.data)); } assertLocalConfig() { diff --git a/src/gtt.js b/src/gtt.js index 70805d0..cdeeab7 100755 --- a/src/gtt.js +++ b/src/gtt.js @@ -4,6 +4,8 @@ import version from './version.js'; import { program } from 'commander'; +import Config from './core/file-config.js'; +import Cli from './core/cli.js'; import config from './core/commands/config.js'; import start from './timekeeping/commands/start.js'; import create from './timekeeping/commands/create.js'; @@ -19,22 +21,37 @@ import delCmd from './timekeeping/commands/delete.js'; import archive from './timekeeping/commands/archive.js'; import report from './reporting/commands/report.js'; +/** + * build the config, only called once a command actually runs + * @param recover if true, fall back to a config with unparsed data instead + * of exiting when the config file fails to parse (used by the config + * command itself, so it stays usable to fix a broken file) + * @returns {Config} + */ +function loadConfig(recover = false) { + try { + return new Config(process.cwd(), recover); + } catch (e) { + Cli.error(`${e.message}`); + } +} + program .version(version) - .addCommand(start()) - .addCommand(create()) - .addCommand(status()) - .addCommand(stop()) - .addCommand(resume()) - .addCommand(cancel()) - .addCommand(list()) - .addCommand(log()) - .addCommand(sync()) - .addCommand(edit()) - .addCommand(delCmd()) - .addCommand(archive()) - .addCommand(report()) - .addCommand(config()) + .addCommand(start(loadConfig)) + .addCommand(create(loadConfig)) + .addCommand(status(loadConfig)) + .addCommand(stop(loadConfig)) + .addCommand(resume(loadConfig)) + .addCommand(cancel(loadConfig)) + .addCommand(list(loadConfig)) + .addCommand(log(loadConfig)) + .addCommand(sync(loadConfig)) + .addCommand(edit(loadConfig)) + .addCommand(delCmd(loadConfig)) + .addCommand(archive(loadConfig)) + .addCommand(report(loadConfig)) + .addCommand(config(() => loadConfig(true))) .parse(process.argv); diff --git a/src/reporting/commands/report.js b/src/reporting/commands/report.js index 624dad3..9c97dca 100755 --- a/src/reporting/commands/report.js +++ b/src/reporting/commands/report.js @@ -4,7 +4,6 @@ import {Command} from 'commander'; import dayjs from '../../core/dayjs.js'; import Cli from '../../core/cli.js'; import Args from '../../core/args.js'; -import Config from '../../core/file-config.js'; import Report from '../api/report.js'; import Owner from '../../core/owner.js'; import ReportCollection from '../api/reportCollection.js'; @@ -28,7 +27,7 @@ function collect(val, arr) { } -function report() { +function report(configLoader) { const report = new Command('report', 'generate a report for the given project and issues') .arguments('[project] [ids...]') .option('-e --type ', 'specify the query type: project, user, group') @@ -81,7 +80,7 @@ function report() { .action(async (project, ids, options, program) => { // init helpers -let config = new Config(process.cwd()); +let config = configLoader(); let args = new Args(program.args); // overwrite config with args and opts diff --git a/src/timekeeping/commands/archive.js b/src/timekeeping/commands/archive.js index 62cc0b9..e9c3ca7 100644 --- a/src/timekeeping/commands/archive.js +++ b/src/timekeeping/commands/archive.js @@ -1,12 +1,11 @@ import fs from 'fs'; import {Command} from 'commander'; -import Config from '../../core/file-config.js'; import Cli from '../../core/cli.js'; import Fs from '../../core/filesystem.js'; import Zip from '../../core/zip.js'; import Timekeeper from '../timekeeper.js'; -function archive() { +function archive(configLoader) { const archive = new Command('archive', 'archive synced time records into yearly zip files (filesYYYY.zip), one folder per month') .option('--year ', 'only archive the given year') .option('--verbose', 'show verbose output') @@ -14,7 +13,7 @@ function archive() { Cli.verbose = program.opts().verbose; -let config = new Config(process.cwd()), +let config = configLoader(), timekeeper = new Timekeeper(config), year = program.opts().year; diff --git a/src/timekeeping/commands/cancel.js b/src/timekeeping/commands/cancel.js index 36d937f..2a467a1 100755 --- a/src/timekeeping/commands/cancel.js +++ b/src/timekeeping/commands/cancel.js @@ -1,18 +1,17 @@ import {Command} from 'commander'; import pc from 'picocolors'; import dayjs from '../../core/dayjs.js'; -import Config from '../../core/file-config.js'; import Cli from '../../core/cli.js'; import Timekeeper from '../timekeeper.js'; -function cancel() { +function cancel(configLoader) { const cancel = new Command('cancel', 'cancel and discard active monitoring time') .option('--verbose', 'show verbose output') .action((options, program) => { Cli.verbose = program.opts().verbose; -let config = new Config(process.cwd()); +let config = configLoader(); let timekeeper = new Timekeeper(config); timekeeper.cancel() diff --git a/src/timekeeping/commands/create.js b/src/timekeeping/commands/create.js index 685b284..13dd006 100755 --- a/src/timekeeping/commands/create.js +++ b/src/timekeeping/commands/create.js @@ -1,12 +1,11 @@ import pc from 'picocolors'; import dayjs from '../../core/dayjs.js'; import {Command} from 'commander'; -import Config from '../../core/file-config.js'; import Cli from '../../core/cli.js'; import Timekeeper from '../timekeeper.js'; -function create() { +function create(configLoader) { const create = new Command('create', 'start monitoring time for the given project and create a new issue or merge request with the given title') .arguments('[project] [title]') .option('-t, --type ', 'specify resource type: issue, merge_request') @@ -15,7 +14,7 @@ function create() { Cli.verbose = program.opts().verbose; -let config = new Config(process.cwd()), +let config = configLoader(), timekeeper = new Timekeeper(config), type = program.opts().type ? program.opts().type : 'issue', title = program.args.length === 1 ? program.args[0] : program.args[1], diff --git a/src/timekeeping/commands/delete.js b/src/timekeeping/commands/delete.js index df92773..1075b71 100755 --- a/src/timekeeping/commands/delete.js +++ b/src/timekeeping/commands/delete.js @@ -1,16 +1,15 @@ import { Command } from 'commander'; import Frame from '../storage/frame.js'; -import Config from '../../core/file-config.js'; import Cli from '../../core/cli.js'; import Fs from '../../core/filesystem.js'; import pc from 'picocolors'; -function delCmd() { +function delCmd(configLoader) { const delCmd = new Command('delete', 'delete time record by the given id') .arguments('[id]') .action((id, opts, program) => { - let config = new Config(process.cwd()); + let config = configLoader(); if (!id && -Infinity === (id = Fs.newest(config.frameDir).name)) Cli.error('No record found.'); diff --git a/src/timekeeping/commands/edit.js b/src/timekeeping/commands/edit.js index b265eca..b4b0b55 100755 --- a/src/timekeeping/commands/edit.js +++ b/src/timekeeping/commands/edit.js @@ -1,5 +1,4 @@ import {Command} from 'commander'; -import Config from '../../core/file-config.js'; import Cli from '../../core/cli.js'; import Fs from '../../core/filesystem.js'; import Time from '../../core/time.js'; @@ -207,7 +206,7 @@ function showInteractiveMenu(frames) { }); } -function edit() { +function edit(configLoader) { const edit = new Command('edit', 'edit time record by the given id') .arguments('[id]') .option('-f, --following ', 'edit also the following (by ctime) of the given [id]') @@ -217,7 +216,7 @@ function edit() { .option('--this_week', 'only list entries for this week') .action((id, opts ,program) => { -let config = new Config(process.cwd()); +let config = configLoader(); let timeFormat = config.set('timeFormat', program.opts().time_format).get('timeFormat', 'log'); let timekeeper = new Timekeeper(config); const listSize = program.opts().listsize; diff --git a/src/timekeeping/commands/list.js b/src/timekeeping/commands/list.js index 9963c95..5da1ac6 100755 --- a/src/timekeeping/commands/list.js +++ b/src/timekeeping/commands/list.js @@ -3,11 +3,10 @@ import pc from 'picocolors'; import Table from 'cli-table'; -import Config from '../../core/file-config.js'; import cli from '../../core/cli.js'; import Timekeeper from '../timekeeper.js'; -function list() { +function list(configLoader) { const list = new Command('list', 'list all open issues or merge requests') .arguments('[project]') .option('--verbose', 'show verbose output') @@ -20,10 +19,10 @@ function list() { .option('--token ', 'API access token') .action((aproject, options, program) => { -let config = new Config(process.cwd()) +let config = configLoader() .set('url', program.opts().url) - .set('token', program.opts().token), - timekeeper = new Timekeeper(config), + .set('token', program.opts().token); +let timekeeper = new Timekeeper(config), type = program.opts().type ? program.opts().type : 'issue', project = program.args[0]; diff --git a/src/timekeeping/commands/log.js b/src/timekeeping/commands/log.js index 4fd0d3c..e1ee396 100755 --- a/src/timekeeping/commands/log.js +++ b/src/timekeeping/commands/log.js @@ -1,13 +1,12 @@ import {Command} from 'commander'; import pc from 'picocolors'; import dayjs from '../../core/dayjs.js'; -import Config from '../../core/file-config.js'; import Cli from '../../core/cli.js'; import Time from '../../core/time.js'; import Timekeeper from '../timekeeper.js'; import mergeRequest from '../api/mergeRequest.js'; -function log() { +function log(configLoader) { const log = new Command('log', 'log recorded time records') .option('--verbose', 'show verbose output') .option('--hours_per_day ', 'hours per day for human readable time formats') @@ -17,8 +16,8 @@ function log() { Cli.verbose = program.opts().verbose; -let config = new Config(process.cwd()).set('hoursPerDay', program.opts().hours_per_day), - timekeeper = new Timekeeper(config), +let config = configLoader().set('hoursPerDay', program.opts().hours_per_day); +let timekeeper = new Timekeeper(config), timeFormat = config.set('timeFormat', program.opts().time_format).get('timeFormat', 'log'); function toHumanReadable(input) { diff --git a/src/timekeeping/commands/resume.js b/src/timekeeping/commands/resume.js index 3e92261..885faf9 100755 --- a/src/timekeeping/commands/resume.js +++ b/src/timekeeping/commands/resume.js @@ -1,7 +1,6 @@ import { Command } from 'commander'; import pc from 'picocolors'; import dayjs from '../../core/dayjs.js'; -import Config from '../../core/file-config.js'; import Cli from '../../core/cli.js'; import Timekeeper from '../timekeeper.js'; import Fs from '../../core/filesystem.js'; @@ -23,7 +22,7 @@ function resumeFrame(timekeeper, frame) { .catch(error => Cli.error(error)); } -function resume() { +function resume(configLoader) { const resume = new Command('resume', 'resume monitoring time for last stopped record') .arguments('[project]') .option('--verbose', 'show verbose output') @@ -32,8 +31,8 @@ function resume() { Cli.verbose = program.opts().verbose; - let config = new Config(process.cwd()).set('project', project), - timekeeper = new Timekeeper(config); + let config = configLoader().set('project', project); + let timekeeper = new Timekeeper(config); if (!config.get('project')) Cli.error('No project set'); diff --git a/src/timekeeping/commands/start.js b/src/timekeeping/commands/start.js index 8082793..8a57b4c 100755 --- a/src/timekeeping/commands/start.js +++ b/src/timekeeping/commands/start.js @@ -1,12 +1,11 @@ import pc from 'picocolors'; import dayjs from '../../core/dayjs.js'; import {Command} from 'commander'; -import Config from '../../core/file-config.js'; import Cli from '../../core/cli.js'; import Timekeeper from '../timekeeper.js'; -function start() { +function start(configLoader) { const start = new Command('start', 'start monitoring time for the given project and resource id') .arguments('[project] [id]') .option('-t, --type ', 'specify resource type: issue, merge_request') @@ -18,7 +17,7 @@ function start() { Cli.verbose = program.opts().verbose; -let config = new Config(process.cwd()), +let config = configLoader(), timekeeper = new Timekeeper(config), type = program.opts().type ? program.opts().type : 'issue', id = program.args.length === 1 ? parseInt(program.args[0]) : parseInt(program.args[1]), diff --git a/src/timekeeping/commands/status.js b/src/timekeeping/commands/status.js index ac921a5..6da9a38 100755 --- a/src/timekeeping/commands/status.js +++ b/src/timekeeping/commands/status.js @@ -1,12 +1,11 @@ import {Command} from 'commander'; import pc from 'picocolors'; import dayjs from '../../core/dayjs.js'; -import Config from '../../core/file-config.js'; import Cli from '../../core/cli.js'; import Timekeeper from '../timekeeper.js'; -function status() { +function status(configLoader) { const status = new Command('status', 'shows if time monitoring is running') .option('--verbose', 'show verbose output') .option('-s', 'short output') @@ -14,8 +13,7 @@ function status() { Cli.verbose = program.opts().verbose; -let config = new Config(process.cwd()), - timekeeper = new Timekeeper(config); +let timekeeper = new Timekeeper(configLoader()); timekeeper.status() .then(frames => { diff --git a/src/timekeeping/commands/stop.js b/src/timekeeping/commands/stop.js index f3ff72f..b7bf362 100755 --- a/src/timekeeping/commands/stop.js +++ b/src/timekeeping/commands/stop.js @@ -1,20 +1,18 @@ import {Command} from 'commander'; import pc from 'picocolors'; import dayjs from '../../core/dayjs.js'; -import Config from '../../core/file-config.js'; import Cli from '../../core/cli.js'; import Timekeeper from '../timekeeper.js'; -function stop() { +function stop(configLoader) { const stop = new Command('stop', 'stop monitoring time') .option('--verbose', 'show verbose output') .action((options, program) => { Cli.verbose = program.opts().verbose; -let config = new Config(process.cwd()), - timekeeper = new Timekeeper(config); +let timekeeper = new Timekeeper(configLoader()); timekeeper.stop() .then(frames => { diff --git a/src/timekeeping/commands/sync.js b/src/timekeeping/commands/sync.js index 2415549..67b38c3 100755 --- a/src/timekeeping/commands/sync.js +++ b/src/timekeeping/commands/sync.js @@ -1,10 +1,9 @@ import {Command} from 'commander'; -import Config from '../../core/file-config.js'; import Cli from '../../core/cli.js'; import Timekeeper from '../timekeeper.js'; import Owner from '../../core/owner.js'; -function sync() { +function sync(configLoader) { const sync = new Command('sync', 'sync local time records to GitLab') .option('--url ', 'URL to GitLabs API') .option('--token ', 'API access token') @@ -13,10 +12,10 @@ function sync() { Cli.verbose = program.opts().verbose; -let config = new Config(process.cwd()) +let config = configLoader() .set('url', program.opts().url) - .set('token', program.opts().token), -timekeeper = new Timekeeper(config), + .set('token', program.opts().token); +let timekeeper = new Timekeeper(config), owner = new Owner(config); timekeeper.syncInit()