Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,24 @@ jobs:
working-directory: dist
run: echo -n "${{ github.sha }}" > COMMIT

- name: Extract changelog section for this tag
run: |
VERSION="${GITHUB_REF_NAME#v}"
awk -v ver="## [$VERSION]" '
$0 == ver { found=1; next }
found && /^## \[/ { exit }
found { print }
' CHANGELOG.md > release-notes.md

if [ ! -s release-notes.md ]; then
echo "No CHANGELOG.md section found for $VERSION" >&2
exit 1
fi

- uses: softprops/action-gh-release@v3
with:
body_path: release-notes.md
files: |
dist/herdr-devserver-status-*.tar.gz
dist/SHA256SUMS
dist/COMMIT
generate_release_notes: true
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
*.log
.env
.vscode/
!.vscode/extensions.json
!.vscode/extensions.json
.seed-state.json
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Changelog

## [0.2.0]

### Fixed

- `has_errors` stuck `true` after recovery once the error needle scrolled out of the read window — recompute every `Running` tick instead of gating on needle presence.
- `status` regressed `Running` → `Starting` once the `Ready` needle/URL scrolled out of the read window — `Running` now sticky, cleared only on process restart.

### Changed

- `ToolDetector::match_output` — added `previous_status: ToolStatus` param.
- `signal_matching::derive_status` — added `previous_status: ToolStatus` param.

## [0.1.0]

### Added
- initial release: framework spec loader, vite detector, daemon discovery/worker loop
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "herdr-devserver-status"
version = "0.1.0"
version = "0.2.0"
edition = "2024"
rust-version = "1.97"
license = "MIT"
Expand Down
3 changes: 3 additions & 0 deletions frameworks/vite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ signals:
- kind: ready
needle: "built in"
counts_as_recent_success: true
- kind: ready
needle: "(client) hmr update"
counts_as_recent_success: true
- kind: building
needle: "build started..."
- kind: error
Expand Down
2 changes: 1 addition & 1 deletion herdr-plugin.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
id = "herdr-devserver-status"
name = "herdr-devserver-status"
version = "0.1.0"
version = "0.2.0"
min_herdr_version = "0.7.0"
description = "Detects selected dev servers running inside Herdr panes and reports their lifecycle status."
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion src/daemon/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ fn tick(
state: &mut TrackedState,
) -> Result<(), crate::herdr::HerdrError> {
let output = client::read_pane_output(pane_id, READ_LINES)?;
let result = detector.match_output(&output, state.has_errors);
let result = detector.match_output(&output, state.status, state.has_errors);

let changed = result.status != state.status
|| result.has_errors != state.has_errors
Expand Down
16 changes: 16 additions & 0 deletions src/herdr/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ pub fn extract_foreground_processes(parsed: &Value) -> Vec<ProcessInfo> {
/// from that stderr JSON is UNVERIFIED — empirically verify against a pane
/// that closes mid-wait before shipping.
pub fn stderr_indicates_timeout(stderr_json: &Value) -> bool {
if stderr_json
.pointer("/error/code")
.and_then(Value::as_str)
.map(|c| c.eq_ignore_ascii_case("timeout"))
.unwrap_or(false)
{
return true;
}
const TIMEOUT_HINT_KEYS: [&str; 2] = ["timeout", "timed_out"];
for key in TIMEOUT_HINT_KEYS {
if let Some(v) = stderr_json.get(key)
Expand All @@ -98,3 +106,11 @@ pub fn stderr_indicates_timeout(stderr_json: &Value) -> bool {
.map(|r| r.eq_ignore_ascii_case("timeout"))
.unwrap_or(false)
}

#[test]
fn recognizes_nested_error_code_timeout() {
let json: Value = serde_json::from_str(
r#"{"error":{"code":"timeout","message":"timed out waiting for output match"},"id":"cli:pane:wait-output"}"#
).unwrap();
assert!(stderr_indicates_timeout(&json));
}
15 changes: 8 additions & 7 deletions src/tools/framework/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,16 @@ impl ToolDetector for FrameworkDetector {
&self.signal_re
}

fn match_output(&self, output: &str, previous_had_errors: bool) -> ToolMatchResult {
fn match_output(
&self,
output: &str,
previous_status: ToolStatus,
previous_had_errors: bool,
) -> ToolMatchResult {
let result =
signal_matching::match_output(&self.spec.signals, self.url_re.as_ref(), output);
let derived = signal_matching::derive_status(previous_had_errors, &result);

let has_errors = if derived.status == ToolStatus::Running
&& result.signals.contains(&SignalKind::Ready)
&& result.signals.contains(&SignalKind::Error)
{
let derived = signal_matching::derive_status(previous_status, previous_had_errors, &result);
let has_errors = if derived.status == ToolStatus::Running {
signal_matching::has_recent_error(&self.spec.signals, output)
} else {
derived.has_errors
Expand Down
7 changes: 6 additions & 1 deletion src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ pub trait ToolDetector: Send + Sync {
fn confirm(&self, procs: &[ProcessInfo]) -> Option<ProcessMatch>;

fn signal_regex(&self) -> &Regex;
fn match_output(&self, output: &str, previous_had_errors: bool) -> ToolMatchResult;
fn match_output(
&self,
output: &str,
previous_status: ToolStatus,
previous_had_errors: bool,
) -> ToolMatchResult;
}

/// All detectors tried, in order, against each newly seen pane's process
Expand Down
14 changes: 9 additions & 5 deletions src/tools/signal_matching.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,20 @@ pub struct DerivedStatus {
pub has_errors: bool,
}

pub fn derive_status(previous_had_errors: bool, result: &MatchResult) -> DerivedStatus {
let is_running = result.signals.contains(&SignalKind::Ready) || result.url.is_some();
pub fn derive_status(
previous_status: ToolStatus,
previous_had_errors: bool,
result: &MatchResult,
) -> DerivedStatus {
let is_running = previous_status == ToolStatus::Running
|| result.signals.contains(&SignalKind::Ready)
|| result.url.is_some();
let saw_error = result.signals.contains(&SignalKind::Error);

if is_running {
// Caller overrides with has_recent_error when both ready and error
// are present in the same read.
DerivedStatus {
status: ToolStatus::Running,
has_errors: false,
has_errors: previous_had_errors,
}
} else {
DerivedStatus {
Expand Down