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
26 changes: 20 additions & 6 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ clap_complete = "4.6"
console = "0.16"
ed25519-dalek = "2.2"
hex = "0.4"
globset = "0.4"
indicatif = "0.18"
insta = "1.47"
pretty_assertions = "1.4"
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,14 @@ cargo run -p crosspack-cli --bin crosspack -- completions bash
Tip: `completions` targets the canonical `crosspack` binary name.
Tip: generated Crosspack scripts include loader logic for package-declared completion files under `<prefix>/share/completions/packages/<shell>/`.

### 6) Optional: print shell setup snippet (PATH + completion/init loader)
### 6) Optional: print shell setup snippet (PATH + MANPATH + completion/init loader)

```bash
cargo run -p crosspack-cli --bin crosspack -- init-shell --shell zsh
```

Tip: `init-shell` auto-detects shell when `--shell` is omitted; fallback is `bash` on Unix and `powershell` on Windows.
Tip: `init-shell` also loads package-declared shell init snippets from `<prefix>/share/shell/init/<shell>/`; `completions` remains completion-only.
Tip: `init-shell` also configures Unix `MANPATH` for package-declared man pages and loads package-declared shell init snippets from `<prefix>/share/shell/init/<shell>/`; `completions` remains completion-only.

## Legacy `--registry-root` mode

Expand Down Expand Up @@ -193,7 +193,7 @@ cargo run -p crosspack-cli --bin crosspack -- --registry-root /path/to/registry
| `services start <package> <service>` | Set managed service state to `running` for an installed package. |
| `services stop <package> <service>` | Set managed service state to `stopped` for an installed package. |
| `services restart <package> <service>` | Set managed service state to `running` for an installed package. |
| `integrations list` | List projected Docker CLI, PATH plugin, and service integrations for installed packages. |
| `integrations list` | List projected Docker CLI, PATH plugin, man page, and service integrations for installed packages. |
| `integrations status <package> <integration>` | Show projection and activation state for matching integrations. |
| `integrations enable <package> <integration>` | Explicitly activate a Docker CLI plugin, PATH plugin, or service integration on the host. |
| `integrations disable <package> <integration>` | Explicitly remove owned host activation while preserving package projection state. |
Expand All @@ -210,7 +210,7 @@ cargo run -p crosspack-cli --bin crosspack -- --registry-root /path/to/registry
| `doctor` | Show prefix paths and transaction health. |
| `version` / `--version` | Print the Crosspack CLI version. |
| `completions <bash\|zsh\|fish\|powershell>` | Print shell completion script for the canonical `crosspack` binary, including package completion loader block. |
| `init-shell [--shell <bash\|zsh\|fish\|powershell>]` | Print shell setup snippet that adds Crosspack bin directory to `PATH`, loads Crosspack/package completion scripts, and sources package shell init snippets. |
| `init-shell [--shell <bash\|zsh\|fish\|powershell>]` | Print shell setup snippet that adds Crosspack bin directory to `PATH`, configures Unix `MANPATH`, loads Crosspack/package completion scripts, and sources package shell init snippets. |

Output contract notes:
- Human-facing lifecycle commands automatically use an enhanced interactive terminal presentation on TTYs (section framing, semantic color, and progress indicators).
Expand All @@ -234,6 +234,7 @@ Crosspack verifies both metadata and artifacts:
Registry metadata can declare typed integrations without maintainer scripts:

- Docker CLI plugins and PATH plugins are projected into the Crosspack prefix during install. Host activation is explicit through `crosspack integrations enable` and reversible through `crosspack integrations disable`.
- Man pages are projected under `<prefix>/share/man/` during install. `crosspack init-shell` exposes the managed man root through `MANPATH` on bash, zsh, and fish without mutating system man directories.
- Services may declare Linux systemd-user, macOS launch-agent, and Windows service source metadata. Service host activation is explicit-only today; manifests should not set `enable = true`, and install fails closed before host mutation if they do.
- Packages may declare metadata-driven shell init commands for `init-shell`. Crosspack writes deterministic snippets under `<prefix>/share/shell/init/<shell>/`; install never edits dotfiles and never executes those commands.
- Service uninstall preserves activation records when host cleanup cannot be verified; it does not claim service disable/remove support.
Expand Down
9 changes: 9 additions & 0 deletions crates/crosspack-cli/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,16 +150,21 @@ fn print_init_shell_snippet(layout: &PrefixLayout, shell: CliCompletionShell) {

fn init_shell_snippet(layout: &PrefixLayout, shell: CliCompletionShell) -> String {
let bin = layout.bin_dir();
let man = layout.man_dir();
let completion_path = crosspack_completion_script_path(layout, shell);
let shell_init_dir = layout.shell_init_shell_dir(shell.package_completion_shell());
let mut output = String::new();
match shell {
CliCompletionShell::Bash | CliCompletionShell::Zsh => {
let escaped_man = escape_single_quote_shell(&man.display().to_string());
let escaped_completion =
escape_single_quote_shell(&completion_path.display().to_string());
let escaped_shell_init =
escape_single_quote_shell(&shell_init_dir.display().to_string());
output.push_str(&format!("export PATH=\"{}:$PATH\"\n", bin.display()));
output.push_str(&format!("if [ -d '{escaped_man}' ]; then\n"));
output.push_str(&format!(" export MANPATH=\"{escaped_man}:${{MANPATH:-}}\"\n"));
output.push_str("fi\n");
output.push_str(&format!("if [ -f '{escaped_completion}' ]; then\n"));
output.push_str(&format!(" . '{escaped_completion}'\n"));
output.push_str("fi\n");
Expand All @@ -174,6 +179,7 @@ fn init_shell_snippet(layout: &PrefixLayout, shell: CliCompletionShell) -> Strin
}
CliCompletionShell::Fish => {
let escaped_bin = escape_single_quote_shell(&bin.display().to_string());
let escaped_man = escape_single_quote_shell(&man.display().to_string());
let escaped_completion =
escape_single_quote_shell(&completion_path.display().to_string());
let escaped_shell_init =
Expand All @@ -182,6 +188,9 @@ fn init_shell_snippet(layout: &PrefixLayout, shell: CliCompletionShell) -> Strin
output.push_str(&format!(" if not contains -- '{escaped_bin}' $PATH\n"));
output.push_str(&format!(" set -gx PATH '{escaped_bin}' $PATH\n"));
output.push_str(" end\nend\n");
output.push_str(&format!("if test -d '{escaped_man}'\n"));
output.push_str(&format!(" set -gx MANPATH '{escaped_man}' $MANPATH\n"));
output.push_str("end\n");
output.push_str(&format!("if test -f '{escaped_completion}'\n"));
output.push_str(&format!(" source '{escaped_completion}'\n"));
output.push_str("end\n");
Expand Down
2 changes: 1 addition & 1 deletion crates/crosspack-cli/src/core_flows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1810,7 +1810,7 @@ fn sync_integration_projection_state(
let desired_projections = integrations
.iter()
.map(|integration| {
projected_integrations(package_name, integration).map(|projections| {
projected_integrations_for_install_root(package_name, install_root, integration, host_platform).map(|projections| {
projections
.into_iter()
.filter(|projection| {
Expand Down
4 changes: 3 additions & 1 deletion crates/crosspack-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ use crosspack_core::{
PackageManifest, ServiceDeclaration,
};
#[cfg(test)]
use crosspack_installer::projected_integrations;
#[cfg(test)]
use crosspack_installer::read_declared_services_state;
#[cfg(test)]
use crosspack_installer::read_installed_package_state;
Expand All @@ -29,7 +31,7 @@ use crosspack_installer::{
exposed_completion_path, gui_asset_path, install_from_artifact_to_dir,
install_from_source_archive_to_dir, plan_docker_cli_plugin_activation,
plan_path_plugin_activation, plan_service_activation, projected_exposed_completion_path,
projected_gui_assets, projected_integrations, projected_shell_init,
projected_gui_assets, projected_integrations_for_install_root, projected_shell_init,
read_active_transaction_marker, read_all_declared_services_states,
read_all_gui_exposure_states, read_all_installed_package_states, read_all_integration_states,
read_all_pins, read_all_shell_init_states, read_gui_exposure_state, read_gui_native_state,
Expand Down
77 changes: 77 additions & 0 deletions crates/crosspack-cli/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6929,6 +6929,28 @@ requirement = "^14"
let _ = std::fs::remove_dir_all(layout.prefix());
}

#[test]
fn init_shell_snippet_exposes_manpath_for_unix_shells_only() {
let layout = PrefixLayout::new(build_test_layout_path(current_unix_nanos()).join("with spaces"));
layout.ensure_base_dirs().expect("must create dirs");

for shell in [
CliCompletionShell::Bash,
CliCompletionShell::Zsh,
CliCompletionShell::Fish,
] {
let rendered = init_shell_snippet(&layout, shell);
let man_marker = layout.man_dir().display().to_string();
assert!(rendered.contains(&man_marker));
assert!(rendered.contains("MANPATH"));
}

let powershell = init_shell_snippet(&layout, CliCompletionShell::Powershell);
assert!(!powershell.contains("MANPATH"));

let _ = std::fs::remove_dir_all(layout.prefix());
}

#[test]
fn generate_completions_outputs_non_empty_script_for_each_shell() {
let shells = [
Expand Down Expand Up @@ -10819,6 +10841,61 @@ old-cc = "<2.0.0"
assert!(line.contains("path=/prefix/bin/kubectl-ctx"));
}

#[test]
fn integration_list_reports_man_page_projection_and_enable_is_unsupported() {
let layout = test_layout();
layout.ensure_base_dirs().expect("must create dirs");
write_install_receipt(
&layout,
&InstallReceipt {
name: "delta".to_string(),
version: "0.18.2".to_string(),
dependencies: Vec::new(),
target: Some("x86_64-unknown-linux-gnu".to_string()),
artifact_url: None,
artifact_sha256: None,
cache_path: None,
exposed_bins: Vec::new(),
exposed_completions: Vec::new(),
snapshot_id: None,
install_mode: InstallMode::Managed,
install_reason: InstallReason::Root,
install_status: "installed".to_string(),
installed_at_unix: 1,
},
)
.expect("must write receipt");
write_integration_state(
&layout,
"delta",
&[IntegrationProjection {
kind: "man_page".to_string(),
key: "man_page:1:delta".to_string(),
rel_path: "man/man1/delta.1".to_string(),
}],
)
.expect("must seed integration state");

let rows = collect_projected_integration_rows(&layout).expect("must collect integrations");
assert_eq!(
format_projected_integration_lines(&rows),
vec!["integration package=delta name=delta key=man_page:1:delta kind=man_page state=projected adapter=none reason=not-enabled path=man/man1/delta.1"],
);

let err = run_integration_activation_command(&layout, "delta", "delta", true)
.expect_err("man page activation should be unsupported");
assert!(
err.to_string()
.contains("integration activation is not supported for kind 'man_page'")
);
assert!(
read_integration_activation_state(&layout)
.expect("must read activation state")
.is_empty(),
"unsupported man page activation must not persist activation state"
);
}

#[test]
fn integrations_activation_failure_persists_status_record_without_false_ok() {
let layout = test_layout();
Expand Down
4 changes: 2 additions & 2 deletions crates/crosspack-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ pub use archive::ArchiveType;
pub use artifact::{Artifact, ArtifactBinary, ArtifactCompletion, ArtifactCompletionShell};
pub use gui::{ArtifactGuiApp, ArtifactGuiFileAssociation, ArtifactGuiProtocol};
pub use manifest::{
PackageIntegration, PackageManifest, PackageShellInit, ServiceDeclaration, ShellInitStrategy,
SourceBuildMetadata,
IntegrationHostPlatform, PackageIntegration, PackageManifest, PackageShellInit,
ServiceDeclaration, ShellInitStrategy, SourceBuildMetadata,
};

#[cfg(test)]
Expand Down
Loading
Loading