use std::fs;
use colored::Colorize;
use nika::error::NikaError;
use nika::init::{
get_all_context_files, get_all_partials, get_all_schemas, get_all_workflows, WORKFLOWS_README,
};
use nika::tools::PermissionMode;
pub fn init_project(
permission: &str,
no_example: bool,
migrate_keys: bool,
) -> Result<(), NikaError> {
let cwd = std::env::current_dir()?;
let nika_dir = cwd.join(".nika");
if nika_dir.exists() {
return Err(NikaError::ValidationError {
reason: format!(
"Project already initialized at {}. Remove .nika/ to reinitialize.",
nika_dir.display()
),
});
}
let permission_mode = match permission.to_lowercase().as_str() {
"deny" => PermissionMode::Deny,
"plan" => PermissionMode::Plan,
"accept-edits" | "acceptedits" => PermissionMode::AcceptEdits,
"accept-all" | "acceptall" | "yolo" => PermissionMode::YoloMode,
other => {
return Err(NikaError::ValidationError {
reason: format!(
"Invalid permission mode: '{}'. Use: deny, plan, accept-edits, yolo",
other
),
});
}
};
fs::create_dir_all(&nika_dir)?;
println!("{} Created {}", "✓".green(), nika_dir.display());
let config_path = nika_dir.join("config.toml");
let config_content = format!(
r#"# Nika Project Configuration
# Generated by `nika init`
[tools]
# Permission mode for file tools
# Options: deny, plan, accept-edits, accept-all
permission = "{}"
# Working directory (default: project root)
# Files outside this directory cannot be accessed
# working_dir = "."
[provider]
# Default LLM provider (claude, openai, mistral, groq, deepseek, native)
# Provider auto-detection checks env vars: ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.
# Can also override with: nika chat --provider <name>
default = "claude"
# Default model (provider-specific)
# Can also override with: nika chat --model <name>
# model = "claude-sonnet-4-6"
"#,
permission_mode
.display_name()
.to_lowercase()
.replace(" (yolo)", "")
);
fs::write(&config_path, config_content)?;
println!("{} Created {}", "✓".green(), config_path.display());
let agents_dir = nika_dir.join("agents");
fs::create_dir_all(&agents_dir)?;
println!("{} Created {}", "✓".green(), agents_dir.display());
let example_agent_path = agents_dir.join("researcher.md");
let example_agent_content = r#"---
name: researcher
description: A helpful research agent that can search and summarize information
model: claude-sonnet-4-6
max_turns: 10
---
You are a Research Agent specialized in finding and synthesizing information.
## Capabilities
- Search the web for relevant information
- Summarize findings in clear, concise language
- Cite sources and provide references
- Answer follow-up questions
## Guidelines
1. Always verify information from multiple sources when possible
2. Clearly distinguish between facts and opinions
3. Acknowledge uncertainty when information is incomplete
4. Provide actionable insights when relevant
## Output Format
Structure your responses with:
- **Summary**: Key findings in 2-3 sentences
- **Details**: Supporting information
- **Sources**: References used (when applicable)
"#;
fs::write(&example_agent_path, example_agent_content)?;
println!("{} Created {}", "✓".green(), example_agent_path.display());
let skills_dir = nika_dir.join("skills");
fs::create_dir_all(&skills_dir)?;
println!("{} Created {}", "✓".green(), skills_dir.display());
let example_skill_path = skills_dir.join("code-review.md");
let example_skill_content = r#"---
name: code-review
description: Skill for reviewing code quality, patterns, and best practices
---
# Code Review Skill
When reviewing code, analyze for:
## Quality Checks
- Clear naming conventions
- Appropriate error handling
- Code duplication
- Complexity and readability
## Security
- Input validation
- Authentication/authorization
- Sensitive data handling
## Best Practices
- SOLID principles
- DRY (Don't Repeat Yourself)
- Single responsibility
- Proper documentation
## Output
Provide feedback in categories:
- 🔴 Critical: Must fix before merge
- 🟡 Important: Should address
- 🟢 Suggestion: Nice to have
"#;
fs::write(&example_skill_path, example_skill_content)?;
println!("{} Created {}", "✓".green(), example_skill_path.display());
let context_dir = nika_dir.join("context");
fs::create_dir_all(&context_dir)?;
println!("{} Created {}", "✓".green(), context_dir.display());
let context_path = context_dir.join("project.md");
let context_content = r#"# Project Context
This file provides shared context for all agents and workflows.
## Project Overview
Describe your project here. This context will be available to agents via `memory.context.project`.
## Key Information
- Project name: [Your Project]
- Tech stack: [Your Stack]
- Key conventions: [Your Conventions]
"#;
fs::write(&context_path, context_content)?;
println!("{} Created {}", "✓".green(), context_path.display());
let memory_dir = nika_dir.join("memory");
fs::create_dir_all(&memory_dir)?;
println!("{} Created {}", "✓".green(), memory_dir.display());
let proposed_dir = nika_dir.join("proposed");
fs::create_dir_all(&proposed_dir)?;
println!("{} Created {}", "✓".green(), proposed_dir.display());
let cache_dir = nika_dir.join("cache");
fs::create_dir_all(&cache_dir)?;
println!("{} Created {}", "✓".green(), cache_dir.display());
let workflows_dir = nika_dir.join("workflows");
fs::create_dir_all(&workflows_dir)?;
println!("{} Created {}", "✓".green(), workflows_dir.display());
let example_subworkflow_path = workflows_dir.join("helpers.nika.yaml");
let example_subworkflow_content = r#"# Helper Sub-Workflows
# These are standalone helper workflows — run them directly
# or compose them into larger DAGs with depends_on.
#
# Examples:
# nika run .nika/workflows/helpers.nika.yaml
# nika run .nika/workflows/helpers.nika.yaml -p provider=openai
schema: "nika/workflow@0.12"
workflow: helpers
description: "Reusable helper workflows for common tasks"
inputs:
content: "Nika is a semantic YAML workflow engine for AI tasks."
target_language: "French"
tasks:
- id: summarize
infer:
prompt: |
Summarize the following content in 3 bullet points:
{{inputs.content}}
temperature: 0.3
max_tokens: 300
- id: translate
infer:
prompt: |
Translate the following text to {{inputs.target_language}}:
{{inputs.content}}
temperature: 0.2
max_tokens: 500
- id: review
depends_on: [summarize, translate]
with:
summary: $summarize
translation: $translate
infer:
prompt: |
Review these outputs for quality:
SUMMARY:
{{with.summary}}
TRANSLATION ({{inputs.target_language}}):
{{with.translation}}
Rate each 1-10 and suggest improvements.
temperature: 0.3
max_tokens: 400
"#;
fs::write(&example_subworkflow_path, example_subworkflow_content)?;
println!(
"{} Created {}",
"✓".green(),
example_subworkflow_path.display()
);
let user_path = nika_dir.join("user.yaml");
let user_content = r#"# Nika User Profile
# Personalize your AI experience
# Your name (used in greetings and personalization)
# name: "Your Name"
# Email (optional, for notifications)
# email: "you@example.com"
# Timezone (for scheduling and timestamps)
timezone: "UTC"
# Preferred language (ISO 639-1 code)
language: "en-US"
# Additional context about you (helps agents understand your preferences)
# context: |
# I prefer concise responses.
# I work primarily with Rust and TypeScript.
"#;
fs::write(&user_path, user_content)?;
println!("{} Created {}", "✓".green(), user_path.display());
let memory_config_path = nika_dir.join("memory.yaml");
let memory_config_content = r#"# Nika Memory Configuration
# Persistent memory across sessions
# Enable/disable memory system
enabled: true
# Storage backend: file, sqlite, redis (file is default)
backend: file
# Time-to-live in seconds for memory entries (0 = no expiry)
ttl_secs: 0
# Maximum number of entries to keep (0 = unlimited)
max_entries: 1000
# Memory scopes (named memory buckets)
scopes:
# Conversation history
conversation:
persist: true
ttl_secs: 86400 # 24 hours
# Project-specific memory
project:
persist: true
ttl_secs: 0 # Never expires
# Temporary scratch space
scratch:
persist: false
ttl_secs: 3600 # 1 hour
"#;
fs::write(&memory_config_path, memory_config_content)?;
println!("{} Created {}", "✓".green(), memory_config_path.display());
let policies_path = nika_dir.join("policies.yaml");
let policies_content = r#"# Nika Security Policies
# Control what agents can do
execution:
# Shell commands that are always allowed (glob patterns)
allow_commands:
- "echo *"
- "cat *"
- "ls *"
- "pwd"
- "date"
- "git status"
- "git diff *"
- "git log *"
- "cargo *"
- "npm *"
- "pnpm *"
# Shell commands that are always blocked
block_commands:
- "rm -rf /*"
- "sudo *"
- "chmod 777 *"
# Require confirmation for potentially destructive commands
confirm_destructive: true
# Maximum execution time for any command (seconds)
max_execution_secs: 300
budget:
# Daily token limit (0 = unlimited)
daily_token_limit: 0
# Monthly cost limit in cents (0 = unlimited)
monthly_cost_limit_cents: 0
# Warn when this percentage of budget is reached
warn_at_percent: 80
network:
# Domains that can be accessed (empty = all allowed)
# allow_domains:
# - "api.example.com"
# Domains that are always blocked
block_domains:
- "localhost:internal"
# Allow localhost/127.0.0.1 access
allow_localhost: true
"#;
fs::write(&policies_path, policies_content)?;
println!("{} Created {}", "✓".green(), policies_path.display());
if !no_example {
let workflows_dir = cwd.join("workflows");
fs::create_dir_all(&workflows_dir)?;
println!("{} Created {}", "✓".green(), workflows_dir.display());
let readme_path = workflows_dir.join("README.md");
fs::write(&readme_path, WORKFLOWS_README)?;
println!("{} Created {}", "✓".green(), readme_path.display());
let workflows = get_all_workflows();
let mut created_tiers = std::collections::HashSet::new();
for workflow in &workflows {
let tier_dir = workflows_dir.join(workflow.tier_dir);
if created_tiers.insert(workflow.tier_dir) {
fs::create_dir_all(&tier_dir)?;
println!("{} Created {}", "✓".green(), tier_dir.display());
}
let wf_path = tier_dir.join(workflow.filename);
fs::write(&wf_path, workflow.content)?;
println!("{} Created {}", "✓".green(), wf_path.display());
}
let context_dir = cwd.join("context");
fs::create_dir_all(&context_dir)?;
println!("{} Created {}", "✓".green(), context_dir.display());
for ctx_file in get_all_context_files() {
let ctx_path = context_dir.join(ctx_file.filename);
fs::write(&ctx_path, ctx_file.content)?;
println!("{} Created {}", "✓".green(), ctx_path.display());
}
let partials_dir = workflows_dir.join("partials");
fs::create_dir_all(&partials_dir)?;
println!("{} Created {}", "✓".green(), partials_dir.display());
for partial in get_all_partials() {
let partial_path = partials_dir.join(partial.filename);
fs::write(&partial_path, partial.content)?;
println!("{} Created {}", "✓".green(), partial_path.display());
}
let schemas_dir = cwd.join("schemas");
fs::create_dir_all(&schemas_dir)?;
println!("{} Created {}", "✓".green(), schemas_dir.display());
for schema in get_all_schemas() {
let schema_path = schemas_dir.join(schema.filename);
fs::write(&schema_path, schema.content)?;
println!("{} Created {}", "✓".green(), schema_path.display());
}
let output_dir = cwd.join("output");
fs::create_dir_all(&output_dir)?;
fs::write(output_dir.join(".gitkeep"), "")?;
println!("{} Created {}", "✓".green(), output_dir.display());
println!();
println!(
"{} {} workflows created across {} tiers",
"✓".green(),
workflows.len().to_string().cyan(),
created_tiers.len().to_string().cyan()
);
}
println!();
println!("{}", "Nika project initialized!".green().bold());
println!();
println!(
" Permission mode: {}",
permission_mode.display_name().cyan()
);
println!(" Config: {}", config_path.display());
println!();
println!(" {} Project structure:", "📁".cyan());
println!();
println!(
" {} .nika/ # Nika configuration",
"⚙️".dimmed()
);
println!(" ├── config.toml # Main configuration");
println!(" ├── agents/ # Agent definitions");
println!(" ├── skills/ # Skill definitions");
println!(" └── ...");
if !no_example {
println!();
println!(
" {} workflows/ # 30 example workflows by tier",
"📂".cyan()
);
println!(" ├── README.md # Quick start guide");
println!(" ├── tier-1-no-deps/ (01-03) # exec, fetch, builtins");
println!(" ├── tier-2-llm/ (04-07) # infer, DAG, for_each");
println!(" ├── tier-3-agent/ (08-09) # agent + file tools");
println!(" ├── tier-4-mcp/ (10) # NovaNet integration");
println!(" ├── tier-5-dev/ (11-20) # Developer use cases");
println!(" └── tier-6-magic/ (21-30) # Everyday automation");
println!();
println!(
" {} context/ # Context files for workflows",
"📁".dimmed()
);
println!(
" {} partials/ # Reusable workflow fragments",
"📁".dimmed()
);
println!(
" {} schemas/ # JSON schemas for validation",
"📁".dimmed()
);
println!(
" {} output/ # Generated outputs (gitignored)",
"📁".dimmed()
);
}
println!();
if !no_example {
println!(" {} Get started:", "→".cyan());
println!();
println!(" # Tier 1: Works immediately (no API key)");
println!(" nika run workflows/tier-1-no-deps/01-exec-basics.nika.yaml");
println!();
println!(" # Tier 2: Setup provider first");
println!(" nika provider set anthropic");
println!(" nika run workflows/tier-2-llm/04-infer-basics.nika.yaml");
println!();
println!(" {} Learn more:", "📖".cyan());
println!(" See workflows/README.md for full tier guide");
}
#[cfg(feature = "tui")]
if migrate_keys {
use nika::secrets::migrate_env_to_keyring;
println!();
println!(
"{}",
"Migrating API keys from environment variables...".cyan()
);
let report = migrate_env_to_keyring();
println!();
println!("{}", report.summary());
if !report.errors.is_empty() {
println!();
println!("{}:", "Errors".red());
for (provider, error) in &report.errors {
println!(" {} - {}", provider, error);
}
}
if report.migrated > 0 {
println!();
println!(
"{}",
"NOTE: You can now remove these env vars from your shell config.".yellow()
);
}
}
#[cfg(not(feature = "tui"))]
if migrate_keys {
println!(
"{} Key migration requires TUI feature. Use: cargo build --features tui",
"Warning:".yellow()
);
}
Ok(())
}