greptile 2.2.10 → 2.4.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)
@@ -171,6 +187,7 @@ esac
171
187
  # Parse URL parameters
172
188
  PROMPT=$(parse_url_param "$URL" "prompt")
173
189
  REPO=$(parse_url_param "$URL" "repo")
190
+ ACK_ID=$(parse_url_param "$URL" "ack")
174
191
 
175
192
  if [ -z "$PROMPT" ]; then
176
193
  log "ERROR: No prompt parameter in URL"
@@ -218,5 +235,9 @@ log "Runner script: $RUNNER"
218
235
  # Print the runner path to stdout — the AppleScript app reads this
219
236
  # and uses it to open Terminal (which requires Automation permission
220
237
  # that only the .app bundle has).
238
+ # Line 1: runner path, Line 2 (optional): ACK ID for auto-redirect
221
239
  echo "$RUNNER"
240
+ if [ -n "$ACK_ID" ]; then
241
+ echo "$ACK_ID"
242
+ fi
222
243
  log "Done"
@@ -10,8 +10,18 @@
10
10
 
11
11
  on open location theURL
12
12
  try
13
- -- 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"
13
+ -- Ensure log directory exists
14
+ do shell script "mkdir -p $HOME/.cache/greptile"
15
+ -- Call greptile-fix to parse URL and create runner script
16
+ -- Output: line 1 = runner path, line 2 (optional) = ACK ID for auto-redirect
17
+ set fixOutput 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"
18
+
19
+ set outputLines to paragraphs of fixOutput
20
+ set runnerPath to item 1 of outputLines
21
+ set ackId to ""
22
+ if (count of outputLines) > 1 then
23
+ set ackId to item 2 of outputLines
24
+ end if
15
25
 
16
26
  if runnerPath is not "" then
17
27
  -- Detect terminal by checking running processes via shell (no accessibility permissions needed).
@@ -34,28 +44,35 @@ on open location theURL
34
44
  end if
35
45
  end try
36
46
 
37
- do shell script "echo 'Detected terminal: " & termApp & "' >> /tmp/greptile-fix.log"
47
+ do shell script "echo 'Detected terminal: " & termApp & "' >> $HOME/.cache/greptile/greptile-fix.log"
38
48
 
39
49
  -- Terminal-specific launch commands for macOS.
40
50
  -- Ghostty/kitty/WezTerm/Alacritty close the window when the -e command exits,
41
51
  -- so we launch bash and source the runner script to keep the shell session alive
42
52
  -- after the IDE command (claude/codex/cursor) finishes.
43
53
  -- 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"
54
+ set cleanEnv to "unset CLAUDECODE; source " & quoted form of runnerPath & " ; exec zsh"
45
55
  if termApp is "Ghostty" then
46
- do shell script "open -na Ghostty --args -e zsh -li -c '" & cleanEnv & "' &>/dev/null &"
56
+ do shell script "open -na Ghostty --args -e zsh -li -c " & quoted form of cleanEnv & " &>/dev/null &"
47
57
  else if termApp is "kitty" then
48
- do shell script "open -na kitty --args zsh -li -c '" & cleanEnv & "' &>/dev/null &"
58
+ do shell script "open -na kitty --args zsh -li -c " & quoted form of cleanEnv & " &>/dev/null &"
49
59
  else if termApp is "WezTerm" then
50
- do shell script "open -na WezTerm --args start -- zsh -li -c '" & cleanEnv & "' &>/dev/null &"
60
+ do shell script "open -na WezTerm --args start -- zsh -li -c " & quoted form of cleanEnv & " &>/dev/null &"
51
61
  else if termApp is "Alacritty" then
52
- do shell script "open -na Alacritty --args -e zsh -li -c '" & cleanEnv & "' &>/dev/null &"
62
+ do shell script "open -na Alacritty --args -e zsh -li -c " & quoted form of cleanEnv & " &>/dev/null &"
53
63
  else
54
64
  -- Terminal.app, iTerm, and Warp natively execute scripts via "open -a"
55
65
  do shell script "open -a " & quoted form of termApp & " " & quoted form of runnerPath
56
66
  end if
67
+
68
+ -- Send ACK to health server so the web page can auto-redirect back to the PR
69
+ if ackId is not "" then
70
+ try
71
+ do shell script "curl -s -X POST http://127.0.0.1:4747/ack/" & quoted form of ackId & " --max-time 2 &>/dev/null &"
72
+ end try
73
+ end if
57
74
  end if
58
75
  on error errMsg
59
- do shell script "echo '[ERROR] " & quoted form of errMsg & "' >> /tmp/greptile-fix.log"
76
+ do shell script "echo '[ERROR] " & quoted form of errMsg & "' >> $HOME/.cache/greptile/greptile-fix.log"
60
77
  end try
61
78
  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
@@ -72,10 +87,52 @@ function checkForUpdate() {
72
87
  res.on('end', () => {
73
88
  try {
74
89
  const latest = JSON.parse(data)
75
- if (latest.version && latest.version !== localVersion) {
90
+ if (latest.version && isNewerVersion(localVersion, latest.version)) {
76
91
  console.log(`Update available: ${localVersion} → ${latest.version}. Installing...`)
77
- // Resolve npm path — launchd has minimal PATH
78
- 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
+ ? [
100
+ path.join(home, '.volta/bin/npm'),
101
+ path.join(home, '.asdf/shims/npm'),
102
+ path.join(home, '.local/bin/npm'),
103
+ ]
104
+ : []),
105
+ ]
106
+ // Also check nvm: find the current default node version's npm
107
+ if (home) {
108
+ try {
109
+ const nvmDir = path.join(home, '.nvm/versions/node')
110
+ if (fs.existsSync(nvmDir)) {
111
+ const versions = fs.readdirSync(nvmDir).sort().reverse()
112
+ for (const v of versions) {
113
+ const candidate = path.join(nvmDir, v, 'bin/npm')
114
+ if (fs.existsSync(candidate)) {
115
+ npmPaths.push(candidate)
116
+ break
117
+ }
118
+ }
119
+ }
120
+ } catch {}
121
+ // Also check fnm
122
+ try {
123
+ const fnmDir = path.join(home, '.local/share/fnm/node-versions')
124
+ if (fs.existsSync(fnmDir)) {
125
+ const versions = fs.readdirSync(fnmDir).sort().reverse()
126
+ for (const v of versions) {
127
+ const candidate = path.join(fnmDir, v, 'installation/bin/npm')
128
+ if (fs.existsSync(candidate)) {
129
+ npmPaths.push(candidate)
130
+ break
131
+ }
132
+ }
133
+ }
134
+ } catch {}
135
+ }
79
136
  let npmBin = 'npm'
80
137
  for (const p of npmPaths) {
81
138
  if (fs.existsSync(p)) {
@@ -83,16 +140,17 @@ function checkForUpdate() {
83
140
  break
84
141
  }
85
142
  }
143
+ isUpdating = true
86
144
  let child
87
145
  try {
88
146
  child = spawn(npmBin, ['install', '-g', 'greptile@latest'], {
89
147
  stdio: 'inherit',
90
148
  })
91
149
  } catch (err) {
150
+ isUpdating = false
92
151
  console.error('Failed to spawn npm:', err.message)
93
152
  return
94
153
  }
95
- isUpdating = true
96
154
  child.on('close', (code) => {
97
155
  isUpdating = false
98
156
  if (code === 0) {
@@ -117,18 +175,42 @@ function checkForUpdate() {
117
175
  })
118
176
  }
119
177
 
178
+ // --- ACK store for "Fix in X" auto-redirect ---
179
+ const ackStore = new Map() // id → timestamp
180
+ const ACK_TTL_MS = 60_000
181
+
182
+ function pruneAcks() {
183
+ const now = Date.now()
184
+ for (const [id, ts] of ackStore) {
185
+ if (now - ts > ACK_TTL_MS) ackStore.delete(id)
186
+ }
187
+ }
188
+ setInterval(pruneAcks, 30_000)
189
+
190
+ const ACK_PATH_RE = /^\/ack\/([a-zA-Z0-9_-]+)$/
191
+
120
192
  const ALLOWED_ORIGINS = ['https://app.greptile.com', 'https://app.staging.greptile.com', 'http://localhost:3000']
121
193
 
194
+ function isAllowedOrigin(origin) {
195
+ if (ALLOWED_ORIGINS.includes(origin)) return true
196
+ // Allow Vercel preview deployments (e.g. https://greptilia-abc123.vercel.app)
197
+ if (/^https:\/\/greptilia-[a-z0-9-]+\.vercel\.app$/.test(origin)) return true
198
+ return false
199
+ }
200
+
122
201
  const server = http.createServer((req, res) => {
123
202
  const origin = req.headers.origin || ''
124
- if (ALLOWED_ORIGINS.includes(origin)) {
203
+ const allowed = isAllowedOrigin(origin)
204
+
205
+ if (allowed) {
125
206
  res.setHeader('Access-Control-Allow-Origin', origin)
207
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
208
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
209
+ res.setHeader('Access-Control-Allow-Private-Network', 'true')
126
210
  }
127
- res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
128
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
129
211
 
130
212
  if (req.method === 'OPTIONS') {
131
- res.writeHead(204)
213
+ res.writeHead(allowed ? 204 : 403)
132
214
  res.end()
133
215
  return
134
216
  }
@@ -139,6 +221,26 @@ const server = http.createServer((req, res) => {
139
221
  return
140
222
  }
141
223
 
224
+ // ACK endpoints for "Fix in X" auto-redirect
225
+ const urlPath = (req.url || '').split('?')[0]
226
+ const ackMatch = urlPath.match(ACK_PATH_RE)
227
+ if (ackMatch) {
228
+ const id = ackMatch[1]
229
+ if (req.method === 'POST') {
230
+ ackStore.set(id, Date.now())
231
+ res.writeHead(200, { 'Content-Type': 'application/json' })
232
+ res.end(JSON.stringify({ ok: true }))
233
+ return
234
+ }
235
+ if (req.method === 'GET') {
236
+ const acked = ackStore.has(id)
237
+ if (acked) ackStore.delete(id)
238
+ res.writeHead(200, { 'Content-Type': 'application/json' })
239
+ res.end(JSON.stringify({ acked }))
240
+ return
241
+ }
242
+ }
243
+
142
244
  res.writeHead(404)
143
245
  res.end()
144
246
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "greptile",
3
- "version": "2.2.10",
3
+ "version": "2.4.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 ""