From 6724d6824c28cba835dd4be892e29a8eaf27f35c Mon Sep 17 00:00:00 2001 From: ndu Date: Sun, 5 Jul 2026 23:39:18 +0200 Subject: [PATCH 1/2] extract arg parsing from Cli into a dedicated Args class Cli mixed terminal-rendering concerns (bars, spinners, colors) with argument parsing (project/iids), even though only report.js used the parsing side. Splitting them lets Cli stay a pure static output helper. --- src/core/args.js | 46 ++++++++++++++++++++++++++++++++ src/core/cli.js | 40 --------------------------- src/reporting/commands/report.js | 7 ++--- 3 files changed, 50 insertions(+), 43 deletions(-) create mode 100644 src/core/args.js diff --git a/src/core/args.js b/src/core/args.js new file mode 100644 index 0000000..d868e6d --- /dev/null +++ b/src/core/args.js @@ -0,0 +1,46 @@ +/** + * Args helper + */ +class Args { + constructor(args) { + this.args = args; + this.data = []; + } + + /** + * parse the args and return the project argument + * @returns {*} + */ + project() { + if (!this.args[0] && !this.data.project) return null; + + if (this.data.project) return this.data.project; + + let projects = [...new Set(this.args.filter(arg => isNaN(new Number(arg))))]; + this.args = this.args.filter(arg => !projects.includes(arg)); + + if(projects.length == 0) + return null; + + return this.data.project = projects; + } + + /** + * parse the args and return an array of issues + * @returns {*} + */ + iids() { + if (this.data.iids) return this.data.iids; + + this.data.iids = [...new Set(this.args.map((issue) => { + if (issue.indexOf(',') === -1) return issue; + return issue.split(','); + }).flat())]; + + if (this.data.iids.length === 0) return null; + + return this.data.iids; + } +} + +export default Args; diff --git a/src/core/cli.js b/src/core/cli.js index 3b70aef..4256221 100755 --- a/src/core/cli.js +++ b/src/core/cli.js @@ -11,11 +11,6 @@ const spinner = { next: () => spinnerFrames[spinnerIndex++ % spinnerFrames.lengt * Cli helper */ class Cli { - constructor(args) { - this.args = args; - this.data = []; - } - /* * emojis */ @@ -223,41 +218,6 @@ class Cli { resolve(); }); } - - /** - * parse the args and return the project argument - * @returns {*} - */ - project() { - if (!this.args[0] && !this.data.project) return null; - - if (this.data.project) return this.data.project; - - let projects = [...new Set(this.args.filter(arg => isNaN(new Number(arg))))]; - this.args = this.args.filter(arg => !projects.includes(arg)); - - if(projects.length == 0) - return null; - - return this.data.project = projects; - } - - /** - * parse the args and return an array of issues - * @returns {*} - */ - iids() { - if (this.data.iids) return this.data.iids; - - this.data.iids = [...new Set(this.args.map((issue) => { - if (issue.indexOf(',') === -1) return issue; - return issue.split(','); - }).flat())]; - - if (this.data.iids.length === 0) return null; - - return this.data.iids; - } } export default Cli; \ No newline at end of file diff --git a/src/reporting/commands/report.js b/src/reporting/commands/report.js index 5f7b894..624dad3 100755 --- a/src/reporting/commands/report.js +++ b/src/reporting/commands/report.js @@ -3,6 +3,7 @@ import pc from 'picocolors'; 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'; @@ -81,14 +82,14 @@ function report() { // init helpers let config = new Config(process.cwd()); -let cli = new Cli(program.args); +let args = new Args(program.args); // overwrite config with args and opts config .set('url', program.opts().url) .set('token', program.opts().token) - .set('project', cli.project()) - .set('iids', cli.iids()) + .set('project', args.project()) + .set('iids', args.iids()) .set('from', program.opts().from) .set('to', program.opts().to) .set('closed', program.opts().closed) From 55d1f482d5ebe5aadbee125cf32fbd8a2c2302ea Mon Sep 17 00:00:00 2001 From: ndu Date: Sun, 5 Jul 2026 23:54:51 +0200 Subject: [PATCH 2/2] refactor all to user cli.js --- src/core/file-config.js | 10 ++++------ src/reporting/output/base.js | 2 +- src/timekeeping/commands/archive.js | 4 ++-- src/timekeeping/commands/cancel.js | 4 ++-- src/timekeeping/commands/create.js | 2 +- src/timekeeping/commands/delete.js | 2 +- src/timekeeping/commands/list.js | 4 ++-- src/timekeeping/commands/log.js | 10 +++++----- src/timekeeping/commands/resume.js | 2 +- src/timekeeping/commands/start.js | 2 +- src/timekeeping/commands/status.js | 8 ++++---- src/timekeeping/commands/stop.js | 4 ++-- src/timekeeping/storage/frameCollection.js | 3 +-- 13 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/core/file-config.js b/src/core/file-config.js index e412b30..cb58488 100755 --- a/src/core/file-config.js +++ b/src/core/file-config.js @@ -6,6 +6,7 @@ import { load as yamlLoad } 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')); @@ -40,8 +41,7 @@ class FileConfig extends Config { try { return readYaml(this.global); } catch (e) { - console.log(`Error parsing configuration: "${this.global}"`); - process.exit(1); + Cli.error(`Error parsing configuration: "${this.global}"`, e); } } @@ -60,15 +60,13 @@ class FileConfig extends Config { try { local = Object.assign(readYaml(local.extend), local); } catch (e) { - console.log(`Error parsing configuration: "${local.extend}"`); - process.exit(1); + Cli.error(`Error parsing configuration: "${local.extend}"`, e); } } return local; } catch (e) { - console.log(`Error parsing configuration: "${this.local}"`); - process.exit(1); + Cli.error(`Error parsing configuration: "${this.local}"`, e); } } diff --git a/src/reporting/output/base.js b/src/reporting/output/base.js index 9f61c3f..ca6d560 100755 --- a/src/reporting/output/base.js +++ b/src/reporting/output/base.js @@ -91,7 +91,7 @@ class Output { * render to stdout */ toStdOut() { - console.log(this.out); + process.stdout.write(`${this.out}\n`); } /** diff --git a/src/timekeeping/commands/archive.js b/src/timekeeping/commands/archive.js index a7b2c9d..62cc0b9 100644 --- a/src/timekeeping/commands/archive.js +++ b/src/timekeeping/commands/archive.js @@ -23,7 +23,7 @@ timekeeper.archiveInit() let years = year ? Object.keys(grouped).filter(y => y === year) : Object.keys(grouped); if (years.length === 0) { - console.log('Nothing to archive.'); + Cli.out('Nothing to archive.\n'); return; } @@ -42,7 +42,7 @@ timekeeper.archiveInit() zip.write(file); frames.forEach(frame => Fs.remove(frame.file)); - console.log(`Archived ${frames.length} record(s) into ${file}`); + Cli.out(`Archived ${frames.length} record(s) into ${file}\n`); }); }) .catch(error => Cli.error(error)); diff --git a/src/timekeeping/commands/cancel.js b/src/timekeeping/commands/cancel.js index d7ccc13..36d937f 100755 --- a/src/timekeeping/commands/cancel.js +++ b/src/timekeeping/commands/cancel.js @@ -19,9 +19,9 @@ timekeeper.cancel() .then(frames => { frames.forEach(frame => { if(!frame.resource.new) - return console.log(`Cancel project ${pc.magenta(frame.project)} ${pc.blue(frame.resource.type)} ${pc.blue(('#' + frame.resource.id))}, started ${pc.green(dayjs(frame.start).fromNow())}`) + return Cli.out(`Cancel project ${pc.magenta(frame.project)} ${pc.blue(frame.resource.type)} ${pc.blue(('#' + frame.resource.id))}, started ${pc.green(dayjs(frame.start).fromNow())}\n`) - console.log(`Cancel project ${pc.magenta(frame.project)} for new ${frame.resource.type} "${pc.blue((frame.resource.id))}", started ${pc.green(dayjs(frame.start).fromNow())}`) + Cli.out(`Cancel project ${pc.magenta(frame.project)} for new ${frame.resource.type} "${pc.blue((frame.resource.id))}", started ${pc.green(dayjs(frame.start).fromNow())}\n`) }) }) .catch(error => Cli.error(error)); diff --git a/src/timekeeping/commands/create.js b/src/timekeeping/commands/create.js index fd86cbb..685b284 100755 --- a/src/timekeeping/commands/create.js +++ b/src/timekeeping/commands/create.js @@ -28,7 +28,7 @@ if (!title) Cli.error('Wrong or missing title'); timekeeper.start(project, type, title) - .then(frame => console.log(`Starting project ${pc.magenta(config.get('project'))} and create ${type} "${pc.blue(title)}" at ${pc.green(dayjs().format('HH:mm'))}`)) + .then(frame => Cli.out(`Starting project ${pc.magenta(config.get('project'))} and create ${type} "${pc.blue(title)}" at ${pc.green(dayjs().format('HH:mm'))}\n`)) .catch(error => Cli.error(error)); } ); diff --git a/src/timekeeping/commands/delete.js b/src/timekeeping/commands/delete.js index 83e9d40..df92773 100755 --- a/src/timekeeping/commands/delete.js +++ b/src/timekeeping/commands/delete.js @@ -21,7 +21,7 @@ function delCmd() { } else { let frame = Frame.fromFile(config, file).stopMe(); Fs.remove(file); - console.log(`Deleting record ${pc.magenta(frame.id)}`); + Cli.out(`Deleting record ${pc.magenta(frame.id)}\n`); } } ); diff --git a/src/timekeeping/commands/list.js b/src/timekeeping/commands/list.js index bbcf185..9963c95 100755 --- a/src/timekeeping/commands/list.js +++ b/src/timekeeping/commands/list.js @@ -39,12 +39,12 @@ timekeeper.list(project, type, program.opts().closed ? 'closed' : 'opened', prog style : {compact : true, 'padding-left' : 1} }); if (tasks.length == 0) { - console.log("No issues or merge requests found."); + cli.out("No issues or merge requests found.\n"); } tasks.forEach(issue => { table.push([pc.magenta(issue.iid.toString()), pc.green(issue.title) + "\n" + pc.gray(issue.data.web_url), issue.state]) }) - console.log(table.toString()); + cli.out(`${table.toString()}\n`); }) .catch(error => cli.error(error)); diff --git a/src/timekeeping/commands/log.js b/src/timekeeping/commands/log.js index 2ae0551..4fd0d3c 100755 --- a/src/timekeeping/commands/log.js +++ b/src/timekeeping/commands/log.js @@ -33,12 +33,12 @@ function column(str, n){ } const logCSV = (frames, times) => { - console.log("frameId, project, issueid, date, starttime, endtime, duration (s), title, note"); + Cli.out("frameId, project, issueid, date, starttime, endtime, duration (s), title, note\n"); Object.keys(frames).sort().forEach(date => { if (!frames.hasOwnProperty(date)) return; frames[date].sort((a, b) => a.start.isBefore(b.start) ? -1 : 1) .forEach(frame => { - console.log(`${frame.id}, ${frame.project}, ${frame.resource.id}, ${dayjs(date).format('YYYY-MM-DD')}, ${frame.start.clone().format('HH:mm')}, ${frame.stop.clone().format('HH:mm')}, ${frame.duration}, ${frame.title!=null?frame.title:''}, ${frame.note!=null?frame.note:''}`); + Cli.out(`${frame.id}, ${frame.project}, ${frame.resource.id}, ${dayjs(date).format('YYYY-MM-DD')}, ${frame.start.clone().format('HH:mm')}, ${frame.stop.clone().format('HH:mm')}, ${frame.duration}, ${frame.title!=null?frame.title:''}, ${frame.note!=null?frame.note:''}\n`); }); }); }; @@ -61,7 +61,7 @@ const logCli = (frames, times) => { } - console.log(pc.green(`${dayjs(date).format('MMMM Do YYYY')} (${toHumanReadable(times[date])})`) + dayNote); + Cli.out(pc.green(`${dayjs(date).format('MMMM Do YYYY')} (${toHumanReadable(times[date])})`) + dayNote + '\n'); frames[date] .sort((a, b) => a.start.isBefore(b.start) ? -1 : 1) .forEach(frame => { @@ -70,8 +70,8 @@ const logCli = (frames, times) => { let issue = frame.resource.new ? column(`(new ${frame.resource.type + ' "' + frame.resource.id}")`, 70).bgBlue: `${pc.blue((frame.resource.type + ' #' + frame.resource.id).padEnd(20))}${column(frame.title!=null?frame.title:'', 50)}`; - console.log(` ${frame.id} ${pc.green(frame.start.clone().format('HH:mm'))} to ${pc.green(frame.stop.clone().format('HH:mm'))}\t${durationText}`+ - `${pc.magenta(column(frame.project, 50))}${issue}${frame.note!=null?frame.note:''}`); + Cli.out(` ${frame.id} ${pc.green(frame.start.clone().format('HH:mm'))} to ${pc.green(frame.stop.clone().format('HH:mm'))}\t${durationText}`+ + `${pc.magenta(column(frame.project, 50))}${issue}${frame.note!=null?frame.note:''}\n`); }); }); }; diff --git a/src/timekeeping/commands/resume.js b/src/timekeeping/commands/resume.js index ff7cfef..3e92261 100755 --- a/src/timekeeping/commands/resume.js +++ b/src/timekeeping/commands/resume.js @@ -19,7 +19,7 @@ function column(str, n) { function resumeFrame(timekeeper, frame) { timekeeper.resume(frame) - .then(frame => console.log(`Starting project ${pc.magenta(frame.project)} ${pc.blue(frame.resource.type)} ${pc.blue(('#' + frame.resource.id))} ${frame.note?frame.note:''} at ${pc.green(dayjs().format('HH:mm'))}`)) + .then(frame => Cli.out(`Starting project ${pc.magenta(frame.project)} ${pc.blue(frame.resource.type)} ${pc.blue(('#' + frame.resource.id))} ${frame.note?frame.note:''} at ${pc.green(dayjs().format('HH:mm'))}\n`)) .catch(error => Cli.error(error)); } diff --git a/src/timekeeping/commands/start.js b/src/timekeeping/commands/start.js index 5673172..8082793 100755 --- a/src/timekeeping/commands/start.js +++ b/src/timekeeping/commands/start.js @@ -41,7 +41,7 @@ if (!id) Cli.error('Wrong or missing issue/merge_request id'); timekeeper.start(project, type, id, note) - .then(frame => console.log(`Starting project ${pc.magenta(config.get('project'))} ${pc.blue(type)} ${pc.blue(('#' + id))} at ${pc.green(dayjs().format('HH:mm'))}`)) + .then(frame => Cli.out(`Starting project ${pc.magenta(config.get('project'))} ${pc.blue(type)} ${pc.blue(('#' + id))} at ${pc.green(dayjs().format('HH:mm'))}\n`)) .catch(error => Cli.error(error)); } ); diff --git a/src/timekeeping/commands/status.js b/src/timekeeping/commands/status.js index 601c325..ac921a5 100755 --- a/src/timekeeping/commands/status.js +++ b/src/timekeeping/commands/status.js @@ -21,16 +21,16 @@ timekeeper.status() .then(frames => { if (frames.length === 0) { if (program.opts().s) { - console.log('gtt idle '); + Cli.out('gtt idle \n'); }else { - console.log('No projects are started right now.'); + Cli.out('No projects are started right now.\n'); } return; } if (program.opts().s) { - frames.forEach(frame => console.log(`${pc.magenta(frame.project)} ${pc.blue(frame.resource.type)} ${pc.blue(('#' + frame.resource.id))} ${pc.green(dayjs(frame.start).fromNow())} (id: ${frame.id})`)); + frames.forEach(frame => Cli.out(`${pc.magenta(frame.project)} ${pc.blue(frame.resource.type)} ${pc.blue(('#' + frame.resource.id))} ${pc.green(dayjs(frame.start).fromNow())} (id: ${frame.id})\n`)); } else { - frames.forEach(frame => console.log(`Project ${pc.magenta(frame.project)} ${pc.blue(frame.resource.type)} ${pc.blue(('#' + frame.resource.id))} ${frame.note?frame.note:''} is running, started ${pc.green(dayjs(frame.start).fromNow())} (id: ${frame.id})`)); + frames.forEach(frame => Cli.out(`Project ${pc.magenta(frame.project)} ${pc.blue(frame.resource.type)} ${pc.blue(('#' + frame.resource.id))} ${frame.note?frame.note:''} is running, started ${pc.green(dayjs(frame.start).fromNow())} (id: ${frame.id})\n`)); } }) .catch(error => Cli.error('Could not read frames.', error)); diff --git a/src/timekeeping/commands/stop.js b/src/timekeeping/commands/stop.js index 3dcd953..f3ff72f 100755 --- a/src/timekeeping/commands/stop.js +++ b/src/timekeeping/commands/stop.js @@ -20,9 +20,9 @@ timekeeper.stop() .then(frames => { frames.forEach(frame => { if(!frame.resource.new) - return console.log(`Stopping project ${pc.magenta(frame.project)} ${pc.blue(frame.resource.type)} ${pc.blue(('#' + frame.resource.id))}, started ${pc.green(dayjs(frame.start).fromNow())} (id: ${frame.id})`) + return Cli.out(`Stopping project ${pc.magenta(frame.project)} ${pc.blue(frame.resource.type)} ${pc.blue(('#' + frame.resource.id))}, started ${pc.green(dayjs(frame.start).fromNow())} (id: ${frame.id})\n`) - console.log(`Stopping project ${pc.magenta(frame.project)} for new ${frame.resource.type} "${pc.blue((frame.resource.id))}", started ${pc.green(dayjs(frame.start).fromNow())} (id: ${frame.id})`) + Cli.out(`Stopping project ${pc.magenta(frame.project)} for new ${frame.resource.type} "${pc.blue((frame.resource.id))}", started ${pc.green(dayjs(frame.start).fromNow())} (id: ${frame.id})\n`) }); }) .catch(error => Cli.error(error)); diff --git a/src/timekeeping/storage/frameCollection.js b/src/timekeeping/storage/frameCollection.js index 5cc78d4..79332c7 100755 --- a/src/timekeeping/storage/frameCollection.js +++ b/src/timekeeping/storage/frameCollection.js @@ -12,8 +12,7 @@ class FrameCollection { try { return Frame.fromFile(this.config, Fs.join(this.config.frameDir, file.name)); } catch (e) { - console.log(e); - throw new Error(`Error parsing frame file: ${file.name}`) + throw new Error(`Error parsing frame file: ${file.name}`, { cause: e }); } }) .filter(frame => frame);