mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-11 20:13:30 +08:00
feat: add ecc2 legacy tool migration import
This commit is contained in:
@@ -189,6 +189,7 @@ ECC 2.0 now ships a bounded migration audit entrypoint:
|
|||||||
- `ecc migrate plan --source ~/.hermes --output migration-plan.md`
|
- `ecc migrate plan --source ~/.hermes --output migration-plan.md`
|
||||||
- `ecc migrate scaffold --source ~/.hermes --output-dir migration-artifacts`
|
- `ecc migrate scaffold --source ~/.hermes --output-dir migration-artifacts`
|
||||||
- `ecc migrate import-skills --source ~/.hermes --output-dir migration-artifacts/skills`
|
- `ecc migrate import-skills --source ~/.hermes --output-dir migration-artifacts/skills`
|
||||||
|
- `ecc migrate import-tools --source ~/.hermes --output-dir migration-artifacts/tools`
|
||||||
- `ecc migrate import-schedules --source ~/.hermes --dry-run`
|
- `ecc migrate import-schedules --source ~/.hermes --dry-run`
|
||||||
- `ecc migrate import-remote --source ~/.hermes --dry-run`
|
- `ecc migrate import-remote --source ~/.hermes --dry-run`
|
||||||
- `ecc migrate import-env --source ~/.hermes --dry-run`
|
- `ecc migrate import-env --source ~/.hermes --dry-run`
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ These stay local and should be configured per operator:
|
|||||||
## Suggested Bring-Up Order
|
## Suggested Bring-Up Order
|
||||||
|
|
||||||
0. Run `ecc migrate audit --source ~/.hermes` first to inventory the legacy workspace and see which parts already map onto ECC2.
|
0. Run `ecc migrate audit --source ~/.hermes` first to inventory the legacy workspace and see which parts already map onto ECC2.
|
||||||
0.5. Generate and review artifacts with `ecc migrate plan` / `ecc migrate scaffold`, scaffold reusable legacy skills with `ecc migrate import-skills --output-dir migration-artifacts/skills`, preview recurring jobs with `ecc migrate import-schedules --dry-run`, preview gateway dispatch with `ecc migrate import-remote --dry-run`, preview safe env/service context with `ecc migrate import-env --dry-run`, then import sanitized workspace memory with `ecc migrate import-memory`.
|
0.5. Generate and review artifacts with `ecc migrate plan` / `ecc migrate scaffold`, scaffold reusable legacy skills with `ecc migrate import-skills --output-dir migration-artifacts/skills`, scaffold legacy tool translation templates with `ecc migrate import-tools --output-dir migration-artifacts/tools`, preview recurring jobs with `ecc migrate import-schedules --dry-run`, preview gateway dispatch with `ecc migrate import-remote --dry-run`, preview safe env/service context with `ecc migrate import-env --dry-run`, then import sanitized workspace memory with `ecc migrate import-memory`.
|
||||||
1. Install ECC and verify the baseline harness setup.
|
1. Install ECC and verify the baseline harness setup.
|
||||||
2. Install Hermes and point it at ECC-imported skills.
|
2. Install Hermes and point it at ECC-imported skills.
|
||||||
3. Register the MCP servers you actually use every day.
|
3. Register the MCP servers you actually use every day.
|
||||||
|
|||||||
474
ecc2/src/main.rs
474
ecc2/src/main.rs
@@ -659,6 +659,18 @@ enum MigrationCommands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
|
/// Scaffold ECC-native templates from legacy tool scripts
|
||||||
|
ImportTools {
|
||||||
|
/// Path to the legacy Hermes/OpenClaw workspace root
|
||||||
|
#[arg(long)]
|
||||||
|
source: PathBuf,
|
||||||
|
/// Directory where imported ECC2 tool artifacts should be written
|
||||||
|
#[arg(long)]
|
||||||
|
output_dir: PathBuf,
|
||||||
|
/// Emit machine-readable JSON instead of the human summary
|
||||||
|
#[arg(long)]
|
||||||
|
json: bool,
|
||||||
|
},
|
||||||
/// Import legacy gateway/dispatch tasks into the ECC2 remote queue
|
/// Import legacy gateway/dispatch tasks into the ECC2 remote queue
|
||||||
ImportRemote {
|
ImportRemote {
|
||||||
/// Path to the legacy Hermes/OpenClaw workspace root
|
/// Path to the legacy Hermes/OpenClaw workspace root
|
||||||
@@ -1128,6 +1140,30 @@ struct LegacySkillTemplateFile {
|
|||||||
orchestration_templates: BTreeMap<String, config::OrchestrationTemplateConfig>,
|
orchestration_templates: BTreeMap<String, config::OrchestrationTemplateConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
struct LegacyToolImportEntry {
|
||||||
|
source_path: String,
|
||||||
|
template_name: String,
|
||||||
|
title: String,
|
||||||
|
summary: String,
|
||||||
|
suggested_surface: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
struct LegacyToolImportReport {
|
||||||
|
source: String,
|
||||||
|
output_dir: String,
|
||||||
|
tools_detected: usize,
|
||||||
|
templates_generated: usize,
|
||||||
|
files_written: Vec<String>,
|
||||||
|
tools: Vec<LegacyToolImportEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
struct LegacyToolTemplateFile {
|
||||||
|
orchestration_templates: BTreeMap<String, config::OrchestrationTemplateConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
enum LegacyRemoteImportRequestStatus {
|
enum LegacyRemoteImportRequestStatus {
|
||||||
@@ -1991,6 +2027,18 @@ async fn main() -> Result<()> {
|
|||||||
println!("{}", format_legacy_skill_import_human(&report));
|
println!("{}", format_legacy_skill_import_human(&report));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
MigrationCommands::ImportTools {
|
||||||
|
source,
|
||||||
|
output_dir,
|
||||||
|
json,
|
||||||
|
} => {
|
||||||
|
let report = import_legacy_tools(&source, &output_dir)?;
|
||||||
|
if json {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||||
|
} else {
|
||||||
|
println!("{}", format_legacy_tool_import_human(&report));
|
||||||
|
}
|
||||||
|
}
|
||||||
MigrationCommands::ImportRemote {
|
MigrationCommands::ImportRemote {
|
||||||
source,
|
source,
|
||||||
dry_run,
|
dry_run,
|
||||||
@@ -5157,9 +5205,15 @@ fn build_legacy_migration_next_steps(artifacts: &[LegacyMigrationArtifact]) -> V
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if categories.contains("tools") || categories.contains("plugins") {
|
if categories.contains("tools") {
|
||||||
steps.push(
|
steps.push(
|
||||||
"Rebuild valuable tool/plugin wrappers as ECC agents, commands, hooks, or harness runners, keeping only reusable workflow behavior."
|
"Scaffold translated legacy tools with `ecc migrate import-tools --source <legacy-workspace> --output-dir <dir>`, then rebuild the valuable ones as ECC-native commands, hooks, or harness runners instead of shelling back out to the old stack."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if categories.contains("plugins") {
|
||||||
|
steps.push(
|
||||||
|
"Rebuild valuable bridge plugins as ECC-native hooks, commands, or skills, keeping only reusable workflow behavior."
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6246,6 +6300,292 @@ fn format_legacy_skill_import_summary_markdown(report: &LegacySkillImportReport)
|
|||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn import_legacy_tools(source: &Path, output_dir: &Path) -> Result<LegacyToolImportReport> {
|
||||||
|
let source = source
|
||||||
|
.canonicalize()
|
||||||
|
.with_context(|| format!("Legacy workspace not found: {}", source.display()))?;
|
||||||
|
if !source.is_dir() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Legacy workspace source must be a directory: {}",
|
||||||
|
source.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tools_dir = source.join("tools");
|
||||||
|
let mut report = LegacyToolImportReport {
|
||||||
|
source: source.display().to_string(),
|
||||||
|
output_dir: output_dir.display().to_string(),
|
||||||
|
tools_detected: 0,
|
||||||
|
templates_generated: 0,
|
||||||
|
files_written: Vec::new(),
|
||||||
|
tools: Vec::new(),
|
||||||
|
};
|
||||||
|
if !tools_dir.is_dir() {
|
||||||
|
return Ok(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tool_paths = collect_legacy_tool_paths(&tools_dir)?;
|
||||||
|
if tool_paths.is_empty() {
|
||||||
|
return Ok(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::create_dir_all(output_dir)
|
||||||
|
.with_context(|| format!("create legacy tool output dir {}", output_dir.display()))?;
|
||||||
|
|
||||||
|
let mut templates = BTreeMap::new();
|
||||||
|
for path in tool_paths {
|
||||||
|
let draft = build_legacy_tool_draft(&source, &tools_dir, &path)?;
|
||||||
|
report.tools_detected += 1;
|
||||||
|
report.templates_generated += 1;
|
||||||
|
report.tools.push(LegacyToolImportEntry {
|
||||||
|
source_path: draft.source_path.clone(),
|
||||||
|
template_name: draft.template_name.clone(),
|
||||||
|
title: draft.title.clone(),
|
||||||
|
summary: draft.summary.clone(),
|
||||||
|
suggested_surface: draft.suggested_surface.clone(),
|
||||||
|
});
|
||||||
|
templates.insert(
|
||||||
|
draft.template_name.clone(),
|
||||||
|
config::OrchestrationTemplateConfig {
|
||||||
|
description: Some(format!(
|
||||||
|
"Migrated legacy tool scaffold from {}",
|
||||||
|
draft.source_path
|
||||||
|
)),
|
||||||
|
project: Some("legacy-migration".to_string()),
|
||||||
|
task_group: Some("legacy tool".to_string()),
|
||||||
|
agent: Some("claude".to_string()),
|
||||||
|
profile: None,
|
||||||
|
worktree: Some(false),
|
||||||
|
steps: vec![config::OrchestrationTemplateStepConfig {
|
||||||
|
name: Some("operator".to_string()),
|
||||||
|
task: format!(
|
||||||
|
"Use the migrated legacy tool context from {}.\nSuggested ECC target surface: {}\nLegacy tool title: {}\nLegacy summary: {}\nLegacy excerpt:\n{}\nRebuild or wrap that behavior as an ECC-native {} for {{{{task}}}}.",
|
||||||
|
draft.source_path,
|
||||||
|
draft.suggested_surface,
|
||||||
|
draft.title,
|
||||||
|
draft.summary,
|
||||||
|
draft.excerpt,
|
||||||
|
draft.suggested_surface
|
||||||
|
),
|
||||||
|
agent: None,
|
||||||
|
profile: None,
|
||||||
|
worktree: Some(false),
|
||||||
|
project: Some("legacy-migration".to_string()),
|
||||||
|
task_group: Some("legacy tool".to_string()),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let templates_path = output_dir.join("ecc2.imported-tools.toml");
|
||||||
|
fs::write(
|
||||||
|
&templates_path,
|
||||||
|
toml::to_string_pretty(&LegacyToolTemplateFile {
|
||||||
|
orchestration_templates: templates,
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.with_context(|| format!("write imported tool templates {}", templates_path.display()))?;
|
||||||
|
report
|
||||||
|
.files_written
|
||||||
|
.push(templates_path.display().to_string());
|
||||||
|
|
||||||
|
let summary_path = output_dir.join("imported-tools.md");
|
||||||
|
fs::write(
|
||||||
|
&summary_path,
|
||||||
|
format_legacy_tool_import_summary_markdown(&report),
|
||||||
|
)
|
||||||
|
.with_context(|| format!("write imported tool summary {}", summary_path.display()))?;
|
||||||
|
report
|
||||||
|
.files_written
|
||||||
|
.push(summary_path.display().to_string());
|
||||||
|
|
||||||
|
Ok(report)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct LegacyToolDraft {
|
||||||
|
source_path: String,
|
||||||
|
template_name: String,
|
||||||
|
title: String,
|
||||||
|
summary: String,
|
||||||
|
excerpt: String,
|
||||||
|
suggested_surface: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_legacy_tool_paths(root: &Path) -> Result<Vec<PathBuf>> {
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
collect_legacy_tool_paths_inner(root, &mut paths)?;
|
||||||
|
paths.sort();
|
||||||
|
Ok(paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_legacy_tool_paths_inner(root: &Path, paths: &mut Vec<PathBuf>) -> Result<()> {
|
||||||
|
let mut entries = fs::read_dir(root)
|
||||||
|
.with_context(|| format!("read legacy tools dir {}", root.display()))?
|
||||||
|
.collect::<std::io::Result<Vec<_>>>()
|
||||||
|
.with_context(|| format!("read entries under {}", root.display()))?;
|
||||||
|
entries.sort_by_key(|entry| entry.path());
|
||||||
|
for entry in entries {
|
||||||
|
let path = entry.path();
|
||||||
|
let file_type = entry
|
||||||
|
.file_type()
|
||||||
|
.with_context(|| format!("read file type for {}", path.display()))?;
|
||||||
|
if file_type.is_dir() {
|
||||||
|
collect_legacy_tool_paths_inner(&path, paths)?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if file_type.is_file() && is_legacy_tool_candidate(&path) {
|
||||||
|
paths.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_legacy_tool_candidate(path: &Path) -> bool {
|
||||||
|
matches!(
|
||||||
|
path.extension().and_then(|ext| ext.to_str()),
|
||||||
|
Some("py" | "js" | "ts" | "mjs" | "cjs" | "sh" | "bash" | "zsh" | "rb" | "pl" | "php")
|
||||||
|
) || path.extension().is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_legacy_tool_draft(
|
||||||
|
source: &Path,
|
||||||
|
tools_dir: &Path,
|
||||||
|
path: &Path,
|
||||||
|
) -> Result<LegacyToolDraft> {
|
||||||
|
let body =
|
||||||
|
fs::read(path).with_context(|| format!("read legacy tool file {}", path.display()))?;
|
||||||
|
let body = String::from_utf8_lossy(&body).into_owned();
|
||||||
|
let source_path = path
|
||||||
|
.strip_prefix(source)
|
||||||
|
.unwrap_or(path)
|
||||||
|
.display()
|
||||||
|
.to_string();
|
||||||
|
let relative_to_tools = path.strip_prefix(tools_dir).unwrap_or(path);
|
||||||
|
let title = extract_legacy_tool_title(relative_to_tools);
|
||||||
|
let summary = extract_legacy_tool_summary(&body).unwrap_or_else(|| title.clone());
|
||||||
|
let excerpt = extract_legacy_tool_excerpt(&body, 10, 700).unwrap_or_else(|| summary.clone());
|
||||||
|
let template_name = format!(
|
||||||
|
"tool_{}",
|
||||||
|
slugify_legacy_skill_template_name(relative_to_tools)
|
||||||
|
);
|
||||||
|
let suggested_surface = classify_legacy_tool_surface(&source_path, &body).to_string();
|
||||||
|
|
||||||
|
Ok(LegacyToolDraft {
|
||||||
|
source_path,
|
||||||
|
template_name,
|
||||||
|
title,
|
||||||
|
summary,
|
||||||
|
excerpt,
|
||||||
|
suggested_surface,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_legacy_tool_title(relative_path: &Path) -> String {
|
||||||
|
relative_path
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|value| value.to_str())
|
||||||
|
.map(|value| value.replace(['-', '_'], " "))
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.unwrap_or_else(|| "legacy tool".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_legacy_tool_summary(body: &str) -> Option<String> {
|
||||||
|
body.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|line| !line.is_empty() && !line.starts_with("#!"))
|
||||||
|
.find_map(|line| {
|
||||||
|
let stripped = line
|
||||||
|
.trim_start_matches("#")
|
||||||
|
.trim_start_matches("//")
|
||||||
|
.trim_start_matches("--")
|
||||||
|
.trim_start_matches("/*")
|
||||||
|
.trim_start_matches('*')
|
||||||
|
.trim();
|
||||||
|
if stripped.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(truncate_connector_text(stripped, 160))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_legacy_tool_excerpt(body: &str, max_lines: usize, max_chars: usize) -> Option<String> {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
let mut chars = 0usize;
|
||||||
|
for line in body.lines().map(str::trim).filter(|line| !line.is_empty()) {
|
||||||
|
if line.starts_with("#!") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if chars >= max_chars || lines.len() >= max_lines {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let remaining = max_chars.saturating_sub(chars);
|
||||||
|
if remaining == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let truncated = truncate_connector_text(line, remaining);
|
||||||
|
chars += truncated.len();
|
||||||
|
lines.push(truncated);
|
||||||
|
}
|
||||||
|
if lines.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(lines.join("\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify_legacy_tool_surface(source_path: &str, body: &str) -> &'static str {
|
||||||
|
let source_lower = source_path.to_ascii_lowercase();
|
||||||
|
let body_lower = body.to_ascii_lowercase();
|
||||||
|
if source_lower.contains("hook")
|
||||||
|
|| body_lower.contains("pretooluse")
|
||||||
|
|| body_lower.contains("posttooluse")
|
||||||
|
|| body_lower.contains("notification")
|
||||||
|
{
|
||||||
|
"hook"
|
||||||
|
} else if source_lower.contains("runner")
|
||||||
|
|| source_lower.contains("agent")
|
||||||
|
|| body_lower.contains("session_name_flag")
|
||||||
|
|| body_lower.contains("include-directories")
|
||||||
|
{
|
||||||
|
"harness runner"
|
||||||
|
} else {
|
||||||
|
"command"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_legacy_tool_import_summary_markdown(report: &LegacyToolImportReport) -> String {
|
||||||
|
let mut lines = vec![
|
||||||
|
"# Imported legacy tools".to_string(),
|
||||||
|
String::new(),
|
||||||
|
format!("- Source: `{}`", report.source),
|
||||||
|
format!("- Output dir: `{}`", report.output_dir),
|
||||||
|
format!("- Tools detected: {}", report.tools_detected),
|
||||||
|
format!("- Templates generated: {}", report.templates_generated),
|
||||||
|
String::new(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if report.tools.is_empty() {
|
||||||
|
lines.push("No legacy tool scripts were detected.".to_string());
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("## Tools".to_string());
|
||||||
|
lines.push(String::new());
|
||||||
|
for tool in &report.tools {
|
||||||
|
lines.push(format!(
|
||||||
|
"- `{}` -> `{}`",
|
||||||
|
tool.source_path, tool.template_name
|
||||||
|
));
|
||||||
|
lines.push(format!(" - Title: {}", tool.title));
|
||||||
|
lines.push(format!(" - Summary: {}", tool.summary));
|
||||||
|
lines.push(format!(" - Suggested surface: {}", tool.suggested_surface));
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
fn build_legacy_remote_add_command(draft: &LegacyRemoteDispatchDraft) -> Option<String> {
|
fn build_legacy_remote_add_command(draft: &LegacyRemoteDispatchDraft) -> Option<String> {
|
||||||
match draft.request_kind {
|
match draft.request_kind {
|
||||||
session::RemoteDispatchKind::Standard => {
|
session::RemoteDispatchKind::Standard => {
|
||||||
@@ -6671,7 +7011,11 @@ fn build_legacy_migration_plan_report(
|
|||||||
target_surface: "ECC agents / hooks / commands / harness runners".to_string(),
|
target_surface: "ECC agents / hooks / commands / harness runners".to_string(),
|
||||||
source_paths: artifact.source_paths.clone(),
|
source_paths: artifact.source_paths.clone(),
|
||||||
command_snippets: vec![
|
command_snippets: vec![
|
||||||
"ecc start --task \"Rebuild one legacy tool as an ECC-native command or hook\"".to_string(),
|
format!(
|
||||||
|
"ecc migrate import-tools --source {} --output-dir migration-artifacts/tools",
|
||||||
|
shell_quote_double(&audit.source)
|
||||||
|
),
|
||||||
|
"ecc template <template-name> --task \"Rebuild one legacy tool as an ECC-native command, hook, or harness runner\"".to_string(),
|
||||||
],
|
],
|
||||||
config_snippets: vec![
|
config_snippets: vec![
|
||||||
"[harness_runners.legacy-runner]\nprogram = \"<runner-binary>\"\nbase_args = []\nproject_markers = [\".legacy-runner\"]".to_string(),
|
"[harness_runners.legacy-runner]\nprogram = \"<runner-binary>\"\nbase_args = []\nproject_markers = [\".legacy-runner\"]".to_string(),
|
||||||
@@ -7081,6 +7425,34 @@ fn format_legacy_skill_import_human(report: &LegacySkillImportReport) -> String
|
|||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_legacy_tool_import_human(report: &LegacyToolImportReport) -> String {
|
||||||
|
let mut lines = vec![
|
||||||
|
format!("Legacy tool import complete for {}", report.source),
|
||||||
|
format!("- output dir {}", report.output_dir),
|
||||||
|
format!("- tools detected {}", report.tools_detected),
|
||||||
|
format!("- templates generated {}", report.templates_generated),
|
||||||
|
];
|
||||||
|
|
||||||
|
if !report.files_written.is_empty() {
|
||||||
|
lines.push("Files".to_string());
|
||||||
|
for path in &report.files_written {
|
||||||
|
lines.push(format!("- {}", path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !report.tools.is_empty() {
|
||||||
|
lines.push("Tools".to_string());
|
||||||
|
for tool in &report.tools {
|
||||||
|
lines.push(format!("- {} -> {}", tool.source_path, tool.template_name));
|
||||||
|
lines.push(format!(" title {}", tool.title));
|
||||||
|
lines.push(format!(" summary {}", tool.summary));
|
||||||
|
lines.push(format!(" suggested surface {}", tool.suggested_surface));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
fn format_legacy_remote_import_human(report: &LegacyRemoteImportReport) -> String {
|
fn format_legacy_remote_import_human(report: &LegacyRemoteImportReport) -> String {
|
||||||
let mut lines = vec![
|
let mut lines = vec![
|
||||||
format!(
|
format!(
|
||||||
@@ -9778,6 +10150,37 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_parses_migrate_import_tools_command() {
|
||||||
|
let cli = Cli::try_parse_from([
|
||||||
|
"ecc",
|
||||||
|
"migrate",
|
||||||
|
"import-tools",
|
||||||
|
"--source",
|
||||||
|
"/tmp/hermes",
|
||||||
|
"--output-dir",
|
||||||
|
"/tmp/out",
|
||||||
|
"--json",
|
||||||
|
])
|
||||||
|
.expect("migrate import-tools should parse");
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Some(Commands::Migrate {
|
||||||
|
command:
|
||||||
|
MigrationCommands::ImportTools {
|
||||||
|
source,
|
||||||
|
output_dir,
|
||||||
|
json,
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
assert_eq!(source, PathBuf::from("/tmp/hermes"));
|
||||||
|
assert_eq!(output_dir, PathBuf::from("/tmp/out"));
|
||||||
|
assert!(json);
|
||||||
|
}
|
||||||
|
_ => panic!("expected migrate import-tools subcommand"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn legacy_migration_audit_report_maps_detected_artifacts() -> Result<()> {
|
fn legacy_migration_audit_report_maps_detected_artifacts() -> Result<()> {
|
||||||
let tempdir = TestDir::new("legacy-migration-audit")?;
|
let tempdir = TestDir::new("legacy-migration-audit")?;
|
||||||
@@ -9910,6 +10313,11 @@ mod tests {
|
|||||||
)?;
|
)?;
|
||||||
fs::write(root.join("workspace/notes/recovery.md"), "# recovery\n")?;
|
fs::write(root.join("workspace/notes/recovery.md"), "# recovery\n")?;
|
||||||
fs::write(root.join("skills/ecc-imports/research.md"), "# research\n")?;
|
fs::write(root.join("skills/ecc-imports/research.md"), "# research\n")?;
|
||||||
|
fs::create_dir_all(root.join("tools"))?;
|
||||||
|
fs::write(
|
||||||
|
root.join("tools/browser.py"),
|
||||||
|
"# Verify the billing portal banner\nprint('browser')\n",
|
||||||
|
)?;
|
||||||
|
|
||||||
let audit = build_legacy_migration_audit_report(root)?;
|
let audit = build_legacy_migration_audit_report(root)?;
|
||||||
let plan = build_legacy_migration_plan_report(&audit);
|
let plan = build_legacy_migration_plan_report(&audit);
|
||||||
@@ -9992,6 +10400,15 @@ mod tests {
|
|||||||
.command_snippets
|
.command_snippets
|
||||||
.iter()
|
.iter()
|
||||||
.any(|command| command.contains("ecc migrate import-skills --source")));
|
.any(|command| command.contains("ecc migrate import-skills --source")));
|
||||||
|
let tools_step = plan
|
||||||
|
.steps
|
||||||
|
.iter()
|
||||||
|
.find(|step| step.category == "tools")
|
||||||
|
.expect("tools step");
|
||||||
|
assert!(tools_step
|
||||||
|
.command_snippets
|
||||||
|
.iter()
|
||||||
|
.any(|command| command.contains("ecc migrate import-tools --source")));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -10489,6 +10906,57 @@ Route existing installs to portal first before checkout.
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_legacy_tools_writes_template_artifacts() -> Result<()> {
|
||||||
|
let tempdir = TestDir::new("legacy-tool-import")?;
|
||||||
|
let root = tempdir.path();
|
||||||
|
fs::create_dir_all(root.join("tools/browser"))?;
|
||||||
|
fs::create_dir_all(root.join("tools/hooks"))?;
|
||||||
|
fs::write(
|
||||||
|
root.join("tools/browser/check_portal.py"),
|
||||||
|
"# Verify the billing portal warning banner\nprint('check banner')\n",
|
||||||
|
)?;
|
||||||
|
fs::write(
|
||||||
|
root.join("tools/hooks/preflight.sh"),
|
||||||
|
"#!/usr/bin/env bash\n# PretoolUse guard for dangerous commands\nexit 0\n",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let output_dir = root.join("out");
|
||||||
|
let report = import_legacy_tools(root, &output_dir)?;
|
||||||
|
|
||||||
|
assert_eq!(report.tools_detected, 2);
|
||||||
|
assert_eq!(report.templates_generated, 2);
|
||||||
|
assert_eq!(report.files_written.len(), 2);
|
||||||
|
assert!(report
|
||||||
|
.tools
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool.template_name == "tool_browser_check_portal_py"));
|
||||||
|
assert!(report
|
||||||
|
.tools
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool.template_name == "tool_hooks_preflight_sh"));
|
||||||
|
assert!(report
|
||||||
|
.tools
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool.suggested_surface == "command"));
|
||||||
|
assert!(report
|
||||||
|
.tools
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool.suggested_surface == "hook"));
|
||||||
|
|
||||||
|
let config_text = fs::read_to_string(output_dir.join("ecc2.imported-tools.toml"))?;
|
||||||
|
assert!(config_text.contains("[orchestration_templates.tool_browser_check_portal_py]"));
|
||||||
|
assert!(config_text.contains("[orchestration_templates.tool_hooks_preflight_sh]"));
|
||||||
|
assert!(config_text.contains("Rebuild or wrap that behavior as an ECC-native"));
|
||||||
|
|
||||||
|
let summary_text = fs::read_to_string(output_dir.join("imported-tools.md"))?;
|
||||||
|
assert!(summary_text.contains("tools/browser/check_portal.py"));
|
||||||
|
assert!(summary_text.contains("tools/hooks/preflight.sh"));
|
||||||
|
assert!(summary_text.contains("Suggested surface: hook"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn legacy_migration_scaffold_writes_plan_and_config_files() -> Result<()> {
|
fn legacy_migration_scaffold_writes_plan_and_config_files() -> Result<()> {
|
||||||
let tempdir = TestDir::new("legacy-migration-scaffold")?;
|
let tempdir = TestDir::new("legacy-migration-scaffold")?;
|
||||||
|
|||||||
Reference in New Issue
Block a user