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
8 changes: 8 additions & 0 deletions src/cli_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ pub enum Command {
subcommand: ProjectSubcommand,
},

/// Opens a new shell in the puff data directory where managed files are stored.
/// Use --print to just print the path instead.
Cd {
/// Print the path instead of spawning a shell
#[arg(short = 'p', long = "print")]
print: bool,
},

/// Generates shell completions for the given shell and prints them to stdout
#[command(after_help = "\
Examples:
Expand Down
1 change: 1 addition & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod add_command;
pub mod cd_command;
pub mod file_forget_command;
pub mod init_command;
pub mod list_command;
Expand Down
53 changes: 53 additions & 0 deletions src/commands/cd_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use anyhow::{anyhow, Result};
use std::fs;
use std::process::Command;

use crate::config::locations::LocationsProvider;

pub struct CdCommand<'a> {
locations_provider: &'a LocationsProvider,
}

impl<'a> CdCommand<'a> {
pub fn new(locations_provider: &'a LocationsProvider) -> Self {
CdCommand {
locations_provider,
}
}

pub fn cd(&self, print: bool) -> Result<()> {
let path = self.locations_provider.get_projects_data_path();

if !path.exists() {
fs::create_dir_all(&path)?;
}

if print {
println!("{}", path.display());
return Ok(());
}

let shell = get_shell()?;
let config_path = self.locations_provider.get_base_config_path()?;
let data_path = self.locations_provider.get_base_data_path()?;
let status = Command::new(&shell)
.current_dir(&path)
.env("PUFF_SUBSHELL", "1")
.env("PUFF_CONFIG_PATH", &config_path)
.env("PUFF_DATA_PATH", &data_path)
.status()
.map_err(|e| anyhow!("Failed to spawn shell '{}': {}", shell, e))?;

std::process::exit(status.code().unwrap_or(1));
}
}

#[cfg(unix)]
fn get_shell() -> Result<String> {
std::env::var("SHELL").or_else(|_| Ok("/bin/sh".to_string()))
}

#[cfg(windows)]
fn get_shell() -> Result<String> {
std::env::var("COMSPEC").or_else(|_| Ok("cmd.exe".to_string()))
}
14 changes: 10 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ use clap::{CommandFactory, Parser};
use clap_complete::generate;
use cli_args::{AppArgs, Command};
use commands::{
add_command::AddCommand, file_forget_command::ForgetCommand, init_command::InitCommand,
list_command::ListCommand, project_forget_command::ProjectForgetCommand,
status_command::StatusCommand,
add_command::AddCommand, cd_command::CdCommand, file_forget_command::ForgetCommand,
init_command::InitCommand, list_command::ListCommand,
project_forget_command::ProjectForgetCommand, status_command::StatusCommand,
};
use config::{
app_config::AppConfigManager, locations::LocationsProvider, projects::ProjectsRetriever,
Expand Down Expand Up @@ -62,6 +62,12 @@ fn run() -> Result<()> {
}
};

if let Command::Cd { print } = args.command {
let cd = CdCommand::new(&locations_provider);
cd.cd(print)?;
return Ok(());
}

AppInitializer {
locations_provider: &locations_provider,
}
Expand Down Expand Up @@ -135,7 +141,7 @@ fn run() -> Result<()> {
}
},
// handled up above
Command::Completions { .. } => unreachable!(),
Command::Completions { .. } | Command::Cd { .. } => unreachable!(),
}

Ok(())
Expand Down
78 changes: 78 additions & 0 deletions tests/e2e/cd.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bats
load helpers

setup() { setup_puff_env; }
teardown() { teardown_puff_env; }

@test "cd --print: prints the projects data path" {
run puff cd --print
assert_success
output="$(echo "$output" | normalize_output)"
assert_output_contains "$(native_path "$PUFF_DATA_PATH")"
assert_output_contains "projects"
}

@test "cd -p: short flag works" {
run puff cd -p
assert_success
output="$(echo "$output" | normalize_output)"
assert_output_contains "$(native_path "$PUFF_DATA_PATH")"
}

@test "cd: spawns shell in projects directory" {
if is_windows; then
local fake_shell="$PUFF_CONFIG_PATH/fake_shell.bat"
echo '@cd' >"$fake_shell"
export COMSPEC="$(native_path "$fake_shell")"
else
local fake_shell="$PUFF_CONFIG_PATH/fake_shell"
printf '#!/usr/bin/env bash\npwd\n' >"$fake_shell"
chmod +x "$fake_shell"
export SHELL="$fake_shell"
fi

run puff cd
assert_success
output="$(echo "$output" | normalize_output)"
assert_output_contains "$(native_path "$PUFF_DATA_PATH")"
}

@test "cd: sets PUFF_SUBSHELL env var" {
if is_windows; then
local fake_shell="$PUFF_CONFIG_PATH/fake_shell.bat"
echo '@echo PUFF_SUBSHELL=%PUFF_SUBSHELL%' >"$fake_shell"
export COMSPEC="$(native_path "$fake_shell")"
else
local fake_shell="$PUFF_CONFIG_PATH/fake_shell"
printf '#!/usr/bin/env bash\necho "PUFF_SUBSHELL=$PUFF_SUBSHELL"\n' >"$fake_shell"
chmod +x "$fake_shell"
export SHELL="$fake_shell"
fi

run puff cd
assert_success
output="$(echo "$output" | normalize_output)"
assert_output_contains "PUFF_SUBSHELL=1"
}

@test "cd: exports PUFF_CONFIG_PATH and PUFF_DATA_PATH" {
if is_windows; then
local fake_shell="$PUFF_CONFIG_PATH/fake_shell.bat"
cat >"$fake_shell" <<'BATCH'
@echo config=%PUFF_CONFIG_PATH%
@echo data=%PUFF_DATA_PATH%
BATCH
export COMSPEC="$(native_path "$fake_shell")"
else
local fake_shell="$PUFF_CONFIG_PATH/fake_shell"
printf '#!/usr/bin/env bash\necho "config=$PUFF_CONFIG_PATH"\necho "data=$PUFF_DATA_PATH"\n' >"$fake_shell"
chmod +x "$fake_shell"
export SHELL="$fake_shell"
fi

run puff cd
assert_success
output="$(echo "$output" | normalize_output)"
assert_output_contains "config=$(native_path "$PUFF_CONFIG_PATH")"
assert_output_contains "data=$(native_path "$PUFF_DATA_PATH")"
}
10 changes: 10 additions & 0 deletions tests/e2e/helpers.bash
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ assert_output_contains() {
fi
}

is_windows() {
command -v cygpath &>/dev/null
}

# Normalizes output for cross-platform path comparison:
# converts backslashes to forward slashes and strips carriage returns.
normalize_output() {
tr '\\' '/' | tr -d '\r'
}

assert_output_not_contains() {
if echo "$output" | grep -qF "$1"; then
echo "Expected output to NOT contain: $1" >&2
Expand Down