huskies: merge 589_story_wizard_auto_detects_project_components_and_configures_scripts_accordingly
This commit is contained in:
@@ -43,6 +43,8 @@ pub(crate) fn step_output_path(
|
|||||||
.join("STACK.md"),
|
.join("STACK.md"),
|
||||||
),
|
),
|
||||||
WizardStep::TestScript => Some(project_root.join("script").join("test")),
|
WizardStep::TestScript => Some(project_root.join("script").join("test")),
|
||||||
|
WizardStep::BuildScript => Some(project_root.join("script").join("build")),
|
||||||
|
WizardStep::LintScript => Some(project_root.join("script").join("lint")),
|
||||||
WizardStep::ReleaseScript => Some(project_root.join("script").join("release")),
|
WizardStep::ReleaseScript => Some(project_root.join("script").join("release")),
|
||||||
WizardStep::TestCoverage => Some(project_root.join("script").join("test_coverage")),
|
WizardStep::TestCoverage => Some(project_root.join("script").join("test_coverage")),
|
||||||
WizardStep::Scaffold => None,
|
WizardStep::Scaffold => None,
|
||||||
@@ -52,7 +54,11 @@ pub(crate) fn step_output_path(
|
|||||||
pub(crate) fn is_script_step(step: WizardStep) -> bool {
|
pub(crate) fn is_script_step(step: WizardStep) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
step,
|
step,
|
||||||
WizardStep::TestScript | WizardStep::ReleaseScript | WizardStep::TestCoverage
|
WizardStep::TestScript
|
||||||
|
| WizardStep::BuildScript
|
||||||
|
| WizardStep::LintScript
|
||||||
|
| WizardStep::ReleaseScript
|
||||||
|
| WizardStep::TestCoverage
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,6 +262,90 @@ pub(crate) fn generation_hint(step: WizardStep, project_root: &Path) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WizardStep::BuildScript => {
|
||||||
|
if bare {
|
||||||
|
"This is a bare project with no existing code. Read the STACK.md generated \
|
||||||
|
in the previous step (or ask the user about their stack if it was skipped) \
|
||||||
|
and generate a `script/build` shell script (#!/usr/bin/env bash, set -euo pipefail) \
|
||||||
|
with appropriate build commands for their chosen language and framework."
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
let has_cargo = project_root.join("Cargo.toml").exists();
|
||||||
|
let has_pkg = project_root.join("package.json").exists();
|
||||||
|
let has_pnpm = project_root.join("pnpm-lock.yaml").exists();
|
||||||
|
let has_frontend_subdir =
|
||||||
|
project_root.join("frontend").join("package.json").exists()
|
||||||
|
|| project_root.join("client").join("package.json").exists();
|
||||||
|
let has_go = project_root.join("go.mod").exists();
|
||||||
|
let mut cmds = Vec::new();
|
||||||
|
if has_cargo {
|
||||||
|
cmds.push("cargo build --release");
|
||||||
|
}
|
||||||
|
if has_pkg {
|
||||||
|
cmds.push(if has_pnpm {
|
||||||
|
"pnpm run build"
|
||||||
|
} else {
|
||||||
|
"npm run build"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if has_frontend_subdir {
|
||||||
|
cmds.push("(cd frontend && npm run build)");
|
||||||
|
}
|
||||||
|
if has_go {
|
||||||
|
cmds.push("go build ./...");
|
||||||
|
}
|
||||||
|
if cmds.is_empty() {
|
||||||
|
"Generate a `script/build` shell script (#!/usr/bin/env bash, set -euo pipefail) that builds the project.".to_string()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"Generate a `script/build` shell script (#!/usr/bin/env bash, set -euo pipefail) that runs: {}",
|
||||||
|
cmds.join(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WizardStep::LintScript => {
|
||||||
|
if bare {
|
||||||
|
"This is a bare project with no existing code. Read the STACK.md generated \
|
||||||
|
in the previous step (or ask the user about their stack if it was skipped) \
|
||||||
|
and generate a `script/lint` shell script (#!/usr/bin/env bash, set -euo pipefail) \
|
||||||
|
with appropriate lint commands for their chosen language and framework."
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
let has_cargo = project_root.join("Cargo.toml").exists();
|
||||||
|
let has_pkg = project_root.join("package.json").exists();
|
||||||
|
let has_pnpm = project_root.join("pnpm-lock.yaml").exists();
|
||||||
|
let has_python = project_root.join("pyproject.toml").exists()
|
||||||
|
|| project_root.join("requirements.txt").exists();
|
||||||
|
let has_go = project_root.join("go.mod").exists();
|
||||||
|
let mut cmds = Vec::new();
|
||||||
|
if has_cargo {
|
||||||
|
cmds.push("cargo fmt --all --check");
|
||||||
|
cmds.push("cargo clippy -- -D warnings");
|
||||||
|
}
|
||||||
|
if has_pkg {
|
||||||
|
cmds.push(if has_pnpm {
|
||||||
|
"pnpm run lint"
|
||||||
|
} else {
|
||||||
|
"npm run lint"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if has_python {
|
||||||
|
cmds.push("flake8 . (or ruff check . if ruff is configured)");
|
||||||
|
}
|
||||||
|
if has_go {
|
||||||
|
cmds.push("go vet ./...");
|
||||||
|
}
|
||||||
|
if cmds.is_empty() {
|
||||||
|
"Generate a `script/lint` shell script (#!/usr/bin/env bash, set -euo pipefail) that runs the project's linters.".to_string()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"Generate a `script/lint` shell script (#!/usr/bin/env bash, set -euo pipefail) that runs: {}",
|
||||||
|
cmds.join(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
WizardStep::ReleaseScript => {
|
WizardStep::ReleaseScript => {
|
||||||
if bare {
|
if bare {
|
||||||
"This is a bare project with no existing code. Read the STACK.md generated \
|
"This is a bare project with no existing code. Read the STACK.md generated \
|
||||||
@@ -554,8 +644,8 @@ mod tests {
|
|||||||
fn wizard_complete_returns_done_message() {
|
fn wizard_complete_returns_done_message() {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
let ctx = setup(&dir);
|
let ctx = setup(&dir);
|
||||||
// Skip all remaining steps.
|
// Skip all remaining steps (scaffold is pre-confirmed, so 7 remaining).
|
||||||
for _ in 0..5 {
|
for _ in 0..7 {
|
||||||
tool_wizard_skip(&ctx).unwrap();
|
tool_wizard_skip(&ctx).unwrap();
|
||||||
}
|
}
|
||||||
let result = tool_wizard_status(&ctx).unwrap();
|
let result = tool_wizard_status(&ctx).unwrap();
|
||||||
@@ -666,4 +756,61 @@ mod tests {
|
|||||||
assert!(hint.contains("cargo nextest"));
|
assert!(hint.contains("cargo nextest"));
|
||||||
assert!(!hint.contains("bare project"));
|
assert!(!hint.contains("bare project"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generation_hint_bare_build_script_references_stack() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||||
|
let hint = generation_hint(WizardStep::BuildScript, dir.path());
|
||||||
|
assert!(hint.contains("bare project"));
|
||||||
|
assert!(hint.contains("STACK.md"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generation_hint_bare_lint_script_references_stack() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
|
||||||
|
let hint = generation_hint(WizardStep::LintScript, dir.path());
|
||||||
|
assert!(hint.contains("bare project"));
|
||||||
|
assert!(hint.contains("STACK.md"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generation_hint_existing_project_build_script_detects_cargo() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
|
||||||
|
let hint = generation_hint(WizardStep::BuildScript, dir.path());
|
||||||
|
assert!(hint.contains("cargo build --release"));
|
||||||
|
assert!(!hint.contains("bare project"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generation_hint_existing_project_lint_script_detects_cargo() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
|
||||||
|
let hint = generation_hint(WizardStep::LintScript, dir.path());
|
||||||
|
assert!(hint.contains("cargo fmt --all --check"));
|
||||||
|
assert!(hint.contains("cargo clippy -- -D warnings"));
|
||||||
|
assert!(!hint.contains("bare project"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn step_output_path_build_script_returns_script_build() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let path = step_output_path(dir.path(), WizardStep::BuildScript).unwrap();
|
||||||
|
assert!(path.ends_with("script/build"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn step_output_path_lint_script_returns_script_lint() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let path = step_output_path(dir.path(), WizardStep::LintScript).unwrap();
|
||||||
|
assert!(path.ends_with("script/lint"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_script_step_includes_build_and_lint() {
|
||||||
|
assert!(is_script_step(WizardStep::BuildScript));
|
||||||
|
assert!(is_script_step(WizardStep::LintScript));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ mod tests {
|
|||||||
let body: serde_json::Value = resp.0.into_body().into_json().await.unwrap();
|
let body: serde_json::Value = resp.0.into_body().into_json().await.unwrap();
|
||||||
assert_eq!(body["current_step_index"], 1);
|
assert_eq!(body["current_step_index"], 1);
|
||||||
assert!(!body["completed"].as_bool().unwrap());
|
assert!(!body["completed"].as_bool().unwrap());
|
||||||
assert_eq!(body["steps"].as_array().unwrap().len(), 6);
|
assert_eq!(body["steps"].as_array().unwrap().len(), 8);
|
||||||
assert_eq!(body["steps"][0]["status"], "confirmed");
|
assert_eq!(body["steps"][0]["status"], "confirmed");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,11 +279,13 @@ mod tests {
|
|||||||
let (dir, client) = setup();
|
let (dir, client) = setup();
|
||||||
WizardState::init_if_missing(dir.path());
|
WizardState::init_if_missing(dir.path());
|
||||||
|
|
||||||
// Steps 2-6 (scaffold is already confirmed)
|
// Steps 2-8 (scaffold is already confirmed)
|
||||||
let steps = [
|
let steps = [
|
||||||
"context",
|
"context",
|
||||||
"stack",
|
"stack",
|
||||||
"test_script",
|
"test_script",
|
||||||
|
"build_script",
|
||||||
|
"lint_script",
|
||||||
"release_script",
|
"release_script",
|
||||||
"test_coverage",
|
"test_coverage",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -223,6 +223,139 @@ fn detect_node_test_cmd(pkg_dir: &Path) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detect the appropriate Node.js build command for a directory containing `package.json`.
|
||||||
|
fn detect_node_build_cmd(pkg_dir: &Path) -> String {
|
||||||
|
if pkg_dir.join("pnpm-lock.yaml").exists() {
|
||||||
|
"pnpm run build".to_string()
|
||||||
|
} else {
|
||||||
|
"npm run build".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect the appropriate Node.js lint command for a directory containing `package.json`.
|
||||||
|
///
|
||||||
|
/// Reads the `package.json` content to identify eslint. Falls back to
|
||||||
|
/// `npm run lint` or `pnpm run lint` based on which lock file is present.
|
||||||
|
fn detect_node_lint_cmd(pkg_dir: &Path) -> String {
|
||||||
|
let has_pnpm = pkg_dir.join("pnpm-lock.yaml").exists();
|
||||||
|
let content = std::fs::read_to_string(pkg_dir.join("package.json")).unwrap_or_default();
|
||||||
|
if content.contains("\"eslint\"") {
|
||||||
|
let pm = if has_pnpm { "pnpm" } else { "npx" };
|
||||||
|
return format!("{pm} eslint .");
|
||||||
|
}
|
||||||
|
if has_pnpm {
|
||||||
|
"pnpm run lint".to_string()
|
||||||
|
} else {
|
||||||
|
"npm run lint".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate `script/build` content for a new project at `root`.
|
||||||
|
///
|
||||||
|
/// Inspects well-known marker files to identify which tech stacks are present
|
||||||
|
/// and emits the appropriate build commands. Multi-stack projects get combined
|
||||||
|
/// commands run sequentially. Falls back to a generic stub when no markers
|
||||||
|
/// are found so the scaffold is always valid.
|
||||||
|
///
|
||||||
|
/// For projects with a frontend in a known subdirectory (`frontend/`, `client/`),
|
||||||
|
/// the build command is detected from the presence of `pnpm-lock.yaml`.
|
||||||
|
pub fn detect_script_build(root: &Path) -> String {
|
||||||
|
let mut commands: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
if root.join("Cargo.toml").exists() {
|
||||||
|
commands.push("cargo build --release".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.join("package.json").exists() {
|
||||||
|
commands.push(detect_node_build_cmd(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect frontend in known subdirectories (e.g. frontend/, client/)
|
||||||
|
for subdir in &["frontend", "client"] {
|
||||||
|
let sub_path = root.join(subdir);
|
||||||
|
if sub_path.join("package.json").exists() {
|
||||||
|
let cmd = detect_node_build_cmd(&sub_path);
|
||||||
|
commands.push(format!("(cd {} && {})", subdir, cmd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.join("pyproject.toml").exists() {
|
||||||
|
commands.push("python -m build".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.join("go.mod").exists() {
|
||||||
|
commands.push("go build ./...".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if commands.is_empty() {
|
||||||
|
return "#!/usr/bin/env bash\nset -euo pipefail\n\n# Add your project's build commands here.\necho \"No build configured\"\n".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut script = "#!/usr/bin/env bash\nset -euo pipefail\n\n".to_string();
|
||||||
|
for cmd in commands {
|
||||||
|
script.push_str(&cmd);
|
||||||
|
script.push('\n');
|
||||||
|
}
|
||||||
|
script
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate `script/lint` content for a new project at `root`.
|
||||||
|
///
|
||||||
|
/// Inspects well-known marker files to identify which linters are present
|
||||||
|
/// and emits the appropriate lint commands. Multi-stack projects get combined
|
||||||
|
/// commands run sequentially. Falls back to a generic stub when no markers
|
||||||
|
/// are found so the scaffold is always valid.
|
||||||
|
///
|
||||||
|
/// For projects with a frontend in a known subdirectory (`frontend/`, `client/`),
|
||||||
|
/// the lint command is detected from the `package.json` (eslint, npm, pnpm).
|
||||||
|
pub fn detect_script_lint(root: &Path) -> String {
|
||||||
|
let mut commands: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
if root.join("Cargo.toml").exists() {
|
||||||
|
commands.push("cargo fmt --all --check".to_string());
|
||||||
|
commands.push("cargo clippy -- -D warnings".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.join("package.json").exists() {
|
||||||
|
commands.push(detect_node_lint_cmd(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect frontend in known subdirectories (e.g. frontend/, client/)
|
||||||
|
for subdir in &["frontend", "client"] {
|
||||||
|
let sub_path = root.join(subdir);
|
||||||
|
if sub_path.join("package.json").exists() {
|
||||||
|
let cmd = detect_node_lint_cmd(&sub_path);
|
||||||
|
commands.push(format!("(cd {} && {})", subdir, cmd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.join("pyproject.toml").exists() || root.join("requirements.txt").exists() {
|
||||||
|
let mut content = std::fs::read_to_string(root.join("pyproject.toml")).unwrap_or_default();
|
||||||
|
content
|
||||||
|
.push_str(&std::fs::read_to_string(root.join("requirements.txt")).unwrap_or_default());
|
||||||
|
if content.contains("ruff") {
|
||||||
|
commands.push("ruff check .".to_string());
|
||||||
|
} else {
|
||||||
|
commands.push("flake8 .".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.join("go.mod").exists() {
|
||||||
|
commands.push("go vet ./...".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if commands.is_empty() {
|
||||||
|
return "#!/usr/bin/env bash\nset -euo pipefail\n\n# Add your project's lint commands here.\necho \"No linters configured\"\n".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut script = "#!/usr/bin/env bash\nset -euo pipefail\n\n".to_string();
|
||||||
|
for cmd in commands {
|
||||||
|
script.push_str(&cmd);
|
||||||
|
script.push('\n');
|
||||||
|
}
|
||||||
|
script
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate `script/test` content for a new project at `root`.
|
/// Generate `script/test` content for a new project at `root`.
|
||||||
///
|
///
|
||||||
/// Inspects well-known marker files to identify which tech stacks are present
|
/// Inspects well-known marker files to identify which tech stacks are present
|
||||||
@@ -449,6 +582,10 @@ pub(crate) fn scaffold_story_kit(root: &Path, port: u16) -> Result<(), String> {
|
|||||||
write_file_if_missing(&tech_root.join("STACK.md"), STORY_KIT_STACK)?;
|
write_file_if_missing(&tech_root.join("STACK.md"), STORY_KIT_STACK)?;
|
||||||
let script_test_content = detect_script_test(root);
|
let script_test_content = detect_script_test(root);
|
||||||
write_script_if_missing(&script_root.join("test"), &script_test_content)?;
|
write_script_if_missing(&script_root.join("test"), &script_test_content)?;
|
||||||
|
let script_build_content = detect_script_build(root);
|
||||||
|
write_script_if_missing(&script_root.join("build"), &script_build_content)?;
|
||||||
|
let script_lint_content = detect_script_lint(root);
|
||||||
|
write_script_if_missing(&script_root.join("lint"), &script_lint_content)?;
|
||||||
write_file_if_missing(&root.join("CLAUDE.md"), STORY_KIT_CLAUDE_MD)?;
|
write_file_if_missing(&root.join("CLAUDE.md"), STORY_KIT_CLAUDE_MD)?;
|
||||||
|
|
||||||
// Write per-transport bot.toml example files so users can see all options.
|
// Write per-transport bot.toml example files so users can see all options.
|
||||||
@@ -1387,6 +1524,347 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- detect_script_build ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_build_no_markers_returns_stub() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let script = detect_script_build(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("No build configured"),
|
||||||
|
"fallback should contain the generic stub message"
|
||||||
|
);
|
||||||
|
assert!(script.starts_with("#!/usr/bin/env bash"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_build_cargo_toml_adds_cargo_build_release() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_build(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("cargo build --release"),
|
||||||
|
"Rust project should run cargo build --release"
|
||||||
|
);
|
||||||
|
assert!(!script.contains("No build configured"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_build_package_json_npm_adds_npm_run_build() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(dir.path().join("package.json"), "{}").unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_build(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("npm run build"),
|
||||||
|
"Node project without pnpm-lock should run npm run build"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_build_package_json_pnpm_adds_pnpm_run_build() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(dir.path().join("package.json"), "{}").unwrap();
|
||||||
|
fs::write(dir.path().join("pnpm-lock.yaml"), "").unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_build(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("pnpm run build"),
|
||||||
|
"Node project with pnpm-lock should run pnpm run build"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!script.lines().any(|l| l.trim() == "npm run build"),
|
||||||
|
"should not use npm when pnpm-lock.yaml is present"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_build_go_mod_adds_go_build() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(dir.path().join("go.mod"), "module example.com/app\n").unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_build(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("go build ./..."),
|
||||||
|
"Go project should run go build ./..."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_build_pyproject_toml_adds_python_build() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(
|
||||||
|
dir.path().join("pyproject.toml"),
|
||||||
|
"[project]\nname = \"x\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_build(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("python -m build"),
|
||||||
|
"Python project should run python -m build"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_build_frontend_subdir_detected() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let frontend = dir.path().join("frontend");
|
||||||
|
fs::create_dir_all(&frontend).unwrap();
|
||||||
|
fs::write(frontend.join("package.json"), "{}").unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_build(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("cd frontend"),
|
||||||
|
"frontend subdir should be detected for build"
|
||||||
|
);
|
||||||
|
assert!(script.contains("npm run build"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_build_rust_plus_frontend_subdir_both_included() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(
|
||||||
|
dir.path().join("Cargo.toml"),
|
||||||
|
"[package]\nname = \"server\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let frontend = dir.path().join("frontend");
|
||||||
|
fs::create_dir_all(&frontend).unwrap();
|
||||||
|
fs::write(frontend.join("package.json"), "{}").unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_build(dir.path());
|
||||||
|
assert!(script.contains("cargo build --release"));
|
||||||
|
assert!(script.contains("cd frontend"));
|
||||||
|
assert!(script.contains("npm run build"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- detect_script_lint ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_lint_no_markers_returns_stub() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let script = detect_script_lint(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("No linters configured"),
|
||||||
|
"fallback should contain the generic stub message"
|
||||||
|
);
|
||||||
|
assert!(script.starts_with("#!/usr/bin/env bash"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_lint_cargo_toml_adds_fmt_and_clippy() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_lint(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("cargo fmt --all --check"),
|
||||||
|
"Rust project should check formatting"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
script.contains("cargo clippy -- -D warnings"),
|
||||||
|
"Rust project should run clippy"
|
||||||
|
);
|
||||||
|
assert!(!script.contains("No linters configured"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_lint_package_json_without_eslint_uses_npm_run_lint() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(dir.path().join("package.json"), "{}").unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_lint(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("npm run lint"),
|
||||||
|
"Node project without eslint dep should fall back to npm run lint"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_lint_package_json_with_eslint_uses_npx_eslint() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(
|
||||||
|
dir.path().join("package.json"),
|
||||||
|
r#"{"devDependencies":{"eslint":"^8.0.0"}}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_lint(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("npx eslint ."),
|
||||||
|
"Node project with eslint should use npx eslint ."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_lint_pnpm_with_eslint_uses_pnpm_eslint() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(
|
||||||
|
dir.path().join("package.json"),
|
||||||
|
r#"{"devDependencies":{"eslint":"^8.0.0"}}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fs::write(dir.path().join("pnpm-lock.yaml"), "").unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_lint(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("pnpm eslint ."),
|
||||||
|
"pnpm project with eslint should use pnpm eslint ."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_lint_python_requirements_uses_flake8() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(dir.path().join("requirements.txt"), "flask\n").unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_lint(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("flake8 ."),
|
||||||
|
"Python project without ruff should use flake8"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_lint_python_with_ruff_uses_ruff() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(
|
||||||
|
dir.path().join("pyproject.toml"),
|
||||||
|
"[project]\nname = \"x\"\n\n[tool.ruff]\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_lint(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("ruff check ."),
|
||||||
|
"Python project with ruff configured should use ruff"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!script.contains("flake8"),
|
||||||
|
"should not use flake8 when ruff is configured"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_lint_go_mod_adds_go_vet() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(dir.path().join("go.mod"), "module example.com/app\n").unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_lint(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("go vet ./..."),
|
||||||
|
"Go project should run go vet ./..."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_lint_frontend_subdir_detected() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let frontend = dir.path().join("frontend");
|
||||||
|
fs::create_dir_all(&frontend).unwrap();
|
||||||
|
fs::write(frontend.join("package.json"), "{}").unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_lint(dir.path());
|
||||||
|
assert!(
|
||||||
|
script.contains("cd frontend"),
|
||||||
|
"frontend subdir should be detected for lint"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detect_script_lint_rust_plus_frontend_subdir_both_included() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(
|
||||||
|
dir.path().join("Cargo.toml"),
|
||||||
|
"[package]\nname = \"server\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let frontend = dir.path().join("frontend");
|
||||||
|
fs::create_dir_all(&frontend).unwrap();
|
||||||
|
fs::write(frontend.join("package.json"), "{}").unwrap();
|
||||||
|
|
||||||
|
let script = detect_script_lint(dir.path());
|
||||||
|
assert!(script.contains("cargo fmt --all --check"));
|
||||||
|
assert!(script.contains("cargo clippy -- -D warnings"));
|
||||||
|
assert!(script.contains("cd frontend"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scaffold_story_kit_creates_script_build_and_lint() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
dir.path().join("script/build").exists(),
|
||||||
|
"script/build should be created by scaffold"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dir.path().join("script/lint").exists(),
|
||||||
|
"script/lint should be created by scaffold"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn scaffold_story_kit_creates_executable_script_build_and_lint() {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||||
|
|
||||||
|
for name in &["build", "lint"] {
|
||||||
|
let path = dir.path().join("script").join(name);
|
||||||
|
assert!(path.exists(), "script/{name} should be created");
|
||||||
|
let perms = fs::metadata(&path).unwrap().permissions();
|
||||||
|
assert!(
|
||||||
|
perms.mode() & 0o111 != 0,
|
||||||
|
"script/{name} should be executable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scaffold_script_build_contains_detected_commands_for_rust() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(
|
||||||
|
dir.path().join("Cargo.toml"),
|
||||||
|
"[package]\nname = \"myapp\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||||
|
|
||||||
|
let content = fs::read_to_string(dir.path().join("script/build")).unwrap();
|
||||||
|
assert!(
|
||||||
|
content.contains("cargo build --release"),
|
||||||
|
"Rust project scaffold should set cargo build --release in script/build"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scaffold_script_lint_contains_detected_commands_for_rust() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
fs::write(
|
||||||
|
dir.path().join("Cargo.toml"),
|
||||||
|
"[package]\nname = \"myapp\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
scaffold_story_kit(dir.path(), 3001).unwrap();
|
||||||
|
|
||||||
|
let content = fs::read_to_string(dir.path().join("script/lint")).unwrap();
|
||||||
|
assert!(
|
||||||
|
content.contains("cargo fmt --all --check"),
|
||||||
|
"Rust project scaffold should include fmt check in script/lint"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
content.contains("cargo clippy -- -D warnings"),
|
||||||
|
"Rust project scaffold should include clippy in script/lint"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// --- generate_project_toml ---
|
// --- generate_project_toml ---
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+11
-3
@@ -16,9 +16,13 @@ pub enum WizardStep {
|
|||||||
Stack,
|
Stack,
|
||||||
/// Step 4: create script/test
|
/// Step 4: create script/test
|
||||||
TestScript,
|
TestScript,
|
||||||
/// Step 5: create script/release
|
/// Step 5: create script/build
|
||||||
|
BuildScript,
|
||||||
|
/// Step 6: create script/lint
|
||||||
|
LintScript,
|
||||||
|
/// Step 7: create script/release
|
||||||
ReleaseScript,
|
ReleaseScript,
|
||||||
/// Step 6: create script/test_coverage
|
/// Step 8: create script/test_coverage
|
||||||
TestCoverage,
|
TestCoverage,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +33,8 @@ impl WizardStep {
|
|||||||
WizardStep::Context,
|
WizardStep::Context,
|
||||||
WizardStep::Stack,
|
WizardStep::Stack,
|
||||||
WizardStep::TestScript,
|
WizardStep::TestScript,
|
||||||
|
WizardStep::BuildScript,
|
||||||
|
WizardStep::LintScript,
|
||||||
WizardStep::ReleaseScript,
|
WizardStep::ReleaseScript,
|
||||||
WizardStep::TestCoverage,
|
WizardStep::TestCoverage,
|
||||||
];
|
];
|
||||||
@@ -40,6 +46,8 @@ impl WizardStep {
|
|||||||
WizardStep::Context => "Generate project context (00_CONTEXT.md)",
|
WizardStep::Context => "Generate project context (00_CONTEXT.md)",
|
||||||
WizardStep::Stack => "Generate tech stack spec (STACK.md)",
|
WizardStep::Stack => "Generate tech stack spec (STACK.md)",
|
||||||
WizardStep::TestScript => "Create test script (script/test)",
|
WizardStep::TestScript => "Create test script (script/test)",
|
||||||
|
WizardStep::BuildScript => "Create build script (script/build)",
|
||||||
|
WizardStep::LintScript => "Create lint script (script/lint)",
|
||||||
WizardStep::ReleaseScript => "Create release script (script/release)",
|
WizardStep::ReleaseScript => "Create release script (script/release)",
|
||||||
WizardStep::TestCoverage => "Create test coverage script (script/test_coverage)",
|
WizardStep::TestCoverage => "Create test coverage script (script/test_coverage)",
|
||||||
}
|
}
|
||||||
@@ -262,7 +270,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn default_state_has_all_steps_pending() {
|
fn default_state_has_all_steps_pending() {
|
||||||
let state = WizardState::default();
|
let state = WizardState::default();
|
||||||
assert_eq!(state.steps.len(), 6);
|
assert_eq!(state.steps.len(), 8);
|
||||||
for step in &state.steps {
|
for step in &state.steps {
|
||||||
assert_eq!(step.status, StepStatus::Pending);
|
assert_eq!(step.status, StepStatus::Pending);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user