leniu-dev 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/auto-test-generator.md +315 -0
- package/.claude/agents/bug-analyzer.md +103 -0
- package/.claude/agents/code-reviewer.md +122 -0
- package/.claude/agents/code-scanner.md +145 -0
- package/.claude/agents/image-reader.md +154 -0
- package/.claude/agents/loki-runner.md +80 -0
- package/.claude/agents/mysql-runner.md +81 -0
- package/.claude/agents/project-manager.md +159 -0
- package/.claude/agents/requirements-analyzer.md +175 -0
- package/.claude/agents/task-fetcher.md +75 -0
- package/.claude/audio/completed.wav +0 -0
- package/.claude/commands/add-todo.md +255 -0
- package/.claude/commands/auto-test.md +252 -0
- package/.claude/commands/check.md +210 -0
- package/.claude/commands/crud.md +454 -0
- package/.claude/commands/dev.md +532 -0
- package/.claude/commands/init-config.md +154 -0
- package/.claude/commands/init-docs.md +681 -0
- package/.claude/commands/next.md +281 -0
- package/.claude/commands/opsx-apply.md +147 -0
- package/.claude/commands/opsx-archive.md +105 -0
- package/.claude/commands/opsx-bulk-archive.md +237 -0
- package/.claude/commands/opsx-continue.md +109 -0
- package/.claude/commands/opsx-explore.md +281 -0
- package/.claude/commands/opsx-ff.md +92 -0
- package/.claude/commands/opsx-new.md +65 -0
- package/.claude/commands/opsx-onboard.md +397 -0
- package/.claude/commands/opsx-sync.md +129 -0
- package/.claude/commands/opsx-verify.md +159 -0
- package/.claude/commands/progress.md +264 -0
- package/.claude/commands/release.md +109 -0
- package/.claude/commands/start.md +199 -0
- package/.claude/commands/sync.md +307 -0
- package/.claude/commands/update-status.md +428 -0
- package/.claude/docs/Mixin/344/275/277/347/224/250/346/214/207/345/215/227.md +299 -0
- package/.claude/docs/README.md +167 -0
- package/.claude/docs//345/211/215/347/253/257/345/274/200/345/217/221/346/214/207/345/215/227.md +599 -0
- package/.claude/docs//345/220/216/347/253/257/345/274/200/345/217/221/346/214/207/345/215/227.md +726 -0
- package/.claude/docs//345/267/245/344/275/234/346/265/201/345/274/200/345/217/221/346/214/207/345/215/227.md +714 -0
- package/.claude/docs//345/267/245/345/205/267/347/261/273/344/275/277/347/224/250/346/214/207/345/215/227.md +463 -0
- package/.claude/docs//346/225/260/346/215/256/345/272/223/350/256/276/350/256/241/350/247/204/350/214/203.md +390 -0
- package/.claude/docs//346/226/260/345/212/237/350/203/275/345/274/200/345/217/221/346/265/201/347/250/213/350/247/204/350/214/203.md +688 -0
- package/.claude/docs//346/226/260/351/241/271/347/233/256/345/274/200/345/217/221/346/265/201/347/250/213.md +365 -0
- package/.claude/docs//346/241/206/346/236/266/350/257/264/346/230/216.md +393 -0
- package/.claude/docs//350/267/257/347/224/261/351/205/215/347/275/256/346/214/207/345/215/227.md +246 -0
- package/.claude/framework-config.json +73 -0
- package/.claude/hooks/lib/notify.js +310 -0
- package/.claude/hooks/pre-tool-use.js +117 -0
- package/.claude/hooks/skill-forced-eval.js +161 -0
- package/.claude/hooks/stop.js +55 -0
- package/.claude/notify-config.json +9 -0
- package/.claude/settings.json +57 -0
- package/.claude/skills/add-skill/SKILL.md +488 -0
- package/.claude/skills/analyze-requirements/SKILL.md +112 -0
- package/.claude/skills/api-development/SKILL.md +315 -0
- package/.claude/skills/architecture-design/SKILL.md +152 -0
- package/.claude/skills/auto-test/SKILL.md +625 -0
- package/.claude/skills/auto-test/references/api-conventions.md +260 -0
- package/.claude/skills/backend-annotations/SKILL.md +248 -0
- package/.claude/skills/banana-image/CHANGELOG.md +37 -0
- package/.claude/skills/banana-image/README.md +146 -0
- package/.claude/skills/banana-image/SKILL.md +171 -0
- package/.claude/skills/banana-image/assets/logo.png +0 -0
- package/.claude/skills/banana-image/references/advanced-usage.md +189 -0
- package/.claude/skills/banana-image/scripts/apply_template.py +125 -0
- package/.claude/skills/banana-image/scripts/banana_image_exec.ts +412 -0
- package/.claude/skills/banana-image/scripts/batch_prep.py +82 -0
- package/.claude/skills/banana-image/scripts/package-lock.json +1437 -0
- package/.claude/skills/banana-image/scripts/package.json +18 -0
- package/.claude/skills/banana-image/scripts/requirements.txt +10 -0
- package/.claude/skills/banana-image/templates/poster.json +22 -0
- package/.claude/skills/banana-image/templates/product.json +17 -0
- package/.claude/skills/banana-image/templates/social.json +22 -0
- package/.claude/skills/banana-image/templates/thumbnail.json +17 -0
- package/.claude/skills/brainstorm/SKILL.md +216 -0
- package/.claude/skills/bug-detective/SKILL.md +295 -0
- package/.claude/skills/bug-detective/references/error-patterns.md +242 -0
- package/.claude/skills/chrome-cdp/SKILL.md +81 -0
- package/.claude/skills/chrome-cdp/scripts/cdp.mjs +838 -0
- package/.claude/skills/chrome-cdp/scripts/run-cdp.sh +7 -0
- package/.claude/skills/code-patterns/SKILL.md +163 -0
- package/.claude/skills/code-patterns/references/leniu-code-patterns.md +87 -0
- package/.claude/skills/codex-code-review/SKILL.md +327 -0
- package/.claude/skills/collaborating-with-codex/SKILL.md +180 -0
- package/.claude/skills/collaborating-with-codex/scripts/codex_bridge.py +275 -0
- package/.claude/skills/collaborating-with-gemini/SKILL.md +194 -0
- package/.claude/skills/collaborating-with-gemini/scripts/gemini_bridge.py +275 -0
- package/.claude/skills/crud-development/SKILL.md +328 -0
- package/.claude/skills/data-permission/SKILL.md +221 -0
- package/.claude/skills/data-permission/references/custom-data-scope.md +90 -0
- package/.claude/skills/database-ops/SKILL.md +210 -0
- package/.claude/skills/error-handler/SKILL.md +310 -0
- package/.claude/skills/file-oss-management/SKILL.md +260 -0
- package/.claude/skills/file-oss-management/references/entities.md +105 -0
- package/.claude/skills/file-oss-management/references/service-impl.md +104 -0
- package/.claude/skills/fix-bug/SKILL.md +269 -0
- package/.claude/skills/git-workflow/SKILL.md +179 -0
- package/.claude/skills/jenkins-deploy/SKILL.md +134 -0
- package/.claude/skills/jenkins-deploy/assets/env_param.template.json +51 -0
- package/.claude/skills/jenkins-deploy/assets/jk_build.py +400 -0
- package/.claude/skills/json-serialization/SKILL.md +341 -0
- package/.claude/skills/lanhu-design/SKILL.md +99 -0
- package/.claude/skills/leniu-api-development/SKILL.md +319 -0
- package/.claude/skills/leniu-api-development/references/real-examples.md +273 -0
- package/.claude/skills/leniu-architecture-design/SKILL.md +383 -0
- package/.claude/skills/leniu-backend-annotations/SKILL.md +277 -0
- package/.claude/skills/leniu-brainstorm/SKILL.md +242 -0
- package/.claude/skills/leniu-brainstorm/references/business-scenarios.md +162 -0
- package/.claude/skills/leniu-code-patterns/SKILL.md +411 -0
- package/.claude/skills/leniu-crud-development/SKILL.md +404 -0
- package/.claude/skills/leniu-crud-development/references/templates.md +597 -0
- package/.claude/skills/leniu-customization-location/SKILL.md +410 -0
- package/.claude/skills/leniu-data-permission/SKILL.md +341 -0
- package/.claude/skills/leniu-database-ops/SKILL.md +426 -0
- package/.claude/skills/leniu-error-handler/SKILL.md +462 -0
- package/.claude/skills/leniu-java-concurrent/SKILL.md +400 -0
- package/.claude/skills/leniu-java-entity/SKILL.md +237 -0
- package/.claude/skills/leniu-java-entity/references/templates.md +237 -0
- package/.claude/skills/leniu-java-logging/SKILL.md +229 -0
- package/.claude/skills/leniu-java-logging/references/data-mask.md +46 -0
- package/.claude/skills/leniu-java-logging/references/logging-scenarios.md +113 -0
- package/.claude/skills/leniu-java-mq/SKILL.md +338 -0
- package/.claude/skills/leniu-java-mybatis/SKILL.md +267 -0
- package/.claude/skills/leniu-java-mybatis/references/report-mapper.md +88 -0
- package/.claude/skills/leniu-java-task/SKILL.md +367 -0
- package/.claude/skills/leniu-marketing-scenario/SKILL.md +448 -0
- package/.claude/skills/leniu-marketing-scenario/references/pay-meal-rules.md +197 -0
- package/.claude/skills/leniu-marketing-scenario/references/price-rules.md +286 -0
- package/.claude/skills/leniu-marketing-scenario/references/recharge-rules.md +188 -0
- package/.claude/skills/leniu-redis-cache/SKILL.md +331 -0
- package/.claude/skills/leniu-report-scenario/SKILL.md +508 -0
- package/.claude/skills/leniu-report-scenario/references/amount-handling.md +448 -0
- package/.claude/skills/leniu-report-scenario/references/analysis-module.md +64 -0
- package/.claude/skills/leniu-report-scenario/references/customization-table-fields.md +93 -0
- package/.claude/skills/leniu-report-scenario/references/customization.md +356 -0
- package/.claude/skills/leniu-report-scenario/references/data-permission.md +182 -0
- package/.claude/skills/leniu-report-scenario/references/export.md +553 -0
- package/.claude/skills/leniu-report-scenario/references/mealtime.md +197 -0
- package/.claude/skills/leniu-report-scenario/references/query-param.md +274 -0
- package/.claude/skills/leniu-report-scenario/references/report-tables.md +162 -0
- package/.claude/skills/leniu-report-scenario/references/standard-customization.md +112 -0
- package/.claude/skills/leniu-report-scenario/references/standard-table-fields.md +113 -0
- package/.claude/skills/leniu-report-scenario/references/total-line.md +179 -0
- package/.claude/skills/leniu-security-guard/SKILL.md +306 -0
- package/.claude/skills/leniu-utils-toolkit/SKILL.md +380 -0
- package/.claude/skills/loki-log-query/SKILL.md +430 -0
- package/.claude/skills/mysql-debug/SKILL.md +406 -0
- package/.claude/skills/performance-doctor/SKILL.md +297 -0
- package/.claude/skills/project-navigator/SKILL.md +211 -0
- package/.claude/skills/redis-cache/SKILL.md +282 -0
- package/.claude/skills/redis-cache/references/listeners.md +23 -0
- package/.claude/skills/scheduled-jobs/SKILL.md +277 -0
- package/.claude/skills/security-guard/SKILL.md +245 -0
- package/.claude/skills/security-guard/references/encrypt-config.md +103 -0
- package/.claude/skills/security-guard/references/sensitive-strategies.md +42 -0
- package/.claude/skills/sms-mail/SKILL.md +346 -0
- package/.claude/skills/sms-mail/references/mail-config.md +88 -0
- package/.claude/skills/sms-mail/references/sms-config.md +74 -0
- package/.claude/skills/social-login/SKILL.md +328 -0
- package/.claude/skills/social-login/references/provider-configs.md +118 -0
- package/.claude/skills/store-pc/SKILL.md +366 -0
- package/.claude/skills/sync-back-merge/SKILL.md +66 -0
- package/.claude/skills/task-tracker/SKILL.md +307 -0
- package/.claude/skills/tech-decision/SKILL.md +393 -0
- package/.claude/skills/tenant-management/SKILL.md +272 -0
- package/.claude/skills/tenant-management/references/tenant-scenarios.md +91 -0
- package/.claude/skills/test-development/SKILL.md +301 -0
- package/.claude/skills/test-development/references/parameterized-examples.md +119 -0
- package/.claude/skills/ui-pc/SKILL.md +438 -0
- package/.claude/skills/utils-toolkit/SKILL.md +354 -0
- package/.claude/skills/utils-toolkit/references/redis-utils-api.md +56 -0
- package/.claude/skills/websocket-sse/SKILL.md +350 -0
- package/.claude/skills/workflow-engine/SKILL.md +249 -0
- package/.claude/skills/yunxiao-task-management/SKILL.md +401 -0
- package/.claude/skills/yunxiao-task-management/templates//346/217/220/346/265/213/345/215/225/346/250/241/346/235/277.html +17 -0
- package/.claude/templates/env-config.md +27 -0
- package/.claude/templates//345/276/205/345/212/236/346/270/205/345/215/225/346/250/241/346/235/277.md +56 -0
- package/.claude/templates//351/234/200/346/261/202/346/226/207/346/241/243/346/250/241/346/235/277.md +85 -0
- package/.claude/templates//351/241/271/347/233/256/347/212/266/346/200/201/346/250/241/346/235/277.md +43 -0
- package/.codex/skills/add-skill/SKILL.md +488 -0
- package/.codex/skills/add-todo/SKILL.md +269 -0
- package/.codex/skills/analyze-requirements/SKILL.md +112 -0
- package/.codex/skills/api-development/SKILL.md +315 -0
- package/.codex/skills/architecture-design/SKILL.md +152 -0
- package/.codex/skills/auto-test/SKILL.md +453 -0
- package/.codex/skills/auto-test/references/api-conventions.md +260 -0
- package/.codex/skills/backend-annotations/SKILL.md +248 -0
- package/.codex/skills/banana-image/CHANGELOG.md +37 -0
- package/.codex/skills/banana-image/README.md +146 -0
- package/.codex/skills/banana-image/SKILL.md +171 -0
- package/.codex/skills/banana-image/assets/logo.png +0 -0
- package/.codex/skills/banana-image/references/advanced-usage.md +189 -0
- package/.codex/skills/banana-image/scripts/apply_template.py +125 -0
- package/.codex/skills/banana-image/scripts/banana_image_exec.ts +412 -0
- package/.codex/skills/banana-image/scripts/batch_prep.py +82 -0
- package/.codex/skills/banana-image/scripts/package-lock.json +1437 -0
- package/.codex/skills/banana-image/scripts/package.json +18 -0
- package/.codex/skills/banana-image/scripts/requirements.txt +10 -0
- package/.codex/skills/banana-image/templates/poster.json +22 -0
- package/.codex/skills/banana-image/templates/product.json +17 -0
- package/.codex/skills/banana-image/templates/social.json +22 -0
- package/.codex/skills/banana-image/templates/thumbnail.json +17 -0
- package/.codex/skills/brainstorm/SKILL.md +216 -0
- package/.codex/skills/bug-detective/SKILL.md +295 -0
- package/.codex/skills/bug-detective/references/error-patterns.md +242 -0
- package/.codex/skills/check/SKILL.md +367 -0
- package/.codex/skills/code-patterns/SKILL.md +163 -0
- package/.codex/skills/code-patterns/references/leniu-code-patterns.md +87 -0
- package/.codex/skills/collaborating-with-codex/SKILL.md +180 -0
- package/.codex/skills/collaborating-with-codex/scripts/codex_bridge.py +275 -0
- package/.codex/skills/collaborating-with-gemini/SKILL.md +194 -0
- package/.codex/skills/collaborating-with-gemini/scripts/gemini_bridge.py +275 -0
- package/.codex/skills/crud/SKILL.md +265 -0
- package/.codex/skills/crud-development/SKILL.md +328 -0
- package/.codex/skills/data-permission/SKILL.md +221 -0
- package/.codex/skills/data-permission/references/custom-data-scope.md +90 -0
- package/.codex/skills/database-ops/SKILL.md +210 -0
- package/.codex/skills/dev/SKILL.md +187 -0
- package/.codex/skills/error-handler/SKILL.md +310 -0
- package/.codex/skills/file-oss-management/SKILL.md +260 -0
- package/.codex/skills/file-oss-management/references/entities.md +105 -0
- package/.codex/skills/file-oss-management/references/service-impl.md +104 -0
- package/.codex/skills/fix-bug/SKILL.md +269 -0
- package/.codex/skills/git-workflow/SKILL.md +179 -0
- package/.codex/skills/init-docs/SKILL.md +194 -0
- package/.codex/skills/jenkins-deploy/SKILL.md +134 -0
- package/.codex/skills/json-serialization/SKILL.md +341 -0
- package/.codex/skills/lanhu-design/SKILL.md +99 -0
- package/.codex/skills/leniu-api-development/SKILL.md +319 -0
- package/.codex/skills/leniu-api-development/references/real-examples.md +273 -0
- package/.codex/skills/leniu-architecture-design/SKILL.md +383 -0
- package/.codex/skills/leniu-backend-annotations/SKILL.md +277 -0
- package/.codex/skills/leniu-brainstorm/SKILL.md +242 -0
- package/.codex/skills/leniu-brainstorm/references/business-scenarios.md +162 -0
- package/.codex/skills/leniu-code-patterns/SKILL.md +411 -0
- package/.codex/skills/leniu-crud-development/SKILL.md +404 -0
- package/.codex/skills/leniu-crud-development/references/templates.md +597 -0
- package/.codex/skills/leniu-customization-location/SKILL.md +410 -0
- package/.codex/skills/leniu-data-permission/SKILL.md +341 -0
- package/.codex/skills/leniu-database-ops/SKILL.md +426 -0
- package/.codex/skills/leniu-error-handler/SKILL.md +462 -0
- package/.codex/skills/leniu-java-amount-handling/SKILL.md +461 -0
- package/.codex/skills/leniu-java-code-style/SKILL.md +510 -0
- package/.codex/skills/leniu-java-concurrent/SKILL.md +400 -0
- package/.codex/skills/leniu-java-entity/SKILL.md +237 -0
- package/.codex/skills/leniu-java-entity/references/templates.md +237 -0
- package/.codex/skills/leniu-java-logging/SKILL.md +229 -0
- package/.codex/skills/leniu-java-logging/references/data-mask.md +46 -0
- package/.codex/skills/leniu-java-logging/references/logging-scenarios.md +113 -0
- package/.codex/skills/leniu-java-mq/SKILL.md +338 -0
- package/.codex/skills/leniu-java-mybatis/SKILL.md +267 -0
- package/.codex/skills/leniu-java-mybatis/references/report-mapper.md +88 -0
- package/.codex/skills/leniu-java-task/SKILL.md +367 -0
- package/.codex/skills/leniu-marketing-scenario/SKILL.md +448 -0
- package/.codex/skills/leniu-marketing-scenario/references/pay-meal-rules.md +197 -0
- package/.codex/skills/leniu-marketing-scenario/references/price-rules.md +286 -0
- package/.codex/skills/leniu-marketing-scenario/references/recharge-rules.md +188 -0
- package/.codex/skills/leniu-redis-cache/SKILL.md +331 -0
- package/.codex/skills/leniu-report-scenario/SKILL.md +508 -0
- package/.codex/skills/leniu-report-scenario/references/amount-handling.md +448 -0
- package/.codex/skills/leniu-report-scenario/references/analysis-module.md +64 -0
- package/.codex/skills/leniu-report-scenario/references/customization-table-fields.md +93 -0
- package/.codex/skills/leniu-report-scenario/references/customization.md +356 -0
- package/.codex/skills/leniu-report-scenario/references/data-permission.md +182 -0
- package/.codex/skills/leniu-report-scenario/references/export.md +553 -0
- package/.codex/skills/leniu-report-scenario/references/mealtime.md +197 -0
- package/.codex/skills/leniu-report-scenario/references/query-param.md +274 -0
- package/.codex/skills/leniu-report-scenario/references/report-tables.md +162 -0
- package/.codex/skills/leniu-report-scenario/references/standard-customization.md +112 -0
- package/.codex/skills/leniu-report-scenario/references/standard-table-fields.md +113 -0
- package/.codex/skills/leniu-report-scenario/references/total-line.md +179 -0
- package/.codex/skills/leniu-security-guard/SKILL.md +306 -0
- package/.codex/skills/leniu-utils-toolkit/SKILL.md +380 -0
- package/.codex/skills/loki-log-query/SKILL.md +430 -0
- package/.codex/skills/loki-log-query/environments.json +45 -0
- package/.codex/skills/mysql-debug/SKILL.md +406 -0
- package/.codex/skills/next/SKILL.md +137 -0
- package/.codex/skills/openspec-apply-change/SKILL.md +165 -0
- package/.codex/skills/openspec-archive-change/SKILL.md +122 -0
- package/.codex/skills/openspec-bulk-archive-change/SKILL.md +254 -0
- package/.codex/skills/openspec-continue-change/SKILL.md +126 -0
- package/.codex/skills/openspec-explore/SKILL.md +299 -0
- package/.codex/skills/openspec-ff-change/SKILL.md +109 -0
- package/.codex/skills/openspec-new-change/SKILL.md +82 -0
- package/.codex/skills/openspec-onboard/SKILL.md +414 -0
- package/.codex/skills/openspec-sync-specs/SKILL.md +146 -0
- package/.codex/skills/openspec-verify-change/SKILL.md +176 -0
- package/.codex/skills/performance-doctor/SKILL.md +297 -0
- package/.codex/skills/progress/SKILL.md +193 -0
- package/.codex/skills/project-navigator/SKILL.md +211 -0
- package/.codex/skills/redis-cache/SKILL.md +282 -0
- package/.codex/skills/redis-cache/references/listeners.md +23 -0
- package/.codex/skills/scheduled-jobs/SKILL.md +277 -0
- package/.codex/skills/security-guard/SKILL.md +245 -0
- package/.codex/skills/security-guard/references/encrypt-config.md +103 -0
- package/.codex/skills/security-guard/references/sensitive-strategies.md +42 -0
- package/.codex/skills/skill-creator/LICENSE.txt +202 -0
- package/.codex/skills/skill-creator/SKILL.md +479 -0
- package/.codex/skills/skill-creator/agents/analyzer.md +274 -0
- package/.codex/skills/skill-creator/agents/comparator.md +202 -0
- package/.codex/skills/skill-creator/agents/grader.md +223 -0
- package/.codex/skills/skill-creator/assets/eval_review.html +146 -0
- package/.codex/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/.codex/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/.codex/skills/skill-creator/references/schemas.md +430 -0
- package/.codex/skills/skill-creator/scripts/__init__.py +0 -0
- package/.codex/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/.codex/skills/skill-creator/scripts/generate_report.py +326 -0
- package/.codex/skills/skill-creator/scripts/improve_description.py +248 -0
- package/.codex/skills/skill-creator/scripts/package_skill.py +136 -0
- package/.codex/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/.codex/skills/skill-creator/scripts/run_eval.py +310 -0
- package/.codex/skills/skill-creator/scripts/run_loop.py +332 -0
- package/.codex/skills/skill-creator/scripts/utils.py +47 -0
- package/.codex/skills/sms-mail/SKILL.md +346 -0
- package/.codex/skills/sms-mail/references/mail-config.md +88 -0
- package/.codex/skills/sms-mail/references/sms-config.md +74 -0
- package/.codex/skills/social-login/SKILL.md +328 -0
- package/.codex/skills/social-login/references/provider-configs.md +118 -0
- package/.codex/skills/start/SKILL.md +154 -0
- package/.codex/skills/store-pc/SKILL.md +366 -0
- package/.codex/skills/sync/SKILL.md +149 -0
- package/.codex/skills/sync-back-merge/SKILL.md +66 -0
- package/.codex/skills/task-tracker/SKILL.md +307 -0
- package/.codex/skills/tech-decision/SKILL.md +393 -0
- package/.codex/skills/tenant-management/SKILL.md +272 -0
- package/.codex/skills/tenant-management/references/tenant-scenarios.md +91 -0
- package/.codex/skills/test-development/SKILL.md +301 -0
- package/.codex/skills/test-development/references/parameterized-examples.md +119 -0
- package/.codex/skills/ui-pc/SKILL.md +438 -0
- package/.codex/skills/update-status/SKILL.md +159 -0
- package/.codex/skills/utils-toolkit/SKILL.md +354 -0
- package/.codex/skills/utils-toolkit/references/redis-utils-api.md +56 -0
- package/.codex/skills/websocket-sse/SKILL.md +350 -0
- package/.codex/skills/workflow-engine/SKILL.md +249 -0
- package/.codex/skills/yunxiao-task-management/SKILL.md +401 -0
- package/.codex/skills/yunxiao-task-management/templates//346/217/220/346/265/213/345/215/225/346/250/241/346/235/277.html +17 -0
- package/.cursor/agents/bug-analyzer.md +102 -0
- package/.cursor/agents/code-reviewer.md +122 -0
- package/.cursor/agents/code-scanner.md +145 -0
- package/.cursor/agents/image-reader.md +154 -0
- package/.cursor/agents/loki-runner.md +80 -0
- package/.cursor/agents/mysql-runner.md +81 -0
- package/.cursor/agents/project-manager.md +159 -0
- package/.cursor/agents/requirements-analyzer.md +141 -0
- package/.cursor/agents/task-fetcher.md +75 -0
- package/.cursor/audio/completed.wav +0 -0
- package/.cursor/commands/opsx-apply.md +152 -0
- package/.cursor/commands/opsx-archive.md +157 -0
- package/.cursor/commands/opsx-bulk-archive.md +242 -0
- package/.cursor/commands/opsx-continue.md +114 -0
- package/.cursor/commands/opsx-explore.md +174 -0
- package/.cursor/commands/opsx-ff.md +94 -0
- package/.cursor/commands/opsx-new.md +69 -0
- package/.cursor/commands/opsx-onboard.md +525 -0
- package/.cursor/commands/opsx-sync.md +134 -0
- package/.cursor/commands/opsx-verify.md +164 -0
- package/.cursor/hooks/cursor-pre-tool-use.js +122 -0
- package/.cursor/hooks/cursor-skill-eval.js +466 -0
- package/.cursor/hooks/lib/notify.js +310 -0
- package/.cursor/hooks/stop.js +55 -0
- package/.cursor/hooks.json +23 -0
- package/.cursor/mcp.json +22 -0
- package/.cursor/rules/skill-activation.mdc +99 -0
- package/.cursor/skills/add-skill/SKILL.md +488 -0
- package/.cursor/skills/analyze-requirements/SKILL.md +112 -0
- package/.cursor/skills/api-development/SKILL.md +315 -0
- package/.cursor/skills/architecture-design/SKILL.md +152 -0
- package/.cursor/skills/auto-test/SKILL.md +453 -0
- package/.cursor/skills/auto-test/references/api-conventions.md +260 -0
- package/.cursor/skills/backend-annotations/SKILL.md +248 -0
- package/.cursor/skills/banana-image/CHANGELOG.md +37 -0
- package/.cursor/skills/banana-image/README.md +146 -0
- package/.cursor/skills/banana-image/SKILL.md +171 -0
- package/.cursor/skills/banana-image/assets/logo.png +0 -0
- package/.cursor/skills/banana-image/references/advanced-usage.md +189 -0
- package/.cursor/skills/banana-image/scripts/apply_template.py +125 -0
- package/.cursor/skills/banana-image/scripts/banana_image_exec.ts +412 -0
- package/.cursor/skills/banana-image/scripts/batch_prep.py +82 -0
- package/.cursor/skills/banana-image/scripts/package-lock.json +1437 -0
- package/.cursor/skills/banana-image/scripts/package.json +18 -0
- package/.cursor/skills/banana-image/scripts/requirements.txt +10 -0
- package/.cursor/skills/banana-image/templates/poster.json +22 -0
- package/.cursor/skills/banana-image/templates/product.json +17 -0
- package/.cursor/skills/banana-image/templates/social.json +22 -0
- package/.cursor/skills/banana-image/templates/thumbnail.json +17 -0
- package/.cursor/skills/brainstorm/SKILL.md +216 -0
- package/.cursor/skills/bug-detective/SKILL.md +295 -0
- package/.cursor/skills/bug-detective/references/error-patterns.md +242 -0
- package/.cursor/skills/code-patterns/SKILL.md +163 -0
- package/.cursor/skills/code-patterns/references/leniu-code-patterns.md +87 -0
- package/.cursor/skills/collaborating-with-codex/SKILL.md +180 -0
- package/.cursor/skills/collaborating-with-codex/scripts/codex_bridge.py +275 -0
- package/.cursor/skills/collaborating-with-gemini/SKILL.md +194 -0
- package/.cursor/skills/collaborating-with-gemini/scripts/gemini_bridge.py +275 -0
- package/.cursor/skills/crud-development/SKILL.md +328 -0
- package/.cursor/skills/data-permission/SKILL.md +221 -0
- package/.cursor/skills/data-permission/references/custom-data-scope.md +90 -0
- package/.cursor/skills/database-ops/SKILL.md +210 -0
- package/.cursor/skills/error-handler/SKILL.md +310 -0
- package/.cursor/skills/file-oss-management/SKILL.md +260 -0
- package/.cursor/skills/file-oss-management/references/entities.md +105 -0
- package/.cursor/skills/file-oss-management/references/service-impl.md +104 -0
- package/.cursor/skills/fix-bug/SKILL.md +269 -0
- package/.cursor/skills/git-workflow/SKILL.md +179 -0
- package/.cursor/skills/jenkins-deploy/SKILL.md +134 -0
- package/.cursor/skills/json-serialization/SKILL.md +341 -0
- package/.cursor/skills/lanhu-design/SKILL.md +99 -0
- package/.cursor/skills/leniu-api-development/SKILL.md +319 -0
- package/.cursor/skills/leniu-api-development/references/real-examples.md +273 -0
- package/.cursor/skills/leniu-architecture-design/SKILL.md +383 -0
- package/.cursor/skills/leniu-backend-annotations/SKILL.md +277 -0
- package/.cursor/skills/leniu-brainstorm/SKILL.md +242 -0
- package/.cursor/skills/leniu-brainstorm/references/business-scenarios.md +162 -0
- package/.cursor/skills/leniu-code-patterns/SKILL.md +411 -0
- package/.cursor/skills/leniu-crud-development/SKILL.md +404 -0
- package/.cursor/skills/leniu-crud-development/references/templates.md +597 -0
- package/.cursor/skills/leniu-customization-location/SKILL.md +410 -0
- package/.cursor/skills/leniu-data-permission/SKILL.md +341 -0
- package/.cursor/skills/leniu-database-ops/SKILL.md +426 -0
- package/.cursor/skills/leniu-error-handler/SKILL.md +462 -0
- package/.cursor/skills/leniu-java-amount-handling/SKILL.md +461 -0
- package/.cursor/skills/leniu-java-code-style/SKILL.md +510 -0
- package/.cursor/skills/leniu-java-concurrent/SKILL.md +400 -0
- package/.cursor/skills/leniu-java-entity/SKILL.md +237 -0
- package/.cursor/skills/leniu-java-entity/references/templates.md +237 -0
- package/.cursor/skills/leniu-java-logging/SKILL.md +229 -0
- package/.cursor/skills/leniu-java-logging/references/data-mask.md +46 -0
- package/.cursor/skills/leniu-java-logging/references/logging-scenarios.md +113 -0
- package/.cursor/skills/leniu-java-mq/SKILL.md +338 -0
- package/.cursor/skills/leniu-java-mybatis/SKILL.md +267 -0
- package/.cursor/skills/leniu-java-mybatis/references/report-mapper.md +88 -0
- package/.cursor/skills/leniu-java-task/SKILL.md +367 -0
- package/.cursor/skills/leniu-marketing-scenario/SKILL.md +448 -0
- package/.cursor/skills/leniu-marketing-scenario/references/pay-meal-rules.md +197 -0
- package/.cursor/skills/leniu-marketing-scenario/references/price-rules.md +286 -0
- package/.cursor/skills/leniu-marketing-scenario/references/recharge-rules.md +188 -0
- package/.cursor/skills/leniu-redis-cache/SKILL.md +331 -0
- package/.cursor/skills/leniu-report-scenario/SKILL.md +508 -0
- package/.cursor/skills/leniu-report-scenario/references/amount-handling.md +448 -0
- package/.cursor/skills/leniu-report-scenario/references/analysis-module.md +64 -0
- package/.cursor/skills/leniu-report-scenario/references/customization-table-fields.md +93 -0
- package/.cursor/skills/leniu-report-scenario/references/customization.md +356 -0
- package/.cursor/skills/leniu-report-scenario/references/data-permission.md +182 -0
- package/.cursor/skills/leniu-report-scenario/references/export.md +553 -0
- package/.cursor/skills/leniu-report-scenario/references/mealtime.md +197 -0
- package/.cursor/skills/leniu-report-scenario/references/query-param.md +274 -0
- package/.cursor/skills/leniu-report-scenario/references/report-tables.md +162 -0
- package/.cursor/skills/leniu-report-scenario/references/standard-customization.md +112 -0
- package/.cursor/skills/leniu-report-scenario/references/standard-table-fields.md +113 -0
- package/.cursor/skills/leniu-report-scenario/references/total-line.md +179 -0
- package/.cursor/skills/leniu-security-guard/SKILL.md +306 -0
- package/.cursor/skills/leniu-utils-toolkit/SKILL.md +380 -0
- package/.cursor/skills/loki-log-query/SKILL.md +430 -0
- package/.cursor/skills/loki-log-query/environments.json +45 -0
- package/.cursor/skills/mysql-debug/SKILL.md +406 -0
- package/.cursor/skills/openspec-apply-change/SKILL.md +165 -0
- package/.cursor/skills/openspec-archive-change/SKILL.md +122 -0
- package/.cursor/skills/openspec-bulk-archive-change/SKILL.md +254 -0
- package/.cursor/skills/openspec-continue-change/SKILL.md +126 -0
- package/.cursor/skills/openspec-explore/SKILL.md +299 -0
- package/.cursor/skills/openspec-ff-change/SKILL.md +109 -0
- package/.cursor/skills/openspec-new-change/SKILL.md +82 -0
- package/.cursor/skills/openspec-onboard/SKILL.md +414 -0
- package/.cursor/skills/openspec-sync-specs/SKILL.md +146 -0
- package/.cursor/skills/openspec-verify-change/SKILL.md +176 -0
- package/.cursor/skills/performance-doctor/SKILL.md +297 -0
- package/.cursor/skills/project-navigator/SKILL.md +211 -0
- package/.cursor/skills/redis-cache/SKILL.md +282 -0
- package/.cursor/skills/redis-cache/references/listeners.md +23 -0
- package/.cursor/skills/scheduled-jobs/SKILL.md +277 -0
- package/.cursor/skills/security-guard/SKILL.md +245 -0
- package/.cursor/skills/security-guard/references/encrypt-config.md +103 -0
- package/.cursor/skills/security-guard/references/sensitive-strategies.md +42 -0
- package/.cursor/skills/skill-creator/LICENSE.txt +202 -0
- package/.cursor/skills/skill-creator/SKILL.md +479 -0
- package/.cursor/skills/skill-creator/agents/analyzer.md +274 -0
- package/.cursor/skills/skill-creator/agents/comparator.md +202 -0
- package/.cursor/skills/skill-creator/agents/grader.md +223 -0
- package/.cursor/skills/skill-creator/assets/eval_review.html +146 -0
- package/.cursor/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/.cursor/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/.cursor/skills/skill-creator/references/schemas.md +430 -0
- package/.cursor/skills/skill-creator/scripts/__init__.py +0 -0
- package/.cursor/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/.cursor/skills/skill-creator/scripts/generate_report.py +326 -0
- package/.cursor/skills/skill-creator/scripts/improve_description.py +248 -0
- package/.cursor/skills/skill-creator/scripts/package_skill.py +136 -0
- package/.cursor/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/.cursor/skills/skill-creator/scripts/run_eval.py +310 -0
- package/.cursor/skills/skill-creator/scripts/run_loop.py +332 -0
- package/.cursor/skills/skill-creator/scripts/utils.py +47 -0
- package/.cursor/skills/sms-mail/SKILL.md +346 -0
- package/.cursor/skills/sms-mail/references/mail-config.md +88 -0
- package/.cursor/skills/sms-mail/references/sms-config.md +74 -0
- package/.cursor/skills/social-login/SKILL.md +328 -0
- package/.cursor/skills/social-login/references/provider-configs.md +118 -0
- package/.cursor/skills/store-pc/SKILL.md +366 -0
- package/.cursor/skills/sync-back-merge/SKILL.md +66 -0
- package/.cursor/skills/task-tracker/SKILL.md +307 -0
- package/.cursor/skills/tech-decision/SKILL.md +393 -0
- package/.cursor/skills/tenant-management/SKILL.md +272 -0
- package/.cursor/skills/tenant-management/references/tenant-scenarios.md +91 -0
- package/.cursor/skills/test-development/SKILL.md +301 -0
- package/.cursor/skills/test-development/references/parameterized-examples.md +119 -0
- package/.cursor/skills/ui-pc/SKILL.md +438 -0
- package/.cursor/skills/utils-toolkit/SKILL.md +354 -0
- package/.cursor/skills/utils-toolkit/references/redis-utils-api.md +56 -0
- package/.cursor/skills/websocket-sse/SKILL.md +350 -0
- package/.cursor/skills/workflow-engine/SKILL.md +249 -0
- package/.cursor/skills/yunxiao-task-management/SKILL.md +401 -0
- package/.cursor/skills/yunxiao-task-management/templates//346/217/220/346/265/213/345/215/225/346/250/241/346/235/277.html +17 -0
- package/.cursor/templates/env-config.md +27 -0
- package/AGENTS.md +275 -0
- package/CLAUDE.md +285 -0
- package/README.md +104 -0
- package/bin/index.js +3069 -0
- package/init.sh +178 -0
- package/package.json +40 -0
- package/scripts/build-skills.js +180 -0
- package/src/platform-map.json +100 -0
- package/src/skills/add-skill/SKILL.md +488 -0
- package/src/skills/add-todo/SKILL.md +269 -0
- package/src/skills/analyze-requirements/SKILL.md +112 -0
- package/src/skills/api-development/SKILL.md +315 -0
- package/src/skills/architecture-design/SKILL.md +152 -0
- package/src/skills/backend-annotations/SKILL.md +248 -0
- package/src/skills/banana-image/CHANGELOG.md +37 -0
- package/src/skills/banana-image/README.md +146 -0
- package/src/skills/banana-image/SKILL.md +171 -0
- package/src/skills/banana-image/assets/logo.png +0 -0
- package/src/skills/banana-image/references/advanced-usage.md +189 -0
- package/src/skills/banana-image/scripts/apply_template.py +125 -0
- package/src/skills/banana-image/scripts/banana_image_exec.ts +412 -0
- package/src/skills/banana-image/scripts/batch_prep.py +82 -0
- package/src/skills/banana-image/scripts/package-lock.json +1437 -0
- package/src/skills/banana-image/scripts/package.json +18 -0
- package/src/skills/banana-image/scripts/requirements.txt +10 -0
- package/src/skills/banana-image/templates/poster.json +22 -0
- package/src/skills/banana-image/templates/product.json +17 -0
- package/src/skills/banana-image/templates/social.json +22 -0
- package/src/skills/banana-image/templates/thumbnail.json +17 -0
- package/src/skills/brainstorm/SKILL.md +216 -0
- package/src/skills/bug-detective/SKILL.md +295 -0
- package/src/skills/bug-detective/references/error-patterns.md +242 -0
- package/src/skills/check/SKILL.md +367 -0
- package/src/skills/code-patterns/SKILL.md +163 -0
- package/src/skills/code-patterns/references/leniu-code-patterns.md +87 -0
- package/src/skills/codex-code-review/SKILL.md +327 -0
- package/src/skills/collaborating-with-codex/SKILL.md +180 -0
- package/src/skills/collaborating-with-codex/scripts/codex_bridge.py +275 -0
- package/src/skills/collaborating-with-gemini/SKILL.md +194 -0
- package/src/skills/collaborating-with-gemini/scripts/gemini_bridge.py +275 -0
- package/src/skills/crud/SKILL.md +265 -0
- package/src/skills/crud-development/SKILL.md +328 -0
- package/src/skills/data-permission/SKILL.md +221 -0
- package/src/skills/data-permission/references/custom-data-scope.md +90 -0
- package/src/skills/database-ops/SKILL.md +210 -0
- package/src/skills/dev/SKILL.md +187 -0
- package/src/skills/error-handler/SKILL.md +310 -0
- package/src/skills/file-oss-management/SKILL.md +260 -0
- package/src/skills/file-oss-management/references/entities.md +105 -0
- package/src/skills/file-oss-management/references/service-impl.md +104 -0
- package/src/skills/fix-bug/SKILL.md +269 -0
- package/src/skills/git-workflow/SKILL.md +179 -0
- package/src/skills/init-docs/SKILL.md +194 -0
- package/src/skills/json-serialization/SKILL.md +341 -0
- package/src/skills/leniu-api-development/SKILL.md +319 -0
- package/src/skills/leniu-api-development/references/real-examples.md +273 -0
- package/src/skills/leniu-architecture-design/SKILL.md +383 -0
- package/src/skills/leniu-backend-annotations/SKILL.md +277 -0
- package/src/skills/leniu-brainstorm/SKILL.md +242 -0
- package/src/skills/leniu-brainstorm/references/business-scenarios.md +162 -0
- package/src/skills/leniu-code-patterns/SKILL.md +411 -0
- package/src/skills/leniu-crud-development/SKILL.md +404 -0
- package/src/skills/leniu-crud-development/references/templates.md +597 -0
- package/src/skills/leniu-customization-location/SKILL.md +410 -0
- package/src/skills/leniu-data-permission/SKILL.md +341 -0
- package/src/skills/leniu-database-ops/SKILL.md +426 -0
- package/src/skills/leniu-error-handler/SKILL.md +462 -0
- package/src/skills/leniu-java-amount-handling/SKILL.md +461 -0
- package/src/skills/leniu-java-code-style/SKILL.md +510 -0
- package/src/skills/leniu-java-concurrent/SKILL.md +400 -0
- package/src/skills/leniu-java-entity/SKILL.md +237 -0
- package/src/skills/leniu-java-entity/references/templates.md +237 -0
- package/src/skills/leniu-java-export/SKILL.md +570 -0
- package/src/skills/leniu-java-logging/SKILL.md +229 -0
- package/src/skills/leniu-java-logging/references/data-mask.md +46 -0
- package/src/skills/leniu-java-logging/references/logging-scenarios.md +113 -0
- package/src/skills/leniu-java-mq/SKILL.md +338 -0
- package/src/skills/leniu-java-mybatis/SKILL.md +267 -0
- package/src/skills/leniu-java-mybatis/references/report-mapper.md +88 -0
- package/src/skills/leniu-java-report-query-param/SKILL.md +291 -0
- package/src/skills/leniu-java-task/SKILL.md +367 -0
- package/src/skills/leniu-java-total-line/SKILL.md +196 -0
- package/src/skills/leniu-marketing-price-rule-customizer/SKILL.md +301 -0
- package/src/skills/leniu-marketing-recharge-rule-customizer/SKILL.md +285 -0
- package/src/skills/leniu-mealtime/SKILL.md +215 -0
- package/src/skills/leniu-redis-cache/SKILL.md +331 -0
- package/src/skills/leniu-report-customization/SKILL.md +415 -0
- package/src/skills/leniu-report-customization/references/table-fields.md +93 -0
- package/src/skills/leniu-report-standard-customization/SKILL.md +391 -0
- package/src/skills/leniu-report-standard-customization/references/analysis-module.md +64 -0
- package/src/skills/leniu-report-standard-customization/references/table-fields.md +113 -0
- package/src/skills/leniu-security-guard/SKILL.md +306 -0
- package/src/skills/leniu-utils-toolkit/SKILL.md +380 -0
- package/src/skills/loki-log-query/SKILL.md +400 -0
- package/src/skills/loki-log-query/environments.json +45 -0
- package/src/skills/mysql-debug/SKILL.md +400 -0
- package/src/skills/next/SKILL.md +137 -0
- package/src/skills/openspec-apply-change/SKILL.md +165 -0
- package/src/skills/openspec-archive-change/SKILL.md +122 -0
- package/src/skills/openspec-bulk-archive-change/SKILL.md +254 -0
- package/src/skills/openspec-continue-change/SKILL.md +126 -0
- package/src/skills/openspec-explore/SKILL.md +299 -0
- package/src/skills/openspec-ff-change/SKILL.md +109 -0
- package/src/skills/openspec-new-change/SKILL.md +82 -0
- package/src/skills/openspec-onboard/SKILL.md +414 -0
- package/src/skills/openspec-sync-specs/SKILL.md +146 -0
- package/src/skills/openspec-verify-change/SKILL.md +176 -0
- package/src/skills/performance-doctor/SKILL.md +297 -0
- package/src/skills/progress/SKILL.md +193 -0
- package/src/skills/project-navigator/SKILL.md +211 -0
- package/src/skills/redis-cache/SKILL.md +282 -0
- package/src/skills/redis-cache/references/listeners.md +23 -0
- package/src/skills/scheduled-jobs/SKILL.md +277 -0
- package/src/skills/security-guard/SKILL.md +245 -0
- package/src/skills/security-guard/references/encrypt-config.md +103 -0
- package/src/skills/security-guard/references/sensitive-strategies.md +42 -0
- package/src/skills/skill-creator/LICENSE.txt +202 -0
- package/src/skills/skill-creator/SKILL.md +479 -0
- package/src/skills/skill-creator/agents/analyzer.md +274 -0
- package/src/skills/skill-creator/agents/comparator.md +202 -0
- package/src/skills/skill-creator/agents/grader.md +223 -0
- package/src/skills/skill-creator/assets/eval_review.html +146 -0
- package/src/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/src/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/src/skills/skill-creator/references/schemas.md +430 -0
- package/src/skills/skill-creator/scripts/__init__.py +0 -0
- package/src/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/src/skills/skill-creator/scripts/generate_report.py +326 -0
- package/src/skills/skill-creator/scripts/improve_description.py +248 -0
- package/src/skills/skill-creator/scripts/package_skill.py +136 -0
- package/src/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/src/skills/skill-creator/scripts/run_eval.py +310 -0
- package/src/skills/skill-creator/scripts/run_loop.py +332 -0
- package/src/skills/skill-creator/scripts/utils.py +47 -0
- package/src/skills/sms-mail/SKILL.md +346 -0
- package/src/skills/sms-mail/references/mail-config.md +88 -0
- package/src/skills/sms-mail/references/sms-config.md +74 -0
- package/src/skills/social-login/SKILL.md +328 -0
- package/src/skills/social-login/references/provider-configs.md +118 -0
- package/src/skills/start/SKILL.md +154 -0
- package/src/skills/store-pc/SKILL.md +366 -0
- package/src/skills/sync/SKILL.md +149 -0
- package/src/skills/sync-back-merge/SKILL.md +66 -0
- package/src/skills/task-tracker/SKILL.md +307 -0
- package/src/skills/tech-decision/SKILL.md +393 -0
- package/src/skills/tenant-management/SKILL.md +272 -0
- package/src/skills/tenant-management/references/tenant-scenarios.md +91 -0
- package/src/skills/test-development/SKILL.md +301 -0
- package/src/skills/test-development/references/parameterized-examples.md +119 -0
- package/src/skills/ui-pc/SKILL.md +438 -0
- package/src/skills/update-status/SKILL.md +159 -0
- package/src/skills/utils-toolkit/SKILL.md +354 -0
- package/src/skills/utils-toolkit/references/redis-utils-api.md +56 -0
- package/src/skills/websocket-sse/SKILL.md +350 -0
- package/src/skills/workflow-engine/SKILL.md +249 -0
- package/src/skills/yunxiao-task-management/SKILL.md +401 -0
- package/src/skills/yunxiao-task-management/templates//346/217/220/346/265/213/345/215/225/346/250/241/346/235/277.html +17 -0
package/bin/index.js
ADDED
|
@@ -0,0 +1,3069 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* leniu-dev — AI 工程化开发工具
|
|
4
|
+
* 用法:
|
|
5
|
+
* npx leniu-dev install # 交互式安装向导(用户级)
|
|
6
|
+
* npx leniu-dev update # 更新已安装的框架文件
|
|
7
|
+
* npx leniu-dev syncback # 推送本地技能修改到源仓库
|
|
8
|
+
* npx leniu-dev config # 环境配置(数据库/日志)
|
|
9
|
+
* npx leniu-dev mcp # MCP 服务器管理
|
|
10
|
+
* npx leniu-dev doctor # 诊断安装状态
|
|
11
|
+
* npx leniu-dev uninstall # 卸载
|
|
12
|
+
* npx leniu-dev help # 帮助
|
|
13
|
+
*
|
|
14
|
+
* 向后兼容(v1 命令自动映射):
|
|
15
|
+
* init → install, global → install, sync-back → syncback
|
|
16
|
+
*/
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const readline = require('readline');
|
|
23
|
+
|
|
24
|
+
// ── ANSI 颜色(Windows CMD/PowerShell 兼容)────────────────────────────────
|
|
25
|
+
// Windows Terminal 设置 WT_SESSION,ConEmu/Cmder 设置 COLORTERM,VSCode 设置 TERM_PROGRAM
|
|
26
|
+
const supportsColor = !!process.stdout.isTTY && (
|
|
27
|
+
process.platform !== 'win32' ||
|
|
28
|
+
!!process.env.WT_SESSION ||
|
|
29
|
+
!!process.env.COLORTERM ||
|
|
30
|
+
process.env.TERM_PROGRAM === 'vscode'
|
|
31
|
+
);
|
|
32
|
+
const ESC = supportsColor ? {
|
|
33
|
+
reset: '\x1b[0m', bold: '\x1b[1m',
|
|
34
|
+
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
|
|
35
|
+
blue: '\x1b[34m', cyan: '\x1b[36m', magenta: '\x1b[35m',
|
|
36
|
+
} : Object.fromEntries(
|
|
37
|
+
['reset','bold','red','green','yellow','blue','cyan','magenta'].map(k => [k, ''])
|
|
38
|
+
);
|
|
39
|
+
const fmt = (color, text) => `${ESC[color]}${text}${ESC.reset}`;
|
|
40
|
+
|
|
41
|
+
// ── 版本 ───────────────────────────────────────────────────────────────────
|
|
42
|
+
const PKG_VERSION = require('../package.json').version;
|
|
43
|
+
|
|
44
|
+
// ── Banner ─────────────────────────────────────────────────────────────────
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(fmt('blue', fmt('bold', '┌─────────────────────────────────────────┐')));
|
|
47
|
+
console.log(fmt('blue', fmt('bold', `│ 🐂 leniu-dev v${PKG_VERSION} │`)));
|
|
48
|
+
console.log(fmt('blue', fmt('bold', `│ AI 工程化开发工具 │`)));
|
|
49
|
+
console.log(fmt('blue', fmt('bold', '└─────────────────────────────────────────┘')));
|
|
50
|
+
console.log('');
|
|
51
|
+
|
|
52
|
+
// ── 参数解析 ───────────────────────────────────────────────────────────────
|
|
53
|
+
const args = process.argv.slice(2);
|
|
54
|
+
let command = ''; // 'install' | 'update' | 'syncback' | 'config' | 'mcp' | 'help' | 'doctor' | 'uninstall'
|
|
55
|
+
let tool = '';
|
|
56
|
+
let targetDir = process.cwd();
|
|
57
|
+
let force = false;
|
|
58
|
+
let skillFilter = ''; // syncback --skill <名称>
|
|
59
|
+
let submitIssue = false; // syncback --submit
|
|
60
|
+
let configType = ''; // config --type <mysql|loki|all>
|
|
61
|
+
let configScope = ''; // config --scope <local|global>
|
|
62
|
+
let configAdd = false; // config --add
|
|
63
|
+
let configFrom = ''; // config --from <file.md>
|
|
64
|
+
let installRole = ''; // install --role <backend|frontend|product|all>
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < args.length; i++) {
|
|
67
|
+
const arg = args[i];
|
|
68
|
+
switch (arg) {
|
|
69
|
+
case 'install':
|
|
70
|
+
command = 'install';
|
|
71
|
+
break;
|
|
72
|
+
case 'update':
|
|
73
|
+
command = 'update';
|
|
74
|
+
break;
|
|
75
|
+
case 'syncback':
|
|
76
|
+
command = 'syncback';
|
|
77
|
+
break;
|
|
78
|
+
case 'config':
|
|
79
|
+
command = 'config';
|
|
80
|
+
break;
|
|
81
|
+
case 'mcp':
|
|
82
|
+
command = 'mcp';
|
|
83
|
+
break;
|
|
84
|
+
case 'help':
|
|
85
|
+
printHelp();
|
|
86
|
+
process.exit(0);
|
|
87
|
+
break;
|
|
88
|
+
case 'doctor':
|
|
89
|
+
command = 'doctor';
|
|
90
|
+
break;
|
|
91
|
+
case 'uninstall':
|
|
92
|
+
command = 'uninstall';
|
|
93
|
+
break;
|
|
94
|
+
// ── 向后兼容 v1 命令 ──
|
|
95
|
+
case 'init':
|
|
96
|
+
case 'global':
|
|
97
|
+
command = 'install';
|
|
98
|
+
break;
|
|
99
|
+
case 'sync-back':
|
|
100
|
+
command = 'syncback';
|
|
101
|
+
break;
|
|
102
|
+
case '--tool': case '-t':
|
|
103
|
+
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
104
|
+
console.error(fmt('red', `错误:${arg} 需要一个值(claude | cursor | codex | all)`));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
tool = args[++i];
|
|
108
|
+
break;
|
|
109
|
+
case '--dir': case '-d':
|
|
110
|
+
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
111
|
+
console.error(fmt('red', `错误:${arg} 需要一个目录路径`));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
targetDir = path.resolve(args[++i]);
|
|
115
|
+
break;
|
|
116
|
+
case '--force': case '-f':
|
|
117
|
+
force = true;
|
|
118
|
+
break;
|
|
119
|
+
case '--skill': case '-s':
|
|
120
|
+
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
121
|
+
console.error(fmt('red', `错误:${arg} 需要一个技能名称`));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
skillFilter = args[++i];
|
|
125
|
+
break;
|
|
126
|
+
case '--submit':
|
|
127
|
+
submitIssue = true;
|
|
128
|
+
break;
|
|
129
|
+
case '--type':
|
|
130
|
+
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
131
|
+
console.error(fmt('red', `错误:${arg} 需要一个值(mysql | loki | all)`));
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
configType = args[++i];
|
|
135
|
+
break;
|
|
136
|
+
case '--scope':
|
|
137
|
+
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
138
|
+
console.error(fmt('red', `错误:${arg} 需要一个值(local | global)`));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
configScope = args[++i];
|
|
142
|
+
break;
|
|
143
|
+
case '--add':
|
|
144
|
+
configAdd = true;
|
|
145
|
+
break;
|
|
146
|
+
case '--from':
|
|
147
|
+
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
148
|
+
console.error(fmt('red', `错误:${arg} 需要一个文件路径`));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
configFrom = path.resolve(args[++i]);
|
|
152
|
+
break;
|
|
153
|
+
case '--role': case '-r':
|
|
154
|
+
if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
|
|
155
|
+
console.error(fmt('red', `错误:${arg} 需要一个值(backend | frontend | product | all)`));
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
installRole = args[++i];
|
|
159
|
+
break;
|
|
160
|
+
case '--help': case '-h':
|
|
161
|
+
printHelp();
|
|
162
|
+
process.exit(0);
|
|
163
|
+
break;
|
|
164
|
+
default:
|
|
165
|
+
// 拒绝未知选项,避免静默忽略导致行为不符预期
|
|
166
|
+
if (arg.startsWith('-')) {
|
|
167
|
+
console.error(fmt('red', `错误:未知选项 "${arg}",运行 --help 查看用法`));
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function printHelp() {
|
|
175
|
+
console.log(`用法: ${fmt('bold', 'npx leniu-dev')} <命令> [选项]\n`);
|
|
176
|
+
console.log('命令:');
|
|
177
|
+
console.log(` ${fmt('bold', 'install')} 交互式安装向导(安装到用户目录 ~/.claude 等)`);
|
|
178
|
+
console.log(` ${fmt('bold', 'update')} 更新已安装的框架文件(跳过用户自定义文件)`);
|
|
179
|
+
console.log(` ${fmt('bold', 'syncback')} 对比本地技能修改,生成 diff 或提交 GitHub Issue`);
|
|
180
|
+
console.log(` ${fmt('bold', 'config')} 环境配置(数据库连接 / Loki 日志)`);
|
|
181
|
+
console.log(` ${fmt('bold', 'mcp')} MCP 服务器管理(安装/卸载/状态)`);
|
|
182
|
+
console.log(` ${fmt('bold', 'doctor')} 诊断安装状态(检查文件/配置/MCP)`);
|
|
183
|
+
console.log(` ${fmt('bold', 'uninstall')} 卸载已安装的文件`);
|
|
184
|
+
console.log(` ${fmt('bold', 'help')} 显示此帮助\n`);
|
|
185
|
+
console.log('选项:');
|
|
186
|
+
console.log(' --tool, -t <工具> 指定工具: claude | cursor | codex | all');
|
|
187
|
+
console.log(' --role, -r <角色> 安装角色: backend | frontend | product | all');
|
|
188
|
+
console.log(' --force, -f 强制覆盖已有文件');
|
|
189
|
+
console.log(' --skill, -s <技能> syncback 时只对比指定技能');
|
|
190
|
+
console.log(' --submit syncback 时自动创建 GitHub Issue(需要 gh CLI)');
|
|
191
|
+
console.log(' --type <类型> config 时指定: mysql | loki | all');
|
|
192
|
+
console.log(' --scope <范围> config 时指定: local | global');
|
|
193
|
+
console.log(' --add config 时追加环境');
|
|
194
|
+
console.log(' --from <文件> config 时从 Markdown 文件解析(跳过交互)');
|
|
195
|
+
console.log(' --help, -h 显示此帮助\n');
|
|
196
|
+
console.log('示例:');
|
|
197
|
+
console.log(` ${fmt('cyan', 'npx leniu-dev install')} # 交互式安装`);
|
|
198
|
+
console.log(` ${fmt('cyan', 'npx leniu-dev install --tool claude')} # 安装 Claude Code`);
|
|
199
|
+
console.log(` ${fmt('cyan', 'npx leniu-dev update')} # 更新到最新版本`);
|
|
200
|
+
console.log(` ${fmt('cyan', 'npx leniu-dev syncback --submit')} # 推送修改到源仓库`);
|
|
201
|
+
console.log(` ${fmt('cyan', 'npx leniu-dev config --type mysql --scope global')} # 配置数据库`);
|
|
202
|
+
console.log(` ${fmt('cyan', 'npx leniu-dev mcp')} # 管理 MCP 服务器`);
|
|
203
|
+
console.log(` ${fmt('cyan', 'npx leniu-dev doctor')} # 诊断安装状态`);
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(fmt('yellow', '向后兼容:init/global → install, sync-back → syncback'));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── 工具定义(init 用)────────────────────────────────────────────────────
|
|
209
|
+
const TOOLS = {
|
|
210
|
+
claude: {
|
|
211
|
+
label: 'Claude Code',
|
|
212
|
+
files: [
|
|
213
|
+
{ src: '.claude', dest: '.claude', label: '.claude/ 目录', isDir: true },
|
|
214
|
+
{ src: 'CLAUDE.md', dest: 'CLAUDE.md', label: 'CLAUDE.md' },
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
cursor: {
|
|
218
|
+
label: 'Cursor',
|
|
219
|
+
files: [
|
|
220
|
+
{ src: '.cursor', dest: '.cursor', label: '.cursor/ 目录', isDir: true },
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
codex: {
|
|
224
|
+
label: 'OpenAI Codex',
|
|
225
|
+
files: [
|
|
226
|
+
{ src: '.codex', dest: '.codex', label: '.codex/ 目录', isDir: true },
|
|
227
|
+
{ src: 'AGENTS.md', dest: 'AGENTS.md', label: 'AGENTS.md' },
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// ── 更新规则(update 用)──────────────────────────────────────────────────
|
|
233
|
+
// update: 框架文件,从本机安装版本覆盖
|
|
234
|
+
// preserve: 用户自定义文件,默认跳过(--force 时强制覆盖)
|
|
235
|
+
const UPDATE_RULES = {
|
|
236
|
+
claude: {
|
|
237
|
+
label: 'Claude Code',
|
|
238
|
+
detect: '.claude',
|
|
239
|
+
update: [
|
|
240
|
+
{ src: '.claude/skills', dest: '.claude/skills', label: 'Skills(技能库)', isDir: true },
|
|
241
|
+
{ src: '.claude/commands', dest: '.claude/commands', label: 'Commands(快捷命令)', isDir: true },
|
|
242
|
+
{ src: '.claude/agents', dest: '.claude/agents', label: 'Agents(子代理)', isDir: true },
|
|
243
|
+
{ src: '.claude/hooks', dest: '.claude/hooks', label: 'Hooks(钩子脚本)', isDir: true },
|
|
244
|
+
{ src: '.claude/templates', dest: '.claude/templates', label: 'Templates', isDir: true },
|
|
245
|
+
{ src: '.claude/audio', dest: '.claude/audio', label: 'Audio(完成音效)', isDir: true },
|
|
246
|
+
{ src: '.claude/framework-config.json', dest: '.claude/framework-config.json', label: 'framework-config.json' },
|
|
247
|
+
],
|
|
248
|
+
preserve: [
|
|
249
|
+
{ dest: '.claude/settings.json', reason: '包含用户 MCP 配置和权限设置' },
|
|
250
|
+
{ dest: '.claude/notify-config.json', reason: '包含用户通知偏好设置' },
|
|
251
|
+
{ dest: 'CLAUDE.md', reason: '包含项目自定义规范' },
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
cursor: {
|
|
255
|
+
label: 'Cursor',
|
|
256
|
+
detect: '.cursor',
|
|
257
|
+
update: [
|
|
258
|
+
{ src: '.cursor/skills', dest: '.cursor/skills', label: 'Skills(技能库)', isDir: true },
|
|
259
|
+
{ src: '.cursor/agents', dest: '.cursor/agents', label: 'Agents(子代理)', isDir: true },
|
|
260
|
+
{ src: '.cursor/hooks', dest: '.cursor/hooks', label: 'Hooks(钩子脚本)', isDir: true },
|
|
261
|
+
{ src: '.cursor/audio', dest: '.cursor/audio', label: 'Audio(完成音效)', isDir: true },
|
|
262
|
+
{ src: '.cursor/hooks.json', dest: '.cursor/hooks.json', label: 'hooks.json(Hooks 配置)' },
|
|
263
|
+
],
|
|
264
|
+
preserve: [
|
|
265
|
+
{ dest: '.cursor/mcp.json', reason: '包含用户 MCP 服务器配置' },
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
codex: {
|
|
269
|
+
label: 'OpenAI Codex',
|
|
270
|
+
detect: '.codex',
|
|
271
|
+
update: [
|
|
272
|
+
{ src: '.codex/skills', dest: '.codex/skills', label: 'Skills(技能库)', isDir: true },
|
|
273
|
+
],
|
|
274
|
+
preserve: [
|
|
275
|
+
{ dest: 'AGENTS.md', reason: '包含项目自定义 Agent 规范' },
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// ── 全局安装规则(global 用)─────────────────────────────────────────────
|
|
281
|
+
// 安装到 ~/.claude / ~/.cursor / ~/.codex,对当前用户所有项目生效
|
|
282
|
+
const HOME_DIR = os.homedir();
|
|
283
|
+
|
|
284
|
+
const GLOBAL_RULES = {
|
|
285
|
+
claude: {
|
|
286
|
+
label: 'Claude Code',
|
|
287
|
+
targetDir: path.join(HOME_DIR, '.claude'),
|
|
288
|
+
files: [
|
|
289
|
+
{ src: '.claude/skills', dest: 'skills', label: 'Skills(全局技能库)', isDir: true },
|
|
290
|
+
{ src: '.claude/commands', dest: 'commands', label: 'Commands(全局命令)', isDir: true },
|
|
291
|
+
{ src: '.claude/agents', dest: 'agents', label: 'Agents(全局子代理)', isDir: true },
|
|
292
|
+
{ src: '.claude/hooks', dest: 'hooks', label: 'Hooks(全局钩子)', isDir: true },
|
|
293
|
+
{ src: '.claude/audio', dest: 'audio', label: 'Audio(完成音效)', isDir: true },
|
|
294
|
+
{ src: '.claude/framework-config.json', dest: 'framework-config.json', label: 'framework-config.json' },
|
|
295
|
+
{ src: '.claude/notify-config.json', dest: 'notify-config.json', label: 'notify-config.json(通知偏好)' },
|
|
296
|
+
{ src: '.claude/settings.json', dest: 'settings.json', label: 'settings.json(Hooks + MCP 配置)', merge: true, rewritePrefix: '.claude/' },
|
|
297
|
+
],
|
|
298
|
+
preserve: [],
|
|
299
|
+
note: `Skills/Commands/Hooks/Settings 已安装到 ~/.claude,对所有项目自动生效`,
|
|
300
|
+
},
|
|
301
|
+
cursor: {
|
|
302
|
+
label: 'Cursor',
|
|
303
|
+
targetDir: path.join(HOME_DIR, '.cursor'),
|
|
304
|
+
files: [
|
|
305
|
+
{ src: '.cursor/skills', dest: 'skills', label: 'Skills(全局技能库)', isDir: true },
|
|
306
|
+
{ src: '.cursor/agents', dest: 'agents', label: 'Agents(全局子代理)', isDir: true },
|
|
307
|
+
{ src: '.cursor/hooks', dest: 'hooks', label: 'Hooks(全局钩子脚本)', isDir: true },
|
|
308
|
+
{ src: '.cursor/audio', dest: 'audio', label: 'Audio(完成音效)', isDir: true },
|
|
309
|
+
{ src: '.cursor/hooks.json', dest: 'hooks.json', label: 'hooks.json(Hooks 触发配置)', rewritePrefix: '.cursor/' },
|
|
310
|
+
{ src: '.cursor/mcp.json', dest: 'mcp.json', label: 'mcp.json(MCP 服务器配置)', merge: true },
|
|
311
|
+
],
|
|
312
|
+
preserve: [],
|
|
313
|
+
note: `Skills/Hooks/MCP 已安装到 ~/.cursor,重启 Cursor 后生效`,
|
|
314
|
+
},
|
|
315
|
+
codex: {
|
|
316
|
+
label: 'OpenAI Codex',
|
|
317
|
+
targetDir: path.join(HOME_DIR, '.codex'),
|
|
318
|
+
files: [
|
|
319
|
+
{ src: '.codex/skills', dest: 'skills', label: 'Skills(全局技能库)', isDir: true },
|
|
320
|
+
],
|
|
321
|
+
preserve: [],
|
|
322
|
+
note: `Skills 已安装到 ~/.codex`,
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/** 合并 JSON 文件:将 src 的键补充到 dest,已有的键保留不覆盖 */
|
|
327
|
+
function mergeJsonFile(srcPath, destPath, label) {
|
|
328
|
+
try {
|
|
329
|
+
const srcData = JSON.parse(fs.readFileSync(srcPath, 'utf8'));
|
|
330
|
+
let destData = {};
|
|
331
|
+
if (fs.existsSync(destPath)) {
|
|
332
|
+
try { destData = JSON.parse(fs.readFileSync(destPath, 'utf8')); } catch { destData = {}; }
|
|
333
|
+
}
|
|
334
|
+
// 深度合并第一层对象键(如 mcpServers),已有的不覆盖
|
|
335
|
+
let added = 0;
|
|
336
|
+
for (const [key, value] of Object.entries(srcData)) {
|
|
337
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
338
|
+
if (!destData[key]) destData[key] = {};
|
|
339
|
+
for (const [subKey, subValue] of Object.entries(value)) {
|
|
340
|
+
if (!(subKey in destData[key])) {
|
|
341
|
+
destData[key][subKey] = subValue;
|
|
342
|
+
added++;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} else if (!(key in destData)) {
|
|
346
|
+
destData[key] = value;
|
|
347
|
+
added++;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
fs.writeFileSync(destPath, JSON.stringify(destData, null, 2) + '\n');
|
|
351
|
+
console.log(` ${fmt('green', '✓')} ${label} ${fmt('magenta', `(合并 +${added} 项,已有配置保留)`)}`);
|
|
352
|
+
return added > 0 ? 1 : 0;
|
|
353
|
+
} catch (e) {
|
|
354
|
+
console.log(` ${fmt('red', '✗')} ${label} 合并失败: ${e.message}`);
|
|
355
|
+
return -1;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** 将 JSON 中的相对路径重写为绝对路径(递归遍历所有字符串值) */
|
|
360
|
+
function rewritePaths(obj, relPrefix, absPrefix) {
|
|
361
|
+
if (typeof obj === 'string') {
|
|
362
|
+
// 匹配 "node .claude/hooks/xxx" 或 ".cursor/hooks/xxx" 等相对路径
|
|
363
|
+
return obj.split(' ').map(part =>
|
|
364
|
+
part.startsWith(relPrefix) ? part.replace(relPrefix, absPrefix) : part
|
|
365
|
+
).join(' ');
|
|
366
|
+
}
|
|
367
|
+
if (Array.isArray(obj)) return obj.map(item => rewritePaths(item, relPrefix, absPrefix));
|
|
368
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
369
|
+
const result = {};
|
|
370
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
371
|
+
result[k] = rewritePaths(v, relPrefix, absPrefix);
|
|
372
|
+
}
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
return obj;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** 重写已写入的 JSON 文件中的相对路径为绝对路径 */
|
|
379
|
+
function rewriteJsonFilePaths(filePath, relPrefix, absPrefix) {
|
|
380
|
+
try {
|
|
381
|
+
let data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
382
|
+
data = rewritePaths(data, relPrefix, absPrefix);
|
|
383
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
384
|
+
} catch { /* 静默失败 */ }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** 全局安装单个工具 */
|
|
388
|
+
function globalInstallTool(toolKey, allowedSkills) {
|
|
389
|
+
const rule = GLOBAL_RULES[toolKey];
|
|
390
|
+
const globalDest = rule.targetDir;
|
|
391
|
+
console.log(fmt('cyan', `[${rule.label}]`) + fmt('blue', ` → ${globalDest}`));
|
|
392
|
+
|
|
393
|
+
let installed = 0, failed = 0;
|
|
394
|
+
|
|
395
|
+
try { fs.mkdirSync(globalDest, { recursive: true }); } catch { /* 已存在忽略 */ }
|
|
396
|
+
|
|
397
|
+
for (const item of rule.files) {
|
|
398
|
+
const srcPath = path.join(SOURCE_DIR, item.src);
|
|
399
|
+
const destPath = path.join(globalDest, item.dest);
|
|
400
|
+
|
|
401
|
+
if (!fs.existsSync(srcPath)) {
|
|
402
|
+
console.log(` ${fmt('yellow', '⚠')} ${item.label} 源文件不存在,跳过`);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// merge 模式:合并 JSON 而非覆盖(保留用户已有配置)
|
|
407
|
+
if (item.merge) {
|
|
408
|
+
const result = mergeJsonFile(srcPath, destPath, item.label);
|
|
409
|
+
// merge 后路径重写
|
|
410
|
+
if (result >= 0 && item.rewritePrefix) {
|
|
411
|
+
rewriteJsonFilePaths(destPath, item.rewritePrefix, globalDest + '/');
|
|
412
|
+
}
|
|
413
|
+
if (result >= 0) installed++; else failed++;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (fs.existsSync(destPath) && !force) {
|
|
418
|
+
console.log(` ${fmt('yellow', '⚠')} ${item.label} 已存在,跳过(--force 可强制覆盖)`);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
if (item.isDir) {
|
|
423
|
+
// 对 skills 目录按角色过滤
|
|
424
|
+
const isSkillsDir = item.src.endsWith('/skills');
|
|
425
|
+
const n = (isSkillsDir && allowedSkills)
|
|
426
|
+
? copyDirFiltered(srcPath, destPath, allowedSkills)
|
|
427
|
+
: copyDir(srcPath, destPath);
|
|
428
|
+
console.log(` ${fmt('green', '✓')} ${item.label} ${fmt('magenta', `(${n} 个文件)`)}`);
|
|
429
|
+
installed += n;
|
|
430
|
+
} else {
|
|
431
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
432
|
+
// 非 merge JSON 文件的路径重写
|
|
433
|
+
if (item.rewritePrefix && destPath.endsWith('.json')) {
|
|
434
|
+
try {
|
|
435
|
+
let data = JSON.parse(fs.readFileSync(srcPath, 'utf8'));
|
|
436
|
+
data = rewritePaths(data, item.rewritePrefix, globalDest + '/');
|
|
437
|
+
fs.writeFileSync(destPath, JSON.stringify(data, null, 2) + '\n');
|
|
438
|
+
} catch { fs.copyFileSync(srcPath, destPath); }
|
|
439
|
+
} else {
|
|
440
|
+
fs.copyFileSync(srcPath, destPath);
|
|
441
|
+
}
|
|
442
|
+
console.log(` ${fmt('green', '✓')} ${item.label}`);
|
|
443
|
+
installed++;
|
|
444
|
+
}
|
|
445
|
+
} catch (e) {
|
|
446
|
+
console.log(` ${fmt('red', '✗')} ${item.label} 安装失败: ${e.message}`);
|
|
447
|
+
failed++;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
for (const item of rule.preserve) {
|
|
452
|
+
const destPath = path.join(globalDest, item.dest);
|
|
453
|
+
if (fs.existsSync(destPath)) {
|
|
454
|
+
console.log(` ${fmt('yellow', '⊘')} ${item.dest} 已保留 — ${item.reason}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return { installed, failed };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** global 命令主流程 */
|
|
462
|
+
function runGlobal(selectedTool) {
|
|
463
|
+
const validKeys = Object.keys(GLOBAL_RULES);
|
|
464
|
+
const toolsToInstall = (!selectedTool || selectedTool === 'all')
|
|
465
|
+
? validKeys
|
|
466
|
+
: [selectedTool];
|
|
467
|
+
|
|
468
|
+
if (selectedTool && selectedTool !== 'all' && !GLOBAL_RULES[selectedTool]) {
|
|
469
|
+
console.error(fmt('red', `无效工具: "${selectedTool}"。有效选项: claude | cursor | codex | all`));
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
console.log(` 安装模式: ${fmt('green', fmt('bold', '全局安装(当前用户所有项目生效)'))}`);
|
|
474
|
+
console.log(` 安装工具: ${fmt('bold', toolsToInstall.join(', '))}`);
|
|
475
|
+
if (force) console.log(` ${fmt('yellow', '⚠ --force 模式:强制覆盖已有文件')}`);
|
|
476
|
+
console.log('');
|
|
477
|
+
console.log(fmt('bold', '正在安装到系统目录...'));
|
|
478
|
+
console.log('');
|
|
479
|
+
|
|
480
|
+
let totalInstalled = 0, totalFailed = 0;
|
|
481
|
+
for (let i = 0; i < toolsToInstall.length; i++) {
|
|
482
|
+
const { installed, failed } = globalInstallTool(toolsToInstall[i]);
|
|
483
|
+
totalInstalled += installed;
|
|
484
|
+
totalFailed += failed;
|
|
485
|
+
if (i < toolsToInstall.length - 1) console.log('');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
console.log('');
|
|
489
|
+
console.log(fmt('green', fmt('bold', '✅ 全局安装完成!')));
|
|
490
|
+
console.log('');
|
|
491
|
+
console.log(` ${fmt('green', `✓ 安装文件: ${totalInstalled} 个`)}`);
|
|
492
|
+
if (totalFailed > 0) {
|
|
493
|
+
console.log(` ${fmt('red', `✗ 失败文件: ${totalFailed} 个`)}(请检查目录权限)`);
|
|
494
|
+
}
|
|
495
|
+
console.log('');
|
|
496
|
+
console.log(fmt('cyan', '安装位置说明:'));
|
|
497
|
+
for (const key of toolsToInstall) {
|
|
498
|
+
const rule = GLOBAL_RULES[key];
|
|
499
|
+
console.log(` ${fmt('bold', rule.label + ':')} ${rule.note}`);
|
|
500
|
+
}
|
|
501
|
+
console.log('');
|
|
502
|
+
console.log(fmt('yellow', '提示:项目级配置(.claude/ 等)优先级高于全局配置,两者可同时使用。'));
|
|
503
|
+
console.log('');
|
|
504
|
+
|
|
505
|
+
if (totalFailed > 0) process.exitCode = 1;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── 牛马吃草动画 ──────────────────────────────────────────────────────────
|
|
509
|
+
|
|
510
|
+
const COW_FRAMES = [
|
|
511
|
+
{ cow: '🐂 ', grass: '🌿🌿🌿🌿🌿' },
|
|
512
|
+
{ cow: '🐂🌿 ', grass: '🌿🌿🌿🌿' },
|
|
513
|
+
{ cow: '🐂🌿🌿 ', grass: '🌿🌿🌿' },
|
|
514
|
+
{ cow: '🐂🌿🌿🌿 ', grass: '🌿🌿' },
|
|
515
|
+
{ cow: '🐂🌿🌿🌿🌿 ', grass: '🌿' },
|
|
516
|
+
{ cow: '🐂🌿🌿🌿🌿🌿', grass: '' },
|
|
517
|
+
];
|
|
518
|
+
|
|
519
|
+
/** 显示牛马吃草进度动画(非 TTY 时静默) */
|
|
520
|
+
function showCowProgress(current, total, label) {
|
|
521
|
+
if (!process.stdout.isTTY) return;
|
|
522
|
+
const ratio = Math.min(current / total, 1);
|
|
523
|
+
const frameIdx = Math.min(Math.floor(ratio * (COW_FRAMES.length - 1)), COW_FRAMES.length - 1);
|
|
524
|
+
const frame = COW_FRAMES[frameIdx];
|
|
525
|
+
const pct = Math.round(ratio * 100);
|
|
526
|
+
const bar = '█'.repeat(Math.floor(ratio * 20)) + '░'.repeat(20 - Math.floor(ratio * 20));
|
|
527
|
+
process.stdout.write(`\r ${frame.cow} ${frame.grass} ${bar} ${pct}% ${label} `);
|
|
528
|
+
if (ratio >= 1) process.stdout.write('\n');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ── 版本更新检测 ──────────────────────────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
/** 检查 npm 上是否有更新版本(带 24h 缓存) */
|
|
534
|
+
function checkForUpdates() {
|
|
535
|
+
const cachePath = path.join(HOME_DIR, '.claude', '.update-check-cache.json');
|
|
536
|
+
const ONE_DAY = 24 * 60 * 60 * 1000;
|
|
537
|
+
|
|
538
|
+
// 读取缓存
|
|
539
|
+
try {
|
|
540
|
+
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
541
|
+
if (Date.now() - new Date(cache.checkedAt).getTime() < ONE_DAY) {
|
|
542
|
+
if (cache.latestVersion && cache.latestVersion !== PKG_VERSION) {
|
|
543
|
+
showUpdateNotice(cache.latestVersion);
|
|
544
|
+
}
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
} catch { /* 无缓存或解析失败 */ }
|
|
548
|
+
|
|
549
|
+
// 异步检查(不阻塞主流程)
|
|
550
|
+
try {
|
|
551
|
+
const { execSync } = require('child_process');
|
|
552
|
+
const latest = execSync('npm view leniu-dev version 2>/dev/null', {
|
|
553
|
+
encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
|
|
554
|
+
}).trim();
|
|
555
|
+
// 写入缓存
|
|
556
|
+
try {
|
|
557
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
558
|
+
fs.writeFileSync(cachePath, JSON.stringify({ latestVersion: latest, checkedAt: new Date().toISOString() }) + '\n');
|
|
559
|
+
} catch { /* 静默 */ }
|
|
560
|
+
if (latest && latest !== PKG_VERSION) {
|
|
561
|
+
showUpdateNotice(latest);
|
|
562
|
+
}
|
|
563
|
+
} catch { /* 网络不通或命令失败,静默跳过 */ }
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function showUpdateNotice(latestVersion) {
|
|
567
|
+
console.log(fmt('yellow', fmt('bold', ` 🐂 leniu-dev 有新版本可用! v${PKG_VERSION} → v${latestVersion}`)));
|
|
568
|
+
console.log(fmt('yellow', ` 运行 ${fmt('bold', 'npx leniu-dev@latest update')} 更新`));
|
|
569
|
+
console.log('');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// 在非 help/doctor 命令时检查更新
|
|
573
|
+
if (command !== '' && !['help', 'doctor'].includes(command)) {
|
|
574
|
+
checkForUpdates();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ── 公共工具函数 ──────────────────────────────────────────────────────────
|
|
578
|
+
const SOURCE_DIR = path.join(__dirname, '..');
|
|
579
|
+
|
|
580
|
+
/** 安全判断路径是否为真实目录(避免 existsSync 将文件误判为已安装目录) */
|
|
581
|
+
function isRealDir(p) {
|
|
582
|
+
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** 递归复制目录,返回实际写入的文件数;单文件失败不中断整体 */
|
|
586
|
+
function copyDir(src, dest) {
|
|
587
|
+
let written = 0;
|
|
588
|
+
try {
|
|
589
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
590
|
+
} catch (e) {
|
|
591
|
+
console.log(` ${fmt('red', '✗')} 无法创建目录 ${dest}: ${e.message}`);
|
|
592
|
+
return written;
|
|
593
|
+
}
|
|
594
|
+
let entries;
|
|
595
|
+
try {
|
|
596
|
+
entries = fs.readdirSync(src);
|
|
597
|
+
} catch (e) {
|
|
598
|
+
console.log(` ${fmt('red', '✗')} 无法读取源目录 ${src}: ${e.message}`);
|
|
599
|
+
return written;
|
|
600
|
+
}
|
|
601
|
+
for (const entry of entries) {
|
|
602
|
+
const s = path.join(src, entry);
|
|
603
|
+
const d = path.join(dest, entry);
|
|
604
|
+
try {
|
|
605
|
+
fs.statSync(s).isDirectory() ? (written += copyDir(s, d)) : (fs.copyFileSync(s, d), written++);
|
|
606
|
+
} catch (e) {
|
|
607
|
+
console.log(` ${fmt('yellow', '⚠')} 跳过文件 ${d}: ${e.message}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return written;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** 构建包含 --dir 上下文的提示命令(方便用户直接复制执行) */
|
|
614
|
+
function hintCmd(subCmd) {
|
|
615
|
+
return `npx leniu-dev ${subCmd}`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ── INIT 逻辑 ─────────────────────────────────────────────────────────────
|
|
619
|
+
function copyItem({ src, dest, label, isDir: srcIsDir }) {
|
|
620
|
+
const srcPath = path.join(SOURCE_DIR, src);
|
|
621
|
+
const destPath = path.join(targetDir, dest);
|
|
622
|
+
|
|
623
|
+
if (!fs.existsSync(srcPath)) {
|
|
624
|
+
console.log(` ${fmt('yellow', '⚠')} ${label} 在源目录中不存在,跳过`);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (fs.existsSync(destPath) && !force) {
|
|
628
|
+
console.log(` ${fmt('yellow', '⚠')} ${label} 已存在,跳过(--force 可强制覆盖)`);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
try {
|
|
632
|
+
if (srcIsDir) {
|
|
633
|
+
const n = copyDir(srcPath, destPath);
|
|
634
|
+
console.log(` ${fmt('green', '✓')} ${label} ${fmt('magenta', `(${n} 个文件)`)}`);
|
|
635
|
+
} else {
|
|
636
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
637
|
+
fs.copyFileSync(srcPath, destPath);
|
|
638
|
+
console.log(` ${fmt('green', '✓')} ${label}`);
|
|
639
|
+
}
|
|
640
|
+
} catch (e) {
|
|
641
|
+
console.log(` ${fmt('red', '✗')} ${label} 复制失败: ${e.message}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function initTool(toolKey) {
|
|
646
|
+
const t = TOOLS[toolKey];
|
|
647
|
+
console.log(fmt('cyan', `[${t.label}]`));
|
|
648
|
+
// 确保目标根目录存在(兼容 --dir 指向尚不存在的路径)
|
|
649
|
+
try { fs.mkdirSync(targetDir, { recursive: true }); } catch { /* 已存在忽略 */ }
|
|
650
|
+
for (const f of t.files) copyItem(f);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function showDoneHint(toolKey) {
|
|
654
|
+
console.log('');
|
|
655
|
+
console.log(fmt('green', fmt('bold', '✅ 初始化完成!')));
|
|
656
|
+
console.log('');
|
|
657
|
+
if (toolKey === 'claude' || toolKey === 'all') {
|
|
658
|
+
console.log(fmt('cyan', 'Claude Code 使用:'));
|
|
659
|
+
console.log(` 1. ${fmt('bold', '必做')}:修改 ${fmt('bold', 'CLAUDE.md')} — 把 [你的xxx] 占位符替换为项目实际信息`);
|
|
660
|
+
console.log(` 2. 在 Claude Code 中输入 ${fmt('bold', '/start')} 快速了解项目`);
|
|
661
|
+
console.log(` 3. 输入 ${fmt('bold', '/dev')} 开始开发新功能`);
|
|
662
|
+
console.log('');
|
|
663
|
+
console.log(fmt('yellow', ' 💡 CLAUDE.md 和 AGENTS.md 是示例模板,务必替换占位符后使用'));
|
|
664
|
+
console.log(fmt('yellow', ` 💡 运行 ${fmt('bold', hintCmd('mcp'))} 管理 MCP 服务器(云效、语雀等)`));
|
|
665
|
+
console.log('');
|
|
666
|
+
}
|
|
667
|
+
if (toolKey === 'cursor' || toolKey === 'all') {
|
|
668
|
+
console.log(fmt('cyan', 'Cursor 使用:'));
|
|
669
|
+
console.log(` 1. 在 Cursor Chat 中输入 ${fmt('bold', '/')} 查看所有可用 Skills`);
|
|
670
|
+
console.log(` 2. 输入 ${fmt('bold', '@技能名')} 手动调用指定技能`);
|
|
671
|
+
console.log(` 3. 在 Settings → MCP 中确认 MCP 服务器已连接`);
|
|
672
|
+
console.log(fmt('yellow', ` 💡 运行 ${fmt('bold', hintCmd('mcp'))} 管理 MCP 服务器(云效、语雀等)`));
|
|
673
|
+
console.log('');
|
|
674
|
+
}
|
|
675
|
+
if (toolKey === 'codex' || toolKey === 'all') {
|
|
676
|
+
console.log(fmt('cyan', 'Codex 使用:'));
|
|
677
|
+
console.log(` 1. 按需修改 ${fmt('bold', 'AGENTS.md')} 中的项目说明`);
|
|
678
|
+
console.log(` 2. 在 Codex 中使用 .codex/skills/ 下的技能`);
|
|
679
|
+
console.log('');
|
|
680
|
+
}
|
|
681
|
+
showJenkinsHint();
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** 检测 jenkins/ 是否已初始化,提示用户配置部署环境 */
|
|
685
|
+
function showJenkinsHint() {
|
|
686
|
+
const homeClaudeDir = path.join(os.homedir(), '.claude');
|
|
687
|
+
const globalConfig = path.join(homeClaudeDir, 'jenkins-config.json');
|
|
688
|
+
const localConfig = path.join(targetDir, '.claude', 'jenkins-config.json');
|
|
689
|
+
const skillAssetsDir = path.join(targetDir, '.claude', 'skills', 'jenkins-deploy', 'assets');
|
|
690
|
+
// 全局安装时也检查全局技能目录
|
|
691
|
+
const globalSkillDir = path.join(homeClaudeDir, 'skills', 'jenkins-deploy', 'assets');
|
|
692
|
+
|
|
693
|
+
const hasSkill = fs.existsSync(skillAssetsDir) || fs.existsSync(globalSkillDir);
|
|
694
|
+
const hasConfig = fs.existsSync(globalConfig) || fs.existsSync(localConfig);
|
|
695
|
+
|
|
696
|
+
// 技能存在但凭证未配置
|
|
697
|
+
if (hasSkill && !hasConfig) {
|
|
698
|
+
console.log(fmt('yellow', fmt('bold', '📦 Jenkins 部署凭证未配置')));
|
|
699
|
+
console.log(` 已安装 ${fmt('bold', 'jenkins-deploy')} 技能,但 ${fmt('bold', 'jenkins-config.json')} 尚未配置。`);
|
|
700
|
+
console.log(` 配置方式:`);
|
|
701
|
+
console.log(` 方式 1:从团队成员处拷贝 ${fmt('bold', '~/.claude/jenkins-config.json')}`);
|
|
702
|
+
console.log(` 方式 2:在 AI 对话中说 ${fmt('bold', '"初始化部署环境"')}`);
|
|
703
|
+
console.log('');
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function run(selectedTool) {
|
|
708
|
+
if (!Object.keys(TOOLS).concat('all').includes(selectedTool)) {
|
|
709
|
+
console.error(fmt('red', `无效工具: "${selectedTool}"。有效选项: claude | cursor | codex | all`));
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
console.log(` 目标目录: ${fmt('bold', targetDir)}`);
|
|
713
|
+
console.log(` 初始化工具: ${fmt('bold', selectedTool)}`);
|
|
714
|
+
console.log('');
|
|
715
|
+
console.log(fmt('bold', '正在复制文件...'));
|
|
716
|
+
console.log('');
|
|
717
|
+
|
|
718
|
+
if (selectedTool === 'all') {
|
|
719
|
+
Object.keys(TOOLS).forEach((k, i) => { if (i) console.log(''); initTool(k); });
|
|
720
|
+
} else {
|
|
721
|
+
initTool(selectedTool);
|
|
722
|
+
}
|
|
723
|
+
showDoneHint(selectedTool);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// ── UPDATE 逻辑 ───────────────────────────────────────────────────────────
|
|
727
|
+
|
|
728
|
+
/** 检测当前目录已安装了哪些工具(用 isRealDir 排除误判) */
|
|
729
|
+
function detectInstalledTools() {
|
|
730
|
+
return Object.keys(UPDATE_RULES).filter(key =>
|
|
731
|
+
isRealDir(path.join(targetDir, UPDATE_RULES[key].detect))
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** 更新单个工具,返回 { updated, failed, preserved } 文件数 */
|
|
736
|
+
function updateTool(toolKey) {
|
|
737
|
+
const rule = UPDATE_RULES[toolKey];
|
|
738
|
+
console.log(fmt('cyan', `[${rule.label}]`));
|
|
739
|
+
|
|
740
|
+
let updated = 0, failed = 0, preserved = 0;
|
|
741
|
+
|
|
742
|
+
// 更新框架文件
|
|
743
|
+
for (const item of rule.update) {
|
|
744
|
+
const srcPath = path.join(SOURCE_DIR, item.src);
|
|
745
|
+
const destPath = path.join(targetDir, item.dest);
|
|
746
|
+
|
|
747
|
+
if (!fs.existsSync(srcPath)) {
|
|
748
|
+
console.log(` ${fmt('yellow', '⚠')} ${item.label} 源文件不存在,跳过`);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
try {
|
|
752
|
+
if (item.isDir) {
|
|
753
|
+
const n = copyDir(srcPath, destPath);
|
|
754
|
+
console.log(` ${fmt('green', '✓')} ${item.label} ${fmt('magenta', `(${n} 个文件)`)}`);
|
|
755
|
+
updated += n;
|
|
756
|
+
} else {
|
|
757
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
758
|
+
fs.copyFileSync(srcPath, destPath);
|
|
759
|
+
console.log(` ${fmt('green', '✓')} ${item.label}`);
|
|
760
|
+
updated++;
|
|
761
|
+
}
|
|
762
|
+
} catch (e) {
|
|
763
|
+
console.log(` ${fmt('red', '✗')} ${item.label} 失败: ${e.message}`);
|
|
764
|
+
failed++;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// 处理保留文件
|
|
769
|
+
for (const item of rule.preserve) {
|
|
770
|
+
const destPath = path.join(targetDir, item.dest);
|
|
771
|
+
const srcPath = path.join(SOURCE_DIR, item.dest);
|
|
772
|
+
|
|
773
|
+
if (force) {
|
|
774
|
+
if (fs.existsSync(srcPath)) {
|
|
775
|
+
try {
|
|
776
|
+
if (isRealDir(srcPath)) {
|
|
777
|
+
const n = copyDir(srcPath, destPath);
|
|
778
|
+
updated += n;
|
|
779
|
+
} else {
|
|
780
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
781
|
+
fs.copyFileSync(srcPath, destPath);
|
|
782
|
+
updated++;
|
|
783
|
+
}
|
|
784
|
+
console.log(` ${fmt('green', '✓')} ${item.dest} ${fmt('yellow', '(强制更新)')}`);
|
|
785
|
+
} catch (e) {
|
|
786
|
+
console.log(` ${fmt('red', '✗')} ${item.dest} 强制更新失败: ${e.message}`);
|
|
787
|
+
failed++;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
} else {
|
|
791
|
+
const exists = fs.existsSync(destPath);
|
|
792
|
+
const mark = exists ? fmt('yellow', '已保留') : fmt('yellow', '不存在,跳过');
|
|
793
|
+
console.log(` ${fmt('yellow', '⊘')} ${item.dest} ${mark} — ${item.reason}`);
|
|
794
|
+
if (exists) preserved++;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return { updated, failed, preserved };
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/** update 命令主流程 — 同时更新用户级和项目级 */
|
|
802
|
+
function runUpdate(selectedTool) {
|
|
803
|
+
console.log(` 本机版本: ${fmt('bold', `v${PKG_VERSION}`)}`);
|
|
804
|
+
if (force) console.log(` ${fmt('yellow', '⚠ --force 模式:将同时更新保留文件')}`);
|
|
805
|
+
console.log('');
|
|
806
|
+
|
|
807
|
+
let toolsToUpdate = [];
|
|
808
|
+
|
|
809
|
+
// 优先检测用户级安装,其次项目级
|
|
810
|
+
const detectUserTools = () => Object.keys(GLOBAL_RULES).filter(key =>
|
|
811
|
+
isRealDir(path.join(HOME_DIR, GLOBAL_RULES[key].targetDir || path.join(HOME_DIR, '.' + key)))
|
|
812
|
+
);
|
|
813
|
+
const userTools = Object.keys(GLOBAL_RULES).filter(key =>
|
|
814
|
+
isRealDir(GLOBAL_RULES[key].targetDir)
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
if (!selectedTool || selectedTool === 'all') {
|
|
818
|
+
// 先尝试用户级,再尝试项目级
|
|
819
|
+
toolsToUpdate = userTools.length > 0 ? userTools : detectInstalledTools();
|
|
820
|
+
if (toolsToUpdate.length === 0) {
|
|
821
|
+
console.log(fmt('yellow', '⚠ 未检测到已安装的 AI 工具配置。'));
|
|
822
|
+
console.log(` 请先运行: ${fmt('bold', hintCmd('install --tool claude'))}\n`);
|
|
823
|
+
process.exit(1);
|
|
824
|
+
}
|
|
825
|
+
console.log(` 检测到已安装: ${fmt('bold', toolsToUpdate.join(', '))}`);
|
|
826
|
+
} else {
|
|
827
|
+
if (!GLOBAL_RULES[selectedTool]) {
|
|
828
|
+
console.error(fmt('red', `无效工具: "${selectedTool}"。有效选项: claude | cursor | codex | all`));
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
toolsToUpdate = [selectedTool];
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// 判断是否有用户级安装
|
|
835
|
+
const isUserLevel = userTools.length > 0;
|
|
836
|
+
console.log(` 更新模式: ${fmt('bold', isUserLevel ? '用户级' : '项目级')}`);
|
|
837
|
+
console.log('');
|
|
838
|
+
console.log(fmt('bold', '正在更新框架文件...'));
|
|
839
|
+
console.log('');
|
|
840
|
+
|
|
841
|
+
let totalUpdated = 0, totalFailed = 0, totalPreserved = 0;
|
|
842
|
+
for (let i = 0; i < toolsToUpdate.length; i++) {
|
|
843
|
+
if (isUserLevel) {
|
|
844
|
+
// 用户级更新:使用全局安装逻辑
|
|
845
|
+
const { installed, failed } = globalInstallTool(toolsToUpdate[i]);
|
|
846
|
+
totalUpdated += installed;
|
|
847
|
+
totalFailed += failed;
|
|
848
|
+
} else {
|
|
849
|
+
// 项目级更新:旧逻辑
|
|
850
|
+
const { updated, failed, preserved } = updateTool(toolsToUpdate[i]);
|
|
851
|
+
totalUpdated += updated;
|
|
852
|
+
totalFailed += failed;
|
|
853
|
+
totalPreserved += preserved;
|
|
854
|
+
}
|
|
855
|
+
if (i < toolsToUpdate.length - 1) console.log('');
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// 更新安装元数据
|
|
859
|
+
if (isUserLevel) {
|
|
860
|
+
writeInstallMeta(buildInstallMeta(toolsToUpdate));
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
console.log('');
|
|
864
|
+
console.log(fmt('green', fmt('bold', '✅ 更新完成!')));
|
|
865
|
+
console.log('');
|
|
866
|
+
console.log(` ${fmt('green', `✓ 更新文件: ${totalUpdated} 个`)}`);
|
|
867
|
+
if (totalFailed > 0) {
|
|
868
|
+
console.log(` ${fmt('red', `✗ 失败文件: ${totalFailed} 个`)}(请检查目录权限)`);
|
|
869
|
+
}
|
|
870
|
+
if (totalPreserved > 0) {
|
|
871
|
+
console.log(` ${fmt('yellow', `⊘ 已保留文件: ${totalPreserved} 个`)}(--force 可强制更新)`);
|
|
872
|
+
}
|
|
873
|
+
console.log('');
|
|
874
|
+
console.log(fmt('cyan', '提示:'));
|
|
875
|
+
console.log(' 重启 Claude Code / Cursor 使新技能生效');
|
|
876
|
+
console.log(` ${fmt('yellow', '注意')}:update 只新增/覆盖文件,不删除旧版本已移除的文件`);
|
|
877
|
+
if (!force && totalPreserved > 0) {
|
|
878
|
+
console.log(` 强制更新保留文件: ${fmt('bold', 'npx leniu-dev update --force')}`);
|
|
879
|
+
}
|
|
880
|
+
console.log('');
|
|
881
|
+
showJenkinsHint();
|
|
882
|
+
|
|
883
|
+
if (totalFailed > 0) process.exitCode = 1;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// ── SYNC-BACK 逻辑 ─────────────────────────────────────────────────────────
|
|
887
|
+
|
|
888
|
+
/** 技能目录名称到工具的映射 */
|
|
889
|
+
const SKILL_DIRS = {
|
|
890
|
+
claude: '.claude/skills',
|
|
891
|
+
cursor: '.cursor/skills',
|
|
892
|
+
codex: '.codex/skills',
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
/** 递归列出目录下所有文件(相对路径) */
|
|
896
|
+
function listFilesRecursive(dir, prefix) {
|
|
897
|
+
prefix = prefix || '';
|
|
898
|
+
let results = [];
|
|
899
|
+
let entries;
|
|
900
|
+
try { entries = fs.readdirSync(dir); } catch { return results; }
|
|
901
|
+
for (const entry of entries) {
|
|
902
|
+
const fullPath = path.join(dir, entry);
|
|
903
|
+
const relPath = prefix ? prefix + '/' + entry : entry;
|
|
904
|
+
try {
|
|
905
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
906
|
+
results = results.concat(listFilesRecursive(fullPath, relPath));
|
|
907
|
+
} else {
|
|
908
|
+
results.push(relPath);
|
|
909
|
+
}
|
|
910
|
+
} catch { /* 跳过不可读文件 */ }
|
|
911
|
+
}
|
|
912
|
+
return results;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* 简易 unified diff 生成器(纯 Node.js,零依赖)
|
|
917
|
+
* 使用贪心 LCS 简化算法,输出标准 unified diff 格式
|
|
918
|
+
*/
|
|
919
|
+
function generateDiff(oldContent, newContent, oldLabel, newLabel) {
|
|
920
|
+
const oldLines = oldContent.split(/\r?\n/);
|
|
921
|
+
const newLines = newContent.split(/\r?\n/);
|
|
922
|
+
|
|
923
|
+
// 计算 LCS 表(简化版,O(n*m) 但对技能文件足够)
|
|
924
|
+
const m = oldLines.length;
|
|
925
|
+
const n = newLines.length;
|
|
926
|
+
|
|
927
|
+
// 对于大文件,跳过 LCS 直接标记全部替换
|
|
928
|
+
if (m * n > 1000000) {
|
|
929
|
+
const lines = [];
|
|
930
|
+
lines.push(`--- ${oldLabel}`);
|
|
931
|
+
lines.push(`+++ ${newLabel}`);
|
|
932
|
+
lines.push(`@@ -1,${m} +1,${n} @@`);
|
|
933
|
+
for (const line of oldLines) lines.push('-' + line);
|
|
934
|
+
for (const line of newLines) lines.push('+' + line);
|
|
935
|
+
return lines.join('\n');
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// LCS 回溯表
|
|
939
|
+
const dp = Array.from({ length: m + 1 }, () => new Uint16Array(n + 1));
|
|
940
|
+
for (let i = 1; i <= m; i++) {
|
|
941
|
+
for (let j = 1; j <= n; j++) {
|
|
942
|
+
if (oldLines[i - 1] === newLines[j - 1]) {
|
|
943
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
944
|
+
} else {
|
|
945
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// 回溯生成编辑操作序列
|
|
951
|
+
const ops = []; // { type: 'equal'|'delete'|'insert', line }
|
|
952
|
+
let i = m, j = n;
|
|
953
|
+
while (i > 0 || j > 0) {
|
|
954
|
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
955
|
+
ops.push({ type: 'equal', oldIdx: i, newIdx: j, line: oldLines[i - 1] });
|
|
956
|
+
i--; j--;
|
|
957
|
+
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
958
|
+
ops.push({ type: 'insert', newIdx: j, line: newLines[j - 1] });
|
|
959
|
+
j--;
|
|
960
|
+
} else {
|
|
961
|
+
ops.push({ type: 'delete', oldIdx: i, line: oldLines[i - 1] });
|
|
962
|
+
i--;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
ops.reverse();
|
|
966
|
+
|
|
967
|
+
// 将操作序列组织为 hunks(上下文 3 行)
|
|
968
|
+
const CTX = 3;
|
|
969
|
+
const hunks = [];
|
|
970
|
+
let hunkOps = [];
|
|
971
|
+
let lastChangeIdx = -999;
|
|
972
|
+
|
|
973
|
+
for (let k = 0; k < ops.length; k++) {
|
|
974
|
+
if (ops[k].type !== 'equal') {
|
|
975
|
+
// 如果距离上次变更超过 2*CTX+1,开始新 hunk
|
|
976
|
+
if (k - lastChangeIdx > 2 * CTX + 1 && hunkOps.length > 0) {
|
|
977
|
+
hunks.push(hunkOps);
|
|
978
|
+
hunkOps = [];
|
|
979
|
+
// 回退加上下文
|
|
980
|
+
const start = Math.max(k - CTX, lastChangeIdx + CTX + 1);
|
|
981
|
+
for (let c = start; c < k; c++) {
|
|
982
|
+
if (ops[c]) hunkOps.push(ops[c]);
|
|
983
|
+
}
|
|
984
|
+
} else if (hunkOps.length === 0) {
|
|
985
|
+
// 新 hunk 加前上下文
|
|
986
|
+
const start = Math.max(0, k - CTX);
|
|
987
|
+
for (let c = start; c < k; c++) {
|
|
988
|
+
hunkOps.push(ops[c]);
|
|
989
|
+
}
|
|
990
|
+
} else {
|
|
991
|
+
// 补充中间的上下文行
|
|
992
|
+
for (let c = lastChangeIdx + 1; c < k; c++) {
|
|
993
|
+
if (!hunkOps.includes(ops[c])) hunkOps.push(ops[c]);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
hunkOps.push(ops[k]);
|
|
997
|
+
lastChangeIdx = k;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
// 最后一个 hunk 加后上下文
|
|
1001
|
+
if (hunkOps.length > 0) {
|
|
1002
|
+
const end = Math.min(ops.length, lastChangeIdx + CTX + 1);
|
|
1003
|
+
for (let c = lastChangeIdx + 1; c < end; c++) {
|
|
1004
|
+
hunkOps.push(ops[c]);
|
|
1005
|
+
}
|
|
1006
|
+
hunks.push(hunkOps);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (hunks.length === 0) return ''; // 无差异
|
|
1010
|
+
|
|
1011
|
+
// 格式化输出
|
|
1012
|
+
const lines = [];
|
|
1013
|
+
lines.push(`--- ${oldLabel}`);
|
|
1014
|
+
lines.push(`+++ ${newLabel}`);
|
|
1015
|
+
|
|
1016
|
+
for (const hunk of hunks) {
|
|
1017
|
+
// 计算 hunk 头
|
|
1018
|
+
let oldStart = Infinity, oldCount = 0, newStart = Infinity, newCount = 0;
|
|
1019
|
+
for (const op of hunk) {
|
|
1020
|
+
if (op.type === 'equal' || op.type === 'delete') {
|
|
1021
|
+
if (op.oldIdx < oldStart) oldStart = op.oldIdx;
|
|
1022
|
+
oldCount++;
|
|
1023
|
+
}
|
|
1024
|
+
if (op.type === 'equal' || op.type === 'insert') {
|
|
1025
|
+
if (op.newIdx < newStart) newStart = op.newIdx;
|
|
1026
|
+
newCount++;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
lines.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`);
|
|
1030
|
+
for (const op of hunk) {
|
|
1031
|
+
if (op.type === 'equal') lines.push(' ' + op.line);
|
|
1032
|
+
if (op.type === 'delete') lines.push('-' + op.line);
|
|
1033
|
+
if (op.type === 'insert') lines.push('+' + op.line);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return lines.join('\n');
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/** 统计 diff 中的增删行数 */
|
|
1041
|
+
function countDiffLines(diffText) {
|
|
1042
|
+
let added = 0, removed = 0;
|
|
1043
|
+
for (const line of diffText.split('\n')) {
|
|
1044
|
+
if (line.startsWith('+') && !line.startsWith('+++')) added++;
|
|
1045
|
+
if (line.startsWith('-') && !line.startsWith('---')) removed++;
|
|
1046
|
+
}
|
|
1047
|
+
return { added, removed };
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/** 检测 gh CLI 是否可用 */
|
|
1051
|
+
function isGhAvailable() {
|
|
1052
|
+
try {
|
|
1053
|
+
const { execSync } = require('child_process');
|
|
1054
|
+
execSync('gh --version', { stdio: 'pipe' });
|
|
1055
|
+
return true;
|
|
1056
|
+
} catch { return false; }
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/** 通过 gh CLI 创建 GitHub Issue */
|
|
1060
|
+
function submitGitHubIssue(changes, allDiffText) {
|
|
1061
|
+
const { execSync } = require('child_process');
|
|
1062
|
+
const skillNames = changes.map(c => c.skillName).join(', ');
|
|
1063
|
+
const title = `[sync-back] 技能改进:${skillNames}`;
|
|
1064
|
+
const body = [
|
|
1065
|
+
'## 技能修改反馈',
|
|
1066
|
+
'',
|
|
1067
|
+
`> 由 \`npx leniu-dev sync-back --submit\` 自动生成`,
|
|
1068
|
+
'',
|
|
1069
|
+
'### 修改的技能',
|
|
1070
|
+
'',
|
|
1071
|
+
...changes.map(c => {
|
|
1072
|
+
const files = c.files.map(f => ` - \`${f.relPath}\` (+${f.added}, -${f.removed})`).join('\n');
|
|
1073
|
+
return `- **${c.skillName}**\n${files}`;
|
|
1074
|
+
}),
|
|
1075
|
+
'',
|
|
1076
|
+
'### Diff',
|
|
1077
|
+
'',
|
|
1078
|
+
'```diff',
|
|
1079
|
+
allDiffText,
|
|
1080
|
+
'```',
|
|
1081
|
+
'',
|
|
1082
|
+
'---',
|
|
1083
|
+
`CLI 版本: v${PKG_VERSION}`,
|
|
1084
|
+
].join('\n');
|
|
1085
|
+
|
|
1086
|
+
try {
|
|
1087
|
+
const result = execSync(
|
|
1088
|
+
`gh issue create --repo xu-cell/ai-engineering-init --title "${title.replace(/"/g, '\\"')}" --body-file -`,
|
|
1089
|
+
{ input: body, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8' }
|
|
1090
|
+
);
|
|
1091
|
+
return result.trim();
|
|
1092
|
+
} catch (e) {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/** sync-back 命令主流程 */
|
|
1098
|
+
function runSyncBack(selectedTool, selectedSkill, doSubmit) {
|
|
1099
|
+
console.log(` 目标目录: ${fmt('bold', targetDir)}`);
|
|
1100
|
+
console.log(` 本机版本: ${fmt('bold', `v${PKG_VERSION}`)}`);
|
|
1101
|
+
console.log('');
|
|
1102
|
+
|
|
1103
|
+
// 1. 确定扫描范围
|
|
1104
|
+
let toolsToScan = [];
|
|
1105
|
+
if (selectedTool && selectedTool !== 'all') {
|
|
1106
|
+
if (!SKILL_DIRS[selectedTool]) {
|
|
1107
|
+
console.error(fmt('red', `无效工具: "${selectedTool}"。有效选项: claude | cursor | codex | all`));
|
|
1108
|
+
process.exit(1);
|
|
1109
|
+
}
|
|
1110
|
+
toolsToScan = [selectedTool];
|
|
1111
|
+
} else {
|
|
1112
|
+
toolsToScan = detectInstalledTools();
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (toolsToScan.length === 0) {
|
|
1116
|
+
console.log(fmt('yellow', '⚠ 当前目录未检测到已安装的 AI 工具配置。'));
|
|
1117
|
+
console.log(` 请先运行: ${fmt('bold', hintCmd('--tool claude'))}\n`);
|
|
1118
|
+
process.exit(1);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
console.log(` 扫描工具: ${fmt('bold', toolsToScan.join(', '))}`);
|
|
1122
|
+
console.log('');
|
|
1123
|
+
console.log(fmt('bold', '🔍 正在对比技能文件...'));
|
|
1124
|
+
console.log('');
|
|
1125
|
+
|
|
1126
|
+
// 2. 对比每个工具的 skills 目录
|
|
1127
|
+
const allChanges = []; // { toolKey, skillName, files: [{ relPath, diff, added, removed }] }
|
|
1128
|
+
|
|
1129
|
+
for (const toolKey of toolsToScan) {
|
|
1130
|
+
const skillDir = SKILL_DIRS[toolKey];
|
|
1131
|
+
const userSkillsDir = path.join(targetDir, skillDir);
|
|
1132
|
+
const srcSkillsDir = path.join(SOURCE_DIR, skillDir);
|
|
1133
|
+
|
|
1134
|
+
if (!isRealDir(userSkillsDir) || !isRealDir(srcSkillsDir)) continue;
|
|
1135
|
+
|
|
1136
|
+
// 列出用户目录中的技能
|
|
1137
|
+
let skillNames;
|
|
1138
|
+
try { skillNames = fs.readdirSync(userSkillsDir); } catch { continue; }
|
|
1139
|
+
|
|
1140
|
+
for (const name of skillNames) {
|
|
1141
|
+
if (selectedSkill && name !== selectedSkill) continue;
|
|
1142
|
+
|
|
1143
|
+
const userSkillDir = path.join(userSkillsDir, name);
|
|
1144
|
+
const srcSkillDir = path.join(srcSkillsDir, name);
|
|
1145
|
+
|
|
1146
|
+
if (!isRealDir(userSkillDir)) continue;
|
|
1147
|
+
|
|
1148
|
+
// 列出用户技能目录下所有文件
|
|
1149
|
+
const userFiles = listFilesRecursive(userSkillDir);
|
|
1150
|
+
const srcFiles = isRealDir(srcSkillDir) ? listFilesRecursive(srcSkillDir) : [];
|
|
1151
|
+
const allFiles = [...new Set([...userFiles, ...srcFiles])].sort();
|
|
1152
|
+
|
|
1153
|
+
const changedFiles = [];
|
|
1154
|
+
|
|
1155
|
+
for (const relFile of allFiles) {
|
|
1156
|
+
const userFile = path.join(userSkillDir, relFile);
|
|
1157
|
+
const srcFile = path.join(srcSkillDir, relFile);
|
|
1158
|
+
|
|
1159
|
+
const userExists = fs.existsSync(userFile);
|
|
1160
|
+
const srcExists = fs.existsSync(srcFile);
|
|
1161
|
+
|
|
1162
|
+
if (userExists && srcExists) {
|
|
1163
|
+
// 两边都有,对比内容
|
|
1164
|
+
const userContent = fs.readFileSync(userFile, 'utf8');
|
|
1165
|
+
const srcContent = fs.readFileSync(srcFile, 'utf8');
|
|
1166
|
+
if (userContent !== srcContent) {
|
|
1167
|
+
const diff = generateDiff(srcContent, userContent,
|
|
1168
|
+
`原版 (v${PKG_VERSION})`, '本地修改');
|
|
1169
|
+
if (diff) {
|
|
1170
|
+
const { added, removed } = countDiffLines(diff);
|
|
1171
|
+
changedFiles.push({ relPath: relFile, diff, added, removed, status: 'modified' });
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
} else if (userExists && !srcExists) {
|
|
1175
|
+
// 用户新增的文件
|
|
1176
|
+
const content = fs.readFileSync(userFile, 'utf8');
|
|
1177
|
+
const lineCount = content.split(/\r?\n/).length;
|
|
1178
|
+
changedFiles.push({
|
|
1179
|
+
relPath: relFile,
|
|
1180
|
+
diff: `--- /dev/null\n+++ 本地新增\n@@ -0,0 +1,${lineCount} @@\n` +
|
|
1181
|
+
content.split(/\r?\n/).map(l => '+' + l).join('\n'),
|
|
1182
|
+
added: lineCount, removed: 0, status: 'added'
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
// srcExists && !userExists: 用户删除的文件(不报告,可能是有意删除)
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (changedFiles.length > 0) {
|
|
1189
|
+
// 去重:只保留第一个工具的结果(多工具 skills 内容相同)
|
|
1190
|
+
const existing = allChanges.find(c => c.skillName === name);
|
|
1191
|
+
if (!existing) {
|
|
1192
|
+
allChanges.push({ toolKey, skillName: name, files: changedFiles });
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// 3. 展示结果
|
|
1199
|
+
if (allChanges.length === 0) {
|
|
1200
|
+
console.log(fmt('green', ' ✓ 未检测到技能修改,所有技能与包版本一致。'));
|
|
1201
|
+
console.log('');
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
console.log(` 检测到 ${fmt('bold', String(allChanges.length))} 个技能有修改:`);
|
|
1206
|
+
console.log('');
|
|
1207
|
+
|
|
1208
|
+
for (let idx = 0; idx < allChanges.length; idx++) {
|
|
1209
|
+
const change = allChanges[idx];
|
|
1210
|
+
console.log(` ${fmt('bold', String(idx + 1) + '.')} ${fmt('cyan', change.skillName)}`);
|
|
1211
|
+
for (const file of change.files) {
|
|
1212
|
+
const statusLabel = file.status === 'added' ? fmt('green', '新增文件') : fmt('yellow', '修改文件');
|
|
1213
|
+
console.log(` ${statusLabel}: ${file.relPath} (${fmt('green', '+' + file.added)} 行, ${fmt('red', '-' + file.removed)} 行)`);
|
|
1214
|
+
}
|
|
1215
|
+
console.log('');
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// 4. 展示 diff 详情
|
|
1219
|
+
const allDiffParts = [];
|
|
1220
|
+
|
|
1221
|
+
for (const change of allChanges) {
|
|
1222
|
+
for (const file of change.files) {
|
|
1223
|
+
const header = `${change.skillName}/${file.relPath}`;
|
|
1224
|
+
console.log(fmt('bold', '─'.repeat(Math.min(50, header.length + 10))));
|
|
1225
|
+
console.log(`📋 ${fmt('bold', header)} 的变更:`);
|
|
1226
|
+
console.log(fmt('bold', '─'.repeat(Math.min(50, header.length + 10))));
|
|
1227
|
+
|
|
1228
|
+
// 着色 diff 输出
|
|
1229
|
+
for (const line of file.diff.split('\n')) {
|
|
1230
|
+
if (line.startsWith('+++') || line.startsWith('---')) {
|
|
1231
|
+
console.log(fmt('bold', line));
|
|
1232
|
+
} else if (line.startsWith('+')) {
|
|
1233
|
+
console.log(fmt('green', line));
|
|
1234
|
+
} else if (line.startsWith('-')) {
|
|
1235
|
+
console.log(fmt('red', line));
|
|
1236
|
+
} else if (line.startsWith('@@')) {
|
|
1237
|
+
console.log(fmt('cyan', line));
|
|
1238
|
+
} else {
|
|
1239
|
+
console.log(line);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
console.log('');
|
|
1243
|
+
|
|
1244
|
+
allDiffParts.push(`# ${header}\n${file.diff}`);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const allDiffText = allDiffParts.join('\n\n');
|
|
1249
|
+
|
|
1250
|
+
// 5. 提交 Issue 或提示
|
|
1251
|
+
if (doSubmit) {
|
|
1252
|
+
console.log(fmt('bold', '📤 正在提交 GitHub Issue...'));
|
|
1253
|
+
console.log('');
|
|
1254
|
+
|
|
1255
|
+
if (!isGhAvailable()) {
|
|
1256
|
+
console.log(fmt('yellow', '⚠ 未检测到 gh CLI,无法自动提交 Issue。'));
|
|
1257
|
+
console.log(` 安装方法: ${fmt('bold', 'https://cli.github.com/')}`);
|
|
1258
|
+
console.log('');
|
|
1259
|
+
console.log(fmt('bold', '📋 请手动复制以下内容到 GitHub Issue:'));
|
|
1260
|
+
console.log('');
|
|
1261
|
+
console.log(fmt('cyan', '─'.repeat(50)));
|
|
1262
|
+
console.log(`标题: [sync-back] 技能改进:${allChanges.map(c => c.skillName).join(', ')}`);
|
|
1263
|
+
console.log(fmt('cyan', '─'.repeat(50)));
|
|
1264
|
+
console.log(allDiffText);
|
|
1265
|
+
console.log(fmt('cyan', '─'.repeat(50)));
|
|
1266
|
+
console.log('');
|
|
1267
|
+
console.log(`提交到: ${fmt('bold', 'https://github.com/xu-cell/ai-engineering-init/issues/new')}`);
|
|
1268
|
+
} else {
|
|
1269
|
+
const issueUrl = submitGitHubIssue(allChanges, allDiffText);
|
|
1270
|
+
if (issueUrl) {
|
|
1271
|
+
console.log(fmt('green', fmt('bold', '✅ Issue 已创建!')));
|
|
1272
|
+
console.log(` ${fmt('bold', issueUrl)}`);
|
|
1273
|
+
} else {
|
|
1274
|
+
console.log(fmt('red', '✗ Issue 创建失败,请检查 gh 认证状态(gh auth status)'));
|
|
1275
|
+
console.log('');
|
|
1276
|
+
console.log(fmt('bold', '📋 请手动复制上方 diff 到 GitHub Issue:'));
|
|
1277
|
+
console.log(` ${fmt('bold', 'https://github.com/xu-cell/ai-engineering-init/issues/new')}`);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
} else {
|
|
1281
|
+
console.log(fmt('cyan', '💡 提交方式:'));
|
|
1282
|
+
if (allChanges.length === 1) {
|
|
1283
|
+
console.log(` → 运行 ${fmt('bold', hintCmd(`sync-back --skill ${allChanges[0].skillName} --submit`))}`);
|
|
1284
|
+
} else {
|
|
1285
|
+
console.log(` → 运行 ${fmt('bold', hintCmd('sync-back --submit'))}`);
|
|
1286
|
+
}
|
|
1287
|
+
console.log(` → 或手动复制上方 diff 到 ${fmt('bold', 'https://github.com/xu-cell/ai-engineering-init/issues/new')}`);
|
|
1288
|
+
}
|
|
1289
|
+
console.log('');
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// ── 环境配置初始化(MySQL / Loki)─────────────────────────────────────────
|
|
1293
|
+
|
|
1294
|
+
function runConfig() {
|
|
1295
|
+
// --from 模式:从 MD 文件解析配置(非交互式)
|
|
1296
|
+
if (configFrom) {
|
|
1297
|
+
runConfigFromFile(configFrom, configScope || 'global');
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if (!process.stdin.isTTY) {
|
|
1302
|
+
console.error(fmt('red', '错误:config 命令需要交互式终端(或使用 --from <file> 跳过交互)'));
|
|
1303
|
+
process.exit(1);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1307
|
+
const ask = (question) => new Promise((resolve) => {
|
|
1308
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
(async () => {
|
|
1312
|
+
try {
|
|
1313
|
+
let type = configType;
|
|
1314
|
+
let scope = configScope;
|
|
1315
|
+
|
|
1316
|
+
// 未指定 --type 时显示交互式菜单
|
|
1317
|
+
if (!type) {
|
|
1318
|
+
console.log(fmt('cyan', '请选择要初始化的配置类型:'));
|
|
1319
|
+
console.log('');
|
|
1320
|
+
console.log(` ${fmt('bold', '1')}) ${fmt('green', 'MySQL 数据库连接')} — 配置 mysql-config.json`);
|
|
1321
|
+
console.log(` ${fmt('bold', '2')}) ${fmt('blue', 'Loki 日志查询')} — 配置 Grafana Loki Token`);
|
|
1322
|
+
console.log(` ${fmt('bold', '3')}) ${fmt('yellow','全部配置')} — 依次配置 MySQL + Loki`);
|
|
1323
|
+
console.log('');
|
|
1324
|
+
const answer = await ask(fmt('bold', '请输入选项 [1-3]: '));
|
|
1325
|
+
switch (answer) {
|
|
1326
|
+
case '1': type = 'mysql'; break;
|
|
1327
|
+
case '2': type = 'loki'; break;
|
|
1328
|
+
case '3': type = 'all'; break;
|
|
1329
|
+
default:
|
|
1330
|
+
console.error(fmt('red', '无效选项,退出。'));
|
|
1331
|
+
rl.close();
|
|
1332
|
+
process.exit(1);
|
|
1333
|
+
}
|
|
1334
|
+
console.log('');
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
if (!['mysql', 'loki', 'all'].includes(type)) {
|
|
1338
|
+
console.error(fmt('red', `错误:不支持的配置类型 "${type}",可选:mysql | loki | all`));
|
|
1339
|
+
rl.close();
|
|
1340
|
+
process.exit(1);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// 未指定 --scope 时询问
|
|
1344
|
+
if (!scope) {
|
|
1345
|
+
console.log(fmt('cyan', '请选择配置范围:'));
|
|
1346
|
+
console.log('');
|
|
1347
|
+
console.log(` ${fmt('bold', '1')}) ${fmt('green', 'global(全局)')} — 写入 ~/.claude/,所有项目共享`);
|
|
1348
|
+
console.log(` ${fmt('bold', '2')}) ${fmt('blue', 'local(本地)')} — 写入当前项目目录`);
|
|
1349
|
+
console.log('');
|
|
1350
|
+
const scopeAnswer = await ask(fmt('bold', '请输入选项 [1-2,默认 1]: ')) || '1';
|
|
1351
|
+
scope = scopeAnswer === '2' ? 'local' : 'global';
|
|
1352
|
+
console.log('');
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (!['local', 'global'].includes(scope)) {
|
|
1356
|
+
console.error(fmt('red', `错误:不支持的范围 "${scope}",可选:local | global`));
|
|
1357
|
+
rl.close();
|
|
1358
|
+
process.exit(1);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const isGlobal = scope === 'global';
|
|
1362
|
+
if (isGlobal) {
|
|
1363
|
+
console.log(fmt('magenta', `配置范围:全局(~/.claude/),所有项目共享`));
|
|
1364
|
+
} else {
|
|
1365
|
+
console.log(fmt('magenta', `配置范围:本地(${targetDir})`));
|
|
1366
|
+
}
|
|
1367
|
+
console.log('');
|
|
1368
|
+
|
|
1369
|
+
if (type === 'mysql' || type === 'all') {
|
|
1370
|
+
await runMysqlConfig(ask, isGlobal);
|
|
1371
|
+
}
|
|
1372
|
+
if (type === 'loki' || type === 'all') {
|
|
1373
|
+
if (type === 'all') console.log('');
|
|
1374
|
+
await runLokiConfig(ask, isGlobal);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
console.log('');
|
|
1378
|
+
console.log(fmt('green', fmt('bold', '配置初始化完成!')));
|
|
1379
|
+
if (isGlobal) {
|
|
1380
|
+
console.log(fmt('cyan', '技能会按 全局(~/.claude/) → 本地(.claude/) 顺序查找配置,本地优先。'));
|
|
1381
|
+
}
|
|
1382
|
+
} finally {
|
|
1383
|
+
rl.close();
|
|
1384
|
+
}
|
|
1385
|
+
})();
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// ── MySQL 数据库配置 ────────────────────────────────────────────────────────
|
|
1389
|
+
|
|
1390
|
+
async function runMysqlConfig(ask, isGlobal) {
|
|
1391
|
+
console.log(fmt('blue', fmt('bold', '┌─ MySQL 数据库连接配置 ─┐')));
|
|
1392
|
+
console.log('');
|
|
1393
|
+
|
|
1394
|
+
const configPath = isGlobal
|
|
1395
|
+
? path.join(HOME_DIR, '.claude', 'mysql-config.json')
|
|
1396
|
+
: path.join(targetDir, '.claude', 'mysql-config.json');
|
|
1397
|
+
|
|
1398
|
+
// 读取已有配置
|
|
1399
|
+
let existingConfig = null;
|
|
1400
|
+
if (fs.existsSync(configPath)) {
|
|
1401
|
+
try { existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch { /* ignore */ }
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// --add 模式
|
|
1405
|
+
if (configAdd && existingConfig && existingConfig.environments) {
|
|
1406
|
+
console.log(fmt('cyan', '当前已配置的环境:'));
|
|
1407
|
+
for (const [key, env] of Object.entries(existingConfig.environments)) {
|
|
1408
|
+
const rangeStr = env.range ? fmt('magenta', ` (range: ${env.range})`) : '';
|
|
1409
|
+
console.log(` ${fmt('bold', key)} — ${env._desc || key}${rangeStr} host=${env.host}`);
|
|
1410
|
+
}
|
|
1411
|
+
console.log('');
|
|
1412
|
+
console.log(fmt('green', '将追加新环境到已有配置。'));
|
|
1413
|
+
console.log('');
|
|
1414
|
+
} else if (existingConfig && !configAdd) {
|
|
1415
|
+
console.log(fmt('yellow', `⚠ 配置文件已存在:${configPath}`));
|
|
1416
|
+
const overwrite = await ask(fmt('bold', '输入 add 追加环境,y 重建,N 跳过 [add/y/N]: '));
|
|
1417
|
+
if (overwrite.toLowerCase() === 'add') {
|
|
1418
|
+
// 进入追加模式
|
|
1419
|
+
} else if (overwrite.toLowerCase() !== 'y') {
|
|
1420
|
+
console.log('已跳过 MySQL 配置。');
|
|
1421
|
+
return;
|
|
1422
|
+
} else {
|
|
1423
|
+
existingConfig = null;
|
|
1424
|
+
}
|
|
1425
|
+
console.log('');
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// 自定义环境名输入
|
|
1429
|
+
console.log(fmt('cyan', '请输入要配置的环境(自定义名称,多个用逗号分隔):'));
|
|
1430
|
+
console.log(fmt('yellow', ' 示例:local, dev, test, prod 或自定义名称'));
|
|
1431
|
+
console.log('');
|
|
1432
|
+
const envAnswer = await ask(fmt('bold', '环境名称: '));
|
|
1433
|
+
const envNames = envAnswer.split(',').map(s => s.trim()).filter(Boolean);
|
|
1434
|
+
|
|
1435
|
+
if (envNames.length === 0) {
|
|
1436
|
+
console.error(fmt('red', '未输入任何环境名,跳过 MySQL 配置。'));
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
console.log('');
|
|
1440
|
+
|
|
1441
|
+
// 收集每个环境的配置
|
|
1442
|
+
const newEnvironments = {};
|
|
1443
|
+
for (const env of envNames) {
|
|
1444
|
+
if (existingConfig && existingConfig.environments && existingConfig.environments[env]) {
|
|
1445
|
+
console.log(fmt('yellow', ` ${env} 已存在,跳过。使用 y 模式可重建。`));
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const isLocal = env === 'local';
|
|
1450
|
+
console.log(fmt('cyan', `── ${env} 环境配置 ──`));
|
|
1451
|
+
|
|
1452
|
+
const host = await ask(` host [${isLocal ? '127.0.0.1' : '无默认'}]: `) || (isLocal ? '127.0.0.1' : '');
|
|
1453
|
+
if (!host) { console.error(fmt('red', ` host 不能为空,跳过。`)); continue; }
|
|
1454
|
+
const port = await ask(' port [3306]: ') || '3306';
|
|
1455
|
+
const user = await ask(` user [${isLocal ? 'root' : '无默认'}]: `) || (isLocal ? 'root' : '');
|
|
1456
|
+
if (!user) { console.error(fmt('red', ` user 不能为空,跳过。`)); continue; }
|
|
1457
|
+
const password = await ask(' password: ');
|
|
1458
|
+
const desc = await ask(` 描述 [${env}环境]: `) || `${env}环境`;
|
|
1459
|
+
const rangeInput = await ask(` 覆盖范围(如 ${fmt('bold', '1~15')} 表示 ${env}1→${env}15,留空=无范围): `);
|
|
1460
|
+
console.log('');
|
|
1461
|
+
|
|
1462
|
+
newEnvironments[env] = { host, port: parseInt(port, 10), user, password, _desc: desc };
|
|
1463
|
+
if (rangeInput) newEnvironments[env].range = rangeInput;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (Object.keys(newEnvironments).length === 0 && !existingConfig) {
|
|
1467
|
+
console.error(fmt('red', '未成功配置任何环境。'));
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// 合并配置
|
|
1472
|
+
const allEnvironments = {
|
|
1473
|
+
...(existingConfig && existingConfig.environments ? existingConfig.environments : {}),
|
|
1474
|
+
...newEnvironments,
|
|
1475
|
+
};
|
|
1476
|
+
|
|
1477
|
+
// 选择默认环境
|
|
1478
|
+
const allEnvKeys = Object.keys(allEnvironments);
|
|
1479
|
+
let defaultEnv = (existingConfig && existingConfig.default) || allEnvKeys[0];
|
|
1480
|
+
if (Object.keys(newEnvironments).length > 0 && allEnvKeys.length > 1) {
|
|
1481
|
+
console.log(fmt('cyan', '请选择默认环境:'));
|
|
1482
|
+
allEnvKeys.forEach((env, i) => {
|
|
1483
|
+
const marker = env === defaultEnv ? fmt('green', ' (当前)') : '';
|
|
1484
|
+
console.log(` ${fmt('bold', String(i + 1))}) ${env}${marker}`);
|
|
1485
|
+
});
|
|
1486
|
+
const defaultAnswer = await ask(fmt('bold', `请输入选项 [1-${allEnvKeys.length},回车保持当前]: `));
|
|
1487
|
+
if (defaultAnswer) {
|
|
1488
|
+
const idx = parseInt(defaultAnswer, 10) - 1;
|
|
1489
|
+
if (idx >= 0 && idx < allEnvKeys.length) defaultEnv = allEnvKeys[idx];
|
|
1490
|
+
}
|
|
1491
|
+
console.log('');
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
const config = {
|
|
1495
|
+
environments: allEnvironments,
|
|
1496
|
+
default: defaultEnv,
|
|
1497
|
+
_comment: '环境支持 range 字段(如 "1~15"),用户说"dev10"时自动匹配。查找顺序:本地 .claude/ > 全局 ~/.claude/',
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
const configJson = JSON.stringify(config, null, 2) + '\n';
|
|
1501
|
+
// 写入主路径
|
|
1502
|
+
const dir = path.dirname(configPath);
|
|
1503
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1504
|
+
fs.writeFileSync(configPath, configJson, 'utf-8');
|
|
1505
|
+
console.log(` ${fmt('green', '✔')} 已写入:${configPath}`);
|
|
1506
|
+
|
|
1507
|
+
// 全局模式:同时写入 ~/.cursor/(如果目录存在)
|
|
1508
|
+
if (isGlobal) {
|
|
1509
|
+
const cursorConfigPath = path.join(HOME_DIR, '.cursor', 'mysql-config.json');
|
|
1510
|
+
if (fs.existsSync(path.join(HOME_DIR, '.cursor'))) {
|
|
1511
|
+
fs.writeFileSync(cursorConfigPath, configJson, 'utf-8');
|
|
1512
|
+
console.log(` ${fmt('green', '✔')} 已同步:${cursorConfigPath}`);
|
|
1513
|
+
}
|
|
1514
|
+
} else {
|
|
1515
|
+
ensureGitignore(['mysql-config.json']);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
console.log('');
|
|
1519
|
+
console.log(fmt('green', 'MySQL 数据库配置完成!'));
|
|
1520
|
+
for (const [key, env] of Object.entries(newEnvironments)) {
|
|
1521
|
+
if (env.range) {
|
|
1522
|
+
console.log(fmt('cyan', ` ${key} 覆盖 ${key}${env.range.replace('~', '→')},说"${key}10"将自动匹配`));
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// ── Loki 日志查询配置 ──────────────────────────────────────────────────────
|
|
1528
|
+
|
|
1529
|
+
async function runLokiConfig(ask, isGlobal) {
|
|
1530
|
+
console.log(fmt('blue', fmt('bold', '┌─ Loki 日志查询配置 ─┐')));
|
|
1531
|
+
console.log('');
|
|
1532
|
+
|
|
1533
|
+
const configPath = isGlobal
|
|
1534
|
+
? path.join(HOME_DIR, '.claude', 'loki-config.json')
|
|
1535
|
+
: getLokiConfigPath();
|
|
1536
|
+
|
|
1537
|
+
if (!configPath) {
|
|
1538
|
+
console.log(fmt('yellow', '⚠ 未检测到配置目录。请先运行 init 安装框架。'));
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
let existingConfig = null;
|
|
1543
|
+
if (fs.existsSync(configPath)) {
|
|
1544
|
+
try { existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch { /* ignore */ }
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
if (existingConfig && existingConfig.environments) {
|
|
1548
|
+
const envs = existingConfig.environments;
|
|
1549
|
+
const envList = Object.keys(envs);
|
|
1550
|
+
console.log(fmt('cyan', '当前已配置的 Loki 环境:'));
|
|
1551
|
+
console.log('');
|
|
1552
|
+
for (const key of envList) {
|
|
1553
|
+
const env = envs[key];
|
|
1554
|
+
const hasToken = env.token && env.token.length > 0;
|
|
1555
|
+
const status = hasToken ? fmt('green', '✔ Token') : fmt('red', '✗ 缺Token');
|
|
1556
|
+
const rangeStr = env.range ? fmt('magenta', ` (range: ${env.range})`) : '';
|
|
1557
|
+
console.log(` ${fmt('bold', key)} — ${env.name || key} ${status}${rangeStr}`);
|
|
1558
|
+
}
|
|
1559
|
+
console.log('');
|
|
1560
|
+
|
|
1561
|
+
if (configAdd) {
|
|
1562
|
+
console.log(fmt('green', '将追加新环境到已有配置。'));
|
|
1563
|
+
} else {
|
|
1564
|
+
const missingTokenEnvs = envList.filter(k => !envs[k].token);
|
|
1565
|
+
const action = await ask(fmt('bold', '输入 token 补充Token,add 追加环境,N 跳过 [token/add/N]: ')) || 'token';
|
|
1566
|
+
if (action.toLowerCase() === 'token') {
|
|
1567
|
+
await updateLokiTokens(ask, existingConfig, configPath, isGlobal);
|
|
1568
|
+
return;
|
|
1569
|
+
} else if (action.toLowerCase() !== 'add') {
|
|
1570
|
+
console.log('已跳过 Loki 配置。');
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
console.log('');
|
|
1575
|
+
|
|
1576
|
+
// 追加环境
|
|
1577
|
+
await addLokiEnvironments(ask, existingConfig, configPath, isGlobal);
|
|
1578
|
+
} else {
|
|
1579
|
+
await createLokiConfig(ask, configPath, isGlobal);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
async function updateLokiTokens(ask, config, configPath, isGlobal) {
|
|
1584
|
+
console.log('');
|
|
1585
|
+
console.log(fmt('cyan', fmt('bold', 'Grafana Token 获取:')));
|
|
1586
|
+
console.log(` Grafana → Administration → Service accounts → Add(Viewer)→ Add token`);
|
|
1587
|
+
console.log('');
|
|
1588
|
+
|
|
1589
|
+
const envs = config.environments;
|
|
1590
|
+
for (const key of Object.keys(envs)) {
|
|
1591
|
+
const env = envs[key];
|
|
1592
|
+
const hasToken = env.token && env.token.length > 0;
|
|
1593
|
+
if (hasToken) {
|
|
1594
|
+
const update = await ask(` ${fmt('bold', key)} 已有 Token,更新?[y/N]: `);
|
|
1595
|
+
if (update.toLowerCase() !== 'y') continue;
|
|
1596
|
+
}
|
|
1597
|
+
if (env.url) console.log(` URL: ${fmt('bold', env.url)}`);
|
|
1598
|
+
const token = await ask(` 输入 ${key} 的 Token: `);
|
|
1599
|
+
if (token) {
|
|
1600
|
+
envs[key].token = token;
|
|
1601
|
+
console.log(` ${fmt('green', '✔')} 已设置`);
|
|
1602
|
+
}
|
|
1603
|
+
console.log('');
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
writeLokiConfig(config, configPath, isGlobal);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
async function addLokiEnvironments(ask, config, configPath, isGlobal) {
|
|
1610
|
+
printLokiTokenGuide();
|
|
1611
|
+
const countAnswer = await ask(fmt('bold', '要追加几个环境?[1]: ')) || '1';
|
|
1612
|
+
const count = Math.max(1, Math.min(10, parseInt(countAnswer, 10) || 1));
|
|
1613
|
+
console.log('');
|
|
1614
|
+
|
|
1615
|
+
for (let i = 0; i < count; i++) {
|
|
1616
|
+
const envData = await collectLokiEnvInput(ask, i + 1, count);
|
|
1617
|
+
if (!envData) continue;
|
|
1618
|
+
config.environments[envData.key] = envData.value;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
writeLokiConfig(config, configPath, isGlobal);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
async function createLokiConfig(ask, configPath, isGlobal) {
|
|
1625
|
+
console.log(fmt('cyan', '将创建新的 Loki 日志查询配置。'));
|
|
1626
|
+
console.log('');
|
|
1627
|
+
printLokiTokenGuide();
|
|
1628
|
+
const countAnswer = await ask(fmt('bold', '要配置几个 Grafana 环境?[1]: ')) || '1';
|
|
1629
|
+
const count = Math.max(1, Math.min(10, parseInt(countAnswer, 10) || 1));
|
|
1630
|
+
console.log('');
|
|
1631
|
+
|
|
1632
|
+
const environments = {};
|
|
1633
|
+
let activeEnv = '';
|
|
1634
|
+
|
|
1635
|
+
for (let i = 0; i < count; i++) {
|
|
1636
|
+
const envData = await collectLokiEnvInput(ask, i + 1, count);
|
|
1637
|
+
if (!envData) continue;
|
|
1638
|
+
environments[envData.key] = envData.value;
|
|
1639
|
+
if (!activeEnv) activeEnv = envData.key;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
if (Object.keys(environments).length === 0) {
|
|
1643
|
+
console.error(fmt('red', '未配置任何环境。'));
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
const envKeys = Object.keys(environments);
|
|
1648
|
+
if (envKeys.length > 1) {
|
|
1649
|
+
console.log(fmt('cyan', '请选择默认活跃环境:'));
|
|
1650
|
+
envKeys.forEach((env, i) => console.log(` ${fmt('bold', String(i + 1))}) ${env}`));
|
|
1651
|
+
const activeAnswer = await ask(fmt('bold', `请输入选项 [1-${envKeys.length}]: `));
|
|
1652
|
+
const idx = parseInt(activeAnswer, 10) - 1;
|
|
1653
|
+
if (idx >= 0 && idx < envKeys.length) activeEnv = envKeys[idx];
|
|
1654
|
+
console.log('');
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
const config = {
|
|
1658
|
+
_comment: 'Loki 多环境配置。环境支持 range 字段。查找顺序:本地 > 全局 ~/.claude/',
|
|
1659
|
+
_setup: 'Token:Grafana → Administration → Service accounts → Add(Viewer)→ Add token',
|
|
1660
|
+
active: activeEnv,
|
|
1661
|
+
environments,
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
writeLokiConfig(config, configPath, isGlobal);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function printLokiTokenGuide() {
|
|
1668
|
+
console.log(fmt('cyan', fmt('bold', 'Grafana Token 获取:')));
|
|
1669
|
+
console.log(` Grafana → Administration → Service accounts → Add(Viewer)→ Add token`);
|
|
1670
|
+
console.log('');
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
async function collectLokiEnvInput(ask, index, total) {
|
|
1674
|
+
console.log(fmt('cyan', `── 环境 ${index}/${total} ──`));
|
|
1675
|
+
const envKey = await ask(` 环境标识(如 monitor-dev、test13): `);
|
|
1676
|
+
if (!envKey) { console.log(fmt('yellow', ' 跳过')); return null; }
|
|
1677
|
+
const name = await ask(` 环境名称(如 "开发环境"): `) || envKey;
|
|
1678
|
+
const url = await ask(` Grafana URL: `);
|
|
1679
|
+
const token = await ask(` Token(可留空稍后配): `);
|
|
1680
|
+
const aliasStr = await ask(` 别名(逗号分隔)[${envKey}]: `) || envKey;
|
|
1681
|
+
const aliases = aliasStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
1682
|
+
const rangeInput = await ask(` 项目覆盖范围(如 ${fmt('bold', 'dev1~15')} 表示 dev01→dev15,留空=无范围): `);
|
|
1683
|
+
console.log('');
|
|
1684
|
+
|
|
1685
|
+
const value = { name, url, token: token || '', aliases };
|
|
1686
|
+
if (rangeInput) {
|
|
1687
|
+
value.range = rangeInput;
|
|
1688
|
+
value.projects = expandRange(rangeInput);
|
|
1689
|
+
if (value.projects.length > 0) {
|
|
1690
|
+
console.log(fmt('cyan', ` 已展开 ${value.projects.length} 个项目:${value.projects.slice(0, 5).join(', ')}${value.projects.length > 5 ? '...' : ''}`));
|
|
1691
|
+
console.log('');
|
|
1692
|
+
}
|
|
1693
|
+
} else {
|
|
1694
|
+
value.projects = [];
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
return { key: envKey, value };
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function writeLokiConfig(config, configPath, isGlobal) {
|
|
1701
|
+
const configJson = JSON.stringify(config, null, 2) + '\n';
|
|
1702
|
+
const dir = path.dirname(configPath);
|
|
1703
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1704
|
+
fs.writeFileSync(configPath, configJson, 'utf-8');
|
|
1705
|
+
console.log(` ${fmt('green', '✔')} 已写入:${configPath}`);
|
|
1706
|
+
|
|
1707
|
+
// 全局模式:同时写入 ~/.cursor/(如果目录存在)
|
|
1708
|
+
if (isGlobal) {
|
|
1709
|
+
const cursorConfigPath = path.join(HOME_DIR, '.cursor', 'loki-config.json');
|
|
1710
|
+
if (fs.existsSync(path.join(HOME_DIR, '.cursor'))) {
|
|
1711
|
+
fs.writeFileSync(cursorConfigPath, configJson, 'utf-8');
|
|
1712
|
+
console.log(` ${fmt('green', '✔')} 已同步:${cursorConfigPath}`);
|
|
1713
|
+
}
|
|
1714
|
+
} else {
|
|
1715
|
+
ensureGitignore(['loki-config.json', 'environments.json']);
|
|
1716
|
+
}
|
|
1717
|
+
console.log('');
|
|
1718
|
+
console.log(fmt('green', 'Loki 日志查询配置完成!'));
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// ── 从 Markdown 文件解析配置(非交互式)──────────────────────────────────────
|
|
1722
|
+
|
|
1723
|
+
function runConfigFromFile(filePath, scope) {
|
|
1724
|
+
if (!fs.existsSync(filePath)) {
|
|
1725
|
+
console.error(fmt('red', `错误:文件不存在 "${filePath}"`));
|
|
1726
|
+
process.exit(1);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1730
|
+
const isGlobal = scope === 'global';
|
|
1731
|
+
|
|
1732
|
+
console.log(fmt('blue', fmt('bold', `从 Markdown 文件解析配置:${filePath}`)));
|
|
1733
|
+
console.log(fmt('magenta', `配置范围:${isGlobal ? '全局(~/.claude/)' : '本地(当前项目)'}`));
|
|
1734
|
+
console.log('');
|
|
1735
|
+
|
|
1736
|
+
// 解析 MySQL 表格
|
|
1737
|
+
const mysqlConfig = parseMysqlFromMd(content);
|
|
1738
|
+
if (mysqlConfig) {
|
|
1739
|
+
const mysqlPath = isGlobal
|
|
1740
|
+
? path.join(HOME_DIR, '.claude', 'mysql-config.json')
|
|
1741
|
+
: path.join(targetDir, '.claude', 'mysql-config.json');
|
|
1742
|
+
|
|
1743
|
+
const configJson = JSON.stringify(mysqlConfig, null, 2) + '\n';
|
|
1744
|
+
const dir = path.dirname(mysqlPath);
|
|
1745
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1746
|
+
fs.writeFileSync(mysqlPath, configJson, 'utf-8');
|
|
1747
|
+
console.log(` ${fmt('green', '✔')} MySQL 配置(${Object.keys(mysqlConfig.environments).length} 个环境)→ ${mysqlPath}`);
|
|
1748
|
+
|
|
1749
|
+
// 全局同步到 cursor
|
|
1750
|
+
if (isGlobal && fs.existsSync(path.join(HOME_DIR, '.cursor'))) {
|
|
1751
|
+
const cursorPath = path.join(HOME_DIR, '.cursor', 'mysql-config.json');
|
|
1752
|
+
fs.writeFileSync(cursorPath, configJson, 'utf-8');
|
|
1753
|
+
console.log(` ${fmt('green', '✔')} 已同步 → ${cursorPath}`);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// 解析 Loki 表格
|
|
1758
|
+
const lokiConfig = parseLokiFromMd(content);
|
|
1759
|
+
if (lokiConfig) {
|
|
1760
|
+
const lokiPath = isGlobal
|
|
1761
|
+
? path.join(HOME_DIR, '.claude', 'loki-config.json')
|
|
1762
|
+
: path.join(targetDir, '.claude', 'loki-config.json');
|
|
1763
|
+
|
|
1764
|
+
const configJson = JSON.stringify(lokiConfig, null, 2) + '\n';
|
|
1765
|
+
const dir = path.dirname(lokiPath);
|
|
1766
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1767
|
+
fs.writeFileSync(lokiPath, configJson, 'utf-8');
|
|
1768
|
+
console.log(` ${fmt('green', '✔')} Loki 配置(${Object.keys(lokiConfig.environments).length} 个环境)→ ${lokiPath}`);
|
|
1769
|
+
|
|
1770
|
+
if (isGlobal && fs.existsSync(path.join(HOME_DIR, '.cursor'))) {
|
|
1771
|
+
const cursorPath = path.join(HOME_DIR, '.cursor', 'loki-config.json');
|
|
1772
|
+
fs.writeFileSync(cursorPath, configJson, 'utf-8');
|
|
1773
|
+
console.log(` ${fmt('green', '✔')} 已同步 → ${cursorPath}`);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
if (!mysqlConfig && !lokiConfig) {
|
|
1778
|
+
console.error(fmt('red', '未在文件中找到 MySQL 或 Loki 配置表格。'));
|
|
1779
|
+
console.log('');
|
|
1780
|
+
console.log('期望格式(MySQL):');
|
|
1781
|
+
console.log(' | 环境 | host | port | user | password | range | 描述 |');
|
|
1782
|
+
console.log('');
|
|
1783
|
+
console.log('期望格式(Loki):');
|
|
1784
|
+
console.log(' | 环境 | 名称 | URL | Token | 别名 | range |');
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
if (!isGlobal) ensureGitignore(['mysql-config.json', 'loki-config.json']);
|
|
1789
|
+
|
|
1790
|
+
console.log('');
|
|
1791
|
+
console.log(fmt('green', fmt('bold', '配置初始化完成!')));
|
|
1792
|
+
if (isGlobal) {
|
|
1793
|
+
console.log(fmt('cyan', '技能按 本地(.claude/) → 全局(~/.claude/) 顺序查找,本地优先。'));
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/** 从 Markdown 内容中解析 MySQL 配置表格 */
|
|
1798
|
+
function parseMysqlFromMd(content) {
|
|
1799
|
+
// 找到 "MySQL" 标题后的表格
|
|
1800
|
+
const mysqlSection = extractSection(content, /mysql|数据库/i);
|
|
1801
|
+
if (!mysqlSection) return null;
|
|
1802
|
+
|
|
1803
|
+
const rows = parseTable(mysqlSection);
|
|
1804
|
+
if (rows.length === 0) return null;
|
|
1805
|
+
|
|
1806
|
+
const environments = {};
|
|
1807
|
+
for (const row of rows) {
|
|
1808
|
+
const env = row['环境'] || row['env'] || '';
|
|
1809
|
+
if (!env) continue;
|
|
1810
|
+
|
|
1811
|
+
const host = row['host'] || '';
|
|
1812
|
+
const port = parseInt(row['port'] || '3306', 10);
|
|
1813
|
+
const user = row['user'] || '';
|
|
1814
|
+
const password = row['password'] || '';
|
|
1815
|
+
const range = row['range'] || '';
|
|
1816
|
+
const desc = row['描述'] || row['desc'] || `${env}环境`;
|
|
1817
|
+
|
|
1818
|
+
if (!host || host.startsWith('YOUR_')) continue; // 跳过未填写的占位符
|
|
1819
|
+
|
|
1820
|
+
environments[env] = { host, port, user, password, _desc: desc };
|
|
1821
|
+
if (range) environments[env].range = range;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
if (Object.keys(environments).length === 0) return null;
|
|
1825
|
+
|
|
1826
|
+
// 提取默认环境
|
|
1827
|
+
const defaultMatch = mysqlSection.match(/默认环境[::]\s*(\S+)/);
|
|
1828
|
+
const defaultEnv = defaultMatch ? defaultMatch[1] : Object.keys(environments)[0];
|
|
1829
|
+
|
|
1830
|
+
return {
|
|
1831
|
+
environments,
|
|
1832
|
+
default: defaultEnv,
|
|
1833
|
+
_comment: '从 Markdown 文件解析生成。支持 range 字段,查找顺序:本地 > 全局 ~/.claude/',
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
/** 从 Markdown 内容中解析 Loki 配置表格 */
|
|
1838
|
+
function parseLokiFromMd(content) {
|
|
1839
|
+
const lokiSection = extractSection(content, /loki|日志/i);
|
|
1840
|
+
if (!lokiSection) return null;
|
|
1841
|
+
|
|
1842
|
+
const rows = parseTable(lokiSection);
|
|
1843
|
+
if (rows.length === 0) return null;
|
|
1844
|
+
|
|
1845
|
+
const environments = {};
|
|
1846
|
+
for (const row of rows) {
|
|
1847
|
+
const env = row['环境'] || row['env'] || '';
|
|
1848
|
+
if (!env) continue;
|
|
1849
|
+
|
|
1850
|
+
const name = row['名称'] || row['name'] || env;
|
|
1851
|
+
const url = row['url'] || row['URL'] || '';
|
|
1852
|
+
const token = row['token'] || row['Token'] || '';
|
|
1853
|
+
const aliasStr = row['别名'] || row['aliases'] || env;
|
|
1854
|
+
const aliases = aliasStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
1855
|
+
const rangeStr = row['range'] || '';
|
|
1856
|
+
|
|
1857
|
+
if (!url || url.startsWith('YOUR_')) continue;
|
|
1858
|
+
|
|
1859
|
+
const envData = { name, url, token: token.startsWith('YOUR_') ? '' : token, aliases };
|
|
1860
|
+
if (rangeStr) {
|
|
1861
|
+
envData.range = rangeStr;
|
|
1862
|
+
envData.projects = expandRange(rangeStr);
|
|
1863
|
+
} else {
|
|
1864
|
+
envData.projects = [];
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
environments[env] = envData;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
if (Object.keys(environments).length === 0) return null;
|
|
1871
|
+
|
|
1872
|
+
const defaultMatch = lokiSection.match(/默认环境[::]\s*(\S+)/);
|
|
1873
|
+
const activeEnv = defaultMatch ? defaultMatch[1] : Object.keys(environments)[0];
|
|
1874
|
+
|
|
1875
|
+
return {
|
|
1876
|
+
_comment: '从 Markdown 文件解析生成。支持 range 字段,查找顺序:本地 > 全局 ~/.claude/',
|
|
1877
|
+
_setup: 'Token:Grafana → Administration → Service accounts → Add(Viewer)→ Add token',
|
|
1878
|
+
active: activeEnv,
|
|
1879
|
+
environments,
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
/** 从 Markdown 中按标题提取段落 */
|
|
1884
|
+
function extractSection(content, titlePattern) {
|
|
1885
|
+
const lines = content.split('\n');
|
|
1886
|
+
let start = -1;
|
|
1887
|
+
let end = lines.length;
|
|
1888
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1889
|
+
const line = lines[i];
|
|
1890
|
+
if (line.match(/^#+\s/) && titlePattern.test(line)) {
|
|
1891
|
+
start = i;
|
|
1892
|
+
const level = line.match(/^(#+)/)[1].length;
|
|
1893
|
+
// 找到同级或更高级标题作为结束
|
|
1894
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1895
|
+
const nextMatch = lines[j].match(/^(#+)\s/);
|
|
1896
|
+
if (nextMatch && nextMatch[1].length <= level) {
|
|
1897
|
+
end = j;
|
|
1898
|
+
break;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
break;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
if (start === -1) return null;
|
|
1905
|
+
return lines.slice(start, end).join('\n');
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
/** 解析 Markdown 表格为对象数组 */
|
|
1909
|
+
function parseTable(text) {
|
|
1910
|
+
const lines = text.split('\n');
|
|
1911
|
+
let headerLine = -1;
|
|
1912
|
+
|
|
1913
|
+
// 找到表头行(含 | 的行,下一行是分隔线 |---|)
|
|
1914
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
1915
|
+
if (lines[i].includes('|') && lines[i + 1] && lines[i + 1].match(/^\|[\s-:|]+\|$/)) {
|
|
1916
|
+
headerLine = i;
|
|
1917
|
+
break;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
if (headerLine === -1) return [];
|
|
1921
|
+
|
|
1922
|
+
const parseRow = (line) => line.split('|').map(s => s.trim()).filter(Boolean);
|
|
1923
|
+
const headers = parseRow(lines[headerLine]);
|
|
1924
|
+
const rows = [];
|
|
1925
|
+
|
|
1926
|
+
for (let i = headerLine + 2; i < lines.length; i++) {
|
|
1927
|
+
const line = lines[i];
|
|
1928
|
+
if (!line.includes('|')) break;
|
|
1929
|
+
const cells = parseRow(line);
|
|
1930
|
+
if (cells.length === 0) break;
|
|
1931
|
+
const row = {};
|
|
1932
|
+
headers.forEach((h, idx) => { row[h] = cells[idx] || ''; });
|
|
1933
|
+
rows.push(row);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
return rows;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// ── Config 工具函数 ─────────────────────────────────────────────────────────
|
|
1940
|
+
|
|
1941
|
+
function getLokiConfigPath() {
|
|
1942
|
+
const lokiJsonClaude = path.join(targetDir, '.claude', 'loki-config.json');
|
|
1943
|
+
if (fs.existsSync(lokiJsonClaude)) return lokiJsonClaude;
|
|
1944
|
+
const envJsonClaude = path.join(targetDir, '.claude', 'skills', 'loki-log-query', 'environments.json');
|
|
1945
|
+
if (fs.existsSync(envJsonClaude)) return envJsonClaude;
|
|
1946
|
+
const envJsonCursor = path.join(targetDir, '.cursor', 'skills', 'loki-log-query', 'environments.json');
|
|
1947
|
+
if (fs.existsSync(envJsonCursor)) return envJsonCursor;
|
|
1948
|
+
if (fs.existsSync(path.join(targetDir, '.claude'))) return lokiJsonClaude;
|
|
1949
|
+
if (fs.existsSync(path.join(targetDir, '.cursor'))) return path.join(targetDir, '.cursor', 'loki-config.json');
|
|
1950
|
+
return null;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
/** 展开范围字符串为项目名列表,如 "dev1~15" → ["dev01","dev02",...,"dev15"] */
|
|
1954
|
+
function expandRange(rangeStr) {
|
|
1955
|
+
const match = rangeStr.match(/^([a-zA-Z-]*)(\d+)\s*[~~]\s*(\d+)$/);
|
|
1956
|
+
if (!match) return [];
|
|
1957
|
+
const prefix = match[1];
|
|
1958
|
+
const start = parseInt(match[2], 10);
|
|
1959
|
+
const end = parseInt(match[3], 10);
|
|
1960
|
+
const maxDigits = Math.max(String(start).length, String(end).length, 2);
|
|
1961
|
+
const result = [];
|
|
1962
|
+
for (let i = start; i <= end; i++) {
|
|
1963
|
+
result.push(prefix + String(i).padStart(maxDigits, '0'));
|
|
1964
|
+
}
|
|
1965
|
+
return result;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function parseSelection(answer, names) {
|
|
1969
|
+
const selected = new Set();
|
|
1970
|
+
for (const part of answer.split(',')) {
|
|
1971
|
+
const trimmed = part.trim();
|
|
1972
|
+
const rangeMatch = trimmed.match(/^(\d)-(\d)$/);
|
|
1973
|
+
if (rangeMatch) {
|
|
1974
|
+
const start = parseInt(rangeMatch[1], 10);
|
|
1975
|
+
const end = parseInt(rangeMatch[2], 10);
|
|
1976
|
+
for (let n = start; n <= end; n++) {
|
|
1977
|
+
if (n >= 1 && n <= names.length) selected.add(names[n - 1]);
|
|
1978
|
+
}
|
|
1979
|
+
} else {
|
|
1980
|
+
const n = parseInt(trimmed, 10);
|
|
1981
|
+
if (n >= 1 && n <= names.length) selected.add(names[n - 1]);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
return [...selected];
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
function ensureGitignore(patterns) {
|
|
1988
|
+
const gitignorePath = path.join(targetDir, '.gitignore');
|
|
1989
|
+
let content = '';
|
|
1990
|
+
if (fs.existsSync(gitignorePath)) {
|
|
1991
|
+
content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
1992
|
+
}
|
|
1993
|
+
const lines = content.split('\n').map(l => l.trim());
|
|
1994
|
+
const toAdd = [];
|
|
1995
|
+
for (const pattern of patterns) {
|
|
1996
|
+
const alreadyIgnored = lines.some(line =>
|
|
1997
|
+
line.endsWith(pattern) || line.endsWith(`/${pattern}`) || line === `**/${pattern}`
|
|
1998
|
+
);
|
|
1999
|
+
if (!alreadyIgnored) {
|
|
2000
|
+
toAdd.push(`# 敏感配置 - ${pattern}`);
|
|
2001
|
+
toAdd.push(`**/${pattern}`);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (toAdd.length > 0) {
|
|
2005
|
+
const separator = content.endsWith('\n') || content === '' ? '' : '\n';
|
|
2006
|
+
fs.appendFileSync(gitignorePath, `${separator}${toAdd.join('\n')}\n`, 'utf-8');
|
|
2007
|
+
console.log(` ${fmt('green', '✔')} 已更新 .gitignore`);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// ── MCP 服务器管理 ──────────────────────────────────────────────────────────
|
|
2012
|
+
|
|
2013
|
+
const MCP_REGISTRY = [
|
|
2014
|
+
{
|
|
2015
|
+
name: 'sequential-thinking',
|
|
2016
|
+
package: '@modelcontextprotocol/server-sequential-thinking',
|
|
2017
|
+
description: '链式推理 — 深度分析、仔细思考、全面评估时使用',
|
|
2018
|
+
env: {},
|
|
2019
|
+
recommended: true,
|
|
2020
|
+
},
|
|
2021
|
+
{
|
|
2022
|
+
name: 'context7',
|
|
2023
|
+
package: '@upstash/context7-mcp',
|
|
2024
|
+
description: '官方文档查询 — 最佳实践、官方文档、标准写法时使用',
|
|
2025
|
+
env: {},
|
|
2026
|
+
recommended: true,
|
|
2027
|
+
},
|
|
2028
|
+
{
|
|
2029
|
+
name: 'github',
|
|
2030
|
+
package: '@modelcontextprotocol/server-github',
|
|
2031
|
+
description: 'GitHub 集成 — 查询 Issues、PR、仓库信息',
|
|
2032
|
+
env: { GITHUB_PERSONAL_ACCESS_TOKEN: '${GITHUB_TOKEN}' },
|
|
2033
|
+
recommended: true,
|
|
2034
|
+
},
|
|
2035
|
+
{
|
|
2036
|
+
name: 'filesystem',
|
|
2037
|
+
package: '@modelcontextprotocol/server-filesystem',
|
|
2038
|
+
description: '文件系统访问 — 读写项目外的文件',
|
|
2039
|
+
env: {},
|
|
2040
|
+
recommended: false,
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
name: 'fetch',
|
|
2044
|
+
package: '@anthropic-ai/mcp-fetch',
|
|
2045
|
+
description: '网页抓取 — 获取网页内容',
|
|
2046
|
+
env: {},
|
|
2047
|
+
recommended: false,
|
|
2048
|
+
},
|
|
2049
|
+
{
|
|
2050
|
+
name: 'yunxiao',
|
|
2051
|
+
package: 'alibabacloud-devops-mcp-server',
|
|
2052
|
+
description: '阿里云效 — DevOps 项目管理、代码仓库、流水线集成',
|
|
2053
|
+
env: { YUNXIAO_ACCESS_TOKEN: '<YOUR_TOKEN>' },
|
|
2054
|
+
recommended: false,
|
|
2055
|
+
},
|
|
2056
|
+
{
|
|
2057
|
+
name: 'yuque',
|
|
2058
|
+
package: 'yuque-mcp',
|
|
2059
|
+
description: '语雀 — 知识库文档读写、搜索、团队协作',
|
|
2060
|
+
env: { YUQUE_TOKEN: '<YOUR_TOKEN>' },
|
|
2061
|
+
recommended: false,
|
|
2062
|
+
},
|
|
2063
|
+
{
|
|
2064
|
+
name: 'codex',
|
|
2065
|
+
command: 'uvx',
|
|
2066
|
+
args: ['--from', 'git+https://github.com/GuDaStudio/codexmcp.git', 'codexmcp'],
|
|
2067
|
+
description: 'Codex 协作 — 代码审查、算法分析、生成补丁(需安装 uv)',
|
|
2068
|
+
env: {},
|
|
2069
|
+
recommended: false,
|
|
2070
|
+
},
|
|
2071
|
+
];
|
|
2072
|
+
|
|
2073
|
+
/** MCP 配置文件路径映射 */
|
|
2074
|
+
const MCP_CONFIG_PATHS = {
|
|
2075
|
+
claude: { file: '.claude/settings.json', key: 'mcpServers' },
|
|
2076
|
+
cursor: { file: '.cursor/mcp.json', key: 'mcpServers' },
|
|
2077
|
+
};
|
|
2078
|
+
|
|
2079
|
+
/** 解析 MCP 配置文件绝对路径 */
|
|
2080
|
+
function resolveMcpConfigPath(toolName, scope = 'project') {
|
|
2081
|
+
const config = MCP_CONFIG_PATHS[toolName];
|
|
2082
|
+
if (!config) return '';
|
|
2083
|
+
const baseDir = scope === 'global' ? HOME_DIR : targetDir;
|
|
2084
|
+
return path.join(baseDir, config.file);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
/** 检测指定作用域中已有的工具配置目录 */
|
|
2088
|
+
function detectMcpTools(scope = 'project') {
|
|
2089
|
+
const baseDir = scope === 'global' ? HOME_DIR : targetDir;
|
|
2090
|
+
const tools = [];
|
|
2091
|
+
if (isRealDir(path.join(baseDir, '.claude'))) tools.push('claude');
|
|
2092
|
+
if (isRealDir(path.join(baseDir, '.cursor'))) tools.push('cursor');
|
|
2093
|
+
return tools;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
/** 读取指定工具的 MCP 已配置服务器 */
|
|
2097
|
+
function getMcpServers(toolName, filePath) {
|
|
2098
|
+
const config = MCP_CONFIG_PATHS[toolName];
|
|
2099
|
+
if (!config) return {};
|
|
2100
|
+
const resolvedPath = filePath || resolveMcpConfigPath(toolName, 'project');
|
|
2101
|
+
try {
|
|
2102
|
+
const data = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
2103
|
+
return data[config.key] || {};
|
|
2104
|
+
} catch { return {}; }
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
/** 写入指定工具的 MCP 配置(保留文件其他字段) */
|
|
2108
|
+
function setMcpServers(toolName, mcpServers, filePath) {
|
|
2109
|
+
const config = MCP_CONFIG_PATHS[toolName];
|
|
2110
|
+
if (!config) return;
|
|
2111
|
+
const resolvedPath = filePath || resolveMcpConfigPath(toolName, 'project');
|
|
2112
|
+
let data = {};
|
|
2113
|
+
try { data = JSON.parse(fs.readFileSync(resolvedPath, 'utf8')); } catch { /* 新建 */ }
|
|
2114
|
+
data[config.key] = mcpServers;
|
|
2115
|
+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
2116
|
+
fs.writeFileSync(resolvedPath, JSON.stringify(data, null, 2) + '\n');
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
/** 生成等效的手动安装 CLI 命令提示 */
|
|
2120
|
+
function buildCliHints(entry, scope) {
|
|
2121
|
+
const scopeFlag = scope === 'global' ? '-s user ' : '';
|
|
2122
|
+
const hints = [];
|
|
2123
|
+
|
|
2124
|
+
// Claude Code CLI
|
|
2125
|
+
if (entry.command && entry.command !== 'npx') {
|
|
2126
|
+
hints.push(`claude: claude mcp add ${entry.name} ${scopeFlag}--transport stdio -- ${entry.command} ${entry.args.join(' ')}`);
|
|
2127
|
+
} else {
|
|
2128
|
+
hints.push(`claude: claude mcp add ${entry.name} ${scopeFlag}-- npx -y ${entry.package}`);
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
// Cursor:无官方 CLI,提示手动编辑
|
|
2132
|
+
const cursorFile = scope === 'global' ? '~/.cursor/mcp.json' : '.cursor/mcp.json';
|
|
2133
|
+
hints.push(`cursor: 手动编辑 ${cursorFile},在 mcpServers 中添加 "${entry.name}" 配置`);
|
|
2134
|
+
|
|
2135
|
+
return hints;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
/** 构建单个 MCP 服务器的配置对象 */
|
|
2139
|
+
function buildMcpServerConfig(entry) {
|
|
2140
|
+
const command = entry.command || 'npx';
|
|
2141
|
+
const args = Array.isArray(entry.args) && entry.args.length > 0
|
|
2142
|
+
? [...entry.args]
|
|
2143
|
+
: ['-y', entry.package];
|
|
2144
|
+
|
|
2145
|
+
const config = { command, args };
|
|
2146
|
+
if (Object.keys(entry.env).length > 0) {
|
|
2147
|
+
config.env = { ...entry.env };
|
|
2148
|
+
}
|
|
2149
|
+
return config;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
/** 获取所有工具中已安装的 MCP 服务器名称集合 */
|
|
2153
|
+
function getInstalledMcpNames(tools, scope = 'project') {
|
|
2154
|
+
const names = new Set();
|
|
2155
|
+
for (const t of tools) {
|
|
2156
|
+
const configPath = resolveMcpConfigPath(t, scope);
|
|
2157
|
+
const servers = getMcpServers(t, configPath);
|
|
2158
|
+
for (const name of Object.keys(servers)) names.add(name);
|
|
2159
|
+
}
|
|
2160
|
+
return names;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
function runMcp() {
|
|
2164
|
+
if (!process.stdin.isTTY) {
|
|
2165
|
+
console.error(fmt('red', '错误:mcp 命令需要交互式终端'));
|
|
2166
|
+
process.exit(1);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2170
|
+
const ask = (question) => new Promise((resolve) => {
|
|
2171
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
2172
|
+
});
|
|
2173
|
+
|
|
2174
|
+
(async () => {
|
|
2175
|
+
try {
|
|
2176
|
+
// 第一步:选择作用域
|
|
2177
|
+
console.log(fmt('cyan', '请选择 MCP 作用域:'));
|
|
2178
|
+
console.log('');
|
|
2179
|
+
console.log(` ${fmt('bold', '1')}) ${fmt('green', '项目级')} — 写入当前项目 .claude/.cursor`);
|
|
2180
|
+
console.log(` ${fmt('bold', '2')}) ${fmt('yellow', '全局(~/.claude / ~/.cursor)')} — 对当前用户所有项目生效`);
|
|
2181
|
+
console.log('');
|
|
2182
|
+
const scopeAnswer = await ask(fmt('bold', '请输入选项 [1-2]: '));
|
|
2183
|
+
const scopeMap = { '1': 'project', '2': 'global' };
|
|
2184
|
+
const scope = scopeMap[scopeAnswer];
|
|
2185
|
+
if (!scope) {
|
|
2186
|
+
console.error(fmt('red', '无效作用域选项,退出。'));
|
|
2187
|
+
process.exit(1);
|
|
2188
|
+
}
|
|
2189
|
+
console.log('');
|
|
2190
|
+
|
|
2191
|
+
const tools = detectMcpTools(scope);
|
|
2192
|
+
if (tools.length === 0) {
|
|
2193
|
+
if (scope === 'global') {
|
|
2194
|
+
console.log(fmt('yellow', '⚠ 全局目录未检测到 ~/.claude/ 或 ~/.cursor/ 配置目录。'));
|
|
2195
|
+
console.log(` 请先运行: ${fmt('bold', 'npx leniu-dev global --tool claude')}`);
|
|
2196
|
+
} else {
|
|
2197
|
+
console.log(fmt('yellow', '⚠ 当前目录未检测到 .claude/ 或 .cursor/ 配置目录。'));
|
|
2198
|
+
console.log(` 请先运行: ${fmt('bold', hintCmd('init --tool claude'))}`);
|
|
2199
|
+
}
|
|
2200
|
+
console.log('');
|
|
2201
|
+
process.exit(1);
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
const scopeLabel = scope === 'global'
|
|
2205
|
+
? `全局(${HOME_DIR}/.claude / ${HOME_DIR}/.cursor)`
|
|
2206
|
+
: `项目级(${targetDir})`;
|
|
2207
|
+
console.log(` 作用域: ${fmt('bold', scopeLabel)}`);
|
|
2208
|
+
console.log(` 检测到工具: ${fmt('bold', tools.join(', '))}`);
|
|
2209
|
+
console.log('');
|
|
2210
|
+
|
|
2211
|
+
// 第二步:选择操作
|
|
2212
|
+
console.log(fmt('cyan', '请选择 MCP 操作:'));
|
|
2213
|
+
console.log('');
|
|
2214
|
+
console.log(` ${fmt('bold', '1')}) ${fmt('green', '安装 MCP 服务器')} — 从预置列表选择并安装到配置`);
|
|
2215
|
+
console.log(` ${fmt('bold', '2')}) ${fmt('red', '卸载 MCP 服务器')} — 从已安装列表中移除`);
|
|
2216
|
+
console.log(` ${fmt('bold', '3')}) ${fmt('cyan', '查看状态')} — 检查已配置的 MCP 服务器`);
|
|
2217
|
+
console.log(` ${fmt('bold', '4')}) ${fmt('yellow', '一键推荐安装')} — 安装所有推荐的 MCP 服务器`);
|
|
2218
|
+
console.log('');
|
|
2219
|
+
const action = await ask(fmt('bold', '请输入选项 [1-4]: '));
|
|
2220
|
+
console.log('');
|
|
2221
|
+
|
|
2222
|
+
switch (action) {
|
|
2223
|
+
case '1': await mcpInstall(tools, ask, scope); break;
|
|
2224
|
+
case '2': await mcpUninstall(tools, ask, scope); break;
|
|
2225
|
+
case '3': mcpStatus(tools, scope); break;
|
|
2226
|
+
case '4': await mcpRecommend(tools, ask, scope); break;
|
|
2227
|
+
default:
|
|
2228
|
+
console.error(fmt('red', '无效选项,退出。'));
|
|
2229
|
+
process.exit(1);
|
|
2230
|
+
}
|
|
2231
|
+
} finally {
|
|
2232
|
+
rl.close();
|
|
2233
|
+
}
|
|
2234
|
+
})();
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
/** 安装 MCP 服务器 */
|
|
2238
|
+
async function mcpInstall(tools, ask, scope = 'project') {
|
|
2239
|
+
const installed = getInstalledMcpNames(tools, scope);
|
|
2240
|
+
|
|
2241
|
+
console.log(fmt('cyan', '可用的 MCP 服务器:'));
|
|
2242
|
+
console.log('');
|
|
2243
|
+
for (let i = 0; i < MCP_REGISTRY.length; i++) {
|
|
2244
|
+
const entry = MCP_REGISTRY[i];
|
|
2245
|
+
const tags = [];
|
|
2246
|
+
if (installed.has(entry.name)) tags.push(fmt('green', '[已安装]'));
|
|
2247
|
+
if (entry.recommended) tags.push(fmt('yellow', '[推荐]'));
|
|
2248
|
+
const tagStr = tags.length > 0 ? ' ' + tags.join(' ') : '';
|
|
2249
|
+
console.log(` ${fmt('bold', String(i + 1))}) ${fmt('bold', entry.name)}${tagStr}`);
|
|
2250
|
+
console.log(` ${entry.description}`);
|
|
2251
|
+
}
|
|
2252
|
+
console.log('');
|
|
2253
|
+
const answer = await ask(fmt('bold', '请选择要安装的服务器(逗号分隔,如 1,2,3): '));
|
|
2254
|
+
const indices = answer.split(',').map(s => parseInt(s.trim(), 10) - 1).filter(i => i >= 0 && i < MCP_REGISTRY.length);
|
|
2255
|
+
|
|
2256
|
+
if (indices.length === 0) {
|
|
2257
|
+
console.log(fmt('yellow', '未选择任何服务器,退出。'));
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
const selected = indices.map(i => MCP_REGISTRY[i]);
|
|
2262
|
+
console.log('');
|
|
2263
|
+
|
|
2264
|
+
// 处理需要 env 的服务器
|
|
2265
|
+
for (const entry of selected) {
|
|
2266
|
+
if (Object.keys(entry.env).length > 0) {
|
|
2267
|
+
console.log(fmt('cyan', `── ${entry.name} 环境变量配置 ──`));
|
|
2268
|
+
for (const [key, defaultVal] of Object.entries(entry.env)) {
|
|
2269
|
+
const val = await ask(` ${key} [${defaultVal}]: `);
|
|
2270
|
+
if (val) entry.env[key] = val;
|
|
2271
|
+
}
|
|
2272
|
+
console.log('');
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// 写入所有检测到的工具配置
|
|
2277
|
+
for (const toolName of tools) {
|
|
2278
|
+
const configPath = resolveMcpConfigPath(toolName, scope);
|
|
2279
|
+
const servers = getMcpServers(toolName, configPath);
|
|
2280
|
+
for (const entry of selected) {
|
|
2281
|
+
servers[entry.name] = buildMcpServerConfig(entry);
|
|
2282
|
+
}
|
|
2283
|
+
setMcpServers(toolName, servers, configPath);
|
|
2284
|
+
console.log(` ${fmt('green', '✓')} ${toolName}: 已写入 ${selected.map(e => e.name).join(', ')}`);
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
console.log('');
|
|
2288
|
+
console.log(fmt('green', fmt('bold', `✅ 已安装 ${selected.length} 个 MCP 服务器!`)));
|
|
2289
|
+
console.log('');
|
|
2290
|
+
|
|
2291
|
+
// 输出等效手动安装命令供参考
|
|
2292
|
+
console.log(fmt('cyan', '💡 等效手动安装命令(仅供参考):'));
|
|
2293
|
+
console.log('');
|
|
2294
|
+
for (const entry of selected) {
|
|
2295
|
+
console.log(` ${fmt('bold', entry.name)}:`);
|
|
2296
|
+
for (const hint of buildCliHints(entry, scope)) {
|
|
2297
|
+
console.log(` ${fmt('yellow', hint)}`);
|
|
2298
|
+
}
|
|
2299
|
+
console.log('');
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
/** 卸载 MCP 服务器 */
|
|
2304
|
+
async function mcpUninstall(tools, ask, scope = 'project') {
|
|
2305
|
+
// 收集所有已安装的服务器(合并去重)
|
|
2306
|
+
const allServers = new Map(); // name → 出现在哪些工具中
|
|
2307
|
+
for (const toolName of tools) {
|
|
2308
|
+
const configPath = resolveMcpConfigPath(toolName, scope);
|
|
2309
|
+
const servers = getMcpServers(toolName, configPath);
|
|
2310
|
+
for (const name of Object.keys(servers)) {
|
|
2311
|
+
if (!allServers.has(name)) allServers.set(name, []);
|
|
2312
|
+
allServers.get(name).push(toolName);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
if (allServers.size === 0) {
|
|
2317
|
+
console.log(fmt('yellow', ' 当前没有已安装的 MCP 服务器。'));
|
|
2318
|
+
console.log(` 运行 ${fmt('bold', hintCmd('mcp'))} 安装服务器。`);
|
|
2319
|
+
console.log('');
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
const serverNames = [...allServers.keys()];
|
|
2324
|
+
console.log(fmt('cyan', '已安装的 MCP 服务器:'));
|
|
2325
|
+
console.log('');
|
|
2326
|
+
for (let i = 0; i < serverNames.length; i++) {
|
|
2327
|
+
const name = serverNames[i];
|
|
2328
|
+
const toolList = allServers.get(name).join(', ');
|
|
2329
|
+
console.log(` ${fmt('bold', String(i + 1))}) ${fmt('bold', name)} ${fmt('magenta', `(${toolList})`)}`);
|
|
2330
|
+
}
|
|
2331
|
+
console.log('');
|
|
2332
|
+
const answer = await ask(fmt('bold', '请选择要卸载的服务器(逗号分隔): '));
|
|
2333
|
+
const indices = answer.split(',').map(s => parseInt(s.trim(), 10) - 1).filter(i => i >= 0 && i < serverNames.length);
|
|
2334
|
+
|
|
2335
|
+
if (indices.length === 0) {
|
|
2336
|
+
console.log(fmt('yellow', '未选择任何服务器,退出。'));
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
const toRemove = indices.map(i => serverNames[i]);
|
|
2341
|
+
console.log('');
|
|
2342
|
+
|
|
2343
|
+
for (const toolName of tools) {
|
|
2344
|
+
const configPath = resolveMcpConfigPath(toolName, scope);
|
|
2345
|
+
const servers = getMcpServers(toolName, configPath);
|
|
2346
|
+
let removed = 0;
|
|
2347
|
+
for (const name of toRemove) {
|
|
2348
|
+
if (name in servers) {
|
|
2349
|
+
delete servers[name];
|
|
2350
|
+
removed++;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
if (removed > 0) {
|
|
2354
|
+
setMcpServers(toolName, servers, configPath);
|
|
2355
|
+
console.log(` ${fmt('green', '✓')} ${toolName}: 已移除 ${toRemove.filter(n => !servers[n]).join(', ')}`);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
console.log('');
|
|
2360
|
+
console.log(fmt('green', fmt('bold', `✅ 已卸载 ${toRemove.length} 个 MCP 服务器!`)));
|
|
2361
|
+
console.log('');
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
/** 查看 MCP 状态 */
|
|
2365
|
+
function mcpStatus(tools, scope = 'project') {
|
|
2366
|
+
let hasAny = false;
|
|
2367
|
+
|
|
2368
|
+
for (const toolName of tools) {
|
|
2369
|
+
const configPath = resolveMcpConfigPath(toolName, scope);
|
|
2370
|
+
const servers = getMcpServers(toolName, configPath);
|
|
2371
|
+
const names = Object.keys(servers);
|
|
2372
|
+
|
|
2373
|
+
console.log(fmt('cyan', `[${toolName}]`) + ` ${configPath}`);
|
|
2374
|
+
|
|
2375
|
+
if (names.length === 0) {
|
|
2376
|
+
console.log(` ${fmt('yellow', '(无已安装的 MCP 服务器)')}`);
|
|
2377
|
+
} else {
|
|
2378
|
+
hasAny = true;
|
|
2379
|
+
for (const name of names) {
|
|
2380
|
+
const srv = servers[name];
|
|
2381
|
+
const args = srv.args || [];
|
|
2382
|
+
const pkg = args.find(a => a.startsWith('@'))
|
|
2383
|
+
|| (srv.command !== 'npx' ? `${srv.command} ${args.join(' ')}` : '—');
|
|
2384
|
+
const envKeys = srv.env ? Object.keys(srv.env).join(', ') : '—';
|
|
2385
|
+
console.log(` ${fmt('green', '●')} ${fmt('bold', name)}`);
|
|
2386
|
+
console.log(` 命令: ${pkg} | 环境变量: ${envKeys}`);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
console.log('');
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
if (!hasAny) {
|
|
2393
|
+
console.log(fmt('yellow', '💡 未安装任何 MCP 服务器。'));
|
|
2394
|
+
console.log(` 运行 ${fmt('bold', hintCmd('mcp'))} 开始安装。`);
|
|
2395
|
+
console.log('');
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
/** 一键推荐安装 */
|
|
2400
|
+
async function mcpRecommend(tools, ask, scope = 'project') {
|
|
2401
|
+
const installed = getInstalledMcpNames(tools, scope);
|
|
2402
|
+
const toInstall = MCP_REGISTRY.filter(e => e.recommended && !installed.has(e.name));
|
|
2403
|
+
|
|
2404
|
+
if (toInstall.length === 0) {
|
|
2405
|
+
console.log(fmt('green', ' ✓ 所有推荐的 MCP 服务器已安装!'));
|
|
2406
|
+
console.log('');
|
|
2407
|
+
mcpStatus(tools, scope);
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
console.log(fmt('cyan', '将安装以下推荐服务器:'));
|
|
2412
|
+
console.log('');
|
|
2413
|
+
for (const entry of toInstall) {
|
|
2414
|
+
console.log(` ${fmt('green', '●')} ${fmt('bold', entry.name)} — ${entry.description}`);
|
|
2415
|
+
}
|
|
2416
|
+
console.log('');
|
|
2417
|
+
|
|
2418
|
+
// 处理需要 env 的服务器
|
|
2419
|
+
for (const entry of toInstall) {
|
|
2420
|
+
if (Object.keys(entry.env).length > 0) {
|
|
2421
|
+
console.log(fmt('cyan', `── ${entry.name} 环境变量配置 ──`));
|
|
2422
|
+
for (const [key, defaultVal] of Object.entries(entry.env)) {
|
|
2423
|
+
const val = await ask(` ${key} [${defaultVal}]: `);
|
|
2424
|
+
if (val) entry.env[key] = val;
|
|
2425
|
+
}
|
|
2426
|
+
console.log('');
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
// 写入配置
|
|
2431
|
+
for (const toolName of tools) {
|
|
2432
|
+
const configPath = resolveMcpConfigPath(toolName, scope);
|
|
2433
|
+
const servers = getMcpServers(toolName, configPath);
|
|
2434
|
+
for (const entry of toInstall) {
|
|
2435
|
+
servers[entry.name] = buildMcpServerConfig(entry);
|
|
2436
|
+
}
|
|
2437
|
+
setMcpServers(toolName, servers, configPath);
|
|
2438
|
+
console.log(` ${fmt('green', '✓')} ${toolName}: 已写入 ${toInstall.map(e => e.name).join(', ')}`);
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
console.log('');
|
|
2442
|
+
console.log(fmt('green', fmt('bold', `✅ 已安装 ${toInstall.length} 个推荐 MCP 服务器!`)));
|
|
2443
|
+
console.log('');
|
|
2444
|
+
|
|
2445
|
+
// 输出等效手动安装命令供参考
|
|
2446
|
+
console.log(fmt('cyan', '💡 等效手动安装命令(仅供参考):'));
|
|
2447
|
+
console.log('');
|
|
2448
|
+
for (const entry of toInstall) {
|
|
2449
|
+
console.log(` ${fmt('bold', entry.name)}:`);
|
|
2450
|
+
for (const hint of buildCliHints(entry, scope)) {
|
|
2451
|
+
console.log(` ${fmt('yellow', hint)}`);
|
|
2452
|
+
}
|
|
2453
|
+
console.log('');
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// ── 角色→技能映射 ─────────────────────────────────────────────────────────
|
|
2458
|
+
|
|
2459
|
+
const SKILL_ROLES = {
|
|
2460
|
+
// 通用技能(所有角色都安装)
|
|
2461
|
+
common: [
|
|
2462
|
+
'brainstorm', 'tech-decision', 'git-workflow', 'project-navigator',
|
|
2463
|
+
'task-tracker', 'codex-code-review', 'analyze-requirements',
|
|
2464
|
+
'bug-detective', 'fix-bug', 'code-patterns', 'architecture-design',
|
|
2465
|
+
'start', 'next', 'progress', 'sync', 'update-status', 'add-todo',
|
|
2466
|
+
'init-docs', 'sync-back-merge', 'add-skill', 'skill-creator',
|
|
2467
|
+
'yunxiao-task-management', 'collaborating-with-codex',
|
|
2468
|
+
// OpenSpec 系列
|
|
2469
|
+
'openspec-apply-change', 'openspec-archive-change', 'openspec-bulk-archive-change',
|
|
2470
|
+
'openspec-continue-change', 'openspec-explore', 'openspec-ff-change',
|
|
2471
|
+
'openspec-new-change', 'openspec-onboard', 'openspec-sync-specs', 'openspec-verify-change',
|
|
2472
|
+
// leniu 通用
|
|
2473
|
+
'leniu-brainstorm', 'leniu-code-patterns',
|
|
2474
|
+
],
|
|
2475
|
+
// 后端研发专属
|
|
2476
|
+
backend: [
|
|
2477
|
+
'crud-development', 'crud', 'dev', 'check', 'api-development',
|
|
2478
|
+
'database-ops', 'backend-annotations', 'utils-toolkit',
|
|
2479
|
+
'error-handler', 'performance-doctor', 'data-permission',
|
|
2480
|
+
'security-guard', 'redis-cache', 'scheduled-jobs', 'json-serialization',
|
|
2481
|
+
'file-oss-management', 'test-development', 'auto-test',
|
|
2482
|
+
'sms-mail', 'social-login', 'tenant-management', 'websocket-sse',
|
|
2483
|
+
'workflow-engine', 'jenkins-deploy', 'mysql-debug', 'loki-log-query',
|
|
2484
|
+
'collaborating-with-gemini',
|
|
2485
|
+
// leniu 后端专属
|
|
2486
|
+
'leniu-api-development', 'leniu-architecture-design', 'leniu-backend-annotations',
|
|
2487
|
+
'leniu-crud-development', 'leniu-customization-location', 'leniu-data-permission',
|
|
2488
|
+
'leniu-database-ops', 'leniu-error-handler', 'leniu-java-amount-handling',
|
|
2489
|
+
'leniu-java-code-style', 'leniu-java-concurrent', 'leniu-java-entity',
|
|
2490
|
+
'leniu-java-export', 'leniu-java-logging', 'leniu-java-mq',
|
|
2491
|
+
'leniu-java-mybatis', 'leniu-java-report-query-param', 'leniu-java-task',
|
|
2492
|
+
'leniu-java-total-line', 'leniu-marketing-price-rule-customizer',
|
|
2493
|
+
'leniu-marketing-recharge-rule-customizer', 'leniu-mealtime',
|
|
2494
|
+
'leniu-redis-cache', 'leniu-report-customization',
|
|
2495
|
+
'leniu-report-standard-customization', 'leniu-security-guard',
|
|
2496
|
+
'leniu-utils-toolkit',
|
|
2497
|
+
],
|
|
2498
|
+
// 前端研发专属
|
|
2499
|
+
frontend: [
|
|
2500
|
+
'ui-pc', 'store-pc', 'collaborating-with-gemini', 'dev', 'check',
|
|
2501
|
+
'chrome-cdp',
|
|
2502
|
+
],
|
|
2503
|
+
// 产品经理专属
|
|
2504
|
+
product: [
|
|
2505
|
+
'banana-image', 'lanhu-design',
|
|
2506
|
+
],
|
|
2507
|
+
};
|
|
2508
|
+
|
|
2509
|
+
/** 获取指定角色应安装的技能列表 */
|
|
2510
|
+
function getSkillsForRole(role) {
|
|
2511
|
+
if (role === 'all') {
|
|
2512
|
+
// 全部 = 所有已知技能(不过滤)
|
|
2513
|
+
return null; // null 表示不过滤,安装所有
|
|
2514
|
+
}
|
|
2515
|
+
const skills = new Set(SKILL_ROLES.common);
|
|
2516
|
+
if (role === 'backend' || role === 'fullstack') {
|
|
2517
|
+
SKILL_ROLES.backend.forEach(s => skills.add(s));
|
|
2518
|
+
}
|
|
2519
|
+
if (role === 'frontend' || role === 'fullstack') {
|
|
2520
|
+
SKILL_ROLES.frontend.forEach(s => skills.add(s));
|
|
2521
|
+
}
|
|
2522
|
+
if (role === 'product') {
|
|
2523
|
+
SKILL_ROLES.product.forEach(s => skills.add(s));
|
|
2524
|
+
}
|
|
2525
|
+
return skills;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
/** 按角色过滤复制技能目录(只复制匹配的技能) */
|
|
2529
|
+
function copyDirFiltered(src, dest, allowedSkills) {
|
|
2530
|
+
let written = 0;
|
|
2531
|
+
try {
|
|
2532
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
2533
|
+
} catch (e) {
|
|
2534
|
+
console.log(` ${fmt('red', '✗')} 无法创建目录 ${dest}: ${e.message}`);
|
|
2535
|
+
return written;
|
|
2536
|
+
}
|
|
2537
|
+
let entries;
|
|
2538
|
+
try { entries = fs.readdirSync(src); } catch { return written; }
|
|
2539
|
+
for (const entry of entries) {
|
|
2540
|
+
const s = path.join(src, entry);
|
|
2541
|
+
const d = path.join(dest, entry);
|
|
2542
|
+
try {
|
|
2543
|
+
if (fs.statSync(s).isDirectory()) {
|
|
2544
|
+
// 如果是 skills 目录的直接子目录,检查是否在允许列表中
|
|
2545
|
+
if (allowedSkills && src.endsWith('/skills') && !allowedSkills.has(entry)) {
|
|
2546
|
+
continue; // 跳过不在角色列表中的技能
|
|
2547
|
+
}
|
|
2548
|
+
written += copyDir(s, d);
|
|
2549
|
+
} else {
|
|
2550
|
+
fs.copyFileSync(s, d);
|
|
2551
|
+
written++;
|
|
2552
|
+
}
|
|
2553
|
+
} catch { /* 跳过 */ }
|
|
2554
|
+
}
|
|
2555
|
+
return written;
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
// ── 安装元数据 ────────────────────────────────────────────────────────────
|
|
2559
|
+
const INSTALL_META_FILE = '.leniu-install-meta.json';
|
|
2560
|
+
|
|
2561
|
+
/** 读取安装元数据 */
|
|
2562
|
+
function readInstallMeta() {
|
|
2563
|
+
const paths = [
|
|
2564
|
+
path.join(HOME_DIR, '.claude', INSTALL_META_FILE),
|
|
2565
|
+
path.join(HOME_DIR, '.cursor', INSTALL_META_FILE),
|
|
2566
|
+
];
|
|
2567
|
+
for (const p of paths) {
|
|
2568
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { /* continue */ }
|
|
2569
|
+
}
|
|
2570
|
+
return null;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
/** 写入安装元数据 */
|
|
2574
|
+
function writeInstallMeta(meta) {
|
|
2575
|
+
const metaPath = path.join(HOME_DIR, '.claude', INSTALL_META_FILE);
|
|
2576
|
+
try {
|
|
2577
|
+
fs.mkdirSync(path.dirname(metaPath), { recursive: true });
|
|
2578
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
2579
|
+
} catch { /* 静默失败 */ }
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
/** 构建安装元数据对象 */
|
|
2583
|
+
function buildInstallMeta(toolsInstalled) {
|
|
2584
|
+
const existing = readInstallMeta() || {};
|
|
2585
|
+
return {
|
|
2586
|
+
...existing,
|
|
2587
|
+
version: PKG_VERSION,
|
|
2588
|
+
installedAt: existing.installedAt || new Date().toISOString(),
|
|
2589
|
+
updatedAt: new Date().toISOString(),
|
|
2590
|
+
tools: toolsInstalled,
|
|
2591
|
+
role: existing.role || 'all',
|
|
2592
|
+
};
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// ── install 命令(用户级安装)────────────────────────────────────────────────
|
|
2596
|
+
// install = 原 global 命令逻辑,固定安装到用户目录
|
|
2597
|
+
|
|
2598
|
+
function runInstall(selectedTool, role) {
|
|
2599
|
+
role = role || 'all';
|
|
2600
|
+
const validKeys = Object.keys(GLOBAL_RULES);
|
|
2601
|
+
const toolsToInstall = (!selectedTool || selectedTool === 'all')
|
|
2602
|
+
? validKeys
|
|
2603
|
+
: [selectedTool];
|
|
2604
|
+
|
|
2605
|
+
if (selectedTool && selectedTool !== 'all' && !GLOBAL_RULES[selectedTool]) {
|
|
2606
|
+
console.error(fmt('red', `无效工具: "${selectedTool}"。有效选项: claude | cursor | codex | all`));
|
|
2607
|
+
process.exit(1);
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
const allowedSkills = getSkillsForRole(role);
|
|
2611
|
+
const roleLabels = { backend: '后端研发', frontend: '前端研发', product: '产品经理', all: '全部' };
|
|
2612
|
+
|
|
2613
|
+
console.log(` 安装模式: ${fmt('green', fmt('bold', '用户级安装(当前用户所有项目生效)'))}`);
|
|
2614
|
+
console.log(` 安装角色: ${fmt('bold', roleLabels[role] || role)}`);
|
|
2615
|
+
console.log(` 安装工具: ${fmt('bold', toolsToInstall.join(', '))}`);
|
|
2616
|
+
if (allowedSkills) {
|
|
2617
|
+
console.log(` 技能数量: ${fmt('bold', String(allowedSkills.size))} 个`);
|
|
2618
|
+
}
|
|
2619
|
+
if (force) console.log(` ${fmt('yellow', '⚠ --force 模式:强制覆盖已有文件')}`);
|
|
2620
|
+
console.log('');
|
|
2621
|
+
console.log(fmt('bold', '正在安装到系统目录...'));
|
|
2622
|
+
console.log('');
|
|
2623
|
+
|
|
2624
|
+
let totalInstalled = 0, totalFailed = 0;
|
|
2625
|
+
for (let i = 0; i < toolsToInstall.length; i++) {
|
|
2626
|
+
showCowProgress(i, toolsToInstall.length, `安装 ${GLOBAL_RULES[toolsToInstall[i]].label}...`);
|
|
2627
|
+
const { installed, failed } = globalInstallTool(toolsToInstall[i], allowedSkills);
|
|
2628
|
+
totalInstalled += installed;
|
|
2629
|
+
totalFailed += failed;
|
|
2630
|
+
if (i < toolsToInstall.length - 1) console.log('');
|
|
2631
|
+
}
|
|
2632
|
+
showCowProgress(toolsToInstall.length, toolsToInstall.length, '安装完成!');
|
|
2633
|
+
|
|
2634
|
+
// 写入安装元数据
|
|
2635
|
+
const meta = buildInstallMeta(toolsToInstall);
|
|
2636
|
+
meta.role = role;
|
|
2637
|
+
if (allowedSkills) meta.skillCount = allowedSkills.size;
|
|
2638
|
+
writeInstallMeta(meta);
|
|
2639
|
+
|
|
2640
|
+
console.log('');
|
|
2641
|
+
console.log(fmt('green', fmt('bold', '✅ 安装完成!')));
|
|
2642
|
+
console.log('');
|
|
2643
|
+
console.log(` ${fmt('green', `✓ 安装文件: ${totalInstalled} 个`)}`);
|
|
2644
|
+
if (totalFailed > 0) {
|
|
2645
|
+
console.log(` ${fmt('red', `✗ 失败文件: ${totalFailed} 个`)}(请检查目录权限)`);
|
|
2646
|
+
}
|
|
2647
|
+
console.log('');
|
|
2648
|
+
console.log(fmt('cyan', '安装位置说明:'));
|
|
2649
|
+
for (const key of toolsToInstall) {
|
|
2650
|
+
const rule = GLOBAL_RULES[key];
|
|
2651
|
+
console.log(` ${fmt('bold', rule.label + ':')} ${rule.note}`);
|
|
2652
|
+
}
|
|
2653
|
+
console.log('');
|
|
2654
|
+
console.log(fmt('yellow', '提示:'));
|
|
2655
|
+
console.log(` 更新: ${fmt('bold', 'npx leniu-dev@latest update')}`);
|
|
2656
|
+
console.log(` 推送修改: ${fmt('bold', 'npx leniu-dev syncback --submit')}`);
|
|
2657
|
+
console.log(` 诊断: ${fmt('bold', 'npx leniu-dev doctor')}`);
|
|
2658
|
+
console.log('');
|
|
2659
|
+
|
|
2660
|
+
if (totalFailed > 0) process.exitCode = 1;
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
// ── doctor 命令(诊断安装状态)──────────────────────────────────────────────
|
|
2664
|
+
|
|
2665
|
+
function runDoctor() {
|
|
2666
|
+
console.log(fmt('bold', '🔍 诊断安装状态...\n'));
|
|
2667
|
+
|
|
2668
|
+
let issues = 0;
|
|
2669
|
+
|
|
2670
|
+
// 1. 检查安装元数据
|
|
2671
|
+
const meta = readInstallMeta();
|
|
2672
|
+
if (meta) {
|
|
2673
|
+
console.log(` ${fmt('green', '✓')} 安装版本: v${meta.version}(安装于 ${meta.installedAt})`);
|
|
2674
|
+
if (meta.version !== PKG_VERSION) {
|
|
2675
|
+
console.log(` ${fmt('yellow', '⚠')} 当前包版本 v${PKG_VERSION} 与安装版本 v${meta.version} 不一致`);
|
|
2676
|
+
console.log(` 运行 ${fmt('bold', 'npx leniu-dev@latest update')} 更新`);
|
|
2677
|
+
issues++;
|
|
2678
|
+
}
|
|
2679
|
+
} else {
|
|
2680
|
+
console.log(` ${fmt('yellow', '⚠')} 未找到安装元数据(可能是旧版安装)`);
|
|
2681
|
+
issues++;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
// 2. 检查工具目录
|
|
2685
|
+
const toolChecks = [
|
|
2686
|
+
{ name: 'Claude Code', dir: path.join(HOME_DIR, '.claude'), subDirs: ['skills', 'commands', 'hooks'] },
|
|
2687
|
+
{ name: 'Cursor', dir: path.join(HOME_DIR, '.cursor'), subDirs: ['skills'] },
|
|
2688
|
+
{ name: 'Codex', dir: path.join(HOME_DIR, '.codex'), subDirs: ['skills'] },
|
|
2689
|
+
];
|
|
2690
|
+
|
|
2691
|
+
console.log('');
|
|
2692
|
+
for (const tc of toolChecks) {
|
|
2693
|
+
if (isRealDir(tc.dir)) {
|
|
2694
|
+
const missing = tc.subDirs.filter(d => !isRealDir(path.join(tc.dir, d)));
|
|
2695
|
+
if (missing.length === 0) {
|
|
2696
|
+
const skillCount = countSkills(path.join(tc.dir, 'skills'));
|
|
2697
|
+
console.log(` ${fmt('green', '✓')} ${tc.name}: ${tc.dir} (${skillCount} 个技能)`);
|
|
2698
|
+
} else {
|
|
2699
|
+
console.log(` ${fmt('yellow', '⚠')} ${tc.name}: 缺少 ${missing.join(', ')}`);
|
|
2700
|
+
issues++;
|
|
2701
|
+
}
|
|
2702
|
+
} else {
|
|
2703
|
+
console.log(` ${fmt('yellow', '○')} ${tc.name}: 未安装`);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
// 3. 检查配置文件
|
|
2708
|
+
console.log('');
|
|
2709
|
+
const configChecks = [
|
|
2710
|
+
{ name: 'MySQL 配置', file: path.join(HOME_DIR, '.claude', 'mysql-config.json') },
|
|
2711
|
+
{ name: 'Loki 配置', file: path.join(HOME_DIR, '.claude', 'loki-config.json') },
|
|
2712
|
+
{ name: 'Jenkins 配置', file: path.join(HOME_DIR, '.claude', 'jenkins-config.json') },
|
|
2713
|
+
];
|
|
2714
|
+
for (const cc of configChecks) {
|
|
2715
|
+
if (fs.existsSync(cc.file)) {
|
|
2716
|
+
console.log(` ${fmt('green', '✓')} ${cc.name}: 已配置`);
|
|
2717
|
+
} else {
|
|
2718
|
+
console.log(` ${fmt('yellow', '○')} ${cc.name}: 未配置`);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
// 4. 检查 MCP 服务器
|
|
2723
|
+
console.log('');
|
|
2724
|
+
const claudeSettings = path.join(HOME_DIR, '.claude', 'settings.json');
|
|
2725
|
+
if (fs.existsSync(claudeSettings)) {
|
|
2726
|
+
try {
|
|
2727
|
+
const data = JSON.parse(fs.readFileSync(claudeSettings, 'utf8'));
|
|
2728
|
+
const mcpCount = data.mcpServers ? Object.keys(data.mcpServers).length : 0;
|
|
2729
|
+
console.log(` ${fmt('green', '✓')} MCP 服务器: ${mcpCount} 个已配置`);
|
|
2730
|
+
} catch {
|
|
2731
|
+
console.log(` ${fmt('yellow', '⚠')} MCP 配置文件解析失败`);
|
|
2732
|
+
issues++;
|
|
2733
|
+
}
|
|
2734
|
+
} else {
|
|
2735
|
+
console.log(` ${fmt('yellow', '○')} MCP: 无 settings.json`);
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
// 结论
|
|
2739
|
+
console.log('');
|
|
2740
|
+
if (issues === 0) {
|
|
2741
|
+
console.log(fmt('green', fmt('bold', '✅ 一切正常!')));
|
|
2742
|
+
} else {
|
|
2743
|
+
console.log(fmt('yellow', fmt('bold', `⚠ 发现 ${issues} 个问题,建议运行 npx leniu-dev@latest install --force 修复`)));
|
|
2744
|
+
}
|
|
2745
|
+
console.log('');
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
/** 统计 skills 目录下的技能数量 */
|
|
2749
|
+
function countSkills(skillsDir) {
|
|
2750
|
+
try {
|
|
2751
|
+
return fs.readdirSync(skillsDir).filter(d =>
|
|
2752
|
+
isRealDir(path.join(skillsDir, d))
|
|
2753
|
+
).length;
|
|
2754
|
+
} catch { return 0; }
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
// ── uninstall 命令 ──────────────────────────────────────────────────────────
|
|
2758
|
+
|
|
2759
|
+
function runUninstall() {
|
|
2760
|
+
const meta = readInstallMeta();
|
|
2761
|
+
if (!meta) {
|
|
2762
|
+
console.log(fmt('yellow', '⚠ 未检测到 leniu-dev 安装记录。'));
|
|
2763
|
+
console.log(` 如需手动清理,请删除以下目录中的 skills/commands/hooks/agents 子目录:`);
|
|
2764
|
+
console.log(` ~/.claude/ ~/.cursor/ ~/.codex/`);
|
|
2765
|
+
console.log('');
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
if (!process.stdin.isTTY) {
|
|
2770
|
+
console.error(fmt('red', '错误:uninstall 命令需要交互式终端'));
|
|
2771
|
+
process.exit(1);
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2775
|
+
console.log(fmt('red', fmt('bold', '⚠ 将删除以下已安装内容:')));
|
|
2776
|
+
console.log('');
|
|
2777
|
+
|
|
2778
|
+
const dirsToRemove = [];
|
|
2779
|
+
const tools = meta.tools || ['claude', 'cursor', 'codex'];
|
|
2780
|
+
for (const toolKey of tools) {
|
|
2781
|
+
const rule = GLOBAL_RULES[toolKey];
|
|
2782
|
+
if (!rule) continue;
|
|
2783
|
+
for (const item of rule.files) {
|
|
2784
|
+
const destPath = path.join(rule.targetDir, item.dest);
|
|
2785
|
+
if (fs.existsSync(destPath)) {
|
|
2786
|
+
console.log(` ${fmt('red', '✗')} ${destPath}`);
|
|
2787
|
+
dirsToRemove.push(destPath);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
if (dirsToRemove.length === 0) {
|
|
2793
|
+
console.log(fmt('yellow', ' 没有找到需要删除的文件。'));
|
|
2794
|
+
rl.close();
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
console.log('');
|
|
2799
|
+
rl.question(fmt('bold', '确认删除?输入 yes 继续: '), (answer) => {
|
|
2800
|
+
rl.close();
|
|
2801
|
+
if (answer.trim().toLowerCase() !== 'yes') {
|
|
2802
|
+
console.log('已取消。');
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
console.log('');
|
|
2806
|
+
let removed = 0;
|
|
2807
|
+
for (const p of dirsToRemove) {
|
|
2808
|
+
try {
|
|
2809
|
+
if (fs.statSync(p).isDirectory()) {
|
|
2810
|
+
fs.rmSync(p, { recursive: true });
|
|
2811
|
+
} else {
|
|
2812
|
+
fs.unlinkSync(p);
|
|
2813
|
+
}
|
|
2814
|
+
console.log(` ${fmt('green', '✓')} 已删除 ${p}`);
|
|
2815
|
+
removed++;
|
|
2816
|
+
} catch (e) {
|
|
2817
|
+
console.log(` ${fmt('red', '✗')} 删除失败 ${p}: ${e.message}`);
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// 删除安装元数据
|
|
2822
|
+
const metaPath = path.join(HOME_DIR, '.claude', INSTALL_META_FILE);
|
|
2823
|
+
try { fs.unlinkSync(metaPath); } catch { /* ignore */ }
|
|
2824
|
+
|
|
2825
|
+
console.log('');
|
|
2826
|
+
console.log(fmt('green', fmt('bold', `✅ 已卸载 ${removed} 个文件/目录。`)));
|
|
2827
|
+
console.log('');
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
// ── 交互式安装向导 ──────────────────────────────────────────────────────────
|
|
2832
|
+
|
|
2833
|
+
function showInstallMenu() {
|
|
2834
|
+
if (!process.stdin.isTTY) {
|
|
2835
|
+
console.error(fmt('red', '错误:非交互环境下必须指定 --tool 参数'));
|
|
2836
|
+
console.error(` 示例: ${fmt('bold', 'npx leniu-dev install --tool claude')}`);
|
|
2837
|
+
process.exit(1);
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2841
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, (a) => resolve(a.trim())));
|
|
2842
|
+
|
|
2843
|
+
(async () => {
|
|
2844
|
+
try {
|
|
2845
|
+
// ── 步骤 1:角色选择 ──
|
|
2846
|
+
console.log(fmt('cyan', fmt('bold', '📋 步骤 1/5:选择你的角色')));
|
|
2847
|
+
console.log('');
|
|
2848
|
+
console.log(` ${fmt('bold', '1')}) ${fmt('green', '🖥️ 后端研发')} — Java/Spring Boot 全栈技能包(${SKILL_ROLES.common.length + SKILL_ROLES.backend.length} 个技能)`);
|
|
2849
|
+
console.log(` ${fmt('bold', '2')}) ${fmt('cyan', '🎨 前端研发')} — Vue/Element UI 组件技能包(${SKILL_ROLES.common.length + SKILL_ROLES.frontend.length} 个技能)`);
|
|
2850
|
+
console.log(` ${fmt('bold', '3')}) ${fmt('yellow', '📋 产品经理')} — 需求分析/任务管理/原型解读(${SKILL_ROLES.common.length + SKILL_ROLES.product.length} 个技能)`);
|
|
2851
|
+
console.log(` ${fmt('bold', '4')}) ${fmt('blue', '🔧 全部安装')} — 安装所有技能和工具(91 个技能)`);
|
|
2852
|
+
console.log('');
|
|
2853
|
+
const roleAnswer = await ask(fmt('bold', '请输入选项 [1-4,默认 4]: ')) || '4';
|
|
2854
|
+
const roleMap = { '1': 'backend', '2': 'frontend', '3': 'product', '4': 'all' };
|
|
2855
|
+
const role = roleMap[roleAnswer];
|
|
2856
|
+
if (!role) {
|
|
2857
|
+
console.error(fmt('red', '无效选项,退出。'));
|
|
2858
|
+
process.exit(1);
|
|
2859
|
+
}
|
|
2860
|
+
console.log('');
|
|
2861
|
+
|
|
2862
|
+
// ── 步骤 2:AI 工具选择 ──
|
|
2863
|
+
console.log(fmt('cyan', fmt('bold', '📋 步骤 2/5:选择 AI 工具')));
|
|
2864
|
+
console.log('');
|
|
2865
|
+
console.log(` ${fmt('bold', '1')}) ${fmt('green', 'Claude Code')} — Skills + Commands + Hooks + Agents`);
|
|
2866
|
+
console.log(` ${fmt('bold', '2')}) ${fmt('cyan', 'Cursor')} — Skills + Agents + Hooks`);
|
|
2867
|
+
console.log(` ${fmt('bold', '3')}) ${fmt('yellow', 'OpenAI Codex')} — Skills`);
|
|
2868
|
+
console.log(` ${fmt('bold', '4')}) ${fmt('blue', '全部工具')} — 同时安装 Claude + Cursor + Codex`);
|
|
2869
|
+
console.log('');
|
|
2870
|
+
const toolAnswer = await ask(fmt('bold', '请输入选项 [1-4,默认 1]: ')) || '1';
|
|
2871
|
+
const toolMap = { '1': 'claude', '2': 'cursor', '3': 'codex', '4': 'all' };
|
|
2872
|
+
const selectedTool = toolMap[toolAnswer];
|
|
2873
|
+
if (!selectedTool) {
|
|
2874
|
+
console.error(fmt('red', '无效选项,退出。'));
|
|
2875
|
+
process.exit(1);
|
|
2876
|
+
}
|
|
2877
|
+
console.log('');
|
|
2878
|
+
|
|
2879
|
+
// ── 步骤 3:服务配置选择 ──
|
|
2880
|
+
console.log(fmt('cyan', fmt('bold', '📋 步骤 3/5:配置服务(可跳过)')));
|
|
2881
|
+
console.log('');
|
|
2882
|
+
const configOptions = [
|
|
2883
|
+
{ key: 'mysql', label: 'MySQL 数据库连接', desc: '用于 mysql-debug 技能查库排查', forRoles: ['backend', 'all'] },
|
|
2884
|
+
{ key: 'loki', label: 'Loki 日志查询', desc: '用于线上日志排查', forRoles: ['backend', 'all'] },
|
|
2885
|
+
];
|
|
2886
|
+
const availableConfigs = configOptions.filter(c => c.forRoles.includes(role));
|
|
2887
|
+
const selectedConfigs = [];
|
|
2888
|
+
|
|
2889
|
+
if (availableConfigs.length > 0) {
|
|
2890
|
+
for (const cfg of availableConfigs) {
|
|
2891
|
+
const cfgAnswer = await ask(` 配置 ${fmt('bold', cfg.label)}?(${cfg.desc})[y/N]: `) || 'n';
|
|
2892
|
+
if (cfgAnswer.toLowerCase() === 'y') selectedConfigs.push(cfg.key);
|
|
2893
|
+
}
|
|
2894
|
+
} else {
|
|
2895
|
+
console.log(` ${fmt('yellow', '当前角色无需配置服务,跳过。')}`);
|
|
2896
|
+
}
|
|
2897
|
+
console.log('');
|
|
2898
|
+
|
|
2899
|
+
// ── 步骤 4:MCP 服务器选择 ──
|
|
2900
|
+
console.log(fmt('cyan', fmt('bold', '📋 步骤 4/5:MCP 服务器(可跳过)')));
|
|
2901
|
+
console.log('');
|
|
2902
|
+
const mcpChoices = MCP_REGISTRY.filter(e => e.recommended);
|
|
2903
|
+
let installMcp = false;
|
|
2904
|
+
if (mcpChoices.length > 0) {
|
|
2905
|
+
console.log(' 推荐安装的 MCP 服务器:');
|
|
2906
|
+
for (const mcp of mcpChoices) {
|
|
2907
|
+
console.log(` ${fmt('green', '●')} ${fmt('bold', mcp.name)} — ${mcp.description}`);
|
|
2908
|
+
}
|
|
2909
|
+
console.log('');
|
|
2910
|
+
const mcpAnswer = await ask(fmt('bold', ' 一键安装推荐 MCP 服务器?[Y/n]: ')) || 'y';
|
|
2911
|
+
installMcp = mcpAnswer.toLowerCase() !== 'n';
|
|
2912
|
+
}
|
|
2913
|
+
console.log('');
|
|
2914
|
+
|
|
2915
|
+
// ── 步骤 5:确认安装 ──
|
|
2916
|
+
const roleLabels = { backend: '🖥️ 后端研发', frontend: '🎨 前端研发', product: '📋 产品经理', all: '🔧 全部' };
|
|
2917
|
+
const toolLabels = { claude: 'Claude Code', cursor: 'Cursor', codex: 'OpenAI Codex', all: '全部工具' };
|
|
2918
|
+
const skillSet = getSkillsForRole(role);
|
|
2919
|
+
const skillCountStr = skillSet ? `${skillSet.size} 个技能` : '全部技能';
|
|
2920
|
+
|
|
2921
|
+
console.log(fmt('cyan', fmt('bold', '📋 步骤 5/5:确认安装')));
|
|
2922
|
+
console.log('');
|
|
2923
|
+
console.log(` 角色: ${fmt('bold', roleLabels[role])}`);
|
|
2924
|
+
console.log(` 工具: ${fmt('bold', toolLabels[selectedTool])}`);
|
|
2925
|
+
console.log(` 技能: ${fmt('bold', skillCountStr)}`);
|
|
2926
|
+
console.log(` 配置: ${fmt('bold', selectedConfigs.length > 0 ? selectedConfigs.join(', ') : '无')}`);
|
|
2927
|
+
console.log(` MCP: ${fmt('bold', installMcp ? '推荐服务器' : '跳过')}`);
|
|
2928
|
+
console.log(` 位置: ${fmt('bold', '~/.claude/ (用户级)')}`);
|
|
2929
|
+
console.log('');
|
|
2930
|
+
const confirm = await ask(fmt('bold', '确认安装?[Y/n]: ')) || 'y';
|
|
2931
|
+
if (confirm.toLowerCase() === 'n') {
|
|
2932
|
+
console.log('已取消安装。');
|
|
2933
|
+
rl.close();
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
console.log('');
|
|
2937
|
+
|
|
2938
|
+
// ── 执行安装 ──
|
|
2939
|
+
rl.close();
|
|
2940
|
+
runInstall(selectedTool, role);
|
|
2941
|
+
|
|
2942
|
+
// ── 执行配置 ──
|
|
2943
|
+
if (selectedConfigs.length > 0) {
|
|
2944
|
+
console.log('');
|
|
2945
|
+
console.log(fmt('bold', '正在配置服务...'));
|
|
2946
|
+
console.log('');
|
|
2947
|
+
const cfgRl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2948
|
+
const cfgAsk = (q) => new Promise((resolve) => cfgRl.question(q, (a) => resolve(a.trim())));
|
|
2949
|
+
try {
|
|
2950
|
+
if (selectedConfigs.includes('mysql')) {
|
|
2951
|
+
await runMysqlConfig(cfgAsk, true);
|
|
2952
|
+
console.log('');
|
|
2953
|
+
}
|
|
2954
|
+
if (selectedConfigs.includes('loki')) {
|
|
2955
|
+
await runLokiConfig(cfgAsk, true);
|
|
2956
|
+
console.log('');
|
|
2957
|
+
}
|
|
2958
|
+
} finally {
|
|
2959
|
+
cfgRl.close();
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
// ── 执行 MCP 安装 ──
|
|
2964
|
+
if (installMcp) {
|
|
2965
|
+
console.log('');
|
|
2966
|
+
console.log(fmt('bold', '正在安装推荐 MCP 服务器...'));
|
|
2967
|
+
console.log('');
|
|
2968
|
+
const tools = detectMcpTools('global');
|
|
2969
|
+
if (tools.length > 0) {
|
|
2970
|
+
const installed = getInstalledMcpNames(tools, 'global');
|
|
2971
|
+
const toInstall = MCP_REGISTRY.filter(e => e.recommended && !installed.has(e.name));
|
|
2972
|
+
if (toInstall.length > 0) {
|
|
2973
|
+
for (const toolName of tools) {
|
|
2974
|
+
const configPath = resolveMcpConfigPath(toolName, 'global');
|
|
2975
|
+
const servers = getMcpServers(toolName, configPath);
|
|
2976
|
+
for (const entry of toInstall) {
|
|
2977
|
+
servers[entry.name] = buildMcpServerConfig(entry);
|
|
2978
|
+
}
|
|
2979
|
+
setMcpServers(toolName, servers, configPath);
|
|
2980
|
+
console.log(` ${fmt('green', '✓')} ${toolName}: 已安装 ${toInstall.map(e => e.name).join(', ')}`);
|
|
2981
|
+
}
|
|
2982
|
+
console.log('');
|
|
2983
|
+
console.log(fmt('green', `✅ 已安装 ${toInstall.length} 个推荐 MCP 服务器!`));
|
|
2984
|
+
} else {
|
|
2985
|
+
console.log(` ${fmt('green', '✓')} 所有推荐 MCP 服务器已安装,无需操作。`);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
} catch (e) {
|
|
2990
|
+
rl.close();
|
|
2991
|
+
console.error(fmt('red', `安装向导异常: ${e.message}`));
|
|
2992
|
+
process.exit(1);
|
|
2993
|
+
}
|
|
2994
|
+
})();
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
// ── 主菜单(无命令时展示)──────────────────────────────────────────────────
|
|
2998
|
+
function showMainMenu() {
|
|
2999
|
+
if (!process.stdin.isTTY) {
|
|
3000
|
+
console.error(fmt('red', '错误:非交互环境下必须指定命令'));
|
|
3001
|
+
console.error(` 示例: ${fmt('bold', 'npx leniu-dev install --tool claude')}`);
|
|
3002
|
+
console.error(` 运行 ${fmt('bold', 'npx leniu-dev help')} 查看所有命令`);
|
|
3003
|
+
process.exit(1);
|
|
3004
|
+
}
|
|
3005
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3006
|
+
console.log(fmt('cyan', '请选择操作:'));
|
|
3007
|
+
console.log('');
|
|
3008
|
+
console.log(` ${fmt('bold', '1')}) ${fmt('green', '安装')} — 安装 AI 工具到用户目录`);
|
|
3009
|
+
console.log(` ${fmt('bold', '2')}) ${fmt('cyan', '更新')} — 更新已安装的框架文件`);
|
|
3010
|
+
console.log(` ${fmt('bold', '3')}) ${fmt('magenta', '推送修改')} — 对比并推送本地技能修改`);
|
|
3011
|
+
console.log(` ${fmt('bold', '4')}) ${fmt('blue', '环境配置')} — 数据库连接 / Loki 日志配置`);
|
|
3012
|
+
console.log(` ${fmt('bold', '5')}) ${fmt('green', 'MCP 管理')} — MCP 服务器安装/卸载/状态`);
|
|
3013
|
+
console.log(` ${fmt('bold', '6')}) ${fmt('cyan', '诊断')} — 检查安装状态`);
|
|
3014
|
+
console.log('');
|
|
3015
|
+
rl.question(fmt('bold', '请输入选项 [1-6]: '), (answer) => {
|
|
3016
|
+
rl.close();
|
|
3017
|
+
console.log('');
|
|
3018
|
+
switch (answer.trim()) {
|
|
3019
|
+
case '1':
|
|
3020
|
+
showInstallMenu();
|
|
3021
|
+
break;
|
|
3022
|
+
case '2':
|
|
3023
|
+
runUpdate(tool);
|
|
3024
|
+
break;
|
|
3025
|
+
case '3':
|
|
3026
|
+
runSyncBack(tool, skillFilter, submitIssue);
|
|
3027
|
+
break;
|
|
3028
|
+
case '4':
|
|
3029
|
+
runConfig();
|
|
3030
|
+
break;
|
|
3031
|
+
case '5':
|
|
3032
|
+
runMcp();
|
|
3033
|
+
break;
|
|
3034
|
+
case '6':
|
|
3035
|
+
runDoctor();
|
|
3036
|
+
break;
|
|
3037
|
+
default:
|
|
3038
|
+
console.error(fmt('red', '无效选项,退出。'));
|
|
3039
|
+
process.exit(1);
|
|
3040
|
+
}
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
// ── 主入口 ────────────────────────────────────────────────────────────────
|
|
3045
|
+
if (command === 'install') {
|
|
3046
|
+
if (tool) {
|
|
3047
|
+
runInstall(tool, installRole || 'all');
|
|
3048
|
+
} else {
|
|
3049
|
+
showInstallMenu();
|
|
3050
|
+
}
|
|
3051
|
+
} else if (command === 'update') {
|
|
3052
|
+
runUpdate(tool);
|
|
3053
|
+
} else if (command === 'syncback') {
|
|
3054
|
+
runSyncBack(tool, skillFilter, submitIssue);
|
|
3055
|
+
} else if (command === 'config') {
|
|
3056
|
+
runConfig();
|
|
3057
|
+
} else if (command === 'mcp') {
|
|
3058
|
+
runMcp();
|
|
3059
|
+
} else if (command === 'doctor') {
|
|
3060
|
+
runDoctor();
|
|
3061
|
+
} else if (command === 'uninstall') {
|
|
3062
|
+
runUninstall();
|
|
3063
|
+
} else if (tool) {
|
|
3064
|
+
// 向后兼容:无 command 但有 --tool,当作 install 执行
|
|
3065
|
+
runInstall(tool);
|
|
3066
|
+
} else {
|
|
3067
|
+
// 无命令无参数:显示主菜单
|
|
3068
|
+
showMainMenu();
|
|
3069
|
+
}
|