Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
74dee8b
feat: TUI snapshot mode, vcs visibility, aligned glow preview
N4M3Z Jul 9, 2026
3d2b658
feat: TUI comment reachable from any detail tab, context hints
N4M3Z Jul 10, 2026
65b9201
fix: harden vcs scan per adversarial review
N4M3Z Jul 10, 2026
10360e9
feat: TUI mouse support, windowed code view, highlighted preview fences
N4M3Z Jul 10, 2026
623356f
fix: reference links resolve across preview fence segments
N4M3Z Jul 10, 2026
1dc87e4
fix: harden mouse hit-testing, code cursor reach, fence parsing
N4M3Z Jul 10, 2026
c3ee901
feat: rich zoom, full ADR body, real diff, repo detail, cached tabs
N4M3Z Jul 10, 2026
7e6cae6
fix: module-qualify detail and code cache keys
N4M3Z Jul 10, 2026
b841d9a
fix: honest diff states, fresh zoom after rescan, smooth list browsing
N4M3Z Jul 10, 2026
9f47e4a
fix: harness-panel review round — list viewport, comment identity, ho…
N4M3Z Jul 12, 2026
f9939e2
fix: Fable panel round — safe escape, honest search mode, live scroll…
N4M3Z Jul 12, 2026
299444f
feat: council round one — rekeyed input, gitui letters, tuicr wrap, d…
N4M3Z Jul 12, 2026
4669a64
feat: council round two — one artifact view, structured provenance
N4M3Z Jul 12, 2026
37fffed
feat: council round three — code cursor, exact scroll everywhere
N4M3Z Jul 12, 2026
fbc9214
feat: diff line-number gutter and per-line diff commenting
N4M3Z Jul 12, 2026
fcfd16c
feat: numbered tabs restored, glow color unlocked, richer style, repo…
N4M3Z Jul 12, 2026
7968115
feat: deploy-to-target picker and harness launch in the TUI
N4M3Z Jul 12, 2026
7a395f1
feat: single-artifact deploy, add-target input, harness launch picker
N4M3Z Jul 12, 2026
84e1a60
fix: deploy/launch council round — slug-safe --only, contained writes
N4M3Z Jul 12, 2026
0b577c1
feat: digits address tabs only, jj view, clickable commits, one compa…
N4M3Z Jul 12, 2026
55a429d
feat: make it actionable — in-panel filter, enterable Overview, colum…
N4M3Z Jul 12, 2026
de35975
fix: actionable-round adversarial pass — exact module jumps, honest help
N4M3Z Jul 12, 2026
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
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ validate = ["dep:rust-embed"]
deploy = []
embed = ["dep:rust-embed"]
dashboard = ["full", "dep:axum", "dep:tokio", "dep:askama", "dep:tower-http", "dep:open"]
tui = ["dep:ratatui", "dep:crossterm", "dep:syntect", "dep:ansi-to-tui"]
tui = ["dep:ratatui", "dep:crossterm", "dep:syntect", "dep:ansi-to-tui", "dep:unicode-width"]

[dependencies]
# cli
Expand Down Expand Up @@ -65,6 +65,7 @@ ratatui = { version = "0.30", optional = true }
crossterm = { version = "0.29", optional = true }
syntect = { version = "5.3", default-features = false, features = ["default-syntaxes", "default-themes", "regex-fancy"], optional = true }
ansi-to-tui = { version = "8.0.1", optional = true }
unicode-width = { version = "0.2", optional = true }

[build-dependencies]
sha2 = "0.10"
Expand Down
7 changes: 7 additions & 0 deletions src/cli/dashboard/routes/artifact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ pub(super) async fn companion_detail(
kind: "skills".to_string(),
module: module_name,
relative_path: companion.relative_path.clone(),
source_path: companion.relative_path.clone(),
description: companion.description.clone(),
content_preview: String::new(),
content_body: companion.content_body.clone(),
Expand All @@ -234,6 +235,7 @@ pub(super) async fn companion_detail(
module_tint: 0,
companions: Vec::new(),
variants: Vec::new(),
vcs: None,
};
let companion_label = format!("{parent} / {name}");
let provenance_raw = scan::read_source_sidecar(
Expand Down Expand Up @@ -654,6 +656,8 @@ mod tests {
module_tint: 0,
companions: Vec::new(),
variants: Vec::new(),
source_path: String::new(),
vcs: None,
}
}

Expand All @@ -665,6 +669,9 @@ mod tests {
source_uri: format!("https://example.com/{name}"),
is_target: false,
artifacts,
local_path: None,
vcs: None,
git_log: Vec::new(),
}
}

Expand Down
151 changes: 130 additions & 21 deletions src/cli/deploy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::cli::config;
/// Modified → skip (unless --force)
/// ```
#[allow(clippy::fn_params_excessive_bools, clippy::too_many_lines)]
#[allow(clippy::too_many_arguments)]
pub fn execute(
path: &str,
target: Option<&str>,
Expand All @@ -29,7 +30,11 @@ pub fn execute(
prune: bool,
_interactive: bool,
dry_run: bool,
only: Option<&str>,
) -> Result<ActionResult, Error> {
// A scoped deploy leaves everything else at the target alone: pruning
// against a filtered key set would quarantine the rest of the module.
let prune = prune && only.is_none();
let module_root = Path::new(path);
require_module_root(module_root)?;
let mut result = ActionResult::new();
Expand Down Expand Up @@ -72,9 +77,10 @@ pub fn execute(
validate_target_boundary(&target_base, Path::new(dir))?;
}
deployed_by_root.entry(target_base.clone()).or_default();
manifests
.entry(target_base.clone())
.or_insert_with(|| load_deployed_manifest(&target_base));
if !manifests.contains_key(&target_base) {
let entries = load_manifest_or_recover(&target_base, only)?;
manifests.insert(target_base.clone(), entries);
}
}

for kind in commands::provider::ContentKind::ALL {
Expand All @@ -89,9 +95,13 @@ pub fn execute(
validate_target_boundary(&target_base, Path::new(dir))?;
}

let existing_manifest = manifests
.entry(target_base.clone())
.or_insert_with(|| load_deployed_manifest(&target_base));
if !manifests.contains_key(&target_base) {
let entries = load_manifest_or_recover(&target_base, only)?;
manifests.insert(target_base.clone(), entries);
}
let Some(existing_manifest) = manifests.get_mut(&target_base) else {
continue;
};
let deployed_keys = deployed_by_root.entry(target_base.clone()).or_default();

deploy_provider_kind_files(
Expand All @@ -103,6 +113,7 @@ pub fn execute(
&mut result,
provider_name,
force,
only,
)?;
}

Expand Down Expand Up @@ -134,6 +145,32 @@ fn resolve_target_base(target_root: &str, effective_target: Option<&str>) -> Pat
}
}

/// Whether a manifest key falls under an `--only` prefix. A prefix ending in
/// `/` or `.` matches literally; a bare prefix (`skills/Alpha`) matches only
/// at a path or extension boundary, so `skills/AlphaOther/` stays untouched.
fn only_matches(manifest_key: &str, prefix: &str) -> bool {
// Providers rename artifacts during assembly (gemini slugifies
// SecurityArchitect to security-architect); compare shapes that survive
// the rename: lowercase with separators dropped.
let key = normalize_only(manifest_key);
let prefix = normalize_only(prefix);
if prefix.ends_with('/') || prefix.ends_with('.') {
return key.starts_with(&prefix);
}
key == prefix
|| key
.strip_prefix(&prefix)
.is_some_and(|rest| rest.starts_with('/') || rest.starts_with('.'))
}

fn normalize_only(value: &str) -> String {
value
.chars()
.filter(|character| *character != '-' && *character != '_')
.flat_map(char::to_lowercase)
.collect()
}

/// Deploy one content kind for a single provider.
#[allow(clippy::too_many_arguments)]
fn deploy_provider_kind_files(
Expand All @@ -145,6 +182,7 @@ fn deploy_provider_kind_files(
result: &mut ActionResult,
provider_name: &str,
force: bool,
only: Option<&str>,
) -> Result<(), Error> {
let files = collect_files_recursive(kind_dir)?;

Expand All @@ -159,6 +197,9 @@ fn deploy_provider_kind_files(
.to_string_lossy()
.to_string();
let manifest_key = format!("{kind}/{relative}");
if only.is_some_and(|prefix| !only_matches(&manifest_key, prefix)) {
continue;
}
deployed_keys.insert(manifest_key.clone());
let target_path = target_base.join(kind.as_str()).join(&relative);

Expand All @@ -167,11 +208,6 @@ fn deploy_provider_kind_files(
let provenance_relative = manifest::provenance_path(&manifest_key);
let sidecar_source = manifest::sidecar_path(&build_path);

if sidecar_source.is_file() {
let provenance_target = target_base.join(&provenance_relative);
let _ = copy_file(&sidecar_source, &provenance_target);
}

let target_content = fs::read_to_string(&target_path).ok();
let status = manifest::status(
target_content.as_deref(),
Expand All @@ -181,7 +217,19 @@ fn deploy_provider_kind_files(

match status {
manifest::FileStatus::New | manifest::FileStatus::Stale => {
ensure_destination_within(&target_path, target_base)?;
copy_file(&build_path, &target_path)?;
// Provenance travels only with content that actually
// installed; a skipped modified file keeps its old sidecar.
if sidecar_source.is_file() {
let provenance_target = target_base.join(&provenance_relative);
if let Err(error) = copy_file(&sidecar_source, &provenance_target) {
eprintln!(
"warning: sidecar copy failed for {}: {error}",
provenance_target.display()
);
}
}
new_manifest.insert(
manifest_key,
manifest::ManifestEntry {
Expand Down Expand Up @@ -236,7 +284,6 @@ fn deploy_provider_kind_files(
}
Ok(())
}

#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)]
fn prune_stale_files(
Expand Down Expand Up @@ -434,7 +481,10 @@ fn require_module_root(module_root: &Path) -> Result<(), Error> {
}

/// Verify the resolved target path stays within the specified base directory.
/// Containment is checked against the deepest existing ancestor BEFORE any
/// directory is created, so an escaping path never mutates the filesystem.
fn validate_target_boundary(target_path: &Path, base_directory: &Path) -> Result<(), Error> {
ensure_destination_within(&target_path.join("probe"), base_directory)?;
fs::create_dir_all(target_path).map_err(|error| {
Error::new(
ErrorKind::Io,
Expand Down Expand Up @@ -471,19 +521,38 @@ fn validate_target_boundary(target_path: &Path, base_directory: &Path) -> Result
/// Load the previously deployed `.manifest` from a provider's target directory.
pub(crate) fn load_deployed_manifest(
target_base: &Path,
) -> HashMap<String, manifest::ManifestEntry> {
) -> Result<HashMap<String, manifest::ManifestEntry>, Error> {
let manifest_path = target_base.join(".manifest");
let Ok(content) = fs::read_to_string(&manifest_path) else {
return HashMap::new();
return Ok(HashMap::new());
};
match manifest::read(&content) {
Ok(entries) => entries,
manifest::read(&content).map_err(|error| {
Error::new(
ErrorKind::Config,
format!("corrupt .manifest at {}: {error}", manifest_path.display()),
)
})
}

/// Manifest for a filtered deploy must parse: silently rebuilding it from a
/// partial deploy would drop every entry outside the filter. A full deploy
/// may rebuild with a warning.
fn load_manifest_or_recover(
target_base: &Path,
only: Option<&str>,
) -> Result<HashMap<String, manifest::ManifestEntry>, Error> {
match load_deployed_manifest(target_base) {
Ok(entries) => Ok(entries),
Err(error) if only.is_some() => Err(Error::new(
ErrorKind::Config,
format!(
"refusing filtered deploy over a corrupt manifest ({error}); \
run a full install to rebuild it"
),
)),
Err(error) => {
eprintln!(
"warning: corrupt .manifest at {}: {error}",
manifest_path.display()
);
HashMap::new()
eprintln!("warning: {error}; rebuilding manifest from this deploy");
Ok(HashMap::new())
}
}
}
Expand All @@ -508,6 +577,46 @@ fn write_manifest(
.map_err(|e| Error::new(ErrorKind::Io, format!("cannot write .manifest: {e}")))
}

/// Verify that writing `target_path` cannot escape `base`: the deepest
/// existing ancestor (which any symlinked component resolves through) must
/// canonicalize inside the base directory.
fn ensure_destination_within(target_path: &Path, base: &Path) -> Result<(), Error> {
fs::create_dir_all(base).map_err(|error| {
Error::new(
ErrorKind::Io,
format!("cannot create {}: {error}", base.display()),
)
})?;
let resolved_base = base.canonicalize().map_err(|error| {
Error::new(
ErrorKind::Io,
format!("cannot resolve {}: {error}", base.display()),
)
})?;
let existing = target_path
.ancestors()
.find(|ancestor| ancestor.exists())
.unwrap_or(base);
let resolved = existing.canonicalize().map_err(|error| {
Error::new(
ErrorKind::Io,
format!("cannot resolve {}: {error}", existing.display()),
)
})?;
if resolved.starts_with(&resolved_base) {
Ok(())
} else {
Err(Error::new(
ErrorKind::Config,
format!(
"destination escapes target: {} resolves to {}",
target_path.display(),
resolved.display()
),
))
}
}

/// Copy a file, creating parent directories as needed.
fn copy_file(source: &Path, target: &Path) -> Result<(), Error> {
if let Some(parent) = target.parent() {
Expand Down
Loading