@tanagram/cli 0.4.13 → 0.4.18
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/commands/run.go +21 -23
- 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.18_darwin_amd64.tar.gz +0 -0
- package/dist/npm/tanagram_0.4.18_darwin_arm64.tar.gz +0 -0
- package/dist/npm/tanagram_0.4.18_linux_amd64.tar.gz +0 -0
- package/dist/npm/tanagram_0.4.18_linux_arm64.tar.gz +0 -0
- package/dist/npm/tanagram_0.4.18_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 +2 -1
- package/go.sum +4 -0
- package/install.js +176 -22
- package/main.go +109 -20
- package/package.json +5 -4
- package/tui/welcome.go +1 -13
- package/utils/process.go +3 -2
- package/tui/puzzle.go +0 -694
- package/tui/renderer.go +0 -359
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
|
@@ -2,19 +2,30 @@ package main
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"encoding/json"
|
|
5
|
+
"flag"
|
|
5
6
|
"fmt"
|
|
6
7
|
"io"
|
|
8
|
+
"log/slog"
|
|
7
9
|
"os"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"strings"
|
|
8
12
|
|
|
9
13
|
"github.com/tanagram/cli/commands"
|
|
10
14
|
"github.com/tanagram/cli/metrics"
|
|
11
15
|
"github.com/tanagram/cli/tui"
|
|
12
16
|
"github.com/tanagram/cli/utils"
|
|
17
|
+
"golang.org/x/term"
|
|
13
18
|
)
|
|
14
19
|
|
|
15
20
|
// Version is set in install.js via `-X` ldflags
|
|
16
21
|
var Version = "dev"
|
|
17
22
|
|
|
23
|
+
var (
|
|
24
|
+
flagLogLevel = flag.String("log-level", "info", "log level: debug, info, warn, error")
|
|
25
|
+
flagLogFormat = flag.String("log-format", "text", "log format: text or json")
|
|
26
|
+
flagLogFile = flag.String("log-file", "", "log output file path (defaults to stderr if not specified)")
|
|
27
|
+
)
|
|
28
|
+
|
|
18
29
|
func main() {
|
|
19
30
|
metrics.SetVersion(Version)
|
|
20
31
|
metrics.Init()
|
|
@@ -30,16 +41,50 @@ func main() {
|
|
|
30
41
|
os.Exit(exitCode)
|
|
31
42
|
}()
|
|
32
43
|
|
|
44
|
+
flag.Parse()
|
|
45
|
+
|
|
33
46
|
// Get subcommand (default to "run" if none provided)
|
|
34
47
|
subcommand := "run"
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
args := flag.Args()
|
|
49
|
+
if len(args) > 0 {
|
|
50
|
+
subcommand = args[0]
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
metrics.Track("cli.start", map[string]interface{}{
|
|
40
54
|
"subcommand": subcommand,
|
|
41
55
|
})
|
|
42
56
|
|
|
57
|
+
var logOutput io.Writer = os.Stderr
|
|
58
|
+
if utils.GetParentProcess() == "claude" {
|
|
59
|
+
// We use "exit-code 2" behavior for claude: https://code.claude.com/docs/en/hooks#simple:-exit-code
|
|
60
|
+
// Claude expects communication (i.e. `stop` hook output) to come on `stderr`,
|
|
61
|
+
// Which means we want to send log output (i.e. not output-to-Claude) to `stdout`.
|
|
62
|
+
logOutput = os.Stdout
|
|
63
|
+
}
|
|
64
|
+
if *flagLogFile != "" {
|
|
65
|
+
logPath, logErr := createLogFile(*flagLogFile)
|
|
66
|
+
if logErr != nil {
|
|
67
|
+
slog.Error("Error setting up logging", "error", logErr)
|
|
68
|
+
exitCode = 1
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
logFile, logErr := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
72
|
+
if logErr != nil {
|
|
73
|
+
slog.Error("Error opening log file", "error", logErr)
|
|
74
|
+
exitCode = 1
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
defer logFile.Close()
|
|
78
|
+
logOutput = logFile
|
|
79
|
+
}
|
|
80
|
+
isTTY := term.IsTerminal(int(os.Stdout.Fd()))
|
|
81
|
+
logger := newLogger(*flagLogLevel, *flagLogFormat, logOutput, isTTY)
|
|
82
|
+
slog.SetDefault(logger)
|
|
83
|
+
|
|
84
|
+
slog.Info("Running CLI with args",
|
|
85
|
+
"args", os.Args[1:],
|
|
86
|
+
)
|
|
87
|
+
|
|
43
88
|
// THIS IS A HUGE HACK
|
|
44
89
|
// Cursor runs its hooks in the ~/.cursor directory as cwd
|
|
45
90
|
// Claude runs its hooks directly as a subprocess, with your terminal directory as cwd
|
|
@@ -54,11 +99,11 @@ func main() {
|
|
|
54
99
|
WorkspaceRoots []string `json:"workspace_roots"`
|
|
55
100
|
}
|
|
56
101
|
if err := json.Unmarshal(input, &payload); err != nil {
|
|
57
|
-
|
|
102
|
+
slog.Error("Error parsing input", "error", err)
|
|
58
103
|
} else if len(payload.WorkspaceRoots) > 0 {
|
|
59
104
|
os.Chdir(payload.WorkspaceRoots[0])
|
|
60
105
|
} else {
|
|
61
|
-
|
|
106
|
+
slog.Warn("No workspace roots found")
|
|
62
107
|
}
|
|
63
108
|
}
|
|
64
109
|
}
|
|
@@ -71,7 +116,7 @@ func main() {
|
|
|
71
116
|
})
|
|
72
117
|
// Auto-setup hooks on first run
|
|
73
118
|
if err := commands.EnsureHooksConfigured(); err != nil {
|
|
74
|
-
|
|
119
|
+
slog.Error("Failed to configure hooks", "error", err)
|
|
75
120
|
exitCode = 1
|
|
76
121
|
return
|
|
77
122
|
}
|
|
@@ -87,7 +132,7 @@ func main() {
|
|
|
87
132
|
})
|
|
88
133
|
// Auto-setup hooks on first run
|
|
89
134
|
if err := commands.EnsureHooksConfigured(); err != nil {
|
|
90
|
-
|
|
135
|
+
slog.Error("Failed to configure hooks", "error", err)
|
|
91
136
|
exitCode = 1
|
|
92
137
|
return
|
|
93
138
|
}
|
|
@@ -98,7 +143,7 @@ func main() {
|
|
|
98
143
|
})
|
|
99
144
|
// Auto-setup hooks on first run
|
|
100
145
|
if err := commands.EnsureHooksConfigured(); err != nil {
|
|
101
|
-
|
|
146
|
+
slog.Error("Failed to configure hooks", "error", err)
|
|
102
147
|
exitCode = 1
|
|
103
148
|
return
|
|
104
149
|
}
|
|
@@ -152,7 +197,7 @@ func main() {
|
|
|
152
197
|
})
|
|
153
198
|
choice, err := tui.RunWelcomeScreen()
|
|
154
199
|
if err != nil {
|
|
155
|
-
|
|
200
|
+
slog.Error("Welcome screen failed", "error", err)
|
|
156
201
|
exitCode = 1
|
|
157
202
|
return
|
|
158
203
|
}
|
|
@@ -177,17 +222,6 @@ func main() {
|
|
|
177
222
|
"command": "sync-policies",
|
|
178
223
|
})
|
|
179
224
|
err = commands.SyncPolicies()
|
|
180
|
-
case "puzzle":
|
|
181
|
-
metrics.Track("cli.command.execute", map[string]interface{}{
|
|
182
|
-
"command": "puzzle",
|
|
183
|
-
})
|
|
184
|
-
err := tui.RunPuzzleEditor()
|
|
185
|
-
if err != nil {
|
|
186
|
-
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
187
|
-
exitCode = 1
|
|
188
|
-
return
|
|
189
|
-
}
|
|
190
|
-
return
|
|
191
225
|
case "version", "-v", "--version":
|
|
192
226
|
fmt.Println(Version)
|
|
193
227
|
return
|
|
@@ -206,7 +240,7 @@ func main() {
|
|
|
206
240
|
"command": subcommand,
|
|
207
241
|
"error": err.Error(),
|
|
208
242
|
})
|
|
209
|
-
|
|
243
|
+
slog.Error("Command failed", "command", subcommand, "error", err)
|
|
210
244
|
exitCode = 1
|
|
211
245
|
return
|
|
212
246
|
}
|
|
@@ -266,3 +300,58 @@ HOOK WORKFLOW:
|
|
|
266
300
|
`
|
|
267
301
|
fmt.Print(help)
|
|
268
302
|
}
|
|
303
|
+
|
|
304
|
+
func newLogger(levelStr, format string, output io.Writer, isTTY bool) *slog.Logger {
|
|
305
|
+
var lvl slog.Level
|
|
306
|
+
switch strings.ToLower(levelStr) {
|
|
307
|
+
case "debug":
|
|
308
|
+
lvl = slog.LevelDebug
|
|
309
|
+
case "info":
|
|
310
|
+
lvl = slog.LevelInfo
|
|
311
|
+
case "warn", "warning":
|
|
312
|
+
lvl = slog.LevelWarn
|
|
313
|
+
case "error":
|
|
314
|
+
lvl = slog.LevelError
|
|
315
|
+
default:
|
|
316
|
+
lvl = slog.LevelInfo
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
opts := &slog.HandlerOptions{
|
|
320
|
+
Level: lvl,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
var h slog.Handler
|
|
324
|
+
switch strings.ToLower(format) {
|
|
325
|
+
case "json":
|
|
326
|
+
h = slog.NewJSONHandler(output, opts)
|
|
327
|
+
default:
|
|
328
|
+
if isTTY {
|
|
329
|
+
opts.ReplaceAttr = func(groups []string, a slog.Attr) slog.Attr {
|
|
330
|
+
if a.Key == slog.TimeKey || a.Key == slog.LevelKey {
|
|
331
|
+
return slog.Attr{}
|
|
332
|
+
}
|
|
333
|
+
return a
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
h = slog.NewTextHandler(output, opts)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return slog.New(h)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
func createLogFile(logFilePath string) (string, error) {
|
|
343
|
+
logPath := logFilePath
|
|
344
|
+
if strings.HasPrefix(logPath, "~/") {
|
|
345
|
+
home, err := os.UserHomeDir()
|
|
346
|
+
if err != nil {
|
|
347
|
+
return "", fmt.Errorf("getting home directory: %w", err)
|
|
348
|
+
}
|
|
349
|
+
logPath = filepath.Join(home, logPath[2:])
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil {
|
|
353
|
+
return "", fmt.Errorf("creating log directory: %w", err)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return logPath, nil
|
|
357
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanagram/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.18",
|
|
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().
|
package/utils/process.go
CHANGED
|
@@ -30,9 +30,10 @@ func GetParentProcess() string {
|
|
|
30
30
|
return
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// Cursor runs hooks via `node` or
|
|
33
|
+
// Cursor runs hooks via `node` or a shell (I've seen both) in ~/.cursor
|
|
34
34
|
// Handle this case and make life easier for users of this function.
|
|
35
|
-
|
|
35
|
+
// `pwsh` is powershell: https://chatgpt.com/share/6927b015-2738-8000-ab6d-792e05c9401b
|
|
36
|
+
if name == "node" || name == "zsh" || name == "bash" || name == "fish" || name == "sh" || strings.Contains(name, "pwsh") {
|
|
36
37
|
cwd, err := parent.Cwd()
|
|
37
38
|
if err == nil && strings.Contains(cwd, ".cursor") {
|
|
38
39
|
parentProcessName = "cursor"
|