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 +27 -2
- package/com.greptile.health.plist +2 -2
- package/greptile-fix +20 -4
- package/greptile-fix.applescript +10 -8
- package/health-server.js +90 -15
- package/package.json +4 -1
- package/postinstall.sh +29 -5
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
|
-
#
|
|
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
|
-
|
|
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
|
|
19
|
+
<string>__HOME__/.cache/greptile/greptile-health.log</string>
|
|
20
20
|
<key>StandardErrorPath</key>
|
|
21
|
-
<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
|
-
|
|
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
|
|
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 /
|
|
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 /
|
|
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)
|
package/greptile-fix.applescript
CHANGED
|
@@ -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>> /
|
|
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 & "' >> /
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 & "' >> /
|
|
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) {
|
|
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) {
|
|
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
|
|
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
|
-
|
|
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)) {
|
|
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: '
|
|
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', () => {
|
|
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
|
-
|
|
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.
|
|
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 "
|
|
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
|
-
|
|
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 ""
|