@beauraines/toggl-cli 0.10.22 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [2.0.0](https://github.com/beauraines/toggl-cli/compare/v0.10.22...v2.0.0) (2023-07-08)
6
+
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ * ready for version 1.0.0
11
+ Reads configuration (API token, timezone, default workspace and projects) from `.toggl-cli.json` from the users home. Adds a command to create the configuration file.
12
+
13
+ ### Features
14
+
15
+ * Adds configuration file ([#91](https://github.com/beauraines/toggl-cli/issues/91)) ([b7d70bf](https://github.com/beauraines/toggl-cli/commit/b7d70bfdb60aa9353e4da18f85e8b279ddf8bf34))
16
+
5
17
  ### [0.10.22](https://github.com/beauraines/toggl-cli/compare/v0.10.21...v0.10.22) (2023-07-08)
6
18
 
7
19
 
package/README.md CHANGED
@@ -8,65 +8,52 @@ This was made possible because [saintedlama](https://github.com/saintedlama) had
8
8
 
9
9
  ## Configuration
10
10
 
11
- 1. Configure your environment, with environment variables or a `.env` file in the project root. Eventually, these will be read from a config file as an alternative
11
+ ### Configuration File
12
+
13
+ You can create the `.toggl-cli.json` in your home directory manually using the template below or with the `toggl create-config` command. The values can be found in your Toggl [profile](https://track.toggl.com/profile) and from the URL for your account settings. The current workspace id is `https://track.toggl.com/${workspace_id}}/settings/general`. Don't forget to change the permissions `chmod 600` so that only you can read/write the configuration file as it has your API token.
14
+
15
+ ```json
16
+ {
17
+ "api_token": "",
18
+ "default_workspace_id": "",
19
+ "timezone": "",
20
+ "default_project_id": ""
21
+ }
22
+ ```
23
+ ### Environment Variables
24
+
25
+ Configure your environment, with environment variables or a `.env` file in the project root. Environment variables will take precedence over a config file.
26
+
12
27
  1. `TOGGL_API_TOKEN` (required)
13
28
  2. `TOGGL_DEFAULT_WORKSPACE_ID` (required)
14
29
  3. `TOGGL_DEFAULT_PROJECT_ID` (optional)
15
30
  4. `TOGGL_TIMEZONE=America/Los_Angeles` (defaults to `America/New_York`)
16
- ## Dependencies
17
-
18
- 1. toggl-client - Used from [saintedlama/toggl-client](https://github.com/saintedlama/toggl-client) repository as the latest code hasn't been released to npm
19
- 3. dotenv
20
- 4. yargs
21
-
22
31
 
23
32
 
24
33
  ## Features
25
34
 
26
- | Feature | Available | Comments |
27
- | ------------------------------------ | --------- | ----------------------------------------------------------- |
28
- | Start time entry | ✅ | |
29
- | Start time entry with description | ✅ | |
30
- | Start time entry with project | ✅ | |
31
- | stop time entry | ✅ | |
32
- | Continue named time entry | ✅ | |
33
- | Report today by project | ✅ | |
34
- | Report this week by project by day | ✅ | |
35
- | Edit time entry | ✅ | |
36
- | Use config from file | | |
37
- | Save config to file | | |
38
- | Refactor: Display and format modules | | |
39
- | Client: reset PAT | | |
40
- | Client: other user feature? | | |
41
- | Client: specify client name | | |
42
- | Colorize output | | |
43
- | Better table output | ✅ | |
44
- | List recent time entries | ✅ | |
45
- | Command line completion | ✅ | [#6](https://github.com/beauraines/toggl-cli-node/issues/6) |
46
-
47
- ## Development Road Map
48
-
49
- Priority order... I think.
50
-
51
- 1. ~today - improve the output~
52
- 2. ~weekly - improve the output format~
53
- 3. ~now - format the display time entry output~
54
- 4. ~toggl continue~
55
- 5. ~duration to display helper function?~
56
- 6. read configuration (token, default workspace) from files (client and utils)
57
- 7. now - update running description, project, start time
58
- 8. now - update start time with overlap detection and adjust prior time entry
59
- 9. project list - format the output, group by client
60
- 10. Add ability to lookup project by name not just id
61
- 11. start - add project name to output, add time started
62
- 12. toggl workspace add - this command is not yet supported.
63
- 13. toggl workspace list - improve output
64
- 14. colorized output
65
- 15. config - some way to save /me information to a file
66
- 1. default workspace?
67
- 2. API key?
68
- 3. display formats?
69
- 16. toggl project add - this command is not yet supported.
35
+ | Feature | Available | Comments |
36
+ | ---------------------------------------- | --------- | ----------------------------------------------------------- |
37
+ | Start time entry | ✅ | |
38
+ | Start time entry with description | ✅ | |
39
+ | Start time entry with project | ✅ | |
40
+ | stop time entry | ✅ | |
41
+ | Continue named time entry | ✅ | |
42
+ | Report today by project | ✅ | |
43
+ | Report this week by project by day | ✅ | |
44
+ | Edit current time entry | ✅ | |
45
+ | Use config from file || |
46
+ | Save config to file || |
47
+ | Refactor: Display and format modules | | |
48
+ | Client: reset PAT | | Is this really necessary? |
49
+ | Client: specify client name || |
50
+ | Colorize output | | |
51
+ | Better table output || |
52
+ | List recent time entries | ✅ | |
53
+ | Command line completion | ✅ | [#6](https://github.com/beauraines/toggl-cli-node/issues/6) |
54
+ | Delete time entry by id | | |
55
+ | Edit other time entries than the current | | |
56
+ | Display earlier time entries | | |
70
57
 
71
58
 
72
59
 
package/client.js CHANGED
@@ -1,29 +1,33 @@
1
1
  import dotenv from 'dotenv'
2
2
  import togglClient from 'toggl-client'
3
+ import { readConfig } from './config.js'
3
4
  dotenv.config()
5
+ import debugClient from 'debug'
6
+ const debug = debugClient('toggl-cli-client')
4
7
 
5
- export default function () {
6
- if (!process.env.TOGGL_API_TOKEN) {
7
- console.log('TOGGL_API_TOKEN environment variable is not set.')
8
- console.log('For development, it can be set in the .env file in the project root')
8
+
9
+ export default async function () {
10
+ let conf
11
+ try {
12
+ conf = await readConfig('.toggl-cli.json')
13
+ debug(conf)
14
+ } catch (error) {
15
+ console.error('Using config from environment variables or create one with the create-config command')
9
16
  }
10
17
 
11
- // TODO Try to read rc file
18
+ const apiToken = process.env.TOGGL_API_TOKEN || conf?.api_token
19
+ debug(apiToken)
12
20
 
13
- // FIXME apiToken is not needed
14
- const apiToken = process.env.TOGGL_API_TOKEN
15
- const client = togglClient()
16
- // const client = togglClient({ apiToken });
17
- // ? Why doesn't a try/catch block work?
18
- // try {
19
- // const client = togglClient({ apiToken });
20
- // } catch (error) {
21
- // console.error(error);
22
- // }
23
- if (!client) {
24
- console.error('There was a problem')
25
- process.exit(1)
26
- }
21
+ let client
22
+ try {
23
+ client = togglClient({ apiToken });
24
+ } catch (error) {
25
+ console.error(error.message);
26
+ console.error('There was a problem')
27
+ process.exit(1)
28
+ }
27
29
 
28
30
  return client
29
31
  }
32
+
33
+
package/cmds/continue.mjs CHANGED
@@ -16,7 +16,7 @@ export const builder = {
16
16
  }
17
17
 
18
18
  export const handler = async function (argv) {
19
- const client = Client()
19
+ const client = await Client()
20
20
  const timeEntries = await client.timeEntries.list(
21
21
  {
22
22
  start_date: dayjs().subtract(14, 'days').toISOString(),
@@ -0,0 +1,28 @@
1
+ import { createConfig } from '../config.js'
2
+ import debugClient from 'debug';
3
+
4
+ const debug = debugClient('toggl-cli-create-config');
5
+
6
+ export const command = 'create-config'
7
+ export const desc = 'Creates a boilerplate .toggl-cli.json configuration file in your home directory.'
8
+ export const builder = {}
9
+
10
+ export const handler = async function (argv) {
11
+ const configProps = [
12
+ "api_token",
13
+ "default_workspace_id",
14
+ "timezone",
15
+ "default_project_id"
16
+ ]
17
+
18
+ let configFile
19
+ try {
20
+ configFile = await createConfig('.toggl-cli.json',configProps)
21
+ console.log(`Configuration file written to ${configFile} in your home directory`)
22
+ console.log(`Edit with your user information from Toggl.`)
23
+ } catch (error) {
24
+ console.error(error.message)
25
+ process.exit(1)
26
+ }
27
+
28
+ }
@@ -9,7 +9,7 @@ export const desc = 'Displays the current running time entry'
9
9
  export const builder = {}
10
10
 
11
11
  export const handler = async function (argv) {
12
- const client = new Client()
12
+ const client = await Client()
13
13
  const currentTimeEntry = await client.timeEntries.current()
14
14
  if (currentTimeEntry) {
15
15
  debug(currentTimeEntry)
package/cmds/edit.mjs CHANGED
@@ -27,7 +27,7 @@ export const handler = async function (argv) {
27
27
  yargs().help()
28
28
  yargs().exit(1, new Error('At least one option must be provided, description, project, start or end'))
29
29
  }
30
- const client = new Client()
30
+ const client = await Client()
31
31
  const currentTimeEntry = await client.timeEntries.current()
32
32
  debug(currentTimeEntry)
33
33
  const params = {}
package/cmds/index.mjs CHANGED
@@ -10,6 +10,7 @@ import * as startTimeEntry from './startTimeEntry.mjs'
10
10
  import * as stopTimeEntry from './stopTimeEntry.mjs'
11
11
  import * as today from './today.mjs'
12
12
  import * as weekly from './weekly.mjs'
13
+ import * as createConfig from './create-config.mjs'
13
14
  export const commands = [
14
15
  continueEntry,
15
16
  current,
@@ -22,5 +23,6 @@ export const commands = [
22
23
  today,
23
24
  web,
24
25
  weekly,
25
- workspace
26
+ workspace,
27
+ createConfig
26
28
  ]
package/cmds/ls.mjs CHANGED
@@ -16,7 +16,7 @@ export const builder = {
16
16
 
17
17
  export const handler = async function (argv) {
18
18
  debug(argv)
19
- const client = Client()
19
+ const client = await Client()
20
20
  const days = argv.today ? 0 : argv.days
21
21
  let timeEntries = await client.timeEntries.list({
22
22
  start_date: dayjs().subtract(days, 'days').startOf('day').toISOString(),
package/cmds/me.mjs CHANGED
@@ -6,7 +6,7 @@ export const desc = 'Displays the current user'
6
6
  export const builder = {}
7
7
 
8
8
  export const handler = async function (argv) {
9
- const client = Client()
9
+ const client = await Client()
10
10
  const currentUser = await client.user.current()
11
11
  // console.log(currentUser);
12
12
 
@@ -8,7 +8,7 @@ export const builder = {
8
8
  }
9
9
 
10
10
  export const handler = async function (argv) {
11
- const client = Client()
11
+ const client = await Client()
12
12
  const workspaces = await client.workspaces.list()
13
13
 
14
14
  const workspace = workspaces[0]
@@ -6,7 +6,7 @@ export const desc = 'Stops the current running time entry'
6
6
  export const builder = {}
7
7
 
8
8
  export const handler = async function (argv) {
9
- const client = Client()
9
+ const client = await Client()
10
10
  const currentTimeEntry = await client.timeEntries.current()
11
11
  if (currentTimeEntry) {
12
12
  const stopped = await client.timeEntries.stop(currentTimeEntry)
package/cmds/today.mjs CHANGED
@@ -13,7 +13,7 @@ export const desc = 'Reports today\'s activities by project'
13
13
  export const builder = {}
14
14
 
15
15
  export const handler = async function (argv) {
16
- const client = Client()
16
+ const client = await Client()
17
17
  const workspace = await getWorkspace()
18
18
  const projects = await getProjects(workspace.id)
19
19
  const params = {
package/cmds/weekly.mjs CHANGED
@@ -20,7 +20,7 @@ export const builder = {
20
20
  }
21
21
 
22
22
  export const handler = async function (argv) {
23
- const client = Client()
23
+ const client = await Client()
24
24
  const workspace = await getWorkspace()
25
25
 
26
26
  const weekOffset = argv.previous ? 1 : 0
@@ -8,7 +8,7 @@ export const builder = {
8
8
  }
9
9
 
10
10
  export const handler = async function (argv) {
11
- const client = Client()
11
+ const client = await Client()
12
12
  const workspaces = await client.workspaces.list()
13
13
  console.log(workspaces)
14
14
  }
package/config.js ADDED
@@ -0,0 +1,82 @@
1
+ import fs from 'fs';
2
+ import {homedir} from 'os';
3
+ import path from 'path';
4
+
5
+ /**
6
+ * Reads the specified config file from the users home directory
7
+ *
8
+ * @param {string} configFile file name for the JSON config file, from the users home directory
9
+ *
10
+ * @returns {Promise} the configuration object
11
+ *
12
+ * @throws {Error} if configuration file not found.
13
+ */
14
+ export const readConfig = async (configFile) => {
15
+ configFile = path.join(homedir(),configFile)
16
+ let config
17
+ if ( await fs.existsSync(configFile) ) {
18
+ config = fs.readFileSync(configFile,
19
+ { encoding: 'utf8', flag: 'r' });
20
+ config = JSON.parse(config);
21
+ } else {
22
+ throw new Error(`Config file not found. You must create one using the config command`)
23
+ }
24
+
25
+ return config
26
+ }
27
+
28
+ /**
29
+ * Validates the config file to ensure that it has all of the required properties specified. This
30
+ * is only schema validation, it does not check if the properties are valid values or data types.
31
+ *
32
+ * @param {string} configFile file name for the JSON config file, from the users home directory
33
+ * @param {Array} configProps an array of properties that defines a valid config file
34
+ *
35
+ * @throws {Configuration file not found}
36
+ * @throws {Invalid configuration file}
37
+ * @returns {boolean}
38
+ */
39
+ export const validateConfig = async (configFile, configProps) => {
40
+ configFile = path.join(homedir(),configFile)
41
+ if (! fs.existsSync(configFile) ) {
42
+ throw new Error('Configuration file not found')
43
+ }
44
+ let config
45
+ config = fs.readFileSync(configFile,
46
+ { encoding: 'utf8', flag: 'r' });
47
+ config = JSON.parse(config);
48
+ // Check for properties
49
+ let validConfig = true
50
+ for (const key of configProps) {
51
+ validConfig = config[key] ? true : false
52
+ }
53
+
54
+ if (!validConfig) {
55
+ throw Error('Invalid configuration file')
56
+ }
57
+ return validConfig
58
+ }
59
+
60
+ /**
61
+ * Creates a boilerplate config file in the user's home directory
62
+ *
63
+ * @param {string} configFile file name for the JSON config file, from the users home directory
64
+ * @param {Array} configProps an array of properties that defines a valid config file
65
+ *
66
+ * @returns {string} Fully qualified path to configuration file
67
+ *
68
+ * @throws {error} if the file already exists
69
+ *
70
+ */
71
+ export const createConfig = async (configFile,configProps) => {
72
+ configFile = path.join(homedir(),configFile)
73
+ if ( await fs.existsSync(configFile) ) {
74
+ throw Error('File already exists. Will not overwrite')
75
+ }
76
+ let config = {}
77
+ for (const key of configProps) {
78
+ config[key] = ''
79
+ }
80
+ fs.writeFileSync(configFile,JSON.stringify(config, null, 2))
81
+ return configFile
82
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beauraines/toggl-cli",
3
- "version": "0.10.22",
4
- "description": "",
3
+ "version": "2.0.0",
4
+ "description": "CLI client for Toggl Time Tracker",
5
5
  "main": "cli.js",
6
6
  "bin": {
7
7
  "toggl": "./cli.js"
@@ -4,7 +4,7 @@ import dotenv from 'dotenv'
4
4
  dotenv.config()
5
5
 
6
6
  async function main () {
7
- const client = new Client()
7
+ const client = await Client()
8
8
  const currentTimeEntry = await client.timeEntries.current()
9
9
  await displayTimeEntry(currentTimeEntry)
10
10
  }
@@ -28,7 +28,7 @@ async function main () {
28
28
  }
29
29
 
30
30
  async function createTimeEntry (params) {
31
- // const client = Client()
31
+ // const client = await Client()
32
32
  const timeEntry = await client.timeEntries.create(
33
33
  {
34
34
  description: params.description,
package/utils.js CHANGED
@@ -4,43 +4,49 @@ import dayjs from 'dayjs'
4
4
  import utc from "dayjs/plugin/utc.js";
5
5
  import timezone from 'dayjs/plugin/timezone.js';
6
6
  import duration from 'dayjs/plugin/duration.js';
7
+ import { readConfig } from './config.js'
7
8
  dayjs.extend(utc);
8
9
  dayjs.extend(timezone);
9
10
  dayjs.extend(duration);
10
11
 
11
- // TODO read from file or GET /me
12
- export const defaultWorkspaceId = process.env.TOGGL_DEFAULT_WORKSPACE_ID
12
+ let conf
13
+ try {
14
+ conf = await readConfig('.toggl-cli.json')
15
+ } catch (error) {
16
+ console.error('Using config from environment variables or create one with the create-config command')
17
+ }
18
+
19
+ export const defaultWorkspaceId = process.env.TOGGL_DEFAULT_WORKSPACE_ID || conf.default_workspace_id
13
20
 
14
- // TODO read from file or ENV
15
- export const defaultProjectId = process.env.TOGGL_DEFAULT_PROJECT_ID
21
+ export const defaultProjectId = process.env.TOGGL_DEFAULT_PROJECT_ID || conf.default_project_id
16
22
 
17
23
  export const getProjects = async function (workspaceId) {
18
- const client = Client()
24
+ const client = await Client()
19
25
  const projects = await client.workspaces.projects(workspaceId)
20
26
  const activeProjects = projects.filter(x => x.active)
21
27
  return activeProjects
22
28
  }
23
29
 
24
30
  export const getWorkspace = async function () {
25
- const client = Client()
31
+ const client = await Client()
26
32
  const workspaces = await client.workspaces.list()
27
33
  return workspaces[0]
28
34
  }
29
35
 
30
36
  export const getProjectByName = async function (workspaceId, string) {
31
- const client = Client()
37
+ const client = await Client()
32
38
  const projects = await client.workspaces.projects(workspaceId)
33
39
  return projects.find(x => x.name.toLowerCase().includes(string.toLowerCase()))
34
40
  }
35
41
 
36
42
  export const getProjectById = async function (workspaceId, projectId) {
37
- const client = Client()
43
+ const client = await Client()
38
44
  const projects = await client.workspaces.projects(workspaceId)
39
45
  return projects.find(x => x.id == projectId)
40
46
  }
41
47
 
42
48
  export const createTimeEntry = async function (params) {
43
- const client = Client()
49
+ const client = await Client()
44
50
 
45
51
  const timeEntry = await client.timeEntries.create(
46
52
  {
@@ -90,7 +96,7 @@ export const formatDurationAsTime = function (milliseconds) {
90
96
  * @returns {String}
91
97
  */
92
98
  export const convertUtcTime = function (dateTime) {
93
- const tz = process.env.TOGGL_TIMEZONE || 'America/New_York'
99
+ const tz = process.env.TOGGL_TIMEZONE || conf?.timezone || 'America/New_York'
94
100
  return dayjs(dateTime).tz(tz).format('YYYY-MM-DD HH:mm')
95
101
  }
96
102
 
@@ -119,7 +125,7 @@ export const displayTimeEntry = async function (timeEntry) {
119
125
 
120
126
  console.info(`Project: ${project?.name} (#${timeEntry.pid})`);
121
127
 
122
- const tz = process.env.TOGGL_TIMEZONE || 'America/New_York'
128
+ const tz = process.env.TOGGL_TIMEZONE || conf?.timezone || 'America/New_York'
123
129
  const startTimeFormatted = dayjs(timeEntry.start).tz(tz).format('YYYY-MM-DD HH:mm')
124
130
  const stopTimeFormatted = timeEntry.stop ? dayjs(timeEntry.stop).tz(tz).format('YYYY-MM-DD HH:mm') : 'Currently Running'
125
131