claude-code-wakatime 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,7 +8,10 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const os_1 = __importDefault(require("os"));
10
10
  const child_process_1 = require("child_process");
11
+ const options_1 = require("./options");
12
+ const VERSION = '1.0.0';
11
13
  const STATE_FILE = path_1.default.join(os_1.default.homedir(), '.wakatime', 'claude-code.json');
14
+ const SESSION_LOG_FILE = path_1.default.join(os_1.default.homedir(), '.wakatime', 'claude-sessions.log');
12
15
  const WAKATIME_CLI = path_1.default.join(os_1.default.homedir(), '.wakatime', 'wakatime-cli');
13
16
  function timestamp() {
14
17
  return Date.now() / 1000;
@@ -22,19 +25,64 @@ function shouldSendHeartbeat() {
22
25
  return true;
23
26
  }
24
27
  }
28
+ function parseInput() {
29
+ try {
30
+ const stdinData = fs_1.default.readFileSync(0, 'utf-8');
31
+ if (stdinData.trim()) {
32
+ const input = JSON.parse(stdinData);
33
+ return input;
34
+ }
35
+ }
36
+ catch (err) {
37
+ console.error(err);
38
+ }
39
+ return undefined;
40
+ }
41
+ function logSessionData(inp) {
42
+ try {
43
+ fs_1.default.mkdirSync(path_1.default.dirname(SESSION_LOG_FILE), { recursive: true });
44
+ fs_1.default.appendFileSync(SESSION_LOG_FILE, JSON.stringify(inp, null, 2) + '\n\n');
45
+ }
46
+ catch (err) {
47
+ // ignore
48
+ }
49
+ }
25
50
  function updateState() {
26
51
  fs_1.default.mkdirSync(path_1.default.dirname(STATE_FILE), { recursive: true });
27
52
  fs_1.default.writeFileSync(STATE_FILE, JSON.stringify({ lastHeartbeatAt: timestamp() }, null, 2));
28
53
  }
29
- function sendHeartbeat() {
54
+ function sendHeartbeat(inp) {
55
+ const projectFolder = inp?.cwd;
30
56
  try {
31
- (0, child_process_1.execFileSync)(WAKATIME_CLI, ['--entity', 'claude code', '--entity-type', 'app', '--category', 'ai coding']);
57
+ const args = [
58
+ '--entity',
59
+ 'claude code',
60
+ '--entity-type',
61
+ 'app',
62
+ '--category',
63
+ 'ai coding',
64
+ '--plugin',
65
+ `claude-code-wakatime/${VERSION}`,
66
+ ];
67
+ if (projectFolder) {
68
+ args.push('--project-folder');
69
+ args.push(projectFolder);
70
+ }
71
+ (0, child_process_1.execFileSync)(WAKATIME_CLI, args);
32
72
  }
33
73
  catch (err) {
34
74
  console.error('Failed to send WakaTime heartbeat:', err.message);
35
75
  }
36
76
  }
37
- if (shouldSendHeartbeat()) {
38
- sendHeartbeat();
39
- updateState();
77
+ function main() {
78
+ const inp = parseInput();
79
+ const options = new options_1.Options();
80
+ const debug = options.getSetting('settings', 'debug');
81
+ if (inp && debug === 'true')
82
+ logSessionData(inp);
83
+ if (shouldSendHeartbeat()) {
84
+ sendHeartbeat(inp);
85
+ updateState();
86
+ }
40
87
  }
88
+ main();
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Options = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const utils_1 = require("./utils");
40
+ class Options {
41
+ constructor() {
42
+ const home = utils_1.Utils.getHomeDirectory();
43
+ const wakaFolder = path.join(home, '.wakatime');
44
+ try {
45
+ if (!fs.existsSync(wakaFolder)) {
46
+ fs.mkdirSync(wakaFolder, { recursive: true });
47
+ }
48
+ this.resourcesLocation = wakaFolder;
49
+ }
50
+ catch (e) {
51
+ console.error(e);
52
+ throw e;
53
+ }
54
+ this.configFile = path.join(home, '.wakatime.cfg');
55
+ this.internalConfigFile = path.join(this.resourcesLocation, 'wakatime-internal.cfg');
56
+ this.logFile = path.join(this.resourcesLocation, 'wakatime.log');
57
+ }
58
+ getSetting(section, key, internal) {
59
+ const content = fs.readFileSync(this.getConfigFile(internal ?? false), 'utf-8');
60
+ if (content.trim()) {
61
+ let currentSection = '';
62
+ let lines = content.split('\n');
63
+ for (var i = 0; i < lines.length; i++) {
64
+ let line = lines[i];
65
+ if (this.startsWith(line.trim(), '[') && this.endsWith(line.trim(), ']')) {
66
+ currentSection = line
67
+ .trim()
68
+ .substring(1, line.trim().length - 1)
69
+ .toLowerCase();
70
+ }
71
+ else if (currentSection === section) {
72
+ let parts = line.split('=');
73
+ let currentKey = parts[0].trim();
74
+ if (currentKey === key && parts.length > 1) {
75
+ return this.removeNulls(parts[1].trim());
76
+ }
77
+ }
78
+ }
79
+ return undefined;
80
+ }
81
+ }
82
+ setSetting(section, key, val, internal) {
83
+ const configFile = this.getConfigFile(internal);
84
+ fs.readFile(configFile, 'utf-8', (err, content) => {
85
+ // ignore errors because config file might not exist yet
86
+ if (err)
87
+ content = '';
88
+ let contents = [];
89
+ let currentSection = '';
90
+ let found = false;
91
+ let lines = content.split('\n');
92
+ for (var i = 0; i < lines.length; i++) {
93
+ let line = lines[i];
94
+ if (this.startsWith(line.trim(), '[') && this.endsWith(line.trim(), ']')) {
95
+ if (currentSection === section && !found) {
96
+ contents.push(this.removeNulls(key + ' = ' + val));
97
+ found = true;
98
+ }
99
+ currentSection = line
100
+ .trim()
101
+ .substring(1, line.trim().length - 1)
102
+ .toLowerCase();
103
+ contents.push(this.removeNulls(line));
104
+ }
105
+ else if (currentSection === section) {
106
+ let parts = line.split('=');
107
+ let currentKey = parts[0].trim();
108
+ if (currentKey === key) {
109
+ if (!found) {
110
+ contents.push(this.removeNulls(key + ' = ' + val));
111
+ found = true;
112
+ }
113
+ }
114
+ else {
115
+ contents.push(this.removeNulls(line));
116
+ }
117
+ }
118
+ else {
119
+ contents.push(this.removeNulls(line));
120
+ }
121
+ }
122
+ if (!found) {
123
+ if (currentSection !== section) {
124
+ contents.push('[' + section + ']');
125
+ }
126
+ contents.push(this.removeNulls(key + ' = ' + val));
127
+ }
128
+ fs.writeFile(configFile, contents.join('\n'), (err) => {
129
+ if (err)
130
+ throw err;
131
+ });
132
+ });
133
+ }
134
+ setSettings(section, settings, internal) {
135
+ const configFile = this.getConfigFile(internal);
136
+ fs.readFile(configFile, 'utf-8', (err, content) => {
137
+ // ignore errors because config file might not exist yet
138
+ if (err)
139
+ content = '';
140
+ let contents = [];
141
+ let currentSection = '';
142
+ const found = {};
143
+ let lines = content.split('\n');
144
+ for (var i = 0; i < lines.length; i++) {
145
+ let line = lines[i];
146
+ if (this.startsWith(line.trim(), '[') && this.endsWith(line.trim(), ']')) {
147
+ if (currentSection === section) {
148
+ settings.forEach((setting) => {
149
+ if (!found[setting.key]) {
150
+ contents.push(this.removeNulls(setting.key + ' = ' + setting.value));
151
+ found[setting.key] = true;
152
+ }
153
+ });
154
+ }
155
+ currentSection = line
156
+ .trim()
157
+ .substring(1, line.trim().length - 1)
158
+ .toLowerCase();
159
+ contents.push(this.removeNulls(line));
160
+ }
161
+ else if (currentSection === section) {
162
+ let parts = line.split('=');
163
+ let currentKey = parts[0].trim();
164
+ let keepLineUnchanged = true;
165
+ settings.forEach((setting) => {
166
+ if (currentKey === setting.key) {
167
+ keepLineUnchanged = false;
168
+ if (!found[setting.key]) {
169
+ contents.push(this.removeNulls(setting.key + ' = ' + setting.value));
170
+ found[setting.key] = true;
171
+ }
172
+ }
173
+ });
174
+ if (keepLineUnchanged) {
175
+ contents.push(this.removeNulls(line));
176
+ }
177
+ }
178
+ else {
179
+ contents.push(this.removeNulls(line));
180
+ }
181
+ }
182
+ settings.forEach((setting) => {
183
+ if (!found[setting.key]) {
184
+ if (currentSection !== section) {
185
+ contents.push('[' + section + ']');
186
+ currentSection = section;
187
+ }
188
+ contents.push(this.removeNulls(setting.key + ' = ' + setting.value));
189
+ found[setting.key] = true;
190
+ }
191
+ });
192
+ fs.writeFile(configFile, contents.join('\n'), (err) => {
193
+ if (err)
194
+ throw err;
195
+ });
196
+ });
197
+ }
198
+ getConfigFile(internal) {
199
+ return internal ? this.internalConfigFile : this.configFile;
200
+ }
201
+ getLogFile() {
202
+ return this.logFile;
203
+ }
204
+ startsWith(outer, inner) {
205
+ return outer.slice(0, inner.length) === inner;
206
+ }
207
+ endsWith(outer, inner) {
208
+ return inner === '' || outer.slice(-inner.length) === inner;
209
+ }
210
+ removeNulls(s) {
211
+ return s.replace(/\0/g, '');
212
+ }
213
+ }
214
+ exports.Options = Options;
package/dist/utils.js ADDED
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Utils = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ class Utils {
40
+ static quote(str) {
41
+ if (str.includes(' '))
42
+ return `"${str.replace('"', '\\"')}"`;
43
+ return str;
44
+ }
45
+ static apiKeyInvalid(key) {
46
+ const err = 'Invalid api key... check https://wakatime.com/api-key for your key';
47
+ if (!key)
48
+ return err;
49
+ const re = new RegExp('^(waka_)?[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$', 'i');
50
+ if (!re.test(key))
51
+ return err;
52
+ return '';
53
+ }
54
+ static formatDate(date) {
55
+ let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
56
+ let ampm = 'AM';
57
+ let hour = date.getHours();
58
+ if (hour > 11) {
59
+ ampm = 'PM';
60
+ hour = hour - 12;
61
+ }
62
+ if (hour == 0) {
63
+ hour = 12;
64
+ }
65
+ let minute = date.getMinutes();
66
+ return `${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()} ${hour}:${minute < 10 ? `0${minute}` : minute} ${ampm}`;
67
+ }
68
+ static obfuscateKey(key) {
69
+ let newKey = '';
70
+ if (key) {
71
+ newKey = key;
72
+ if (key.length > 4)
73
+ newKey = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX' + key.substring(key.length - 4);
74
+ }
75
+ return newKey;
76
+ }
77
+ static wrapArg(arg) {
78
+ if (arg.indexOf(' ') > -1)
79
+ return '"' + arg.replace(/"/g, '\\"') + '"';
80
+ return arg;
81
+ }
82
+ static formatArguments(binary, args) {
83
+ let clone = args.slice(0);
84
+ clone.unshift(this.wrapArg(binary));
85
+ let newCmds = [];
86
+ let lastCmd = '';
87
+ for (let i = 0; i < clone.length; i++) {
88
+ if (lastCmd == '--key')
89
+ newCmds.push(this.wrapArg(this.obfuscateKey(clone[i])));
90
+ else
91
+ newCmds.push(this.wrapArg(clone[i]));
92
+ lastCmd = clone[i];
93
+ }
94
+ return newCmds.join(' ');
95
+ }
96
+ static apiUrlToDashboardUrl(url) {
97
+ url = url
98
+ .replace('://api.', '://')
99
+ .replace('/api/v1', '')
100
+ .replace(/^api\./, '')
101
+ .replace('/api', '');
102
+ return url;
103
+ }
104
+ static isWindows() {
105
+ return os.platform() === 'win32';
106
+ }
107
+ static getHomeDirectory() {
108
+ let home = process.env.WAKATIME_HOME;
109
+ if (home && home.trim() && fs.existsSync(home.trim()))
110
+ return home.trim();
111
+ return process.env[this.isWindows() ? 'USERPROFILE' : 'HOME'] || process.cwd();
112
+ }
113
+ static buildOptions(stdin) {
114
+ const options = {
115
+ windowsHide: true,
116
+ };
117
+ if (stdin) {
118
+ options.stdio = ['pipe', 'pipe', 'pipe'];
119
+ }
120
+ if (!this.isWindows() && !process.env.WAKATIME_HOME && !process.env.HOME) {
121
+ options['env'] = { ...process.env, WAKATIME_HOME: this.getHomeDirectory() };
122
+ }
123
+ return options;
124
+ }
125
+ }
126
+ exports.Utils = Utils;
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "claude-code-wakatime",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "WakaTime plugin for Claude Code",
5
5
  "bin": {
6
6
  "claude-code-wakatime": "dist/index.js"
7
7
  },
8
8
  "scripts": {
9
9
  "postinstall": "node dist/install-hooks.js",
10
+ "prebuild": "node scripts/generate-version.js",
10
11
  "build": "tsc",
11
- "watch": "tsc --watch",
12
+ "watch": "npm run prebuild && tsc --watch",
12
13
  "release:major": "npm version major && npm publish && git push && git push --tags",
13
14
  "release:minor": "npm version minor && npm publish && git push && git push --tags",
14
15
  "release:patch": "npm version patch && npm publish && git push && git push --tags"
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const packagePath = path.join(__dirname, '..', 'package.json');
7
+ const versionFilePath = path.join(__dirname, '..', 'src', 'version.ts');
8
+
9
+ try {
10
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
11
+ const version = packageJson.version;
12
+
13
+ const versionFileContent = `// This file is auto-generated during build. Do not edit manually.
14
+ export const VERSION = '${version}';
15
+ `;
16
+
17
+ fs.writeFileSync(versionFilePath, versionFileContent);
18
+ console.log(`Generated version.ts with version ${version}`);
19
+ } catch (error) {
20
+ console.error('Error generating version file:', error);
21
+ process.exit(1);
22
+ }
package/src/index.ts CHANGED
@@ -4,14 +4,23 @@ import fs from 'fs';
4
4
  import path from 'path';
5
5
  import os from 'os';
6
6
  import { execFileSync } from 'child_process';
7
-
7
+ import { Options } from './options';
8
+ import { VERSION } from './version';
8
9
  const STATE_FILE = path.join(os.homedir(), '.wakatime', 'claude-code.json');
10
+ const SESSION_LOG_FILE = path.join(os.homedir(), '.wakatime', 'claude-sessions.log');
9
11
  const WAKATIME_CLI = path.join(os.homedir(), '.wakatime', 'wakatime-cli');
10
12
 
11
13
  type State = {
12
14
  lastHeartbeatAt?: number;
13
15
  };
14
16
 
17
+ type Input = {
18
+ session_id: string;
19
+ transcript_path: string;
20
+ cwd: string;
21
+ hook_event_name: string;
22
+ };
23
+
15
24
  function timestamp() {
16
25
  return Date.now() / 1000;
17
26
  }
@@ -25,20 +34,68 @@ function shouldSendHeartbeat(): boolean {
25
34
  }
26
35
  }
27
36
 
37
+ function parseInput() {
38
+ try {
39
+ const stdinData = fs.readFileSync(0, 'utf-8');
40
+ if (stdinData.trim()) {
41
+ const input: Input = JSON.parse(stdinData);
42
+ return input;
43
+ }
44
+ } catch (err) {
45
+ console.error(err);
46
+ }
47
+ return undefined;
48
+ }
49
+
50
+ function logSessionData(inp: Input) {
51
+ try {
52
+ fs.mkdirSync(path.dirname(SESSION_LOG_FILE), { recursive: true });
53
+ fs.appendFileSync(SESSION_LOG_FILE, JSON.stringify(inp, null, 2) + '\n\n');
54
+ } catch (err) {
55
+ // ignore
56
+ }
57
+ }
58
+
28
59
  function updateState() {
29
60
  fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
30
61
  fs.writeFileSync(STATE_FILE, JSON.stringify({ lastHeartbeatAt: timestamp() } as State, null, 2));
31
62
  }
32
63
 
33
- function sendHeartbeat() {
64
+ function sendHeartbeat(inp?: Input) {
65
+ const projectFolder = inp?.cwd;
34
66
  try {
35
- execFileSync(WAKATIME_CLI, ['--entity', 'claude code', '--entity-type', 'app', '--category', 'ai coding']);
67
+ const args: string[] = [
68
+ '--entity',
69
+ 'claude code',
70
+ '--entity-type',
71
+ 'app',
72
+ '--category',
73
+ 'ai coding',
74
+ '--plugin',
75
+ `claude-code-wakatime/${VERSION}`,
76
+ ];
77
+ if (projectFolder) {
78
+ args.push('--project-folder');
79
+ args.push(projectFolder);
80
+ }
81
+ execFileSync(WAKATIME_CLI, args);
36
82
  } catch (err: any) {
37
83
  console.error('Failed to send WakaTime heartbeat:', err.message);
38
84
  }
39
85
  }
40
86
 
41
- if (shouldSendHeartbeat()) {
42
- sendHeartbeat();
43
- updateState();
87
+ function main() {
88
+ const inp = parseInput();
89
+
90
+ const options = new Options();
91
+ const debug = options.getSetting('settings', 'debug');
92
+
93
+ if (inp && debug === 'true') logSessionData(inp);
94
+
95
+ if (shouldSendHeartbeat()) {
96
+ sendHeartbeat(inp);
97
+ updateState();
98
+ }
44
99
  }
100
+
101
+ main();
package/src/options.ts ADDED
@@ -0,0 +1,198 @@
1
+ import * as child_process from 'child_process';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ import { Utils } from './utils';
6
+
7
+ export interface Setting {
8
+ key: string;
9
+ value: string;
10
+ error?: string;
11
+ }
12
+
13
+ export class Options {
14
+ private configFile: string;
15
+ private internalConfigFile: string;
16
+ private resourcesLocation: string;
17
+ private logFile: string;
18
+
19
+ constructor() {
20
+ const home = Utils.getHomeDirectory();
21
+ const wakaFolder = path.join(home, '.wakatime');
22
+ try {
23
+ if (!fs.existsSync(wakaFolder)) {
24
+ fs.mkdirSync(wakaFolder, { recursive: true });
25
+ }
26
+ this.resourcesLocation = wakaFolder;
27
+ } catch (e) {
28
+ console.error(e);
29
+ throw e;
30
+ }
31
+
32
+ this.configFile = path.join(home, '.wakatime.cfg');
33
+ this.internalConfigFile = path.join(this.resourcesLocation, 'wakatime-internal.cfg');
34
+ this.logFile = path.join(this.resourcesLocation, 'wakatime.log');
35
+ }
36
+
37
+ public getSetting(section: string, key: string, internal?: boolean): string | undefined {
38
+ const content = fs.readFileSync(this.getConfigFile(internal ?? false), 'utf-8');
39
+ if (content.trim()) {
40
+ let currentSection = '';
41
+ let lines = content.split('\n');
42
+ for (var i = 0; i < lines.length; i++) {
43
+ let line = lines[i];
44
+ if (this.startsWith(line.trim(), '[') && this.endsWith(line.trim(), ']')) {
45
+ currentSection = line
46
+ .trim()
47
+ .substring(1, line.trim().length - 1)
48
+ .toLowerCase();
49
+ } else if (currentSection === section) {
50
+ let parts = line.split('=');
51
+ let currentKey = parts[0].trim();
52
+ if (currentKey === key && parts.length > 1) {
53
+ return this.removeNulls(parts[1].trim());
54
+ }
55
+ }
56
+ }
57
+
58
+ return undefined;
59
+ }
60
+ }
61
+
62
+ public setSetting(section: string, key: string, val: string, internal: boolean): void {
63
+ const configFile = this.getConfigFile(internal);
64
+ fs.readFile(configFile, 'utf-8', (err: NodeJS.ErrnoException | null, content: string) => {
65
+ // ignore errors because config file might not exist yet
66
+ if (err) content = '';
67
+
68
+ let contents: string[] = [];
69
+ let currentSection = '';
70
+
71
+ let found = false;
72
+ let lines = content.split('\n');
73
+ for (var i = 0; i < lines.length; i++) {
74
+ let line = lines[i];
75
+ if (this.startsWith(line.trim(), '[') && this.endsWith(line.trim(), ']')) {
76
+ if (currentSection === section && !found) {
77
+ contents.push(this.removeNulls(key + ' = ' + val));
78
+ found = true;
79
+ }
80
+ currentSection = line
81
+ .trim()
82
+ .substring(1, line.trim().length - 1)
83
+ .toLowerCase();
84
+ contents.push(this.removeNulls(line));
85
+ } else if (currentSection === section) {
86
+ let parts = line.split('=');
87
+ let currentKey = parts[0].trim();
88
+ if (currentKey === key) {
89
+ if (!found) {
90
+ contents.push(this.removeNulls(key + ' = ' + val));
91
+ found = true;
92
+ }
93
+ } else {
94
+ contents.push(this.removeNulls(line));
95
+ }
96
+ } else {
97
+ contents.push(this.removeNulls(line));
98
+ }
99
+ }
100
+
101
+ if (!found) {
102
+ if (currentSection !== section) {
103
+ contents.push('[' + section + ']');
104
+ }
105
+ contents.push(this.removeNulls(key + ' = ' + val));
106
+ }
107
+
108
+ fs.writeFile(configFile as string, contents.join('\n'), (err) => {
109
+ if (err) throw err;
110
+ });
111
+ });
112
+ }
113
+
114
+ public setSettings(section: string, settings: Setting[], internal: boolean): void {
115
+ const configFile = this.getConfigFile(internal);
116
+ fs.readFile(configFile, 'utf-8', (err: NodeJS.ErrnoException | null, content: string) => {
117
+ // ignore errors because config file might not exist yet
118
+ if (err) content = '';
119
+
120
+ let contents: string[] = [];
121
+ let currentSection = '';
122
+
123
+ const found: Record<string, boolean> = {};
124
+ let lines = content.split('\n');
125
+ for (var i = 0; i < lines.length; i++) {
126
+ let line = lines[i];
127
+ if (this.startsWith(line.trim(), '[') && this.endsWith(line.trim(), ']')) {
128
+ if (currentSection === section) {
129
+ settings.forEach((setting) => {
130
+ if (!found[setting.key]) {
131
+ contents.push(this.removeNulls(setting.key + ' = ' + setting.value));
132
+ found[setting.key] = true;
133
+ }
134
+ });
135
+ }
136
+ currentSection = line
137
+ .trim()
138
+ .substring(1, line.trim().length - 1)
139
+ .toLowerCase();
140
+ contents.push(this.removeNulls(line));
141
+ } else if (currentSection === section) {
142
+ let parts = line.split('=');
143
+ let currentKey = parts[0].trim();
144
+ let keepLineUnchanged = true;
145
+ settings.forEach((setting) => {
146
+ if (currentKey === setting.key) {
147
+ keepLineUnchanged = false;
148
+ if (!found[setting.key]) {
149
+ contents.push(this.removeNulls(setting.key + ' = ' + setting.value));
150
+ found[setting.key] = true;
151
+ }
152
+ }
153
+ });
154
+ if (keepLineUnchanged) {
155
+ contents.push(this.removeNulls(line));
156
+ }
157
+ } else {
158
+ contents.push(this.removeNulls(line));
159
+ }
160
+ }
161
+
162
+ settings.forEach((setting) => {
163
+ if (!found[setting.key]) {
164
+ if (currentSection !== section) {
165
+ contents.push('[' + section + ']');
166
+ currentSection = section;
167
+ }
168
+ contents.push(this.removeNulls(setting.key + ' = ' + setting.value));
169
+ found[setting.key] = true;
170
+ }
171
+ });
172
+
173
+ fs.writeFile(configFile as string, contents.join('\n'), (err) => {
174
+ if (err) throw err;
175
+ });
176
+ });
177
+ }
178
+
179
+ public getConfigFile(internal: boolean): string {
180
+ return internal ? this.internalConfigFile : this.configFile;
181
+ }
182
+
183
+ public getLogFile(): string {
184
+ return this.logFile;
185
+ }
186
+
187
+ private startsWith(outer: string, inner: string): boolean {
188
+ return outer.slice(0, inner.length) === inner;
189
+ }
190
+
191
+ private endsWith(outer: string, inner: string): boolean {
192
+ return inner === '' || outer.slice(-inner.length) === inner;
193
+ }
194
+
195
+ private removeNulls(s: string): string {
196
+ return s.replace(/\0/g, '');
197
+ }
198
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,93 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as child_process from 'child_process';
4
+ import { StdioOptions } from 'child_process';
5
+
6
+ export class Utils {
7
+ public static quote(str: string): string {
8
+ if (str.includes(' ')) return `"${str.replace('"', '\\"')}"`;
9
+ return str;
10
+ }
11
+
12
+ public static apiKeyInvalid(key?: string): string {
13
+ const err = 'Invalid api key... check https://wakatime.com/api-key for your key';
14
+ if (!key) return err;
15
+ const re = new RegExp('^(waka_)?[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$', 'i');
16
+ if (!re.test(key)) return err;
17
+ return '';
18
+ }
19
+
20
+ public static formatDate(date: Date): String {
21
+ let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
22
+ let ampm = 'AM';
23
+ let hour = date.getHours();
24
+ if (hour > 11) {
25
+ ampm = 'PM';
26
+ hour = hour - 12;
27
+ }
28
+ if (hour == 0) {
29
+ hour = 12;
30
+ }
31
+ let minute = date.getMinutes();
32
+ return `${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()} ${hour}:${minute < 10 ? `0${minute}` : minute} ${ampm}`;
33
+ }
34
+
35
+ public static obfuscateKey(key: string): string {
36
+ let newKey = '';
37
+ if (key) {
38
+ newKey = key;
39
+ if (key.length > 4) newKey = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX' + key.substring(key.length - 4);
40
+ }
41
+ return newKey;
42
+ }
43
+
44
+ public static wrapArg(arg: string): string {
45
+ if (arg.indexOf(' ') > -1) return '"' + arg.replace(/"/g, '\\"') + '"';
46
+ return arg;
47
+ }
48
+
49
+ public static formatArguments(binary: string, args: string[]): string {
50
+ let clone = args.slice(0);
51
+ clone.unshift(this.wrapArg(binary));
52
+ let newCmds: string[] = [];
53
+ let lastCmd = '';
54
+ for (let i = 0; i < clone.length; i++) {
55
+ if (lastCmd == '--key') newCmds.push(this.wrapArg(this.obfuscateKey(clone[i])));
56
+ else newCmds.push(this.wrapArg(clone[i]));
57
+ lastCmd = clone[i];
58
+ }
59
+ return newCmds.join(' ');
60
+ }
61
+
62
+ public static apiUrlToDashboardUrl(url: string): string {
63
+ url = url
64
+ .replace('://api.', '://')
65
+ .replace('/api/v1', '')
66
+ .replace(/^api\./, '')
67
+ .replace('/api', '');
68
+ return url;
69
+ }
70
+
71
+ public static isWindows(): boolean {
72
+ return os.platform() === 'win32';
73
+ }
74
+
75
+ public static getHomeDirectory(): string {
76
+ let home = process.env.WAKATIME_HOME;
77
+ if (home && home.trim() && fs.existsSync(home.trim())) return home.trim();
78
+ return process.env[this.isWindows() ? 'USERPROFILE' : 'HOME'] || process.cwd();
79
+ }
80
+
81
+ public static buildOptions(stdin?: boolean): Object {
82
+ const options: child_process.ExecFileOptions = {
83
+ windowsHide: true,
84
+ };
85
+ if (stdin) {
86
+ (options as any).stdio = ['pipe', 'pipe', 'pipe'] as StdioOptions;
87
+ }
88
+ if (!this.isWindows() && !process.env.WAKATIME_HOME && !process.env.HOME) {
89
+ options['env'] = { ...process.env, WAKATIME_HOME: this.getHomeDirectory() };
90
+ }
91
+ return options;
92
+ }
93
+ }