nika-init 0.64.0

Nika project scaffolding — course generator, workflow templates, showcase
Documentation
//! Minimal init workflows — 1 per verb
//!
//! Five workflows demonstrating the five Nika verbs:
//! exec, fetch, infer, invoke, agent.
//!
//! These are generated by `nika init` as starter examples.
//! LLM workflows use `{{PROVIDER}}` and `{{MODEL}}` placeholders
//! that are replaced at generation time.

use super::WorkflowTemplate;

/// Exec verb — shell commands, env vars, piping
pub const MINIMAL_EXEC: &str = r##"# ═══════════════════════════════════════════════════════════════════
# exec: — Run shell commands
# ═══════════════════════════════════════════════════════════════════
#
# The simplest Nika verb. No API key, no provider — just your shell.
#
# Run:  nika run workflows/01-exec.nika.yaml

schema: "nika/workflow@0.12"
workflow: exec-basics
description: "Shell commands with exec:"

tasks:
  - id: hello
    exec:
      command: echo "Hello from Nika!"

  - id: system_info
    exec:
      command: uname -a
      timeout: 10

  - id: count_files
    depends_on: [hello]
    exec:
      command: ls -1 | wc -l
      shell: true
"##;

/// Fetch verb — HTTP requests, JSON APIs
pub const MINIMAL_FETCH: &str = r##"# ═══════════════════════════════════════════════════════════════════
# fetch: — HTTP requests
# ═══════════════════════════════════════════════════════════════════
#
# Make HTTP requests. No API key needed for public endpoints.
#
# Run:  nika run workflows/02-fetch.nika.yaml

schema: "nika/workflow@0.12"
workflow: fetch-basics
description: "HTTP requests with fetch:"

tasks:
  - id: get_ip
    fetch:
      url: "https://httpbin.org/ip"
      method: GET

  - id: post_data
    fetch:
      url: "https://httpbin.org/post"
      method: POST
      headers:
        Content-Type: "application/json"
      body: '{"message": "Hello from Nika"}'

  - id: show_result
    depends_on: [get_ip]
    with:
      ip_data: $get_ip
    exec:
      command: echo "Your IP data — {{with.ip_data}}"
"##;

/// Infer verb — LLM generation
pub const MINIMAL_INFER: &str = r##"# ═══════════════════════════════════════════════════════════════════
# infer: — LLM generation
# ═══════════════════════════════════════════════════════════════════
#
# Send prompts to an LLM. Requires a provider API key.
#
# Setup: nika provider set {{PROVIDER}}
# Run:   nika run workflows/03-infer.nika.yaml

schema: "nika/workflow@0.12"
workflow: infer-basics
description: "LLM prompts with infer:"

tasks:
  - id: haiku
    infer:
      model: "{{MODEL}}"
      prompt: "Write a haiku about open source software."
      temperature: 0.8
      max_tokens: 100

  - id: explain
    infer:
      model: "{{MODEL}}"
      system: "You are a concise technical writer."
      prompt: "Explain what a DAG is in 2 sentences."
      temperature: 0.3
      max_tokens: 200

  - id: combine
    depends_on: [haiku, explain]
    with:
      poem: $haiku
      definition: $explain
    infer:
      model: "{{MODEL}}"
      prompt: |
        Combine these into a short blog post intro:

        HAIKU:
        {{with.poem}}

        DAG DEFINITION:
        {{with.definition}}
      temperature: 0.5
      max_tokens: 400
"##;

/// Invoke verb — MCP tool calls
pub const MINIMAL_INVOKE: &str = r##"# ═══════════════════════════════════════════════════════════════════
# invoke: — MCP tool calls
# ═══════════════════════════════════════════════════════════════════
#
# Call builtin tools or external MCP servers.
# Builtin tools (nika:*) need no setup.
#
# Run:  nika run workflows/04-invoke.nika.yaml

schema: "nika/workflow@0.12"
workflow: invoke-basics
description: "Tool calls with invoke:"

tasks:
  - id: log_start
    invoke:
      tool: "nika:log"
      params:
        message: "Workflow started"
        level: info

  - id: emit_data
    invoke:
      tool: "nika:emit"
      params:
        key: "greeting"
        value: "Hello from invoke!"

  - id: assert_check
    depends_on: [emit_data]
    invoke:
      tool: "nika:assert"
      params:
        condition: true
        message: "Emit succeeded"
"##;

/// Agent verb — multi-turn LLM loop with tools
pub const MINIMAL_AGENT: &str = r##"# ═══════════════════════════════════════════════════════════════════
# agent: — Multi-turn LLM with tools
# ═══════════════════════════════════════════════════════════════════
#
# An autonomous agent that can use tools in a loop.
# Requires a provider API key.
#
# Setup: nika provider set {{PROVIDER}}
# Run:   nika run workflows/05-agent.nika.yaml

schema: "nika/workflow@0.12"
workflow: agent-basics
description: "Multi-turn agent with tools"

tasks:
  - id: scout
    agent:
      model: "{{MODEL}}"
      system: |
        You are a helpful file scout. List the files in the current
        directory and describe what you find. Be concise.
      prompt: "What files are in this project?"
      tools:
        - "nika:glob"
        - "nika:read"
      max_turns: 5
      stop_condition: "answer_found"
"##;

/// Return minimal workflow templates (5 total, 1 per verb)
pub fn get_minimal_workflows() -> Vec<WorkflowTemplate> {
    vec![
        WorkflowTemplate {
            filename: "01-exec.nika.yaml",
            tier_dir: "minimal",
            content: MINIMAL_EXEC,
        },
        WorkflowTemplate {
            filename: "02-fetch.nika.yaml",
            tier_dir: "minimal",
            content: MINIMAL_FETCH,
        },
        WorkflowTemplate {
            filename: "03-infer.nika.yaml",
            tier_dir: "minimal",
            content: MINIMAL_INFER,
        },
        WorkflowTemplate {
            filename: "04-invoke.nika.yaml",
            tier_dir: "minimal",
            content: MINIMAL_INVOKE,
        },
        WorkflowTemplate {
            filename: "05-agent.nika.yaml",
            tier_dir: "minimal",
            content: MINIMAL_AGENT,
        },
    ]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_minimal_workflow_count() {
        let workflows = get_minimal_workflows();
        assert_eq!(
            workflows.len(),
            5,
            "Should have exactly 5 minimal workflows"
        );
    }

    #[test]
    fn test_minimal_filenames_unique() {
        let workflows = get_minimal_workflows();
        let mut names: Vec<&str> = workflows.iter().map(|w| w.filename).collect();
        let len = names.len();
        names.sort();
        names.dedup();
        assert_eq!(names.len(), len, "All filenames must be unique");
    }

    #[test]
    fn test_minimal_all_have_schema() {
        let workflows = get_minimal_workflows();
        for w in &workflows {
            assert!(
                w.content.contains("schema: \"nika/workflow@0.12\""),
                "Workflow {} must declare schema",
                w.filename
            );
        }
    }

    #[test]
    fn test_minimal_all_have_tasks() {
        let workflows = get_minimal_workflows();
        for w in &workflows {
            assert!(
                w.content.contains("tasks:"),
                "Workflow {} must have tasks section",
                w.filename
            );
        }
    }

    #[test]
    fn test_minimal_all_nika_yaml_extension() {
        let workflows = get_minimal_workflows();
        for w in &workflows {
            assert!(
                w.filename.ends_with(".nika.yaml"),
                "Workflow {} must end with .nika.yaml",
                w.filename
            );
        }
    }

    #[test]
    fn test_minimal_verbs_covered() {
        let workflows = get_minimal_workflows();
        let all_content: String = workflows.iter().map(|w| w.content).collect();
        assert!(all_content.contains("exec:"), "Must cover exec: verb");
        assert!(all_content.contains("fetch:"), "Must cover fetch: verb");
        assert!(all_content.contains("infer:"), "Must cover infer: verb");
        assert!(all_content.contains("invoke:"), "Must cover invoke: verb");
        assert!(all_content.contains("agent:"), "Must cover agent: verb");
    }

    #[test]
    fn test_minimal_valid_yaml() {
        let workflows = get_minimal_workflows();
        for w in &workflows {
            // Skip YAML validation for templates with {{PROVIDER}} / {{MODEL}} placeholders
            if w.content.contains("{{PROVIDER}}") || w.content.contains("{{MODEL}}") {
                continue;
            }
            let parsed: Result<serde_json::Value, _> = serde_saphyr::from_str(w.content);
            assert!(
                parsed.is_ok(),
                "Workflow {} must be valid YAML: {:?}",
                w.filename,
                parsed.err()
            );
        }
    }
}