Repository navigation
feat(watch): health check — show running watch instance status (#808) #809
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b1b78e1
ff66f51
9390fa7
f2c734b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| --- | ||
| "@bradygaster/squad-cli": minor | ||
| --- | ||
|
|
||
| feat(watch): health check — show running watch instance status (#808) | ||
|
|
||
| Adds `squad watch --health` to display the status of a running watch | ||
| instance: PID, uptime, auth account, capabilities, and auth drift | ||
| detection. Writes `.squad/.watch-pid.json` at startup for instance | ||
| tracking. Detects and cleans up stale PID files from crashed instances. | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,153 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Watch health check — reports status of a running watch instance. | ||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||
| * Reads the PID file (stored in OS temp dir) written at watch startup to | ||||||||||||||||||||||||||||||||||||||||||||||
| * determine whether a watch process is alive, its uptime, auth account, and | ||||||||||||||||||||||||||||||||||||||||||||||
| * whether auth has drifted since launch. | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import fs from 'node:fs'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import path from 'node:path'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import os from 'node:os'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import crypto from 'node:crypto'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { execFileSync } from 'node:child_process'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** Shape of the PID file written by runWatch at startup. */ | ||||||||||||||||||||||||||||||||||||||||||||||
| export interface WatchPidInfo { | ||||||||||||||||||||||||||||||||||||||||||||||
| pid: number; | ||||||||||||||||||||||||||||||||||||||||||||||
| startedAt: string; | ||||||||||||||||||||||||||||||||||||||||||||||
| user: string; | ||||||||||||||||||||||||||||||||||||||||||||||
| interval: number; | ||||||||||||||||||||||||||||||||||||||||||||||
| capabilities: string[]; | ||||||||||||||||||||||||||||||||||||||||||||||
| repo: string; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Path to the PID file in the OS temp directory. | ||||||||||||||||||||||||||||||||||||||||||||||
| * Uses an MD5 hash of the repo path so different clones/worktrees get | ||||||||||||||||||||||||||||||||||||||||||||||
| * unique PID files without polluting the repo (avoids accidental commits). | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| export function getPidPath(teamRoot: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||
| const hash = crypto.createHash('md5').update(teamRoot).digest('hex').slice(0, 12); | ||||||||||||||||||||||||||||||||||||||||||||||
| return path.join(os.tmpdir(), `squad-watch-${hash}.json`); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Write the PID file at watch startup. | ||||||||||||||||||||||||||||||||||||||||||||||
| * The caller is responsible for registering exit handlers to clean up. | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| export function writePidFile(teamRoot: string, info: WatchPidInfo): void { | ||||||||||||||||||||||||||||||||||||||||||||||
| const pidPath = getPidPath(teamRoot); | ||||||||||||||||||||||||||||||||||||||||||||||
| fs.writeFileSync(pidPath, JSON.stringify(info, null, 2)); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** Remove the PID file (best-effort, swallows errors). */ | ||||||||||||||||||||||||||||||||||||||||||||||
| export function removePidFile(teamRoot: string): void { | ||||||||||||||||||||||||||||||||||||||||||||||
| try { fs.unlinkSync(getPidPath(teamRoot)); } catch { /* already gone */ } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||
| * Check if a process with the given PID is still alive. | ||||||||||||||||||||||||||||||||||||||||||||||
| * Uses `process.kill(pid, 0)` — signal 0 tests existence without killing. | ||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||
| export function isProcessAlive(pid: number): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| process.kill(pid, 0); | ||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+52
to
+58
|
||||||||||||||||||||||||||||||||||||||||||||||
| */ | |
| export function isProcessAlive(pid: number): boolean { | |
| try { | |
| process.kill(pid, 0); | |
| return true; | |
| } catch { | |
| return false; | |
| * `EPERM` means the process exists but cannot be signaled by this user. | |
| */ | |
| export function isProcessAlive(pid: number): boolean { | |
| try { | |
| process.kill(pid, 0); | |
| return true; | |
| } catch (error: unknown) { | |
| const err = error as NodeJS.ErrnoException; | |
| if (err.code === 'EPERM') { | |
| return true; | |
| } | |
| if (err.code === 'ESRCH') { | |
| return false; | |
| } | |
| throw error; |
Copilot
AI
Apr 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probeCurrentGhUser() has the same stdout/stderr issue as getActiveGhUser(): execFileSync() does not include stderr when the command succeeds. If gh auth status --active prints the active account to stderr with exit code 0, auth drift detection here will silently fail. Consider switching to spawnSync() and merging outputs (or using a more reliable gh api query).
Copilot
AI
Apr 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
startedAt is not validated before computing uptime. If the PID file is missing startedAt or contains an invalid timestamp, new Date(info.startedAt).getTime() becomes NaN and the output will show NaNm. Consider validating startedAt and falling back to an "unknown" uptime (or treating the PID file as invalid).
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |||
| * always runs — it is not an opt-in capability. | ||||
| */ | ||||
|
|
||||
| import fs from 'node:fs'; | ||||
|
||||
| import fs from 'node:fs'; |
Copilot
AI
Apr 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
execFileSync() only returns stdout; the comment says it captures stderr too, but stderr output from a successful gh auth status invocation won't be included in result. If gh writes the account info to stderr with exit code 0, this will fail to detect the active user—consider using spawnSync() and combining stdout+stderr (or call gh api user -q .login).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changeset claims the CLI adds
squad watch --health, but there is currently no argument parsing/dispatch for--healthinpackages/squad-cli/src/cli-entry.ts(and no other CLI router references it). Either wire up the flag/command as described, or adjust the release note to match the implemented API surface.