├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── config.yml
│ └── feature_request.md
├── release-drafter.yml
└── workflows
│ └── merge_to_main.yaml
├── .gitignore
├── .vscode
├── launch.json
├── settings.json
└── tasks.json
├── .vscodeignore
├── LICENSE
├── README.md
├── codetime.readme.md
├── custom.d.ts
├── eslint.config.mjs
├── images
├── 404-image.png
├── app-icon-blue.png
├── codetime-b-90.png
├── codetime-c-128.png
└── codetime-g-30.png
├── package.json
├── resources
├── README.md
├── dark
│ └── add.svg
└── light
│ └── add.svg
├── src
├── Constants.ts
├── DataController.ts
├── Util.ts
├── auth
│ └── AuthProvider.ts
├── cache
│ └── CacheManager.ts
├── command-helper.ts
├── events
│ └── KpmItems.ts
├── extension.ts
├── http
│ └── HttpClient.ts
├── local
│ └── 404.ts
├── managers
│ ├── ChangeStateManager.ts
│ ├── ConfigManager.ts
│ ├── ExecManager.ts
│ ├── ExtensionManager.ts
│ ├── FileManager.ts
│ ├── FlowManager.ts
│ ├── KpmManager.ts
│ ├── LocalStorageManager.ts
│ ├── ProgressManager.ts
│ ├── PromptManager.ts
│ ├── ScreenManager.ts
│ ├── SlackManager.ts
│ ├── StatusBarManager.ts
│ ├── SummaryManager.ts
│ ├── SyncManger.ts
│ ├── TrackerManager.ts
│ └── WebViewManager.ts
├── menu
│ └── AccountManager.ts
├── message_handlers
│ ├── authenticated_plugin_user.ts
│ ├── current_day_stats_update.ts
│ ├── flow_score.ts
│ ├── flow_state.ts
│ └── integration_connection.ts
├── model
│ ├── CodeTimeSummary.ts
│ ├── Project.ts
│ └── models.ts
├── notifications
│ └── endOfDay.ts
├── repo
│ ├── GitUtil.ts
│ └── KpmRepoManager.ts
├── sidebar
│ └── CodeTimeView.ts
├── user
│ └── OnboardManager.ts
└── websockets.ts
├── swdc-vscode-2.8.8.vsix
├── test
├── extension.test.ts
└── index.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
12 | **Describe the bug**
13 | A clear and concise description of what the bug is.
14 | ```paste below
15 |
16 | ```
17 | **ISSUE TYPE**
18 | - Bug Report
19 |
20 | **Steps to reproduce**
21 |
22 |
23 | 1. Go to '...'
24 | 2. Click on '....'
25 | 3. Scroll down to '....'
26 | 4. See error
27 |
28 |
29 |
30 | **Expected behavior**
31 | A clear and concise description of what you expected to happen.
32 | ```paste below
33 |
34 | ```
35 |
36 | **ACTUAL RESULTS**
37 |
38 | ```paste below
39 |
40 | ```
41 |
42 | **Screenshots**
43 | If applicable, add screenshots to help explain your problem.
44 |
45 | **Desktop (please complete the following information):**
46 | - OS: [e.g. macOS]
47 | - Plugin version [e.g. Code Time v1.2.1]
48 |
49 | **Additional context**
50 | Add any other context about the problem here.
51 | ```paste below
52 |
53 | ```
54 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true # default
2 | contact_links:
3 | - name: 🌱 Join the community
4 | url: https://join.slack.com/t/software-community/shared_invite/enQtOTM4NjEyMjI1MDQ0LWM2ZDk0MDZiYzRjNjJiNGE2NWM3NDI3YzI1MjYzMmVjNjBmY2Q4Mzg4NjgyMjBmODMzZjk4M2RkMzYxMmY0ZjI
5 | about: Want to talk to our engineering team directly? Join us on Slack
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐾 Feature request
3 | about: Suggest an idea/feature for this project
4 | ---
5 |
6 |
7 | ##### SUMMARY
8 |
9 | ```paste below
10 |
11 | ```
12 |
13 | ##### ISSUE TYPE
14 | - Feature Request
15 |
16 | ##### ADDITIONAL INFORMATION
17 |
18 |
19 |
20 | ```paste below
21 |
22 | ```
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: "v$RESOLVED_VERSION"
2 | tag-template: "v$RESOLVED_VERSION"
3 | prerelease: true
4 | categories:
5 | - title: "🚀 Features"
6 | labels:
7 | - "feature"
8 | - "enhancement"
9 | - title: "🐛 Bug Fixes"
10 | labels:
11 | - "fix"
12 | - "bugfix"
13 | - "bug"
14 | - title: "🧰 Maintenance"
15 | labels:
16 | - "chore"
17 | - "debt"
18 | - title: "🛠 Dependency Updates"
19 | labels:
20 | - "dependencies"
21 | - title: "⚙️ GitOps"
22 | labels:
23 | - "git-ops"
24 | - "ci-cd"
25 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
26 | change-title-escapes: '\<*_&'
27 | version-resolver:
28 | major:
29 | labels:
30 | - "major"
31 | - "breaking"
32 | minor:
33 | labels:
34 | - "minor"
35 | patch:
36 | labels:
37 | - "patch"
38 | default: patch
39 | template: |
40 | ## Changes
41 |
42 | $CHANGES
43 |
--------------------------------------------------------------------------------
/.github/workflows/merge_to_main.yaml:
--------------------------------------------------------------------------------
1 | name: Merge to main
2 | on:
3 | push:
4 | branches:
5 | - main
6 | jobs:
7 | draft_release:
8 | runs-on: ubuntu-20.04
9 | steps:
10 | - uses: release-drafter/release-drafter@v5.15.0
11 | env:
12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | out
2 | node_modules
3 | .DS_Store
4 | lib/Constants.ts.local
5 | dist/
6 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "extensionHost",
6 | "request": "launch",
7 | "name": "Launch Extension Prod",
8 | "runtimeExecutable": "${execPath}",
9 | "sourceMaps": true,
10 | "args": [
11 | "--extensionDevelopmentPath=${workspaceFolder}"
12 | ],
13 | "env": {
14 | "NODE_ENV": "test",
15 | "APP_ENV": "prod"
16 | },
17 | "outFiles": [
18 | "${workspaceFolder}/dist/extension.js"
19 | ],
20 | "preLaunchTask": "npm: watch"
21 | },
22 | {
23 | "type": "extensionHost",
24 | "request": "launch",
25 | "name": "Launch Extension Dev",
26 | "runtimeExecutable": "${execPath}",
27 | "sourceMaps": true,
28 | "args": [
29 | "--extensionDevelopmentPath=${workspaceFolder}"
30 | ],
31 | "env": {
32 | "NODE_ENV": "test",
33 | "APP_ENV": "development"
34 | },
35 | "outFiles": [
36 | "${workspaceFolder}/dist/extension.js"
37 | ],
38 | "preLaunchTask": "npm: watch"
39 | },
40 | {
41 | "name": "Extension Tests",
42 | "type": "extensionHost",
43 | "request": "launch",
44 | "runtimeExecutable": "${execPath}",
45 | "args": [
46 | "--extensionDevelopmentPath=${workspaceFolder}",
47 | "--extensionTestsPath=${workspaceFolder}/out/test"
48 | ],
49 | "outFiles": [
50 | "${workspaceFolder}/out/test/**/*.js"
51 | ],
52 | "preLaunchTask": "npm: test-compile"
53 | }
54 | ],
55 | "compounds": [
56 | {
57 | "name": "Multiple Extensions",
58 | // Launch 2 debugger extensions at once
59 | "configurations": [
60 | "Launch Extension",
61 | "Launch Extension"
62 | ]
63 | }
64 | ]
65 | }
66 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.exclude": {
3 | "out": false
4 | },
5 | "search.exclude": {
6 | "out": true
7 | },
8 | "editor.tabSize": 2,
9 | "editor.insertSpaces": true,
10 | "workbench.colorCustomizations": {},
11 | "editor.formatOnSave": true
12 | }
13 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "problemMatcher": {
4 | "owner": "typescript",
5 | "fileLocation": "relative",
6 | "pattern": {
7 | "regexp": "^([^\\s].*)\\((\\d+|\\,\\d+|\\d+,\\d+,\\d+,\\d+)\\):\\s+(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$",
8 | "file": 1,
9 | "location": 2,
10 | "severity": 3,
11 | "code": 4,
12 | "message": 5
13 | },
14 | "background": {
15 | "activeOnStart": true,
16 | "beginsPattern": "^\\s*\\d{1,2}:\\d{1,2}:\\d{1,2}(?: AM| PM)? - File change detected\\. Starting incremental compilation\\.\\.\\.",
17 | "endsPattern": "^\\s*\\d{1,2}:\\d{1,2}:\\d{1,2}(?: AM| PM)? - Compilation complete\\. Watching for file changes\\."
18 | }
19 | },
20 | "tasks": [
21 | {
22 | "type": "npm",
23 | "script": "watch",
24 | "problemMatcher": "$tsc-watch",
25 | "isBackground": true,
26 | "presentation": {
27 | "reveal": "never"
28 | },
29 | "group": {
30 | "kind": "build",
31 | "isDefault": true
32 | }
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | typings/**
3 | test/**
4 | **/*.ts
5 | **/*.map
6 | .gitignore
7 | .github/**
8 | *.vsix
9 | tsconfig.json
10 | eslint.config.mjs
11 | node_modules
12 | out/
13 | lib/
14 | webpack.config.js
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2018 Software
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Code Time
2 |
3 |
4 |
5 | [Code Time](https://www.software.com/code-time) is an open source plugin for automatic programming metrics and time tracking in Visual Studio Code. Join our community of nearly half a million developers who use Code Time to reclaim time for focused, uninterrupted coding. Protect valuable code time and stay in flow.
6 |
7 | Looking for more ways to help your team improve their productivity? When you connect our [GitHub App](https://bit.ly/software-github) or [Bitbucket App](https://bit.ly/software-bitbucket), you can measure your team’s performance against engineering KPIs and benchmarks, including velocity, quality, and scale. You can identify bottlenecks in your release process — like slow code reviews — and take immediate action, so you can ship faster and more efficiently.
8 |
9 | 
10 |
11 | ## Getting Started
12 |
13 | Create an account to keep track of your coding data and unlock access to advanced data visualizations in the Code Time dashboard and web app. You can customize your profile, such as your work hours and office type, for advanced time tracking. You can also connect Outlook or Google Calendar to visualize your code time vs. meetings in a single calendar.
14 |
15 | Registering an account also lets you connect multiple code editors on multiple devices using the same email account. Your code time data will sync automatically across your devices.
16 |
17 | Open Code Time in the sidebar and follow the **Getting Started** prompts to create a new account, or click **Log in** to sign into an existing account.
18 |
19 | ## Protect Code Time
20 |
21 | 
22 |
23 | [Automatic Flow Mode](https://www.software.com/src/auto-flow-mode) makes it easy to eliminate distractions, mute notifications, and stay focused when you are in flow. It detects when you are in a high velocity coding session and automatically silences distractions and prevents interruptions.
24 |
25 | With Automatic Flow Mode, you can quickly toggle Zen mode or enter full screen. If you connect a Slack workspace, you can pause notifications, update your profile status, and set your presence to *away*. If you connect Google Calendar or Microsoft Outlook, you can also block time on your calendar. You can customize Flow Mode by clicking *Configure settings* in the Code Time sidebar.
26 |
27 | If you'd prefer to turn on Flow Mode manually during each coding session, you can enable or disable Automatic Flow Mode from the Code Time settings view. Make sure to hit the save button after you're done updating your settings. To then turn on Flow Mode manually, click *Enter Flow Mode* in the sidebar to toggle your flow automations. Once you are in Flow Mode, click *Exit Flow Mode* to resume receiving notifications and reset your Slack status.
28 |
29 | ## Track Development Metrics
30 |
31 | 
32 |
33 | Your coding stats can help you understand how you are improving over time.
34 |
35 | Your status bar shows you in real-time how many hours and minutes you code each day. A rocket will appear if your active code time exceeds your daily average on this day of the week.
36 |
37 | To see an overview of your coding activity and project metrics, open the **Code Time panel** by clicking on the Code Time icon in your sidebar. Click **Dashboard** to open your personalized Code Time dashboard in a new editor tab. Your dashboard summarizes your coding data — such as your code time, code time outside work hours, and meeting time — today, yesterday, last week, and over the last 90 days.
38 |
39 | Your _code time_ is the total time you have spent in your editor today. Your _active code time_ is the time you spend actively writing and editing code in your editor or IDE. It captures periods of intense focus and flow. Each metric shows how you compare today to your personal averages. Each average is calculated by day of week over the last 90 days (e.g. a Friday average is an average of all previous Fridays).
40 |
41 | ## Explore Data Visualizations
42 |
43 | 
44 |
45 | Click **More data at software.com** in the Code Time sidebar or visit [app.software.com](https://app.software.com) to see more advanced data visualizations. You will need to create a free Software.com account to use the web app. In the Code Time dashboard, you will be able to track:
46 |
47 | **Active code time.** Visualize your daily active code time. See productivity trends compared to 90-day averages. See how you stack up against the Software.com community.
48 |
49 | **Meeting time.** Connect your [Google Calendar](https://www.software.com/integrations/google-calendar) or [Outlook Calendar](https://www.software.com/integrations/microsoft-outlook) to visualize meeting time versus code time.
50 |
51 | **Work-life balance.** See how much coding happens during work hours versus nights and weekends so you can find ways to improve your work-life balance.
52 |
53 | ## Engineering KPIs for Teams
54 |
55 | 
56 |
57 | Take your team to the next level. Software.com makes it easy to automatically track engineering KPIs, including lead time, deployment frequency, and more.
58 |
59 | **Measure the impact of new tools.** See how changes to your engineering organization impact performance over time. Roll out new hiring plans, AI coding tools, platforms, and processes with greater confidence.
60 |
61 | **Remove bottlenecks and unlock new growth.** Setting up the right systems and tools sits at the heart of the developer experience. Development observability helps you uncover hidden bottlenecks and create an environment where every developer can be their most productive.
62 |
63 | **Build high-performing teams.** Better business outcomes are driven by teams that ship predictable and consistent results. Set up standardized metrics across your organization that help teams measure and improve development performance on their own.
64 |
65 | **Code Time for your entire team.** When you [upgrade your organization](https://www.software.com/pricing) to the Pro plan, you’ll unlock advanced Code Time features for every developer on your team, including custom reports and data exports.
66 |
67 | To see your team's engineering KPIs, visit the [web app](https://app.software.com) and connect our [GitHub](https://bit.ly/software-github) or [Bitbucket](https://bit.ly/software-bitbucket) app to your Software.com account. It's free and takes just a few minutes.
68 |
69 | ## It’s Safe, Secure, and Free
70 |
71 | **We never access your code:** We do not read, transmit, or store source code. We only provide metrics about programming, and we make it easy to see the data we collect. You can learn more about how we secure your data on our [security page](https://www.software.com/security).
72 |
73 | **Your data is private:** We will never share your individually identifiable data with your boss. When you create or join a team, we only show KPIs at the team and company level.
74 |
75 | **Free for you, forever:** We provide 90 days of data history for free, forever. We provide [premium plans](https://app.software.com/billing) for advanced features and historical data access.
76 |
77 | Code Time also collects basic usage metrics to help us make informed decisions about our roadmap.
78 |
79 | **Data collected and purpose**
80 | | Data Collected | Purpose of Data |
81 | | -------------- | --------------- |
82 | | Local Timezone | Applying time zones to aggregate metrics so that daily values are accurate |
83 | | Hostname | Used for de-duplicating accounts and linking multiple installations together under same login. Hostname is hashed before usage |
84 | | File open, save, close events | These events are used to capture general activity within the editor and extend code time sessions |
85 | | Editor focus / unfocus | These events are used to capture general activity within the editor and extend code time sessions |
86 | | Document change events | These events are used to characterize what types of changes are being made to a document. CONTENT OF A DOCUMENT IS NEVER STORED OR TRANSMITTED |
87 | | Project directory | Used to segment activity and Code Time across projects |
88 | | Git Repo Name | Used to segment activity and Code Time across git repos (Can be turned off in settings) |
89 | | File name | Used to segment activity and Code Time across file names (File names are one-way hashed before use) |
90 |
91 | [More details here](https://docs.software.com/article/43-code-time-data)
92 |
93 | ## Join the Community
94 |
95 | Enjoying Code Time? Let us know how it’s going by tweeting or following us at [@software_hq](https://twitter.com/software_hq).
96 |
97 | Have any questions? Create an issue in the [Code Time project](https://github.com/swdotcom/swdc-vscode) on GitHub or send us an email at [support@software.com](mailto:support@software.com) and we’ll get back to you as soon as we can.
98 |
--------------------------------------------------------------------------------
/codetime.readme.md:
--------------------------------------------------------------------------------
1 | # Code Time for Visual Studio Code
2 |
3 | > Programming metrics right in VS Code.
4 |
5 |
6 |
7 |
8 |
9 | ## Power up your development
10 |
11 | **In-editor dashboard**
12 | Get daily and weekly reports of your programming activity right in your code editor.
13 |
14 | **Status bar metrics**
15 | After installing our plugin, your status bar will show real-time metrics about time coded per day.
16 |
17 | **Weekly email reports**
18 | Get a weekly report delivered right to your email inbox.
19 |
20 | **Data visualizations**
21 | Go to our web app to get simple data visualizations, such as a rolling heatmap of your best programming times by hour of the day.
22 |
23 | **Calendar integration**
24 | Integrate with Google Calendar to automatically set calendar events to protect your best programming times from meetings and interrupts.
25 |
26 | **More stats**
27 | See your best music for coding and the speed, frequency, and top files across your commits.
28 |
29 | ## Why you should try it out
30 |
31 | - Automatic time reports by project
32 | - See what time you code your best—find your “flow”
33 | - Defend your best code times against meetings and interrupts
34 | - Find out what you can learn from your data
35 |
36 | ## It’s safe, secure, and free
37 |
38 | **We never access your code**
39 | We do not process, send, or store your proprietary code. We only provide metrics about programming, and we make it easy to see the data we collect.
40 |
41 | **Your data is private**
42 | We will never share your individually identifiable data with your boss. In the future, we will roll up data into groups and teams but we will keep your data anonymized.
43 |
44 | **Free for you, forever**
45 | We provide 90 days of data history for free, forever. In the future, we will provide premium plans for advanced features and historical data access.
46 |
47 |
48 |
49 | ## Getting started
50 |
51 | 1. [Install the Code Time plugin](https://marketplace.visualstudio.com/items?itemName=softwaredotcom.swdc-vscode) from the Visual Studio Code Marketplace.
52 |
53 | 2. After installing Code Time, an alert will appear prompting you to login (you can also click on "Code Time" in the status bar of Visual Studio Code.
54 |
55 | 3. You can visit the web app any time at [https://app.software.com/](https://app.software.com/).
56 |
57 |
58 |
59 | ## FAQs
60 |
61 | **What does the rocket ship icon in my status bar mean?**
62 |
63 | In the status bar of your editor/IDE, we show a rocket ship icon when your time code today exceeds your daily code time's 90-day average
64 |
65 | **Does keeping VSCode open affect time tracking/metrics?**
66 |
67 | The answer is "No", keeping the editor open should not skew your metrics. You do not need to close VS Code to ensure that Code Time metrics are correct.
68 |
69 | The timer starts when you start typing. It stops after 15 minutes without typing or opening/closing files in the editor. We then truncate the last 15 minutes of inactivity from the data.
70 |
71 | ## Contributing & Feedback
72 |
73 | Definitely let us know if you have more questions!
74 |
75 | Contact [cody@software.com](mailto:cody@software.com) with any additional questions or comments.
76 |
--------------------------------------------------------------------------------
/custom.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.svg" {
2 | const content: any;
3 | export default content;
4 | }
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig, globalIgnores } from 'eslint/config';
2 | import typescriptEslint from '@typescript-eslint/eslint-plugin';
3 | import globals from 'globals';
4 | import tsParser from '@typescript-eslint/parser';
5 |
6 | export default defineConfig([globalIgnores(['**/out', '**/dist', '**/*.d.ts']), {
7 | plugins: {
8 | '@typescript-eslint': typescriptEslint,
9 | },
10 |
11 | languageOptions: {
12 | globals: {
13 | ...globals.node,
14 | },
15 |
16 | parser: tsParser,
17 | ecmaVersion: 6,
18 | sourceType: 'module',
19 | },
20 |
21 | rules: {
22 | curly: 'warn',
23 | eqeqeq: 'warn',
24 | 'no-throw-literal': 'warn',
25 | quotes: ['warn', 'single'],
26 | },
27 | }]);
--------------------------------------------------------------------------------
/images/404-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swdotcom/swdc-vscode/85b307b7f8641ae1f2210e02e06d83fc236d789a/images/404-image.png
--------------------------------------------------------------------------------
/images/app-icon-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swdotcom/swdc-vscode/85b307b7f8641ae1f2210e02e06d83fc236d789a/images/app-icon-blue.png
--------------------------------------------------------------------------------
/images/codetime-b-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swdotcom/swdc-vscode/85b307b7f8641ae1f2210e02e06d83fc236d789a/images/codetime-b-90.png
--------------------------------------------------------------------------------
/images/codetime-c-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swdotcom/swdc-vscode/85b307b7f8641ae1f2210e02e06d83fc236d789a/images/codetime-c-128.png
--------------------------------------------------------------------------------
/images/codetime-g-30.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swdotcom/swdc-vscode/85b307b7f8641ae1f2210e02e06d83fc236d789a/images/codetime-g-30.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "swdc-vscode",
3 | "displayName": "Code Time",
4 | "version": "2.8.8",
5 | "publisher": "softwaredotcom",
6 | "description": "Code Time is an open source plugin that provides programming metrics right in Visual Studio Code.",
7 | "author": {
8 | "name": "Software.com"
9 | },
10 | "license": "SEE LICENSE IN LICENSE",
11 | "icon": "images/codetime-c-128.png",
12 | "galleryBanner": {
13 | "color": "#384356",
14 | "theme": "dark"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "https://github.com/swdotcom/swdc-vscode"
19 | },
20 | "categories": [
21 | "Other"
22 | ],
23 | "keywords": [
24 | "codetime",
25 | "flow",
26 | "mode",
27 | "time",
28 | "productivity"
29 | ],
30 | "capabilities": {
31 | "supported": true
32 | },
33 | "activationEvents": [
34 | "onStartupFinished"
35 | ],
36 | "extensionKind": [
37 | "ui",
38 | "workspace"
39 | ],
40 | "engines": {
41 | "vscode": "^1.100.2"
42 | },
43 | "main": "./dist/extension.js",
44 | "scripts": {
45 | "lint": "eslint .",
46 | "build": "vsce package",
47 | "watch": "tsc -watch -p ./",
48 | "webpack": "yarn compile:extension & yarn compile:views",
49 | "webpack-dev": "yarn watch:extension & yarn watch:views",
50 | "compile:extension": "tsc -p ./",
51 | "compile:views": "webpack --mode development",
52 | "watch:extension": "tsc -watch -p ./",
53 | "watch:views": "webpack --watch --mode development",
54 | "vscode:prepublish": "webpack --mode production",
55 | "test-compile": "tsc -p ./"
56 | },
57 | "contributes": {
58 | "commands": [
59 | {
60 | "command": "codetime.viewProjectReports",
61 | "title": "Code Time: Project reports"
62 | },
63 | {
64 | "command": "codetime.displayReadme",
65 | "title": "Code Time: Learn more"
66 | },
67 | {
68 | "command": "codetime.manageSlackConnection",
69 | "title": "Code Time: Manage Slack connection"
70 | },
71 | {
72 | "command": "codetime.connectSlack",
73 | "title": "Code Time: Connect Slack workspace"
74 | },
75 | {
76 | "command": "codetime.viewDashboard",
77 | "title": "Code Time: View Dashboard"
78 | },
79 | {
80 | "command": "codetime.enableFlowMode",
81 | "title": "Code Time: Enable Flow Mode"
82 | },
83 | {
84 | "command": "codetime.exitFlowMode",
85 | "title": "Code Time: Exit Flow Mode"
86 | },
87 | {
88 | "command": "codetime.logout",
89 | "title": "Code Time: Log out"
90 | },
91 | {
92 | "command": "codetime.authSignIn",
93 | "title": "Code Time: Software.com Sign In",
94 | "icon": "$(sign-in)"
95 | }
96 | ],
97 | "configuration": [
98 | {
99 | "type": "object",
100 | "title": "Code Time"
101 | }
102 | ],
103 | "viewsContainers": {
104 | "activitybar": [
105 | {
106 | "id": "code-time-sidebar",
107 | "title": "Code Time",
108 | "icon": "images/codetime-g-30.png"
109 | }
110 | ]
111 | },
112 | "views": {
113 | "code-time-sidebar": [
114 | {
115 | "id": "codetime.webView",
116 | "type": "webview",
117 | "name": "",
118 | "icon": "images/codetime-g-30.png"
119 | }
120 | ]
121 | }
122 | },
123 | "devDependencies": {
124 | "@types/copy-webpack-plugin": "^8.0.1",
125 | "@types/mocha": "^9.0.0",
126 | "@types/node": "^16.10.3",
127 | "@types/vscode": "^1.52.0",
128 | "@typescript-eslint/eslint-plugin": "^8.32.0",
129 | "@typescript-eslint/parser": "^8.32.0",
130 | "eslint": "^9.26.0",
131 | "copy-webpack-plugin": "^11.0.0",
132 | "file-loader": "^6.2.0",
133 | "ts-loader": "^9.2.6",
134 | "typescript": "^5.4.5",
135 | "webpack": "^5.94.0",
136 | "webpack-cli": "^4.10.0"
137 | },
138 | "dependencies": {
139 | "@swdotcom/editor-flow": "1.1.3",
140 | "@types/uuid": "10.0.0",
141 | "@types/ws": "^8.5.12",
142 | "axios": "1.9.0",
143 | "date-fns": "4.1.0",
144 | "node-cache": "5.1.2",
145 | "swdc-tracker": "1.6.0",
146 | "uuid": "11.1.0",
147 | "ws": "8.18.2"
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/resources/README.md:
--------------------------------------------------------------------------------
1 | # Code Time
2 |
3 |
4 |
5 | [Code Time](https://www.software.com/code-time) is an open source plugin for automatic programming metrics and time tracking in Visual Studio Code. Join our community of over 250,000 developers who use Code Time to reclaim time for focused, uninterrupted coding. Protect valuable code time and stay in flow.
6 |
7 | ---
8 |
9 | ✨ Want more out of *Software*? When you connect our [GitHub App](https://bit.ly/software-github), you can measure your team’s key DevOps metrics, including lead time and delivery velocity. You can identify bottlenecks in your release process — like slow code reviews — and take immediate action, so you can ship faster and more efficiently.
10 |
11 | ---
12 |
13 | 
14 |
15 | ## Getting Started
16 |
17 | Create an account to keep track of your coding data and unlock access to advanced data visualizations in the Code Time dashboard and web app. You can customize your profile, such as your work hours and office type, for advanced time tracking. You can also connect Outlook or Google Calendar to visualize your code time vs. meetings in a single calendar.
18 |
19 | Registering an account also lets you connect multiple code editors on multiple devices using the same email account. Your code time data will sync automatically across your devices.
20 |
21 | Open Code Time in the sidebar and follow the **Getting Started** prompts to create a new account, or click **Log in** to sign into an existing account.
22 |
23 | ## Protect Code Time
24 |
25 | 
26 |
27 | [Automatic Flow Mode](https://www.software.com/src/auto-flow-mode) makes it easy to eliminate distractions, mute notifications, and stay focused when you are in flow. It detects when you are in a high velocity coding session and automatically silences distractions and prevents interruptions.
28 |
29 | With Automatic Flow Mode, you can quickly toggle Zen mode or enter full screen. If you connect a Slack workspace, you can pause notifications, update your profile status, and set your presence to *away*. If you connect Google Calendar or Microsoft Outlook, you can also block time on your calendar. You can customize Flow Mode by clicking the gear icon at the the top of the Code Time sidebar.
30 |
31 | If you'd prefer to turn on Flow Mode manually during each coding session, you can enable or disable Automatic Flow Mode from the Code Time settings view. Make sure to hit the save button after you're done updating your settings. To then turn on Flow Mode manually, click *Enter Flow Mode* in the sidebar to toggle your flow automations. Once you are in Flow Mode, click *Exit Flow Mode* to resume receiving notifications and reset your Slack status.
32 |
33 | ## Track Development Metrics
34 |
35 | 
36 |
37 | Your coding stats can help you understand how you are improving over time.
38 |
39 | Your status bar shows you in real-time how many hours and minutes you code each day. A rocket will appear if your active code time exceeds your daily average on this day of the week.
40 |
41 | To see an overview of your coding activity and project metrics, open the **Code Time panel** by clicking on the Code Time icon in your sidebar. Click **Dashboard** to open your personalized Code Time dashboard in a new editor tab. Your dashboard summarizes your coding data — such as your code time, code time outside work hours, and meeting time — yesterday, last week, and over the last 90 days.
42 |
43 | Your _code time_ is the total time you have spent in your editor today. Your _active code time_ is the time you spend actively writing and editing code in your editor or IDE. It captures periods of intense focus and flow. Each metric shows how you compare today to your personal averages. Each average is calculated by day of week over the last 90 days (e.g. a Friday average is an average of all previous Fridays).
44 |
45 | ## Explore Data Visualizations
46 |
47 | 
48 |
49 | Click **More data at software.com** in the Code Time sidebar or visit [app.software.com](https://app.software.com) to see more advanced data visualizations. You will need to create a free Software account to use the web app. In the Code Time dashboard, you will be able to track:
50 |
51 | **Active code time.** Visualize your daily active code time. See productivity trends compared to 90-day averages. See how you stack up against the Software community of over 250,000 developers.
52 |
53 | **Meeting time.** Connect your [Google Calendar](https://www.software.com/integrations/google-calendar) or [Outlook Calendar](https://www.software.com/integrations/microsoft-outlook) to visualize meeting time versus code time.
54 |
55 | **Work-life balance.** See how much coding happens during work hours versus nights and weekends so you can find ways to improve your work-life balance.
56 |
57 | ## Measure Key DevOps Metrics
58 |
59 | 
60 |
61 | *Software* makes it easy to measure your team's key DevOps metrics, including [lead time](https://www.software.com/devops-guides/lead-time) and [delivery velocity](https://www.software.com/devops-guides/delivery-velocity-score). With better visibility, you can quickly find and fix bottlenecks in your delivery pipeline.
62 |
63 | **Diagnose bottlenecks with alerts.** Monitors automatically notify you when metrics cross a certain threshold (like when [code reviews take longer](https://www.software.com/src/code-reviews-bottleneck-in-your-delivery-pipeline) to review, approve, or merge), so you can take action and prevent delays.
64 |
65 | **Customize dashboards to track DevOps efficiency.** Organize, visualize, and monitor the engineering metrics you care about most — like lead time, delivery frequency, and pull request review time.
66 |
67 | **Benchmark your DevOps performance.** According to research from DORA, Google Cloud's DevOps Research and Assessment team, elite engineering teams have 973x more frequent code deployments and 6570x faster lead time. With Software, you can compare your delivery speed and efficiency with other high-performing engineering organizations in our global community.
68 |
69 | To see your team's DevOps metrics, visit the [web app](https://app.software.com) and connect the [*Software* GitHub App](https://bit.ly/software-github) to your Software account. It's free and takes just a few minutes.
70 |
71 | ## It’s Safe, Secure, and Free
72 |
73 | **We never access your code:** We do not read, transmit, or store source code. We only provide metrics about programming, and we make it easy to see the data we collect. You can learn more about how we secure your data on our [security page](https://www.software.com/security).
74 |
75 | **Your data is private:** We will never share your individually identifiable data with your boss. When you create or join a team, we only show system-level metrics for your GitHub organization.
76 |
77 | **Free for you, forever:** We provide 90 days of data history for free, forever. We provide [premium plans](https://www.software.com/pricing) for advanced features and historical data access.
78 |
79 | Code Time also collects basic usage metrics to help us make informed decisions about our roadmap.
80 |
81 | ## Join the Community
82 |
83 | Enjoying Code Time? Let us know how it’s going by tweeting or following us at [@software_hq](https://twitter.com/software_hq).
84 |
85 | We're also the creators behind Music Time for VS Code, an extension that helps you find your most productive songs for coding. You can learn more [here](https://www.software.com/music-time).
86 |
87 | Have any questions? Create an issue in the [Code Time project](https://github.com/swdotcom/swdc-vscode) on GitHub or send us an email at [support@software.com](mailto:support@software.com) and we’ll get back to you as soon as we can.
88 |
--------------------------------------------------------------------------------
/resources/dark/add.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/light/add.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Constants.ts:
--------------------------------------------------------------------------------
1 | export const LOGIN_LABEL = 'Log in';
2 | export const LOGOUT_LABEL = 'Log out';
3 | export const UNTITLED = 'Untitled';
4 | export const NO_PROJ_NAME = 'Unnamed';
5 | export const CODE_TIME_PLUGIN_ID = 2;
6 | export const CODE_TIME_EXT_ID = 'softwaredotcom.swdc-vscode';
7 | export const MUSIC_TIME_EXT_ID = "softwaredotcom.music-time";
8 | export const EDITOR_OPS_EXT_ID = "softwaredotcom.editor-ops";
9 | export const CODE_TIME_TYPE = 'codetime';
10 | export const YES_LABEL = 'Yes';
11 | export const SIGN_UP_LABEL = 'Sign up';
12 | export const DISCONNECT_LABEL = 'Disconnect';
13 | export const HIDE_CODE_TIME_STATUS_LABEL = 'Hide Code Time status';
14 | export const SHOW_CODE_TIME_STATUS_LABEL = 'Show Code Time status';
15 |
16 | const isDev = process.env.APP_ENV === 'development'
17 | export const SOFTWARE_DIRECTORY = isDev ? '.software-dev' : '.software';
18 | export const websockets_url = isDev ? 'ws://localhost:5001/websockets' : 'wss://api.software.com/websockets';
19 | export const app_url = isDev ? 'http://localhost:3000' : 'https://app.software.com';
20 |
21 | export const vscode_issues_url = 'https://github.com/swdotcom/swdc-vscode/issues';
22 |
23 | export const ONE_MIN_MILLIS = 1000 * 60;
24 |
--------------------------------------------------------------------------------
/src/DataController.ts:
--------------------------------------------------------------------------------
1 | import {commands} from 'vscode';
2 | import {isResponseOk, appGet} from './http/HttpClient';
3 | import {
4 | getItem,
5 | setItem,
6 | setAuthCallbackState,
7 | logIt,
8 | musicTimeExtInstalled,
9 | editorOpsExtInstalled,
10 | showInformationMessage,
11 | } from './Util';
12 | import {initializeWebsockets} from './websockets';
13 | import {SummaryManager} from './managers/SummaryManager';
14 | import { updateFlowModeStatus } from './managers/FlowManager';
15 | import { AuthProvider } from './auth/AuthProvider';
16 |
17 | let currentUser: any | null = null;
18 | let authProvider: AuthProvider | null = null;
19 |
20 | export function initializeAuthProvider(provider: AuthProvider) {
21 | authProvider = provider;
22 | }
23 |
24 | export async function getCachedSlackIntegrations() {
25 | currentUser = await getCachedUser();
26 | if (currentUser?.integration_connections?.length) {
27 | return currentUser?.integration_connections?.filter(
28 | (integration: any) => integration.status === 'ACTIVE' && (integration.integration_type_id === 14));
29 | }
30 | return [];
31 | }
32 |
33 | export async function getCachedUser() {
34 | if (!currentUser) {
35 | currentUser = await getUser();
36 | }
37 | return currentUser;
38 | }
39 |
40 | export function isRegistered() {
41 | return !!getItem('name');
42 | }
43 |
44 | export async function getUserPreferences() {
45 | currentUser = await getCachedUser()
46 | if (currentUser) {
47 | return currentUser.preferences_parsed;
48 | }
49 | return {}
50 | }
51 |
52 | export async function getUser(token_override: any = '') {
53 | const resp = await appGet('/api/v1/user', {}, token_override);
54 | if (isResponseOk(resp) && resp.data) {
55 | currentUser = resp.data;
56 | return currentUser;
57 | }
58 | return null;
59 | }
60 |
61 | export async function authenticationCompleteHandler(user: any, override_jwt: any = '') {
62 | setAuthCallbackState(null);
63 |
64 | if (user?.registered === 1) {
65 | currentUser = user;
66 | // new user
67 | if (override_jwt) {
68 | setItem('jwt', override_jwt);
69 | } else if (user.plugin_jwt) {
70 | setItem('jwt', user.plugin_jwt);
71 | }
72 | setItem('name', user.email);
73 | setItem('updatedAt', new Date().getTime());
74 |
75 | setItem('logging_in', false);
76 | // ensure the session is updated
77 | if (authProvider) {
78 | authProvider.updateSession(getItem('jwt'), user);
79 | }
80 | // update the login status
81 | showInformationMessage('Successfully logged on to Code Time');
82 |
83 | await reload();
84 | }
85 | }
86 |
87 | export async function userDeletedCompletionHandler() {
88 | commands.executeCommand('codetime.logout');
89 | }
90 |
91 | export async function reload() {
92 | updateFlowModeStatus();
93 |
94 | try {
95 | initializeWebsockets();
96 | } catch (e: any) {
97 | logIt(`Failed to initialize websockets: ${e.message}`);
98 | }
99 |
100 | // re-initialize user and preferences
101 | await getUser();
102 |
103 | // fetch after logging on
104 | SummaryManager.getInstance().updateSessionSummaryFromServer();
105 |
106 | if (musicTimeExtInstalled()) {
107 | setTimeout(() => {
108 | commands.executeCommand("musictime.refreshMusicTimeView")
109 | }, 1000);
110 | }
111 |
112 | if (editorOpsExtInstalled()) {
113 | setTimeout(() => {
114 | commands.executeCommand("editorOps.refreshEditorOpsView")
115 | }, 1000);
116 | }
117 |
118 | commands.executeCommand('codetime.refreshCodeTimeView');
119 | }
120 |
--------------------------------------------------------------------------------
/src/Util.ts:
--------------------------------------------------------------------------------
1 | import {workspace, extensions, window, Uri, commands, ViewColumn, WorkspaceFolder, env} from 'vscode';
2 | import {
3 | CODE_TIME_EXT_ID,
4 | CODE_TIME_PLUGIN_ID,
5 | CODE_TIME_TYPE,
6 | SOFTWARE_DIRECTORY,
7 | MUSIC_TIME_EXT_ID,
8 | EDITOR_OPS_EXT_ID
9 | } from './Constants';
10 | import { v4 as uuidv4 } from 'uuid';
11 |
12 | import {showModalSignupPrompt} from './managers/SlackManager';
13 | import {execCmd} from './managers/ExecManager';
14 | import {getBooleanJsonItem, getJsonItem, setJsonItem, storeJsonData} from './managers/FileManager';
15 | import { formatISO } from 'date-fns';
16 | import { initializeWebsockets, websocketAlive } from './websockets';
17 |
18 | import * as fs from 'fs';
19 | import * as path from 'path';
20 | import * as os from 'os';
21 |
22 | const outputChannel = window.createOutputChannel('CodeTime');
23 |
24 | export const alpha = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
25 |
26 | let workspace_name: string | null = null;
27 | let hostname: string | null = null;
28 | let osUsername: string | null = null;
29 | let editorName: string = '';
30 | let osName: string = '';
31 |
32 | export function getRandomNumberWithinRange(min: number, max: number) {
33 | return Math.floor(Math.random() * (max - min) + min);
34 | }
35 |
36 | export function getWorkspaceName() {
37 | if (!workspace_name) {
38 | workspace_name = uuidv4();
39 | }
40 | return workspace_name;
41 | }
42 |
43 | export function getPluginId() {
44 | return CODE_TIME_PLUGIN_ID;
45 | }
46 |
47 | export function getPluginName() {
48 | return CODE_TIME_EXT_ID;
49 | }
50 |
51 | export function getPluginType() {
52 | return CODE_TIME_TYPE;
53 | }
54 |
55 | export function getVersion() {
56 | const extension = extensions.getExtension(CODE_TIME_EXT_ID);
57 | return extension ? extension.packageJSON.version : '2.5.27';
58 | }
59 |
60 | export function getEditorName() {
61 | if (!editorName) {
62 | try {
63 | editorName = env.appName
64 | } catch (e) {
65 | editorName = 'vscode'
66 | }
67 | }
68 | return editorName;
69 | }
70 |
71 | export function isGitProject(projectDir: string) {
72 | if (!projectDir) {
73 | return false;
74 | }
75 |
76 | const gitRemotesDir = path.join(projectDir, '.git', 'refs', 'remotes');
77 | if (!fs.existsSync(gitRemotesDir)) {
78 | return false;
79 | }
80 | return true;
81 | }
82 |
83 | /**
84 | * These will return the workspace folders.
85 | * use the uri.fsPath to get the full path
86 | * use the name to get the folder name
87 | */
88 | export function getWorkspaceFolders(): WorkspaceFolder[] {
89 | let folders = [];
90 | if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) {
91 | for (let i = 0; i < workspace.workspaceFolders.length; i++) {
92 | let workspaceFolder = workspace.workspaceFolders[i];
93 | let folderUri = workspaceFolder.uri;
94 | if (folderUri && folderUri.fsPath) {
95 | folders.push(workspaceFolder);
96 | }
97 | }
98 | }
99 | return folders;
100 | }
101 |
102 | export function getFirstWorkspaceFolder(): WorkspaceFolder | null {
103 | const workspaceFolders: WorkspaceFolder[] = getWorkspaceFolders();
104 | if (workspaceFolders && workspaceFolders.length) {
105 | return workspaceFolders[0];
106 | }
107 | return null;
108 | }
109 |
110 | export function getNumberOfTextDocumentsOpen() {
111 | return workspace.textDocuments ? workspace.textDocuments.length : 0;
112 | }
113 |
114 | export function updateFlowChange(in_flow: boolean) {
115 | setJsonItem(getFlowChangeFile(), "in_flow", in_flow);
116 | }
117 |
118 | export function isFlowModeEnabled() {
119 | // nullish coalesce the "in_flow" flag if it doesn't exist
120 | return getBooleanJsonItem(getFlowChangeFile(), "in_flow") ?? false;
121 | }
122 |
123 | export function setItem(key: string, value: any) {
124 | setJsonItem(getSoftwareSessionFile(), key, value);
125 | }
126 |
127 | export function getItem(key: string) {
128 | return getJsonItem(getSoftwareSessionFile(), key);
129 | }
130 |
131 | export function getBooleanItem(key: string) {
132 | return getBooleanJsonItem(getSoftwareSessionFile(), key);
133 | }
134 |
135 | export function isActiveIntegration(type: string, integration: any) {
136 | if (integration && integration.status.toLowerCase() === "active") {
137 | // handle integration_connection attribute
138 | if (integration.integration_type) {
139 | return !!(integration.integration_type.type.toLowerCase() === type.toLowerCase())
140 | }
141 | // still hasn't updated to use that in within the file, check the older version attribute
142 | return !!(integration.name.toLowerCase() === type.toLowerCase())
143 | }
144 | return false;
145 | }
146 |
147 | export function getPluginUuid() {
148 | let plugin_uuid = getJsonItem(getDeviceFile(), 'plugin_uuid');
149 | if (!plugin_uuid) {
150 | let name = `${getOsUsername()}${getHostname()}`;
151 | if (!name) {
152 | name = getOs();
153 | }
154 | const hashName = require('crypto')
155 | .createHash('sha1')
156 | .update(name)
157 | .digest('hex');
158 | plugin_uuid = `${hashName.trim()}:${uuidv4()}`;
159 | // set it for the 1st and only time
160 | setJsonItem(getDeviceFile(), 'plugin_uuid', plugin_uuid);
161 | }
162 | return plugin_uuid;
163 | }
164 |
165 | export function getAuthCallbackState(autoCreate = true) {
166 | let auth_callback_state = getJsonItem(getDeviceFile(), 'auth_callback_state', false);
167 | if (!auth_callback_state && autoCreate) {
168 | auth_callback_state = uuidv4();
169 | setAuthCallbackState(auth_callback_state);
170 | }
171 | return auth_callback_state;
172 | }
173 |
174 | export function setAuthCallbackState(value: string | null) {
175 | setJsonItem(getDeviceFile(), 'auth_callback_state', value);
176 | }
177 |
178 | export function isLinux() {
179 | return isWindows() || isMac() ? false : true;
180 | }
181 |
182 | // process.platform return the following...
183 | // -> 'darwin', 'freebsd', 'linux', 'sunos' or 'win32'
184 | export function isWindows() {
185 | return process.platform.indexOf('win32') !== -1;
186 | }
187 |
188 | export function isMac() {
189 | return process.platform.indexOf('darwin') !== -1;
190 | }
191 |
192 | export function getHostname(): any {
193 | if (!hostname) {
194 | hostname = execCmd('hostname');
195 | }
196 | return hostname;
197 | }
198 |
199 | export function getOs() {
200 | if (!osName) {
201 | let parts = [];
202 | let osType = os.type();
203 | if (osType) {
204 | parts.push(osType);
205 | }
206 | let osRelease = os.release();
207 | if (osRelease) {
208 | parts.push(osRelease);
209 | }
210 | let platform = os.platform();
211 | if (platform) {
212 | parts.push(platform);
213 | }
214 | if (parts.length > 0) {
215 | osName = parts.join('_');
216 | }
217 | }
218 | return osName;
219 | }
220 |
221 | export function getOsUsername() {
222 | if (!osUsername) {
223 | try {
224 | // Throws a SystemError if a user has no username or homedir
225 | osUsername = os.userInfo().username;
226 | } catch (e: any) {
227 | console.error('Username not available.', e.message);
228 | }
229 |
230 | if (!osUsername) {
231 | osUsername = execCmd('whoami');
232 | }
233 | }
234 | return osUsername;
235 | }
236 |
237 | function getFile(name: string, default_data: any = {}) {
238 | const file_path = getSoftwareDir();
239 | const file = isWindows() ? `${file_path}\\${name}` : `${file_path}/${name}`;
240 | if (!fs.existsSync(file)) {
241 | storeJsonData(file, default_data);
242 | }
243 | return file;
244 | }
245 |
246 | export function getDeviceFile() {
247 | return getFile('device.json');
248 | }
249 |
250 | export function getSoftwareSessionFile() {
251 | return getFile('session.json');
252 | }
253 |
254 | export function getGitEventFile() {
255 | return getFile('gitEvents.json');
256 | }
257 |
258 | export function getSessionSummaryFile() {
259 | return getFile('sessionSummary.json');
260 | }
261 |
262 | export function getFlowChangeFile() {
263 | return getFile('flowChange.json');
264 | }
265 |
266 | export function getExtensionsFile() {
267 | return getFile('extensions.json');
268 | }
269 |
270 | export function getSoftwareDir() {
271 | const homedir = os.homedir();
272 | const softwareDataDir = isWindows() ? `${homedir}\\${SOFTWARE_DIRECTORY}` : (process.env.XDG_CONFIG_HOME ? `${process.env.XDG_CONFIG_HOME}/${SOFTWARE_DIRECTORY.substring(1)}` : `${homedir}/${SOFTWARE_DIRECTORY}`);
273 |
274 | if (!fs.existsSync(softwareDataDir)) {
275 | fs.mkdirSync(softwareDataDir);
276 | }
277 | return softwareDataDir;
278 | }
279 |
280 | export function getLocalREADMEFile() {
281 | const resourcePath: string = path.join(__dirname, 'resources');
282 | const file = path.join(resourcePath, 'README.md');
283 | return file;
284 | }
285 |
286 | export function displayReadme() {
287 | const readmeUri = Uri.file(getLocalREADMEFile());
288 |
289 | commands.executeCommand('markdown.showPreview', readmeUri, ViewColumn.One);
290 | setItem('vscode_CtReadme', true);
291 | }
292 |
293 | export function getExtensionName() {
294 | return 'swdc-vscode';
295 | }
296 |
297 | export function getLogId() {
298 | return 'CodeTime';
299 | }
300 |
301 | export function logIt(message: string, isError: boolean = false) {
302 | const windowMsg: string = isPrimaryWindow() ? '(p)' : '';
303 | outputChannel.appendLine(`${formatISO(new Date())} ${getLogId()}${windowMsg}: ${message}`);
304 | if (isError) {
305 | console.error(message)
306 | }
307 | }
308 |
309 | export function getOffsetSeconds() {
310 | let d = new Date();
311 | return d.getTimezoneOffset() * 60;
312 | }
313 |
314 | export function getAuthQueryObject(): URLSearchParams {
315 | const params = new URLSearchParams();
316 | params.append('plugin_uuid', getPluginUuid());
317 | params.append('plugin_id', `${getPluginId()}`);
318 | params.append('plugin_version', getVersion());
319 | params.append('auth_callback_state', getAuthCallbackState(true));
320 | return params;
321 | }
322 |
323 | export function launchWebUrl(url: string) {
324 | if (!websocketAlive()) {
325 | try {
326 | initializeWebsockets();
327 | } catch (e) {
328 | console.error('Failed to initialize websockets', e);
329 | }
330 | }
331 | env.openExternal(Uri.parse(url));
332 | }
333 |
334 | /**
335 | * humanize the minutes
336 | */
337 | export function humanizeMinutes(min: any) {
338 | min = parseInt(min, 0) || 0;
339 | let str = '';
340 | if (min === 60) {
341 | str = '1h';
342 | } else if (min > 60) {
343 | const hours = Math.floor(min / 60);
344 | const minutes = min % 60;
345 |
346 | const hoursStr = Math.floor(hours).toFixed(0) + 'h';
347 | if ((parseFloat(min) / 60) % 1 === 0) {
348 | str = hoursStr;
349 | } else {
350 | str = `${hoursStr} ${minutes}m`;
351 | }
352 | } else if (min === 1) {
353 | str = '1m';
354 | } else {
355 | // less than 60 seconds
356 | str = min.toFixed(0) + 'm';
357 | }
358 | return str;
359 | }
360 |
361 | export function showInformationMessage(message: string) {
362 | logIt(message);
363 | return window.showInformationMessage(`${message}`);
364 | }
365 |
366 | export function showWarningMessage(message: string) {
367 | return window.showWarningMessage(`${message}`);
368 | }
369 |
370 | export function noSpacesProjectDir(projectDir: string): string {
371 | return projectDir.replace(/^\s+/g, '');
372 | }
373 |
374 | export function checkRegistrationForReport(showSignup = true) {
375 | if (!getItem('name')) {
376 | if (showSignup) {
377 | showModalSignupPrompt(
378 | 'Unlock your personalized dashboard and visualize your coding activity. Create an account to get started.'
379 | );
380 | }
381 | return false;
382 | }
383 | return true;
384 | }
385 |
386 | export function isPrimaryWindow() {
387 | let workspaceWindow = getItem('vscode_primary_window');
388 | if (!workspaceWindow) {
389 | // its not set yet, update it to this window
390 | workspaceWindow = getWorkspaceName();
391 | setItem('vscode_primary_window', workspaceWindow);
392 | }
393 | return !!(workspaceWindow === getWorkspaceName());
394 | }
395 |
396 | export function musicTimeExtInstalled() {
397 | return !!extensions.getExtension(MUSIC_TIME_EXT_ID);
398 | }
399 |
400 | export function editorOpsExtInstalled() {
401 | return !!extensions.getExtension(EDITOR_OPS_EXT_ID)
402 | }
403 |
404 | export function getFileNameFromPath(filePath: string) {
405 | const parts = isWindows() ? filePath.split('\\') : filePath.split('/');
406 | return parts[parts.length - 1].split('.')[0];
407 | }
408 |
--------------------------------------------------------------------------------
/src/auth/AuthProvider.ts:
--------------------------------------------------------------------------------
1 | import {
2 | authentication, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent,
3 | AuthenticationSession, Disposable, Event, env, EventEmitter, ExtensionContext, ProgressLocation,
4 | Uri, UriHandler, window
5 | } from "vscode";
6 | import { v4 as uuid } from 'uuid';
7 | import { app_url } from "../Constants";
8 | import { getAuthQueryObject, getBooleanItem, logIt, setItem } from "../Util";
9 | import { authenticationCompleteHandler, getUser } from "../DataController";
10 |
11 | export const AUTH_TYPE = 'codetime_auth';
12 | const AUTH_NAME = 'Software.com';
13 | const SESSIONS_KEY = `${AUTH_TYPE}.sessions`
14 |
15 | let instance: AuthProvider;
16 |
17 | export function getAuthInstance(): AuthProvider {
18 | if (!instance) {
19 | logIt('AuthenticationProvider not initialized');
20 | }
21 | return instance;
22 | }
23 |
24 | class UriEventHandler extends EventEmitter implements UriHandler {
25 | public handleUri(uri: Uri) {
26 | this.fire(uri);
27 | }
28 | }
29 |
30 | export class AuthProvider implements AuthenticationProvider, Disposable {
31 | private _sessionChangeEmitter = new EventEmitter();
32 | private _disposable: Disposable;
33 | private _pendingStates: string[] = [];
34 | private _codeExchangePromises = new Map; cancel: EventEmitter }>();
35 | private _uriHandler = new UriEventHandler();
36 |
37 | constructor(private readonly context: ExtensionContext) {
38 | this._disposable = Disposable.from(
39 | authentication.registerAuthenticationProvider(AUTH_TYPE, AUTH_NAME, this, { supportsMultipleAccounts: false }),
40 | window.registerUriHandler(this._uriHandler)
41 | )
42 | instance = this;
43 | }
44 |
45 | get onDidChangeSessions() {
46 | return this._sessionChangeEmitter.event;
47 | }
48 |
49 | get redirectUri() {
50 | const publisher = this.context.extension.packageJSON.publisher;
51 | const name = this.context.extension.packageJSON.name;
52 | return `${env.uriScheme}://${publisher}.${name}`;
53 | }
54 |
55 | /**
56 | * Get the existing sessions
57 | * @param scopes
58 | * @returns
59 | */
60 | public async getSessions(scopes?: string[]): Promise {
61 | const allSessions = await this.context.secrets.get(SESSIONS_KEY);
62 |
63 | if (allSessions) {
64 | return JSON.parse(allSessions) as AuthenticationSession[];
65 | }
66 |
67 | return [];
68 | }
69 |
70 | public async updateSession(jwtToken: string, user: any = null): Promise {
71 | let session: AuthenticationSession = {
72 | id: uuid(),
73 | accessToken: jwtToken,
74 | account: {
75 | label: '',
76 | id: ''
77 | },
78 | scopes: []
79 | }
80 | try {
81 | const sessionUpdate = !!user
82 | if (!user) {
83 | user = await getUser(jwtToken);
84 | await authenticationCompleteHandler(user, jwtToken);
85 | }
86 |
87 | session = {
88 | id: uuid(),
89 | accessToken: jwtToken,
90 | account: {
91 | label: user.email,
92 | id: user.id
93 | },
94 | scopes: []
95 | };
96 |
97 | await this.context.secrets.store(SESSIONS_KEY, JSON.stringify([session]))
98 |
99 | if (sessionUpdate) {
100 | this._sessionChangeEmitter.fire({ added: [], removed: [], changed: [session] });
101 | } else {
102 | this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] });
103 | }
104 | } catch (e: any) {
105 | if (e.message) {
106 | logIt(`Error creating session: ${e?.message}`);
107 | }
108 | }
109 | return session;
110 | }
111 |
112 | /**
113 | * Create a new auth session
114 | * @param scopes
115 | * @returns
116 | */
117 | public async createSession(scopes: string[]): Promise {
118 | const jwtToken = await this.login(scopes);
119 | if (!jwtToken) {
120 | throw new Error(`Software.com login failure`);
121 | }
122 | return this.updateSession(jwtToken);
123 | }
124 |
125 | /**
126 | * Remove an existing session
127 | * @param sessionId
128 | */
129 | public async removeSession(sessionId: string): Promise {
130 | const allSessions = await this.context.secrets.get(SESSIONS_KEY);
131 | if (allSessions) {
132 | let sessions = JSON.parse(allSessions) as AuthenticationSession[];
133 | const sessionIdx = sessions.findIndex(s => s.id === sessionId);
134 | const session = sessions[sessionIdx];
135 | sessions.splice(sessionIdx, 1);
136 |
137 | await this.context.secrets.store(SESSIONS_KEY, JSON.stringify(sessions));
138 |
139 | if (session) {
140 | this._sessionChangeEmitter.fire({ added: [], removed: [session], changed: [] });
141 | }
142 | }
143 | }
144 |
145 | /**
146 | * Dispose the registered services
147 | */
148 | public async dispose() {
149 | this._disposable.dispose();
150 | }
151 |
152 | /**
153 | * Auth Log
154 | */
155 | private async login(scopes: string[] = []) {
156 | return await window.withProgress({
157 | location: ProgressLocation.Notification,
158 | title: "Signing in to Software.com...",
159 | cancellable: true
160 | }, async (_, token) => {
161 | setItem('logging_in', true);
162 | const stateId = uuid();
163 |
164 | this._pendingStates.push(stateId);
165 |
166 | const scopeString = scopes.join(' ');
167 | let params: URLSearchParams = getAuthQueryObject();
168 | params.append('response_type', 'token');
169 | params.append('redirect_uri', this.redirectUri);
170 | params.append('state', stateId);
171 | params.append('prompt', 'login');
172 | const uri = Uri.parse(`${app_url}/plugin/authorize?${params.toString()}`);
173 | await env.openExternal(uri);
174 |
175 | let codeExchangePromise = this._codeExchangePromises.get(scopeString);
176 | if (!codeExchangePromise) {
177 | codeExchangePromise = promiseFromEvent(this._uriHandler.event, this.handleUri(scopes));
178 | this._codeExchangePromises.set(scopeString, codeExchangePromise);
179 | }
180 |
181 | try {
182 | return await Promise.race([
183 | codeExchangePromise.promise,
184 | // 2 minute timeout
185 | new Promise((_, reject) => setTimeout(() => reject('Cancelled'), 120000)),
186 | // websocket login check
187 | new Promise((_, reject) => {
188 | const interval = setInterval(async () => {
189 | if (getBooleanItem('logging_in') === false) {
190 | clearInterval(interval);
191 | reject('Cancelled');
192 | }
193 | }, 1500);
194 | }),
195 | // cancel button
196 | promiseFromEvent(token.onCancellationRequested, (_, __, reject) => { reject('Login Cancelled'); }).promise
197 | ]);
198 | } finally {
199 | this._pendingStates = this._pendingStates.filter(n => n !== stateId);
200 | codeExchangePromise?.cancel.fire();
201 | this._codeExchangePromises.delete(scopeString);
202 | // reset logging_in flag
203 | setItem('logging_in', false);
204 | }
205 | });
206 | }
207 |
208 | /**
209 | * Handle the redirect to VS Code (after sign in from Auth0)
210 | * @param scopes
211 | * @returns
212 | */
213 | private handleUri: (scopes: readonly string[]) => PromiseAdapter =
214 | (scopes) => async (uri, resolve, reject) => {
215 | const query = new URLSearchParams(uri.query);
216 | const access_token = query.get('access_token');
217 | const state = query.get('state');
218 |
219 | if (!access_token) {
220 | reject(new Error('Authentication token not found'));
221 | return;
222 | }
223 | if (!state) {
224 | reject(new Error('Authentication state not found'));
225 | return;
226 | }
227 |
228 | // Check if it is a valid auth request started by the extension
229 | if (!this._pendingStates.some(n => n === state)) {
230 | reject(new Error('Authentication state not found'));
231 | return;
232 | }
233 |
234 | resolve(access_token);
235 | }
236 | }
237 |
238 | export interface PromiseAdapter {
239 | (
240 | value: T,
241 | resolve:
242 | (value: U | PromiseLike) => void,
243 | reject:
244 | (reason: any) => void
245 | ): any;
246 | }
247 |
248 | const passthrough = (value: any, resolve: (value?: any) => void) => resolve(value);
249 |
250 | /**
251 | * Return a promise that resolves with the next emitted event, or with some future
252 | * event as decided by an adapter.
253 | *
254 | * If specified, the adapter is a function that will be called with
255 | * `(event, resolve, reject)`. It will be called once per event until it resolves or
256 | * rejects.
257 | *
258 | * The default adapter is the passthrough function `(value, resolve) => resolve(value)`.
259 | *
260 | * @param event the event
261 | * @param adapter controls resolution of the returned promise
262 | * @returns a promise that resolves or rejects as specified by the adapter
263 | */
264 | export function promiseFromEvent(event: Event, adapter: PromiseAdapter = passthrough): { promise: Promise; cancel: EventEmitter } {
265 | let subscription: Disposable;
266 | let cancel = new EventEmitter();
267 |
268 | return {
269 | promise: new Promise((resolve, reject) => {
270 | cancel.event(_ => reject('Cancelled'));
271 | subscription = event((value: T) => {
272 | try {
273 | Promise.resolve(adapter(value, resolve, reject))
274 | .catch(reject);
275 | } catch (error) {
276 | reject(error);
277 | }
278 | });
279 | }).then(
280 | (result: U) => {
281 | subscription.dispose();
282 | return result;
283 | },
284 | error => {
285 | subscription.dispose();
286 | throw error;
287 | }
288 | ),
289 | cancel
290 | };
291 | }
292 |
293 |
--------------------------------------------------------------------------------
/src/cache/CacheManager.ts:
--------------------------------------------------------------------------------
1 | const NodeCache = require("node-cache");
2 |
3 | export class CacheManager {
4 | private static instance: CacheManager;
5 | private myCache;
6 |
7 | private constructor() {
8 | // default cache of 2 minutes
9 | this.myCache = new NodeCache({ stdTTL: 120 });
10 | }
11 |
12 | static getInstance(): CacheManager {
13 | if (!CacheManager.instance) {
14 | CacheManager.instance = new CacheManager();
15 | }
16 |
17 | return CacheManager.instance;
18 | }
19 |
20 | get(key: string) {
21 | return this.myCache.get(key);
22 | }
23 |
24 | set(key: string, value: any, ttl: number = -1) {
25 | if (ttl > 0) {
26 | this.myCache.set(key, value, ttl);
27 | } else {
28 | // use the standard cache ttl
29 | this.myCache.set(key, value);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/command-helper.ts:
--------------------------------------------------------------------------------
1 | import {commands, Disposable, window, ExtensionContext, authentication} from 'vscode';
2 | import {launchWebUrl, displayReadme, setItem, showInformationMessage} from './Util';
3 | import {KpmManager} from './managers/KpmManager';
4 | import {KpmItem} from './model/models';
5 | import {createAnonymousUser, authLogin} from './menu/AccountManager';
6 | import {app_url, vscode_issues_url} from './Constants';
7 | import {enableFlow, pauseFlow} from './managers/FlowManager';
8 | import {showDashboard} from './managers/WebViewManager';
9 | import {closeSettings, configureSettings, updateSettings} from './managers/ConfigManager';
10 | import {toggleStatusBar, updateFlowModeStatusBar, updateStatusBarWithSummaryData} from './managers/StatusBarManager';
11 | import {CodeTimeView} from './sidebar/CodeTimeView';
12 | import { progressIt } from './managers/ProgressManager';
13 | import { LocalStorageManager } from './managers/LocalStorageManager';
14 | import { getCachedUser, reload } from './DataController';
15 | import { AUTH_TYPE, getAuthInstance } from './auth/AuthProvider';
16 |
17 | export function createCommands(
18 | ctx: ExtensionContext,
19 | kpmController: KpmManager,
20 | storageManager: LocalStorageManager
21 | ): {
22 | dispose: () => void;
23 | } {
24 | let cmds = [];
25 | ctx.subscriptions.push(getAuthInstance());
26 |
27 | cmds.push(kpmController);
28 |
29 | // INITALIZE SIDEBAR WEB VIEW PROVIDER
30 | const sidebar: CodeTimeView = new CodeTimeView(ctx.extensionUri);
31 | cmds.push(
32 | commands.registerCommand('codetime.softwareKpmDashboard', () => {
33 | launchWebUrl(`${app_url}/dashboard/code_time`)
34 | })
35 | )
36 |
37 | cmds.push(
38 | window.registerWebviewViewProvider('codetime.webView', sidebar, {
39 | webviewOptions: {
40 | retainContextWhenHidden: false,
41 | },
42 | })
43 | );
44 |
45 | // REFRESH EDITOR OPS SIDEBAR
46 | cmds.push(
47 | commands.registerCommand('codetime.refreshCodeTimeView', () => {
48 | sidebar.refresh();
49 | })
50 | );
51 |
52 | // DISPLAY EDITOR OPS SIDEBAR
53 | cmds.push(
54 | commands.registerCommand('codetime.displaySidebar', () => {
55 | // opens the sidebar manually from a the above command
56 | commands.executeCommand('workbench.view.extension.code-time-sidebar');
57 | })
58 | );
59 |
60 | // TOGGLE STATUS BAR METRIC VISIBILITY
61 | cmds.push(
62 | commands.registerCommand('codetime.toggleStatusBar', () => {
63 | toggleStatusBar();
64 | commands.executeCommand('codetime.refreshCodeTimeView');
65 | })
66 | );
67 |
68 | // LAUNCH SWITCH ACCOUNT
69 | cmds.push(
70 | commands.registerCommand('codetime.switchAccount', () => {
71 | authLogin();
72 | })
73 | );
74 |
75 | // LAUNCH EMAIL LOGIN
76 | cmds.push(
77 | commands.registerCommand('codetime.codeTimeLogin', (item: KpmItem) => {
78 | authLogin();
79 | })
80 | );
81 |
82 | // LAUNCH EMAIL LOGIN
83 | cmds.push(
84 | commands.registerCommand('codetime.codeTimeSignup', (item: KpmItem) => {
85 | authLogin();
86 | })
87 | );
88 |
89 | // LAUNCH SIGN UP FLOW
90 | cmds.push(
91 | commands.registerCommand('codetime.registerAccount', () => {
92 | authLogin();
93 | })
94 | );
95 |
96 | // LAUNCH EXISTING ACCOUNT LOGIN
97 | cmds.push(
98 | commands.registerCommand('codetime.login', () => {
99 | authLogin();
100 | })
101 | );
102 |
103 | // LAUNCH GOOGLE LOGIN
104 | cmds.push(
105 | commands.registerCommand('codetime.googleLogin', (item: KpmItem) => {
106 | authLogin();
107 | })
108 | );
109 |
110 | // LAUNCH GITHUB LOGIN
111 | cmds.push(
112 | commands.registerCommand('codetime.githubLogin', (item: KpmItem) => {
113 | authLogin();
114 | })
115 | );
116 |
117 | // SUBMIT AN ISSUE
118 | cmds.push(
119 | commands.registerCommand('codetime.submitAnIssue', (item: KpmItem) => {
120 | launchWebUrl(vscode_issues_url);
121 | })
122 | );
123 |
124 | // DISPLAY README MD
125 | cmds.push(
126 | commands.registerCommand('codetime.displayReadme', () => {
127 | displayReadme();
128 | })
129 | );
130 |
131 | // DISPLAY PROJECT METRICS REPORT
132 | cmds.push(
133 | commands.registerCommand('codetime.viewProjectReports', () => {
134 | launchWebUrl(`${app_url}/code_time/reports`);
135 | })
136 | );
137 |
138 | // DISPLAY CODETIME DASHBOARD WEBVIEW
139 | cmds.push(
140 | commands.registerCommand('codetime.viewDashboard', (params: any) => {
141 | showDashboard(params);
142 | })
143 | );
144 |
145 | cmds.push(
146 | commands.registerCommand('codetime.connectSlack', () => {
147 | launchWebUrl(`${app_url}/code_time/integration_type/slack`);
148 | })
149 | );
150 |
151 | cmds.push(
152 | commands.registerCommand('codetime.enableFlowMode', () => {
153 | enableFlow({automated: false});
154 | })
155 | );
156 |
157 | cmds.push(
158 | commands.registerCommand('codetime.exitFlowMode', () => {
159 | pauseFlow();
160 | })
161 | );
162 |
163 | cmds.push(
164 | commands.registerCommand('codetime.manageSlackConnection', () => {
165 | launchWebUrl(`${app_url}/code_time/integration_type/slack`);
166 | })
167 | );
168 |
169 | cmds.push(
170 | commands.registerCommand('codetime.skipSlackConnect', () => {
171 | setItem('vscode_CtskipSlackConnect', true);
172 | // refresh the view
173 | commands.executeCommand('codetime.refreshCodeTimeView');
174 | })
175 | );
176 |
177 | cmds.push(
178 | commands.registerCommand('codetime.updateViewMetrics', () => {
179 | updateFlowModeStatusBar();
180 | updateStatusBarWithSummaryData();
181 | })
182 | );
183 |
184 | // Close the settings view
185 | cmds.push(
186 | commands.registerCommand('codetime.closeSettings', (payload: any) => {
187 | closeSettings();
188 | })
189 | );
190 |
191 | cmds.push(
192 | commands.registerCommand('codetime.configureSettings', () => {
193 | configureSettings();
194 | })
195 | );
196 |
197 | cmds.push(
198 | commands.registerCommand('codetime.updateSidebarSettings', (payload: any) => {
199 | progressIt('Updating settings...', updateSettings, [payload.path, payload.json, true]);
200 | })
201 | );
202 |
203 | // Update the settings preferences
204 | cmds.push(
205 | commands.registerCommand('codetime.updateSettings', (payload: any) => {
206 | progressIt('Updating settings...', updateSettings, [payload.path, payload.json]);
207 | })
208 | );
209 |
210 | // show the org overview
211 | cmds.push(
212 | commands.registerCommand('codetime.showOrgDashboard', (slug: string) => {
213 | launchWebUrl(`${app_url}/organizations/${slug}`);
214 | })
215 | );
216 |
217 | // show the connect org view
218 | cmds.push(
219 | commands.registerCommand('codetime.createOrg', () => {
220 | launchWebUrl(`${app_url}/organizations/new`);
221 | })
222 | );
223 |
224 | // show the Software.com flow mode info
225 | cmds.push(
226 | commands.registerCommand('codetime.displayFlowModeInfo', () => {
227 | launchWebUrl("https://www.software.com/src/auto-flow-mode");
228 | })
229 | )
230 |
231 | cmds.push(
232 | commands.registerCommand('codetime.logout', async () => {
233 | const user = await getCachedUser()
234 | if (user?.registered) {
235 | // clear the storage and recreate an anon user
236 | storageManager.clearStorage();
237 |
238 | // reset the user session
239 | await createAnonymousUser();
240 |
241 | // update the login status
242 | showInformationMessage(`Successfully logged out of your Code Time account`);
243 | await reload()
244 | }
245 | })
246 | )
247 |
248 | cmds.push(
249 | commands.registerCommand('codetime.authSignIn', async () => {
250 | authLogin();
251 | })
252 | )
253 |
254 | cmds.push(
255 | authentication.onDidChangeSessions(async e => {
256 | await authentication.getSession(AUTH_TYPE, ['profile'], { createIfNone: false });
257 | })
258 | )
259 |
260 | return Disposable.from(...cmds);
261 | }
262 |
--------------------------------------------------------------------------------
/src/events/KpmItems.ts:
--------------------------------------------------------------------------------
1 | import {KpmItem, UIInteractionType} from '../model/models';
2 |
3 | export function configureSettingsKpmItem(): KpmItem {
4 | const item: KpmItem = new KpmItem();
5 | item.name = 'ct_configure_settings_btn';
6 | item.description = 'End of day notification - configure settings';
7 | item.location = 'ct_notification';
8 | item.label = 'Settings';
9 | item.interactionType = UIInteractionType.Click;
10 | item.interactionIcon = null;
11 | item.color = null;
12 | return item;
13 | }
14 |
15 | export function showMeTheDataKpmItem(): KpmItem {
16 | const item: KpmItem = new KpmItem();
17 | item.name = 'ct_show_me_the_data_btn';
18 | item.description = 'End of day notification - Show me the data';
19 | item.location = 'ct_notification';
20 | item.label = 'Show me the data';
21 | item.interactionType = UIInteractionType.Click;
22 | item.interactionIcon = null;
23 | item.color = null;
24 | return item;
25 | }
26 |
27 | export function getActionButton(
28 | label: string,
29 | tooltip: string,
30 | command: string,
31 | icon: any | null = null,
32 | eventDescription: string = '',
33 | color: any | null = null,
34 | description: string | null = ''
35 | ): KpmItem {
36 | const item: KpmItem = new KpmItem();
37 | item.tooltip = tooltip ?? '';
38 | item.label = label;
39 | item.id = label;
40 | item.command = command;
41 | item.icon = icon;
42 | item.contextValue = 'action_button';
43 | item.eventDescription = eventDescription;
44 | item.color = color;
45 | item.description = description;
46 | return item;
47 | }
48 |
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2018 Software. All Rights Reserved.
2 |
3 | // The module 'vscode' contains the VS Code extensibility API
4 | // Import the module and reference it with the alias vscode in your code below
5 | import {window, ExtensionContext, commands, authentication} from 'vscode';
6 | import {getUser, initializeAuthProvider} from './DataController';
7 | import {onboardInit} from './user/OnboardManager';
8 | import {
9 | getVersion,
10 | logIt,
11 | getPluginName,
12 | getItem,
13 | setItem,
14 | getWorkspaceName,
15 | isPrimaryWindow,
16 | displayReadme,
17 | getRandomNumberWithinRange,
18 | getBooleanItem
19 | } from './Util';
20 | import {createCommands} from './command-helper';
21 | import {KpmManager} from './managers/KpmManager';
22 | import {TrackerManager} from './managers/TrackerManager';
23 | import {initializeWebsockets, disposeWebsocketTimeouts} from './websockets';
24 | import {
25 | initializeStatusBar,
26 | updateFlowModeStatusBar,
27 | updateStatusBarWithSummaryData,
28 | } from './managers/StatusBarManager';
29 | import {SummaryManager} from './managers/SummaryManager';
30 | import {SyncManager} from './managers/SyncManger';
31 | import {ChangeStateManager} from './managers/ChangeStateManager';
32 | import {initializeFlowModeState} from './managers/FlowManager';
33 | import { ExtensionManager } from './managers/ExtensionManager';
34 | import { LocalStorageManager } from './managers/LocalStorageManager';
35 | import { setEndOfDayNotification } from './notifications/endOfDay';
36 | import { AUTH_TYPE, AuthProvider } from './auth/AuthProvider';
37 |
38 | let currentColorKind: number | undefined = undefined;
39 | let storageManager: LocalStorageManager | undefined = undefined;
40 | let user: any = null;
41 |
42 | const tracker: TrackerManager = TrackerManager.getInstance();
43 |
44 | //
45 | // Add the keystroke controller to the ext ctx, which
46 | // will then listen for text document changes.
47 | //
48 | const kpmController: KpmManager = KpmManager.getInstance();
49 |
50 | export function deactivate(ctx: ExtensionContext) {
51 | // store the deactivate event
52 | tracker.trackEditorAction('editor', 'deactivate');
53 |
54 | TrackerManager.getInstance().dispose();
55 | ChangeStateManager.getInstance().dispose();
56 | ExtensionManager.getInstance().dispose();
57 |
58 | // dispose the file watchers
59 | kpmController.dispose();
60 |
61 | if (isPrimaryWindow()) {
62 | if (storageManager) storageManager.clearDupStorageKeys();
63 | }
64 |
65 | disposeWebsocketTimeouts();
66 | }
67 |
68 | export async function activate(ctx: ExtensionContext) {
69 | const authProvider:AuthProvider = new AuthProvider(ctx);
70 | storageManager = LocalStorageManager.getInstance(ctx);
71 | initializeSession(storageManager);
72 | initializeAuthProvider(authProvider);
73 |
74 | // add the code time commands
75 | ctx.subscriptions.push(createCommands(ctx, kpmController, storageManager));
76 | TrackerManager.storageMgr = storageManager;
77 |
78 | // session: {id: , accessToken: , account: {label: , id: }, scopes: [,...]}
79 | const session = await authentication.getSession(AUTH_TYPE, [], { createIfNone: false });
80 | let jwt = getItem('jwt');
81 | user = await getUser();
82 | if (session) {
83 | // fetch the user with the non-session jwt to compare
84 | if (!user || user.email != session.account.label) {
85 | jwt = session.accessToken;
86 | // update the local storage with the new user
87 | setItem('name', session.account.label);
88 | setItem('jwt', jwt);
89 | user = await getUser(jwt);
90 | }
91 | } else if (jwt && user?.registered) {
92 | // update the session with the existing jwt
93 | authProvider.updateSession(jwt, user);
94 | }
95 |
96 | if (jwt) {
97 | intializePlugin();
98 | } else if (window.state.focused) {
99 | onboardInit(ctx, intializePlugin /*successFunction*/);
100 | } else {
101 | // 5 to 10 second delay
102 | const secondDelay = getRandomNumberWithinRange(6, 10);
103 | setTimeout(() => {
104 | onboardInit(ctx, intializePlugin /*successFunction*/);
105 | }, 1000 * secondDelay);
106 | }
107 | }
108 |
109 | export async function intializePlugin() {
110 | logIt(`Loaded ${getPluginName()} v${getVersion()}`);
111 |
112 | // INIT websockets
113 | try {
114 | initializeWebsockets();
115 | } catch (e: any) {
116 | logIt(`Failed to initialize websockets: ${e.message}`);
117 | }
118 |
119 | // INIT keystroke analysis tracker
120 | await tracker.init();
121 |
122 | // initialize user and preferences
123 | if (!user) user = await getUser();
124 |
125 | // show the sidebar if this is the 1st
126 | if (!getBooleanItem('vscode_CtInit')) {
127 | setItem('vscode_CtInit', true);
128 |
129 | setTimeout(() => {
130 | commands.executeCommand('codetime.displaySidebar');
131 | }, 1000);
132 |
133 | displayReadme();
134 | }
135 |
136 | initializeStatusBar();
137 |
138 | if (isPrimaryWindow()) {
139 | // store the activate event
140 | tracker.trackEditorAction('editor', 'activate');
141 | // it's the primary window. initialize flow mode and session summary information
142 | initializeFlowModeState();
143 | SummaryManager.getInstance().updateSessionSummaryFromServer();
144 | } else {
145 | // it's a secondary window. update the statusbar
146 | updateFlowModeStatusBar();
147 | updateStatusBarWithSummaryData();
148 | }
149 |
150 | setTimeout(() => {
151 | // INIT doc change events
152 | ChangeStateManager.getInstance();
153 |
154 | // INIT extension manager change listener
155 | ExtensionManager.getInstance().initialize();
156 |
157 | // INIT session summary sync manager
158 | SyncManager.getInstance();
159 | }, 3000);
160 |
161 | setTimeout(() => {
162 | // Set the end of the day notification trigger if it's enabled
163 | setEndOfDayNotification();
164 | }, 5000);
165 | }
166 |
167 | export function getCurrentColorKind() {
168 | if (!currentColorKind) {
169 | currentColorKind = window.activeColorTheme.kind;
170 | }
171 | return currentColorKind;
172 | }
173 |
174 | function initializeSession(storageManager: LocalStorageManager) {
175 | if (window.state.focused) {
176 | setItem('vscode_primary_window', getWorkspaceName());
177 | if (storageManager) storageManager.clearDupStorageKeys();
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/http/HttpClient.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { version, window } from 'vscode';
3 | import { app_url } from '../Constants';
4 | import {
5 | logIt,
6 | getPluginId,
7 | getPluginName,
8 | getVersion,
9 | getOs,
10 | getPluginUuid,
11 | getItem,
12 | setItem
13 | } from '../Util';
14 |
15 | // build the axios client
16 | const appApi: any = axios.create({
17 | baseURL: app_url,
18 | timeout: 15000,
19 | headers: {
20 | 'X-SWDC-Plugin-Id': getPluginId(),
21 | 'X-SWDC-Plugin-Name': getPluginName(),
22 | 'X-SWDC-Plugin-Version': getVersion(),
23 | 'X-SWDC-Plugin-OS': getOs(),
24 | 'X-SWDC-Plugin-UUID': getPluginUuid(),
25 | 'X-SWDC-Plugin-Type': 'codetime',
26 | 'X-SWDC-Plugin-Editor': 'vscode',
27 | 'X-SWDC-Plugin-Editor-Version': version
28 | }
29 | });
30 |
31 | // Evaluate these headers on every request since these values can change
32 | async function dynamicHeaders(override_token?: string) {
33 | let headers: any = {
34 | 'X-SWDC-Is-Light-Mode': (!!(window.activeColorTheme.kind === 1)).toString(),
35 | 'X-SWDC-Plugin-TZ': Intl.DateTimeFormat().resolvedOptions().timeZone,
36 | 'X-SWDC-Plugin-Offset': new Date().getTimezoneOffset()
37 | }
38 |
39 | const token = await getAuthorization()
40 |
41 | if (token || override_token) {
42 | if (override_token) {
43 | headers['Authorization'] = override_token;
44 | } else {
45 | headers['Authorization'] = token;
46 | }
47 | }
48 |
49 | return headers
50 | }
51 |
52 | export async function appGet(api: string, queryParams: any = {}, token_override: any = '') {
53 | return await appApi.get(api, { params: queryParams, headers: await dynamicHeaders(token_override) }).catch((err: any) => {
54 | logIt(`error for GET ${api}, message: ${err.message}`);
55 | if (getResponseStatus(err?.response) === 401) {
56 | // clear the JWT because it is invalid
57 | setItem('jwt', null)
58 | }
59 | return err;
60 | });
61 | }
62 |
63 | export async function appPut(api: string, payload: any) {
64 | return await appApi.put(api, payload, { headers: await dynamicHeaders() }).catch((err: any) => {
65 | logIt(`error for PUT ${api}, message: ${err.message}`);
66 | return err;
67 | });
68 | }
69 |
70 | export async function appPost(api: string, payload: any) {
71 | return await appApi.post(api, payload, { headers: await dynamicHeaders() }).catch((err: any) => {
72 | logIt(`error for POST ${api}, message: ${err.message}`);
73 | return err;
74 | });
75 | }
76 |
77 | export async function appDelete(api: string) {
78 | return await appApi.delete(api, { headers: await dynamicHeaders() }).catch((err: any) => {
79 | logIt(`error for DELETE ${api}, message: ${err.message}`);
80 | return err;
81 | });
82 | }
83 |
84 | /**
85 | * check if the reponse is ok or not
86 | * axios always sends the following
87 | * status:200
88 | * statusText:"OK"
89 | *
90 | code:"ENOTFOUND"
91 | config:Object {adapter: , transformRequest: Object, transformResponse: Object, …}
92 | errno:"ENOTFOUND"
93 | host:"api.spotify.com"
94 | hostname:"api.spotify.com"
95 | message:"getaddrinfo ENOTFOUND api.spotify.com api.spotify.com:443"
96 | port:443
97 | */
98 | export function isResponseOk(resp: any) {
99 | let status = getResponseStatus(resp);
100 | if (status && resp && status < 300) {
101 | return true;
102 | }
103 | return false;
104 | }
105 |
106 | function getResponseStatus(resp: any) {
107 | let status = null;
108 | if (resp?.status) {
109 | status = resp.status;
110 | } else if (resp?.response && resp.response.status) {
111 | status = resp.response.status;
112 | } else if (resp?.code === 'ECONNABORTED') {
113 | status = 500;
114 | } else if (resp?.code === 'ECONNREFUSED') {
115 | status = 503;
116 | }
117 | return status;
118 | }
119 |
120 | async function getAuthorization() {
121 | const token = getItem('jwt');
122 |
123 | // Split the string and return the last portion incase it has a prefix like `JWT `
124 | return token?.trim().split(' ').at(-1);
125 | }
126 |
--------------------------------------------------------------------------------
/src/local/404.ts:
--------------------------------------------------------------------------------
1 | export async function getConnectionErrorHtml() {
2 | return `
3 |
4 |
5 |
6 |
7 | Code Time
8 |
67 |
78 |
79 |
80 |
81 |
Oops! Something went wrong.
82 |
83 |
84 | It looks like this view is temporarily unavailable, but we're working to fix the problem.
85 |