Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.

Commit 1429f8c

Browse files
z23ccclaude
andcommitted
refactor: split admin.rs into module + extract exchange.rs, add MCP tests
- admin.rs → admin/mod.rs + admin/exchange.rs (export/import extracted) - task.rs → task/mod.rs (ready for further splitting) - Added MCP server trycmd tests (initialize, tools/list) admin.rs: 1753 → 1659 lines (exchange.rs: 102 lines extracted) Both now use module directory structure for future sub-splits. 0 clippy, 224+ tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f96ba73 commit 1429f8c

5 files changed

Lines changed: 116 additions & 97 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//! Export and import commands.
2+
3+
use std::env;
4+
use std::fs;
5+
6+
use serde_json::json;
7+
8+
use crate::output::{error_exit, json_output};
9+
10+
use flowctl_core::types::{EPICS_DIR, FLOW_DIR, TASKS_DIR};
11+
12+
pub fn cmd_export(json: bool, epic_filter: Option<String>, _format: String) {
13+
let flow_dir = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")).join(FLOW_DIR);
14+
if !flow_dir.exists() {
15+
error_exit(".flow/ does not exist. Run 'flowctl init' first.");
16+
}
17+
let cwd = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
18+
let conn = flowctl_db::open(&cwd)
19+
.unwrap_or_else(|e| error_exit(&format!("Failed to open DB: {e}")));
20+
21+
let epic_repo = flowctl_db::EpicRepo::new(&conn);
22+
let task_repo = flowctl_db::TaskRepo::new(&conn);
23+
24+
let epics_dir = flow_dir.join(EPICS_DIR);
25+
let _ = fs::create_dir_all(&epics_dir);
26+
let epics = match &epic_filter {
27+
Some(id) => match epic_repo.get(id) {
28+
Ok(e) => vec![e],
29+
Err(_) => { error_exit(&format!("Epic {} not found", id)); }
30+
},
31+
None => epic_repo.list(None).unwrap_or_default(),
32+
};
33+
34+
let mut epics_exported = 0;
35+
for epic in &epics {
36+
let (_, body) = epic_repo.get_with_body(&epic.id).unwrap_or((epic.clone(), String::new()));
37+
let doc = flowctl_core::frontmatter::Document { frontmatter: epic.clone(), body };
38+
if let Ok(content) = flowctl_core::frontmatter::write(&doc) {
39+
let path = epics_dir.join(format!("{}.md", epic.id));
40+
let _ = fs::write(&path, content);
41+
epics_exported += 1;
42+
}
43+
}
44+
45+
let tasks_dir = flow_dir.join(TASKS_DIR);
46+
let _ = fs::create_dir_all(&tasks_dir);
47+
let mut tasks_exported = 0;
48+
for epic in &epics {
49+
let tasks = task_repo.list_by_epic(&epic.id).unwrap_or_default();
50+
for task in &tasks {
51+
let (_, body) = task_repo.get_with_body(&task.id).unwrap_or((task.clone(), String::new()));
52+
let doc = flowctl_core::frontmatter::Document { frontmatter: task.clone(), body };
53+
if let Ok(content) = flowctl_core::frontmatter::write(&doc) {
54+
let path = tasks_dir.join(format!("{}.md", task.id));
55+
let _ = fs::write(&path, content);
56+
tasks_exported += 1;
57+
}
58+
}
59+
}
60+
61+
if json {
62+
json_output(json!({
63+
"success": true,
64+
"epics_exported": epics_exported,
65+
"tasks_exported": tasks_exported,
66+
}));
67+
} else {
68+
println!("Exported {} epics, {} tasks to .flow/", epics_exported, tasks_exported);
69+
}
70+
}
71+
72+
pub fn cmd_import(json: bool) {
73+
let flow_dir = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")).join(FLOW_DIR);
74+
if !flow_dir.exists() {
75+
error_exit(".flow/ does not exist. Run 'flowctl init' first.");
76+
}
77+
let cwd = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
78+
let conn = flowctl_db::open(&cwd)
79+
.unwrap_or_else(|e| error_exit(&format!("Failed to open DB: {e}")));
80+
81+
let state_dir = flowctl_db::resolve_state_dir(&cwd).ok();
82+
let result = flowctl_db::reindex(&conn, &flow_dir, state_dir.as_deref())
83+
.unwrap_or_else(|e| error_exit(&format!("Import failed: {e}")));
84+
85+
if json {
86+
json_output(json!({
87+
"success": true,
88+
"epics_imported": result.epics_indexed,
89+
"tasks_imported": result.tasks_indexed,
90+
"files_skipped": result.files_skipped,
91+
"warnings": result.warnings,
92+
}));
93+
} else {
94+
println!(
95+
"Imported {} epics, {} tasks ({} skipped)",
96+
result.epics_indexed, result.tasks_indexed, result.files_skipped
97+
);
98+
for w in &result.warnings {
99+
eprintln!(" warning: {w}");
100+
}
101+
}
102+
}

flowctl/crates/flowctl-cli/src/commands/admin.rs renamed to flowctl/crates/flowctl-cli/src/commands/admin/mod.rs

Lines changed: 3 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1654,100 +1654,6 @@ fn cmd_config_set(json_mode: bool, key: &str, value: &str) {
16541654
}
16551655
}
16561656

1657-
// ── Export command ────────────────────────────────────────────────
1658-
1659-
pub fn cmd_export(json: bool, epic_filter: Option<String>, _format: String) {
1660-
let flow_dir = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")).join(FLOW_DIR);
1661-
if !flow_dir.exists() {
1662-
error_exit(".flow/ does not exist. Run 'flowctl init' first.");
1663-
}
1664-
let cwd = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
1665-
let conn = flowctl_db::open(&cwd)
1666-
.unwrap_or_else(|e| error_exit(&format!("Failed to open DB: {e}")));
1667-
1668-
let epic_repo = flowctl_db::EpicRepo::new(&conn);
1669-
let task_repo = flowctl_db::TaskRepo::new(&conn);
1670-
1671-
// Export epics.
1672-
let epics_dir = flow_dir.join(EPICS_DIR);
1673-
let _ = fs::create_dir_all(&epics_dir);
1674-
let epics = match &epic_filter {
1675-
Some(id) => match epic_repo.get(id) {
1676-
Ok(e) => vec![e],
1677-
Err(_) => { error_exit(&format!("Epic {} not found", id)); }
1678-
},
1679-
None => epic_repo.list(None).unwrap_or_default(),
1680-
};
1681-
1682-
let mut epics_exported = 0;
1683-
for epic in &epics {
1684-
let (_, body) = epic_repo.get_with_body(&epic.id).unwrap_or((epic.clone(), String::new()));
1685-
let doc = flowctl_core::frontmatter::Document { frontmatter: epic.clone(), body };
1686-
if let Ok(content) = flowctl_core::frontmatter::write(&doc) {
1687-
let path = epics_dir.join(format!("{}.md", epic.id));
1688-
let _ = fs::write(&path, content);
1689-
epics_exported += 1;
1690-
}
1691-
}
1692-
1693-
// Export tasks.
1694-
let tasks_dir = flow_dir.join(TASKS_DIR);
1695-
let _ = fs::create_dir_all(&tasks_dir);
1696-
let mut tasks_exported = 0;
1697-
for epic in &epics {
1698-
let tasks = task_repo.list_by_epic(&epic.id).unwrap_or_default();
1699-
for task in &tasks {
1700-
let (_, body) = task_repo.get_with_body(&task.id).unwrap_or((task.clone(), String::new()));
1701-
let doc = flowctl_core::frontmatter::Document { frontmatter: task.clone(), body };
1702-
if let Ok(content) = flowctl_core::frontmatter::write(&doc) {
1703-
let path = tasks_dir.join(format!("{}.md", task.id));
1704-
let _ = fs::write(&path, content);
1705-
tasks_exported += 1;
1706-
}
1707-
}
1708-
}
1709-
1710-
if json {
1711-
json_output(json!({
1712-
"success": true,
1713-
"epics_exported": epics_exported,
1714-
"tasks_exported": tasks_exported,
1715-
}));
1716-
} else {
1717-
println!("Exported {} epics, {} tasks to .flow/", epics_exported, tasks_exported);
1718-
}
1719-
}
1720-
1721-
// ── Import command ───────────────────────────────────────────────
1722-
1723-
pub fn cmd_import(json: bool) {
1724-
let flow_dir = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")).join(FLOW_DIR);
1725-
if !flow_dir.exists() {
1726-
error_exit(".flow/ does not exist. Run 'flowctl init' first.");
1727-
}
1728-
let cwd = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
1729-
let conn = flowctl_db::open(&cwd)
1730-
.unwrap_or_else(|e| error_exit(&format!("Failed to open DB: {e}")));
1731-
1732-
let state_dir = flowctl_db::resolve_state_dir(&cwd).ok();
1733-
let result = flowctl_db::reindex(&conn, &flow_dir, state_dir.as_deref())
1734-
.unwrap_or_else(|e| error_exit(&format!("Import failed: {e}")));
1735-
1736-
if json {
1737-
json_output(json!({
1738-
"success": true,
1739-
"epics_imported": result.epics_indexed,
1740-
"tasks_imported": result.tasks_indexed,
1741-
"files_skipped": result.files_skipped,
1742-
"warnings": result.warnings,
1743-
}));
1744-
} else {
1745-
println!(
1746-
"Imported {} epics, {} tasks ({} skipped)",
1747-
result.epics_indexed, result.tasks_indexed, result.files_skipped
1748-
);
1749-
for w in &result.warnings {
1750-
eprintln!(" warning: {w}");
1751-
}
1752-
}
1753-
}
1657+
// ── Extracted submodules ─────────────────────────────────────────
1658+
mod exchange;
1659+
pub use exchange::{cmd_export, cmd_import};
File renamed without changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
bin.name = "flowctl"
2+
args = ["mcp"]
3+
stdin = '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
4+
stdout = """
5+
{"id":1,"jsonrpc":"2.0","result":{"capabilities":{"tools":{}},"protocolVersion":"2024-11-05","serverInfo":{"name":"flowctl","version":"0.1.0"}}}
6+
"""
7+
status.code = 0
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
bin.name = "flowctl"
2+
args = ["mcp"]
3+
stdin = '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
4+
status.code = 0

0 commit comments

Comments
 (0)