#!/bin/bash
# =============================================================================
# Jobs Monitor — Builder & Installer
# Built by Arun Thomas · https://github.com/arunofhyd/JobsMonitor
#
# This fetches the latest source code from GitHub and compiles Jobs Monitor
# LOCALLY on your Mac into your Applications folder.
# 100% Native, 100% Private, zero telemetry.
# =============================================================================
APP_NAME="JobsMonitor"
PLIST_LABEL="com.apple.aoh.jobsmonitor"
MAIN_SWIFT_URL="https://raw.githubusercontent.com/arunofhyd/JobsMonitor/main/main.swift"
LOGO_PNG_URL="https://raw.githubusercontent.com/arunofhyd/JobsMonitor/main/logo-jobsmonitor.png"
APP_ICON_ICNS_URL="https://raw.githubusercontent.com/arunofhyd/JobsMonitor/main/AppIcon.icns"
APP_ICON_PNG_URL="https://raw.githubusercontent.com/arunofhyd/JobsMonitor/main/AppIcon.png"
# Terminal Colors & Formatting
BLUE='\033[0;34m'
GREEN='\033[0;32m'
RED='\033[0;31m'
WHITE='\033[1;37m'
GREY='\033[0;90m'
NC='\033[0m'
BOLD='\033[1m'
step() { printf ' %b➔%b %s\n' "${BLUE}" "${NC}" "$1"; }
ok() { printf ' %b✔%b %s\n' "${GREEN}" "${NC}" "$1"; }
fail() { printf ' %b✖%b %s\n' "${RED}" "${NC}" "$1"; }
line() { printf '%b\n' "${GREY}─────────────────────────────────────────────────────────────${NC}"; }
clear 2>/dev/null || true
printf "\n"
line
printf '%b\n' "${WHITE}${BOLD} Jobs Monitor Installer${NC}"
line
printf "\n"
# ---- Step 1: Pre-flight Checks --------------------------------------------
step "Checking system prerequisites…"
if ! command -v swiftc >/dev/null 2>&1; then
fail "Swift compiler (swiftc) not found."
printf '\n%b\n' " ${GREY}Triggering Xcode Command Line Tools installation…${NC}"
xcode-select --install >/dev/null 2>&1 || true
printf '%b\n\n' " ${GREY}Please complete the Apple installer prompt and re-run this command.${NC}"
exit 1
fi
ok "Prerequisites verified."
printf "\n"
# ---- Step 2: Workspace & Fetch Source --------------------------------------
step "Preparing temporary build workspace…"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
BUILD_DIR="$(mktemp -d)"
trap 'rm -rf "$BUILD_DIR"' EXIT
if [ -f "$SCRIPT_DIR/main.swift" ]; then
step "Using local source code from repository…"
cp "$SCRIPT_DIR/main.swift" "$BUILD_DIR/main.swift"
[ -f "$SCRIPT_DIR/logo-jobsmonitor.png" ] && cp "$SCRIPT_DIR/logo-jobsmonitor.png" "$BUILD_DIR/logo-jobsmonitor.png"
[ -f "$SCRIPT_DIR/AppIcon.icns" ] && cp "$SCRIPT_DIR/AppIcon.icns" "$BUILD_DIR/AppIcon.icns"
[ -f "$SCRIPT_DIR/AppIcon.png" ] && cp "$SCRIPT_DIR/AppIcon.png" "$BUILD_DIR/AppIcon.png"
else
step "Fetching latest source code from GitHub…"
if ! curl -sSL "$MAIN_SWIFT_URL" -o "$BUILD_DIR/main.swift"; then
fail "Could not fetch main.swift from GitHub. Please check your internet connection."
exit 1
fi
curl -sSL "$APP_ICON_ICNS_URL" -o "$BUILD_DIR/AppIcon.icns" 2>/dev/null || true
curl -sSL "$APP_ICON_PNG_URL" -o "$BUILD_DIR/AppIcon.png" 2>/dev/null || true
curl -sSL "$LOGO_PNG_URL" -o "$BUILD_DIR/logo-jobsmonitor.png" 2>/dev/null || true
curl -sSL "$VERSION_CHECK_URL" -o "$BUILD_DIR/version.json" 2>/dev/null || true
fi
# Dynamically extract version from version.json as single source of truth
APP_VERSION=""
if [ -f "$BUILD_DIR/version.json" ]; then
APP_VERSION=$(python3 -c "import json; print(json.load(open('$BUILD_DIR/version.json'))['version'])" 2>/dev/null || true)
elif [ -f "$SCRIPT_DIR/version.json" ]; then
APP_VERSION=$(python3 -c "import json; print(json.load(open('$SCRIPT_DIR/version.json'))['version'])" 2>/dev/null || true)
fi
if [ -z "$APP_VERSION" ]; then
APP_VERSION="$(grep -m1 'APP_VERSION' "$BUILD_DIR/main.swift" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true)"
fi
if [ -z "$APP_VERSION" ]; then
APP_VERSION="2.3.5"
fi
ok "Source code and assets prepared (v${APP_VERSION})."
printf "\n"
# ---- Step 3: Compiling the app -------------------------------------------
step "Compiling Jobs Monitor v${APP_VERSION}…"
COMPILE_ERR="$(mktemp)"
if ! swiftc -O "$BUILD_DIR/main.swift" -o "$BUILD_DIR/$APP_NAME" -framework AppKit -framework ServiceManagement -framework UserNotifications -framework SwiftUI -framework Foundation -framework WebKit -framework AVFoundation 2>"$COMPILE_ERR"; then
fail "Could not compile the application locally."
printf '%b\n' " ${GREY}Falling back to downloading pre-built release...${NC}"
rm -f "$COMPILE_ERR"
ZIP_URL="https://github.com/arunofhyd/JobsMonitor/releases/download/v${APP_VERSION}/JobsMonitor.zip"
if ! curl -fsSL "$ZIP_URL" -o "$BUILD_DIR/JobsMonitor.zip"; then
ZIP_URL="https://github.com/arunofhyd/JobsMonitor/releases/latest/download/JobsMonitor.zip"
curl -fsSL "$ZIP_URL" -o "$BUILD_DIR/JobsMonitor.zip" || { fail "Download failed."; exit 1; }
fi
rm -rf "$BUILD_DIR/$APP_NAME.app"
unzip -o -q "$BUILD_DIR/JobsMonitor.zip" -d "$BUILD_DIR"
ok "Pre-built app downloaded successfully."
PREBUILT_DOWNLOADED=true
else
rm -f "$COMPILE_ERR"
ok "App compiled successfully."
PREBUILT_DOWNLOADED=false
fi
printf "\n"
# ---- Step 4: Building app bundle & app icon -------------------------------
if [ "$1" = "--ci" ] || [ "$CI" = "true" ]; then
BUILD_DIR_LOCAL="$SCRIPT_DIR/Build"
mkdir -p "$BUILD_DIR_LOCAL"
TARGET_APP="$BUILD_DIR_LOCAL/$APP_NAME.app"
else
TARGET_APP="/Applications/$APP_NAME.app"
pkill -x "$APP_NAME" 2>/dev/null || true
fi
APP_BUNDLE="$BUILD_DIR/$APP_NAME.app"
if [ "$PREBUILT_DOWNLOADED" != "true" ]; then
step "Building app bundle & app icon…"
rm -rf "$APP_BUNDLE"
mkdir -p "$APP_BUNDLE/Contents/MacOS"
mkdir -p "$APP_BUNDLE/Contents/Resources"
cp "$BUILD_DIR/$APP_NAME" "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
chmod 755 "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
[ -f "$BUILD_DIR/AppIcon.icns" ] && cp "$BUILD_DIR/AppIcon.icns" "$APP_BUNDLE/Contents/Resources/AppIcon.icns"
[ -f "$BUILD_DIR/AppIcon.png" ] && cp "$BUILD_DIR/AppIcon.png" "$APP_BUNDLE/Contents/Resources/AppIcon.png"
[ -f "$BUILD_DIR/logo-jobsmonitor.png" ] && cp "$BUILD_DIR/logo-jobsmonitor.png" "$APP_BUNDLE/Contents/Resources/logo-jobsmonitor.png"
if [ ! -f "$APP_BUNDLE/Contents/Resources/AppIcon.png" ] && [ -f "$BUILD_DIR/logo-jobsmonitor.png" ]; then
cp "$BUILD_DIR/logo-jobsmonitor.png" "$APP_BUNDLE/Contents/Resources/AppIcon.png"
fi
cat << EOF > "$APP_BUNDLE/Contents/Info.plist"
CFBundleExecutable
$APP_NAME
CFBundleIdentifier
$PLIST_LABEL
CFBundleName
Jobs Monitor
CFBundleIconFile
AppIcon
CFBundlePackageType
APPL
CFBundleShortVersionString
$APP_VERSION
LSMinimumSystemVersion
12.0
LSUIElement
EOF
codesign --force --deep --options runtime --sign - --requirements '=designated => identifier "'"$PLIST_LABEL"'"' "$APP_BUNDLE" >/dev/null 2>&1 || true
xattr -dr com.apple.quarantine "$APP_BUNDLE" >/dev/null 2>&1 || true
ok "App bundle created."
printf "\n"
fi
if [ "$1" = "--ci" ] || [ "$CI" = "true" ]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
mkdir -p "$SCRIPT_DIR/Build"
cp -R "$APP_BUNDLE" "$SCRIPT_DIR/Build/"
ok "CI mode detected. App copied to Build/$APP_NAME.app"
exit 0
fi
# ---- Step 5: Install to /Applications & setup LaunchAgent ----------------
step "Installing Jobs Monitor to /Applications…"
pkill -x "$APP_NAME" >/dev/null 2>&1 || true
TARGET_APP="/Applications/$APP_NAME.app"
INSTALLED=false
if [ "$FORCE_MODAL" = "1" ] || [ "$1" = "--modal" ] || [ "$1" = "--gui" ] || [ "$2" = "--modal" ]; then
INSTALLED=false
elif [ -w "/Applications" ]; then
rm -rf "$TARGET_APP" 2>/dev/null || true
if cp -R "$APP_BUNDLE" "$TARGET_APP" 2>/dev/null; then
INSTALLED=true
fi
fi
setup_launch_agent() {
LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents"
mkdir -p "$LAUNCH_AGENTS_DIR"
PLIST_PATH="$LAUNCH_AGENTS_DIR/$PLIST_LABEL.plist"
cat << EOF > "$PLIST_PATH"
Label$PLIST_LABEL
ProgramArguments
$TARGET_APP/Contents/MacOS/$APP_NAME
RunAtLoad
KeepAlive
EOF
# Unload & clean up legacy LaunchAgent if present
launchctl unload "$LAUNCH_AGENTS_DIR/com.aoh.jobsmonitor.plist" 2>/dev/null || true
rm -f "$LAUNCH_AGENTS_DIR/com.aoh.jobsmonitor.plist" 2>/dev/null || true
defaults delete "com.aoh.jobsmonitor" has_launched_jobsmonitor 2>/dev/null || true
defaults delete "$PLIST_LABEL" has_launched_jobsmonitor 2>/dev/null || true
pkill -9 -x "$APP_NAME" 2>/dev/null || true
launchctl unload "$PLIST_PATH" 2>/dev/null || true
launchctl load "$PLIST_PATH" 2>/dev/null || true
touch "$TARGET_APP"
open "$TARGET_APP" --args --open-preferences
}
if [ "$INSTALLED" = "true" ]; then
xattr -dr com.apple.quarantine "$TARGET_APP" >/dev/null 2>&1 || true
setup_launch_agent
ok "Jobs Monitor installed & running."
printf "\n"
line
printf '%b\n' "${WHITE}${BOLD} ✓ Jobs Monitor is installed and running in your menu bar.${NC}"
line
printf "\n"
exit 0
fi
# Fallback: Open Drag Modal if elevated permission required
printf " ➔ Standard install required elevated privileges. Opening installer modal…\n"
cat > Installer.swift <<'INSTEOF'
import Cocoa
import QuartzCore
let appName = "Jobs Monitor"
let sourcePath = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : ""
func performInstallation(src: URL) -> Bool {
let dest = URL(fileURLWithPath: "/Applications").appendingPathComponent("Jobs Monitor.app")
do {
if FileManager.default.fileExists(atPath: dest.path) {
try FileManager.default.removeItem(at: dest)
}
try FileManager.default.copyItem(at: src, to: dest)
} catch {
let script = "do shell script \"rm -rf '/Applications/Jobs Monitor.app'; cp -R '\(src.path)' '/Applications/Jobs Monitor.app'\" with administrator privileges"
if let s = NSAppleScript(source: script) {
var err: NSDictionary?
s.executeAndReturnError(&err)
if err != nil { return false }
}
}
let clean = Process()
clean.launchPath = "/usr/bin/xattr"
clean.arguments = ["-dr", "com.apple.quarantine", dest.path]
clean.standardOutput = Pipe(); clean.standardError = Pipe()
try? clean.run(); clean.waitUntilExit()
NSSound(named: "Glass")?.play()
NSWorkspace.shared.open(dest)
NSApp.terminate(nil)
return true
}
class ActionTarget: NSObject {
@objc static func oneClickInstall() {
if !sourcePath.isEmpty {
let src = URL(fileURLWithPath: sourcePath)
_ = performInstallation(src: src)
}
}
}
class DragIcon: NSImageView, NSDraggingSource {
var fileURL: URL?
func draggingSession(_ s: NSDraggingSession, sourceOperationMaskFor c: NSDraggingContext) -> NSDragOperation { .copy }
override func mouseDown(with event: NSEvent) {
guard let url = fileURL, let originalImg = image else { return }
let item = NSPasteboardItem()
item.setString(url.absoluteString, forType: .fileURL)
let drag = NSDraggingItem(pasteboardWriter: item)
let dragImg = NSImage(size: bounds.size)
dragImg.lockFocus()
if let ctx = NSGraphicsContext.current { ctx.imageInterpolation = .high }
originalImg.draw(in: bounds)
dragImg.unlockFocus()
drag.setDraggingFrame(bounds, contents: dragImg)
beginDraggingSession(with: [drag], event: event, source: self)
}
}
class DropZone: NSImageView {
override init(frame f: NSRect) { super.init(frame: f); registerForDraggedTypes([.fileURL]) }
required init?(coder: NSCoder) { super.init(coder: coder); registerForDraggedTypes([.fileURL]) }
override func draggingEntered(_ s: NSDraggingInfo) -> NSDragOperation { .copy }
override func performDragOperation(_ s: NSDraggingInfo) -> Bool {
guard let str = s.draggingPasteboard.propertyList(forType: .fileURL) as? String,
let src = URL(string: str) else { return false }
return performInstallation(src: src)
}
}
let app = NSApplication.shared
app.setActivationPolicy(.regular)
let W: CGFloat = 620, H: CGFloat = 410
let win = NSWindow(contentRect: NSRect(x: 0, y: 0, width: W, height: H), styleMask: [.titled, .closable], backing: .buffered, defer: false)
win.title = "Install Jobs Monitor"
win.center()
let bg = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: W, height: H))
bg.material = .windowBackground; bg.state = .active
win.contentView = bg
let title = NSTextField(labelWithString: "Install Jobs Monitor")
title.frame = NSRect(x: 0, y: H - 65, width: W, height: 30)
title.alignment = .center
title.font = NSFont.systemFont(ofSize: 22, weight: .bold)
bg.addSubview(title)
let sub = NSTextField(labelWithString: "Drag Jobs Monitor to Applications or click Instant Install below")
sub.frame = NSRect(x: 0, y: H - 90, width: W, height: 20)
sub.alignment = .center
sub.font = NSFont.systemFont(ofSize: 13)
sub.textColor = .secondaryLabelColor
bg.addSubview(sub)
let iconSize: CGFloat = 128, midY: CGFloat = 145
let appIcon = DragIcon(frame: NSRect(x: 90, y: midY, width: iconSize, height: iconSize))
appIcon.imageScaling = .scaleProportionallyUpOrDown
var loadedAppIcon: NSImage?
let resourcesDir = URL(fileURLWithPath: sourcePath).appendingPathComponent("Contents/Resources")
for name in ["AppIcon.png", "logo-jobsmonitor.png", "AppIcon.icns"] {
let iconPath = resourcesDir.appendingPathComponent(name).path
if FileManager.default.fileExists(atPath: iconPath), let img = NSImage(contentsOfFile: iconPath) {
loadedAppIcon = img
break
}
}
if loadedAppIcon == nil {
loadedAppIcon = NSWorkspace.shared.icon(forFile: sourcePath)
}
loadedAppIcon?.size = NSSize(width: iconSize, height: iconSize)
appIcon.image = loadedAppIcon
appIcon.fileURL = URL(fileURLWithPath: sourcePath)
bg.addSubview(appIcon)
let appLabel = NSTextField(labelWithString: appName)
appLabel.frame = NSRect(x: 90, y: midY - 26, width: iconSize, height: 18)
appLabel.alignment = .center
appLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium)
bg.addSubview(appLabel)
// Gradient Arrow View (Fading Stem Arrow)
class GradientArrowView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let w = bounds.width
let h = bounds.height
let midY = h / 2.0
let stemH: CGFloat = 10
let headH: CGFloat = 26
let headW: CGFloat = 20
let stemR = w - headW
let path = NSBezierPath()
path.move(to: NSPoint(x: 0, y: midY - stemH/2))
path.line(to: NSPoint(x: stemR, y: midY - stemH/2))
path.line(to: NSPoint(x: stemR, y: midY - headH/2))
path.line(to: NSPoint(x: w, y: midY))
path.line(to: NSPoint(x: stemR, y: midY + headH/2))
path.line(to: NSPoint(x: stemR, y: midY + stemH/2))
path.line(to: NSPoint(x: 0, y: midY + stemH/2))
path.close()
let startColor = NSColor.labelColor.withAlphaComponent(0.0)
let endColor = NSColor.labelColor.withAlphaComponent(0.50)
let gradient = NSGradient(starting: startColor, ending: endColor)
gradient?.draw(in: path, angle: 0)
}
}
let arrow = GradientArrowView(frame: NSRect(x: (W - 70)/2, y: midY + iconSize/2 - 18, width: 70, height: 36))
bg.addSubview(arrow)
let drop = DropZone(frame: NSRect(x: W - 90 - iconSize, y: midY, width: iconSize, height: iconSize))
drop.imageScaling = .scaleProportionallyUpOrDown
let appsIcon = NSWorkspace.shared.icon(forFile: "/Applications")
appsIcon.size = NSSize(width: 128, height: 128)
drop.image = appsIcon
bg.addSubview(drop)
let appsLabel = NSTextField(labelWithString: "Applications")
appsLabel.frame = NSRect(x: W - 90 - iconSize, y: midY - 26, width: iconSize, height: 18)
appsLabel.alignment = .center
appsLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium)
bg.addSubview(appsLabel)
class AnimatedInstallButton: NSButton {
private var trackingArea: NSTrackingArea?
private var isHovered = false { didSet { animateState() } }
private var isPressed = false { didSet { animateState() } }
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
wantsLayer = true
isBordered = false
focusRingType = .none
layer?.cornerRadius = 10
layer?.backgroundColor = NSColor(red: 0.18, green: 0.18, blue: 0.20, alpha: 1.0).cgColor
contentTintColor = .white
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
if let area = trackingArea { removeTrackingArea(area) }
let area = NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .activeAlways], owner: self, userInfo: nil)
addTrackingArea(area)
trackingArea = area
}
override func mouseEntered(with event: NSEvent) {
isHovered = true
NSCursor.pointingHand.set()
}
override func mouseExited(with event: NSEvent) {
isHovered = false
NSCursor.arrow.set()
}
override func mouseDown(with event: NSEvent) {
isPressed = true
super.mouseDown(with: event)
}
override func mouseUp(with event: NSEvent) {
isPressed = false
super.mouseUp(with: event)
}
private func animateState() {
NSAnimationContext.runAnimationGroup { ctx in
ctx.duration = 0.15
ctx.allowsImplicitAnimation = true
if isPressed {
layer?.backgroundColor = NSColor(red: 0.14, green: 0.14, blue: 0.16, alpha: 1.0).cgColor
layer?.transform = CATransform3DMakeScale(0.95, 0.95, 1.0)
} else if isHovered {
layer?.backgroundColor = NSColor(red: 0.28, green: 0.28, blue: 0.30, alpha: 1.0).cgColor
layer?.transform = CATransform3DMakeScale(1.04, 1.04, 1.0)
layer?.shadowColor = NSColor.black.cgColor
layer?.shadowRadius = 8
layer?.shadowOpacity = 0.35
layer?.shadowOffset = .zero
} else {
layer?.backgroundColor = NSColor(red: 0.18, green: 0.18, blue: 0.20, alpha: 1.0).cgColor
layer?.transform = CATransform3DIdentity
layer?.shadowOpacity = 0
}
}
}
}
let installBtn = AnimatedInstallButton(frame: NSRect(x: (W - 270)/2, y: 35, width: 270, height: 42))
installBtn.title = "⚡ One-Click Install to /Applications"
installBtn.font = NSFont.systemFont(ofSize: 13, weight: .bold)
installBtn.target = ActionTarget.self
installBtn.action = #selector(ActionTarget.oneClickInstall)
bg.addSubview(installBtn)
win.makeKeyAndOrderFront(nil)
app.activate(ignoringOtherApps: true)
app.run()
INSTEOF
if swiftc -O -o Installer Installer.swift -framework Cocoa >/dev/null 2>&1; then
./Installer "$APP_BUNDLE"
fi
setup_launch_agent
printf "\n"