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
2 changes: 1 addition & 1 deletion src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ pub fn run_command(action: ContainerAction, editor: &str) -> Result<()> {
let config_path = PathBuf::from(&config_file);
if config_path.exists() {
let dev_container = DevContainer::from_config(&config_path, &ws.name)?;
ws.open(vec![], false, &dev_container, editor)?;
ws.open(vec![], false, &dev_container, editor, None)?;
} else {
ws.open_classic(vec![], false, editor)?;
}
Expand Down
15 changes: 13 additions & 2 deletions src/launch.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
use std::{ffi::OsString, fmt::Display, path::PathBuf, str::FromStr};
use std::{
ffi::OsString,
fmt::Display,
path::{Path, PathBuf},
str::FromStr,
};

use clap::ValueEnum;
use color_eyre::eyre::{self, Result, bail, eyre};
Expand Down Expand Up @@ -138,7 +143,11 @@ impl Setup {

/// Launches vscode with the given configuration.
/// Returns the dev container that was used, if any.
pub fn launch(self, config: Option<PathBuf>) -> Result<Option<DevContainer>> {
pub fn launch(
self,
config: Option<PathBuf>,
subfolder: Option<&Path>,
) -> Result<Option<DevContainer>> {
let editor_name = format_editor_name(&self.behavior.command);

match self.behavior.strategy {
Expand All @@ -152,6 +161,7 @@ impl Setup {
self.dry_run,
dev_container,
&self.behavior.command,
subfolder,
)?;
} else {
info!("No dev container found, opening on host system with {editor_name}...");
Expand All @@ -173,6 +183,7 @@ impl Setup {
self.dry_run,
dev_container,
&self.behavior.command,
subfolder,
)?;
} else {
bail!(
Expand Down
47 changes: 39 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ mod workspace;

use chrono::Utc;
use clap::Parser;
use color_eyre::eyre::Result;
use color_eyre::eyre::{Result, WrapErr};
use log::trace;
use std::io::Write;
use std::path::PathBuf;
use std::path::{Path, PathBuf};

use crate::config_store::ConfigStore;
use crate::history::{Entry, Tracker};
Expand Down Expand Up @@ -52,6 +52,31 @@ fn resolve_launch_config(config: Option<&PathBuf>, store: &ConfigStore) -> Resul
.transpose()
}

fn workspace_root_from_config(
config: &Path,
path_arg: &Path,
) -> Result<(PathBuf, Option<PathBuf>)> {
let abs = std::fs::canonicalize(config)
.wrap_err_with(|| format!("Config path does not exist: {}", config.display()))?;
let mut current = abs.as_path();
let root = loop {
let Some(parent) = current.parent() else {
break abs.parent().unwrap_or(&abs).to_path_buf();
};
if parent.file_name().is_some_and(|n| n == ".devcontainer") {
break parent.parent().unwrap_or(parent).to_path_buf();
}
current = parent;
};
let path_abs = std::fs::canonicalize(path_arg).unwrap_or(path_arg.to_path_buf());
let sub = if path_abs.starts_with(&root) && path_abs != root {
path_abs.strip_prefix(&root).ok().map(Path::to_path_buf)
} else {
None
};
Ok((root, sub))
}

fn main() -> Result<()> {
color_eyre::install()?;

Expand All @@ -70,28 +95,34 @@ fn main() -> Result<()> {
match opts.command {
opts::Commands::Open { path, launch } => {
let mut tracker = load_tracker(opts.history_path)?;
let path = path.as_path();
let ws = Workspace::from_path(path)?;
let ws_name = ws.name.clone();

let resolved_config = resolve_launch_config(launch.config.as_ref(), &config_store)?;
let config_name = resolved_config
.as_ref()
.and_then(|p| config_store::config_name_from_path(p, &config_store));

let (workspace_path, subfolder) = if let Some(ref config) = resolved_config {
workspace_root_from_config(config, &path)?
} else {
(path.clone(), None)
};
Comment on lines +104 to +108
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When --config resolves to an external config, this now derives the workspace root from the config store path and ignores the user’s project path, so vscli open --config rust-dev ~/projects/my-app opens the config directory instead of the project. Preserve the path argument when it isn’t under the config-derived root.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/main.rs, line 104:

<comment>When `--config` resolves to an external config, this now derives the workspace root from the config store path and ignores the user’s project path, so `vscli open --config rust-dev ~/projects/my-app` opens the config directory instead of the project. Preserve the path argument when it isn’t under the config-derived root.</comment>

<file context>
@@ -70,28 +95,34 @@ fn main() -> Result<()> {
                 .as_ref()
                 .and_then(|p| config_store::config_name_from_path(p, &config_store));
 
+            let (workspace_path, subfolder) = if let Some(ref config) = resolved_config {
+                workspace_root_from_config(config, &path)?
+            } else {
</file context>
Suggested change
let (workspace_path, subfolder) = if let Some(ref config) = resolved_config {
workspace_root_from_config(config, &path)?
} else {
(path.clone(), None)
};
let (workspace_path, subfolder) = if let Some(ref config) = resolved_config {
let (root, sub) = workspace_root_from_config(config, &path)?;
let path_abs = path.canonicalize().unwrap_or_else(|_| path.clone());
if path_abs.starts_with(&root) {
(root, sub)
} else {
(path.clone(), None)
}
} else {
(path.clone(), None)
};
Fix with Cubic


let ws = Workspace::from_path(&workspace_path)?;
let ws_name = ws.name.clone();

let behavior = Behavior {
strategy: launch.behavior.unwrap_or_default(),
args: launch.args,
command: launch.command.unwrap_or_else(|| "code".to_string()),
};
let setup = Setup::new(ws, behavior.clone(), opts.dry_run);
let dev_container = setup.launch(resolved_config)?;
let dev_container = setup.launch(resolved_config, subfolder.as_deref())?;

tracker.history.upsert(Entry {
workspace_name: ws_name,
dev_container_name: dev_container.as_ref().and_then(|dc| dc.name.clone()),
config_name,
workspace_path: path.canonicalize()?,
workspace_path: workspace_path.canonicalize()?,
config_path: dev_container.map(|dc| dc.config_path),
behavior,
last_opened: Utc::now(),
Expand Down Expand Up @@ -130,7 +161,7 @@ fn main() -> Result<()> {
.and_then(|p| config_store::config_name_from_path(p, &config_store));

let setup = Setup::new(ws, entry.behavior.clone(), opts.dry_run);
let dev_container = setup.launch(resolved_config)?;
let dev_container = setup.launch(resolved_config, None)?;

tracker.history.update(
id,
Expand Down
14 changes: 11 additions & 3 deletions src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,22 @@ impl Workspace {
dry_run: bool,
dev_container: &DevContainer,
command: &str,
subfolder: Option<&Path>,
) -> Result<()> {
// Checking if '--folder-uri' is present in the arguments
if args.iter().any(|arg| arg == "--folder-uri") {
bail!("Specifying `--folder-uri` is not possible while using vscli.");
}

// get the folder path from the selected dev container
let container_folder: String = dev_container.workspace_path_in_container.clone();
let mut container_folder: String = dev_container.workspace_path_in_container.clone();
if let Some(sub) = subfolder {
let sub_str = sub.to_string_lossy().replace('\\', "/");
if !sub_str.is_empty() && sub_str != "." {
if !container_folder.ends_with('/') {
container_folder.push('/');
}
container_folder.push_str(&sub_str);
}
}

let mut ws_path: String = self.path.to_string_lossy().into_owned();
let mut dc_path: String = dev_container.config_path.to_string_lossy().into_owned();
Expand Down