Remove CLI project support (#10838)
Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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(¤t_dir, instruction, session_id)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user