git-watchtower 1.6.0 → 1.7.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/bin/git-watchtower.js +89 -9
- package/package.json +6 -1
- package/sounds/README.md +34 -0
- package/src/casino/index.js +721 -0
- package/src/casino/sounds.js +245 -0
- package/src/cli/args.js +239 -0
- package/src/config/loader.js +329 -0
- package/src/config/schema.js +305 -0
- package/src/git/branch.js +428 -0
- package/src/git/commands.js +416 -0
- package/src/git/pr.js +111 -0
- package/src/git/remote.js +127 -0
- package/src/index.js +179 -0
- package/src/polling/engine.js +157 -0
- package/src/server/process.js +329 -0
- package/src/server/static.js +95 -0
- package/src/state/store.js +527 -0
- package/src/telemetry/analytics.js +142 -0
- package/src/telemetry/config.js +123 -0
- package/src/telemetry/index.js +93 -0
- package/src/ui/actions.js +425 -0
- package/src/ui/ansi.js +498 -0
- package/src/ui/keybindings.js +198 -0
- package/src/ui/renderer.js +1326 -0
- package/src/utils/async.js +219 -0
- package/src/utils/browser.js +40 -0
- package/src/utils/errors.js +490 -0
- package/src/utils/gitignore.js +174 -0
- package/src/utils/sound.js +33 -0
- package/src/utils/time.js +27 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostHog analytics wrapper (singleton)
|
|
3
|
+
*
|
|
4
|
+
* All methods are safe no-ops when telemetry is disabled.
|
|
5
|
+
* Events are fire-and-forget — never blocks the TUI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { isTelemetryEnabled, getOrCreateDistinctId } = require('./config');
|
|
9
|
+
|
|
10
|
+
const POSTHOG_API_KEY = 'phc_fdGL8TVN5aFPXmQ4f1hI8y6sqnscD7dy9j5SM5gTylG';
|
|
11
|
+
const POSTHOG_HOST = 'https://us.i.posthog.com';
|
|
12
|
+
|
|
13
|
+
/** @type {import('posthog-node').PostHog | null} */
|
|
14
|
+
let client = null;
|
|
15
|
+
let distinctId = '';
|
|
16
|
+
let appVersion = '';
|
|
17
|
+
let enabled = false;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize the PostHog client if telemetry is enabled
|
|
21
|
+
* @param {{ version: string }} options
|
|
22
|
+
*/
|
|
23
|
+
function init({ version }) {
|
|
24
|
+
appVersion = version;
|
|
25
|
+
|
|
26
|
+
if (!isTelemetryEnabled()) {
|
|
27
|
+
enabled = false;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const { PostHog } = require('posthog-node');
|
|
33
|
+
distinctId = getOrCreateDistinctId();
|
|
34
|
+
client = new PostHog(POSTHOG_API_KEY, {
|
|
35
|
+
host: POSTHOG_HOST,
|
|
36
|
+
flushAt: 10,
|
|
37
|
+
flushInterval: 30000,
|
|
38
|
+
requestTimeout: 5000,
|
|
39
|
+
disableGeoip: true,
|
|
40
|
+
});
|
|
41
|
+
enabled = true;
|
|
42
|
+
} catch {
|
|
43
|
+
// If posthog-node fails to load, silently disable telemetry
|
|
44
|
+
enabled = false;
|
|
45
|
+
client = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Capture an analytics event (fire-and-forget)
|
|
51
|
+
* @param {string} event - Event name
|
|
52
|
+
* @param {Record<string, any>} [properties] - Event properties
|
|
53
|
+
*/
|
|
54
|
+
function capture(event, properties = {}) {
|
|
55
|
+
if (!enabled || !client) return;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
client.capture({
|
|
59
|
+
distinctId,
|
|
60
|
+
event,
|
|
61
|
+
properties: {
|
|
62
|
+
...properties,
|
|
63
|
+
$lib: 'git-watchtower',
|
|
64
|
+
$lib_version: appVersion,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
} catch {
|
|
68
|
+
// Never let telemetry errors affect the app
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Capture an error for PostHog error tracking
|
|
74
|
+
* @param {Error} error
|
|
75
|
+
*/
|
|
76
|
+
function captureError(error) {
|
|
77
|
+
if (!enabled || !client) return;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const errorType = error.constructor?.name || 'Error';
|
|
81
|
+
const errorCode = /** @type {any} */ (error).code || '';
|
|
82
|
+
const errorMessage = errorCode || (error.message || '').substring(0, 200);
|
|
83
|
+
|
|
84
|
+
/** @type {Record<string, any>} */
|
|
85
|
+
const properties = {
|
|
86
|
+
$exception_type: errorType,
|
|
87
|
+
$exception_message: errorMessage,
|
|
88
|
+
$exception_source: 'node',
|
|
89
|
+
$lib: 'git-watchtower',
|
|
90
|
+
$lib_version: appVersion,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Include stack trace — contains only our package's file paths and
|
|
94
|
+
// line numbers, no user data. Required for PostHog error tracking
|
|
95
|
+
// to group, deduplicate, and show useful backtraces.
|
|
96
|
+
if (error.stack) {
|
|
97
|
+
properties.$exception_stack_trace_raw = error.stack;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
client.capture({
|
|
101
|
+
distinctId,
|
|
102
|
+
event: '$exception',
|
|
103
|
+
properties,
|
|
104
|
+
});
|
|
105
|
+
} catch {
|
|
106
|
+
// Never let telemetry errors affect the app
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Flush pending events and shutdown the PostHog client
|
|
112
|
+
* Call this before process exit to ensure events are sent.
|
|
113
|
+
* @returns {Promise<void>}
|
|
114
|
+
*/
|
|
115
|
+
async function shutdown() {
|
|
116
|
+
if (!enabled || !client) return;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await client.shutdown();
|
|
120
|
+
} catch {
|
|
121
|
+
// Best-effort flush
|
|
122
|
+
} finally {
|
|
123
|
+
client = null;
|
|
124
|
+
enabled = false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if telemetry is currently active
|
|
130
|
+
* @returns {boolean}
|
|
131
|
+
*/
|
|
132
|
+
function isEnabled() {
|
|
133
|
+
return enabled;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
init,
|
|
138
|
+
capture,
|
|
139
|
+
captureError,
|
|
140
|
+
shutdown,
|
|
141
|
+
isEnabled,
|
|
142
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry configuration management
|
|
3
|
+
*
|
|
4
|
+
* Stores telemetry preferences in ~/.git-watchtower/config.json
|
|
5
|
+
* (separate from per-project .watchtowerrc.json).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
|
|
13
|
+
const CONFIG_DIR_NAME = '.git-watchtower';
|
|
14
|
+
const CONFIG_FILE_NAME = 'config.json';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the telemetry config directory path
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function getConfigDir() {
|
|
21
|
+
return path.join(os.homedir(), CONFIG_DIR_NAME);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the telemetry config file path
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function getConfigPath() {
|
|
29
|
+
return path.join(getConfigDir(), CONFIG_FILE_NAME);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Load telemetry config from disk
|
|
34
|
+
* @returns {{ telemetryEnabled: boolean, distinctId: string, promptedAt: string } | null}
|
|
35
|
+
*/
|
|
36
|
+
function loadTelemetryConfig() {
|
|
37
|
+
try {
|
|
38
|
+
const data = fs.readFileSync(getConfigPath(), 'utf8');
|
|
39
|
+
const config = JSON.parse(data);
|
|
40
|
+
if (config && typeof config === 'object') {
|
|
41
|
+
return config;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Save telemetry config to disk
|
|
51
|
+
* @param {{ telemetryEnabled: boolean, distinctId: string, promptedAt: string }} config
|
|
52
|
+
*/
|
|
53
|
+
function saveTelemetryConfig(config) {
|
|
54
|
+
const dir = getConfigDir();
|
|
55
|
+
if (!fs.existsSync(dir)) {
|
|
56
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get existing distinctId or create a new one
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
function getOrCreateDistinctId() {
|
|
66
|
+
const config = loadTelemetryConfig();
|
|
67
|
+
if (config && config.distinctId) {
|
|
68
|
+
return config.distinctId;
|
|
69
|
+
}
|
|
70
|
+
return crypto.randomUUID();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if telemetry is enabled
|
|
75
|
+
*
|
|
76
|
+
* Priority:
|
|
77
|
+
* 1. GIT_WATCHTOWER_TELEMETRY=false env var always disables (CI/corporate)
|
|
78
|
+
* 2. If no config file exists, telemetry is disabled (not yet prompted)
|
|
79
|
+
* 3. Otherwise, return the stored preference
|
|
80
|
+
*
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
function isTelemetryEnabled() {
|
|
84
|
+
const envVar = process.env.GIT_WATCHTOWER_TELEMETRY;
|
|
85
|
+
if (envVar !== undefined && envVar.toLowerCase() === 'false') {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const config = loadTelemetryConfig();
|
|
90
|
+
if (!config) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return config.telemetryEnabled === true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if the user has already been prompted for telemetry
|
|
99
|
+
* @returns {boolean}
|
|
100
|
+
*/
|
|
101
|
+
function hasBeenPrompted() {
|
|
102
|
+
return loadTelemetryConfig() !== null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if telemetry is force-disabled via environment variable
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
function isEnvDisabled() {
|
|
110
|
+
const envVar = process.env.GIT_WATCHTOWER_TELEMETRY;
|
|
111
|
+
return envVar !== undefined && envVar.toLowerCase() === 'false';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
getConfigDir,
|
|
116
|
+
getConfigPath,
|
|
117
|
+
loadTelemetryConfig,
|
|
118
|
+
saveTelemetryConfig,
|
|
119
|
+
getOrCreateDistinctId,
|
|
120
|
+
isTelemetryEnabled,
|
|
121
|
+
hasBeenPrompted,
|
|
122
|
+
isEnvDisabled,
|
|
123
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry public API
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const telemetry = require('../src/telemetry');
|
|
6
|
+
* await telemetry.promptIfNeeded(promptYesNoFn);
|
|
7
|
+
* telemetry.init({ version: '1.6.1' });
|
|
8
|
+
* telemetry.capture('tool_launched', { os: 'linux' });
|
|
9
|
+
* await telemetry.shutdown();
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const analytics = require('./analytics');
|
|
13
|
+
const config = require('./config');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Show opt-in telemetry prompt if the user hasn't been asked yet.
|
|
17
|
+
*
|
|
18
|
+
* Skips the prompt when:
|
|
19
|
+
* - Already prompted (config file exists)
|
|
20
|
+
* - Env var forces telemetry off
|
|
21
|
+
* - No TTY (non-interactive / CI)
|
|
22
|
+
*
|
|
23
|
+
* @param {(question: string, defaultValue?: boolean) => Promise<boolean>} promptYesNo
|
|
24
|
+
*/
|
|
25
|
+
async function promptIfNeeded(promptYesNo) {
|
|
26
|
+
// Already prompted — respect existing choice
|
|
27
|
+
if (config.hasBeenPrompted()) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Env var forces off — no point asking
|
|
32
|
+
if (config.isEnvDisabled()) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Non-interactive — default to disabled
|
|
37
|
+
if (!process.stdin.isTTY) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log('\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510');
|
|
43
|
+
console.log('\u2502 Help improve Git Watchtower \u2502');
|
|
44
|
+
console.log('\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524');
|
|
45
|
+
console.log('\u2502 We\'d love to collect anonymous usage data to improve \u2502');
|
|
46
|
+
console.log('\u2502 Git Watchtower. This telemetry is used to improve the \u2502');
|
|
47
|
+
console.log('\u2502 product. It includes: \u2502');
|
|
48
|
+
console.log('\u2502 - Which features are used (not your code or data) \u2502');
|
|
49
|
+
console.log('\u2502 - Error types encountered (not stack traces) \u2502');
|
|
50
|
+
console.log('\u2502 - Session duration and OS/Node.js version \u2502');
|
|
51
|
+
console.log('\u2502 \u2502');
|
|
52
|
+
console.log('\u2502 No personal information, file contents, branch names, \u2502');
|
|
53
|
+
console.log('\u2502 or repository data is ever collected. \u2502');
|
|
54
|
+
console.log('\u2502 \u2502');
|
|
55
|
+
console.log('\u2502 You can change this anytime by editing: \u2502');
|
|
56
|
+
console.log('\u2502 ~/.git-watchtower/config.json \u2502');
|
|
57
|
+
console.log('\u2502 Or set GIT_WATCHTOWER_TELEMETRY=false \u2502');
|
|
58
|
+
console.log('\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518');
|
|
59
|
+
console.log('');
|
|
60
|
+
|
|
61
|
+
const answer = await promptYesNo('Enable anonymous telemetry to help improve Git Watchtower?', false);
|
|
62
|
+
|
|
63
|
+
const distinctId = config.getOrCreateDistinctId();
|
|
64
|
+
config.saveTelemetryConfig({
|
|
65
|
+
telemetryEnabled: answer,
|
|
66
|
+
distinctId,
|
|
67
|
+
promptedAt: new Date().toISOString(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (answer) {
|
|
71
|
+
console.log(' Thank you! Telemetry enabled.\n');
|
|
72
|
+
} else {
|
|
73
|
+
console.log(' No problem! Telemetry disabled.\n');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
// Analytics
|
|
79
|
+
init: analytics.init,
|
|
80
|
+
capture: analytics.capture,
|
|
81
|
+
captureError: analytics.captureError,
|
|
82
|
+
shutdown: analytics.shutdown,
|
|
83
|
+
isEnabled: analytics.isEnabled,
|
|
84
|
+
|
|
85
|
+
// Prompt
|
|
86
|
+
promptIfNeeded,
|
|
87
|
+
|
|
88
|
+
// Config (for advanced use)
|
|
89
|
+
isTelemetryEnabled: config.isTelemetryEnabled,
|
|
90
|
+
hasBeenPrompted: config.hasBeenPrompted,
|
|
91
|
+
loadTelemetryConfig: config.loadTelemetryConfig,
|
|
92
|
+
saveTelemetryConfig: config.saveTelemetryConfig,
|
|
93
|
+
};
|