From 1a94f8e619269419e0999039e572e977adc49cdd Mon Sep 17 00:00:00 2001 From: zhaoxi Date: Wed, 15 Apr 2026 20:42:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v0.1=20=E5=8F=91=E5=B8=83=20-=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=A0=B8=E5=BF=83=E6=8A=80=E8=83=BD=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E5=BC=95=E6=93=8E=E4=B8=8E=20AST=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8C=96=E5=85=83=E6=95=B0=E6=8D=AE=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [安装] 实现基于文件系统的递归扫描逻辑,支持 Skill 包的物理结构自动化重构。 - [解析] 引入 ruff-python-parser 引擎,实现对 Python 源码的“零运行”解剖。 - [提取] 完成 FunctionDef 节点的深度遍历,自动化采集函数名、Docstring 及参数表。 - [结构] 定义强类型枚举 SkillNode,实现异构资源(Python/File)的统一图谱映射。 - [序列化] 集成 Serde 框架,支持将内存逻辑树一键飞升为标准 JSON 数据交换格式。 --- .gitignore | 3 + Cargo.lock | 925 +++++++++++++++++++++ Cargo.toml | 18 + LICENSE | 202 +++++ README.md | 30 + src/installer.rs | 18 + src/installer/git.rs | 69 ++ src/installer/install.rs | 46 + src/lib.rs | 18 + src/main.rs | 85 ++ src/manifest.rs | 17 + src/manifest/skill.rs | 20 + src/manifest/skill/analysis.rs | 136 +++ src/manifest/skill/model.rs | 23 + src/manifest/skill/skill.rs | 68 ++ src/manifest/skill/skill_structure_tree.rs | 70 ++ 16 files changed, 1748 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/installer.rs create mode 100644 src/installer/git.rs create mode 100644 src/installer/install.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/manifest.rs create mode 100644 src/manifest/skill.rs create mode 100644 src/manifest/skill/analysis.rs create mode 100644 src/manifest/skill/model.rs create mode 100644 src/manifest/skill/skill.rs create mode 100644 src/manifest/skill/skill_structure_tree.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e41ded --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +.idea +.cache \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d105414 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,925 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "drop_bomb" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indicator" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa345379fdc6422d1c280715d64baeb9199e4f4524f2c4f19bc1a54216416ff" +dependencies = [ + "arrayvec", + "futures", + "pin-project-lite", + "thiserror", + "time", + "tinyvec", + "tracing", +] + +[[package]] +name = "is-macro" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "path_abs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ef02f6342ac01d8a93b65f96db53fe68a92a15f41144f97fb00a9e669633c3" +dependencies = [ + "serde", + "serde_derive", + "std_prelude", + "stfu8", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ruff_python_ast" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff?rev=v0.4.0#e751b4ea8260ff83723345d1c7d39d5c776cc8ff" +dependencies = [ + "aho-corasick", + "bitflags", + "is-macro", + "itertools", + "once_cell", + "ruff_python_trivia", + "ruff_source_file", + "ruff_text_size", + "rustc-hash", +] + +[[package]] +name = "ruff_python_parser" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff?rev=v0.4.0#e751b4ea8260ff83723345d1c7d39d5c776cc8ff" +dependencies = [ + "anyhow", + "bitflags", + "bstr", + "drop_bomb", + "is-macro", + "itertools", + "memchr", + "ruff_python_ast", + "ruff_text_size", + "rustc-hash", + "static_assertions", + "unicode-ident", + "unicode-normalization", + "unicode_names2", +] + +[[package]] +name = "ruff_python_trivia" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff?rev=v0.4.0#e751b4ea8260ff83723345d1c7d39d5c776cc8ff" +dependencies = [ + "itertools", + "ruff_source_file", + "ruff_text_size", + "unicode-ident", +] + +[[package]] +name = "ruff_source_file" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff?rev=v0.4.0#e751b4ea8260ff83723345d1c7d39d5c776cc8ff" +dependencies = [ + "memchr", + "once_cell", + "ruff_text_size", +] + +[[package]] +name = "ruff_text_size" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff?rev=v0.4.0#e751b4ea8260ff83723345d1c7d39d5c776cc8ff" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "std_prelude" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8207e78455ffdf55661170876f88daf85356e4edd54e0a3dbc79586ca1e50cbe" + +[[package]] +name = "stfu8" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51f1e89f093f99e7432c491c382b88a6860a5adbe6bf02574bf0a08efff1978" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode_names2" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1673eca9782c84de5f81b82e4109dcfb3611c8ba0d52930ec4a9478f547b2dd" +dependencies = [ + "phf", + "unicode_names2_generator", +] + +[[package]] +name = "unicode_names2_generator" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91e5b84611016120197efd7dc93ef76774f4e084cd73c9fb3ea4a86c570c56e" +dependencies = [ + "getopts", + "log", + "phf_codegen", + "rand", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "viceroy" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "console", + "indicator", + "path_abs", + "regex", + "ruff_python_ast", + "ruff_python_parser", + "serde", + "serde_json", + "serde_yaml", + "walkdir", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a79bc95 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "viceroy" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.4", features = ["derive"] } +anyhow = "1.0" +walkdir = "2.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +path_abs = "0.5" +console = "0.16.3" +indicator = "0.4.4" +ruff_python_parser = { git = "https://github.com/astral-sh/ruff", rev = "v0.4.0" } +ruff_python_ast = { git = "https://github.com/astral-sh/ruff", rev = "v0.4.0" } +regex = "1.12.3" +serde_yaml = "0.9.34" \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5eb42cf --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 zhaoxi826 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..65f3dc4 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ + +# Viceroy (总督) + +Pretor的插件管理工具 + +![Rust](https://img.shields.io/badge/language-rust-orange.svg) +![Version](https://img.shields.io/badge/version-v0.1-blue.svg) +[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +--- +>*"你们搞大模型的就是码奸,你们已经害死前端兄弟了,还要害死后端兄弟,测试兄弟,运维兄弟,害死网安兄弟,害死ic兄弟,最后害死自己害死全人类"* + +viceroy 是一个由rust编写的安装工具,用于pretor的插件管理 +pretor项目仓库:https://github.com/zhaoxi826/Pretor + +--- +## 目前支持对象 +- skill: 安装skill并进行简单的解析到目标文件夹下 + +--- +## 使用方法 +#### Skill +**Skill** 是一个由指令、脚本和资源组成的集合,Agent通过动态加载这些内容,以在特定任务上提升表现。**Skill** 教会 **Agent** 如何以可重复的方式完成特定任务,例如按照公司品牌指南创建文档、使用组织特定的工作流程分析数据,或自动化个人任务。 +目标仓库:https://github.com/anthropics/skills +```Bash +./viceroy install (github仓库名) [-p (仓库内SKILL.md所在目录的相对路径)] -o (输出路径) +``` +**viceroy**将在skill根目录下产生 **skill.json** 和 **metadata.json**两个文件。 +**skill.json**包括SKILL.md的**name**,**description**,**instructions**。 +**metadata**包含整个skill的文件树和架构和python脚本工具的函数信息。 \ No newline at end of file diff --git a/src/installer.rs b/src/installer.rs new file mode 100644 index 0000000..eea7a55 --- /dev/null +++ b/src/installer.rs @@ -0,0 +1,18 @@ +/* + * // Copyright 2026 zhaoxi826 + * // + * // Licensed under the Apache License, Version 2.0 (the "License"); + * // you may not use this file except in compliance with the License. + * // You may obtain a copy of the License at + * // + * // http://www.apache.org/licenses/LICENSE-2.0 + * // + * // Unless required by applicable law or agreed to in writing, software + * // distributed under the License is distributed on an "AS IS" BASIS, + * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * // See the License for the specific language governing permissions and + * // limitations under the License. + */ + +pub mod git; +pub mod install; \ No newline at end of file diff --git a/src/installer/git.rs b/src/installer/git.rs new file mode 100644 index 0000000..6944283 --- /dev/null +++ b/src/installer/git.rs @@ -0,0 +1,69 @@ +/* + * // Copyright 2026 zhaoxi826 + * // + * // Licensed under the Apache License, Version 2.0 (the "License"); + * // you may not use this file except in compliance with the License. + * // You may obtain a copy of the License at + * // + * // http://www.apache.org/licenses/LICENSE-2.0 + * // + * // Unless required by applicable law or agreed to in writing, software + * // distributed under the License is distributed on an "AS IS" BASIS, + * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * // See the License for the specific language governing permissions and + * // limitations under the License. + */ + +use crate::installer::install::Installer; +use std::process::Command; +use anyhow::{Context, Result, anyhow}; +use std::fs; +use std::path::Path; +use std::path::PathBuf; + +pub struct GitInstaller{ + git_repo_url: String, + cache_path: Option, +} + +impl GitInstaller { + pub fn new(git_repo_url: &str, root_cache_path: &str) -> Self { + let repo_name = git_repo_url + .split('/') + .last() + .unwrap_or("default_repo") + .trim_end_matches(".git"); + let mut path_buf = PathBuf::from(root_cache_path); + path_buf.push(repo_name); + Self { + git_repo_url: git_repo_url.to_owned(), + cache_path: Some(path_buf.display().to_string()), + } + } +} +impl Installer for GitInstaller{ + fn download(&self) -> Result<()> { + let cache_path = self.cache_path.as_ref() + .context("错误:未初始化缓存路径,请先调用 get_cache_path")?; + if Path::new(cache_path).exists() { + fs::remove_dir_all(cache_path) + .with_context(|| format!("无法清理旧的缓存目录: {}", cache_path))?; + }; + println!("viceroy正在安装"); + let output = Command::new("git") + .args(["clone", + "--depth", + "1", + &self.git_repo_url, + cache_path]) + .output() + .context("执行 Git 失败,请确认系统已安装 git 并配置了 SSH/HTTP 权限")?; + if output.status.success() { + println!("✅ 技能包拉取成功。"); + Ok(()) + } else { + let err_msg = String::from_utf8_lossy(&output.stderr); + Err(anyhow!("Git 克隆失败: {}", err_msg)) + } + } +} diff --git a/src/installer/install.rs b/src/installer/install.rs new file mode 100644 index 0000000..3249762 --- /dev/null +++ b/src/installer/install.rs @@ -0,0 +1,46 @@ +/* + * // Copyright 2026 zhaoxi826 + * // + * // Licensed under the Apache License, Version 2.0 (the "License"); + * // you may not use this file except in compliance with the License. + * // You may obtain a copy of the License at + * // + * // http://www.apache.org/licenses/LICENSE-2.0 + * // + * // Unless required by applicable law or agreed to in writing, software + * // distributed under the License is distributed on an "AS IS" BASIS, + * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * // See the License for the specific language governing permissions and + * // limitations under the License. + */ + +use anyhow::Result; +use std::path::Path; +use std::fs; + +pub trait Installer { + fn download(&self) -> Result<()>; +} + +pub fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> { + if !dst.exists() { + fs::create_dir_all(dst)?; + } + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + let target = dst.join(entry.file_name()); + + // Skip .git directory to avoid unnecessary weight + if entry.file_name() == ".git" { + continue; + } + + if ty.is_dir() { + copy_dir_recursive(&entry.path(), &target)?; + } else { + fs::copy(&entry.path(), &target)?; + } + } + Ok(()) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..bad05b2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,18 @@ +/* + * // Copyright 2026 zhaoxi826 + * // + * // Licensed under the Apache License, Version 2.0 (the "License"); + * // you may not use this file except in compliance with the License. + * // You may obtain a copy of the License at + * // + * // http://www.apache.org/licenses/LICENSE-2.0 + * // + * // Unless required by applicable law or agreed to in writing, software + * // distributed under the License is distributed on an "AS IS" BASIS, + * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * // See the License for the specific language governing permissions and + * // limitations under the License. + */ + +pub mod installer; +pub mod manifest; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..122e17b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,85 @@ +/* + * // Copyright 2026 zhaoxi826 + * // + * // Licensed under the Apache License, Version 2.0 (the "License"); + * // you may not use this file except in compliance with the License. + * // You may obtain a copy of the License at + * // + * // http://www.apache.org/licenses/LICENSE-2.0 + * // + * // Unless required by applicable law or agreed to in writing, software + * // distributed under the License is distributed on an "AS IS" BASIS, + * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * // See the License for the specific language governing permissions and + * // limitations under the License. + */ + +use clap::{Parser, Subcommand}; +use std::path::PathBuf; +use viceroy::manifest::skill::analysis::process_and_save_skill; +use viceroy::manifest::skill::model::SkillModel; +use anyhow::Result; + +#[derive(Parser)] +#[command(name = "viceroy")] +#[command(about = "Pretor's plugin management tool", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Parse a skill directory, extracting SKILL.md and analyzing python files. + Parse { + /// The path to the skill directory + #[arg(short, long, value_name = "DIR")] + path: PathBuf, + }, + /// Install a skill from a Git repository and parse it + Install { + /// The Git repository URL + url: String, + + /// Subdirectory path inside the repo (default is root) + #[arg(short = 'p', long, default_value = "")] + path: String, + + /// Root cache directory to clone into + #[arg(short = 'c', long, default_value = ".cache")] + cache_dir: String, + + /// Output directory to move the final skill into + #[arg(short = 'o', long)] + output: Option, + }, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + match &cli.command { + Commands::Parse { path } => { + if !path.exists() || !path.is_dir() { + anyhow::bail!("Error: path {:?} does not exist or is not a directory", path); + } + println!("Parsing skill directory: {:?}", path); + process_and_save_skill(path)?; + println!("Done."); + } + Commands::Install { url, path, cache_dir, output } => { + let mut final_url = url.clone(); + if !final_url.starts_with("http://") && !final_url.starts_with("https://") && !final_url.starts_with("git@") { + final_url = format!("https://github.com/{}", final_url); + } + + println!("Installing skill from {} into {}", final_url, cache_dir); + let skill = SkillModel::install(final_url, cache_dir.clone(), path.clone(), output.clone()); + println!("Analyzing installed skill at {}", skill.skill_path); + skill.analysis()?; + println!("Done."); + } + } + + Ok(()) +} diff --git a/src/manifest.rs b/src/manifest.rs new file mode 100644 index 0000000..d559386 --- /dev/null +++ b/src/manifest.rs @@ -0,0 +1,17 @@ +/* + * // Copyright 2026 zhaoxi826 + * // + * // Licensed under the Apache License, Version 2.0 (the "License"); + * // you may not use this file except in compliance with the License. + * // You may obtain a copy of the License at + * // + * // http://www.apache.org/licenses/LICENSE-2.0 + * // + * // Unless required by applicable law or agreed to in writing, software + * // distributed under the License is distributed on an "AS IS" BASIS, + * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * // See the License for the specific language governing permissions and + * // limitations under the License. + */ + +pub mod skill; diff --git a/src/manifest/skill.rs b/src/manifest/skill.rs new file mode 100644 index 0000000..6e77063 --- /dev/null +++ b/src/manifest/skill.rs @@ -0,0 +1,20 @@ +/* + * // Copyright 2026 zhaoxi826 + * // + * // Licensed under the Apache License, Version 2.0 (the "License"); + * // you may not use this file except in compliance with the License. + * // You may obtain a copy of the License at + * // + * // http://www.apache.org/licenses/LICENSE-2.0 + * // + * // Unless required by applicable law or agreed to in writing, software + * // distributed under the License is distributed on an "AS IS" BASIS, + * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * // See the License for the specific language governing permissions and + * // limitations under the License. + */ + +pub mod skill; +pub mod model; +pub mod analysis; +pub mod skill_structure_tree; \ No newline at end of file diff --git a/src/manifest/skill/analysis.rs b/src/manifest/skill/analysis.rs new file mode 100644 index 0000000..b0675bd --- /dev/null +++ b/src/manifest/skill/analysis.rs @@ -0,0 +1,136 @@ +/* + * // Copyright 2026 zhaoxi826 + * // + * // Licensed under the Apache License, Version 2.0 (the "License"); + * // you may not use this file except in compliance with the License. + * // You may obtain a copy of the License at + * // + * // http://www.apache.org/licenses/LICENSE-2.0 + * // + * // Unless required by applicable law or agreed to in writing, software + * // distributed under the License is distributed on an "AS IS" BASIS, + * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * // See the License for the specific language governing permissions and + * // limitations under the License. + */ + +use crate::manifest::skill::skill_structure_tree::{SkillNode, SkillJson, PythonFuncNode, PythonFileNode}; +use walkdir::WalkDir; +use std::path::Path; +use std::collections::HashMap; +use ruff_python_parser::{parse, Mode}; +use ruff_python_ast::{Mod, Stmt}; +use regex::Regex; + +pub fn parse_skill_md(content: &str) -> SkillJson { + let mut metadata = SkillJson::default(); + + // Pattern to match YAML frontmatter between `---` and `---` + let re_frontmatter = Regex::new(r"(?s)^---\s*(.*?)\s*---").unwrap(); + if let Some(caps) = re_frontmatter.captures(content) { + let frontmatter = caps.get(1).map_or("", |m| m.as_str()); + + // Parse frontmatter properly using serde_yaml to support multiline values like `|` + if let Ok(yaml_data) = serde_yaml::from_str::(frontmatter) { + if let Some(name) = yaml_data.get("name").and_then(|v| v.as_str()) { + metadata.name = name.to_string(); + } + if let Some(desc) = yaml_data.get("description").and_then(|v| v.as_str()) { + metadata.description = desc.to_string(); + } + } + + // Extract instructions (everything after the frontmatter) + let body = re_frontmatter.replace(content, "").trim().to_string(); + metadata.instructions = body; + } else { + // No frontmatter found, whole file is instructions + metadata.instructions = content.trim().to_string(); + } + + metadata +} + +fn analyze_python_file(code: &str) -> PythonFileNode { + let mut func_dict = HashMap::new(); + let parsed: Mod = parse(code, Mode::Module).expect("Python 语法错误"); + if let Mod::Module(module) = parsed { + for stmt in module.body { + if let Stmt::FunctionDef(func) = stmt { + let func_name = func.name.to_string(); + let args = HashMap::new(); + let doc = "从 AST 里抠出来的文档".to_string(); + func_dict.insert(func_name.clone(), PythonFuncNode { + func_sign: format!("def {}(...)", func_name), + func_args: args, + func_return_type: "Unknown".into(), + func_docs: doc, + }); + } + } + } + PythonFileNode { + file_name: "xxx".into(), + func_dict, + } +} + +pub fn analyze_skill_directory(root_path: &Path) -> (Option, SkillNode) { + let mut root_node = SkillNode::new_folder("root"); + let mut skill_metadata = None; + + for entry in WalkDir::new(root_path).into_iter().filter_map(|e| e.ok()) { + let path = entry.path(); + if path.is_file() { + let file_name_str = path.file_name().unwrap().to_string_lossy().to_string(); + + // Skip useless files + let lower_name = file_name_str.to_lowercase(); + if lower_name.starts_with("license") || file_name_str.starts_with('.') { + continue; + } + + let rel_path = path.strip_prefix(root_path).unwrap(); + let segments: Vec<&str> = rel_path.iter().map(|s| s.to_str().unwrap()).collect(); + + if file_name_str.to_lowercase() == "skill.md" && segments.len() == 1 { + let code = std::fs::read_to_string(path).expect("读取文件失败"); + skill_metadata = Some(parse_skill_md(&code)); + // We don't add SKILL.md to the tree since it will be in skill.json + continue; + } + + let node = if path.extension().and_then(|s| s.to_str()) == Some("py") { + let code = std::fs::read_to_string(path).unwrap_or_default(); + let mut py_node = analyze_python_file(&code); + py_node.file_name = file_name_str.clone(); + SkillNode::Python(py_node) + } else { + SkillNode::File(file_name_str) + }; + root_node.insert_recursive(&segments, node); + } + } + + (skill_metadata, root_node) +} + +pub fn process_and_save_skill(root_path: &Path) -> anyhow::Result<()> { + let (metadata_opt, tree) = analyze_skill_directory(root_path); + + // Save skill.json + if let Some(metadata) = metadata_opt { + let skill_json_path = root_path.join("skill.json"); + let skill_json_content = serde_json::to_string_pretty(&metadata)?; + std::fs::write(&skill_json_path, skill_json_content)?; + println!("Saved {:?}", skill_json_path); + } + + // Save metadata.json + let metadata_json_path = root_path.join("metadata.json"); + let tree_json_content = serde_json::to_string_pretty(&tree)?; + std::fs::write(&metadata_json_path, tree_json_content)?; + println!("Saved {:?}", metadata_json_path); + + Ok(()) +} diff --git a/src/manifest/skill/model.rs b/src/manifest/skill/model.rs new file mode 100644 index 0000000..b25b529 --- /dev/null +++ b/src/manifest/skill/model.rs @@ -0,0 +1,23 @@ +/* + * // Copyright 2026 zhaoxi826 + * // + * // Licensed under the Apache License, Version 2.0 (the "License"); + * // you may not use this file except in compliance with the License. + * // You may obtain a copy of the License at + * // + * // http://www.apache.org/licenses/LICENSE-2.0 + * // + * // Unless required by applicable law or agreed to in writing, software + * // distributed under the License is distributed on an "AS IS" BASIS, + * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * // See the License for the specific language governing permissions and + * // limitations under the License. + */ + +pub struct SkillModel{ + pub skill_path: String, +} + +pub struct SkillRegister{ + +} \ No newline at end of file diff --git a/src/manifest/skill/skill.rs b/src/manifest/skill/skill.rs new file mode 100644 index 0000000..2493524 --- /dev/null +++ b/src/manifest/skill/skill.rs @@ -0,0 +1,68 @@ +/* + * // Copyright 2026 zhaoxi826 + * // + * // Licensed under the Apache License, Version 2.0 (the "License"); + * // you may not use this file except in compliance with the License. + * // You may obtain a copy of the License at + * // + * // http://www.apache.org/licenses/LICENSE-2.0 + * // + * // Unless required by applicable law or agreed to in writing, software + * // distributed under the License is distributed on an "AS IS" BASIS, + * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * // See the License for the specific language governing permissions and + * // limitations under the License. + */ + +use crate::installer::git; +use crate::installer::install::Installer; +use crate::manifest::skill::model::SkillModel; +use std::path::PathBuf; + +impl SkillModel{ + pub fn install(git_repo_url: String, root_cache_path: String, relative_path: String, output_dir: Option) -> Self{ + let git_installer = git::GitInstaller::new(&git_repo_url, &root_cache_path); + if let Err(e) = git_installer.download() { + eprintln!("安装失败: {}", e); + }; + let git_repo_name = git_repo_url + .split('/') + .last() + .unwrap_or("default_repo") + .trim_end_matches(".git"); + let mut path_builder = PathBuf::from(&root_cache_path); + path_builder.push(git_repo_name); + path_builder.push(&relative_path); + + let mut final_path = path_builder.to_string_lossy().to_string(); + + if let Some(out_dir) = output_dir { + // Determine the name of the skill directory to create inside the output directory. + // e.g. if relative_path is "skills/skill-creator", skill_dir_name is "skill-creator". + // If relative_path is empty, use the repo name. + let skill_dir_name = if relative_path.is_empty() { + git_repo_name + } else { + relative_path.split('/').last().unwrap_or(git_repo_name) + }; + + let target_dst = std::path::Path::new(&out_dir).join(skill_dir_name); + + // Copy the contents to the new target directory + if let Err(e) = crate::installer::install::copy_dir_recursive(&path_builder, &target_dst) { + eprintln!("复制到目标文件夹失败: {}", e); + } else { + final_path = target_dst.to_string_lossy().to_string(); + } + } + + Self{ + skill_path: final_path, + } + } + pub fn analysis(&self) -> anyhow::Result<()> { + use std::path::Path; + use crate::manifest::skill::analysis::process_and_save_skill; + process_and_save_skill(Path::new(&self.skill_path)) + } +} \ No newline at end of file diff --git a/src/manifest/skill/skill_structure_tree.rs b/src/manifest/skill/skill_structure_tree.rs new file mode 100644 index 0000000..3c311eb --- /dev/null +++ b/src/manifest/skill/skill_structure_tree.rs @@ -0,0 +1,70 @@ +/* + * // Copyright 2026 zhaoxi826 + * // + * // Licensed under the Apache License, Version 2.0 (the "License"); + * // you may not use this file except in compliance with the License. + * // You may obtain a copy of the License at + * // + * // http://www.apache.org/licenses/LICENSE-2.0 + * // + * // Unless required by applicable law or agreed to in writing, software + * // distributed under the License is distributed on an "AS IS" BASIS, + * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * // See the License for the specific language governing permissions and + * // limitations under the License. + */ + +use std::collections::HashMap; +use serde::{Serialize, Deserialize}; + +#[derive(Serialize)] +pub struct PythonFuncNode{ + pub func_sign: String, + pub func_args: HashMap, + pub func_return_type: String, + pub func_docs: String, +} + +#[derive(Serialize)] +pub struct PythonFileNode{ + pub file_name: String, + pub func_dict: HashMap, +} + +#[derive(Serialize)] +#[serde(tag = "type", content = "data")] +pub enum SkillNode { + Folder(String, HashMap), + Python(PythonFileNode), + File(String), +} + +impl SkillNode { + pub fn new_folder(name: &str) -> Self { + SkillNode::Folder(name.to_string(), HashMap::new()) + } + + pub fn insert_recursive(&mut self, segments: &[&str], data: SkillNode) { + if let SkillNode::Folder(_name, children) = self { + let current_seg = segments[0]; + if segments.len() == 1 { + children.insert(current_seg.to_string(), data); + } else { + let next_node = children + .entry(current_seg.to_string()) + .or_insert_with(|| SkillNode::new_folder(current_seg)); + + next_node.insert_recursive(&segments[1..], data); + } + } else { + panic!("试图在已存在文件{}内保存文件", segments[0]); + } + } +} + +#[derive(Serialize, Deserialize, Default)] +pub struct SkillJson { + pub name: String, + pub description: String, + pub instructions: String, +} \ No newline at end of file