From f1142fc7ac4bfd61091dc869301011c9fc3ec079 Mon Sep 17 00:00:00 2001 From: b92c Date: Thu, 9 Jul 2026 22:06:47 -0300 Subject: [PATCH 1/5] feat: add macOS installation support via automation script and update documentation --- README.md | 83 +++++++-------- README.pt-BR.md | 84 +++++++-------- install_macos.sh | 140 +++++++++++++++++++++++++ main.js | 262 +++++++++++++++++++++++++++++++++++++++------- package-lock.json | 42 ++++---- package.json | 12 ++- src/settings.html | 3 + 7 files changed, 474 insertions(+), 152 deletions(-) create mode 100755 install_macos.sh diff --git a/README.md b/README.md index dbbe4af..433b3a5 100644 --- a/README.md +++ b/README.md @@ -68,10 +68,9 @@ terminal — **window _and_ tab**. ## Requirements -- **Linux**. X11: full support (GNOME/Mutter tested). **Wayland: partial** — - the overlay runs via XWayland; tab-focus works in Warp (`focus_url`); - window focus reaches XWayland terminals only. See Troubleshooting. -- `wmctrl`, `xdotool`, `jq` — `sudo apt install wmctrl xdotool jq` +- **Linux** (X11: full support, Wayland: partial — see [Troubleshooting](#troubleshooting)) or **macOS** (supports Apple Silicon M1–M5). +- On Linux: `wmctrl`, `xdotool`, `jq` — `sudo apt install wmctrl xdotool jq` +- On macOS: Homebrew and `jq` — `brew install jq` - Node.js 20+ - A supported agent: [Claude Code](https://claude.com/claude-code), [Antigravity CLI](https://antigravity.google/docs/cli/reference), @@ -79,56 +78,61 @@ terminal — **window _and_ tab**. ## Install -Pick whichever fits. **All three require the agent hooks** so the overlay can -see Claude Code / Antigravity / OpenCode sessions — from source you run -`npm run setup-hook`; in a packaged build you click **Install hooks** in the -tray menu (or the overlay's onboarding button). +Pick whichever fits your platform. **All options require the agent hooks** so the overlay can see Claude Code / Antigravity / OpenCode sessions — from source you run `npm run setup-hook`; in a packaged build you click **Install/update hooks** in the tray menu or Preferences window (or the overlay's onboarding button). -### A — AppImage (recommended, self-updating) +### macOS (M1 to M5 / arm64) -**One line** — fetches the latest release, makes it executable, installs the icon -into the system theme and creates the application-menu shortcut (with -`StartupWMClass` matching the app's real WM_CLASS): +#### Option 1: Automated script (recommended) +Run the following single-line command in your terminal. It will verify dependencies (installing `jq` via Homebrew if needed), download the latest `.dmg` release, copy `AI Traffic Lights.app` to `/Applications`, and configure shell aliases (`atl` and `ai-traffic-lights`) in your `~/.zshrc` and `~/.bash_profile`: ```bash -curl -fsSL https://raw.githubusercontent.com/aronpc/ai-traffic-lights/main/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/aronpc/ai-traffic-lights/main/install_macos.sh | bash +``` + +To run the application from the terminal, open a new shell session (or run `source ~/.zshrc`) and execute: +```bash +atl ``` -Then open it from your **application menu** (search "AI Traffic Lights") or run -`~/Applications/AI-Traffic-Lights.AppImage`. To remove: -`curl -fsSL .../install.sh | bash -s -- --uninstall`. +#### Option 2: Manual Install +1. Download the `.dmg` file from the [latest release](https://github.com/aronpc/ai-traffic-lights/releases/latest). +2. Open the `.dmg` file and drag `AI Traffic Lights.app` to your `/Applications` folder. -This is the only build that **updates itself**: on launch and hourly it checks -for a new release, the version badge becomes `↓ vX` (download) → `↻` (restart -into the new version), and the tray has a "Check for updates" entry. +--- -
Without install.sh (manual) +### Linux -Grab the `.AppImage` from the [latest release](https://github.com/aronpc/ai-traffic-lights/releases/latest), -drop it in a **user-writable** folder (the self-updater rewrites that file in -place — so not `/opt` or `/usr`), and run it: +#### Option 1: AppImage (recommended, self-updating) +A single command that fetches the latest release, makes it executable, installs the icon into the system theme, and creates an application menu shortcut: + +```bash +curl -fsSL https://raw.githubusercontent.com/aronpc/ai-traffic-lights/main/install.sh | bash +``` +Then open it from your application menu or run `~/Applications/AI-Traffic-Lights.AppImage`. To uninstall: +```bash +curl -fsSL https://raw.githubusercontent.com/aronpc/ai-traffic-lights/main/install.sh | bash -s -- --uninstall +``` + +
Manual AppImage install + +Grab the `.AppImage` from the [latest release](https://github.com/aronpc/ai-traffic-lights/releases/latest), drop it in a user-writable folder (the self-updater rewrites that file in place — do not use `/opt` or `/usr`), and run: ```bash chmod +x AI-Traffic-Lights-*.AppImage ./AI-Traffic-Lights-*.AppImage ``` -
-### B — .deb - -Grab the `.deb` from the [latest release](https://github.com/aronpc/ai-traffic-lights/releases/latest) and: +#### Option 2: Debian Package (.deb) +Download the `.deb` from the [latest release](https://github.com/aronpc/ai-traffic-lights/releases/latest) and install it: ```bash sudo dpkg -i ai-traffic-lights_*.deb ``` -Installs under `/opt` and registers a `.desktop` entry. **No self-update** on -this path (the updater can't replace a root-owned package) — you only get a -`↑ vX` prompt that opens the release page when a new version ships. - -### C — from source (development) +--- +### From Source (Development - Linux & macOS) ```bash git clone https://github.com/aronpc/ai-traffic-lights.git cd ai-traffic-lights @@ -139,16 +143,9 @@ npm run setup-hook # registers the adapters: Claude Code (~/.claude), npm start # opens the overlay ``` -`setup-hook` is idempotent and surgical: it backs up `settings.json` and never -touches hooks from other tools. The registered command points to a -self-updating **stable copy** of the hook in -`~/.local/share/ai-traffic-lights/bin/` — so moving the project (or running -the packaged AppImage, whose mount path changes every run) never breaks it. -`npm run remove-hook` undoes everything just as cleanly. The tray menu offers -the same install/remove actions for packaged installs. +`setup-hook` is idempotent and surgical: it backs up `settings.json` and never touches hooks from other tools. The registered command points to a self-updating **stable copy** of the hook in `~/.local/share/ai-traffic-lights/bin/` — so moving the project (or running the packaged AppImage/App, whose mount path changes every run) never breaks it. `npm run remove-hook` undoes everything just as cleanly. The tray menu and Preferences window offer the same install/remove actions for packaged installs. -New Claude Code sessions show up immediately; sessions already open appear on -their next event. +New Claude Code sessions show up immediately; sessions already open appear on their next event. ## How it works @@ -259,9 +256,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details. - **Overlay shows "no active sessions"** — did you run `npm run setup-hook`? Already-open sessions only appear after their next event (send any prompt). -- **Click doesn't focus / focuses the wrong window** — check `wmctrl` and - `xdotool` are installed; on Wayland they don't work (roadmap). The exact-tab - jump only works in Warp for now. +- **Click doesn't focus / focuses the wrong window** — on Linux, check that `wmctrl` and `xdotool` are installed. On macOS, click-to-focus uses AppleScript (`osascript`) to focus the window. If it fails, ensure `AI Traffic Lights.app` (or your terminal if running from source) has **Accessibility** permissions granted in *System Settings > Privacy & Security > Accessibility*. - **Wayland** — the overlay itself runs fine (XWayland). Native-Wayland windows can't be focused by third parties, so click-to-focus relies on the terminal's focus URI (Warp today); the global shortcut may not fire while a diff --git a/README.pt-BR.md b/README.pt-BR.md index 44cbdc4..8887084 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -67,11 +67,9 @@ por sessão, clique para pular pro terminal — **janela _e_ aba**. ## Requisitos -- **Linux**. X11: suporte completo (testado em GNOME/Mutter). **Wayland: - parcial** — o overlay roda via XWayland; o foco de aba funciona no Warp - (`focus_url`); o foco de janela alcança só terminais XWayland. Ver Solução - de problemas. -- `wmctrl`, `xdotool`, `jq` — `sudo apt install wmctrl xdotool jq` +- **Linux** (suporte completo no X11, parcial no Wayland — veja [Solução de problemas](#solucao-de-problemas)) ou **macOS** (suporta Apple Silicon M1–M5). +- No Linux: `wmctrl`, `xdotool`, `jq` — `sudo apt install wmctrl xdotool jq` +- No macOS: Homebrew e `jq` — `brew install jq` - Node.js 20+ - Um agente suportado: [Claude Code](https://claude.com/claude-code), [Antigravity CLI](https://antigravity.google/docs/cli/reference), @@ -79,56 +77,61 @@ por sessão, clique para pular pro terminal — **janela _e_ aba**. ## Instalação -Escolha a que preferir. **Todas exigem os hooks dos agentes** para o overlay -enxergar sessões do Claude Code / Antigravity / OpenCode — pelo fonte você roda -`npm run setup-hook`; num build empacotado você clica em **Instalar hooks** no -menu do tray (ou no botão de onboarding do overlay). +Escolha a que preferir para a sua plataforma. **Todas as opções exigem os hooks dos agentes** para o overlay enxergar as sessões do Claude Code / Antigravity / OpenCode — pelo fonte você roda `npm run setup-hook`; num build empacotado você clica em **Instalar/atualizar hooks** no menu do tray ou na janela de Preferências (ou no botão de onboarding do overlay). -### A — AppImage (recomendado, auto-atualizável) +### macOS (M1 a M5 / arm64) -**Uma linha** — baixa a versão mais recente, dá permissão de execução, instala o -ícone no tema do sistema e cria o atalho no menu de aplicativos (com -`StartupWMClass` casando o WM_CLASS real do app): +#### Opção 1: Script automatizado (recomendado) +Rode o comando de linha única abaixo no terminal. Ele irá verificar dependências (instalando o `jq` via Homebrew se necessário), baixar a versão `.dmg` mais recente, copiar o `AI Traffic Lights.app` para `/Applications` e configurar aliases de terminal (`atl` e `ai-traffic-lights`) no seu `~/.zshrc` e `~/.bash_profile`: ```bash -curl -fsSL https://raw.githubusercontent.com/aronpc/ai-traffic-lights/main/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/aronpc/ai-traffic-lights/main/install_macos.sh | bash +``` + +Para iniciar o aplicativo via terminal, abra uma nova aba do terminal (ou execute `source ~/.zshrc`) e execute: +```bash +atl ``` -Depois abra pelo **menu de aplicativos** (busque "AI Traffic Lights") ou rode -`~/Applications/AI-Traffic-Lights.AppImage`. Para remover: -`curl -fsSL .../install.sh | bash -s -- --uninstall`. +#### Opção 2: Instalação Manual +1. Baixe o arquivo `.dmg` da [última release](https://github.com/aronpc/ai-traffic-lights/releases/latest). +2. Abra o arquivo `.dmg` e arraste o `AI Traffic Lights.app` para a sua pasta `/Applications`. -É o único build que **se atualiza sozinho**: no boot e a cada hora ele checa por -uma release nova, o badge de versão vira `↓ vX` (baixar) → `↻` (reiniciar na -versão nova), e o tray tem um item "Verificar atualizações". +--- -
Sem o install.sh (manual) +### Linux -Baixe o `.AppImage` da [última release](https://github.com/aronpc/ai-traffic-lights/releases/latest) -numa pasta **gravável pelo usuário** (o auto-updater reescreve esse arquivo no -lugar — então não `/opt` nem `/usr`), e rode: +#### Opção 1: AppImage (recomendado, auto-atualizável) +Um comando único que baixa a versão mais recente, torna-a executável, instala o ícone no tema do sistema e cria o atalho no menu de aplicativos: + +```bash +curl -fsSL https://raw.githubusercontent.com/aronpc/ai-traffic-lights/main/install.sh | bash +``` +Depois abra pelo menu de aplicativos ou execute `~/Applications/AI-Traffic-Lights.AppImage`. Para desinstalar: +```bash +curl -fsSL https://raw.githubusercontent.com/aronpc/ai-traffic-lights/main/install.sh | bash -s -- --uninstall +``` + +
Instalação manual do AppImage + +Baixe o `.AppImage` da [última release](https://github.com/aronpc/ai-traffic-lights/releases/latest), coloque em uma pasta com permissão de escrita para o usuário (o auto-atualizador reescreve esse arquivo no lugar — não use `/opt` ou `/usr`) e rode: ```bash chmod +x AI-Traffic-Lights-*.AppImage ./AI-Traffic-Lights-*.AppImage ``` -
-### B — .deb - -Baixe o `.deb` da [última release](https://github.com/aronpc/ai-traffic-lights/releases/latest) e: +#### Opção 2: Pacote Debian (.deb) +Baixe o `.deb` da [última release](https://github.com/aronpc/ai-traffic-lights/releases/latest) e instale: ```bash sudo dpkg -i ai-traffic-lights_*.deb ``` -Instala em `/opt` e registra um `.desktop`. **Sem auto-update** nesse caminho (o -updater não consegue substituir um pacoto root-owned) — você só recebe um aviso -`↑ vX` que abre a página da release quando sai uma versão nova. - -### C — pelo fonte (desenvolvimento) +--- +### Pelo fonte (Desenvolvimento - Linux & macOS) ```bash git clone https://github.com/aronpc/ai-traffic-lights.git cd ai-traffic-lights @@ -139,16 +142,9 @@ npm run setup-hook # registra os adapters: Claude Code (~/.claude), npm start # abre o overlay ``` -O `setup-hook` é idempotente e cirúrgico: faz backup do `settings.json` e -nunca toca hooks de outras ferramentas. O comando registrado aponta para uma -**cópia estável** auto-atualizada do hook em -`~/.local/share/ai-traffic-lights/bin/` — mover o projeto (ou rodar o -AppImage, cujo ponto de montagem muda a cada execução) nunca quebra nada. -`npm run remove-hook` desfaz tudo com o mesmo cuidado. O menu do tray oferece -as mesmas ações de instalar/remover para instalações empacotadas. +O `setup-hook` é idempotente e cirúrgico: faz backup do `settings.json` e nunca toca hooks de outras ferramentas. O comando registrado aponta para uma **cópia estável** auto-atualizada do hook em `~/.local/share/ai-traffic-lights/bin/` — mover o projeto (ou rodar o AppImage/App, cujo ponto de montagem muda a cada execução) nunca quebra nada. `npm run remove-hook` desfaz tudo com o mesmo cuidado. O menu do tray e a janela de Preferências oferecem as mesmas ações de instalar/remover para instalações empacotadas. -Sessões novas do Claude Code aparecem imediatamente; sessões já abertas -aparecem no próximo evento delas. +Sessões novas do Claude Code aparecem imediatamente; sessões já abertas aparecem no próximo evento delas. ## Como funciona @@ -257,9 +253,7 @@ Detalhes em [CONTRIBUTING.md](CONTRIBUTING.md). - **Overlay mostra "nenhuma sessão ativa"** — rodou `npm run setup-hook`? Sessões já abertas só aparecem no próximo evento (mande qualquer prompt). -- **Clique não foca / foca a janela errada** — `wmctrl` e `xdotool` - instalados? No Wayland eles não funcionam (roadmap). O pulo pra aba exata - só existe no Warp por enquanto. +- **Clique não foca / foca a janela errada** — no Linux, certifique-se de que `wmctrl` e `xdotool` estão instalados. No macOS, o click-to-focus utiliza AppleScript (`osascript`) para focar a janela. Se falhar, certifique-se de que o `AI Traffic Lights.app` (ou o seu terminal, caso execute pelo fonte) possui permissões de **Acessibilidade** concedidas em *Ajustes do Sistema > Privacidade e Segurança > Acessibilidade*. - **Wayland** — o overlay em si roda bem (XWayland). Janelas Wayland nativas não podem ser focadas por terceiros, então o click-to-focus depende da URI de foco do terminal (Warp hoje); o atalho global pode não disparar com um diff --git a/install_macos.sh b/install_macos.sh new file mode 100755 index 0000000..4f29922 --- /dev/null +++ b/install_macos.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# +# install_macos.sh — Instala e configura o AI Traffic Lights no macOS (M1 a M5 / arm64). +# +# Uso (1 linha): +# curl -fsSL https://raw.githubusercontent.com/aronpc/ai-traffic-lights/main/install_macos.sh | bash +# +set -euo pipefail + +REPO="aronpc/ai-traffic-lights" +APP_TITLE="AI Traffic Lights" +APP_NAME="AI Traffic Lights.app" +DMG_NAME="AI-Traffic-Lights.dmg" +API_URL="https://api.github.com/repos/${REPO}/releases/latest" + +info() { printf '\033[1;34m›\033[0m %s\n' "$*"; } +ok() { printf '\033[1;32m✓\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m⚠️\033[0m %s\n' "$*"; } +die() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; exit 1; } + +# ----------------------------- verificar SO e Arquitetura ----------------------------- +OS="$(uname -s)" +ARCH="$(uname -m)" + +if [ "$OS" != "Darwin" ]; then + die "Este instalador é exclusivo para macOS. SO atual: $OS" +fi + +if [ "$ARCH" != "arm64" ]; then + warn "Sua arquitetura é $ARCH. Este build é otimizado para Apple Silicon (M1, M2, M3, M4, M5)." + # Prossiga mesmo assim caso queira rodar via Rosetta, mas envie alerta. +fi + +# ----------------------------- verificar dependências (Homebrew & jq) ----------------------------- +if ! command -v brew >/dev/null 2>&1; then + die "Homebrew não encontrado. Por favor, instale o Homebrew em https://brew.sh e tente novamente." +fi + +if ! command -v jq >/dev/null 2>&1; then + info "Instalando dependência 'jq' via Homebrew..." + brew install jq + ok "'jq' instalado com sucesso." +else + ok "'jq' já está instalado." +fi + +# ----------------------------- obter release e baixar DMG ----------------------------- +info "Consultando a versão mais recente para macOS no GitHub..." +json="" +if ! json="$(curl -fsSL -H 'Accept: application/vnd.github+json' "$API_URL")"; then + warn "Não foi possível conectar à API do GitHub." +fi + +download_url="" +if [ -n "$json" ]; then + download_url="$(printf '%s\n' "$json" | grep -oE '"browser_download_url":[[:space:]]*"[^"]+\.dmg"' | head -1 | sed -E 's/.*"([^"]+)"$/\1/' || true)" + version="$(printf '%s\n' "$json" | grep -oE '"tag_name":[[:space:]]*"v[^"]+"' | head -1 | sed -E 's/.*"v([^"]+)"$/\1/' || true)" +fi + +TMP_DIR="/tmp/ai-traffic-lights-install" +rm -rf "$TMP_DIR" && mkdir -p "$TMP_DIR" +DMG_PATH="$TMP_DIR/$DMG_NAME" + +if [ -n "$download_url" ]; then + info "Baixando versão v${version} de: $download_url" + curl -fSL --retry 3 -o "$DMG_PATH" "$download_url" + ok "Download concluído." + + info "Montando instalador e copiando aplicativo para /Applications..." + MOUNT_POINT="$TMP_DIR/mount" + mkdir -p "$MOUNT_POINT" + hdiutil attach -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$DMG_PATH" + + # Copiar app substituindo se já existir + rm -rf "/Applications/$APP_NAME" + cp -R "$MOUNT_POINT/$APP_NAME" "/Applications/" + + hdiutil detach "$MOUNT_POINT" + ok "Aplicativo copiado para /Applications/$APP_NAME" +else + warn "Nenhuma release .dmg oficial encontrada no repositório GitHub ainda." + warn "Se você está compilando localmente, rode 'npm run dist' e copie o app para a pasta /Applications." +fi + +# Detect if run from inside the repository (development fallback) +LOCAL_REPO="" +if [ -f "package.json" ] && grep -q '"name": "ai-traffic-lights"' package.json; then + LOCAL_REPO="$(pwd)" +fi + +if [ -n "$LOCAL_REPO" ]; then + if ! command -v npm >/dev/null 2>&1; then + die "Instalação em modo desenvolvimento detectada, mas 'npm' não foi encontrado. Por favor, instale o Node.js e tente novamente." + fi + info "Executando em modo desenvolvimento. Instalando dependências do Node.js..." + (cd "$LOCAL_REPO" && npm install) + ok "Dependências do Node.js instaladas." +fi + +# ----------------------------- configurar aliases ----------------------------- +info "Configurando aliases para inicialização rápida..." + +if [ -n "$LOCAL_REPO" ]; then + ALIAS_LINE_1="alias atl=\"[ -d '/Applications/AI Traffic Lights.app' ] && open -a '$APP_TITLE' || npm start --prefix '$LOCAL_REPO'\"" + ALIAS_LINE_2="alias ai-traffic-lights=\"[ -d '/Applications/AI Traffic Lights.app' ] && open -a '$APP_TITLE' || npm start --prefix '$LOCAL_REPO'\"" +else + ALIAS_LINE_1="alias atl=\"open -a '$APP_TITLE'\"" + ALIAS_LINE_2="alias ai-traffic-lights=\"open -a '$APP_TITLE'\"" +fi + +setup_profile_aliases() { + local profile="$1" + if [ -f "$profile" ]; then + # Remover aliases existentes para evitar duplicados + sed -i '' '/alias atl=/d' "$profile" 2>/dev/null || true + sed -i '' '/alias ai-traffic-lights=/d' "$profile" 2>/dev/null || true + + echo "" >> "$profile" + echo "# AI Traffic Lights aliases" >> "$profile" + echo "$ALIAS_LINE_1" >> "$profile" + echo "$ALIAS_LINE_2" >> "$profile" + ok "Aliases adicionados em: $profile" + fi +} + +setup_profile_aliases "$HOME/.zshrc" +setup_profile_aliases "$HOME/.bash_profile" + +# ----------------------------- finalização ----------------------------- +printf '\n\033[1;32m✓ Configuração concluída!\033[0m\n\n' +cat < 1; i++) { - set.add(p); - try { - const m = fs.readFileSync(`/proc/${p}/status`, 'utf8').match(/^PPid:\s+(\d+)/m); - if (!m) break; - p = parseInt(m[1], 10); - } catch { break; } + if (process.platform === 'darwin') { + for (let i = 0; i < 25 && p > 1; i++) { + set.add(p); + try { + const ppidStr = execFileSync('ps', ['-o', 'ppid=', '-p', p], { encoding: 'utf8', timeout: 1000 }).trim(); + if (!ppidStr) break; + p = parseInt(ppidStr, 10); + } catch { break; } + } + } else { + for (let i = 0; i < 25 && p > 1; i++) { + set.add(p); + try { + const m = fs.readFileSync(`/proc/${p}/status`, 'utf8').match(/^PPid:\s+(\d+)/m); + if (!m) break; + p = parseInt(m[1], 10); + } catch { break; } + } } return set; } +function findTerminalAppNameFromPid(pid) { + const ancestors = Array.from(ancestorPidsOf(pid)); + for (const p of ancestors) { + try { + const commPath = execFileSync('ps', ['-p', p, '-o', 'comm='], { encoding: 'utf8', timeout: 500 }).trim(); + const name = path.basename(commPath).toLowerCase(); + if (name.includes('warp') || commPath.includes('Warp.app')) return 'Warp'; + if (name.includes('iterm') || commPath.includes('iTerm.app')) return 'iTerm'; + if (name.includes('terminal') || commPath.includes('Terminal.app')) return 'Terminal'; + if (name.includes('ghostty') || commPath.includes('Ghostty.app')) return 'Ghostty'; + } catch {} + } + return null; +} + function raiseWindow(windowid, pid) { if (!pid) return false; + if (process.platform === 'darwin') { + const ancestors = Array.from(ancestorPidsOf(pid)); + for (let i = ancestors.length - 1; i >= 0; i--) { + const apid = ancestors[i]; + try { + const check = execFileSync('osascript', ['-e', `tell application "System Events" to get name of first process whose unix id is ${apid}`], { encoding: 'utf8', timeout: 500 }).trim(); + if (check) { + execFileSync('osascript', ['-e', `tell application "System Events" to set frontmost of first process whose unix id is ${apid} to true`], { timeout: 1000 }); + return true; + } + } catch {} + } + const appName = findTerminalAppNameFromPid(pid); + if (appName) { + try { + execFileSync('osascript', ['-e', `tell application "${appName}" to activate`], { timeout: 2000 }); + return true; + } catch {} + } + return false; + } let list = ''; try { list = execFileSync('wmctrl', ['-l', '-p'], { encoding: 'utf8', timeout: 2000 }); } catch { return false; } const wins = []; @@ -254,7 +336,8 @@ function focusTab(state) { if (!ch) return; try { if (ch.kind === 'warp') { - execFileSync('xdg-open', [ch.value], { timeout: 2000 }); + const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open'; + execFileSync(cmd, [ch.value], { timeout: 2000 }); } else if (ch.kind === 'tilix') { execFileSync('gdbus', ['call', '--session', '--dest', 'com.gexperts.Tilix', '--object-path', '/com/gexperts/Tilix', '--method', 'org.gtk.Actions.Activate', @@ -263,14 +346,42 @@ function focusTab(state) { } catch {} } -// Enriquece o alvo com os hints de foco lidos AO VIVO do /proc//environ. +function getProcessEnviron(pid) { + if (!pid) return ''; + if (process.platform === 'darwin') { + try { + const output = execFileSync('ps', ['-p', pid, '-E'], { encoding: 'utf8', timeout: 1000 }); + const lines = output.split('\n'); + if (lines.length < 2) return ''; + const content = lines.slice(1).join(' '); + const envVars = []; + const parts = content.split(/\s+/); + for (const part of parts) { + if (part.includes('=')) { + envVars.push(part); + } + } + return envVars.join('\0'); + } catch { + return ''; + } + } else { + try { + return fs.readFileSync(`/proc/${pid}/environ`, 'utf8'); + } catch { + return ''; + } + } +} + +// Enriquece o alvo com os hints de foco lidos AO VIVO do processo. // O state file guarda um snapshot capturado no prompt; o environ é a fonte // viva — cobre sessões cujo evento veio antes do hook atual e as detectadas // só via /proc (sem focus_url/tilix_id no state). O state tem precedência. function enrichTarget(target) { if (!target || !target.pid || (target.focus_url && target.tilix_id)) return target; try { - const hints = focus.parseEnviron(fs.readFileSync(`/proc/${target.pid}/environ`, 'utf8')); + const hints = focus.parseEnviron(getProcessEnviron(target.pid)); return { ...target, focus_url: target.focus_url || hints.focus_url, @@ -382,6 +493,14 @@ function detectLaunchers() { // Quais terminais suportados estão no PATH? (pra 'auto' e pra validar o seletor) function availableTerminals() { + if (process.platform === 'darwin') { + const list = []; + if (fs.existsSync('/Applications/iTerm.app')) list.push('iterm2'); + if (fs.existsSync('/System/Applications/Utilities/Terminal.app')) list.push('terminal'); + if (fs.existsSync('/Applications/Warp.app')) list.push('warp'); + if (fs.existsSync('/Applications/Ghostty.app')) list.push('ghostty'); + return list; + } return launcher.TERMINAL_ORDER.filter((t) => !!scanPathBin(t)); } @@ -407,6 +526,46 @@ function launchAgent({ agent, cwd }) { const entry = detectLaunchers().find((l) => l.id === agent); if (!entry) { notifyUser(T('ntf_no_launcher', { agent: a.label })); return; } const dir = (cwd && typeof cwd === 'string') ? cwd : (lastSessionCwd() || process.env.HOME || '/'); + + if (process.platform === 'darwin') { + const term = settingsCfg.terminal === 'auto' ? (availableTerminals()[0] || 'terminal') : settingsCfg.terminal; + + if (term === 'terminal') { + const appleScript = ` + tell application "Terminal" + do script "cd '${dir.replace(/'/g, "'\\''")}' && ${entry.path.replace(/'/g, "'\\''")}" + activate + end tell + `; + try { spawn('osascript', ['-e', appleScript], { detached: true, stdio: 'ignore' }).unref(); } catch (e) { notifyUser(`Launch failed: ${e.message}`); } + return; + } + + if (term === 'iterm2') { + const appleScript = ` + tell application "iTerm" + create window with default profile + tell current session of current window + write text "cd '${dir.replace(/'/g, "'\\''")}' && ${entry.path.replace(/'/g, "'\\''")}" + end tell + activate + end tell + `; + try { spawn('osascript', ['-e', appleScript], { detached: true, stdio: 'ignore' }).unref(); } catch (e) { notifyUser(`Launch failed: ${e.message}`); } + return; + } + + if (term === 'warp') { + try { spawn('open', ['-a', 'Warp', dir], { detached: true, stdio: 'ignore' }).unref(); } catch (e) { notifyUser(`Launch failed: ${e.message}`); } + return; + } + + if (term === 'ghostty') { + try { spawn('open', ['-a', 'Ghostty', '--args', `--working-directory=${dir}`, entry.path], { detached: true, stdio: 'ignore' }).unref(); } catch (e) { notifyUser(`Launch failed: ${e.message}`); } + return; + } + } + const term = launcher.pickTerminal(settingsCfg.terminal, availableTerminals()); if (settingsCfg.terminal === 'custom' && settingsCfg.terminalCmd.trim()) { // Custom: template com {cwd} e {cmd}. Split simples (sem shell, sem quoting rico). @@ -511,7 +670,7 @@ function saveBounds() { // Aplica _NET_WM_STATE_SKIP_TASKBAR + SKIP_PAGER via wmctrl no X11 id da // janela. No Wayland wmctrl é inócuo (silencioso). Idempotente. function applySkip() { - if (!win || win.isDestroyed() || IS_WAYLAND) return; + if (!win || win.isDestroyed() || IS_WAYLAND || process.platform === 'darwin') return; try { const buf = win.getNativeWindowHandle(); // X11: XID little-endian const xid = '0x' + buf.readUInt32LE(0).toString(16).padStart(8, '0'); @@ -1031,7 +1190,7 @@ function glmCredsFromSessions() { if (!s.pid || !/^glm/i.test(s.model || '')) continue; let env; try { - const raw = fs.readFileSync(`/proc/${s.pid}/environ`, 'utf8'); + const raw = getProcessEnviron(s.pid); env = usage.parseEnviron(raw, ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN']); } catch { continue; } // processo morreu entre readSessions e a leitura if (!env.ANTHROPIC_BASE_URL || !env.ANTHROPIC_AUTH_TOKEN) continue; @@ -1051,17 +1210,24 @@ function glmCredsFromSessions() { // FALLBACK: o processo PRINCIPAL do Claude Code às vezes não herda as env vars // do GLM no environ (lançado via wrapper/alias que não repassa), mas seus // SUBPROCESSOS sim (MCP servers, shells filhos, etc.). Se glmCredsFromSessions -// não achou nada nos pids das sessões, varre todo o /proc procurando qualquer +// não achou nada nos pids das sessões, varre todo o sistema procurando qualquer // processo com ANTHROPIC_BASE_URL (z.ai/bigmodel) + token. A conta é uma só — // qualquer processo que tenha as credenciais serve pra buscar o % do plano. // Dedup por token. Nunca lança; só lê o que o dono consegue (EACCES → skip). function glmCredsFromProc() { const byToken = new Map(); - let pids; - try { pids = fs.readdirSync('/proc').filter((d) => /^\d+$/.test(d)); } catch { return []; } + let pids = []; + if (process.platform === 'darwin') { + try { + const output = execFileSync('ps', ['-ax', '-o', 'pid='], { encoding: 'utf8', timeout: 2000 }); + pids = output.split('\n').map(l => parseInt(l.trim(), 10)).filter(n => !isNaN(n)); + } catch { return []; } + } else { + try { pids = fs.readdirSync('/proc').filter((d) => /^\d+$/.test(d)).map(n => parseInt(n, 10)); } catch { return []; } + } for (const pid of pids) { let raw; - try { raw = fs.readFileSync(`/proc/${pid}/environ`, 'utf8'); } catch { continue; } // EACCES/morto + try { raw = getProcessEnviron(pid); } catch { continue; } // morto ou sem permissão const env = usage.parseEnviron(raw, ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN']); if (!env.ANTHROPIC_BASE_URL || !env.ANTHROPIC_AUTH_TOKEN) continue; if (!/api\.z\.ai|bigmodel\.cn/.test(env.ANTHROPIC_BASE_URL)) continue; // só backend GLM @@ -1115,10 +1281,30 @@ function mergeGlmCreds(a, b) { return [...byToken.values()]; } +function getProcessCwd(pid) { + if (!pid) return null; + if (process.platform === 'darwin') { + try { + const output = execFileSync('lsof', ['-p', pid, '-a', '-d', 'cwd', '-Fn'], { encoding: 'utf8', timeout: 1000 }); + for (const line of output.split('\n')) { + if (line.startsWith('n')) { + return line.slice(1).trim(); + } + } + } catch {} + return null; + } else { + try { + return fs.readlinkSync(`/proc/${pid}/cwd`); + } catch { + return null; + } + } +} + // Codex é passivo: o uso vive no rollout da sessão, associado por cwd. As // sessões Codex vivas são detectadas por /proc (sem state file próprio) e o -// cwd é lido de /proc//cwd (symlink legível pelo dono — ao contrário do -// cwd do hook, que o ptrace_scope bloqueia). Dedup por cwd. +// cwd é lido de /proc//cwd no Linux ou via lsof no macOS. Dedup por cwd. function codexCwdsFromSessions() { let sessions = []; try { sessions = readSessions(); } catch { return []; } @@ -1126,7 +1312,7 @@ function codexCwdsFromSessions() { for (const s of sessions) { if (!s.pid || agentOf(s) !== 'codex') continue; try { - const cwd = fs.readlinkSync(`/proc/${s.pid}/cwd`); + const cwd = getProcessCwd(s.pid); if (cwd) cwds.add(cwd); } catch { /* processo morreu ou sem permissão */ } } diff --git a/package-lock.json b/package-lock.json index c724165..3b5d8e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,20 @@ { "name": "ai-traffic-lights", - "version": "0.4.1", + "version": "0.6.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ai-traffic-lights", - "version": "0.4.1", + "version": "0.6.7", "license": "MIT", "dependencies": { - "chokidar": "^3.6.0" + "chokidar": "^3.6.0", + "electron-updater": "^6.8.9" }, "devDependencies": { "electron": "^43.0.0", - "electron-builder": "^26.15.3", - "electron-updater": "^6.8.9" + "electron-builder": "^26.15.3" } }, "node_modules/@electron-internal/extract-zip": { @@ -382,6 +382,7 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, + "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -403,6 +404,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -419,6 +421,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -433,6 +436,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 10.0.0" } @@ -919,7 +923,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/asn1js": { @@ -1099,7 +1102,6 @@ "version": "9.7.0", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4", @@ -1337,7 +1339,8 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -1381,7 +1384,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1538,7 +1540,6 @@ "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "26.15.3", "builder-util": "26.15.3", @@ -1696,7 +1697,6 @@ "version": "6.8.9", "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.9.tgz", "integrity": "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==", - "dev": true, "license": "MIT", "dependencies": { "builder-util-runtime": "9.7.0", @@ -1713,7 +1713,6 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1729,6 +1728,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -1749,6 +1749,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -1977,7 +1978,6 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -1992,7 +1992,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -2005,7 +2004,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -2252,7 +2250,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-flag": { @@ -2516,7 +2513,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", - "dev": true, "funding": [ { "type": "github", @@ -2594,7 +2590,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, "license": "MIT" }, "node_modules/lodash": { @@ -2608,7 +2603,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "dev": true, "license": "MIT" }, "node_modules/lodash.isequal": { @@ -2616,7 +2610,6 @@ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "dev": true, "license": "MIT" }, "node_modules/lowercase-keys": { @@ -2767,6 +2760,7 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -2778,7 +2772,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/node-abi": { @@ -3054,6 +3047,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -3071,6 +3065,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -3288,6 +3283,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -3335,7 +3331,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -3558,6 +3553,7 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -3601,7 +3597,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", - "dev": true, "license": "MIT" }, "node_modules/tinyglobby": { @@ -3645,7 +3640,6 @@ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index b6d0375..772d10d 100644 --- a/package.json +++ b/package.json @@ -51,9 +51,19 @@ "--disable-dev-shm-usage", "--ozone-platform=x11" ] - }, "appImage": { "artifactName": "${name}-${version}.${ext}" + }, + "mac": { + "target": [ + "dmg", + "zip" + ], + "arch": [ + "arm64" + ], + "category": "public.app-category.utilities", + "icon": "build/icon.png" } }, "devDependencies": { diff --git a/src/settings.html b/src/settings.html index 99295bd..81fa204 100644 --- a/src/settings.html +++ b/src/settings.html @@ -166,6 +166,9 @@ + + +
Onde o Quick Launcher (botões + agente) abre o terminal. Custom usa {cwd} e {cmd}.
From e2c83422d46e078f85cf8eb67c2b3d5d19f806ed Mon Sep 17 00:00:00 2001 From: b92c Date: Sat, 11 Jul 2026 19:03:25 -0300 Subject: [PATCH 2/5] feat: add support for additional terminals and establish standardized AI agent entrypoint documentation --- README.pt-BR.md | 2 +- install_macos.sh | 38 +++++------ main.js | 160 +++++++++++++++++++++++++++++++++++------------ src/settings.js | 2 +- 4 files changed, 136 insertions(+), 66 deletions(-) diff --git a/README.pt-BR.md b/README.pt-BR.md index 8887084..ad0947b 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -67,7 +67,7 @@ por sessão, clique para pular pro terminal — **janela _e_ aba**. ## Requisitos -- **Linux** (suporte completo no X11, parcial no Wayland — veja [Solução de problemas](#solucao-de-problemas)) ou **macOS** (suporta Apple Silicon M1–M5). +- **Linux** (suporte completo no X11, parcial no Wayland — veja [Solução de problemas](#solução-de-problemas)) ou **macOS** (suporta Apple Silicon M1–M5). - No Linux: `wmctrl`, `xdotool`, `jq` — `sudo apt install wmctrl xdotool jq` - No macOS: Homebrew e `jq` — `brew install jq` - Node.js 20+ diff --git a/install_macos.sh b/install_macos.sh index 4f29922..9b7723a 100755 --- a/install_macos.sh +++ b/install_macos.sh @@ -1,10 +1,5 @@ #!/usr/bin/env bash -# -# install_macos.sh — Instala e configura o AI Traffic Lights no macOS (M1 a M5 / arm64). -# -# Uso (1 linha): -# curl -fsSL https://raw.githubusercontent.com/aronpc/ai-traffic-lights/main/install_macos.sh | bash -# + set -euo pipefail REPO="aronpc/ai-traffic-lights" @@ -18,7 +13,6 @@ ok() { printf '\033[1;32m✓\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m⚠️\033[0m %s\n' "$*"; } die() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; exit 1; } -# ----------------------------- verificar SO e Arquitetura ----------------------------- OS="$(uname -s)" ARCH="$(uname -m)" @@ -28,10 +22,8 @@ fi if [ "$ARCH" != "arm64" ]; then warn "Sua arquitetura é $ARCH. Este build é otimizado para Apple Silicon (M1, M2, M3, M4, M5)." - # Prossiga mesmo assim caso queira rodar via Rosetta, mas envie alerta. fi -# ----------------------------- verificar dependências (Homebrew & jq) ----------------------------- if ! command -v brew >/dev/null 2>&1; then die "Homebrew não encontrado. Por favor, instale o Homebrew em https://brew.sh e tente novamente." fi @@ -44,7 +36,6 @@ else ok "'jq' já está instalado." fi -# ----------------------------- obter release e baixar DMG ----------------------------- info "Consultando a versão mais recente para macOS no GitHub..." json="" if ! json="$(curl -fsSL -H 'Accept: application/vnd.github+json' "$API_URL")"; then @@ -53,15 +44,15 @@ fi download_url="" if [ -n "$json" ]; then - download_url="$(printf '%s\n' "$json" | grep -oE '"browser_download_url":[[:space:]]*"[^"]+\.dmg"' | head -1 | sed -E 's/.*"([^"]+)"$/\1/' || true)" - version="$(printf '%s\n' "$json" | grep -oE '"tag_name":[[:space:]]*"v[^"]+"' | head -1 | sed -E 's/.*"v([^"]+)"$/\1/' || true)" + download_url="$(printf '%s\n' "$json" | jq -r '.assets[] | select(.name | endswith(".dmg")) | .browser_download_url' | head -1 || true)" + version="$(printf '%s\n' "$json" | jq -r '.tag_name | sub("^v"; "")' || true)" fi TMP_DIR="/tmp/ai-traffic-lights-install" rm -rf "$TMP_DIR" && mkdir -p "$TMP_DIR" DMG_PATH="$TMP_DIR/$DMG_NAME" -if [ -n "$download_url" ]; then +if [ -n "$download_url" ] && [ "$download_url" != "null" ]; then info "Baixando versão v${version} de: $download_url" curl -fSL --retry 3 -o "$DMG_PATH" "$download_url" ok "Download concluído." @@ -71,18 +62,21 @@ if [ -n "$download_url" ]; then mkdir -p "$MOUNT_POINT" hdiutil attach -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$DMG_PATH" - # Copiar app substituindo se já existir - rm -rf "/Applications/$APP_NAME" - cp -R "$MOUNT_POINT/$APP_NAME" "/Applications/" - - hdiutil detach "$MOUNT_POINT" - ok "Aplicativo copiado para /Applications/$APP_NAME" + rm -rf "/Applications/${APP_NAME}.tmp" + if cp -R "$MOUNT_POINT/$APP_NAME" "/Applications/${APP_NAME}.tmp"; then + rm -rf "/Applications/$APP_NAME" + mv "/Applications/${APP_NAME}.tmp" "/Applications/$APP_NAME" + hdiutil detach "$MOUNT_POINT" + ok "Aplicativo copiado para /Applications/$APP_NAME" + else + hdiutil detach "$MOUNT_POINT" + die "Falha ao copiar o aplicativo para /Applications." + fi else warn "Nenhuma release .dmg oficial encontrada no repositório GitHub ainda." warn "Se você está compilando localmente, rode 'npm run dist' e copie o app para a pasta /Applications." fi -# Detect if run from inside the repository (development fallback) LOCAL_REPO="" if [ -f "package.json" ] && grep -q '"name": "ai-traffic-lights"' package.json; then LOCAL_REPO="$(pwd)" @@ -97,7 +91,6 @@ if [ -n "$LOCAL_REPO" ]; then ok "Dependências do Node.js instaladas." fi -# ----------------------------- configurar aliases ----------------------------- info "Configurando aliases para inicialização rápida..." if [ -n "$LOCAL_REPO" ]; then @@ -111,12 +104,10 @@ fi setup_profile_aliases() { local profile="$1" if [ -f "$profile" ]; then - # Remover aliases existentes para evitar duplicados sed -i '' '/alias atl=/d' "$profile" 2>/dev/null || true sed -i '' '/alias ai-traffic-lights=/d' "$profile" 2>/dev/null || true echo "" >> "$profile" - echo "# AI Traffic Lights aliases" >> "$profile" echo "$ALIAS_LINE_1" >> "$profile" echo "$ALIAS_LINE_2" >> "$profile" ok "Aliases adicionados em: $profile" @@ -126,7 +117,6 @@ setup_profile_aliases() { setup_profile_aliases "$HOME/.zshrc" setup_profile_aliases "$HOME/.bash_profile" -# ----------------------------- finalização ----------------------------- printf '\n\033[1;32m✓ Configuração concluída!\033[0m\n\n' cat < !!scanPathBin(t)); @@ -531,9 +571,11 @@ function launchAgent({ agent, cwd }) { const term = settingsCfg.terminal === 'auto' ? (availableTerminals()[0] || 'terminal') : settingsCfg.terminal; if (term === 'terminal') { + const escDir = escapeAppleScriptString(dir); + const escPath = escapeAppleScriptString(entry.path); const appleScript = ` tell application "Terminal" - do script "cd '${dir.replace(/'/g, "'\\''")}' && ${entry.path.replace(/'/g, "'\\''")}" + do script "cd " & quoted form of "${escDir}" & " && " & quoted form of "${escPath}" activate end tell `; @@ -542,11 +584,13 @@ function launchAgent({ agent, cwd }) { } if (term === 'iterm2') { + const escDir = escapeAppleScriptString(dir); + const escPath = escapeAppleScriptString(entry.path); const appleScript = ` tell application "iTerm" create window with default profile tell current session of current window - write text "cd '${dir.replace(/'/g, "'\\''")}' && ${entry.path.replace(/'/g, "'\\''")}" + write text "cd " & quoted form of "${escDir}" & " && " & quoted form of "${escPath}" end tell activate end tell @@ -556,12 +600,30 @@ function launchAgent({ agent, cwd }) { } if (term === 'warp') { - try { spawn('open', ['-a', 'Warp', dir], { detached: true, stdio: 'ignore' }).unref(); } catch (e) { notifyUser(`Launch failed: ${e.message}`); } + const warpDir = path.join(process.env.HOME || '/', '.warp', 'launch_configurations'); + try { + fs.mkdirSync(warpDir, { recursive: true }); + const configName = `ai-traffic-lights-${agent}`; + const yamlPath = path.join(warpDir, `${configName}.yaml`); + const yamlContent = [ + `name: AI Traffic Lights - ${agent}`, + `windows:`, + ` - tabs:`, + ` - panes:`, + ` - cwd: ${JSON.stringify(dir)}`, + ` commands:`, + ` - ${JSON.stringify(entry.path)}` + ].join('\n') + '\n'; + fs.writeFileSync(yamlPath, yamlContent, 'utf8'); + spawn('open', [`warp://launch/${configName}`], { detached: true, stdio: 'ignore' }).unref(); + } catch (e) { + notifyUser(`Launch failed: ${e.message}`); + } return; } if (term === 'ghostty') { - try { spawn('open', ['-a', 'Ghostty', '--args', `--working-directory=${dir}`, entry.path], { detached: true, stdio: 'ignore' }).unref(); } catch (e) { notifyUser(`Launch failed: ${e.message}`); } + try { spawn('open', ['-a', 'Ghostty', '--args', `--working-directory=${dir}`, `--initial-command=${entry.path}`], { detached: true, stdio: 'ignore' }).unref(); } catch (e) { notifyUser(`Launch failed: ${e.message}`); } return; } } @@ -1216,31 +1278,49 @@ function glmCredsFromSessions() { // Dedup por token. Nunca lança; só lê o que o dono consegue (EACCES → skip). function glmCredsFromProc() { const byToken = new Map(); - let pids = []; if (process.platform === 'darwin') { try { - const output = execFileSync('ps', ['-ax', '-o', 'pid='], { encoding: 'utf8', timeout: 2000 }); - pids = output.split('\n').map(l => parseInt(l.trim(), 10)).filter(n => !isNaN(n)); + const output = execFileSync('ps', ['-ax', '-E', '-o', 'pid=,args='], { encoding: 'utf8', timeout: 3000 }); + for (const line of output.split('\n')) { + const trimmed = line.trim(); + if (!trimmed) continue; + const m = trimmed.match(/^(\d+)\s+(.+)$/); + if (!m) continue; + const content = m[2]; + const rawEnv = parseMacOSEnviron(content); + const env = usage.parseEnviron(rawEnv, ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN']); + if (!env.ANTHROPIC_BASE_URL || !env.ANTHROPIC_AUTH_TOKEN) continue; + if (!/api\.z\.ai|bigmodel\.cn/.test(env.ANTHROPIC_BASE_URL)) continue; + const token = env.ANTHROPIC_AUTH_TOKEN; + if (byToken.has(token)) continue; + let suffix; + try { suffix = crypto_().createHash('sha256').update(token).digest('hex').slice(0, 6); } + catch { suffix = String(byToken.size + 1); } + let label = ''; + try { label = new URL(env.ANTHROPIC_BASE_URL).host.replace(/^api\./, ''); } catch {} + byToken.set(token, { env, label, suffix }); + if (byToken.size >= 2) break; + } } catch { return []; } } else { + let pids = []; try { pids = fs.readdirSync('/proc').filter((d) => /^\d+$/.test(d)).map(n => parseInt(n, 10)); } catch { return []; } - } - for (const pid of pids) { - let raw; - try { raw = getProcessEnviron(pid); } catch { continue; } // morto ou sem permissão - const env = usage.parseEnviron(raw, ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN']); - if (!env.ANTHROPIC_BASE_URL || !env.ANTHROPIC_AUTH_TOKEN) continue; - if (!/api\.z\.ai|bigmodel\.cn/.test(env.ANTHROPIC_BASE_URL)) continue; // só backend GLM - const token = env.ANTHROPIC_AUTH_TOKEN; - if (byToken.has(token)) continue; - let suffix; - try { suffix = crypto_().createHash('sha256').update(token).digest('hex').slice(0, 6); } - catch { suffix = String(byToken.size + 1); } - let label = ''; - try { label = new URL(env.ANTHROPIC_BASE_URL).host.replace(/^api\./, ''); } catch {} - byToken.set(token, { env, label, suffix }); - // bastam 2 contas distintas — não precisa varrer os ~1000 processos todos - if (byToken.size >= 2) break; + for (const pid of pids) { + let raw; + try { raw = getProcessEnviron(pid); } catch { continue; } + const env = usage.parseEnviron(raw, ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN']); + if (!env.ANTHROPIC_BASE_URL || !env.ANTHROPIC_AUTH_TOKEN) continue; + if (!/api\.z\.ai|bigmodel\.cn/.test(env.ANTHROPIC_BASE_URL)) continue; + const token = env.ANTHROPIC_AUTH_TOKEN; + if (byToken.has(token)) continue; + let suffix; + try { suffix = crypto_().createHash('sha256').update(token).digest('hex').slice(0, 6); } + catch { suffix = String(byToken.size + 1); } + let label = ''; + try { label = new URL(env.ANTHROPIC_BASE_URL).host.replace(/^api\./, ''); } catch {} + byToken.set(token, { env, label, suffix }); + if (byToken.size >= 2) break; + } } return [...byToken.values()]; } diff --git a/src/settings.js b/src/settings.js index f726f7c..fdd71cc 100644 --- a/src/settings.js +++ b/src/settings.js @@ -82,7 +82,7 @@ function mergeWithDefaults(raw) { if (typeof raw.soundFile === 'string' && raw.soundFile.length <= 4096) out.soundFile = raw.soundFile; if (isValidShortcut(raw.shortcut)) out.shortcut = raw.shortcut; if (raw.lang === 'auto' || raw.lang === 'en' || raw.lang === 'pt') out.lang = raw.lang; - const TERMINAL_OK = new Set(['auto', 'tilix', 'gnome-terminal', 'ghostty', 'custom']); + const TERMINAL_OK = new Set(['auto', 'tilix', 'gnome-terminal', 'ghostty', 'iterm2', 'terminal', 'warp', 'custom']); if (TERMINAL_OK.has(raw.terminal)) out.terminal = raw.terminal; if (typeof raw.terminalCmd === 'string' && raw.terminalCmd.length <= 1000) out.terminalCmd = raw.terminalCmd; // launchers: só strings (paths), chaves curtas — ignorado se malformado. From 6d1bbb618a75145ee8d5bed05870b50696b7c0d7 Mon Sep 17 00:00:00 2001 From: b92c Date: Sat, 11 Jul 2026 19:26:50 -0300 Subject: [PATCH 3/5] chore: change language from PT-BR to EN --- install_macos.sh | 52 ++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/install_macos.sh b/install_macos.sh index 9b7723a..2d4a45a 100755 --- a/install_macos.sh +++ b/install_macos.sh @@ -10,36 +10,36 @@ API_URL="https://api.github.com/repos/${REPO}/releases/latest" info() { printf '\033[1;34m›\033[0m %s\n' "$*"; } ok() { printf '\033[1;32m✓\033[0m %s\n' "$*"; } -warn() { printf '\033[1;33m⚠️\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m!\033[0m %s\n' "$*"; } die() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; exit 1; } OS="$(uname -s)" ARCH="$(uname -m)" if [ "$OS" != "Darwin" ]; then - die "Este instalador é exclusivo para macOS. SO atual: $OS" + die "This installer is exclusive to macOS. Current OS: $OS" fi if [ "$ARCH" != "arm64" ]; then - warn "Sua arquitetura é $ARCH. Este build é otimizado para Apple Silicon (M1, M2, M3, M4, M5)." + warn "Your architecture is $ARCH. This build is optimized for Apple Silicon (M1, M2, M3, M4, M5)." fi if ! command -v brew >/dev/null 2>&1; then - die "Homebrew não encontrado. Por favor, instale o Homebrew em https://brew.sh e tente novamente." + die "Homebrew not found. Please install Homebrew at https://brew.sh and try again." fi if ! command -v jq >/dev/null 2>&1; then - info "Instalando dependência 'jq' via Homebrew..." + info "Installing dependency 'jq' via Homebrew..." brew install jq - ok "'jq' instalado com sucesso." + ok "'jq' successfully installed." else - ok "'jq' já está instalado." + ok "'jq' is already installed." fi -info "Consultando a versão mais recente para macOS no GitHub..." +info "Checking for the latest macOS release on GitHub..." json="" if ! json="$(curl -fsSL -H 'Accept: application/vnd.github+json' "$API_URL")"; then - warn "Não foi possível conectar à API do GitHub." + warn "Could not connect to the GitHub API." fi download_url="" @@ -53,11 +53,11 @@ rm -rf "$TMP_DIR" && mkdir -p "$TMP_DIR" DMG_PATH="$TMP_DIR/$DMG_NAME" if [ -n "$download_url" ] && [ "$download_url" != "null" ]; then - info "Baixando versão v${version} de: $download_url" + info "Downloading version v${version} from: $download_url" curl -fSL --retry 3 -o "$DMG_PATH" "$download_url" - ok "Download concluído." + ok "Download complete." - info "Montando instalador e copiando aplicativo para /Applications..." + info "Mounting installer and copying application to /Applications..." MOUNT_POINT="$TMP_DIR/mount" mkdir -p "$MOUNT_POINT" hdiutil attach -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$DMG_PATH" @@ -67,14 +67,14 @@ if [ -n "$download_url" ] && [ "$download_url" != "null" ]; then rm -rf "/Applications/$APP_NAME" mv "/Applications/${APP_NAME}.tmp" "/Applications/$APP_NAME" hdiutil detach "$MOUNT_POINT" - ok "Aplicativo copiado para /Applications/$APP_NAME" + ok "Application copied to /Applications/$APP_NAME" else hdiutil detach "$MOUNT_POINT" - die "Falha ao copiar o aplicativo para /Applications." + die "Failed to copy the application to /Applications." fi else - warn "Nenhuma release .dmg oficial encontrada no repositório GitHub ainda." - warn "Se você está compilando localmente, rode 'npm run dist' e copie o app para a pasta /Applications." + warn "No official .dmg release found on GitHub repository yet." + warn "If compiling locally, run 'npm run dist' and copy the app to /Applications." fi LOCAL_REPO="" @@ -84,14 +84,14 @@ fi if [ -n "$LOCAL_REPO" ]; then if ! command -v npm >/dev/null 2>&1; then - die "Instalação em modo desenvolvimento detectada, mas 'npm' não foi encontrado. Por favor, instale o Node.js e tente novamente." + die "Development installation detected, but 'npm' was not found. Please install Node.js and try again." fi - info "Executando em modo desenvolvimento. Instalando dependências do Node.js..." + info "Running in development mode. Installing Node.js dependencies..." (cd "$LOCAL_REPO" && npm install) - ok "Dependências do Node.js instaladas." + ok "Node.js dependencies installed." fi -info "Configurando aliases para inicialização rápida..." +info "Configuring aliases for quick start..." if [ -n "$LOCAL_REPO" ]; then ALIAS_LINE_1="alias atl=\"[ -d '/Applications/AI Traffic Lights.app' ] && open -a '$APP_TITLE' || npm start --prefix '$LOCAL_REPO'\"" @@ -110,21 +110,21 @@ setup_profile_aliases() { echo "" >> "$profile" echo "$ALIAS_LINE_1" >> "$profile" echo "$ALIAS_LINE_2" >> "$profile" - ok "Aliases adicionados em: $profile" + ok "Aliases added to: $profile" fi } setup_profile_aliases "$HOME/.zshrc" setup_profile_aliases "$HOME/.bash_profile" -printf '\n\033[1;32m✓ Configuração concluída!\033[0m\n\n' +printf '\n\033[1;32m✓ Configuration complete!\033[0m\n\n' cat < Date: Sat, 11 Jul 2026 19:39:13 -0300 Subject: [PATCH 4/5] fix(package.json): fix build target configuration syntax error --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 772d10d..aafa689 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "--disable-dev-shm-usage", "--ozone-platform=x11" ] + }, "appImage": { "artifactName": "${name}-${version}.${ext}" }, From 07fd36bab1bd209cd6e9b64cc18f93843b3510d8 Mon Sep 17 00:00:00 2001 From: b92c Date: Mon, 13 Jul 2026 21:57:35 -0300 Subject: [PATCH 5/5] feat: add macOS compatibility to shell hooks and robust process name parsing --- .gitignore | 3 +++ hooks/traffic-hook.sh | 50 ++++++++++++++++++++++++++++--------------- main.js | 3 +++ package-lock.json | 4 ++-- 4 files changed, 41 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index e722236..958f4c3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ graphify-out/ # lixo de redirecionamento acidental — builtin `cd` com output redirecionado cria um arquivo "cd" /cd +AGENTS.md +GEMINI.md +CLAUDE.md diff --git a/hooks/traffic-hook.sh b/hooks/traffic-hook.sh index 94e09ba..31c3d90 100755 --- a/hooks/traffic-hook.sh +++ b/hooks/traffic-hook.sh @@ -97,23 +97,37 @@ main() { ntype="${BASH_REMATCH[1]}" fi - # Sobe a árvore até achar o processo do agente. Zero forks. - # claude: binário próprio (comm=claude). antigravity/gemini: script Node (comm=node) ou binário agy/antigravity — - # o PRIMEIRO ancestral é o agente. codex: binário Rust (comm=codex). + # Sobe a árvore até achar o processo do agente. Zero forks no Linux. + # No macOS (sem /proc), usamos ps. local agent_pid=$$ pid=$$ comm="" ppid="" - while [ "${pid:-0}" -gt 1 ] 2>/dev/null; do - comm="" - read -r comm < "/proc/$pid/comm" 2>/dev/null - case "$AGENT:$comm" in - claude:claude|claude:claude-agent-acp|gemini:node|antigravity:node|antigravity:agy|antigravity:antigravity|codex:codex) agent_pid="$pid"; break ;; - esac - ppid="" - while IFS=$' \t' read -r k v; do - [ "$k" = "PPid:" ] && { ppid="$v"; break; } - done < "/proc/$pid/status" 2>/dev/null - [ -z "$ppid" ] && break - pid="$ppid" - done + if [ -d "/proc" ]; then + while [ "${pid:-0}" -gt 1 ] 2>/dev/null; do + comm="" + read -r comm < "/proc/$pid/comm" 2>/dev/null + case "$AGENT:$comm" in + claude:claude|claude:claude-agent-acp|gemini:node|antigravity:node|antigravity:agy|antigravity:antigravity|codex:codex) agent_pid="$pid"; break ;; + esac + ppid="" + while IFS=$' \t' read -r k v; do + [ "$k" = "PPid:" ] && { ppid="$v"; break; } + done < "/proc/$pid/status" 2>/dev/null + [ -z "$ppid" ] && break + pid="$ppid" + done + else + while [ "${pid:-0}" -gt 1 ] 2>/dev/null; do + local ps_out + ps_out=$(ps -p "$pid" -o ppid=,comm= 2>/dev/null) + ppid="" comm="" + read -r ppid comm <<< "$ps_out" + comm=$(basename "$comm") + case "$AGENT:$comm" in + claude:claude|claude:claude-agent-acp|gemini:node|antigravity:node|antigravity:agy|antigravity:antigravity|codex:codex) agent_pid="$pid"; break ;; + esac + [ -z "$ppid" ] && break + pid="$ppid" + done + fi # Canais nativos de foco de aba (invisível pro X11; só o terminal alcança): # Warp → WARP_FOCUS_URL (warp://session/) via xdg-open @@ -133,7 +147,9 @@ main() { fi local ts - printf -v ts '%(%s)T' -1 # epoch fork-free (bash 4.2+) + if ! printf -v ts '%(%s)T' -1 2>/dev/null || [ -z "${ts:-}" ]; then + ts=$(date +%s) + fi [ -d "$STATE_DIR" ] || mkdir -p "$STATE_DIR" 2>/dev/null || return 0 local file="$STATE_DIR/${sid}.json" diff --git a/main.js b/main.js index c58c8f7..b442bb1 100644 --- a/main.js +++ b/main.js @@ -209,6 +209,8 @@ function discoverAgentProcs() { pcomm = path.basename(parentOutput); } catch {} + if (pcomm.startsWith('-')) pcomm = pcomm.slice(1); + if (SHELLS.has(pcomm)) { found.push({ pid, agent }); } @@ -233,6 +235,7 @@ function discoverAgentProcs() { if (!m) continue; let pcomm = ''; try { pcomm = fs.readFileSync(`/proc/${m[1]}/comm`, 'utf8').trim(); } catch {} + if (pcomm.startsWith('-')) pcomm = pcomm.slice(1); if (SHELLS.has(pcomm)) found.push({ pid: parseInt(ent, 10), agent }); } catch {} } diff --git a/package-lock.json b/package-lock.json index 3b5d8e2..b68e3a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ai-traffic-lights", - "version": "0.6.7", + "version": "0.6.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ai-traffic-lights", - "version": "0.6.7", + "version": "0.6.8", "license": "MIT", "dependencies": { "chokidar": "^3.6.0",