@tanagram/cli 0.4.14 → 0.4.19
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 +2 -0
- package/dist/npm/darwin-arm64/LICENSE +21 -0
- package/dist/npm/darwin-arm64/README.md +267 -0
- package/dist/npm/darwin-arm64/tanagram +0 -0
- package/dist/npm/darwin-x64/LICENSE +21 -0
- package/dist/npm/darwin-x64/README.md +267 -0
- package/dist/npm/darwin-x64/tanagram +0 -0
- package/dist/npm/linux-arm64/LICENSE +21 -0
- package/dist/npm/linux-arm64/README.md +267 -0
- package/dist/npm/linux-arm64/tanagram +0 -0
- package/dist/npm/linux-x64/LICENSE +21 -0
- package/dist/npm/linux-x64/README.md +267 -0
- package/dist/npm/linux-x64/tanagram +0 -0
- package/dist/npm/tanagram_0.4.19_darwin_amd64.tar.gz +0 -0
- package/dist/npm/tanagram_0.4.19_darwin_arm64.tar.gz +0 -0
- package/dist/npm/tanagram_0.4.19_linux_amd64.tar.gz +0 -0
- package/dist/npm/tanagram_0.4.19_linux_arm64.tar.gz +0 -0
- package/dist/npm/tanagram_0.4.19_windows_amd64.zip +0 -0
- package/dist/npm/win32-x64/LICENSE +21 -0
- package/dist/npm/win32-x64/README.md +267 -0
- package/dist/npm/win32-x64/tanagram.exe +0 -0
- package/go.mod +1 -0
- package/go.sum +2 -0
- package/install.js +176 -22
- package/main.go +29 -13
- package/package.json +5 -4
- package/tui/welcome.go +1 -13
- package/tui/puzzle.go +0 -694
- package/tui/renderer.go +0 -359
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Tanagram CLI
|
|
2
|
+
|
|
3
|
+
A lightweight Go CLI that enforces policies from `AGENTS.md` files on your local git changes.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
Run `tanagram` before committing to catch policy violations locally:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
$ tanagram
|
|
11
|
+
|
|
12
|
+
✗ Found 1 policy violation(s):
|
|
13
|
+
|
|
14
|
+
webui/src/Button.tsx:42 - [No hardcoded colors] Don't use hard-coded color values; use theme colors instead
|
|
15
|
+
> background: "#FF5733"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
### Quick Start (3 steps)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# 1. Install globally via npm
|
|
24
|
+
npm install -g @tanagram/cli
|
|
25
|
+
|
|
26
|
+
# 2. Automatically add a Claude Code hook
|
|
27
|
+
tanagram config claude
|
|
28
|
+
# and/or if you use Cursor:
|
|
29
|
+
tanagram config cursor
|
|
30
|
+
|
|
31
|
+
# 3. Run tanagram (will prompt for API key on first run)
|
|
32
|
+
tanagram
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Requirements:**
|
|
36
|
+
- Node.js >= 14.0.0
|
|
37
|
+
- **Anthropic API Key** (you'll be prompted to enter it on first run)
|
|
38
|
+
|
|
39
|
+
The CLI is written in Go but distributed via npm for easier installation and version management. During installation, npm automatically downloads the Go compiler and builds the binary for your platform (no manual setup needed!).
|
|
40
|
+
|
|
41
|
+
### API Key Setup
|
|
42
|
+
|
|
43
|
+
Tanagram uses Claude AI (via Anthropic API) to extract policies from your instruction files. On first run, you'll be prompted to enter your API key, which will be saved to `~/.tanagram/config.json`.
|
|
44
|
+
|
|
45
|
+
**Get an API key:**
|
|
46
|
+
1. Sign up at [https://console.anthropic.com](https://console.anthropic.com)
|
|
47
|
+
2. Create an API key in the dashboard
|
|
48
|
+
3. Run `tanagram` and enter your key when prompted
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Check all changes (unstaged + staged) - automatically syncs if policies changed
|
|
54
|
+
tanagram
|
|
55
|
+
# or explicitly:
|
|
56
|
+
tanagram run
|
|
57
|
+
|
|
58
|
+
# Manually sync instruction files to cache
|
|
59
|
+
tanagram sync
|
|
60
|
+
|
|
61
|
+
# View all cached policies
|
|
62
|
+
tanagram list
|
|
63
|
+
|
|
64
|
+
# Show help
|
|
65
|
+
tanagram help
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Smart Caching:** Policies are cached and automatically resynced when instruction files change (detected via MD5 hash).
|
|
69
|
+
|
|
70
|
+
### Commands
|
|
71
|
+
|
|
72
|
+
- **`run`** (default) - Check git changes against policies with auto-sync
|
|
73
|
+
- **`sync`** - Manually sync all instruction files to cache
|
|
74
|
+
- **`list`** - View all cached policies (shows enforceable vs unenforceable)
|
|
75
|
+
- **`help`** - Show usage information
|
|
76
|
+
|
|
77
|
+
### Claude Code Hook
|
|
78
|
+
|
|
79
|
+
Install the CLI as a Claude Code [hook](https://code.claude.com/docs/en/hooks) to have Claude automatically iterate on Tanagram's output.
|
|
80
|
+
|
|
81
|
+
**Easy setup (recommended):**
|
|
82
|
+
```bash
|
|
83
|
+
tanagram config claude
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This automatically adds the hook to your `~/.claude/settings.json`. It's safe to run multiple times and will preserve any existing settings.
|
|
87
|
+
|
|
88
|
+
**Check hook status:**
|
|
89
|
+
```bash
|
|
90
|
+
tanagram config list
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Manual setup (alternative):**
|
|
94
|
+
If you prefer to manually edit your settings, add this to your `~/.claude/settings.json` (user settings) or `.claude/settings.json` (project settings):
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"hooks": {
|
|
99
|
+
"SessionStart": [
|
|
100
|
+
{
|
|
101
|
+
"hooks": [
|
|
102
|
+
{
|
|
103
|
+
"command": "tanagram snapshot",
|
|
104
|
+
"type": "command"
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"matcher": "startup|clear"
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"Stop": [
|
|
111
|
+
{
|
|
112
|
+
"hooks": [
|
|
113
|
+
{
|
|
114
|
+
"command": "tanagram",
|
|
115
|
+
"type": "command"
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
If you have existing hooks, you can merge this hook into your existing config.
|
|
125
|
+
|
|
126
|
+
### Cursor Hook
|
|
127
|
+
|
|
128
|
+
Install the CLI as a Cursor Code [hook](https://cursor.com/docs/agent/hooks) to have Cursor automatically iterate on Tanagram's output.
|
|
129
|
+
|
|
130
|
+
**Easy setup (recommended):**
|
|
131
|
+
```bash
|
|
132
|
+
tanagram config cursor
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This automatically adds the hook to your `~/.cursor/hooks.json`. It's safe to run multiple times and will preserve any existing settings.
|
|
136
|
+
|
|
137
|
+
**Check hook status:**
|
|
138
|
+
```bash
|
|
139
|
+
tanagram config list
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Manual setup (alternative):**
|
|
143
|
+
If you prefer to manually edit your settings, add this to your `~/.cursor/hooks.json` (user settings) or `.cursor/hooks.json` (project settings):
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"hooks": {
|
|
148
|
+
"beforeSubmitPrompt": [
|
|
149
|
+
{
|
|
150
|
+
"command": "tanagram snapshot"
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
"stop": [
|
|
154
|
+
{
|
|
155
|
+
"command": "tanagram"
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
"version": 1
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
If you have existing hooks, you can merge this hook into your existing config.
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
## How It Works
|
|
167
|
+
|
|
168
|
+
1. **Finds instruction files** - Searches for `AGENTS.md`, `POLICIES.md`, `CLAUDE.md`, `BUGBOT.md`, and `.cursor/rules/*.mdc` in your git repository
|
|
169
|
+
2. **Checks cache** - Loads cached policies and MD5 hashes from `.tanagram/`
|
|
170
|
+
3. **Auto-syncs** - Detects file changes via MD5 and automatically resyncs if needed
|
|
171
|
+
4. **LLM extraction** - Uses Claude AI to extract ALL policies from instruction files
|
|
172
|
+
5. **Gets git diff** - Analyzes all your changes (unstaged + staged)
|
|
173
|
+
6. **LLM detection** - Checks violations using intelligent semantic analysis
|
|
174
|
+
7. **Reports results** - Terminal output with detailed reasoning for each violation
|
|
175
|
+
|
|
176
|
+
### Cache Location
|
|
177
|
+
|
|
178
|
+
Policies are cached in `.tanagram/cache.gob` at your git repository root. Add this to your `.gitignore`:
|
|
179
|
+
|
|
180
|
+
```gitignore
|
|
181
|
+
.tanagram/
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### What Can Be Enforced
|
|
185
|
+
|
|
186
|
+
**Everything!** Because the LLM reads and understands code like a human:
|
|
187
|
+
|
|
188
|
+
**Simple patterns:**
|
|
189
|
+
- "Don't use hard-coded colors" → Detects `#FF5733`, `rgb()`, etc.
|
|
190
|
+
- "Use ruff format, not black" → Detects `black` usage
|
|
191
|
+
- "Always use === instead of ==" → Detects `==` operators
|
|
192
|
+
|
|
193
|
+
**Complex guidelines:**
|
|
194
|
+
- "Break down code into modular functions" → Analyzes function length and complexity
|
|
195
|
+
- "Don't deeply layer code" → Detects excessive nesting
|
|
196
|
+
- "Ensure no code smells" → Identifies common anti-patterns
|
|
197
|
+
- "Use structured logging with request IDs" → Checks logging patterns
|
|
198
|
+
- "Prefer async/await for I/O" → Understands async patterns
|
|
199
|
+
|
|
200
|
+
**Language-specific idioms:**
|
|
201
|
+
- Knows Go uses PascalCase for exports (not Python's snake_case)
|
|
202
|
+
- Won't flag Go code for missing Python type hints
|
|
203
|
+
- Understands JavaScript !== Python !== Go
|
|
204
|
+
|
|
205
|
+
### Exit Codes
|
|
206
|
+
|
|
207
|
+
- `0` - No violations found
|
|
208
|
+
- `2` - Violations found (triggers Claude Code automatic fix behavior)
|
|
209
|
+
|
|
210
|
+
## Example
|
|
211
|
+
|
|
212
|
+
Create an `AGENTS.md` in your repo with policies:
|
|
213
|
+
|
|
214
|
+
```markdown
|
|
215
|
+
# Development Policies
|
|
216
|
+
|
|
217
|
+
- Don't use hard-coded color values; use theme colors instead
|
|
218
|
+
- Use ruff format for Python formatting, not black
|
|
219
|
+
- Always use async/await for database operations
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Or use Cursor rules files in `.cursor/rules/`:
|
|
223
|
+
|
|
224
|
+
```markdown
|
|
225
|
+
---
|
|
226
|
+
description: TypeScript coding standards
|
|
227
|
+
globs: ["*.ts", "*.tsx"]
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
# TypeScript Standards
|
|
231
|
+
|
|
232
|
+
- Use strict type checking
|
|
233
|
+
- Avoid using 'any' type
|
|
234
|
+
- Prefer interfaces over type aliases
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Then run `tanagram` to enforce them locally!
|
|
238
|
+
|
|
239
|
+
**Note:** For `.mdc` files, Tanagram extracts policies from the markdown content only (YAML frontmatter is used by Cursor and ignored during policy extraction).
|
|
240
|
+
|
|
241
|
+
## Tanagram Web Integration
|
|
242
|
+
|
|
243
|
+
You can also use [Tanagram](https://tanagram.ai) to manage policies across your organization and enforce them on PRs.
|
|
244
|
+
If you have policies defined online, you can enforce them while you develop locally with the CLI as well.
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# Connect your account
|
|
248
|
+
tanagram login
|
|
249
|
+
|
|
250
|
+
# Download policies from your Tanagram account and cache them locally
|
|
251
|
+
tanagram sync
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
For customers with an on-prem installation, set the `TANAGRAM_WEB_HOSTNAME` environment variable to the URL of your Tanagram instance — for example:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
export TANAGRAM_WEB_HOSTNAME=https://yourcompany.tanagram.ai
|
|
258
|
+
|
|
259
|
+
tanagram login
|
|
260
|
+
tanagram sync
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
Built by [@fluttermatt](https://x.com/fluttermatt) and the [Tanagram](https://tanagram.ai/) team. Talk to us [on Twitter](https://x.com/tanagram_) or email: founders AT tanagram.ai
|
|
266
|
+
|
|
267
|
+
#
|
|
Binary file
|
package/go.mod
CHANGED
|
@@ -19,6 +19,7 @@ require (
|
|
|
19
19
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
|
20
20
|
github.com/danieljoos/wincred v1.2.2 // indirect
|
|
21
21
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
|
22
|
+
github.com/getsentry/sentry-go v0.40.0 // indirect
|
|
22
23
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
|
23
24
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
|
24
25
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
package/go.sum
CHANGED
|
@@ -22,6 +22,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|
|
22
22
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
23
23
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
|
24
24
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
|
25
|
+
github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
|
|
26
|
+
github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
|
25
27
|
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
|
26
28
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
|
27
29
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
package/install.js
CHANGED
|
@@ -4,11 +4,134 @@ const { execSync } = require('child_process');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
|
+
const https = require('https');
|
|
7
8
|
const pkg = require('./package.json');
|
|
8
9
|
|
|
9
10
|
const GO_VERSION = '1.21.0';
|
|
10
11
|
const GO_CACHE_DIR = path.join(os.homedir(), '.tanagram', `go-${GO_VERSION}`);
|
|
11
12
|
|
|
13
|
+
// Map Node.js platform/arch to GoReleaser naming
|
|
14
|
+
function getPlatformInfo() {
|
|
15
|
+
const platform = os.platform();
|
|
16
|
+
const arch = os.arch();
|
|
17
|
+
|
|
18
|
+
const platformMap = {
|
|
19
|
+
'darwin': 'darwin',
|
|
20
|
+
'linux': 'linux',
|
|
21
|
+
'win32': 'windows'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const archMap = {
|
|
25
|
+
'x64': 'amd64',
|
|
26
|
+
'arm64': 'arm64'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const nodePlatformDir = {
|
|
30
|
+
'darwin': arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64',
|
|
31
|
+
'linux': arch === 'arm64' ? 'linux-arm64' : 'linux-x64',
|
|
32
|
+
'win32': 'win32-x64'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
goos: platformMap[platform] || platform,
|
|
37
|
+
goarch: archMap[arch] || arch,
|
|
38
|
+
binaryName: platform === 'win32' ? 'tanagram.exe' : 'tanagram',
|
|
39
|
+
platformDir: nodePlatformDir[platform]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check for prebuilt binary bundled with npm package
|
|
44
|
+
function checkPrebuiltBinary() {
|
|
45
|
+
const { platformDir, binaryName } = getPlatformInfo();
|
|
46
|
+
|
|
47
|
+
// Check in dist/npm directory (where CI puts them)
|
|
48
|
+
const distPath = path.join(__dirname, 'dist', 'npm', platformDir, binaryName);
|
|
49
|
+
if (fs.existsSync(distPath)) {
|
|
50
|
+
console.log(`✓ Found prebuilt binary for ${platformDir}`);
|
|
51
|
+
return distPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check in platform directory at package root
|
|
55
|
+
const rootPath = path.join(__dirname, platformDir, binaryName);
|
|
56
|
+
if (fs.existsSync(rootPath)) {
|
|
57
|
+
console.log(`✓ Found prebuilt binary for ${platformDir}`);
|
|
58
|
+
return rootPath;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Download prebuilt binary from GitHub releases
|
|
65
|
+
async function downloadPrebuiltBinary() {
|
|
66
|
+
const { goos, goarch, binaryName } = getPlatformInfo();
|
|
67
|
+
const version = pkg.version;
|
|
68
|
+
const ext = goos === 'windows' ? 'zip' : 'tar.gz';
|
|
69
|
+
const releaseTag = `cli-v${version}`;
|
|
70
|
+
const assetName = `tanagram_${version}_${goos}_${goarch}.${ext}`;
|
|
71
|
+
|
|
72
|
+
const url = `https://github.com/tanagram/monorepo/releases/download/${releaseTag}/${assetName}`;
|
|
73
|
+
const tmpDir = path.join(os.tmpdir(), `tanagram-${Date.now()}`);
|
|
74
|
+
const archivePath = path.join(tmpDir, assetName);
|
|
75
|
+
|
|
76
|
+
console.log(`📦 Downloading prebuilt binary from GitHub releases...`);
|
|
77
|
+
console.log(` ${url}`);
|
|
78
|
+
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
81
|
+
|
|
82
|
+
const download = (downloadUrl, redirectCount = 0) => {
|
|
83
|
+
if (redirectCount > 5) {
|
|
84
|
+
reject(new Error('Too many redirects'));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
https.get(downloadUrl, (response) => {
|
|
89
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
90
|
+
download(response.headers.location, redirectCount + 1);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (response.statusCode !== 200) {
|
|
95
|
+
reject(new Error(`Download failed: HTTP ${response.statusCode}`));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const file = fs.createWriteStream(archivePath);
|
|
100
|
+
response.pipe(file);
|
|
101
|
+
|
|
102
|
+
file.on('finish', () => {
|
|
103
|
+
file.close();
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// Extract the archive
|
|
107
|
+
if (ext === 'zip') {
|
|
108
|
+
execSync(`unzip -o "${archivePath}" -d "${tmpDir}"`, { stdio: 'pipe' });
|
|
109
|
+
} else {
|
|
110
|
+
execSync(`tar -xzf "${archivePath}" -C "${tmpDir}"`, { stdio: 'pipe' });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const extractedBinary = path.join(tmpDir, binaryName);
|
|
114
|
+
if (fs.existsSync(extractedBinary)) {
|
|
115
|
+
resolve(extractedBinary);
|
|
116
|
+
} else {
|
|
117
|
+
reject(new Error('Binary not found in archive'));
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
reject(new Error(`Failed to extract: ${err.message}`));
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
file.on('error', (err) => {
|
|
125
|
+
fs.unlinkSync(archivePath);
|
|
126
|
+
reject(err);
|
|
127
|
+
});
|
|
128
|
+
}).on('error', reject);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
download(url);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
12
135
|
// Check if Go is already installed locally
|
|
13
136
|
function checkLocalGo() {
|
|
14
137
|
try {
|
|
@@ -64,13 +187,9 @@ async function downloadGo() {
|
|
|
64
187
|
}
|
|
65
188
|
}
|
|
66
189
|
|
|
67
|
-
// Build the binary
|
|
190
|
+
// Build the binary from source
|
|
68
191
|
function buildBinary(goCommand) {
|
|
69
|
-
const
|
|
70
|
-
const arch = os.arch();
|
|
71
|
-
const goos = platform === 'win32' ? 'windows' : platform;
|
|
72
|
-
const goarch = arch === 'x64' ? 'amd64' : arch === 'arm64' ? 'arm64' : arch;
|
|
73
|
-
const binaryName = platform === 'win32' ? 'tanagram.exe' : 'tanagram';
|
|
192
|
+
const { goos, goarch, binaryName } = getPlatformInfo();
|
|
74
193
|
const binaryPath = path.join(__dirname, 'bin', binaryName);
|
|
75
194
|
|
|
76
195
|
const binDir = path.join(__dirname, 'bin');
|
|
@@ -78,7 +197,7 @@ function buildBinary(goCommand) {
|
|
|
78
197
|
fs.mkdirSync(binDir, { recursive: true });
|
|
79
198
|
}
|
|
80
199
|
|
|
81
|
-
console.log('🔧 Building Tanagram CLI...');
|
|
200
|
+
console.log('🔧 Building Tanagram CLI from source...');
|
|
82
201
|
|
|
83
202
|
try {
|
|
84
203
|
const ldflags = `-X 'main.Version=${pkg.version}'`;
|
|
@@ -92,17 +211,37 @@ function buildBinary(goCommand) {
|
|
|
92
211
|
}
|
|
93
212
|
});
|
|
94
213
|
|
|
95
|
-
if (platform !== 'win32') {
|
|
214
|
+
if (os.platform() !== 'win32') {
|
|
96
215
|
fs.chmodSync(binaryPath, '755');
|
|
97
216
|
}
|
|
98
217
|
|
|
99
|
-
console.log('✓ Tanagram CLI
|
|
218
|
+
console.log('✓ Tanagram CLI built successfully');
|
|
100
219
|
return binaryPath;
|
|
101
220
|
} catch (error) {
|
|
102
221
|
throw new Error(`Build failed: ${error.message}`);
|
|
103
222
|
}
|
|
104
223
|
}
|
|
105
224
|
|
|
225
|
+
// Copy prebuilt binary to bin directory
|
|
226
|
+
function installPrebuiltBinary(sourcePath) {
|
|
227
|
+
const { binaryName } = getPlatformInfo();
|
|
228
|
+
const binDir = path.join(__dirname, 'bin');
|
|
229
|
+
const targetPath = path.join(binDir, binaryName);
|
|
230
|
+
|
|
231
|
+
if (!fs.existsSync(binDir)) {
|
|
232
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
236
|
+
|
|
237
|
+
if (os.platform() !== 'win32') {
|
|
238
|
+
fs.chmodSync(targetPath, '755');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log('✓ Tanagram CLI installed successfully');
|
|
242
|
+
return targetPath;
|
|
243
|
+
}
|
|
244
|
+
|
|
106
245
|
function isCIEnvironment() {
|
|
107
246
|
return (
|
|
108
247
|
process.env.CI === 'true' ||
|
|
@@ -147,23 +286,38 @@ function configureEditorHooks(binaryPath) {
|
|
|
147
286
|
// Main installation flow
|
|
148
287
|
(async () => {
|
|
149
288
|
try {
|
|
150
|
-
|
|
151
|
-
let goCommand = checkLocalGo();
|
|
289
|
+
let binaryPath;
|
|
152
290
|
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
291
|
+
// 1. Check for prebuilt binary bundled with npm package
|
|
292
|
+
const prebuiltPath = checkPrebuiltBinary();
|
|
293
|
+
if (prebuiltPath) {
|
|
294
|
+
binaryPath = installPrebuiltBinary(prebuiltPath);
|
|
295
|
+
} else {
|
|
296
|
+
// 2. Try to download prebuilt binary from GitHub releases
|
|
297
|
+
try {
|
|
298
|
+
console.log('Looking for prebuilt binary...');
|
|
299
|
+
const downloadedPath = await downloadPrebuiltBinary();
|
|
300
|
+
binaryPath = installPrebuiltBinary(downloadedPath);
|
|
301
|
+
} catch (downloadErr) {
|
|
302
|
+
console.log(`Prebuilt binary not available: ${downloadErr.message}`);
|
|
303
|
+
console.log('Falling back to building from source...');
|
|
157
304
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
305
|
+
// 3. Fall back to building from source
|
|
306
|
+
let goCommand = checkLocalGo();
|
|
307
|
+
|
|
308
|
+
if (!goCommand) {
|
|
309
|
+
goCommand = checkCachedGo();
|
|
310
|
+
}
|
|
162
311
|
|
|
163
|
-
|
|
164
|
-
|
|
312
|
+
if (!goCommand) {
|
|
313
|
+
goCommand = await downloadGo();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
binaryPath = buildBinary(goCommand);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
165
319
|
|
|
166
|
-
//
|
|
320
|
+
// Configure hooks
|
|
167
321
|
configureEditorHooks(binaryPath);
|
|
168
322
|
|
|
169
323
|
process.exit(0);
|
package/main.go
CHANGED
|
@@ -9,7 +9,9 @@ import (
|
|
|
9
9
|
"os"
|
|
10
10
|
"path/filepath"
|
|
11
11
|
"strings"
|
|
12
|
+
"time"
|
|
12
13
|
|
|
14
|
+
"github.com/getsentry/sentry-go"
|
|
13
15
|
"github.com/tanagram/cli/commands"
|
|
14
16
|
"github.com/tanagram/cli/metrics"
|
|
15
17
|
"github.com/tanagram/cli/tui"
|
|
@@ -30,14 +32,28 @@ func main() {
|
|
|
30
32
|
metrics.SetVersion(Version)
|
|
31
33
|
metrics.Init()
|
|
32
34
|
|
|
35
|
+
if err := sentry.Init(sentry.ClientOptions{
|
|
36
|
+
Dsn: "https://a967718dd129e143907fe01b4e80cad2@o4509017064472576.ingest.us.sentry.io/4510649104007168",
|
|
37
|
+
Release: "tanagram-cli@" + Version,
|
|
38
|
+
Environment: getEnvironment(),
|
|
39
|
+
EnableTracing: true,
|
|
40
|
+
TracesSampleRate: 0.1,
|
|
41
|
+
}); err != nil {
|
|
42
|
+
slog.Warn("Sentry initialization failed", "error", err)
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
exitCode := 0
|
|
34
46
|
defer func() {
|
|
47
|
+
if r := recover(); r != nil {
|
|
48
|
+
sentry.CurrentHub().Recover(r)
|
|
49
|
+
slog.Error("Panic recovered", "panic", r)
|
|
50
|
+
exitCode = 1
|
|
51
|
+
}
|
|
52
|
+
sentry.Flush(2 * time.Second)
|
|
35
53
|
metrics.Track("cli.exit", map[string]interface{}{
|
|
36
54
|
"exit_code": exitCode,
|
|
37
55
|
})
|
|
38
56
|
metrics.Close()
|
|
39
|
-
// os.Exit immediately exits without calling other `defer`s, so we need to group these two statements
|
|
40
|
-
// and call them in the right order.
|
|
41
57
|
os.Exit(exitCode)
|
|
42
58
|
}()
|
|
43
59
|
|
|
@@ -222,17 +238,6 @@ func main() {
|
|
|
222
238
|
"command": "sync-policies",
|
|
223
239
|
})
|
|
224
240
|
err = commands.SyncPolicies()
|
|
225
|
-
case "puzzle":
|
|
226
|
-
metrics.Track("cli.command.execute", map[string]interface{}{
|
|
227
|
-
"command": "puzzle",
|
|
228
|
-
})
|
|
229
|
-
err := tui.RunPuzzleEditor()
|
|
230
|
-
if err != nil {
|
|
231
|
-
slog.Error("Puzzle editor failed", "error", err)
|
|
232
|
-
exitCode = 1
|
|
233
|
-
return
|
|
234
|
-
}
|
|
235
|
-
return
|
|
236
241
|
case "version", "-v", "--version":
|
|
237
242
|
fmt.Println(Version)
|
|
238
243
|
return
|
|
@@ -247,6 +252,10 @@ func main() {
|
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
if err != nil {
|
|
255
|
+
sentry.WithScope(func(scope *sentry.Scope) {
|
|
256
|
+
scope.SetTag("command", subcommand)
|
|
257
|
+
sentry.CaptureException(err)
|
|
258
|
+
})
|
|
250
259
|
metrics.Track("cli.command.error", map[string]interface{}{
|
|
251
260
|
"command": subcommand,
|
|
252
261
|
"error": err.Error(),
|
|
@@ -257,6 +266,13 @@ func main() {
|
|
|
257
266
|
}
|
|
258
267
|
}
|
|
259
268
|
|
|
269
|
+
func getEnvironment() string {
|
|
270
|
+
if env := os.Getenv("SENTRY_ENVIRONMENT"); env != "" {
|
|
271
|
+
return env
|
|
272
|
+
}
|
|
273
|
+
return "unconfigured"
|
|
274
|
+
}
|
|
275
|
+
|
|
260
276
|
func printHelp() {
|
|
261
277
|
help := `Tanagram - Policy enforcement for git changes
|
|
262
278
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanagram/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.19",
|
|
4
4
|
"description": "Tanagram - Catch sloppy code before it ships",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"postinstall": "node install.js",
|
|
11
|
-
"test": "go test ./..."
|
|
12
|
-
"prepublishOnly": "export $(grep -v '^#' .env.publish | xargs) && node install.js"
|
|
11
|
+
"test": "go test ./..."
|
|
13
12
|
},
|
|
14
13
|
"keywords": [
|
|
15
14
|
"tanagram",
|
|
@@ -23,7 +22,8 @@
|
|
|
23
22
|
"license": "MIT",
|
|
24
23
|
"repository": {
|
|
25
24
|
"type": "git",
|
|
26
|
-
"url": "https://github.com/tanagram/
|
|
25
|
+
"url": "https://github.com/tanagram/monorepo.git",
|
|
26
|
+
"directory": "cli"
|
|
27
27
|
},
|
|
28
28
|
"engines": {
|
|
29
29
|
"node": ">=14.0.0"
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"bin/tanagram.js",
|
|
36
|
+
"dist/npm/",
|
|
36
37
|
"api/",
|
|
37
38
|
"auth/",
|
|
38
39
|
"checker/",
|
package/tui/welcome.go
CHANGED
|
@@ -70,19 +70,7 @@ func (m WelcomeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
70
70
|
|
|
71
71
|
// View renders the UI
|
|
72
72
|
func (m WelcomeModel) View() string {
|
|
73
|
-
|
|
74
|
-
var logo string
|
|
75
|
-
|
|
76
|
-
pieces, err := LoadTanagramConfig("tanagram-config.json")
|
|
77
|
-
if err == nil && len(pieces) > 0 {
|
|
78
|
-
// Render using the shared renderer
|
|
79
|
-
renderer := NewTanagramRenderer(90, 45)
|
|
80
|
-
canvas := renderer.RenderPiecesToCanvas(pieces)
|
|
81
|
-
logo = renderer.CanvasToString(canvas, pieces)
|
|
82
|
-
} else {
|
|
83
|
-
// Fallback to simple text if config not found
|
|
84
|
-
logo = "TANAGRAM"
|
|
85
|
-
}
|
|
73
|
+
logo := "TANAGRAM"
|
|
86
74
|
|
|
87
75
|
// Title style
|
|
88
76
|
titleStyle := lipgloss.NewStyle().
|