claude-code-wakatime 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 logFile: string;
17
+ public resourcesLocation: 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,97 @@
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
+
94
+ public static timestamp() {
95
+ return Date.now() / 1000;
96
+ }
97
+ }