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
5 changes: 5 additions & 0 deletions Cargo.lock

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

8 changes: 3 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ path = ".github/packaging/winget/main.rs"
test = true

[dependencies]
chrono = "0.4.41"
chrono = { version = "0.4.41", features = ["serde"] }
clap = { version = "4.5.60", features = ["derive"] }
darkbio-crypto = { version = "0.18.2", features = ["cbor", "xdsa"] }
darkbio-trust = { version = "0.5.1", features = ["release", "staging", "develop"] }
Expand All @@ -43,17 +43,15 @@ qrcode = { version = "0.14", default-features = false }
ureq = { version = "3.4", default-features = false, features = ["rustls"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
semver = { version = "1.0.28", features = ["serde"] }
sha2 = "0.10"
thiserror = "2.0"
tracing = "0.1"
tracing-subscriber = "0.3.23"
tungstenite = { version = "0.30", features = ["rustls-tls-webpki-roots"] }

[dev-dependencies]
semver = "1"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_System_Console"] }
windows-sys = { version = "0.61", features = ["Win32_System_Console", "Win32_System_Threading"] }

[target.'cfg(unix)'.dependencies]
signal-hook = "0.3"
Expand Down
42 changes: 41 additions & 1 deletion src/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

//! Independent diagnostics composed from connection primitives.

use crate::{context::Context, error::Error, firmware::Packages};
use crate::{context::Context, error::Error, firmware::Packages, update};
use darkbio_connect::schema;
use serde_json::{Value, json};

Expand All @@ -28,6 +28,7 @@ pub(crate) fn run(context: &Context) -> Result<(), Error> {
darkbio_connect::wire::VERSION,
),
);
checks.update();
let mut discovered = 0;
for (name, result) in [
("usb", darkbio_connect::hardware::list()),
Expand Down Expand Up @@ -197,10 +198,49 @@ struct Checks<'a> {
failure: Option<Error>,
}
impl Checks<'_> {
/// Looks up the newest ark afresh. A newer one is a warn and a failed
/// lookup a skip, so neither sets the exit code.
fn update(&mut self) {
// Under CI nothing is looked up
if update::disabled() {
self.skip("update", "CI is set");
return;
}

// Look up within --timeout, keeping the answer for later commands
let running = update::running();
let channel = update::Channel::for_version(&running);
let asked = chrono::Utc::now();
let result = update::refresh(
&crate::data::cache::directory(),
channel,
asked,
std::time::Duration::from_secs(self.context.options.timeout),
);

// Only a newer version needs action; an unpublished local build is current too
match result {
Ok(newest) if newest.cmp_precedence(&running).is_gt() => self.warn(
"update",
&format!("ark {newest} is available, this is {running}"),
&update::hint(channel),
),
Ok(_) => self.ok(
"update",
&format!("ark {running} is the newest {}", channel.description()),
),
Err(error) => self.skip("update", error),
}
}

/// Records a successful diagnostic with its observed detail.
fn ok(&mut self, name: &str, detail: &str) {
self.add(name, "ok", detail, None);
}
/// Records something to act on with its hint, never failing the command.
fn warn(&mut self, name: &str, detail: &str, hint: &str) {
self.add(name, "warn", detail, Some(hint));
}
/// Records an unmet prerequisite without making the command fail by itself.
fn skip(&mut self, name: &str, detail: &str) {
self.add(name, "skip", detail, None);
Expand Down
2 changes: 1 addition & 1 deletion src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ fn decorate(command: &mut clap::Command, parent: &str, theme: &Theme) {
"nothing; unavailable checks are skipped",
"none; develop and staging package hosts may need browser login",
"seconds per check",
"checks: result, name, detail, hint; JSON: tool, connect, wire, minimum_firmware, minimum_develop_publish, checks",
"checks: result (ok, warn, fail or skip), name, detail, hint; JSON: tool, connect, wire, minimum_firmware, minimum_develop_publish, checks",
"ark doctor\nark doctor --json",
),
"completions" => (
Expand Down
14 changes: 9 additions & 5 deletions src/help/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ on their phone, in Ark Companion. You cannot approve for them.

## Reading results

An approve event means the owner needs to act. CLI error codes are stable;
`ark help output` lists them with next steps. error[ark]: passes through the
Ark's own verdict; read its message, never match its number or wording.
Partial results survive errors. JSON also preserves the latest partial result
on interruption.
An approve event means the owner needs to act. A note that a newer ark is
available names the upgrade and repeats on every command until it happens, so
pass it on to the person; upgrading is their call. doctor reports the same as
its update check, and a nonempty CI turns the note off.

CLI error codes are stable; `ark help output` lists them with next steps.
error[ark]: passes through the Ark's own verdict; read its message, never
match its number or wording. Partial results survive errors. JSON also
preserves the latest partial result on interruption.

Without --json, app reports stream raw to stdout. The app's own stderr is
announced, then written unprefixed. With --json both app streams are in the
Expand Down
23 changes: 20 additions & 3 deletions src/help/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ one key, error, with the fields listed under Error codes:

-q drops progress, notes, warnings and steps. Errors, hints, owner approval
instructions, diagnostic logs and app output remain. -v enables step narration.
--log debug enables connect diagnostics; --log trace enables connect and wire
traces. They are independent of -v. HTTP and subprocess log targets are excluded
so authorization headers and package login credentials cannot enter the stream.
--log debug enables update and connect diagnostics; --log trace adds wire
traces. They are independent of -v. HTTP and subprocess log targets are
excluded so authorization headers and package login credentials cannot enter
the stream.

Terminal progress refreshes once a second, showing new steps and completion at
once. Redirected progress and JSON report at ten-percent boundaries or every
Expand All @@ -70,6 +71,22 @@ error events carry an error object. Progress messages describe transfers,
processing phases or elapsed time. Their wording is for reading, not a
structured progress API.

## New releases

At most once an hour, ark asks GitHub for its newest release. A release build
reads the redirect at https://github.com/dark-bio/cli/releases/latest, and a
development build reads the release list from api.github.com. The request
carries nothing about this computer, its Arks or the running version. It runs
in a detached copy of ark that exits within 30 s, so no command waits for it.
The answer is kept in update.json in ark's cache directory, and a nonempty CI
turns the lookup off.

While the kept answer names a newer version, every command except help,
completions, --version and doctor starts with a note naming both versions and
how to upgrade. Under --json it is an ordinary note event. The note never
changes the result or the exit code, and -q hides it. doctor looks up afresh
and reports the answer as its update check.

## Error codes

The code in error[code]: is stable and its exit code is the class below. This
Expand Down
34 changes: 23 additions & 11 deletions src/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//! Only connect and wire diagnostics enter the CLI's log stream. HTTP and
//! subprocess logging is excluded so authorization headers cannot appear.
//! Only update, connect and wire diagnostics enter the CLI's log stream. HTTP
//! and subprocess logging is excluded so authorization headers cannot appear.

use crate::{args::Log as Level, output::Output};
use serde_json::{Map, Value, json};
Expand All @@ -29,22 +29,27 @@ pub(crate) fn init(output: Output, verbose: bool, level: Option<Level>) {
.try_init();
}

/// Rejects every target outside connect and wire, regardless of diagnostic level.
/// Rejects every target outside update, connect and wire, regardless of diagnostic level.
fn enabled(target: &str, severity: tracing::Level, verbose: bool, level: Option<Level>) -> bool {
if target == "darkbio_connect::setup" {
return verbose;
}
match level {
Some(Level::Debug) => {
(target == "darkbio_connect" || target.starts_with("darkbio_connect::"))
(target == "ark::update"
|| target == "darkbio_connect"
|| target.starts_with("darkbio_connect::"))
&& severity <= tracing::Level::DEBUG
}
Some(Level::Trace) => ["darkbio_connect", "darkbio_wire"].iter().any(|name| {
target == *name
|| target
.strip_prefix(name)
.is_some_and(|suffix| suffix.starts_with("::"))
}),
Some(Level::Trace) => {
target == "ark::update"
|| ["darkbio_connect", "darkbio_wire"].iter().any(|name| {
target == *name
|| target
.strip_prefix(name)
.is_some_and(|suffix| suffix.starts_with("::"))
})
}
None => false,
}
}
Expand Down Expand Up @@ -109,10 +114,16 @@ impl Visit for Fields {
mod tests {
use super::*;

/// Diagnostic selection includes update failures without exposing HTTP or subprocess logs.
#[test]
fn diagnostics_are_separate_from_steps_and_exclude_http_and_subprocesses() {
fn test_diagnostics_are_separate_from_steps_and_exclude_http_and_subprocesses() {
for level in [None, Some(Level::Debug), Some(Level::Trace)] {
for verbose in [false, true] {
assert_eq!(
enabled("ark::update", tracing::Level::DEBUG, verbose, level),
level.is_some(),
"{level:?}, verbose={verbose}"
);
assert_eq!(
enabled(
"darkbio_connect::setup",
Expand All @@ -127,6 +138,7 @@ mod tests {
"hyper::client",
"rustls",
"ark::access",
"ark::update_http",
"std::process",
"darkbio_connect_http",
] {
Expand Down
16 changes: 16 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod output;
mod pairing;
mod progress;
mod style;
mod update;

use args::{Cli, Command};
use clap::{FromArgMatches, Parser};
Expand All @@ -35,6 +36,10 @@ use std::process::ExitCode;
/// Help and usage failures honor stream formatting even before typed parsing succeeds.
fn main() -> ExitCode {
let arguments: Vec<_> = std::env::args_os().collect();
if arguments.len() == 2 && arguments[1] == update::ENTRY_POINT {
update::run();
return ExitCode::SUCCESS;
}
let json = arguments
.iter()
.skip(1)
Expand Down Expand Up @@ -89,6 +94,17 @@ fn main() -> ExitCode {
output,
interrupt,
};
// Valid commands print the release note, except help, completions, --version, a bare run and doctor
if validation.is_ok()
&& !cli.help
&& !cli.version
&& !matches!(
&cli.command,
None | Some(Command::Help { .. } | Command::Completions { .. } | Command::Doctor)
)
{
update::start(&context.output, chrono::Utc::now());
}
let result = if let Err(error) = validation {
Err(error)
} else if cli.help {
Expand Down
6 changes: 4 additions & 2 deletions src/output/human.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,17 +459,19 @@ mod tests {
);
}

/// Every result carries its own mark, and each hint stays under its check.
#[test]
fn checklist_keeps_skips_explicit_and_hints_local() {
fn test_checklist_keeps_skips_explicit_and_hints_local() {
let theme = Theme::test(80, Color::Basic, true);
let rows = [
json!({"name":"usb","result":"ok","detail":"1 device found","hint":null}),
json!({"name":"relay","result":"fail","detail":"no answer","hint":"open `Ark Companion`"}),
json!({"name":"slots","result":"skip","detail":"Ark locked","hint":null}),
json!({"name":"tool","result":"warn","detail":"new release","hint":"run `brew upgrade ark-cli`"}),
];
assert_eq!(
checklist(&theme, &rows),
" \x1b[1m\u{2713} usb\x1b[0m 1 device found\n \x1b[1m\u{2717} relay\x1b[0m no answer\n \x1b[1mhint:\x1b[0m open \x1b[1mArk Companion\x1b[0m\n \u{00b7} slots skipped: Ark locked"
" \x1b[1m\u{2713} usb\x1b[0m 1 device found\n \x1b[1m\u{2717} relay\x1b[0m no answer\n \x1b[1mhint:\x1b[0m open \x1b[1mArk Companion\x1b[0m\n \u{00b7} slots skipped: Ark locked\n \x1b[1m! tool\x1b[0m new release\n \x1b[1mhint:\x1b[0m run \x1b[1mbrew upgrade ark-cli\x1b[0m"
);
}
}
Loading
Loading