@tanagram/cli 0.5.53 → 0.5.55

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.
Binary file
Binary file
Binary file
Binary file
Binary file
package/install.js CHANGED
@@ -171,6 +171,62 @@ function installClaudeSkill() {
171
171
  }
172
172
  }
173
173
 
174
+ function installStopHook() {
175
+ if (process.env.TANAGRAM_SKIP_HOOK === '1' || process.env.TANAGRAM_SKIP_HOOK === 'true') {
176
+ console.error('Skipping Tanagram Stop hook installation (TANAGRAM_SKIP_HOOK set).');
177
+ return;
178
+ }
179
+ if (isCIEnvironment()) {
180
+ console.error('Skipping Tanagram Stop hook installation (CI environment).');
181
+ return;
182
+ }
183
+
184
+ console.error('Installing Tanagram Stop hook for Claude Code...');
185
+
186
+ try {
187
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
188
+ let settings = {};
189
+
190
+ if (fs.existsSync(settingsPath)) {
191
+ const raw = fs.readFileSync(settingsPath, 'utf8');
192
+ settings = JSON.parse(raw);
193
+ }
194
+
195
+ // Ensure hooks.Stop array exists
196
+ if (!settings.hooks) settings.hooks = {};
197
+ if (!Array.isArray(settings.hooks.Stop)) settings.hooks.Stop = [];
198
+
199
+ // Check if tanagram stop-hook is already configured
200
+ const alreadyInstalled = settings.hooks.Stop.some(entry =>
201
+ Array.isArray(entry.hooks) && entry.hooks.some(h => h.command === 'tanagram stop-hook')
202
+ );
203
+
204
+ if (alreadyInstalled) {
205
+ console.error('✓ Tanagram Stop hook already configured');
206
+ return;
207
+ }
208
+
209
+ settings.hooks.Stop.push({
210
+ hooks: [
211
+ {
212
+ type: 'command',
213
+ command: 'tanagram stop-hook',
214
+ timeout: 120
215
+ }
216
+ ]
217
+ });
218
+
219
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
220
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
221
+
222
+ console.error(`✓ Tanagram Stop hook installed to ${settingsPath}`);
223
+ track('cli.stop_hook.install.success');
224
+ } catch (err) {
225
+ console.error('Warning: Failed to install Stop hook:', err.message);
226
+ track('cli.stop_hook.install.failure', { error: err.message });
227
+ }
228
+ }
229
+
174
230
  function ensureOpenCode() {
175
231
  if (isCIEnvironment()) {
176
232
  return;
@@ -217,6 +273,9 @@ function ensureOpenCode() {
217
273
  // Install Claude skill
218
274
  installClaudeSkill();
219
275
 
276
+ // Install Stop hook for Claude Code
277
+ installStopHook();
278
+
220
279
  // Install OpenCode (LLM agent for eval)
221
280
  ensureOpenCode();
222
281
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanagram/cli",
3
- "version": "0.5.53",
3
+ "version": "0.5.55",
4
4
  "description": "Tanagram - Catch sloppy code before it ships",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "postinstall": "node install.js",
11
+ "preuninstall": "node uninstall.js",
11
12
  "test": "go test ./..."
12
13
  },
13
14
  "keywords": [
@@ -33,6 +34,7 @@
33
34
  "bin/tanagram.js",
34
35
  "dist/npm/",
35
36
  "install.js",
37
+ "uninstall.js",
36
38
  "skills/",
37
39
  "README.md",
38
40
  "LICENSE"
package/uninstall.js ADDED
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const https = require('https');
7
+ const crypto = require('crypto');
8
+ const pkg = require('./package.json');
9
+
10
+ const POSTHOG_KEY = 'phc_sMsUvf0nK50rZdztSlX9rDJqIreLcXj4dyGS0tORQpQ';
11
+ const POSTHOG_HOST = 'phe.tanagram.ai';
12
+
13
+ function getDistinctId() {
14
+ const machineId = os.hostname() + os.userInfo().username;
15
+ return crypto.createHash('sha256').update(machineId).digest('hex').slice(0, 16);
16
+ }
17
+
18
+ function track(event, properties = {}) {
19
+ if (isCIEnvironment()) return;
20
+
21
+ const data = JSON.stringify({
22
+ api_key: POSTHOG_KEY,
23
+ event: event,
24
+ properties: {
25
+ distinct_id: getDistinctId(),
26
+ version: pkg.version,
27
+ platform: os.platform(),
28
+ arch: os.arch(),
29
+ node_version: process.version,
30
+ ...properties
31
+ },
32
+ timestamp: new Date().toISOString()
33
+ });
34
+
35
+ const req = https.request({
36
+ hostname: POSTHOG_HOST,
37
+ port: 443,
38
+ path: '/capture/',
39
+ method: 'POST',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ 'Content-Length': data.length
43
+ }
44
+ });
45
+
46
+ req.on('error', () => {});
47
+ req.write(data);
48
+ req.end();
49
+ }
50
+
51
+ function isCIEnvironment() {
52
+ return (
53
+ process.env.CI === 'true' ||
54
+ process.env.GITHUB_ACTIONS === 'true' ||
55
+ process.env.BUILDKITE === 'true' ||
56
+ process.env.GITLAB_CI === 'true' ||
57
+ process.env.TF_BUILD === 'True'
58
+ );
59
+ }
60
+
61
+ function removeClaudeSkill() {
62
+ const skillsDir = path.join(os.homedir(), '.claude', 'skills', 'tanagram');
63
+
64
+ try {
65
+ if (fs.existsSync(skillsDir)) {
66
+ fs.rmSync(skillsDir, { recursive: true });
67
+ console.error('✓ Tanagram skill removed');
68
+ track('cli.skill.uninstall.success');
69
+ }
70
+ } catch (err) {
71
+ console.error('Warning: Failed to remove Tanagram skill:', err.message);
72
+ track('cli.skill.uninstall.failure', { error: err.message });
73
+ }
74
+ }
75
+
76
+ function removeStopHook() {
77
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
78
+
79
+ try {
80
+ if (!fs.existsSync(settingsPath)) return;
81
+
82
+ const raw = fs.readFileSync(settingsPath, 'utf8');
83
+ const settings = JSON.parse(raw);
84
+
85
+ if (!settings.hooks || !Array.isArray(settings.hooks.Stop)) return;
86
+
87
+ const before = settings.hooks.Stop.length;
88
+ settings.hooks.Stop = settings.hooks.Stop.filter(entry =>
89
+ !(Array.isArray(entry.hooks) && entry.hooks.some(h => h.command === 'tanagram stop-hook'))
90
+ );
91
+ const after = settings.hooks.Stop.length;
92
+
93
+ if (before === after) return;
94
+
95
+ // Clean up empty arrays/objects
96
+ if (settings.hooks.Stop.length === 0) {
97
+ delete settings.hooks.Stop;
98
+ }
99
+ if (Object.keys(settings.hooks).length === 0) {
100
+ delete settings.hooks;
101
+ }
102
+
103
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
104
+ console.error('✓ Tanagram Stop hook removed');
105
+ track('cli.stop_hook.uninstall.success');
106
+ } catch (err) {
107
+ console.error('Warning: Failed to remove Stop hook:', err.message);
108
+ track('cli.stop_hook.uninstall.failure', { error: err.message });
109
+ }
110
+ }
111
+
112
+ function removeOpenCode() {
113
+ if (isCIEnvironment()) return;
114
+
115
+ const { execSync } = require('child_process');
116
+
117
+ try {
118
+ execSync('opencode --version', { stdio: 'ignore' });
119
+ } catch {
120
+ // Not installed, nothing to do
121
+ return;
122
+ }
123
+
124
+ try {
125
+ execSync('npm uninstall -g opencode-ai', { stdio: 'pipe' });
126
+ console.error('✓ OpenCode uninstalled');
127
+ track('cli.opencode.uninstall.success');
128
+ } catch (err) {
129
+ console.error('Warning: Failed to uninstall OpenCode:', err.message);
130
+ track('cli.opencode.uninstall.failure', { error: err.message });
131
+ }
132
+ }
133
+
134
+ // Main uninstall flow
135
+ track('cli.uninstall.start');
136
+
137
+ removeClaudeSkill();
138
+ removeStopHook();
139
+ removeOpenCode();
140
+
141
+ track('cli.uninstall.success');
142
+ console.error('✓ Tanagram CLI uninstalled successfully');