Skip to main content

nika_init/
lib.rs

1//! # Nika Init — Project scaffolding
2//!
3//! This crate contains workflow templates and the interactive course
4//! generated by `nika init`.
5//!
6//! ## Organization
7//!
8//! - **minimal**: 5 starter workflows (1 per verb: exec, fetch, infer, invoke, agent)
9//! - **course**: 12-level interactive learning path with checks, hints, and progress
10//! - **context**: Reusable context files for workflows
11//! - **schemas**: JSON schemas for structured output validation
12//!
13//! ## Usage
14//!
15//! ```rust,ignore
16//! use nika_init::{get_all_workflows, get_all_context_files, get_all_schemas, WORKFLOWS_README};
17//! ```
18
19pub mod error;
20
21mod context;
22pub mod course;
23mod minimal;
24mod schemas;
25mod showcase_advanced;
26mod showcase_fetch;
27mod showcase_infra;
28mod showcase_patterns;
29
30pub use context::*;
31pub use minimal::*;
32pub use schemas::*;
33pub use showcase_advanced::*;
34pub use showcase_fetch::*;
35pub use showcase_infra::*;
36pub use showcase_patterns::*;
37
38/// Workflow definition with metadata
39pub struct WorkflowTemplate {
40    /// Filename (e.g., "01-exec.nika.yaml")
41    pub filename: &'static str,
42    /// Subdirectory (e.g., "minimal")
43    pub tier_dir: &'static str,
44    /// YAML content
45    pub content: &'static str,
46}
47
48/// Context file definition
49pub struct ContextFile {
50    pub filename: &'static str,
51    pub dir: &'static str,
52    pub content: &'static str,
53}
54
55/// All workflows (5 minimal + 15 patterns + 15 advanced + 15 infra + 15 fetch)
56pub fn get_all_workflows() -> Vec<WorkflowTemplate> {
57    let mut all = minimal::get_minimal_workflows();
58    all.extend(showcase_patterns::get_showcase_workflows());
59    all.extend(showcase_advanced::get_showcase_advanced_workflows());
60    all.extend(showcase_infra::get_showcase_infra_workflows());
61    all.extend(showcase_fetch::get_showcase_fetch_workflows());
62    all
63}
64
65pub fn get_all_context_files() -> Vec<ContextFile> {
66    context::get_context_files()
67}
68
69pub fn get_all_schemas() -> Vec<ContextFile> {
70    schemas::get_schema_files()
71}
72
73/// README content for workflows directory
74pub const WORKFLOWS_README: &str = r#"# Nika Workflows
75
76> 5 minimal starter workflows + interactive course with 12 levels.
77
78## Quick Start
79
80```bash
81# 1. Run immediately (no API key needed)
82nika run workflows/minimal/01-exec.nika.yaml
83nika run workflows/minimal/02-fetch.nika.yaml
84
85# 2. Setup LLM provider, then run LLM workflows
86nika provider set anthropic       # or: openai, mistral, groq, deepseek, gemini
87nika run workflows/minimal/03-infer.nika.yaml
88nika run workflows/minimal/04-invoke.nika.yaml
89nika run workflows/minimal/05-agent.nika.yaml
90```
91
92## Minimal Starters (5 workflows)
93
94| # | File | Verb | Prerequisites |
95|---|------|------|---------------|
96| 01 | `01-exec.nika.yaml` | `exec:` | None |
97| 02 | `02-fetch.nika.yaml` | `fetch:` | None |
98| 03 | `03-infer.nika.yaml` | `infer:` | LLM provider |
99| 04 | `04-invoke.nika.yaml` | `invoke:` | None (builtins) |
100| 05 | `05-agent.nika.yaml` | `agent:` | LLM provider |
101
102## Interactive Course (12 levels)
103
104Start the course to learn Nika from zero to production:
105
106```bash
107nika course next
108```
109
110| # | Level | Exercises | Theme |
111|---|-------|-----------|-------|
112| 01 | Jailbreak | 5 | Break free — exec: basics |
113| 02 | Hot Wire | 4 | Network — fetch: HTTP |
114| 03 | Fork Bomb | 4 | Multiply — DAG patterns |
115| 04 | Root Access | 3 | Unlock LLM — infer: |
116| 05 | Shapeshifter | 3 | Transform — with: bindings |
117| 06 | Pay-Per-Dream | 3 | Structured output |
118| 07 | Swiss Knife | 3 | Builtin tools — invoke: |
119| 08 | Gone Rogue | 3 | Autonomous — agent: |
120| 09 | Data Heist | 4 | Extraction — fetch: extract |
121| 10 | Open Protocol | 3 | MCP integration |
122| 11 | Pixel Pirate | 4 | Media pipeline |
123| 12 | SuperNovae | 5 | Boss — full orchestration |
124
125## Provider Setup
126
127```bash
128nika provider list                   # Check available providers
129nika provider set anthropic          # Claude (recommended)
130nika provider set openai             # GPT-4
131nika provider set mistral            # Mistral Large
132nika provider set groq               # Groq (fast, free tier)
133nika provider set deepseek           # DeepSeek
134nika provider set gemini             # Google Gemini
135```
136
137## Learn More
138
139- [Nika Documentation](https://github.com/supernovae-st/nika)
140- [NovaNet](https://github.com/supernovae-st/novanet)
141"#;
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_all_workflows_count() {
149        let workflows = get_all_workflows();
150        assert_eq!(
151            workflows.len(),
152            65,
153            "Should have 5 minimal + 15 patterns + 15 advanced + 15 infra + 15 fetch"
154        );
155    }
156
157    #[test]
158    fn test_workflow_filenames_unique() {
159        let workflows = get_all_workflows();
160        let mut f: Vec<&str> = workflows.iter().map(|w| w.filename).collect();
161        let n = f.len();
162        f.sort();
163        f.dedup();
164        assert_eq!(f.len(), n);
165    }
166
167    #[test]
168    fn test_workflows_have_schema() {
169        let workflows = get_all_workflows();
170        for w in &workflows {
171            assert!(
172                w.content.contains("schema: \"nika/workflow@0.12\""),
173                "Workflow {} should have schema declaration",
174                w.filename
175            );
176        }
177    }
178
179    #[test]
180    fn test_workflows_have_workflow_name() {
181        let workflows = get_all_workflows();
182        for w in &workflows {
183            assert!(
184                w.content.contains("workflow:"),
185                "Workflow {} should have workflow: declaration",
186                w.filename
187            );
188        }
189    }
190
191    #[test]
192    fn test_workflows_have_tasks() {
193        for w in get_all_workflows() {
194            assert!(w.content.contains("tasks:"));
195        }
196    }
197
198    #[test]
199    fn test_context_files_exist() {
200        assert!(get_all_context_files().len() >= 2);
201    }
202
203    #[test]
204    fn test_schema_files_valid_json() {
205        let files = get_all_schemas();
206        for f in &files {
207            assert!(
208                f.filename.ends_with(".schema.json"),
209                "Schema {} should end with .schema.json",
210                f.filename
211            );
212            let parsed: Result<serde_json::Value, _> = serde_json::from_str(f.content);
213            assert!(
214                parsed.is_ok(),
215                "Schema {} should be valid JSON: {:?}",
216                f.filename,
217                parsed.err()
218            );
219        }
220    }
221
222    #[test]
223    fn test_readme_exists() {
224        assert!(!WORKFLOWS_README.is_empty(), "README should not be empty");
225        assert!(
226            WORKFLOWS_README.contains("Nika Workflows"),
227            "README should have title"
228        );
229        assert!(
230            WORKFLOWS_README.contains("Quick Start"),
231            "README should have Quick Start section"
232        );
233    }
234
235    #[test]
236    fn test_workflows_valid_yaml() {
237        let workflows = get_all_workflows();
238        for w in &workflows {
239            // Skip templates with placeholders
240            if w.content.contains("{{PROVIDER}}") || w.content.contains("{{MODEL}}") {
241                continue;
242            }
243            let parsed: Result<serde_json::Value, _> = serde_saphyr::from_str(w.content);
244            assert!(
245                parsed.is_ok(),
246                "Workflow {} should be valid YAML: {:?}",
247                w.filename,
248                parsed.err()
249            );
250        }
251    }
252
253    #[test]
254    fn test_course_levels_exist() {
255        assert_eq!(
256            course::levels::LEVELS.len(),
257            12,
258            "Course should have 12 levels"
259        );
260    }
261
262    #[test]
263    fn test_course_total_exercises() {
264        // 5+4+4+3+3+3+3+3+4+3+4+5 = 44
265        assert_eq!(
266            course::levels::total_exercises(),
267            44,
268            "Course should have 44 total exercises"
269        );
270    }
271}