greptile 2.2.9 → 2.3.0

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/build-app.sh CHANGED
@@ -24,12 +24,37 @@ APP_PATH="$OUTPUT_DIR/$APP_NAME.app"
24
24
 
25
25
  echo "Building $APP_NAME.app..."
26
26
 
27
- # 1. Compile AppleScript into .app bundle
27
+ # Resolve the absolute path to greptile-fix at install time so the .app
28
+ # works even when launched by macOS with a minimal PATH (e.g. nvm/fnm/volta users).
29
+ GREPTILE_FIX_BIN="$(command -v greptile-fix 2>/dev/null || echo "")"
30
+ if [ -z "$GREPTILE_FIX_BIN" ]; then
31
+ # Fallback: it should be next to this script in bin/
32
+ GREPTILE_FIX_BIN="$SCRIPT_DIR/bin/greptile-fix.js"
33
+ fi
34
+ GREPTILE_FIX_DIR="$(dirname "$GREPTILE_FIX_BIN")"
35
+ echo "Resolved greptile-fix: $GREPTILE_FIX_BIN"
36
+
37
+ # 1. Compile AppleScript into .app bundle — inject the resolved bin directory
38
+ # into the PATH so it works regardless of node version manager.
28
39
  if [ -d "$APP_PATH" ]; then
29
40
  rm -rf "$APP_PATH"
30
41
  fi
31
42
 
32
- osacompile -o "$APP_PATH" "$SCRIPT_DIR/greptile-fix.applescript"
43
+ TEMP_SCRIPT="$(mktemp /tmp/greptile-fix-XXXXXX.applescript)"
44
+ # Clean up temp file on exit (success or failure)
45
+ trap 'rm -f "$TEMP_SCRIPT"' EXIT
46
+
47
+ # Escape sed special characters in the directory path (& \ /)
48
+ ESCAPED_DIR="$(printf '%s' "$GREPTILE_FIX_DIR" | sed 's/[&/\]/\\&/g')"
49
+ sed "s|PATH=/opt/homebrew/bin:/usr/local/bin:|PATH=$ESCAPED_DIR:/opt/homebrew/bin:/usr/local/bin:|" \
50
+ "$SCRIPT_DIR/greptile-fix.applescript" > "$TEMP_SCRIPT"
51
+
52
+ osacompile -o "$APP_PATH" "$TEMP_SCRIPT"
53
+
54
+ if [ ! -d "$APP_PATH" ]; then
55
+ echo "Error: osacompile failed to create $APP_PATH"
56
+ exit 1
57
+ fi
33
58
 
34
59
  # 2. Add URL scheme to Info.plist
35
60
  PLIST="$APP_PATH/Contents/Info.plist"
@@ -16,8 +16,8 @@
16
16
  <key>KeepAlive</key>
17
17
  <true/>
18
18
  <key>StandardOutPath</key>
19
- <string>/tmp/greptile-health.log</string>
19
+ <string>__HOME__/.cache/greptile/greptile-health.log</string>
20
20
  <key>StandardErrorPath</key>
21
- <string>/tmp/greptile-health.log</string>
21
+ <string>__HOME__/.cache/greptile/greptile-health.log</string>
22
22
  </dict>
23
23
  </plist>
package/greptile-fix CHANGED
@@ -15,11 +15,23 @@
15
15
 
16
16
  set -euo pipefail
17
17
 
18
+ # Verify python3 is available (used for URL decoding and JSON operations)
19
+ if ! command -v python3 &>/dev/null; then
20
+ echo "Error: python3 is required but not found in PATH." >&2
21
+ osascript -e 'display dialog "Greptile Fix requires Python 3, which was not found on this system.\n\nPlease install it via: xcode-select --install" buttons {"OK"} default button "OK" with icon caution' 2>/dev/null || true
22
+ exit 1
23
+ fi
24
+
18
25
  CONFIG_DIR="$HOME/.greptile"
19
26
  REPOS_FILE="$CONFIG_DIR/repos.json"
20
- LOG_FILE="/tmp/greptile-fix.log"
27
+ CACHE_DIR="$HOME/.cache/greptile"
28
+ LOG_FILE="$CACHE_DIR/greptile-fix.log"
21
29
 
22
30
  log() {
31
+ # Rotate log if it exceeds 1MB
32
+ if [ -f "$LOG_FILE" ] && [ "$(wc -c < "$LOG_FILE" 2>/dev/null || echo 0)" -gt 1048576 ]; then
33
+ mv -f "$LOG_FILE" "$LOG_FILE.old" 2>/dev/null || true
34
+ fi
23
35
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
24
36
  }
25
37
 
@@ -54,11 +66,15 @@ parse_url_param() {
54
66
  done
55
67
  }
56
68
 
57
- # Ensure config directory exists
69
+ # Ensure config and cache directories exist
58
70
  ensure_config_dir() {
59
71
  if [ ! -d "$CONFIG_DIR" ]; then
60
72
  mkdir -p "$CONFIG_DIR"
61
73
  fi
74
+ if [ ! -d "$CACHE_DIR" ]; then
75
+ mkdir -p "$CACHE_DIR"
76
+ chmod 700 "$CACHE_DIR"
77
+ fi
62
78
  if [ ! -f "$REPOS_FILE" ]; then
63
79
  echo '{}' > "$REPOS_FILE"
64
80
  fi
@@ -120,13 +136,13 @@ create_runner_script() {
120
136
  # Write args to a temp file — avoids all shell quoting issues.
121
137
  # Line 1: repo path, Line 2+: prompt (may be multiline)
122
138
  local argsfile
123
- argsfile=$(mktemp /tmp/greptile-fix-args-XXXXXX)
139
+ argsfile=$(mktemp "$CACHE_DIR/greptile-fix-args-XXXXXX")
124
140
  printf '%s\n' "$repo_path" > "$argsfile"
125
141
  printf '%s' "$prompt" >> "$argsfile"
126
142
 
127
143
  # Write a runner script that reads args from the file, then cleans up
128
144
  local runner
129
- runner=$(mktemp /tmp/greptile-fix-run-XXXXXX)
145
+ runner=$(mktemp "$CACHE_DIR/greptile-fix-run-XXXXXX")
130
146
  chmod +x "$runner"
131
147
 
132
148
  # Note: $argsfile and $cli_cmd are safe to interpolate (mktemp path and validated CLI name)
@@ -10,8 +10,10 @@
10
10
 
11
11
  on open location theURL
12
12
  try
13
+ -- Ensure log directory exists
14
+ do shell script "mkdir -p $HOME/.cache/greptile"
13
15
  -- Call greptile-fix to parse URL and create runner script; it prints the runner path to stdout
14
- set runnerPath to do shell script "PATH=/opt/homebrew/bin:/usr/local/bin:$PATH greptile-fix " & quoted form of theURL & " 2>> /tmp/greptile-fix.log"
16
+ set runnerPath to do shell script "PATH=/opt/homebrew/bin:/usr/local/bin:$PATH greptile-fix " & quoted form of theURL & " 2>> $HOME/.cache/greptile/greptile-fix.log"
15
17
 
16
18
  if runnerPath is not "" then
17
19
  -- Detect terminal by checking running processes via shell (no accessibility permissions needed).
@@ -34,28 +36,28 @@ on open location theURL
34
36
  end if
35
37
  end try
36
38
 
37
- do shell script "echo 'Detected terminal: " & termApp & "' >> /tmp/greptile-fix.log"
39
+ do shell script "echo 'Detected terminal: " & termApp & "' >> $HOME/.cache/greptile/greptile-fix.log"
38
40
 
39
41
  -- Terminal-specific launch commands for macOS.
40
42
  -- Ghostty/kitty/WezTerm/Alacritty close the window when the -e command exits,
41
43
  -- so we launch bash and source the runner script to keep the shell session alive
42
44
  -- after the IDE command (claude/codex/cursor) finishes.
43
45
  -- Unset CLAUDECODE so the new session doesn't think it's nested inside an existing one.
44
- set cleanEnv to "unset CLAUDECODE; source " & runnerPath & " ; exec zsh"
46
+ set cleanEnv to "unset CLAUDECODE; source " & quoted form of runnerPath & " ; exec zsh"
45
47
  if termApp is "Ghostty" then
46
- do shell script "open -na Ghostty --args -e zsh -li -c '" & cleanEnv & "' &>/dev/null &"
48
+ do shell script "open -na Ghostty --args -e zsh -li -c " & quoted form of cleanEnv & " &>/dev/null &"
47
49
  else if termApp is "kitty" then
48
- do shell script "open -na kitty --args zsh -li -c '" & cleanEnv & "' &>/dev/null &"
50
+ do shell script "open -na kitty --args zsh -li -c " & quoted form of cleanEnv & " &>/dev/null &"
49
51
  else if termApp is "WezTerm" then
50
- do shell script "open -na WezTerm --args start -- zsh -li -c '" & cleanEnv & "' &>/dev/null &"
52
+ do shell script "open -na WezTerm --args start -- zsh -li -c " & quoted form of cleanEnv & " &>/dev/null &"
51
53
  else if termApp is "Alacritty" then
52
- do shell script "open -na Alacritty --args -e zsh -li -c '" & cleanEnv & "' &>/dev/null &"
54
+ do shell script "open -na Alacritty --args -e zsh -li -c " & quoted form of cleanEnv & " &>/dev/null &"
53
55
  else
54
56
  -- Terminal.app, iTerm, and Warp natively execute scripts via "open -a"
55
57
  do shell script "open -a " & quoted form of termApp & " " & quoted form of runnerPath
56
58
  end if
57
59
  end if
58
60
  on error errMsg
59
- do shell script "echo '[ERROR] " & quoted form of errMsg & "' >> /tmp/greptile-fix.log"
61
+ do shell script "echo '[ERROR] " & quoted form of errMsg & "' >> $HOME/.cache/greptile/greptile-fix.log"
60
62
  end try
61
63
  end open location
package/health-server.js CHANGED
@@ -7,6 +7,21 @@ const { spawn } = require('child_process')
7
7
 
8
8
  const PORT = 4747
9
9
 
10
+ /**
11
+ * Compare two semver strings. Returns true if b is newer than a.
12
+ * Handles x.y.z format; ignores pre-release suffixes.
13
+ */
14
+ function isNewerVersion(current, latest) {
15
+ const parse = (v) => (v || '').replace(/^v/, '').split('-')[0].split('.').map(Number)
16
+ const a = parse(current)
17
+ const b = parse(latest)
18
+ for (let i = 0; i < 3; i++) {
19
+ if ((b[i] || 0) > (a[i] || 0)) return true
20
+ if ((b[i] || 0) < (a[i] || 0)) return false
21
+ }
22
+ return false
23
+ }
24
+
10
25
  // Self-destruct: if the package has been uninstalled, clean up and exit.
11
26
  // Checks that package.json next to this script still exists.
12
27
  // Note: we intentionally don't check `command -v greptile-fix` because
@@ -56,36 +71,84 @@ function checkForUpdate() {
56
71
  }
57
72
 
58
73
  const req = https.get('https://registry.npmjs.org/greptile/latest', { timeout: 10_000 }, (res) => {
59
- if (res.statusCode !== 200) { res.resume(); return }
74
+ if (res.statusCode !== 200) {
75
+ res.resume()
76
+ return
77
+ }
60
78
  let data = ''
61
79
  const MAX_BODY = 100_000 // 100KB safety limit
62
80
  res.on('data', (chunk) => {
63
81
  data += chunk
64
- if (data.length > MAX_BODY) { req.destroy(); return }
82
+ if (data.length > MAX_BODY) {
83
+ req.destroy()
84
+ return
85
+ }
65
86
  })
66
87
  res.on('end', () => {
67
88
  try {
68
89
  const latest = JSON.parse(data)
69
- if (latest.version && latest.version !== localVersion) {
90
+ if (latest.version && isNewerVersion(localVersion, latest.version)) {
70
91
  console.log(`Update available: ${localVersion} → ${latest.version}. Installing...`)
71
- // Resolve npm path — launchd has minimal PATH
72
- const npmPaths = ['/opt/homebrew/bin/npm', '/usr/local/bin/npm']
92
+ // Resolve npm path — launchd has minimal PATH.
93
+ // Check well-known locations including node version managers.
94
+ const home = process.env.HOME || ''
95
+ const npmPaths = [
96
+ '/opt/homebrew/bin/npm',
97
+ '/usr/local/bin/npm',
98
+ ...(home ? [
99
+ path.join(home, '.volta/bin/npm'),
100
+ path.join(home, '.asdf/shims/npm'),
101
+ path.join(home, '.local/bin/npm'),
102
+ ] : []),
103
+ ]
104
+ // Also check nvm: find the current default node version's npm
105
+ if (home) {
106
+ try {
107
+ const nvmDir = path.join(home, '.nvm/versions/node')
108
+ if (fs.existsSync(nvmDir)) {
109
+ const versions = fs.readdirSync(nvmDir).sort().reverse()
110
+ for (const v of versions) {
111
+ const candidate = path.join(nvmDir, v, 'bin/npm')
112
+ if (fs.existsSync(candidate)) {
113
+ npmPaths.push(candidate)
114
+ break
115
+ }
116
+ }
117
+ }
118
+ } catch {}
119
+ // Also check fnm
120
+ try {
121
+ const fnmDir = path.join(home, '.local/share/fnm/node-versions')
122
+ if (fs.existsSync(fnmDir)) {
123
+ const versions = fs.readdirSync(fnmDir).sort().reverse()
124
+ for (const v of versions) {
125
+ const candidate = path.join(fnmDir, v, 'installation/bin/npm')
126
+ if (fs.existsSync(candidate)) {
127
+ npmPaths.push(candidate)
128
+ break
129
+ }
130
+ }
131
+ }
132
+ } catch {}
133
+ }
73
134
  let npmBin = 'npm'
74
135
  for (const p of npmPaths) {
75
- if (fs.existsSync(p)) { npmBin = p; break }
136
+ if (fs.existsSync(p)) {
137
+ npmBin = p
138
+ break
139
+ }
76
140
  }
141
+ isUpdating = true
77
142
  let child
78
143
  try {
79
144
  child = spawn(npmBin, ['install', '-g', 'greptile@latest'], {
80
- stdio: 'ignore',
81
- detached: true,
145
+ stdio: 'inherit',
82
146
  })
83
147
  } catch (err) {
148
+ isUpdating = false
84
149
  console.error('Failed to spawn npm:', err.message)
85
150
  return
86
151
  }
87
- isUpdating = true
88
- child.unref()
89
152
  child.on('close', (code) => {
90
153
  isUpdating = false
91
154
  if (code === 0) {
@@ -105,21 +168,33 @@ function checkForUpdate() {
105
168
  })
106
169
  })
107
170
  req.on('error', () => {}) // Silently ignore network errors
108
- req.on('timeout', () => { req.destroy() })
171
+ req.on('timeout', () => {
172
+ req.destroy()
173
+ })
109
174
  }
110
175
 
111
176
  const ALLOWED_ORIGINS = ['https://app.greptile.com', 'https://app.staging.greptile.com', 'http://localhost:3000']
112
177
 
178
+ function isAllowedOrigin(origin) {
179
+ if (ALLOWED_ORIGINS.includes(origin)) return true
180
+ // Allow Vercel preview deployments (e.g. https://greptilia-xyz.vercel.app)
181
+ if (/^https:\/\/[a-z0-9-]+\.vercel\.app$/.test(origin)) return true
182
+ return false
183
+ }
184
+
113
185
  const server = http.createServer((req, res) => {
114
186
  const origin = req.headers.origin || ''
115
- if (ALLOWED_ORIGINS.includes(origin)) {
187
+ const allowed = isAllowedOrigin(origin)
188
+
189
+ if (allowed) {
116
190
  res.setHeader('Access-Control-Allow-Origin', origin)
191
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
192
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
193
+ res.setHeader('Access-Control-Allow-Private-Network', 'true')
117
194
  }
118
- res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
119
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
120
195
 
121
196
  if (req.method === 'OPTIONS') {
122
- res.writeHead(204)
197
+ res.writeHead(allowed ? 204 : 403)
123
198
  res.end()
124
199
  return
125
200
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "greptile",
3
- "version": "2.2.9",
3
+ "version": "2.3.0",
4
4
  "description": "Bridge for Greptile code review 'Fix in Claude Code' and 'Fix in Codex' links",
5
5
  "bin": {
6
6
  "greptile-fix": "bin/greptile-fix.js"
@@ -19,6 +19,9 @@
19
19
  "health-server.js",
20
20
  "com.greptile.health.plist"
21
21
  ],
22
+ "engines": {
23
+ "node": ">=18.0.0"
24
+ },
22
25
  "os": [
23
26
  "darwin"
24
27
  ],
package/postinstall.sh CHANGED
@@ -20,7 +20,10 @@ APP_DIR="$HOME/Applications"
20
20
  mkdir -p "$APP_DIR"
21
21
 
22
22
  # Build the .app bundle into ~/Applications
23
- bash "$SCRIPT_DIR/build-app.sh" "$APP_DIR"
23
+ if ! bash "$SCRIPT_DIR/build-app.sh" "$APP_DIR"; then
24
+ echo "greptile: Warning — failed to build .app bundle. The greptile:// URL scheme may not work."
25
+ echo "greptile: You can retry by running: bash $(printf '%q' "$SCRIPT_DIR/build-app.sh") $(printf '%q' "$APP_DIR")"
26
+ fi
24
27
 
25
28
  # --- Health server LaunchAgent ---
26
29
  PLIST_NAME="com.greptile.health.plist"
@@ -31,18 +34,39 @@ if [ -f "$PLIST_SRC" ]; then
31
34
  # Ensure LaunchAgents directory exists
32
35
  mkdir -p "$HOME/Library/LaunchAgents"
33
36
 
34
- # Resolve the node binary path
35
- NODE_BIN="$(command -v node 2>/dev/null || echo "/usr/local/bin/node")"
37
+ # Resolve the node binary path (check common locations for Apple Silicon and Intel Macs)
38
+ NODE_BIN="$(command -v node 2>/dev/null || echo "")"
39
+ if [ -z "$NODE_BIN" ]; then
40
+ for candidate in /opt/homebrew/bin/node /usr/local/bin/node; do
41
+ if [ -x "$candidate" ]; then
42
+ NODE_BIN="$candidate"
43
+ break
44
+ fi
45
+ done
46
+ fi
47
+ if [ -z "$NODE_BIN" ]; then
48
+ echo "greptile: Warning — could not find node binary. Health server may not start."
49
+ NODE_BIN="/usr/local/bin/node"
50
+ fi
51
+
52
+ # Ensure log directory exists
53
+ mkdir -p "$HOME/.cache/greptile"
54
+ chmod 700 "$HOME/.cache/greptile"
36
55
 
37
56
  # Create a configured copy with the correct paths
38
57
  sed -e "s|__PACKAGE_DIR__|$SCRIPT_DIR|g" \
58
+ -e "s|__HOME__|$HOME|g" \
39
59
  -e "s|/usr/local/bin/node|$NODE_BIN|g" \
40
60
  "$PLIST_SRC" > "$PLIST_DST"
41
61
 
42
62
  # Load the agent (unload first in case it's already loaded)
43
63
  launchctl unload "$PLIST_DST" 2>/dev/null || true
44
- launchctl load "$PLIST_DST"
45
- echo "Greptile health server installed and started."
64
+ if launchctl load "$PLIST_DST" 2>/dev/null; then
65
+ echo "Greptile health server installed and started."
66
+ else
67
+ echo "greptile: Warning — failed to load health server LaunchAgent."
68
+ echo "greptile: You can retry by running: launchctl load $(printf '%q' "$PLIST_DST")"
69
+ fi
46
70
  fi
47
71
 
48
72
  echo ""