Skip to content

Commit d053a7b

Browse files
committed
feat: add interactive init wizard with dialoguer prompts
Replace hardcoded defaults in `dual init` with a 5-step interactive wizard (image selection, ports, setup command, summary/confirm). Add --yes/-y flag for non-interactive mode. Move file-writing logic from config.rs into new init module. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Entire-Checkpoint: cc13ef3e9119
1 parent 3324d66 commit d053a7b

File tree

7 files changed

+504
-105
lines changed

7 files changed

+504
-105
lines changed

Cargo.lock

Lines changed: 68 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ license = "MIT"
99
[dependencies]
1010
clap = { version = "4", features = ["derive"] }
1111
crossterm = "0.28"
12+
dialoguer = "0.11"
1213
dirs = "6"
1314
fs2 = "0.4"
1415
http-body-util = "0.1"

src/cli.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub enum Command {
1818
/// Short name for the repo (derived from directory name if omitted)
1919
#[arg(short, long)]
2020
name: Option<String>,
21+
/// Accept all defaults without prompts
22+
#[arg(short, long)]
23+
yes: bool,
2124
},
2225

2326
/// Create a new branch workspace for an existing repo

src/config.rs

Lines changed: 5 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -203,42 +203,6 @@ fn merge_config(dual: &DualConfig, dc_hints: Option<&RepoHints>) -> RepoHints {
203203
}
204204
}
205205

206-
/// Write a default .dual/settings.json with Dual-specific fields only.
207-
/// Container config belongs in devcontainer.json.
208-
/// Creates the .dual/ directory if it doesn't exist.
209-
pub fn write_default_dual_config(
210-
repo_root: &Path,
211-
devcontainer_path: &str,
212-
) -> Result<(), HintsError> {
213-
let dual_dir = repo_root.join(DUAL_DIR);
214-
std::fs::create_dir_all(&dual_dir).map_err(|e| HintsError::WriteError(dual_dir.clone(), e))?;
215-
216-
let config = DualConfig {
217-
devcontainer: devcontainer_path.to_string(),
218-
..Default::default()
219-
};
220-
221-
let path = dual_dir.join(SETTINGS_FILENAME);
222-
let contents = serde_json::to_string_pretty(&config).map_err(HintsError::JsonSerializeError)?;
223-
std::fs::write(&path, contents).map_err(|e| HintsError::WriteError(path, e))?;
224-
Ok(())
225-
}
226-
227-
/// Write a default devcontainer.json with minimal container config.
228-
/// Creates .devcontainer/ directory if it doesn't exist.
229-
pub fn write_default_devcontainer(repo_root: &Path) -> Result<(), HintsError> {
230-
let dc_dir = repo_root.join(".devcontainer");
231-
std::fs::create_dir_all(&dc_dir).map_err(|e| HintsError::WriteError(dc_dir.clone(), e))?;
232-
233-
let dc_path = dc_dir.join("devcontainer.json");
234-
let content = r#"{
235-
"image": "node:20"
236-
}
237-
"#;
238-
std::fs::write(&dc_path, content).map_err(|e| HintsError::WriteError(dc_path, e))?;
239-
Ok(())
240-
}
241-
242206
/// Write DualConfig to a workspace directory's .dual/settings.json.
243207
pub fn write_dual_config(workspace_dir: &Path, config: &DualConfig) -> Result<(), HintsError> {
244208
let dual_dir = workspace_dir.join(DUAL_DIR);
@@ -509,7 +473,11 @@ mod tests {
509473
.unwrap();
510474

511475
// Must also have .dual/settings.json
512-
write_default_dual_config(&dir, ".devcontainer/devcontainer.json").unwrap();
476+
let config = DualConfig {
477+
devcontainer: ".devcontainer/devcontainer.json".to_string(),
478+
..Default::default()
479+
};
480+
write_dual_config(&dir, &config).unwrap();
513481

514482
let hints = load_hints(&dir).unwrap();
515483
assert_eq!(hints.image, "python:3.12");
@@ -577,49 +545,6 @@ mod tests {
577545
let _ = std::fs::remove_dir_all(&dir);
578546
}
579547

580-
#[test]
581-
fn write_default_dual_config_creates_json() {
582-
let dir = std::env::temp_dir().join("dual-test-default-dual-config");
583-
let _ = std::fs::remove_dir_all(&dir);
584-
std::fs::create_dir_all(&dir).unwrap();
585-
586-
write_default_dual_config(&dir, ".devcontainer/devcontainer.json").unwrap();
587-
588-
let path = dir.join(".dual").join("settings.json");
589-
assert!(path.exists());
590-
591-
let content = std::fs::read_to_string(&path).unwrap();
592-
assert!(content.contains("devcontainer"));
593-
assert!(content.contains(".devcontainer/devcontainer.json"));
594-
595-
// Verify it's parseable as valid DualConfig
596-
let config = load_dual_config(&dir).unwrap();
597-
assert_eq!(config.devcontainer, ".devcontainer/devcontainer.json");
598-
599-
let _ = std::fs::remove_dir_all(&dir);
600-
}
601-
602-
#[test]
603-
fn write_default_devcontainer_creates_dir_and_file() {
604-
let dir = std::env::temp_dir().join("dual-test-default-devcontainer");
605-
let _ = std::fs::remove_dir_all(&dir);
606-
std::fs::create_dir_all(&dir).unwrap();
607-
608-
write_default_devcontainer(&dir).unwrap();
609-
610-
let dc_path = dir.join(".devcontainer").join("devcontainer.json");
611-
assert!(dc_path.exists());
612-
613-
let content = std::fs::read_to_string(&dc_path).unwrap();
614-
assert!(content.contains("\"image\": \"node:20\""));
615-
616-
// Verify it's valid JSON
617-
let dc: crate::devcontainer::DevcontainerJson = serde_json::from_str(&content).unwrap();
618-
assert_eq!(dc.image.as_deref(), Some("node:20"));
619-
620-
let _ = std::fs::remove_dir_all(&dir);
621-
}
622-
623548
#[test]
624549
fn write_and_load_dual_config_roundtrip() {
625550
let dir = std::env::temp_dir().join("dual-test-dual-config-roundtrip");

0 commit comments

Comments
 (0)