@tanagram/cli 0.1.20 → 0.1.23

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/README.md CHANGED
@@ -215,47 +215,4 @@ Then run `tanagram` to enforce them locally!
215
215
 
216
216
  ---
217
217
 
218
- ## Contributing
219
-
220
- Contributions are welcome! Please feel free to submit a Pull Request.
221
-
222
- ### Development Setup
223
-
224
- ```bash
225
- # Clone the repository
226
- git clone https://github.com/tanagram/cli.git
227
- cd cli
228
-
229
- # Install dependencies and build
230
- npm install
231
-
232
- # Run tests
233
- npm test
234
-
235
- # Build manually
236
- go build -o bin/tanagram .
237
- ```
238
-
239
- ### Publishing a Release
240
-
241
- To publish a new version:
242
-
243
- ```bash
244
- # 1. Update version in package.json
245
- npm version patch # or minor, or major
246
-
247
- # 2. Commit and tag
248
- git add package.json package-lock.json
249
- git commit -m "Bump version to $(node -p "require('./package.json').version")"
250
- git tag v$(node -p "require('./package.json').version")
251
- git push && git push origin --tags
252
-
253
- # 3. Publish to npm
254
- npm publish
255
- ```
256
-
257
- **Note:** The Go compiler is automatically downloaded and used during `npm install` via the `go-bin` package, so no pre-built binaries needed!
258
-
259
- ## License
260
-
261
- MIT
218
+ Built by [@fluttermatt](https://x.com/fluttermatt) and the Tanagram team. Talk to us [on Twitter](https://x.com/tanagram_) or email: founders AT tanagram.ai
package/commands/run.go CHANGED
@@ -11,6 +11,7 @@ import (
11
11
  "github.com/tanagram/cli/checker"
12
12
  "github.com/tanagram/cli/extractor"
13
13
  "github.com/tanagram/cli/git"
14
+ "github.com/tanagram/cli/metrics"
14
15
  "github.com/tanagram/cli/parser"
15
16
  "github.com/tanagram/cli/storage"
16
17
  )
@@ -72,6 +73,7 @@ func Run() error {
72
73
  if len(filesToSync) > 0 {
73
74
  fmt.Printf("\nSyncing policies with LLM (processing %d changed file(s) in parallel)...\n", len(filesToSync))
74
75
 
76
+ syncStart := time.Now()
75
77
  ctx := context.Background()
76
78
 
77
79
  // Result type for collecting sync results
@@ -164,7 +166,15 @@ func Run() error {
164
166
  return fmt.Errorf("failed to save cache: %w", err)
165
167
  }
166
168
 
169
+ syncDuration := time.Since(syncStart)
167
170
  fmt.Printf("\n✓ Synced %d policies from %d changed file(s)\n", totalPolicies, len(filesToSync))
171
+
172
+ // Track sync metrics
173
+ metrics.Track("cli.sync.complete", map[string]interface{}{
174
+ "files_synced": len(filesToSync),
175
+ "policies_synced": totalPolicies,
176
+ "duration_seconds": syncDuration.Seconds(),
177
+ })
168
178
  }
169
179
 
170
180
  // Load all policies from cache
@@ -196,7 +206,18 @@ func Run() error {
196
206
 
197
207
  // Check changes against policies (both regex and LLM-based)
198
208
  ctx := context.Background()
209
+ checkStart := time.Now()
199
210
  result := checker.CheckChanges(ctx, diffResult.Changes, policies)
211
+ checkDuration := time.Since(checkStart)
212
+
213
+ // Track policy check results (similar to policy.execute.result in github-app)
214
+ metrics.Track("cli.policy.check.result", map[string]interface{}{
215
+ "policies_checked": len(policies),
216
+ "changes_checked": len(diffResult.Changes),
217
+ "violations_found": len(result.Violations),
218
+ "has_violations": len(result.Violations) > 0,
219
+ "duration_seconds": checkDuration.Seconds(),
220
+ })
200
221
 
201
222
  // Handle results based on whether violations were found
202
223
  if len(result.Violations) > 0 {
package/go.mod CHANGED
@@ -5,7 +5,9 @@ go 1.23.0
5
5
  require github.com/anthropics/anthropic-sdk-go v1.17.0
6
6
 
7
7
  require (
8
- github.com/stretchr/testify v1.9.0 // indirect
8
+ github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
9
+ github.com/posthog/posthog-go v1.6.12 // indirect
10
+ github.com/stretchr/testify v1.10.0 // indirect
9
11
  github.com/tidwall/gjson v1.18.0 // indirect
10
12
  github.com/tidwall/match v1.1.1 // indirect
11
13
  github.com/tidwall/pretty v1.2.1 // indirect
package/go.sum CHANGED
@@ -2,10 +2,16 @@ github.com/anthropics/anthropic-sdk-go v1.17.0 h1:BwK8ApcmaAUkvZTiQE0yi3R9XneEFs
2
2
  github.com/anthropics/anthropic-sdk-go v1.17.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
3
3
  github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4
4
  github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5
+ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
6
+ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
5
7
  github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6
8
  github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
9
+ github.com/posthog/posthog-go v1.6.12 h1:rsOBL/YdMfLJtOVjKJLgdzYmvaL3aIW6IVbAteSe+aI=
10
+ github.com/posthog/posthog-go v1.6.12/go.mod h1:LcC1Nu4AgvV22EndTtrMXTy+7RGVC0MhChSw7Qk5XkY=
7
11
  github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
8
12
  github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
13
+ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
14
+ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
9
15
  github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
10
16
  github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
11
17
  github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
package/install.js CHANGED
@@ -6,48 +6,94 @@ const path = require('path');
6
6
  const os = require('os');
7
7
 
8
8
  const GO_VERSION = '1.21.0';
9
+ const GO_CACHE_DIR = path.join(os.homedir(), '.tanagram', `go-${GO_VERSION}`);
9
10
 
10
- // Build binary using go-bin
11
- async function buildWithGoBin() {
12
- const platform = os.platform();
13
- const arch = os.arch();
14
- const goos = platform === 'win32' ? 'windows' : platform;
15
- const goarch = arch === 'x64' ? 'amd64' : arch === 'arm64' ? 'arm64' : arch;
16
- const binaryName = platform === 'win32' ? 'tanagram.exe' : 'tanagram';
17
- const binaryPath = path.join(__dirname, 'bin', binaryName);
11
+ // Check if Go is already installed locally
12
+ function checkLocalGo() {
13
+ try {
14
+ const version = execSync('go version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
15
+ console.log(`✓ Found local Go: ${version.trim()}`);
16
+ return 'go';
17
+ } catch (error) {
18
+ return null;
19
+ }
20
+ }
18
21
 
19
- const binDir = path.join(__dirname, 'bin');
20
- if (!fs.existsSync(binDir)) {
21
- fs.mkdirSync(binDir, { recursive: true });
22
+ // Check if go-bin was previously cached
23
+ function checkCachedGo() {
24
+ const cachedGoPath = path.join(GO_CACHE_DIR, 'bin', 'go');
25
+ if (fs.existsSync(cachedGoPath)) {
26
+ console.log(`✓ Using cached Go from ${GO_CACHE_DIR}`);
27
+ return cachedGoPath;
22
28
  }
29
+ return null;
30
+ }
23
31
 
24
- console.log('📦 Installing Go compiler via npm...');
32
+ // Download and cache Go using go-bin
33
+ async function downloadGo() {
34
+ console.log('📦 Downloading Go compiler via npm...');
25
35
 
26
36
  try {
27
- // Install Go using go-bin
28
- // Use require.resolve to find go-bin in node_modules
37
+ // Load go-bin
29
38
  let goBin;
30
39
  try {
31
40
  goBin = require('go-bin');
32
41
  } catch (err) {
33
- // Fallback: try to require from parent node_modules
34
42
  const parentPath = path.join(__dirname, '..', '..', 'go-bin');
35
43
  goBin = require(parentPath);
36
44
  }
37
- const goDir = await goBin({ version: GO_VERSION, dir: __dirname });
38
- const goPath = path.join(goDir, 'bin', 'go');
39
45
 
40
- console.log('🔧 Building Tanagram CLI...');
46
+ // Create cache directory
47
+ const cacheParent = path.dirname(GO_CACHE_DIR);
48
+ if (!fs.existsSync(cacheParent)) {
49
+ fs.mkdirSync(cacheParent, { recursive: true });
50
+ }
51
+
52
+ // Download Go to cache
53
+ const goDir = await goBin({
54
+ version: GO_VERSION,
55
+ dir: cacheParent,
56
+ includeTag: true
57
+ });
41
58
 
42
- // Build the binary using the downloaded Go
43
- execSync(`"${goPath}" build -o "${binaryPath}" .`, {
59
+ console.log(`✓ Go cached at ${goDir}`);
60
+ return path.join(goDir, 'bin', 'go');
61
+ } catch (error) {
62
+ throw new Error(`Failed to download Go: ${error.message}`);
63
+ }
64
+ }
65
+
66
+ // Build the binary
67
+ function buildBinary(goCommand) {
68
+ const platform = os.platform();
69
+ const arch = os.arch();
70
+ const goos = platform === 'win32' ? 'windows' : platform;
71
+ const goarch = arch === 'x64' ? 'amd64' : arch === 'arm64' ? 'arm64' : arch;
72
+ const binaryName = platform === 'win32' ? 'tanagram.exe' : 'tanagram';
73
+ const binaryPath = path.join(__dirname, 'bin', binaryName);
74
+
75
+ const binDir = path.join(__dirname, 'bin');
76
+ if (!fs.existsSync(binDir)) {
77
+ fs.mkdirSync(binDir, { recursive: true });
78
+ }
79
+
80
+ console.log('🔧 Building Tanagram CLI...');
81
+
82
+ // Build ldflags to inject PostHog key at build time
83
+ let ldflags = '';
84
+ if (process.env.POSTHOG_WRITE_KEY) {
85
+ ldflags = `-ldflags="-X 'github.com/tanagram/cli/metrics.posthogWriteKey=${process.env.POSTHOG_WRITE_KEY}'"`;
86
+ console.log(' 📊 Injecting PostHog metrics key...');
87
+ }
88
+
89
+ try {
90
+ execSync(`"${goCommand}" build ${ldflags} -o "${binaryPath}" .`, {
44
91
  cwd: __dirname,
45
92
  stdio: 'inherit',
46
93
  env: {
47
94
  ...process.env,
48
95
  GOOS: goos,
49
- GOARCH: goarch,
50
- GOROOT: goDir
96
+ GOARCH: goarch
51
97
  }
52
98
  });
53
99
 
@@ -55,29 +101,35 @@ async function buildWithGoBin() {
55
101
  fs.chmodSync(binaryPath, '755');
56
102
  }
57
103
 
58
- // Clean up Go installation
59
- console.log('🧹 Cleaning up...');
60
- fs.rmSync(goDir, { recursive: true, force: true });
61
-
62
104
  console.log('✓ Tanagram CLI installed successfully');
63
105
  return true;
64
106
  } catch (error) {
65
- console.error('Failed to build with go-bin:', error.message);
66
- return false;
107
+ throw new Error(`Build failed: ${error.message}`);
67
108
  }
68
109
  }
69
110
 
70
111
  // Main installation flow
71
112
  (async () => {
72
113
  try {
73
- const built = await buildWithGoBin();
74
- if (!built) {
75
- console.error('\n❌ Installation failed');
76
- process.exit(1);
114
+ // 1. Check if Go is already installed locally
115
+ let goCommand = checkLocalGo();
116
+
117
+ // 2. If not, check if we have a cached go-bin download
118
+ if (!goCommand) {
119
+ goCommand = checkCachedGo();
120
+ }
121
+
122
+ // 3. If still no Go, download via go-bin and cache it
123
+ if (!goCommand) {
124
+ goCommand = await downloadGo();
77
125
  }
126
+
127
+ // 4. Build the binary
128
+ buildBinary(goCommand);
129
+
78
130
  process.exit(0);
79
131
  } catch (error) {
80
- console.error('Installation error:', error.message);
132
+ console.error('\n❌ Installation failed:', error.message);
81
133
  process.exit(1);
82
134
  }
83
135
  })();
package/main.go CHANGED
@@ -5,9 +5,14 @@ import (
5
5
  "os"
6
6
 
7
7
  "github.com/tanagram/cli/commands"
8
+ "github.com/tanagram/cli/metrics"
8
9
  )
9
10
 
10
11
  func main() {
12
+ // Initialize metrics (similar to PosthogService initialization)
13
+ metrics.Init()
14
+ defer metrics.Close()
15
+
11
16
  // Get subcommand (default to "run" if none provided)
12
17
  subcommand := "run"
13
18
  if len(os.Args) > 1 {
@@ -17,10 +22,19 @@ func main() {
17
22
  var err error
18
23
  switch subcommand {
19
24
  case "run":
25
+ metrics.Track("command.execute", map[string]interface{}{
26
+ "command": "run",
27
+ })
20
28
  err = commands.Run()
21
29
  case "sync":
30
+ metrics.Track("command.execute", map[string]interface{}{
31
+ "command": "sync",
32
+ })
22
33
  err = commands.Sync()
23
34
  case "list":
35
+ metrics.Track("command.execute", map[string]interface{}{
36
+ "command": "list",
37
+ })
24
38
  err = commands.List()
25
39
  case "help", "-h", "--help":
26
40
  printHelp()
@@ -32,6 +46,10 @@ func main() {
32
46
  }
33
47
 
34
48
  if err != nil {
49
+ metrics.Track("command.error", map[string]interface{}{
50
+ "command": subcommand,
51
+ "error": err.Error(),
52
+ })
35
53
  fmt.Fprintf(os.Stderr, "Error: %v\n", err)
36
54
  os.Exit(1)
37
55
  }
@@ -0,0 +1,116 @@
1
+ package metrics
2
+
3
+ import (
4
+ "os"
5
+ "runtime"
6
+ "time"
7
+
8
+ "github.com/posthog/posthog-go"
9
+ )
10
+
11
+ var (
12
+ client posthog.Client
13
+
14
+ // Injected at build time via -ldflags
15
+ posthogWriteKey = ""
16
+ posthogHost = "https://us.i.posthog.com"
17
+ )
18
+
19
+ // Init initializes the PostHog client
20
+ // Similar to PosthogService.py and webui/app/lib/posthog.ts
21
+ func Init() {
22
+ // Use embedded key (set at build time)
23
+ writeKey := posthogWriteKey
24
+
25
+ // Allow override via env var for local development
26
+ if envKey := os.Getenv("POSTHOG_WRITE_KEY"); envKey != "" {
27
+ writeKey = envKey
28
+ }
29
+
30
+ if writeKey == "" {
31
+ // Fail silently if not configured - metrics are optional
32
+ return
33
+ }
34
+
35
+ host := posthogHost
36
+ // Allow override via env var for local development
37
+ if envHost := os.Getenv("POSTHOG_HOST"); envHost != "" {
38
+ host = envHost
39
+ }
40
+
41
+ var err error
42
+ client, err = posthog.NewWithConfig(
43
+ writeKey,
44
+ posthog.Config{
45
+ Endpoint: host,
46
+ },
47
+ )
48
+ if err != nil {
49
+ // Log but don't fail - metrics shouldn't break the CLI
50
+ return
51
+ }
52
+ }
53
+
54
+ // Close flushes and closes the PostHog client
55
+ func Close() {
56
+ if client != nil {
57
+ client.Close()
58
+ }
59
+ }
60
+
61
+ // Track sends an event to PostHog
62
+ // Similar to MetricsService.put_metric_single_count()
63
+ func Track(event string, properties map[string]interface{}) {
64
+ if client == nil {
65
+ return // Silently skip if not initialized
66
+ }
67
+
68
+ if properties == nil {
69
+ properties = make(map[string]interface{})
70
+ }
71
+
72
+ // Add context dimensions similar to MetricsService
73
+ properties["$process_person_profile"] = false // Match Python implementation
74
+ properties["_deployment_env"] = getDeploymentEnv()
75
+ properties["os"] = runtime.GOOS
76
+ properties["arch"] = runtime.GOARCH
77
+ properties["cli_version"] = getVersion()
78
+
79
+ err := client.Enqueue(posthog.Capture{
80
+ DistinctId: getDistinctId(),
81
+ Event: event,
82
+ Properties: properties,
83
+ Timestamp: time.Now(),
84
+ })
85
+
86
+ if err != nil {
87
+ // Silently fail - don't break CLI execution for metrics
88
+ return
89
+ }
90
+ }
91
+
92
+ // getDistinctId returns a stable anonymous identifier
93
+ // Similar to user_id_for_github_dot_com() pattern
94
+ func getDistinctId() string {
95
+ // Use hostname as stable identifier
96
+ hostname, err := os.Hostname()
97
+ if err != nil {
98
+ return "unknown"
99
+ }
100
+ return "cli_" + hostname
101
+ }
102
+
103
+ // getDeploymentEnv returns the deployment environment
104
+ // Similar to _env_name_from_github_app_id()
105
+ func getDeploymentEnv() string {
106
+ if env := os.Getenv("TANAGRAM_ENV"); env != "" {
107
+ return env
108
+ }
109
+ return "unknown"
110
+ }
111
+
112
+ // getVersion returns the CLI version
113
+ func getVersion() string {
114
+ // TODO: embed version at build time with -ldflags
115
+ return "0.1.14"
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanagram/cli",
3
- "version": "0.1.20",
3
+ "version": "0.1.23",
4
4
  "description": "Tanagram - Catch sloppy code before it ships",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "scripts": {
10
10
  "postinstall": "node install.js",
11
- "test": "go test ./..."
11
+ "test": "go test ./...",
12
+ "prepublishOnly": "export $(grep -v '^#' .env.publish | xargs) && node install.js"
12
13
  },
13
14
  "keywords": [
14
15
  "tanagram",
@@ -38,6 +39,7 @@
38
39
  "fixtures/",
39
40
  "git/",
40
41
  "llm/",
42
+ "metrics/",
41
43
  "parser/",
42
44
  "storage/",
43
45
  "main.go",