opengrok-mcp-server 3.3.3

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.
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env bash
2
+ # opengrok-mcp-wrapper.sh
3
+ #
4
+ # Credential wrapper for the OpenGrok MCP server.
5
+ # Resolves credentials from OS keychain or encrypted file, then exec's the server.
6
+ #
7
+ # Usage:
8
+ # opengrok-mcp-wrapper.sh --setup # Interactive one-time credential setup
9
+ # opengrok-mcp-wrapper.sh # Silent mode — spawned by MCP clients
10
+ #
11
+ # Environment overrides:
12
+ # OPENGROK_BIN Path to opengrok-mcp binary (default: same dir as this script)
13
+ # OPENGROK_CONFIG_DIR Config directory (default: ~/.config/opengrok-mcp)
14
+
15
+ set -euo pipefail
16
+
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ OPENGROK_BIN="${OPENGROK_BIN:-$SCRIPT_DIR/opengrok-mcp}"
19
+ CONFIG_DIR="${OPENGROK_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opengrok-mcp}"
20
+ CONFIG_FILE="$CONFIG_DIR/config"
21
+ ENC_FILE="$CONFIG_DIR/credentials.enc"
22
+ DOTENV_FILE="$CONFIG_DIR/.env"
23
+ SALT_FILE="$CONFIG_DIR/.salt"
24
+
25
+ # ── Encryption helpers ────────────────────────────────────────────────────────
26
+
27
+ # Ensure a random salt file exists (created once, reused for key derivation)
28
+ _ensure_salt() {
29
+ if [[ ! -f "$SALT_FILE" ]] || [[ ! -s "$SALT_FILE" ]]; then
30
+ mkdir -p "$CONFIG_DIR"
31
+ dd if=/dev/urandom bs=16 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' > "$SALT_FILE"
32
+ chmod 600 "$SALT_FILE"
33
+ if [[ ! -s "$SALT_FILE" ]]; then
34
+ echo "opengrok-mcp: ⚠ Failed to generate cryptographic salt — encryption will use weaker key material." >&2
35
+ rm -f "$SALT_FILE"
36
+ return 1
37
+ fi
38
+ fi
39
+ }
40
+
41
+ # Derive a passphrase from machine-id + username + random salt (unique per install)
42
+ _key_material() {
43
+ local machine_id salt
44
+ machine_id="$(cat /etc/machine-id 2>/dev/null || hostname 2>/dev/null || echo 'default')"
45
+ salt="$(cat "$SALT_FILE" 2>/dev/null || echo '')"
46
+ printf '%s%s%s' "$machine_id" "$USER" "$salt"
47
+ }
48
+
49
+ encrypt_credentials() {
50
+ local password="$1"
51
+ local out_file="$2"
52
+ local kmat
53
+ _ensure_salt
54
+ kmat="$(_key_material)"
55
+ printf '%s' "$password" \
56
+ | openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -pass "pass:${kmat}" -out "$out_file" 2>/dev/null \
57
+ || { echo "opengrok-mcp: openssl encryption failed." >&2; return 1; }
58
+ chmod 600 "$out_file"
59
+ }
60
+
61
+ decrypt_credentials() {
62
+ local in_file="$1"
63
+ local kmat
64
+ kmat="$(_key_material)"
65
+ openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -d \
66
+ -pass "pass:${kmat}" -in "$in_file" 2>/dev/null
67
+ }
68
+
69
+ # Legacy key material (without salt) for migration from pre-salt encrypted files
70
+ _key_material_legacy() {
71
+ local machine_id
72
+ machine_id="$(cat /etc/machine-id 2>/dev/null || hostname 2>/dev/null || echo 'default')"
73
+ printf '%s%s' "$machine_id" "$USER"
74
+ }
75
+
76
+ # Migrate encrypted credentials from legacy (no salt) to salted key derivation
77
+ _migrate_credentials() {
78
+ if [[ -f "$ENC_FILE" ]] && [[ ! -f "$SALT_FILE" ]] && command -v openssl >/dev/null 2>&1; then
79
+ local legacy_kmat pw
80
+ legacy_kmat="$(_key_material_legacy)"
81
+ pw="$(openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -d \
82
+ -pass "pass:${legacy_kmat}" -in "$ENC_FILE" 2>/dev/null)" || return 0
83
+ if [[ -n "$pw" ]]; then
84
+ # Re-encrypt with salted key (encrypt_credentials calls _ensure_salt)
85
+ encrypt_credentials "$pw" "$ENC_FILE" || true
86
+ fi
87
+ fi
88
+ }
89
+
90
+ # ── Keychain helpers ──────────────────────────────────────────────────────────
91
+
92
+ _macos_keychain_store() {
93
+ local username="$1" password="$2"
94
+ # Delete existing entry first (ignore failure if not found)
95
+ security delete-generic-password -s opengrok-mcp -a "$username" 2>/dev/null || true
96
+ security add-generic-password -s opengrok-mcp -a "$username" -w "$password" 2>/dev/null
97
+ }
98
+
99
+ _macos_keychain_read() {
100
+ local username="$1"
101
+ security find-generic-password -s opengrok-mcp -a "$username" -w 2>/dev/null
102
+ }
103
+
104
+ _secrettool_available() {
105
+ # Return 0 only if secret-tool is installed AND D-Bus/keyring is reachable
106
+ command -v secret-tool >/dev/null 2>&1 || return 1
107
+ # Probe: a failed lookup returns exit 1; a D-Bus error returns exit >1
108
+ secret-tool lookup service opengrok-mcp-probe username probe 2>/dev/null
109
+ local rc=$?
110
+ [[ $rc -eq 0 || $rc -eq 1 ]] # 0=found, 1=not found — both mean D-Bus works
111
+ }
112
+
113
+ _secrettool_store() {
114
+ local username="$1" password="$2"
115
+ printf '%s' "$password" \
116
+ | secret-tool store --label="OpenGrok MCP credentials" \
117
+ service opengrok-mcp username "$username" 2>/dev/null
118
+ }
119
+
120
+ _secrettool_read() {
121
+ local username="$1"
122
+ secret-tool lookup service opengrok-mcp username "$username" 2>/dev/null
123
+ }
124
+
125
+ # ── Safe .env / config file parser ───────────────────────────────────────────
126
+ # Does NOT use `source` — prevents arbitrary code execution.
127
+ _load_file_vars() {
128
+ local file="$1"
129
+ [[ -f "$file" ]] || return 0
130
+ while IFS='=' read -r key value; do
131
+ # Trim whitespace from key
132
+ key="${key#"${key%%[! ]*}"}"
133
+ key="${key%"${key##*[! ]}"}"
134
+ # Skip comments and blank lines
135
+ [[ -z "$key" || "$key" == \#* ]] && continue
136
+ # Only export safe variable names (alphanumeric + underscore, starts with letter)
137
+ if [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
138
+ export "$key"="$value"
139
+ fi
140
+ done < "$file"
141
+ }
142
+
143
+ # ═════════════════════════════════════════════════════════════════════════════
144
+ # --setup mode: interactive credential setup
145
+ # ═════════════════════════════════════════════════════════════════════════════
146
+
147
+ _run_setup() {
148
+ echo ""
149
+ echo "╔════════════════════════════════════════════╗"
150
+ echo "║ OpenGrok MCP — Credential Setup ║"
151
+ echo "╚════════════════════════════════════════════╝"
152
+ echo ""
153
+
154
+ # Prompt for configuration
155
+ local default_url="https://opengrok.example.com/source/"
156
+ read -r -p "OpenGrok Base URL [${default_url}]: " input_url
157
+ local base_url="${input_url:-$default_url}"
158
+
159
+ read -r -p "Username [${USER}]: " input_user
160
+ local username="${input_user:-$USER}"
161
+
162
+ local password=""
163
+ while [[ -z "$password" ]]; do
164
+ read -r -s -p "Password: " password
165
+ echo ""
166
+ if [[ -z "$password" ]]; then
167
+ echo "Password cannot be empty. Please try again."
168
+ fi
169
+ done
170
+
171
+ read -r -p "Verify SSL certificates? [Y/n]: " input_ssl
172
+ local verify_ssl="true"
173
+ if [[ "${input_ssl,,}" == "n" || "${input_ssl,,}" == "no" ]]; then
174
+ verify_ssl="false"
175
+ fi
176
+
177
+ echo ""
178
+ echo "Testing connection..."
179
+
180
+ # Quick connectivity test using curl (avoid depending on the server itself)
181
+ local test_url="${base_url%/}/api/v1/projects"
182
+ local curl_args=(-fsSL --max-time 10 -u "${username}:${password}" -o /dev/null -w "%{http_code}")
183
+ if [[ "$verify_ssl" == "false" ]]; then
184
+ curl_args+=(-k)
185
+ fi
186
+ local http_code
187
+ http_code="$(curl "${curl_args[@]}" "$test_url" 2>/dev/null)" || http_code="000"
188
+
189
+ case "$http_code" in
190
+ 200|201) echo " ✓ Connection successful (HTTP $http_code)" ;;
191
+ 401|403) echo " ✗ Authentication failed (HTTP $http_code). Check username and password." ; return 1 ;;
192
+ 000) echo " ✗ Could not reach $test_url. Check the URL and your network." ; return 1 ;;
193
+ *) echo " ! Unexpected response (HTTP $http_code). Proceeding anyway." ;;
194
+ esac
195
+
196
+ # ── Store credentials in the most secure available store ──────────────────
197
+ echo ""
198
+ echo "Storing credentials..."
199
+
200
+ local stored=false
201
+
202
+ case "$(uname -s)" in
203
+ Darwin)
204
+ if _macos_keychain_store "$username" "$password"; then
205
+ echo " ✓ Stored in macOS Keychain"
206
+ stored=true
207
+ else
208
+ echo " ! Keychain write failed, falling back to encrypted file."
209
+ fi
210
+ ;;
211
+ Linux)
212
+ if _secrettool_available; then
213
+ if _secrettool_store "$username" "$password"; then
214
+ echo " ✓ Stored in GNOME Keyring / KDE Wallet via secret-tool"
215
+ stored=true
216
+ else
217
+ echo " ! secret-tool write failed, falling back to encrypted file."
218
+ fi
219
+ else
220
+ echo " ℹ No desktop keychain available (headless/SSH session)."
221
+ fi
222
+ ;;
223
+ esac
224
+
225
+ if [[ "$stored" == "false" ]]; then
226
+ if command -v openssl >/dev/null 2>&1; then
227
+ mkdir -p "$CONFIG_DIR"
228
+ if encrypt_credentials "$password" "$ENC_FILE"; then
229
+ echo " ✓ Stored in encrypted file: $ENC_FILE"
230
+ echo " (AES-256-CBC, key derived from machine-id + username)"
231
+ stored=true
232
+ else
233
+ echo " ! Encryption failed."
234
+ fi
235
+ fi
236
+ fi
237
+
238
+ if [[ "$stored" == "false" ]]; then
239
+ # Last resort: plaintext .env (warn user)
240
+ mkdir -p "$CONFIG_DIR"
241
+ printf '# Created: %s\nOPENGROK_PASSWORD=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$password" > "$DOTENV_FILE"
242
+ chmod 600 "$DOTENV_FILE"
243
+ echo " ⚠ Fallback: password saved to $DOTENV_FILE (plaintext)"
244
+ echo " Consider installing openssl or configuring a keychain for better security."
245
+ fi
246
+
247
+ # ── Store non-secret config (URL, username, SSL) ──────────────────────────
248
+ mkdir -p "$CONFIG_DIR"
249
+ cat > "$CONFIG_FILE" <<EOF
250
+ # OpenGrok MCP non-secret configuration
251
+ # Generated by: opengrok-mcp-wrapper.sh --setup
252
+ OPENGROK_BASE_URL=${base_url}
253
+ OPENGROK_USERNAME=${username}
254
+ OPENGROK_VERIFY_SSL=${verify_ssl}
255
+ EOF
256
+ chmod 644 "$CONFIG_FILE"
257
+ echo " ✓ Saved config: $CONFIG_FILE"
258
+
259
+ echo ""
260
+ echo "╔═══════════════════════════════════════════════════════╗"
261
+ echo "║ Setup complete! Add opengrok to your MCP client. ║"
262
+ echo "╚═══════════════════════════════════════════════════════╝"
263
+ echo ""
264
+ echo "Example client config (Claude Code, Cursor, Windsurf):"
265
+ echo ""
266
+ echo ' { "mcpServers": { "opengrok": { "command": "'"${BASH_SOURCE[0]}"'" } } }'
267
+ echo ""
268
+ echo " For OpenCode (opencode.ai):"
269
+ echo ' { "mcp": { "opengrok": { "type": "local", "command": ["'"${BASH_SOURCE[0]}"'"] } } }'
270
+ echo ""
271
+ echo "See MCP_CLIENTS.md for all client configurations."
272
+ echo ""
273
+ }
274
+
275
+ # ═════════════════════════════════════════════════════════════════════════════
276
+ # Server mode: silent credential resolution, then exec the server
277
+ # ═════════════════════════════════════════════════════════════════════════════
278
+
279
+ _run_server() {
280
+ # Verify the binary exists
281
+ if [[ ! -x "$OPENGROK_BIN" ]]; then
282
+ echo "opengrok-mcp: binary not found or not executable: $OPENGROK_BIN" >&2
283
+ echo "Set OPENGROK_BIN or ensure opengrok-mcp is in the same directory." >&2
284
+ exit 1
285
+ fi
286
+
287
+ # Load non-secret config (URL, username, SSL verify)
288
+ _load_file_vars "$CONFIG_FILE"
289
+
290
+ # ── Credential resolution chain ────────────────────────────────────────────
291
+
292
+ # 1. Already in environment → use it (backward compat / CI override)
293
+ if [[ -n "${OPENGROK_PASSWORD:-}" ]]; then
294
+ exec "$OPENGROK_BIN" "$@"
295
+ fi
296
+
297
+ # 2. OS Keychain
298
+ local pw=""
299
+ case "$(uname -s)" in
300
+ Darwin)
301
+ pw="$(_macos_keychain_read "${OPENGROK_USERNAME:-$USER}")" || pw=""
302
+ if [[ -n "$pw" ]]; then
303
+ export OPENGROK_PASSWORD="$pw"
304
+ exec "$OPENGROK_BIN" "$@"
305
+ fi
306
+ ;;
307
+ Linux)
308
+ if _secrettool_available 2>/dev/null; then
309
+ pw="$(_secrettool_read "${OPENGROK_USERNAME:-$USER}")" || pw=""
310
+ if [[ -n "$pw" ]]; then
311
+ export OPENGROK_PASSWORD="$pw"
312
+ exec "$OPENGROK_BIN" "$@"
313
+ fi
314
+ fi
315
+ ;;
316
+ esac
317
+
318
+ # 3. Encrypted credential file (migrate legacy unsalted credentials if needed)
319
+ _migrate_credentials
320
+ if [[ -f "$ENC_FILE" ]]; then
321
+ if command -v openssl >/dev/null 2>&1; then
322
+ pw="$(decrypt_credentials "$ENC_FILE")" || pw=""
323
+ if [[ -n "$pw" ]]; then
324
+ export OPENGROK_PASSWORD="$pw"
325
+ exec "$OPENGROK_BIN" "$@"
326
+ fi
327
+ fi
328
+ fi
329
+
330
+ # 4. Plaintext .env fallback
331
+ if [[ -f "$DOTENV_FILE" ]]; then
332
+ # Warn if .env file is older than 30 days
333
+ local file_age_days=0
334
+ if command -v stat >/dev/null 2>&1; then
335
+ local mod_epoch
336
+ mod_epoch="$(stat -c %Y "$DOTENV_FILE" 2>/dev/null || stat -f %m "$DOTENV_FILE" 2>/dev/null || echo 0)"
337
+ if [[ "$mod_epoch" -gt 0 ]]; then
338
+ file_age_days=$(( ($(date +%s) - mod_epoch) / 86400 ))
339
+ fi
340
+ fi
341
+ if [[ "$file_age_days" -ge 30 ]]; then
342
+ echo "opengrok-mcp: ⚠ Plaintext .env file is ${file_age_days} days old. Consider running --setup with openssl installed." >&2
343
+ fi
344
+ _load_file_vars "$DOTENV_FILE"
345
+ if [[ -n "${OPENGROK_PASSWORD:-}" ]]; then
346
+ exec "$OPENGROK_BIN" "$@"
347
+ fi
348
+ fi
349
+
350
+ # 5. Nothing found — fail with clear message
351
+ echo "opengrok-mcp: No credentials found." >&2
352
+ echo "Run once in your terminal to set up credentials:" >&2
353
+ echo " ${BASH_SOURCE[0]} --setup" >&2
354
+ exit 1
355
+ }
356
+
357
+ # ═════════════════════════════════════════════════════════════════════════════
358
+ # Entry point
359
+ # ═════════════════════════════════════════════════════════════════════════════
360
+
361
+ case "${1:-}" in
362
+ --setup|-s)
363
+ _run_setup
364
+ ;;
365
+ --version|-v)
366
+ exec "$OPENGROK_BIN" --version
367
+ ;;
368
+ --help|-h)
369
+ echo "Usage: $(basename "${BASH_SOURCE[0]}") [--setup | --version | --help]"
370
+ echo ""
371
+ echo " --setup Interactive credential setup (run once in your terminal)"
372
+ echo " --version Print server version and exit"
373
+ echo " --help Show this help"
374
+ echo " (no args) Resolve credentials and start the MCP server (spawned by client)"
375
+ ;;
376
+ *)
377
+ _run_server "$@"
378
+ ;;
379
+ esac
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Packages the standalone MCP server binary into platform-specific archives
4
+ * for distribution via GitHub Releases.
5
+ *
6
+ * Usage: node scripts/package-server.js
7
+ *
8
+ * Outputs (in project root):
9
+ * opengrok-mcp-{version}-linux.tar.gz
10
+ * opengrok-mcp-{version}-darwin.tar.gz
11
+ * opengrok-mcp-{version}-win.zip
12
+ *
13
+ * Each archive contains:
14
+ * opengrok-mcp <- the binary
15
+ * opengrok-mcp-wrapper.sh <- Linux/macOS credential wrapper
16
+ * opengrok-mcp-wrapper.cmd <- Windows .cmd launcher
17
+ * opengrok-mcp-wrapper.ps1 <- Windows PowerShell credential wrapper
18
+ * MCP_CLIENTS.md <- setup and client config guide
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { execSync } = require('child_process');
24
+ const { version } = require('../package.json');
25
+
26
+ const ROOT = path.resolve(__dirname, '..');
27
+ const OUT_DIR = path.join(ROOT, 'out', 'server');
28
+ const SCRIPTS_DIR = path.join(ROOT, 'scripts');
29
+
30
+ const SERVER_BIN = path.join(OUT_DIR, 'main.js');
31
+ const WRAPPER_SH = path.join(SCRIPTS_DIR, 'opengrok-mcp-wrapper.sh');
32
+ const WRAPPER_CMD = path.join(SCRIPTS_DIR, 'opengrok-mcp-wrapper.cmd');
33
+ const WRAPPER_PS1 = path.join(SCRIPTS_DIR, 'opengrok-mcp-wrapper.ps1');
34
+ const MCP_CLIENTS_MD = path.join(ROOT, 'MCP_CLIENTS.md');
35
+
36
+ // Verify required files exist
37
+ for (const [label, file] of [
38
+ ['server binary', SERVER_BIN],
39
+ ['wrapper .sh', WRAPPER_SH],
40
+ ['wrapper .cmd', WRAPPER_CMD],
41
+ ['wrapper .ps1', WRAPPER_PS1],
42
+ ['MCP_CLIENTS.md', MCP_CLIENTS_MD],
43
+ ]) {
44
+ if (!fs.existsSync(file)) {
45
+ console.error(`Missing required file [${label}]: ${file}`);
46
+ console.error('Run npm run compile before packaging.');
47
+ process.exit(1);
48
+ }
49
+ }
50
+
51
+ // Create a staging directory
52
+ const STAGE = path.join(ROOT, 'out', '_package_stage');
53
+ if (fs.existsSync(STAGE)) fs.rmSync(STAGE, { recursive: true });
54
+ fs.mkdirSync(STAGE, { recursive: true });
55
+
56
+ // Copy files into staging dir
57
+ fs.copyFileSync(SERVER_BIN, path.join(STAGE, 'opengrok-mcp'));
58
+ fs.copyFileSync(WRAPPER_SH, path.join(STAGE, 'opengrok-mcp-wrapper.sh'));
59
+ fs.copyFileSync(WRAPPER_CMD, path.join(STAGE, 'opengrok-mcp-wrapper.cmd'));
60
+ fs.copyFileSync(WRAPPER_PS1, path.join(STAGE, 'opengrok-mcp-wrapper.ps1'));
61
+ fs.copyFileSync(MCP_CLIENTS_MD, path.join(STAGE, 'MCP_CLIENTS.md'));
62
+
63
+ // Set executable bit on Unix files (no-op on Windows, harmless)
64
+ try {
65
+ fs.chmodSync(path.join(STAGE, 'opengrok-mcp'), 0o755);
66
+ fs.chmodSync(path.join(STAGE, 'opengrok-mcp-wrapper.sh'), 0o755);
67
+ } catch {
68
+ // On Windows chmod may fail — acceptable
69
+ }
70
+
71
+ // Build archives
72
+ const archives = [
73
+ {
74
+ name: `opengrok-mcp-server-${version}-linux.tar.gz`,
75
+ cmd: `tar -czf "${ROOT}/opengrok-mcp-server-${version}-linux.tar.gz" -C "${STAGE}" .`,
76
+ },
77
+ {
78
+ name: `opengrok-mcp-server-${version}-darwin.tar.gz`,
79
+ cmd: `tar -czf "${ROOT}/opengrok-mcp-server-${version}-darwin.tar.gz" -C "${STAGE}" .`,
80
+ },
81
+ {
82
+ name: `opengrok-mcp-server-${version}-win.zip`,
83
+ build: buildWindowsZip,
84
+ },
85
+ ];
86
+
87
+ for (const archive of archives) {
88
+ const outPath = path.join(ROOT, archive.name);
89
+ console.log(`Building ${archive.name}...`);
90
+ try {
91
+ if (archive.cmd) {
92
+ execSync(archive.cmd, { stdio: 'inherit' });
93
+ } else if (archive.build) {
94
+ archive.build(outPath);
95
+ }
96
+ const stat = fs.statSync(outPath);
97
+ console.log(` → ${outPath} (${(stat.size / 1024).toFixed(1)} KB)`);
98
+ } catch (err) {
99
+ console.error(` ✗ Failed to build ${archive.name}:`, err.message);
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ // Cleanup staging dir
105
+ fs.rmSync(STAGE, { recursive: true });
106
+
107
+ console.log('\nPackaging complete. Archives ready for release:');
108
+ for (const archive of archives) {
109
+ console.log(` ${archive.name}`);
110
+ }
111
+
112
+ /**
113
+ * Build a .zip archive for Windows using Node's built-in zlib (no external deps).
114
+ * Falls back to PowerShell's Compress-Archive on Windows if available.
115
+ */
116
+ function buildWindowsZip(outPath) {
117
+ // Try PowerShell first (available on Windows CI runners)
118
+ try {
119
+ execSync(
120
+ `powershell -NoProfile -Command "Compress-Archive -Path '${STAGE}\\*' -DestinationPath '${outPath}' -Force"`,
121
+ { stdio: 'inherit' }
122
+ );
123
+ return;
124
+ } catch {
125
+ // Not on Windows — fall through to zip CLI
126
+ }
127
+
128
+ // Try zip CLI (available on Linux/macOS CI)
129
+ try {
130
+ execSync(`cd "${STAGE}" && zip -r "${outPath}" .`, { stdio: 'inherit' });
131
+ return;
132
+ } catch {
133
+ // zip not available
134
+ }
135
+
136
+ // Minimal fallback: create a tar.gz with .zip extension hint in the name
137
+ // (not ideal but won't break the release pipeline)
138
+ console.warn(' Warning: zip not available. Creating tar.gz for Windows archive.');
139
+ execSync(`tar -czf "${outPath}" -C "${STAGE}" .`, { stdio: 'inherit' });
140
+ }
@@ -0,0 +1,137 @@
1
+ # OpenGrok MCP Release Script
2
+ # Usage: .\scripts\release.ps1 -Version [patch|minor|major] [-Dry]
3
+
4
+ param(
5
+ [Parameter(Mandatory=$true)]
6
+ [ValidateSet('patch', 'minor', 'major')]
7
+ [string]$Version,
8
+
9
+ [switch]$Dry = $false
10
+ )
11
+
12
+ $ErrorActionPreference = "Stop"
13
+
14
+ # Colors for output
15
+ function Write-Step([string]$Message) {
16
+ Write-Host "===> $Message" -ForegroundColor Cyan
17
+ }
18
+
19
+ function Write-Success([string]$Message) {
20
+ Write-Host "Success: $Message" -ForegroundColor Green
21
+ }
22
+
23
+ function Write-Fail([string]$Message) {
24
+ Write-Host "Error: $Message" -ForegroundColor Red
25
+ }
26
+
27
+ # Check if working directory is clean
28
+ Write-Step "Checking Git status..."
29
+ $gitStatus = git status --porcelain
30
+ if ($gitStatus -and !$Dry) {
31
+ Write-Fail "Working directory has uncommitted changes. Please commit or stash them first."
32
+ exit 1
33
+ }
34
+ Write-Success "Working directory is clean"
35
+
36
+ # Get current version
37
+ Write-Step "Reading current version..."
38
+ $packageJson = Get-Content "package.json" -Raw | ConvertFrom-Json
39
+ $currentVersion = $packageJson.version
40
+ Write-Host "Current version: $currentVersion" -ForegroundColor Yellow
41
+
42
+ # Bump version
43
+ Write-Step "Bumping $Version version..."
44
+ if ($Dry) {
45
+ Write-Host "[DRY RUN] Would run: npm version $Version --no-git-tag-version" -ForegroundColor Yellow
46
+ } else {
47
+ npm version $Version --no-git-tag-version
48
+ }
49
+
50
+ # Get new version
51
+ $packageJson = Get-Content "package.json" -Raw | ConvertFrom-Json
52
+ $newVersion = $packageJson.version
53
+ Write-Success "New version: $newVersion"
54
+
55
+ # Run tests
56
+ Write-Step "Running tests..."
57
+ if ($Dry) {
58
+ Write-Host "[DRY RUN] Would run: npm test" -ForegroundColor Yellow
59
+ } else {
60
+ npm test
61
+ if ($LASTEXITCODE -ne 0) {
62
+ Write-Fail "Tests failed. Aborting release."
63
+ exit 1
64
+ }
65
+ Write-Success "Tests passed"
66
+ }
67
+
68
+ # Build extension
69
+ Write-Step "Building extension..."
70
+ if ($Dry) {
71
+ Write-Host "[DRY RUN] Would run: npm run compile" -ForegroundColor Yellow
72
+ } else {
73
+ npm run compile
74
+ Write-Success "Extension compiled"
75
+ }
76
+
77
+ # Package standalone server archives
78
+ Write-Step "Packaging standalone server archives..."
79
+ if ($Dry) {
80
+ Write-Host "[DRY RUN] Would run: npm run package-server" -ForegroundColor Yellow
81
+ } else {
82
+ npm run package-server
83
+ Write-Success "Standalone archives created"
84
+ }
85
+
86
+ # Package VSIX
87
+ Write-Step "Packaging VSIX..."
88
+ if ($Dry) {
89
+ Write-Host "[DRY RUN] Would run: npm run vsix" -ForegroundColor Yellow
90
+ } else {
91
+ npm run vsix
92
+ Write-Success "VSIX packaged: opengrok-mcp-$newVersion.vsix"
93
+ }
94
+
95
+ # Git commit and tag
96
+ Write-Step "Creating Git commit and tag..."
97
+ if ($Dry) {
98
+ Write-Host "[DRY RUN] Would run:" -ForegroundColor Yellow
99
+ Write-Host " git add package.json CHANGELOG.md" -ForegroundColor Yellow
100
+ Write-Host " git commit -m 'chore: release v$newVersion'" -ForegroundColor Yellow
101
+ Write-Host " git tag v$newVersion" -ForegroundColor Yellow
102
+ } else {
103
+ git add package.json CHANGELOG.md
104
+ git commit -m "chore: release v$newVersion"
105
+ git tag "v$newVersion"
106
+ Write-Success "Created commit and tag v$newVersion"
107
+ }
108
+
109
+ # Summary
110
+ Write-Host ""
111
+ Write-Host "========================================" -ForegroundColor Green
112
+ Write-Host "Release Summary" -ForegroundColor Green
113
+ Write-Host "========================================" -ForegroundColor Green
114
+ Write-Host "Version: $currentVersion -> $newVersion"
115
+ Write-Host "Tag: v$newVersion"
116
+ Write-Host "VSIX: opengrok-mcp-$newVersion.vsix"
117
+ Write-Host "Archives: opengrok-mcp-$newVersion-linux.tar.gz, -darwin.tar.gz, -win.zip"
118
+ Write-Host ""
119
+
120
+ if ($Dry) {
121
+ Write-Host "[DRY RUN] No changes were made" -ForegroundColor Yellow
122
+ } else {
123
+ Write-Host "Next steps:" -ForegroundColor Cyan
124
+ Write-Host "1. Review the changes: git log -1"
125
+ Write-Host "2. Push to GitHub:"
126
+ Write-Host " git push origin $((git branch --show-current))"
127
+ Write-Host " git push origin v$newVersion"
128
+ Write-Host ""
129
+ Write-Host "3. GitHub Actions will automatically:" -ForegroundColor Yellow
130
+ Write-Host " - Run tests on the tag"
131
+ Write-Host " - Build the VSIX + standalone server archives"
132
+ Write-Host " - Create a GitHub Release"
133
+ Write-Host " - Attach the VSIX and platform archives as downloadable artifacts"
134
+ Write-Host ""
135
+ Write-Host "4. Check the release at:" -ForegroundColor Cyan
136
+ Write-Host " https://github.com/IcyHot09/opengrok-mcp-server/releases"
137
+ }