@tanagram/cli 0.6.8 → 0.6.24
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 +6 -0
- package/dist/npm/darwin-arm64/README.md +6 -0
- package/dist/npm/darwin-arm64/tanagram +0 -0
- package/dist/npm/darwin-x64/README.md +6 -0
- package/dist/npm/darwin-x64/tanagram +0 -0
- package/dist/npm/linux-arm64/README.md +6 -0
- package/dist/npm/linux-arm64/tanagram +0 -0
- package/dist/npm/linux-x64/README.md +6 -0
- package/dist/npm/linux-x64/tanagram +0 -0
- package/dist/npm/tanagram_0.6.24_darwin_amd64.tar.gz +0 -0
- package/dist/npm/tanagram_0.6.24_darwin_arm64.tar.gz +0 -0
- package/dist/npm/tanagram_0.6.24_linux_amd64.tar.gz +0 -0
- package/dist/npm/tanagram_0.6.24_linux_arm64.tar.gz +0 -0
- package/dist/npm/tanagram_0.6.24_windows_amd64.zip +0 -0
- package/dist/npm/win32-x64/README.md +6 -0
- package/dist/npm/win32-x64/tanagram.exe +0 -0
- package/install.js +87 -60
- package/npm-shared.js +110 -0
- package/package.json +3 -2
- package/skills/tanagram-codify/SKILL.md +136 -0
- package/skills.config.js +5 -0
- package/uninstall.js +70 -80
- package/dist/npm/tanagram_0.6.8_darwin_amd64.tar.gz +0 -0
- package/dist/npm/tanagram_0.6.8_darwin_arm64.tar.gz +0 -0
- package/dist/npm/tanagram_0.6.8_linux_amd64.tar.gz +0 -0
- package/dist/npm/tanagram_0.6.8_linux_arm64.tar.gz +0 -0
- package/dist/npm/tanagram_0.6.8_windows_amd64.zip +0 -0
package/README.md
CHANGED
|
@@ -26,6 +26,12 @@ tanagram help # Show available commands
|
|
|
26
26
|
|
|
27
27
|
Set `TANAGRAM_TOKEN` to skip interactive login (useful for CI/CD).
|
|
28
28
|
|
|
29
|
+
For local development, you can override service hosts with environment variables:
|
|
30
|
+
|
|
31
|
+
- `TANAGRAM_WEB_URL` for browser login
|
|
32
|
+
- `TANAGRAM_API_URL` for normal CLI API requests and the LLM proxy
|
|
33
|
+
- `TANAGRAM_LORE_URL` only for Lore upload session create/complete requests
|
|
34
|
+
|
|
29
35
|
## Requirements
|
|
30
36
|
|
|
31
37
|
- Node.js >= 14.0.0
|
|
@@ -26,6 +26,12 @@ tanagram help # Show available commands
|
|
|
26
26
|
|
|
27
27
|
Set `TANAGRAM_TOKEN` to skip interactive login (useful for CI/CD).
|
|
28
28
|
|
|
29
|
+
For local development, you can override service hosts with environment variables:
|
|
30
|
+
|
|
31
|
+
- `TANAGRAM_WEB_URL` for browser login
|
|
32
|
+
- `TANAGRAM_API_URL` for normal CLI API requests and the LLM proxy
|
|
33
|
+
- `TANAGRAM_LORE_URL` only for Lore upload session create/complete requests
|
|
34
|
+
|
|
29
35
|
## Requirements
|
|
30
36
|
|
|
31
37
|
- Node.js >= 14.0.0
|
|
Binary file
|
|
@@ -26,6 +26,12 @@ tanagram help # Show available commands
|
|
|
26
26
|
|
|
27
27
|
Set `TANAGRAM_TOKEN` to skip interactive login (useful for CI/CD).
|
|
28
28
|
|
|
29
|
+
For local development, you can override service hosts with environment variables:
|
|
30
|
+
|
|
31
|
+
- `TANAGRAM_WEB_URL` for browser login
|
|
32
|
+
- `TANAGRAM_API_URL` for normal CLI API requests and the LLM proxy
|
|
33
|
+
- `TANAGRAM_LORE_URL` only for Lore upload session create/complete requests
|
|
34
|
+
|
|
29
35
|
## Requirements
|
|
30
36
|
|
|
31
37
|
- Node.js >= 14.0.0
|
|
Binary file
|
|
@@ -26,6 +26,12 @@ tanagram help # Show available commands
|
|
|
26
26
|
|
|
27
27
|
Set `TANAGRAM_TOKEN` to skip interactive login (useful for CI/CD).
|
|
28
28
|
|
|
29
|
+
For local development, you can override service hosts with environment variables:
|
|
30
|
+
|
|
31
|
+
- `TANAGRAM_WEB_URL` for browser login
|
|
32
|
+
- `TANAGRAM_API_URL` for normal CLI API requests and the LLM proxy
|
|
33
|
+
- `TANAGRAM_LORE_URL` only for Lore upload session create/complete requests
|
|
34
|
+
|
|
29
35
|
## Requirements
|
|
30
36
|
|
|
31
37
|
- Node.js >= 14.0.0
|
|
Binary file
|
|
@@ -26,6 +26,12 @@ tanagram help # Show available commands
|
|
|
26
26
|
|
|
27
27
|
Set `TANAGRAM_TOKEN` to skip interactive login (useful for CI/CD).
|
|
28
28
|
|
|
29
|
+
For local development, you can override service hosts with environment variables:
|
|
30
|
+
|
|
31
|
+
- `TANAGRAM_WEB_URL` for browser login
|
|
32
|
+
- `TANAGRAM_API_URL` for normal CLI API requests and the LLM proxy
|
|
33
|
+
- `TANAGRAM_LORE_URL` only for Lore upload session create/complete requests
|
|
34
|
+
|
|
29
35
|
## Requirements
|
|
30
36
|
|
|
31
37
|
- Node.js >= 14.0.0
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -26,6 +26,12 @@ tanagram help # Show available commands
|
|
|
26
26
|
|
|
27
27
|
Set `TANAGRAM_TOKEN` to skip interactive login (useful for CI/CD).
|
|
28
28
|
|
|
29
|
+
For local development, you can override service hosts with environment variables:
|
|
30
|
+
|
|
31
|
+
- `TANAGRAM_WEB_URL` for browser login
|
|
32
|
+
- `TANAGRAM_API_URL` for normal CLI API requests and the LLM proxy
|
|
33
|
+
- `TANAGRAM_LORE_URL` only for Lore upload session create/complete requests
|
|
34
|
+
|
|
29
35
|
## Requirements
|
|
30
36
|
|
|
31
37
|
- Node.js >= 14.0.0
|
|
Binary file
|
package/install.js
CHANGED
|
@@ -3,56 +3,20 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return 'cli_' + os.hostname();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Track event to PostHog
|
|
24
|
-
function track(event, properties = {}) {
|
|
25
|
-
if (isCIEnvironment()) return;
|
|
26
|
-
|
|
27
|
-
const data = JSON.stringify({
|
|
28
|
-
api_key: POSTHOG_KEY,
|
|
29
|
-
event: event,
|
|
30
|
-
properties: {
|
|
31
|
-
distinct_id: getDistinctId(),
|
|
32
|
-
version: pkg.version,
|
|
33
|
-
platform: os.platform(),
|
|
34
|
-
arch: os.arch(),
|
|
35
|
-
node_version: process.version,
|
|
36
|
-
...properties
|
|
37
|
-
},
|
|
38
|
-
timestamp: new Date().toISOString()
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const req = https.request({
|
|
42
|
-
hostname: POSTHOG_HOST,
|
|
43
|
-
port: 443,
|
|
44
|
-
path: '/capture/',
|
|
45
|
-
method: 'POST',
|
|
46
|
-
headers: {
|
|
47
|
-
'Content-Type': 'application/json',
|
|
48
|
-
'Content-Length': data.length
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
req.on('error', () => {}); // Ignore errors
|
|
53
|
-
req.write(data);
|
|
54
|
-
req.end();
|
|
55
|
-
}
|
|
6
|
+
const { SKILLS } = require('./skills.config');
|
|
7
|
+
const {
|
|
8
|
+
createTracker,
|
|
9
|
+
detectInstallPackageManager,
|
|
10
|
+
isCIEnvironment,
|
|
11
|
+
loreDir,
|
|
12
|
+
loreInstallSourcePath,
|
|
13
|
+
loreLaunchAgentPlistPath,
|
|
14
|
+
loreStableBinaryPath,
|
|
15
|
+
tanagramDir,
|
|
16
|
+
LORE_LABEL
|
|
17
|
+
} = require('./npm-shared');
|
|
18
|
+
|
|
19
|
+
const track = createTracker();
|
|
56
20
|
|
|
57
21
|
// Map Node.js platform/arch to directory naming
|
|
58
22
|
function getPlatformInfo() {
|
|
@@ -119,16 +83,79 @@ function installPrebuiltBinary(sourcePath) {
|
|
|
119
83
|
return targetPath;
|
|
120
84
|
}
|
|
121
85
|
|
|
122
|
-
function
|
|
123
|
-
return (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
86
|
+
function isLoreEnabled() {
|
|
87
|
+
return fs.existsSync(loreLaunchAgentPlistPath());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function copyFileAtomic(sourcePath, targetPath) {
|
|
91
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
92
|
+
const tempPath = `${targetPath}.tmp`;
|
|
93
|
+
fs.copyFileSync(sourcePath, tempPath);
|
|
94
|
+
if (os.platform() !== 'win32') {
|
|
95
|
+
fs.chmodSync(tempPath, '755');
|
|
96
|
+
}
|
|
97
|
+
fs.renameSync(tempPath, targetPath);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function writeLoreInstallSource(sourcePath) {
|
|
101
|
+
fs.mkdirSync(loreDir(), { recursive: true });
|
|
102
|
+
fs.writeFileSync(
|
|
103
|
+
loreInstallSourcePath(),
|
|
104
|
+
JSON.stringify(
|
|
105
|
+
{
|
|
106
|
+
package_manager: detectInstallPackageManager(),
|
|
107
|
+
binary_path: sourcePath,
|
|
108
|
+
updated_at: new Date().toISOString()
|
|
109
|
+
},
|
|
110
|
+
null,
|
|
111
|
+
2
|
|
112
|
+
) + '\n'
|
|
129
113
|
);
|
|
130
114
|
}
|
|
131
115
|
|
|
116
|
+
function restartLoreLaunchAgent() {
|
|
117
|
+
if (os.platform() !== 'darwin' || !isLoreEnabled()) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { spawnSync } = require('child_process');
|
|
122
|
+
const uid = os.userInfo().uid;
|
|
123
|
+
const launchTarget = `gui/${uid}/${LORE_LABEL}`;
|
|
124
|
+
|
|
125
|
+
let result = spawnSync('launchctl', ['kickstart', '-k', launchTarget], { encoding: 'utf8' });
|
|
126
|
+
if (result.status === 0) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
result = spawnSync('launchctl', ['bootstrap', `gui/${uid}`, loreLaunchAgentPlistPath()], { encoding: 'utf8' });
|
|
131
|
+
const bootstrapOutput = `${result.stdout || ''}${result.stderr || ''}`;
|
|
132
|
+
if (result.status !== 0 && !/already exists/i.test(bootstrapOutput)) {
|
|
133
|
+
throw new Error(bootstrapOutput.trim() || 'launchctl bootstrap failed');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
result = spawnSync('launchctl', ['kickstart', '-k', launchTarget], { encoding: 'utf8' });
|
|
137
|
+
if (result.status !== 0) {
|
|
138
|
+
throw new Error(`${result.stdout || ''}${result.stderr || ''}`.trim() || 'launchctl kickstart failed');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function refreshLoreBinaryIfEnabled(sourcePath) {
|
|
143
|
+
if (os.platform() !== 'darwin' || !isLoreEnabled()) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
copyFileAtomic(sourcePath, loreStableBinaryPath());
|
|
149
|
+
writeLoreInstallSource(sourcePath);
|
|
150
|
+
restartLoreLaunchAgent();
|
|
151
|
+
console.error('✓ Refreshed Lore background service binary');
|
|
152
|
+
track('cli.lore.refresh.success', { package_manager: detectInstallPackageManager() });
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error(`⚠ Failed to refresh Lore background service: ${err.message}`);
|
|
155
|
+
track('cli.lore.refresh.failure', { error: err.message });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
132
159
|
function installSkill(skillName) {
|
|
133
160
|
try {
|
|
134
161
|
const skillsSourceDir = path.join(__dirname, 'skills', skillName);
|
|
@@ -276,8 +303,7 @@ function ensureOpenCode() {
|
|
|
276
303
|
// Main installation flow
|
|
277
304
|
(async () => {
|
|
278
305
|
// Check if this is a first-time install (no .tanagram directory exists)
|
|
279
|
-
const
|
|
280
|
-
const isFirstTimeUser = !fs.existsSync(tanagramDir);
|
|
306
|
+
const isFirstTimeUser = !fs.existsSync(tanagramDir());
|
|
281
307
|
|
|
282
308
|
// Track install start
|
|
283
309
|
track('cli.install.start', { first_time: isFirstTimeUser, install_method: 'npm' });
|
|
@@ -285,7 +311,8 @@ function ensureOpenCode() {
|
|
|
285
311
|
try {
|
|
286
312
|
// Find and install prebuilt binary
|
|
287
313
|
const prebuiltPath = findPrebuiltBinary();
|
|
288
|
-
installPrebuiltBinary(prebuiltPath);
|
|
314
|
+
const installedBinaryPath = installPrebuiltBinary(prebuiltPath);
|
|
315
|
+
refreshLoreBinaryIfEnabled(installedBinaryPath);
|
|
289
316
|
|
|
290
317
|
// Install Claude skills
|
|
291
318
|
installClaudeSkills();
|
package/npm-shared.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const pkg = require('./package.json');
|
|
6
|
+
|
|
7
|
+
const POSTHOG_KEY = 'phc_sMsUvf0nK50rZdztSlX9rDJqIreLcXj4dyGS0tORQpQ';
|
|
8
|
+
const POSTHOG_HOST = 'phe.tanagram.ai';
|
|
9
|
+
const LORE_LABEL = 'com.tanagram.cli.lore';
|
|
10
|
+
|
|
11
|
+
function defaultDistinctId() {
|
|
12
|
+
return `cli_${os.hostname()}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createTracker(options = {}) {
|
|
16
|
+
const getDistinctId = options.getDistinctId || defaultDistinctId;
|
|
17
|
+
|
|
18
|
+
return function track(event, properties = {}) {
|
|
19
|
+
if (isCIEnvironment()) return;
|
|
20
|
+
|
|
21
|
+
const data = JSON.stringify({
|
|
22
|
+
api_key: POSTHOG_KEY,
|
|
23
|
+
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
|
+
|
|
52
|
+
function isCIEnvironment() {
|
|
53
|
+
return (
|
|
54
|
+
process.env.CI === 'true' ||
|
|
55
|
+
process.env.GITHUB_ACTIONS === 'true' ||
|
|
56
|
+
process.env.BUILDKITE === 'true' ||
|
|
57
|
+
process.env.GITLAB_CI === 'true' ||
|
|
58
|
+
process.env.TF_BUILD === 'True'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function tanagramDir() {
|
|
63
|
+
return path.join(os.homedir(), '.tanagram');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function loreDir() {
|
|
67
|
+
return path.join(tanagramDir(), 'lore');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function loreStableBinaryPath() {
|
|
71
|
+
return path.join(tanagramDir(), 'bin', 'tanagram-lore');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function loreInstallSourcePath() {
|
|
75
|
+
return path.join(loreDir(), 'install_source.json');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function loreStatusPath() {
|
|
79
|
+
return path.join(loreDir(), 'status.json');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function loreLaunchAgentPlistPath() {
|
|
83
|
+
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${LORE_LABEL}.plist`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function detectInstallPackageManager() {
|
|
87
|
+
const userAgent = (process.env.npm_config_user_agent || '').toLowerCase();
|
|
88
|
+
const execPath = (process.env.npm_execpath || '').toLowerCase();
|
|
89
|
+
|
|
90
|
+
if (userAgent.includes('pnpm') || execPath.includes('pnpm')) {
|
|
91
|
+
return 'pnpm';
|
|
92
|
+
}
|
|
93
|
+
if (userAgent.includes('yarn') || execPath.includes('yarn')) {
|
|
94
|
+
return 'yarn';
|
|
95
|
+
}
|
|
96
|
+
return 'npm';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
createTracker,
|
|
101
|
+
detectInstallPackageManager,
|
|
102
|
+
isCIEnvironment,
|
|
103
|
+
loreDir,
|
|
104
|
+
loreInstallSourcePath,
|
|
105
|
+
loreLaunchAgentPlistPath,
|
|
106
|
+
loreStableBinaryPath,
|
|
107
|
+
loreStatusPath,
|
|
108
|
+
tanagramDir,
|
|
109
|
+
LORE_LABEL
|
|
110
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanagram/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.24",
|
|
4
4
|
"description": "Tanagram - Catch sloppy code before it ships",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -34,8 +34,9 @@
|
|
|
34
34
|
"bin/tanagram.js",
|
|
35
35
|
"dist/npm/",
|
|
36
36
|
"install.js",
|
|
37
|
+
"npm-shared.js",
|
|
37
38
|
"uninstall.js",
|
|
38
|
-
"skills.config",
|
|
39
|
+
"skills.config.js",
|
|
39
40
|
"skills/",
|
|
40
41
|
"README.md",
|
|
41
42
|
"LICENSE"
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanagram-codify
|
|
3
|
+
description: Codify an observed repeatable, enforceable code pattern into a Tanagram rule. Use when you observe the opportunity to codify a repeatable rule or when prompted by the user
|
|
4
|
+
allowed-tools: Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Tanagram Codify — Turn Observed Patterns Into Rules
|
|
8
|
+
|
|
9
|
+
Turns a concrete, repeatable code pattern — observed in a diff, a file, or described by the user — into an enforceable Tanagram rule.
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
Use this skill when:
|
|
14
|
+
- The user explicitly asks to "codify", "enforce", or "make a rule" for a pattern
|
|
15
|
+
- You observe a `git diff` that introduces a new utility, decorator, or abstraction that *every* call site should adopt
|
|
16
|
+
- A pattern is clearly intentional (not a one-off), mechanical enough to check programmatically, and worth catching in code review
|
|
17
|
+
|
|
18
|
+
**Do NOT create a rule when:**
|
|
19
|
+
- The pattern is already present in tanagram rules (observed via `tanagram rules list`)
|
|
20
|
+
- The pattern is already caught by a linter or type checker
|
|
21
|
+
- The pattern is too context-specific to apply broadly
|
|
22
|
+
- The "pattern" is just a style preference with no correctness or reliability implication
|
|
23
|
+
- The diff only has one call site and no obvious future surface area
|
|
24
|
+
|
|
25
|
+
## Prerequisites
|
|
26
|
+
|
|
27
|
+
- `tanagram` CLI installed and authenticated (`tanagram login`)
|
|
28
|
+
- Inside a git repository with relevant code visible
|
|
29
|
+
|
|
30
|
+
## Procedure
|
|
31
|
+
|
|
32
|
+
### Step 1: Identify the pattern
|
|
33
|
+
|
|
34
|
+
Study the code — the diff, the file, or the user's description — and extract:
|
|
35
|
+
|
|
36
|
+
1. **The canonical correct pattern**: What does correct code look like? Include the import, decorator, function call, or structure as concretely as possible.
|
|
37
|
+
2. **The anti-pattern it replaces**: What would a developer write if they didn't know about this convention? Be specific.
|
|
38
|
+
3. **The consequence of the anti-pattern**: What goes wrong — silent failure, missing observability, race condition, inconsistency, etc.?
|
|
39
|
+
4. **The scope**: Which files, layers, or contexts does this rule apply to? (e.g., "all Inngest function handlers", "all API route files", "all database models")
|
|
40
|
+
|
|
41
|
+
A pattern is worth codifying when it meets **all three** of these:
|
|
42
|
+
- **Repeatable** — The same structure will appear again as the codebase grows
|
|
43
|
+
- **Mechanical** — A reviewer (or LLM) can recognize a violation without deep context
|
|
44
|
+
- **Consequential** — Getting it wrong produces a meaningful problem (not just aesthetic)
|
|
45
|
+
|
|
46
|
+
### Step 2: Check for duplicate rules
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
tanagram rules list --json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Read the `name` and `description` of existing rules. If an existing rule already covers the pattern (even partially), skip creation and report the overlap to the user.
|
|
53
|
+
|
|
54
|
+
### Step 3: Determine the repo slug
|
|
55
|
+
|
|
56
|
+
Look at the `repos` field in the output from Step 2. Use the same slug (e.g., `tanagram/monorepo`) for the new rule. If no existing rules exist, ask the user for the repo slug.
|
|
57
|
+
|
|
58
|
+
### Step 4: Draft the rule
|
|
59
|
+
|
|
60
|
+
A well-formed rule has two parts:
|
|
61
|
+
|
|
62
|
+
**Name** — PascalCase, specific, self-explanatory without reading the description. 5–7 words maximum.
|
|
63
|
+
- Good: `InngestFunctionsMustUseTraceDecorator`
|
|
64
|
+
- Bad: `UseDecoratorPattern`, `FixInngest`
|
|
65
|
+
|
|
66
|
+
**Description** — Must include all four of these, in order:
|
|
67
|
+
1. **Scope**: What kind of code this applies to
|
|
68
|
+
2. **Anti-pattern**: What incorrect code looks like (quote or paraphrase real code)
|
|
69
|
+
3. **Consequence**: What goes wrong if the anti-pattern is used
|
|
70
|
+
4. **Correct pattern**: What correct code looks like, including the import if relevant
|
|
71
|
+
|
|
72
|
+
Example of a well-formed description:
|
|
73
|
+
> Every Inngest function handler and its `on_failure` handler must be decorated with `@trace_inngest_function('function-id')`, using the same ID as `fn_id=` in `create_function(...)`. This decorator opens a root Sentry transaction and sets the `inngest.function` tag. Anti-pattern: manually calling `sentry_sdk.set_tag("inngest.function", "...")` inside the function body — this is ad-hoc, easy to forget, and not scoped to a transaction. Correct: `from src.inngest.functions.utils.tracing import trace_inngest_function` then apply `@trace_inngest_function("my-function-id")` immediately below the `@create_function(...)` decorator.
|
|
74
|
+
|
|
75
|
+
### Step 5: Ask for permission
|
|
76
|
+
Present your proposal to the user and ask for confirmation to create a Tanagram rule
|
|
77
|
+
|
|
78
|
+
### Step 6: Create the rule
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
tanagram rules create \
|
|
82
|
+
--name "RuleNameInPascalCase" \
|
|
83
|
+
--description "Scope. Anti-pattern: ... Consequence: ... Correct pattern: ..." \
|
|
84
|
+
--repos "<repo-slug>"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Step 7: Confirm and report
|
|
88
|
+
|
|
89
|
+
After the CLI returns a rule ID, tell the user:
|
|
90
|
+
|
|
91
|
+
> **Codified:** "[Rule Name]" (`pol_xxxx`)
|
|
92
|
+
>
|
|
93
|
+
> Catches: [one sentence on what it catches]
|
|
94
|
+
> Scope: [what code it applies to]
|
|
95
|
+
|
|
96
|
+
## Quality Bar for Descriptions
|
|
97
|
+
|
|
98
|
+
Before submitting, verify the description answers all of these:
|
|
99
|
+
- [ ] What code does this apply to? (scope)
|
|
100
|
+
- [ ] What does the wrong version look like?
|
|
101
|
+
- [ ] Why is the wrong version harmful?
|
|
102
|
+
- [ ] What does the right version look like?
|
|
103
|
+
- [ ] Is there an import or specific symbol the correct pattern requires?
|
|
104
|
+
|
|
105
|
+
## Examples
|
|
106
|
+
|
|
107
|
+
### Pattern: a new decorator that all handlers must use
|
|
108
|
+
|
|
109
|
+
User shows a diff introducing `@trace_inngest_function` and applying it to several Inngest handlers.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
tanagram rules create \
|
|
113
|
+
--name "InngestFunctionsMustUseTraceDecorator" \
|
|
114
|
+
--description "Every Inngest function handler (and its on_failure handler) must be decorated with @trace_inngest_function('function-id') from src.inngest.functions.utils.tracing, using the same function ID string passed to fn_id= in create_function(). Anti-pattern: calling sentry_sdk.set_tag('inngest.function', '...') manually inside the function body — this is ad-hoc and not scoped to a Sentry transaction. Correct: apply @trace_inngest_function('my-fn-id') immediately below the @create_function(...) decorator." \
|
|
115
|
+
--repos "tanagram/monorepo"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Pattern: use set_context instead of multiple set_tag calls
|
|
119
|
+
|
|
120
|
+
User shows a diff migrating from several `set_tag` calls to one `set_context` with a dict.
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
tanagram rules create \
|
|
124
|
+
--name "UseSetContextForStructuredSentryData" \
|
|
125
|
+
--description "When attaching multiple related fields to a Sentry event (e.g., fields about a GitHub check run, a repository, or a session), use sentry_sdk.set_context('context-name', {...}) with a single dict rather than multiple sentry_sdk.set_tag() calls. Anti-pattern: set_tag('github.check_run_id', ...) followed by set_tag('github.pull_request_number', ...) — this scatters related data and is harder to query. Correct: sentry_sdk.set_context('github.check_run', {'owner_login': ..., 'check_run_id': ..., 'pull_request_numbers': ...}). Reserve set_tag for single scalar values used as Sentry filter dimensions." \
|
|
126
|
+
--repos "acme/monorepo"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Common Mistakes to Avoid
|
|
130
|
+
|
|
131
|
+
- **Too vague**: "Use the new utility" — a rule must describe what violation looks like in code
|
|
132
|
+
- **Bundling**: Don't combine two unrelated patterns into one rule; create separate rules
|
|
133
|
+
- **Over-scoping**: Don't apply a rule to the whole codebase if it only applies to one layer (e.g., Inngest handlers)
|
|
134
|
+
- **Redundant rules**: Always check existing rules first
|
|
135
|
+
- **Missing the anti-pattern**: A rule without a concrete anti-pattern is not checkable
|
|
136
|
+
|
package/skills.config.js
ADDED
package/uninstall.js
CHANGED
|
@@ -1,99 +1,88 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const path = require(
|
|
5
|
-
const os = require(
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
const { SKILLS } = require("./skills.config");
|
|
8
|
+
const {
|
|
9
|
+
createTracker,
|
|
10
|
+
isCIEnvironment,
|
|
11
|
+
loreInstallSourcePath,
|
|
12
|
+
loreLaunchAgentPlistPath,
|
|
13
|
+
loreStableBinaryPath,
|
|
14
|
+
loreStatusPath,
|
|
15
|
+
LORE_LABEL,
|
|
16
|
+
} = require("./npm-shared");
|
|
17
|
+
|
|
18
|
+
const track = createTracker({
|
|
19
|
+
getDistinctId() {
|
|
20
|
+
const machineId = os.hostname() + os.userInfo().username;
|
|
21
|
+
return crypto
|
|
22
|
+
.createHash("sha256")
|
|
23
|
+
.update(machineId)
|
|
24
|
+
.digest("hex")
|
|
25
|
+
.slice(0, 16);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function removeIfExists(targetPath) {
|
|
30
|
+
if (fs.existsSync(targetPath)) {
|
|
31
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
32
|
+
}
|
|
21
33
|
}
|
|
22
34
|
|
|
23
|
-
function
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
platform: os.platform(),
|
|
33
|
-
arch: os.arch(),
|
|
34
|
-
node_version: process.version,
|
|
35
|
-
...properties
|
|
36
|
-
},
|
|
37
|
-
timestamp: new Date().toISOString()
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const req = https.request({
|
|
41
|
-
hostname: POSTHOG_HOST,
|
|
42
|
-
port: 443,
|
|
43
|
-
path: '/capture/',
|
|
44
|
-
method: 'POST',
|
|
45
|
-
headers: {
|
|
46
|
-
'Content-Type': 'application/json',
|
|
47
|
-
'Content-Length': data.length
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
req.on('error', () => {});
|
|
52
|
-
req.write(data);
|
|
53
|
-
req.end();
|
|
54
|
-
}
|
|
35
|
+
function removeLoreArtifacts() {
|
|
36
|
+
if (os.platform() === "darwin") {
|
|
37
|
+
const { spawnSync } = require("child_process");
|
|
38
|
+
spawnSync(
|
|
39
|
+
"launchctl",
|
|
40
|
+
["bootout", `gui/${os.userInfo().uid}`, loreLaunchAgentPlistPath()],
|
|
41
|
+
{ encoding: "utf8" },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
55
44
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
process.env.BUILDKITE === 'true' ||
|
|
61
|
-
process.env.GITLAB_CI === 'true' ||
|
|
62
|
-
process.env.TF_BUILD === 'True'
|
|
63
|
-
);
|
|
45
|
+
removeIfExists(loreLaunchAgentPlistPath());
|
|
46
|
+
removeIfExists(loreStableBinaryPath());
|
|
47
|
+
removeIfExists(loreStatusPath());
|
|
48
|
+
removeIfExists(loreInstallSourcePath());
|
|
64
49
|
}
|
|
65
50
|
|
|
66
51
|
function removeClaudeSkills() {
|
|
67
52
|
for (const skill of SKILLS) {
|
|
68
|
-
const skillsDir = path.join(os.homedir(),
|
|
53
|
+
const skillsDir = path.join(os.homedir(), ".claude", "skills", skill);
|
|
69
54
|
|
|
70
55
|
try {
|
|
71
56
|
if (fs.existsSync(skillsDir)) {
|
|
72
57
|
fs.rmSync(skillsDir, { recursive: true });
|
|
73
58
|
console.error(`✓ ${skill} skill removed`);
|
|
74
|
-
track(
|
|
59
|
+
track("cli.skill.uninstall.success", { skill });
|
|
75
60
|
}
|
|
76
61
|
} catch (err) {
|
|
77
62
|
console.error(`Warning: Failed to remove ${skill} skill:`, err.message);
|
|
78
|
-
track(
|
|
63
|
+
track("cli.skill.uninstall.failure", { skill, error: err.message });
|
|
79
64
|
}
|
|
80
65
|
}
|
|
81
66
|
}
|
|
82
67
|
|
|
83
68
|
function removeStopHook() {
|
|
84
|
-
const settingsPath = path.join(os.homedir(),
|
|
69
|
+
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
85
70
|
|
|
86
71
|
try {
|
|
87
72
|
if (!fs.existsSync(settingsPath)) return;
|
|
88
73
|
|
|
89
|
-
const raw = fs.readFileSync(settingsPath,
|
|
74
|
+
const raw = fs.readFileSync(settingsPath, "utf8");
|
|
90
75
|
const settings = JSON.parse(raw);
|
|
91
76
|
|
|
92
77
|
if (!settings.hooks || !Array.isArray(settings.hooks.Stop)) return;
|
|
93
78
|
|
|
94
79
|
const before = settings.hooks.Stop.length;
|
|
95
|
-
settings.hooks.Stop = settings.hooks.Stop.filter(
|
|
96
|
-
|
|
80
|
+
settings.hooks.Stop = settings.hooks.Stop.filter(
|
|
81
|
+
(entry) =>
|
|
82
|
+
!(
|
|
83
|
+
Array.isArray(entry.hooks) &&
|
|
84
|
+
entry.hooks.some((h) => h.command === "tanagram stop-hook")
|
|
85
|
+
),
|
|
97
86
|
);
|
|
98
87
|
const after = settings.hooks.Stop.length;
|
|
99
88
|
|
|
@@ -107,43 +96,44 @@ function removeStopHook() {
|
|
|
107
96
|
delete settings.hooks;
|
|
108
97
|
}
|
|
109
98
|
|
|
110
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) +
|
|
111
|
-
console.error(
|
|
112
|
-
track(
|
|
99
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
100
|
+
console.error("✓ Tanagram Stop hook removed");
|
|
101
|
+
track("cli.stop_hook.uninstall.success");
|
|
113
102
|
} catch (err) {
|
|
114
|
-
console.error(
|
|
115
|
-
track(
|
|
103
|
+
console.error("Warning: Failed to remove Stop hook:", err.message);
|
|
104
|
+
track("cli.stop_hook.uninstall.failure", { error: err.message });
|
|
116
105
|
}
|
|
117
106
|
}
|
|
118
107
|
|
|
119
108
|
function removeOpenCode() {
|
|
120
109
|
if (isCIEnvironment()) return;
|
|
121
110
|
|
|
122
|
-
const { execSync } = require(
|
|
111
|
+
const { execSync } = require("child_process");
|
|
123
112
|
|
|
124
113
|
try {
|
|
125
|
-
execSync(
|
|
114
|
+
execSync("opencode --version", { stdio: "ignore" });
|
|
126
115
|
} catch {
|
|
127
116
|
// Not installed, nothing to do
|
|
128
117
|
return;
|
|
129
118
|
}
|
|
130
119
|
|
|
131
120
|
try {
|
|
132
|
-
execSync(
|
|
133
|
-
console.error(
|
|
134
|
-
track(
|
|
121
|
+
execSync("npm uninstall -g opencode-ai", { stdio: "pipe" });
|
|
122
|
+
console.error("✓ OpenCode uninstalled");
|
|
123
|
+
track("cli.opencode.uninstall.success");
|
|
135
124
|
} catch (err) {
|
|
136
|
-
console.error(
|
|
137
|
-
track(
|
|
125
|
+
console.error("Warning: Failed to uninstall OpenCode:", err.message);
|
|
126
|
+
track("cli.opencode.uninstall.failure", { error: err.message });
|
|
138
127
|
}
|
|
139
128
|
}
|
|
140
129
|
|
|
141
130
|
// Main uninstall flow
|
|
142
|
-
track(
|
|
131
|
+
track("cli.uninstall.start");
|
|
143
132
|
|
|
144
133
|
removeClaudeSkills();
|
|
145
134
|
removeStopHook();
|
|
146
135
|
removeOpenCode();
|
|
136
|
+
removeLoreArtifacts();
|
|
147
137
|
|
|
148
|
-
track(
|
|
149
|
-
console.error(
|
|
138
|
+
track("cli.uninstall.success");
|
|
139
|
+
console.error("✓ Tanagram CLI uninstalled successfully");
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|