Remove CLI project support (#10838)

Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com>
This commit is contained in:
Douwe Osinga
2026-08-03 16:11:42 +02:00
committed by GitHub
parent 10df80e409
commit 514fe5f43e
9 changed files with 0 additions and 527 deletions
-25
View File
@@ -17,7 +17,6 @@ use crate::commands::configure::configure_telemetry_consent_dialog;
use crate::commands::configure::handle_configure;
use crate::commands::info::handle_info;
use crate::commands::plugin::{handle_plugin_install, handle_plugin_update};
use crate::commands::project::{handle_project_default, handle_projects_interactive};
use crate::commands::recipe::{handle_deeplink, handle_list, handle_open, handle_validate};
use crate::commands::term::{
handle_term_info, handle_term_init, handle_term_log, handle_term_run, Shell,
@@ -38,8 +37,6 @@ use goose::session::session_manager::SessionType;
use goose::session::SessionManager;
use std::io::Read;
use std::path::PathBuf;
use tracing::warn;
const GOOSE_SERVER_SECRET_KEY_ENV: &str = "GOOSE_SERVER__SECRET_KEY";
fn generate_serve_secret_key() -> String {
@@ -946,14 +943,6 @@ enum Command {
extension_opts: ExtensionOptions,
},
/// Open the last project directory
#[command(about = "Open the last project directory", visible_alias = "p")]
Project {},
/// List recent project directories
#[command(about = "List recent project directories", visible_alias = "ps")]
Projects,
/// Execute commands from an instruction file
#[command(about = "Execute commands from an instruction file or stdin")]
Run {
@@ -1349,8 +1338,6 @@ fn get_command_name(command: &Option<Command>) -> &'static str {
Some(Command::Acp { .. }) => "acp",
Some(Command::Serve { .. }) => "serve",
Some(Command::Session { .. }) => "session",
Some(Command::Project {}) => "project",
Some(Command::Projects) => "projects",
Some(Command::Run { .. }) => "run",
Some(Command::Gateway { .. }) => "gateway",
Some(Command::Schedule { .. }) => "schedule",
@@ -2228,10 +2215,6 @@ pub async fn cli() -> anyhow::Result<()> {
let cli = Cli::parse();
if let Err(e) = crate::project_tracker::update_project_tracker(None, None) {
warn!("Warning: Failed to update project tracker: {}", e);
}
let command_name = get_command_name(&cli.command);
tracing::info!(
monotonic_counter.goose.cli_commands = 1,
@@ -2303,14 +2286,6 @@ pub async fn cli() -> anyhow::Result<()> {
)
.await
}
Some(Command::Project {}) => {
handle_project_default()?;
Ok(())
}
Some(Command::Projects) => {
handle_projects_interactive()?;
Ok(())
}
Some(Command::Run {
input_opts,
identifier,
-1
View File
@@ -3,7 +3,6 @@ pub mod doctor;
pub mod gateway;
pub mod info;
pub mod plugin;
pub mod project;
pub mod recipe;
pub mod review;
pub mod schedule;
-310
View File
@@ -1,310 +0,0 @@
use anyhow::Result;
use chrono::DateTime;
use cliclack::{self, intro, outro};
use std::path::Path;
use crate::project_tracker::ProjectTracker;
use goose::utils::safe_truncate;
/// Format a DateTime for display
fn format_date(date: DateTime<chrono::Utc>) -> String {
// Format: "2025-05-08 18:15:30"
date.format("%Y-%m-%d %H:%M:%S").to_string()
}
/// Handle the default project command
///
/// Offers options to resume the most recently accessed project
pub fn handle_project_default() -> Result<()> {
let goose_bin = std::env::current_exe()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| "goose".to_string());
let tracker = ProjectTracker::load()?;
let mut projects = tracker.list_projects();
if projects.is_empty() {
// If no projects exist, just start a new one in the current directory
println!("No previous projects found. Starting a new session in the current directory.");
let mut command = std::process::Command::new(&goose_bin);
command.arg("session");
let status = command.status()?;
if !status.success() {
println!("Failed to run goose. Exit code: {:?}", status.code());
}
return Ok(());
}
// Sort projects by last_accessed (newest first)
projects.sort_by_key(|project| std::cmp::Reverse(project.last_accessed));
// Get the most recent project
let project = &projects[0];
let project_dir = &project.path;
// Check if the directory exists
if !Path::new(project_dir).exists() {
println!(
"Most recent project directory '{}' no longer exists.",
project_dir
);
return Ok(());
}
// Format the path for display
let path = Path::new(project_dir);
let components: Vec<_> = path.components().collect();
let len = components.len();
let short_path = if len <= 2 {
project_dir.clone()
} else {
let mut path_str = String::new();
path_str.push_str("...");
for component in components.iter().skip(len - 2) {
path_str.push('/');
path_str.push_str(component.as_os_str().to_string_lossy().as_ref());
}
path_str
};
// Ask the user what they want to do
let _ = intro("goose Project Manager");
let current_dir = std::env::current_dir()?;
let current_dir_display = current_dir.display();
let choice = cliclack::select("Choose an option:")
.item(
"resume",
format!("Resume project with session: {}", short_path),
"Continue with the previous session",
)
.item(
"fresh",
format!("Resume project with fresh session: {}", short_path),
"Change to the project directory but start a new session",
)
.item(
"new",
format!(
"Start new project in current directory: {}",
current_dir_display
),
"Stay in the current directory and start a new session",
)
.interact()?;
match choice {
"resume" => {
let _ = outro(format!("Changing to directory: {}", project_dir));
// Get the session ID if available
let session_id = project.last_session_id.clone();
// Change to the project directory
std::env::set_current_dir(project_dir)?;
// Build the command to run goose
let mut command = std::process::Command::new(&goose_bin);
command.arg("session");
if let Some(id) = session_id {
command.arg("--name").arg(&id).arg("--resume");
println!("Resuming session: {}", id);
}
// Execute the command
let status = command.status()?;
if !status.success() {
println!("Failed to run goose. Exit code: {:?}", status.code());
}
}
"fresh" => {
let _ = outro(format!(
"Changing to directory: {} with a fresh session",
project_dir
));
// Change to the project directory
std::env::set_current_dir(project_dir)?;
// Build the command to run goose with a fresh session
let mut command = std::process::Command::new(&goose_bin);
command.arg("session");
// Execute the command
let status = command.status()?;
if !status.success() {
println!("Failed to run goose. Exit code: {:?}", status.code());
}
}
"new" => {
let _ = outro("Starting a new session in the current directory");
// Build the command to run goose
let mut command = std::process::Command::new(&goose_bin);
command.arg("session");
// Execute the command
let status = command.status()?;
if !status.success() {
println!("Failed to run goose. Exit code: {:?}", status.code());
}
}
_ => {
let _ = outro("Operation canceled");
}
}
Ok(())
}
/// Handle the interactive projects command
///
/// Shows a list of projects and lets the user select one to resume
pub fn handle_projects_interactive() -> Result<()> {
let goose_bin = std::env::current_exe()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| "goose".to_string());
let tracker = ProjectTracker::load()?;
let mut projects = tracker.list_projects();
if projects.is_empty() {
println!("No projects found.");
return Ok(());
}
// Sort projects by last_accessed (newest first)
projects.sort_by_key(|project| std::cmp::Reverse(project.last_accessed));
// Format project paths for display
let project_choices: Vec<(String, String)> = projects
.iter()
.enumerate()
.map(|(i, project)| {
let path = Path::new(&project.path);
let components: Vec<_> = path.components().collect();
let len = components.len();
let short_path = if len <= 2 {
project.path.clone()
} else {
let mut path_str = String::new();
path_str.push_str("...");
for component in components.iter().skip(len - 2) {
path_str.push('/');
path_str.push_str(component.as_os_str().to_string_lossy().as_ref());
}
path_str
};
// Include last instruction if available (truncated)
let instruction_preview =
project
.last_instruction
.as_ref()
.map_or(String::new(), |instr| {
let truncated = safe_truncate(instr, 40);
format!(" [{}]", truncated)
});
let formatted_date = format_date(project.last_accessed);
(
format!("{}", i + 1), // Value to return
format!("{} ({}){}", short_path, formatted_date, instruction_preview), // Display text with instruction
)
})
.collect();
// Let the user select a project
let _ = intro("goose Project Manager");
let mut select = cliclack::select("Select a project:");
// Add each project as an option
for (value, display) in &project_choices {
select = select.item(value, display, "");
}
// Add a cancel option
let cancel_value = String::from("cancel");
select = select.item(&cancel_value, "Cancel", "Don't resume any project");
let selected = select.interact()?;
if selected == "cancel" {
let _ = outro("Project selection canceled.");
return Ok(());
}
// Parse the selected index
let index = selected.parse::<usize>().unwrap_or(0);
if index == 0 || index > projects.len() {
let _ = outro("Invalid selection.");
return Ok(());
}
// Get the selected project
let project = &projects[index - 1];
let project_dir = &project.path;
// Check if the directory exists
if !Path::new(project_dir).exists() {
let _ = outro(format!(
"Project directory '{}' no longer exists.",
project_dir
));
return Ok(());
}
// Ask if the user wants to resume the session or start a new one
let session_id = project.last_session_id.clone();
let has_previous_session = session_id.is_some();
// Change to the project directory first
std::env::set_current_dir(project_dir)?;
let _ = outro(format!("Changed to directory: {}", project_dir));
// Only ask about resuming if there's a previous session
let resume_session = if has_previous_session {
let session_choice = cliclack::select("What would you like to do?")
.item(
"resume",
"Resume previous session",
"Continue with the previous session",
)
.item(
"new",
"Start new session",
"Start a fresh session in this project directory",
)
.interact()?;
session_choice == "resume"
} else {
false
};
// Build the command to run goose
let mut command = std::process::Command::new(&goose_bin);
command.arg("session");
if resume_session {
if let Some(id) = session_id {
command.arg("--name").arg(&id).arg("--resume");
println!("Resuming session: {}", id);
}
} else {
println!("Starting new session");
}
// Execute the command
let status = command.status()?;
if !status.success() {
println!("Failed to run goose. Exit code: {:?}", status.code());
}
Ok(())
}
-1
View File
@@ -9,7 +9,6 @@ compile_error!("Features `rustls-tls` and `native-tls` are mutually exclusive");
pub mod cli;
pub mod commands;
pub mod logging;
pub mod project_tracker;
pub mod recipes;
pub mod scenario_tests;
pub mod session;
-142
View File
@@ -1,142 +0,0 @@
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use goose::config::paths::Paths;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
/// Structure to track project information
#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectInfo {
/// The absolute path to the project directory
pub path: String,
/// Last time the project was accessed
pub last_accessed: DateTime<Utc>,
/// Last instruction sent to goose (if available)
pub last_instruction: Option<String>,
/// Last session ID associated with this project
pub last_session_id: Option<String>,
}
/// Structure to hold all tracked projects
#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectTracker {
projects: HashMap<String, ProjectInfo>,
}
/// Project information with path as a separate field for easier access
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectInfoDisplay {
/// The absolute path to the project directory
pub path: String,
/// Last time the project was accessed
pub last_accessed: DateTime<Utc>,
/// Last instruction sent to goose (if available)
pub last_instruction: Option<String>,
/// Last session ID associated with this project
pub last_session_id: Option<String>,
}
impl ProjectTracker {
/// Get the path to the projects.json file
fn get_projects_file() -> Result<PathBuf> {
let projects_file = Paths::in_data_dir("projects.json");
if let Some(parent) = projects_file.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
Ok(projects_file)
}
/// Load the project tracker from the projects.json file
pub fn load() -> Result<Self> {
let projects_file = Self::get_projects_file()?;
if projects_file.exists() {
let file_content = fs::read_to_string(&projects_file)?;
let tracker: ProjectTracker = serde_json::from_str(&file_content)
.context("Failed to parse projects.json file")?;
Ok(tracker)
} else {
// If the file doesn't exist, create a new empty tracker
Ok(ProjectTracker {
projects: HashMap::new(),
})
}
}
/// Save the project tracker to the projects.json file
pub fn save(&self) -> Result<()> {
let projects_file = Self::get_projects_file()?;
let json = serde_json::to_string_pretty(self)?;
fs::write(projects_file, json)?;
Ok(())
}
/// Update project information for the current directory
///
/// # Arguments
/// * `project_dir` - The project directory to update
/// * `instruction` - Optional instruction that was sent to goose
/// * `session_id` - Optional session ID associated with this project
pub fn update_project(
&mut self,
project_dir: &Path,
instruction: Option<&str>,
session_id: Option<&str>,
) -> Result<()> {
let dir_str = project_dir.to_string_lossy().to_string();
// Create or update the project entry
let project_info = self.projects.entry(dir_str.clone()).or_insert(ProjectInfo {
path: dir_str,
last_accessed: Utc::now(),
last_instruction: None,
last_session_id: None,
});
// Update the last accessed time
project_info.last_accessed = Utc::now();
// Update the last instruction if provided
if let Some(instr) = instruction {
project_info.last_instruction = Some(instr.to_string());
}
// Update the session ID if provided
if let Some(id) = session_id {
project_info.last_session_id = Some(id.to_string());
}
self.save()
}
/// List all tracked projects
///
/// Returns a vector of ProjectInfoDisplay objects
pub fn list_projects(&self) -> Vec<ProjectInfoDisplay> {
self.projects
.values()
.map(|info| ProjectInfoDisplay {
path: info.path.clone(),
last_accessed: info.last_accessed,
last_instruction: info.last_instruction.clone(),
last_session_id: info.last_session_id.clone(),
})
.collect()
}
}
/// Update the project tracker with the current directory and optional instruction
///
/// # Arguments
/// * `instruction` - Optional instruction that was sent to goose
/// * `session_id` - Optional session ID associated with this project
pub fn update_project_tracker(instruction: Option<&str>, session_id: Option<&str>) -> Result<()> {
let current_dir = std::env::current_dir()?;
let mut tracker = ProjectTracker::load()?;
tracker.update_project(&current_dir, instruction, session_id)
}
-10
View File
@@ -737,16 +737,6 @@ impl CliSession {
history.save(editor);
self.push_message(Message::user().with_text(content));
if let Err(e) = crate::project_tracker::update_project_tracker(
Some(content),
Some(&self.session_id),
) {
eprintln!(
"Warning: Failed to update project tracker with instruction: {}",
e
);
}
let _provider = self.agent.provider().await?;
println!();
@@ -65,7 +65,6 @@ instructions: |
- Core Commands (configure, info, version, update)
- Session Management (session and subcommands)
- Task Execution (run, bench, recipe, schedule, mcp, acp)
- Project Management (project, projects)
- Interface (web)
- Interactive Session Features (slash commands, themes, etc.)
@@ -728,32 +728,6 @@ goose serve --with-builtin developer,memory
---
### Project Management
#### project
Start working on your last project or create a new one.
**Alias**: `p`
**Usage:**
```bash
goose project
```
---
#### projects
Choose one of your projects to start working on.
**Alias**: `ps`
**Usage:**
```bash
goose projects
```
---
### Terminal Integration
#### term
@@ -322,17 +322,6 @@ Sessions created in goose Desktop can be resumed in the CLI and vice versa. All
While you can resume sessions, we recommend creating new sessions for new tasks to reduce the chance of [doom spiraling](/docs/troubleshooting/known-issues#stuck-in-a-loop-or-unresponsive).
:::
### Resume Project-Based Sessions
<Tabs groupId="interface">
<TabItem value="ui" label="goose Desktop" default>
Project-based sessions are only available through the CLI.
</TabItem>
<TabItem value="cli" label="goose CLI">
You can use the [`project`](/docs/guides/goose-cli-commands#project) and [`projects`](/docs/guides/goose-cli-commands#projects) commands to start or resume sessions from a project, which is a tracked working directory with session metadata.
</TabItem>
</Tabs>
## Duplicate Sessions
Create a complete copy of any session to reuse configurations, experiment with variations, or preserve important work.