huskies: merge 610_story_extract_wizard_service
This commit is contained in:
@@ -12,106 +12,60 @@
|
||||
//! 5. `wizard_retry` — discard staged content and regenerate from scratch
|
||||
|
||||
use crate::http::context::AppContext;
|
||||
use crate::io::wizard::{StepStatus, WizardState, WizardStep, format_wizard_state};
|
||||
use crate::io::wizard::WizardStep;
|
||||
use crate::service::wizard::state_machine;
|
||||
use crate::service::wizard::{self as svc};
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||
// ── Thin adapters (kept for callers in chat/commands/setup.rs) ────────────────
|
||||
|
||||
/// Return the filesystem path (relative to `project_root`) for a step's output.
|
||||
/// Return the filesystem path for a step's output file.
|
||||
///
|
||||
/// Returns `None` for `Scaffold` since that step has no single output file — it
|
||||
/// creates the full `.huskies/` directory structure and is handled by
|
||||
/// `huskies init` before the server starts.
|
||||
/// Pure path concatenation — delegates to `service::wizard::state_machine`.
|
||||
pub(crate) fn step_output_path(
|
||||
project_root: &Path,
|
||||
step: WizardStep,
|
||||
) -> Option<std::path::PathBuf> {
|
||||
match step {
|
||||
WizardStep::Context => Some(
|
||||
project_root
|
||||
.join(".huskies")
|
||||
.join("specs")
|
||||
.join("00_CONTEXT.md"),
|
||||
),
|
||||
WizardStep::Stack => Some(
|
||||
project_root
|
||||
.join(".huskies")
|
||||
.join("specs")
|
||||
.join("tech")
|
||||
.join("STACK.md"),
|
||||
),
|
||||
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::TestCoverage => Some(project_root.join("script").join("test_coverage")),
|
||||
WizardStep::Scaffold => None,
|
||||
}
|
||||
state_machine::step_output_path(project_root, step)
|
||||
}
|
||||
|
||||
/// Return true when `step` produces an executable script file.
|
||||
pub(crate) fn is_script_step(step: WizardStep) -> bool {
|
||||
matches!(
|
||||
step,
|
||||
WizardStep::TestScript
|
||||
| WizardStep::BuildScript
|
||||
| WizardStep::LintScript
|
||||
| WizardStep::ReleaseScript
|
||||
| WizardStep::TestCoverage
|
||||
)
|
||||
state_machine::is_script_step(step)
|
||||
}
|
||||
|
||||
/// Write `content` to `path`, skipping if the file already exists with real
|
||||
/// (non-template) content.
|
||||
/// Write `content` to `path`, skipping if the file already has real content.
|
||||
///
|
||||
/// Scaffold template files (those containing [`TEMPLATE_SENTINEL`]) are treated
|
||||
/// as placeholders and will be overwritten with the wizard-generated content.
|
||||
/// Files with real user content are never overwritten. For script steps the
|
||||
/// file is also made executable after writing.
|
||||
/// Delegates to `service::wizard::write_step_file`.
|
||||
pub(crate) fn write_if_missing(
|
||||
path: &Path,
|
||||
content: &str,
|
||||
executable: bool,
|
||||
) -> Result<bool, String> {
|
||||
use crate::io::onboarding::TEMPLATE_SENTINEL;
|
||||
if path.exists() {
|
||||
// Overwrite scaffold template placeholders; preserve real user content.
|
||||
let is_template = std::fs::read_to_string(path)
|
||||
.map(|s| s.contains(TEMPLATE_SENTINEL))
|
||||
.unwrap_or(false);
|
||||
if !is_template {
|
||||
return Ok(false); // real content already present — skip
|
||||
}
|
||||
}
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create directory {}: {e}", parent.display()))?;
|
||||
}
|
||||
fs::write(path, content).map_err(|e| format!("Failed to write {}: {e}", path.display()))?;
|
||||
|
||||
if executable {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(path)
|
||||
.map_err(|e| format!("Failed to read permissions: {e}"))?
|
||||
.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(path, perms)
|
||||
.map_err(|e| format!("Failed to set permissions: {e}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
svc::write_step_file(path, content, executable).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Serialise a `WizardStep` to its snake_case string (e.g. `"test_script"`).
|
||||
fn step_slug(step: WizardStep) -> String {
|
||||
serde_json::to_value(step)
|
||||
.ok()
|
||||
.and_then(|v| v.as_str().map(String::from))
|
||||
.unwrap_or_default()
|
||||
/// Return true when the project directory has no meaningful source files.
|
||||
///
|
||||
/// Delegates to `service::wizard::state_machine::is_bare_project` after
|
||||
/// reading directory entries via `service::wizard::io`.
|
||||
#[cfg(test)]
|
||||
fn is_bare_project(project_root: &Path) -> bool {
|
||||
use crate::service::wizard::io as wio;
|
||||
let names = wio::list_dir_names(project_root);
|
||||
state_machine::is_bare_project(&names)
|
||||
}
|
||||
|
||||
/// Return a generation hint for `step` based on the project at `project_root`.
|
||||
///
|
||||
/// Reads filesystem state then delegates pure logic to `state_machine`.
|
||||
pub(crate) fn generation_hint(step: WizardStep, project_root: &Path) -> String {
|
||||
use crate::service::wizard::io as wio;
|
||||
let names = wio::list_dir_names(project_root);
|
||||
let tools = wio::detect_project_tools(project_root);
|
||||
let is_bare = state_machine::is_bare_project(&names);
|
||||
state_machine::generation_hint(step, is_bare, &tools)
|
||||
}
|
||||
|
||||
// ── MCP tool handlers ─────────────────────────────────────────────────────────
|
||||
@@ -119,9 +73,7 @@ fn step_slug(step: WizardStep) -> String {
|
||||
/// `wizard_status` — return current wizard state as a human-readable summary.
|
||||
pub(super) fn tool_wizard_status(ctx: &AppContext) -> Result<String, String> {
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let state =
|
||||
WizardState::load(&root).ok_or("No wizard active. Run `huskies init` to begin setup.")?;
|
||||
Ok(format_wizard_state(&state))
|
||||
svc::status(&root).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// `wizard_generate` — mark the current step as generating or stage content.
|
||||
@@ -133,245 +85,8 @@ pub(super) fn tool_wizard_status(ctx: &AppContext) -> Result<String, String> {
|
||||
/// until `wizard_confirm` is called.
|
||||
pub(super) fn tool_wizard_generate(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let mut state = WizardState::load(&root).ok_or("No wizard active.")?;
|
||||
|
||||
if state.completed {
|
||||
return Ok("Wizard is already complete.".to_string());
|
||||
}
|
||||
|
||||
let current_idx = state.current_step_index();
|
||||
let step = state.steps[current_idx].step;
|
||||
|
||||
// If content is provided, stage it for confirmation.
|
||||
if let Some(content) = args.get("content").and_then(|v| v.as_str()) {
|
||||
state.set_step_status(
|
||||
step,
|
||||
StepStatus::AwaitingConfirmation,
|
||||
Some(content.to_string()),
|
||||
);
|
||||
state
|
||||
.save(&root)
|
||||
.map_err(|e| format!("Failed to save wizard state: {e}"))?;
|
||||
return Ok(format!(
|
||||
"Content staged for '{}'. Run `wizard_confirm` to write it to disk, `wizard_retry` to regenerate, or `wizard_skip` to skip.",
|
||||
step.label()
|
||||
));
|
||||
}
|
||||
|
||||
// No content provided — mark as generating and return a hint.
|
||||
state.set_step_status(step, StepStatus::Generating, None);
|
||||
state
|
||||
.save(&root)
|
||||
.map_err(|e| format!("Failed to save wizard state: {e}"))?;
|
||||
|
||||
let hint = generation_hint(step, &root);
|
||||
let slug = step_slug(step);
|
||||
|
||||
Ok(format!(
|
||||
"Step '{}' marked as generating.\n\n{hint}\n\nOnce you have the content, call `wizard_generate` again with a `content` argument (or PUT /wizard/step/{slug}/content). Then call `wizard_confirm` to write it to disk.",
|
||||
step.label(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Return true if the project directory has no meaningful source files.
|
||||
pub(crate) fn is_bare_project(project_root: &Path) -> bool {
|
||||
std::fs::read_dir(project_root)
|
||||
.ok()
|
||||
.map(|entries| {
|
||||
let names: Vec<String> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||
.collect();
|
||||
// A bare project only has huskies scaffolding and no real code
|
||||
names.iter().all(|n| {
|
||||
n.starts_with('.')
|
||||
|| n == "CLAUDE.md"
|
||||
|| n == "LICENSE"
|
||||
|| n == "README.md"
|
||||
|| n == "script"
|
||||
})
|
||||
})
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Return a generation hint for a step based on the project root.
|
||||
pub(crate) fn generation_hint(step: WizardStep, project_root: &Path) -> String {
|
||||
let bare = is_bare_project(project_root);
|
||||
|
||||
match step {
|
||||
WizardStep::Context => {
|
||||
if bare {
|
||||
"This is a bare project with no existing code. Ask the user what they want \
|
||||
to build — the project's purpose, goals, target users, and key features. \
|
||||
Then generate `.huskies/specs/00_CONTEXT.md` from their answers covering:\n\
|
||||
- High-level goal of the project\n\
|
||||
- Core features\n\
|
||||
- Domain concepts and entities\n\
|
||||
- Glossary of abbreviations and technical terms"
|
||||
.to_string()
|
||||
} else {
|
||||
"Read the project source tree and generate a `.huskies/specs/00_CONTEXT.md` describing:\n\
|
||||
- High-level goal of the project\n\
|
||||
- Core features\n\
|
||||
- Domain concepts and entities\n\
|
||||
- Glossary of abbreviations and technical terms".to_string()
|
||||
}
|
||||
}
|
||||
WizardStep::Stack => {
|
||||
if bare {
|
||||
"This is a bare project with no existing code. Ask the user what language, \
|
||||
frameworks, and tools they plan to use. Then generate `.huskies/specs/tech/STACK.md` \
|
||||
from their answers covering:\n\
|
||||
- Language, frameworks, and runtimes\n\
|
||||
- Coding standards and linting rules\n\
|
||||
- Quality gates (commands that must pass before merging)\n\
|
||||
- Approved libraries and their purpose".to_string()
|
||||
} else {
|
||||
"Read the project source tree and generate a `.huskies/specs/tech/STACK.md` describing:\n\
|
||||
- Language, frameworks, and runtimes\n\
|
||||
- Coding standards and linting rules\n\
|
||||
- Quality gates (commands that must pass before merging)\n\
|
||||
- Approved libraries and their purpose".to_string()
|
||||
}
|
||||
}
|
||||
WizardStep::TestScript => {
|
||||
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 {
|
||||
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::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 => {
|
||||
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 => {
|
||||
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 `huskies init`.".to_string()
|
||||
}
|
||||
}
|
||||
let content = args.get("content").and_then(|v| v.as_str());
|
||||
svc::generate(&root, content).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// `wizard_confirm` — confirm the current step and write its content to disk.
|
||||
@@ -382,111 +97,20 @@ pub(crate) fn generation_hint(step: WizardStep, project_root: &Path) -> String {
|
||||
/// advances to the next pending step.
|
||||
pub(super) fn tool_wizard_confirm(ctx: &AppContext) -> Result<String, String> {
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let mut state = WizardState::load(&root).ok_or("No wizard active.")?;
|
||||
|
||||
if state.completed {
|
||||
return Ok("Wizard is already complete.".to_string());
|
||||
}
|
||||
|
||||
let current_idx = state.current_step_index();
|
||||
let step = state.steps[current_idx].step;
|
||||
let content = state.steps[current_idx].content.clone();
|
||||
|
||||
// Write content to disk (only if a file path exists and the file is absent).
|
||||
let write_msg = if let (Some(c), Some(ref path)) = (&content, step_output_path(&root, step)) {
|
||||
let executable = is_script_step(step);
|
||||
match write_if_missing(path, c, executable)? {
|
||||
true => format!(" File written: `{}`.", path.display()),
|
||||
false => format!(" File `{}` already exists — skipped.", path.display()),
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
state
|
||||
.confirm_step(step)
|
||||
.map_err(|e| format!("Cannot confirm step: {e}"))?;
|
||||
state
|
||||
.save(&root)
|
||||
.map_err(|e| format!("Failed to save wizard state: {e}"))?;
|
||||
|
||||
let next_idx = state.current_step_index();
|
||||
if state.completed {
|
||||
Ok(format!(
|
||||
"Step '{}' confirmed.{write_msg}\n\nSetup wizard complete! All steps done.",
|
||||
step.label()
|
||||
))
|
||||
} else {
|
||||
let next = &state.steps[next_idx];
|
||||
Ok(format!(
|
||||
"Step '{}' confirmed.{write_msg}\n\nNext: {} — run `wizard_generate` to begin.",
|
||||
step.label(),
|
||||
next.step.label()
|
||||
))
|
||||
}
|
||||
svc::confirm(&root).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// `wizard_skip` — skip the current step without writing any file.
|
||||
pub(super) fn tool_wizard_skip(ctx: &AppContext) -> Result<String, String> {
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let mut state = WizardState::load(&root).ok_or("No wizard active.")?;
|
||||
|
||||
if state.completed {
|
||||
return Ok("Wizard is already complete.".to_string());
|
||||
}
|
||||
|
||||
let current_idx = state.current_step_index();
|
||||
let step = state.steps[current_idx].step;
|
||||
|
||||
state
|
||||
.skip_step(step)
|
||||
.map_err(|e| format!("Cannot skip step: {e}"))?;
|
||||
state
|
||||
.save(&root)
|
||||
.map_err(|e| format!("Failed to save wizard state: {e}"))?;
|
||||
|
||||
let next_idx = state.current_step_index();
|
||||
if state.completed {
|
||||
Ok(format!(
|
||||
"Step '{}' skipped. Setup wizard complete!",
|
||||
step.label()
|
||||
))
|
||||
} else {
|
||||
let next = &state.steps[next_idx];
|
||||
Ok(format!(
|
||||
"Step '{}' skipped.\n\nNext: {} — run `wizard_generate` to begin.",
|
||||
step.label(),
|
||||
next.step.label()
|
||||
))
|
||||
}
|
||||
svc::skip(&root).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// `wizard_retry` — discard staged content and reset the current step to
|
||||
/// `Pending` so it can be regenerated.
|
||||
pub(super) fn tool_wizard_retry(ctx: &AppContext) -> Result<String, String> {
|
||||
let root = ctx.state.get_project_root()?;
|
||||
let mut state = WizardState::load(&root).ok_or("No wizard active.")?;
|
||||
|
||||
if state.completed {
|
||||
return Ok("Wizard is already complete.".to_string());
|
||||
}
|
||||
|
||||
let current_idx = state.current_step_index();
|
||||
let step = state.steps[current_idx].step;
|
||||
|
||||
// Clear content and reset to pending.
|
||||
if let Some(s) = state.steps.iter_mut().find(|s| s.step == step) {
|
||||
s.status = StepStatus::Pending;
|
||||
s.content = None;
|
||||
}
|
||||
state
|
||||
.save(&root)
|
||||
.map_err(|e| format!("Failed to save wizard state: {e}"))?;
|
||||
|
||||
Ok(format!(
|
||||
"Step '{}' reset to pending. Run `wizard_generate` to regenerate content.",
|
||||
step.label()
|
||||
))
|
||||
svc::retry(&root).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ── tests ─────────────────────────────────────────────────────────────────────
|
||||
@@ -495,6 +119,7 @@ pub(super) fn tool_wizard_retry(ctx: &AppContext) -> Result<String, String> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::http::context::AppContext;
|
||||
use crate::io::wizard::{StepStatus, WizardState, format_wizard_state};
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn setup(dir: &TempDir) -> AppContext {
|
||||
|
||||
+11
-31
@@ -1,6 +1,7 @@
|
||||
//! HTTP wizard endpoints — REST API for the project setup wizard.
|
||||
use crate::http::context::{AppContext, OpenApiResult, bad_request, not_found};
|
||||
use crate::io::wizard::{StepStatus, WizardState, WizardStep};
|
||||
use crate::io::wizard::{WizardState, WizardStep};
|
||||
use crate::service::wizard as svc;
|
||||
use poem_openapi::{Object, OpenApi, Tags, param::Path, payload::Json};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
@@ -80,8 +81,7 @@ impl WizardApi {
|
||||
#[oai(path = "/wizard", method = "get")]
|
||||
async fn get_wizard_state(&self) -> OpenApiResult<Json<WizardResponse>> {
|
||||
let root = self.ctx.state.get_project_root().map_err(bad_request)?;
|
||||
let state =
|
||||
WizardState::load(&root).ok_or_else(|| not_found("No wizard active".to_string()))?;
|
||||
let state = svc::get_state(&root).map_err(|_| not_found("No wizard active".to_string()))?;
|
||||
Ok(Json(WizardResponse::from(&state)))
|
||||
}
|
||||
|
||||
@@ -97,16 +97,8 @@ impl WizardApi {
|
||||
) -> OpenApiResult<Json<WizardResponse>> {
|
||||
let root = self.ctx.state.get_project_root().map_err(bad_request)?;
|
||||
let wizard_step = parse_step(&step.0)?;
|
||||
let mut state =
|
||||
WizardState::load(&root).ok_or_else(|| not_found("No wizard active".to_string()))?;
|
||||
|
||||
state.set_step_status(
|
||||
wizard_step,
|
||||
StepStatus::AwaitingConfirmation,
|
||||
payload.0.content,
|
||||
);
|
||||
state.save(&root).map_err(bad_request)?;
|
||||
|
||||
let state = svc::set_step_content(&root, wizard_step, payload.0.content)
|
||||
.map_err(|e| bad_request(e.to_string()))?;
|
||||
Ok(Json(WizardResponse::from(&state)))
|
||||
}
|
||||
|
||||
@@ -117,12 +109,8 @@ impl WizardApi {
|
||||
async fn confirm_step(&self, step: Path<String>) -> OpenApiResult<Json<WizardResponse>> {
|
||||
let root = self.ctx.state.get_project_root().map_err(bad_request)?;
|
||||
let wizard_step = parse_step(&step.0)?;
|
||||
let mut state =
|
||||
WizardState::load(&root).ok_or_else(|| not_found("No wizard active".to_string()))?;
|
||||
|
||||
state.confirm_step(wizard_step).map_err(bad_request)?;
|
||||
state.save(&root).map_err(bad_request)?;
|
||||
|
||||
let state =
|
||||
svc::mark_step_confirmed(&root, wizard_step).map_err(|e| bad_request(e.to_string()))?;
|
||||
Ok(Json(WizardResponse::from(&state)))
|
||||
}
|
||||
|
||||
@@ -133,12 +121,8 @@ impl WizardApi {
|
||||
async fn skip_step(&self, step: Path<String>) -> OpenApiResult<Json<WizardResponse>> {
|
||||
let root = self.ctx.state.get_project_root().map_err(bad_request)?;
|
||||
let wizard_step = parse_step(&step.0)?;
|
||||
let mut state =
|
||||
WizardState::load(&root).ok_or_else(|| not_found("No wizard active".to_string()))?;
|
||||
|
||||
state.skip_step(wizard_step).map_err(bad_request)?;
|
||||
state.save(&root).map_err(bad_request)?;
|
||||
|
||||
let state =
|
||||
svc::mark_step_skipped(&root, wizard_step).map_err(|e| bad_request(e.to_string()))?;
|
||||
Ok(Json(WizardResponse::from(&state)))
|
||||
}
|
||||
|
||||
@@ -147,12 +131,8 @@ impl WizardApi {
|
||||
async fn mark_generating(&self, step: Path<String>) -> OpenApiResult<Json<WizardResponse>> {
|
||||
let root = self.ctx.state.get_project_root().map_err(bad_request)?;
|
||||
let wizard_step = parse_step(&step.0)?;
|
||||
let mut state =
|
||||
WizardState::load(&root).ok_or_else(|| not_found("No wizard active".to_string()))?;
|
||||
|
||||
state.set_step_status(wizard_step, StepStatus::Generating, None);
|
||||
state.save(&root).map_err(bad_request)?;
|
||||
|
||||
let state = svc::mark_step_generating(&root, wizard_step)
|
||||
.map_err(|e| bad_request(e.to_string()))?;
|
||||
Ok(Json(WizardResponse::from(&state)))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user