storkit: merge 433_story_setup_wizard_interviews_user_on_bare_projects_with_no_existing_code

This commit is contained in:
dave
2026-03-29 00:42:57 +00:00
parent a70a06a5fb
commit fec417cb16
3 changed files with 215 additions and 23 deletions
+139 -20
View File
@@ -153,7 +153,7 @@ pub(super) fn tool_wizard_generate(args: &Value, ctx: &AppContext) -> Result<Str
}
/// Return true if the project directory has no meaningful source files.
fn is_bare_project(project_root: &Path) -> bool {
pub(crate) fn is_bare_project(project_root: &Path) -> bool {
std::fs::read_dir(project_root)
.ok()
.map(|entries| {
@@ -175,7 +175,7 @@ fn is_bare_project(project_root: &Path) -> bool {
}
/// Return a generation hint for a step based on the project root.
fn generation_hint(step: WizardStep, project_root: &Path) -> String {
pub(crate) fn generation_hint(step: WizardStep, project_root: &Path) -> String {
let bare = is_bare_project(project_root);
match step {
@@ -214,30 +214,54 @@ fn generation_hint(step: WizardStep, project_root: &Path) -> String {
}
}
WizardStep::TestScript => {
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 mut cmds = Vec::new();
if has_cargo {
cmds.push("cargo nextest run");
}
if has_pkg {
cmds.push(if has_pnpm { "pnpm test" } else { "npm test" });
}
if cmds.is_empty() {
"Generate a `script/test` shell script (#!/usr/bin/env bash, set -euo pipefail) that runs the project's test suite.".to_string()
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/test` shell script (#!/usr/bin/env bash, set -euo pipefail) \
with appropriate test commands for their chosen language and framework."
.to_string()
} else {
format!(
"Generate a `script/test` shell script (#!/usr/bin/env bash, set -euo pipefail) that runs: {}",
cmds.join(", ")
)
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 mut cmds = Vec::new();
if has_cargo {
cmds.push("cargo nextest run");
}
if has_pkg {
cmds.push(if has_pnpm { "pnpm test" } else { "npm test" });
}
if cmds.is_empty() {
"Generate a `script/test` shell script (#!/usr/bin/env bash, set -euo pipefail) that runs the project's test suite.".to_string()
} else {
format!(
"Generate a `script/test` shell script (#!/usr/bin/env bash, set -euo pipefail) that runs: {}",
cmds.join(", ")
)
}
}
}
WizardStep::ReleaseScript => {
"Generate a `script/release` shell script (#!/usr/bin/env bash, set -euo pipefail) that builds and releases the project (e.g. `cargo build --release` or `npm run build`).".to_string()
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/release` shell script (#!/usr/bin/env bash, set -euo pipefail) \
with appropriate build/release commands for their chosen language and framework."
.to_string()
} else {
"Generate a `script/release` shell script (#!/usr/bin/env bash, set -euo pipefail) that builds and releases the project (e.g. `cargo build --release` or `npm run build`).".to_string()
}
}
WizardStep::TestCoverage => {
"Generate a `script/test_coverage` shell script (#!/usr/bin/env bash, set -euo pipefail) that generates a test coverage report (e.g. `cargo llvm-cov nextest` or `npm run coverage`).".to_string()
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/test_coverage` shell script (#!/usr/bin/env bash, set -euo pipefail) \
with appropriate test coverage commands for their chosen language and framework."
.to_string()
} else {
"Generate a `script/test_coverage` shell script (#!/usr/bin/env bash, set -euo pipefail) that generates a test coverage report (e.g. `cargo llvm-cov nextest` or `npm run coverage`).".to_string()
}
}
WizardStep::Scaffold => "Scaffold step is handled automatically by `storkit init`.".to_string(),
}
@@ -517,4 +541,99 @@ mod tests {
assert!(output.contains("Scaffold"));
assert!(output.contains("← current"));
}
#[test]
fn is_bare_project_detects_empty_dir() {
let dir = TempDir::new().unwrap();
assert!(is_bare_project(dir.path()));
}
#[test]
fn is_bare_project_detects_scaffold_only_dir() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
std::fs::write(dir.path().join("CLAUDE.md"), "# Claude").unwrap();
std::fs::write(dir.path().join("README.md"), "# Readme").unwrap();
std::fs::create_dir_all(dir.path().join("script")).unwrap();
assert!(is_bare_project(dir.path()));
}
#[test]
fn is_bare_project_false_when_source_files_exist() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
assert!(!is_bare_project(dir.path()));
}
#[test]
fn is_bare_project_false_with_src_directory() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
assert!(!is_bare_project(dir.path()));
}
#[test]
fn generation_hint_bare_context_asks_user() {
let dir = TempDir::new().unwrap();
// Bare project — only scaffolding
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
let hint = generation_hint(WizardStep::Context, dir.path());
assert!(hint.contains("bare project"));
assert!(hint.contains("Ask the user"));
}
#[test]
fn generation_hint_bare_stack_asks_user() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
let hint = generation_hint(WizardStep::Stack, dir.path());
assert!(hint.contains("bare project"));
assert!(hint.contains("Ask the user"));
}
#[test]
fn generation_hint_bare_test_script_references_stack() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
let hint = generation_hint(WizardStep::TestScript, dir.path());
assert!(hint.contains("bare project"));
assert!(hint.contains("STACK.md"));
}
#[test]
fn generation_hint_bare_release_script_references_stack() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
let hint = generation_hint(WizardStep::ReleaseScript, dir.path());
assert!(hint.contains("bare project"));
assert!(hint.contains("STACK.md"));
}
#[test]
fn generation_hint_bare_test_coverage_references_stack() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
let hint = generation_hint(WizardStep::TestCoverage, dir.path());
assert!(hint.contains("bare project"));
assert!(hint.contains("STACK.md"));
}
#[test]
fn generation_hint_existing_project_reads_code() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
let hint = generation_hint(WizardStep::Context, dir.path());
assert!(hint.contains("Read the project"));
assert!(!hint.contains("bare project"));
}
#[test]
fn generation_hint_existing_project_test_script_detects_cargo() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
let hint = generation_hint(WizardStep::TestScript, dir.path());
assert!(hint.contains("cargo nextest"));
assert!(!hint.contains("bare project"));
}
}