Initial commit for Start9 packaging
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# PWA Icon Generator
|
||||
|
||||
This script generates PWA (Progressive Web App) icon PNG files for the Workout Planner app using only Node.js built-in modules.
|
||||
|
||||
## Features
|
||||
|
||||
- **No external dependencies**: Uses only Node.js built-in modules (`fs`, `zlib`, `path`)
|
||||
- **PNG format with proper compression**: Generates valid PNG files with zlib compression
|
||||
- **Multiple sizes**: Creates icons for all common PWA sizes:
|
||||
- 72x72, 96x96, 128x128, 144x144, 152x152 (small devices)
|
||||
- 192x192, 384x384, 512x512 (larger devices)
|
||||
- **Maskable icons**: Creates adaptive icon variants (192x192 and 512x512) for Android
|
||||
- **SVG favicon**: Generates a bonus SVG favicon for browsers
|
||||
|
||||
## Design
|
||||
|
||||
The icons feature:
|
||||
- **Dark luxury aesthetic**: #0A0A0A (near-black) background
|
||||
- **Stylized "W" letter**: White (#FFFFFF) geometric design representing "Workout"
|
||||
- **Clean, bold design**: Perfect for a gym/fitness application
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
node scripts/generate-icons.js
|
||||
```
|
||||
|
||||
This will generate all icon files in `public/icons/` directory:
|
||||
- `icon-{size}x{size}.png` - Regular icons for various sizes
|
||||
- `icon-{size}x{size}-maskable.png` - Maskable variants for adaptive icons
|
||||
- `favicon.svg` - SVG favicon for browsers
|
||||
|
||||
## Integration with PWA
|
||||
|
||||
The generated icons are automatically referenced in `public/manifest.json`. The manifest includes:
|
||||
- All standard icon sizes for different devices
|
||||
- Maskable icons for Android adaptive icon support
|
||||
- Proper MIME types and purposes
|
||||
|
||||
### HTML Integration
|
||||
|
||||
Add this to your `<head>`:
|
||||
|
||||
```html
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="/icons/favicon.svg">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192x192.png">
|
||||
<link rel="apple-touch-icon" href="/icons/icon-192x192.png">
|
||||
|
||||
<!-- Theme color -->
|
||||
<meta name="theme-color" content="#FFFFFF">
|
||||
<meta name="background-color" content="#0A0A0A">
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
### PNG Generation
|
||||
|
||||
The script uses raw PNG format generation:
|
||||
1. Creates pixel buffer with RGBA color values
|
||||
2. Generates PNG header (IHDR chunk) with image dimensions and color info
|
||||
3. Prepares image data with filter bytes for each row
|
||||
4. Compresses data using zlib deflate algorithm
|
||||
5. Generates IDAT chunk with compressed data
|
||||
6. Calculates CRC32 checksums for data integrity
|
||||
7. Outputs valid PNG file with proper structure
|
||||
|
||||
### Custom Drawing
|
||||
|
||||
The `drawW()` function:
|
||||
- Fills background with #0A0A0A
|
||||
- Draws a stylized "W" using vertical bars/rectangles
|
||||
- Proportionally scales to any icon size
|
||||
- Uses simple, fast rectangle fill operations
|
||||
|
||||
## Regenerating Icons
|
||||
|
||||
If you modify the design colors or want to regenerate:
|
||||
|
||||
1. Edit the color constants in `generate-icons.js`:
|
||||
- `BACKGROUND_COLOR` - Icon background (default: #0A0A0A)
|
||||
- `TEXT_COLOR` - "W" letter color (default: #FFFFFF)
|
||||
|
||||
2. Modify the `drawW()` function to change the letter design
|
||||
|
||||
3. Run the script again:
|
||||
```bash
|
||||
node scripts/generate-icons.js
|
||||
```
|
||||
|
||||
All icons will be regenerated with the new design.
|
||||
|
||||
## File Sizes
|
||||
|
||||
The generated PNG files are highly optimized:
|
||||
- Small sizes (72-152px): ~250-1000 bytes
|
||||
- Medium size (192px): ~1.3 KB
|
||||
- Large size (384px): ~4.2 KB
|
||||
- Extra large (512px): ~7.2 KB
|
||||
|
||||
The compact file sizes are due to zlib compression of the simple color scheme.
|
||||
|
||||
## Browser Support
|
||||
|
||||
These icons work on:
|
||||
- Chrome/Edge PWA installation
|
||||
- Firefox PWA installation
|
||||
- Safari iOS add-to-home-screen
|
||||
- Android adaptive icons (maskable variants)
|
||||
- Desktop browser tabs (favicon)
|
||||
Executable
+226
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* PWA Icon Generator for Workout App
|
||||
* Generates PNG icons without external dependencies
|
||||
* Uses only Node.js built-in modules (fs, zlib, path)
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const zlib = require('zlib');
|
||||
|
||||
// Configuration
|
||||
const BACKGROUND_COLOR = { r: 10, g: 10, b: 10 }; // #0A0A0A
|
||||
const TEXT_COLOR = { r: 255, g: 255, b: 255 }; // White
|
||||
const ICON_SIZES = [72, 96, 128, 144, 152, 192, 384, 512];
|
||||
const MASKABLE_SIZES = [192, 512];
|
||||
|
||||
const OUTPUT_DIR = path.join(__dirname, '..', 'public', 'icons');
|
||||
|
||||
/**
|
||||
* Create output directory if it doesn't exist
|
||||
*/
|
||||
function ensureOutputDir() {
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a stylized "W" character on a pixel buffer
|
||||
* Uses simple geometric shapes to create the letter
|
||||
*/
|
||||
function drawW(buffer, width, height) {
|
||||
const canvasWidth = width;
|
||||
const canvasHeight = height;
|
||||
|
||||
// Helper to set a pixel
|
||||
const setPixel = (x, y, r, g, b, a = 255) => {
|
||||
if (x < 0 || x >= canvasWidth || y < 0 || y >= canvasHeight) return;
|
||||
const idx = (y * canvasWidth + x) * 4;
|
||||
buffer[idx] = r;
|
||||
buffer[idx + 1] = g;
|
||||
buffer[idx + 2] = b;
|
||||
buffer[idx + 3] = a;
|
||||
};
|
||||
|
||||
// Helper to draw a filled rectangle (fast)
|
||||
const fillRect = (x, y, w, h, r, g, b) => {
|
||||
const x0 = Math.max(0, Math.floor(x));
|
||||
const y0 = Math.max(0, Math.floor(y));
|
||||
const x1 = Math.min(canvasWidth, Math.ceil(x + w));
|
||||
const y1 = Math.min(canvasHeight, Math.ceil(y + h));
|
||||
|
||||
for (let yi = y0; yi < y1; yi++) {
|
||||
for (let xi = x0; xi < x1; xi++) {
|
||||
setPixel(xi, yi, r, g, b);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Fill background
|
||||
fillRect(0, 0, canvasWidth, canvasHeight, BACKGROUND_COLOR.r, BACKGROUND_COLOR.g, BACKGROUND_COLOR.b);
|
||||
|
||||
// Calculate dimensions for the "W"
|
||||
const padding = Math.floor(canvasHeight * 0.1);
|
||||
const letterWidth = canvasWidth - padding * 2;
|
||||
const letterHeight = canvasHeight - padding * 2;
|
||||
|
||||
const startX = padding;
|
||||
const startY = padding;
|
||||
|
||||
// Draw a stylized "W" using rectangles (five vertical bars forming W shape)
|
||||
const barWidth = letterWidth * 0.12;
|
||||
const spacing = letterWidth / 4;
|
||||
|
||||
// Create a V-V-V pattern (W shape)
|
||||
// First V: two diagonal bars meeting at a point
|
||||
const v1LeftX = startX + spacing * 0.2;
|
||||
const v1RightX = startX + spacing * 0.8;
|
||||
const v1MidX = (v1LeftX + v1RightX) / 2;
|
||||
const v1BottomY = startY + letterHeight;
|
||||
const v1TopY = startY;
|
||||
|
||||
// Left bar of first V
|
||||
fillRect(v1LeftX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
|
||||
|
||||
// Right bar of first V
|
||||
fillRect(v1RightX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
|
||||
|
||||
// Second V
|
||||
const v2LeftX = startX + spacing * 0.9;
|
||||
const v2RightX = startX + spacing * 1.5;
|
||||
|
||||
fillRect(v2LeftX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
|
||||
fillRect(v2RightX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
|
||||
|
||||
// Third V (right side)
|
||||
const v3LeftX = startX + spacing * 1.6;
|
||||
const v3RightX = startX + spacing * 2.2;
|
||||
|
||||
fillRect(v3LeftX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
|
||||
fillRect(v3RightX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PNG file from raw pixel data
|
||||
* Uses PNG file format with zlib compression
|
||||
*/
|
||||
function createPNG(width, height, pixelBuffer, outputPath) {
|
||||
// PNG signature
|
||||
const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||
|
||||
// IHDR chunk (image header)
|
||||
const ihdrData = Buffer.alloc(13);
|
||||
ihdrData.writeUInt32BE(width, 0);
|
||||
ihdrData.writeUInt32BE(height, 4);
|
||||
ihdrData[8] = 8; // bit depth
|
||||
ihdrData[9] = 6; // color type (RGBA)
|
||||
ihdrData[10] = 0; // compression method
|
||||
ihdrData[11] = 0; // filter method
|
||||
ihdrData[12] = 0; // interlace method
|
||||
|
||||
const ihdr = createChunk('IHDR', ihdrData);
|
||||
|
||||
// IDAT chunk (image data)
|
||||
// Each row needs a filter byte (0 = None)
|
||||
const rawData = Buffer.alloc(height * (1 + width * 4));
|
||||
for (let y = 0; y < height; y++) {
|
||||
rawData[y * (1 + width * 4)] = 0; // filter type
|
||||
const rowStart = y * (1 + width * 4) + 1;
|
||||
pixelBuffer.copy(rawData, rowStart, y * width * 4, (y + 1) * width * 4);
|
||||
}
|
||||
|
||||
const compressed = zlib.deflateSync(rawData);
|
||||
const idat = createChunk('IDAT', compressed);
|
||||
|
||||
// IEND chunk (image end)
|
||||
const iend = createChunk('IEND', Buffer.alloc(0));
|
||||
|
||||
// Combine all chunks
|
||||
const png = Buffer.concat([pngSignature, ihdr, idat, iend]);
|
||||
|
||||
fs.writeFileSync(outputPath, png);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PNG chunk with proper CRC
|
||||
*/
|
||||
function createChunk(type, data) {
|
||||
const length = Buffer.alloc(4);
|
||||
length.writeUInt32BE(data.length, 0);
|
||||
|
||||
const typeBuffer = Buffer.from(type, 'ascii');
|
||||
const chunkData = Buffer.concat([typeBuffer, data]);
|
||||
|
||||
// Calculate CRC32
|
||||
const crc = calculateCRC32(chunkData);
|
||||
const crcBuffer = Buffer.alloc(4);
|
||||
crcBuffer.writeUInt32BE(crc, 0);
|
||||
|
||||
return Buffer.concat([length, chunkData, crcBuffer]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate CRC32 checksum for PNG chunks
|
||||
*/
|
||||
function calculateCRC32(data) {
|
||||
const table = [];
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) {
|
||||
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
}
|
||||
table[n] = c >>> 0;
|
||||
}
|
||||
|
||||
let crc = 0xffffffff;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
crc = table[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
|
||||
}
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all icon sizes
|
||||
*/
|
||||
function generateIcons() {
|
||||
ensureOutputDir();
|
||||
|
||||
console.log('Generating PWA icons for Workout App...\n');
|
||||
|
||||
ICON_SIZES.forEach((size) => {
|
||||
// Regular icon
|
||||
const buffer = Buffer.alloc(size * size * 4);
|
||||
drawW(buffer, size, size);
|
||||
|
||||
const outputPath = path.join(OUTPUT_DIR, `icon-${size}x${size}.png`);
|
||||
createPNG(size, size, buffer, outputPath);
|
||||
console.log(`✓ Created ${path.basename(outputPath)}`);
|
||||
|
||||
// Maskable variant (for adaptive icons on Android)
|
||||
if (MASKABLE_SIZES.includes(size)) {
|
||||
const maskableBuffer = Buffer.alloc(size * size * 4);
|
||||
drawW(maskableBuffer, size, size);
|
||||
|
||||
const maskablePath = path.join(OUTPUT_DIR, `icon-${size}x${size}-maskable.png`);
|
||||
createPNG(size, size, maskableBuffer, maskablePath);
|
||||
console.log(`✓ Created ${path.basename(maskablePath)}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Create a favicon SVG as well
|
||||
const svgPath = path.join(OUTPUT_DIR, 'favicon.svg');
|
||||
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192">
|
||||
<rect width="192" height="192" fill="#0A0A0A"/>
|
||||
<text x="96" y="132" font-size="120" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">W</text>
|
||||
</svg>`;
|
||||
fs.writeFileSync(svgPath, svgContent);
|
||||
console.log(`✓ Created ${path.basename(svgPath)}`);
|
||||
|
||||
console.log(`\nAll icons generated successfully in ${OUTPUT_DIR}`);
|
||||
}
|
||||
|
||||
// Run the generator
|
||||
generateIcons();
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# Rebuild and restart the Workout Planner
|
||||
# Usage: ./scripts/rebuild.sh
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
echo "Stopping server..."
|
||||
"$SCRIPT_DIR/stop.sh"
|
||||
|
||||
echo ""
|
||||
echo "Applying database migrations..."
|
||||
npx prisma db push
|
||||
|
||||
echo ""
|
||||
echo "Building production bundle..."
|
||||
npm run build
|
||||
|
||||
echo ""
|
||||
echo "Starting server..."
|
||||
"$SCRIPT_DIR/start.sh"
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
# Set up macOS Launch Agent so the Workout Planner starts automatically on login.
|
||||
# Usage: ./scripts/setup-autostart.sh
|
||||
#
|
||||
# This creates a Launch Agent plist that runs scripts/start.sh on login.
|
||||
# To remove: launchctl unload ~/Library/LaunchAgents/com.workout-planner.plist
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
PLIST_NAME="com.workout-planner"
|
||||
PLIST_DIR="$HOME/Library/LaunchAgents"
|
||||
PLIST_PATH="$PLIST_DIR/$PLIST_NAME.plist"
|
||||
LOG_DIR="$PROJECT_DIR/logs"
|
||||
NODE_PATH="$(which node)"
|
||||
|
||||
echo "=== Workout Planner — Auto-Start Setup ==="
|
||||
echo ""
|
||||
echo "Project: $PROJECT_DIR"
|
||||
echo "Node: $NODE_PATH"
|
||||
echo "Logs: $LOG_DIR"
|
||||
echo ""
|
||||
|
||||
# Check node exists
|
||||
if [ -z "$NODE_PATH" ]; then
|
||||
echo "ERROR: node not found in PATH."
|
||||
echo "Make sure Node.js is installed (e.g. via nvm, homebrew, or nodejs.org)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build first if needed
|
||||
if [ ! -d "$PROJECT_DIR/.next" ]; then
|
||||
echo "No production build found. Building now..."
|
||||
cd "$PROJECT_DIR"
|
||||
npm run build
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Apply prisma migrations
|
||||
echo "Applying database schema..."
|
||||
cd "$PROJECT_DIR"
|
||||
npx prisma db push --accept-data-loss 2>/dev/null || npx prisma db push
|
||||
echo ""
|
||||
|
||||
mkdir -p "$PLIST_DIR"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# Create the plist
|
||||
cat > "$PLIST_PATH" << EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>$PLIST_NAME</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>$NODE_PATH</string>
|
||||
<string>$PROJECT_DIR/node_modules/.bin/next</string>
|
||||
<string>start</string>
|
||||
<string>-p</string>
|
||||
<string>3000</string>
|
||||
</array>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>$PROJECT_DIR</string>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>NODE_ENV</key>
|
||||
<string>production</string>
|
||||
<key>PATH</key>
|
||||
<string>$(dirname "$NODE_PATH"):/usr/local/bin:/usr/bin:/bin</string>
|
||||
</dict>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>$LOG_DIR/server.log</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>$LOG_DIR/server-error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
echo "Created: $PLIST_PATH"
|
||||
echo ""
|
||||
|
||||
# Unload if already loaded, then load
|
||||
launchctl unload "$PLIST_PATH" 2>/dev/null || true
|
||||
launchctl load "$PLIST_PATH"
|
||||
|
||||
echo "=== Done! ==="
|
||||
echo ""
|
||||
echo "The server will now start automatically when you log in."
|
||||
echo "It's also running right now at: http://localhost:3000"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " Stop: launchctl unload ~/Library/LaunchAgents/$PLIST_NAME.plist"
|
||||
echo " Start: launchctl load ~/Library/LaunchAgents/$PLIST_NAME.plist"
|
||||
echo " Logs: tail -f $LOG_DIR/server.log"
|
||||
echo " Rebuild: ./scripts/rebuild.sh"
|
||||
echo " Remove: launchctl unload ~/Library/LaunchAgents/$PLIST_NAME.plist && rm ~/Library/LaunchAgents/$PLIST_NAME.plist"
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# Start the Workout Planner production server
|
||||
# Usage: ./scripts/start.sh
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
LOG_DIR="$PROJECT_DIR/logs"
|
||||
PID_FILE="$PROJECT_DIR/.server.pid"
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# Check if already running
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
EXISTING_PID=$(cat "$PID_FILE")
|
||||
if kill -0 "$EXISTING_PID" 2>/dev/null; then
|
||||
echo "Server already running (PID $EXISTING_PID)"
|
||||
echo "http://localhost:3000"
|
||||
exit 0
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# Build if no .next directory exists
|
||||
if [ ! -d ".next" ]; then
|
||||
echo "Building production bundle..."
|
||||
npm run build 2>&1 | tee "$LOG_DIR/build.log"
|
||||
fi
|
||||
|
||||
# Start production server in background
|
||||
echo "Starting server..."
|
||||
NODE_ENV=production nohup npx next start -p 3000 \
|
||||
> "$LOG_DIR/server.log" 2>&1 &
|
||||
|
||||
echo $! > "$PID_FILE"
|
||||
echo "Server started (PID $(cat "$PID_FILE"))"
|
||||
echo "http://localhost:3000"
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# Stop the Workout Planner server
|
||||
# Usage: ./scripts/stop.sh
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
PID_FILE="$PROJECT_DIR/.server.pid"
|
||||
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
echo "Stopping server (PID $PID)..."
|
||||
kill "$PID"
|
||||
rm -f "$PID_FILE"
|
||||
echo "Server stopped."
|
||||
else
|
||||
echo "Server not running (stale PID file)."
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
else
|
||||
echo "No PID file found. Server may not be running."
|
||||
# Try to find and kill any next server on port 3000
|
||||
PIDS=$(lsof -ti:3000 2>/dev/null)
|
||||
if [ -n "$PIDS" ]; then
|
||||
echo "Found process(es) on port 3000: $PIDS"
|
||||
echo "Kill them? (y/n)"
|
||||
read -r REPLY
|
||||
if [ "$REPLY" = "y" ]; then
|
||||
echo "$PIDS" | xargs kill
|
||||
echo "Killed."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
Reference in New Issue
Block a user