greptile 2.3.0 → 3.0.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/README.md +62 -0
- package/dist/greptile.js +467 -0
- package/package.json +51 -20
- package/bin/greptile-fix.js +0 -10
- package/build-app.sh +0 -81
- package/com.greptile.health.plist +0 -23
- package/greptile-fix +0 -238
- package/greptile-fix.applescript +0 -63
- package/health-server.js +0 -228
- package/postinstall.sh +0 -77
- package/preuninstall.sh +0 -23
package/health-server.js
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
const http = require('http')
|
|
3
|
-
const https = require('https')
|
|
4
|
-
const fs = require('fs')
|
|
5
|
-
const path = require('path')
|
|
6
|
-
const { spawn } = require('child_process')
|
|
7
|
-
|
|
8
|
-
const PORT = 4747
|
|
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
|
-
|
|
25
|
-
// Self-destruct: if the package has been uninstalled, clean up and exit.
|
|
26
|
-
// Checks that package.json next to this script still exists.
|
|
27
|
-
// Note: we intentionally don't check `command -v greptile-fix` because
|
|
28
|
-
// launchd runs with a minimal PATH that doesn't include npm global bin.
|
|
29
|
-
const PACKAGE_MARKER = path.join(__dirname, 'package.json')
|
|
30
|
-
const SELF_CHECK_INTERVAL_MS = 30_000
|
|
31
|
-
const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000 // 6 hours
|
|
32
|
-
let isUpdating = false
|
|
33
|
-
|
|
34
|
-
function selfCheck() {
|
|
35
|
-
// Don't self-destruct during an update — npm temporarily removes the package dir
|
|
36
|
-
if (isUpdating) return
|
|
37
|
-
|
|
38
|
-
const markerGone = !fs.existsSync(PACKAGE_MARKER)
|
|
39
|
-
|
|
40
|
-
if (markerGone) {
|
|
41
|
-
console.log('Package uninstalled — cleaning up and exiting.')
|
|
42
|
-
const { execSync } = require('child_process')
|
|
43
|
-
const home = process.env.HOME || ''
|
|
44
|
-
const plistPath = path.join(home, 'Library/LaunchAgents/com.greptile.health.plist')
|
|
45
|
-
// Remove .app bundle and plist file
|
|
46
|
-
try {
|
|
47
|
-
fs.rmSync(path.join(home, 'Applications/Greptile Fix.app'), { recursive: true, force: true })
|
|
48
|
-
} catch {}
|
|
49
|
-
try {
|
|
50
|
-
fs.unlinkSync(plistPath)
|
|
51
|
-
} catch {}
|
|
52
|
-
server.close()
|
|
53
|
-
// Remove the launchd job by label so KeepAlive won't restart us
|
|
54
|
-
try {
|
|
55
|
-
execSync('launchctl remove com.greptile.health 2>/dev/null')
|
|
56
|
-
} catch {}
|
|
57
|
-
process.exit(0)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Auto-update: periodically check npm registry for a newer version and install it.
|
|
62
|
-
// After a successful update, the process exits so launchd restarts it with the new code.
|
|
63
|
-
function checkForUpdate() {
|
|
64
|
-
if (isUpdating) return
|
|
65
|
-
let localVersion
|
|
66
|
-
try {
|
|
67
|
-
const pkg = JSON.parse(fs.readFileSync(PACKAGE_MARKER, 'utf8'))
|
|
68
|
-
localVersion = pkg.version
|
|
69
|
-
} catch {
|
|
70
|
-
return // Can't read local version, skip
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const req = https.get('https://registry.npmjs.org/greptile/latest', { timeout: 10_000 }, (res) => {
|
|
74
|
-
if (res.statusCode !== 200) {
|
|
75
|
-
res.resume()
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
let data = ''
|
|
79
|
-
const MAX_BODY = 100_000 // 100KB safety limit
|
|
80
|
-
res.on('data', (chunk) => {
|
|
81
|
-
data += chunk
|
|
82
|
-
if (data.length > MAX_BODY) {
|
|
83
|
-
req.destroy()
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
})
|
|
87
|
-
res.on('end', () => {
|
|
88
|
-
try {
|
|
89
|
-
const latest = JSON.parse(data)
|
|
90
|
-
if (latest.version && isNewerVersion(localVersion, latest.version)) {
|
|
91
|
-
console.log(`Update available: ${localVersion} → ${latest.version}. Installing...`)
|
|
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
|
-
}
|
|
134
|
-
let npmBin = 'npm'
|
|
135
|
-
for (const p of npmPaths) {
|
|
136
|
-
if (fs.existsSync(p)) {
|
|
137
|
-
npmBin = p
|
|
138
|
-
break
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
isUpdating = true
|
|
142
|
-
let child
|
|
143
|
-
try {
|
|
144
|
-
child = spawn(npmBin, ['install', '-g', 'greptile@latest'], {
|
|
145
|
-
stdio: 'inherit',
|
|
146
|
-
})
|
|
147
|
-
} catch (err) {
|
|
148
|
-
isUpdating = false
|
|
149
|
-
console.error('Failed to spawn npm:', err.message)
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
child.on('close', (code) => {
|
|
153
|
-
isUpdating = false
|
|
154
|
-
if (code === 0) {
|
|
155
|
-
console.log(`Updated to greptile@${latest.version}. Restarting...`)
|
|
156
|
-
// Exit so launchd restarts the server with the new code
|
|
157
|
-
setTimeout(() => process.exit(0), 2000)
|
|
158
|
-
} else {
|
|
159
|
-
console.error(`Update failed with exit code ${code}`)
|
|
160
|
-
}
|
|
161
|
-
})
|
|
162
|
-
child.on('error', (err) => {
|
|
163
|
-
isUpdating = false
|
|
164
|
-
console.error('Update spawn error:', err.message)
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
} catch {}
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
req.on('error', () => {}) // Silently ignore network errors
|
|
171
|
-
req.on('timeout', () => {
|
|
172
|
-
req.destroy()
|
|
173
|
-
})
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const ALLOWED_ORIGINS = ['https://app.greptile.com', 'https://app.staging.greptile.com', 'http://localhost:3000']
|
|
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
|
-
|
|
185
|
-
const server = http.createServer((req, res) => {
|
|
186
|
-
const origin = req.headers.origin || ''
|
|
187
|
-
const allowed = isAllowedOrigin(origin)
|
|
188
|
-
|
|
189
|
-
if (allowed) {
|
|
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')
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (req.method === 'OPTIONS') {
|
|
197
|
-
res.writeHead(allowed ? 204 : 403)
|
|
198
|
-
res.end()
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (req.method === 'GET' && req.url === '/health') {
|
|
203
|
-
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
204
|
-
res.end(JSON.stringify({ status: 'ok' }))
|
|
205
|
-
return
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
res.writeHead(404)
|
|
209
|
-
res.end()
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
server.on('error', (err) => {
|
|
213
|
-
if (err.code === 'EADDRINUSE') {
|
|
214
|
-
console.error(`Port ${PORT} is already in use. Exiting.`)
|
|
215
|
-
process.exit(0) // Exit cleanly so launchd KeepAlive doesn't restart in a tight loop
|
|
216
|
-
}
|
|
217
|
-
console.error('Health server error:', err.message)
|
|
218
|
-
process.exit(1)
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
server.listen(PORT, '127.0.0.1', () => {
|
|
222
|
-
console.log(`Greptile health server listening on http://127.0.0.1:${PORT}`)
|
|
223
|
-
// Start self-check loop after server is up
|
|
224
|
-
setInterval(selfCheck, SELF_CHECK_INTERVAL_MS)
|
|
225
|
-
// Check for updates on startup and then every 6 hours
|
|
226
|
-
checkForUpdate()
|
|
227
|
-
setInterval(checkForUpdate, UPDATE_CHECK_INTERVAL_MS)
|
|
228
|
-
})
|
package/postinstall.sh
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
# postinstall.sh — Builds and registers the Greptile Fix .app bundle after npm install.
|
|
4
|
-
#
|
|
5
|
-
# The .app bundle handles the greptile:// URL scheme on macOS.
|
|
6
|
-
# It is installed to ~/Applications so it persists independently of npm.
|
|
7
|
-
|
|
8
|
-
set -euo pipefail
|
|
9
|
-
|
|
10
|
-
# Only run on macOS
|
|
11
|
-
if [ "$(uname)" != "Darwin" ]; then
|
|
12
|
-
echo "greptile: Skipping .app bundle setup (macOS only)"
|
|
13
|
-
exit 0
|
|
14
|
-
fi
|
|
15
|
-
|
|
16
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
17
|
-
APP_DIR="$HOME/Applications"
|
|
18
|
-
|
|
19
|
-
# Ensure ~/Applications exists
|
|
20
|
-
mkdir -p "$APP_DIR"
|
|
21
|
-
|
|
22
|
-
# Build the .app bundle into ~/Applications
|
|
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
|
|
27
|
-
|
|
28
|
-
# --- Health server LaunchAgent ---
|
|
29
|
-
PLIST_NAME="com.greptile.health.plist"
|
|
30
|
-
PLIST_SRC="$SCRIPT_DIR/$PLIST_NAME"
|
|
31
|
-
PLIST_DST="$HOME/Library/LaunchAgents/$PLIST_NAME"
|
|
32
|
-
|
|
33
|
-
if [ -f "$PLIST_SRC" ]; then
|
|
34
|
-
# Ensure LaunchAgents directory exists
|
|
35
|
-
mkdir -p "$HOME/Library/LaunchAgents"
|
|
36
|
-
|
|
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"
|
|
55
|
-
|
|
56
|
-
# Create a configured copy with the correct paths
|
|
57
|
-
sed -e "s|__PACKAGE_DIR__|$SCRIPT_DIR|g" \
|
|
58
|
-
-e "s|__HOME__|$HOME|g" \
|
|
59
|
-
-e "s|/usr/local/bin/node|$NODE_BIN|g" \
|
|
60
|
-
"$PLIST_SRC" > "$PLIST_DST"
|
|
61
|
-
|
|
62
|
-
# Load the agent (unload first in case it's already loaded)
|
|
63
|
-
launchctl unload "$PLIST_DST" 2>/dev/null || true
|
|
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
|
|
70
|
-
fi
|
|
71
|
-
|
|
72
|
-
echo ""
|
|
73
|
-
echo "✓ greptile-cli installed successfully."
|
|
74
|
-
echo ""
|
|
75
|
-
echo "Repo path mappings are stored in ~/.greptile/repos.json."
|
|
76
|
-
echo "The first time you click 'Fix in Claude Code' for a repo,"
|
|
77
|
-
echo "you'll be asked to select the local folder for that repo."
|
package/preuninstall.sh
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
# preuninstall.sh — Removes the Greptile Fix .app bundle on npm uninstall.
|
|
4
|
-
|
|
5
|
-
# Only run on macOS
|
|
6
|
-
if [ "$(uname)" != "Darwin" ]; then
|
|
7
|
-
exit 0
|
|
8
|
-
fi
|
|
9
|
-
|
|
10
|
-
APP_PATH="$HOME/Applications/Greptile Fix.app"
|
|
11
|
-
|
|
12
|
-
if [ -d "$APP_PATH" ]; then
|
|
13
|
-
rm -rf "$APP_PATH"
|
|
14
|
-
fi
|
|
15
|
-
|
|
16
|
-
# --- Health server LaunchAgent ---
|
|
17
|
-
PLIST_PATH="$HOME/Library/LaunchAgents/com.greptile.health.plist"
|
|
18
|
-
if [ -f "$PLIST_PATH" ]; then
|
|
19
|
-
launchctl unload "$PLIST_PATH" 2>/dev/null || true
|
|
20
|
-
rm -f "$PLIST_PATH"
|
|
21
|
-
fi
|
|
22
|
-
|
|
23
|
-
echo "✓ greptile-cli uninstalled successfully."
|