@huicai/cli 2026.804.0 → 2026.1019.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/LICENSE +391 -32
- package/README.md +176 -10
- package/bin/huicai.js +166 -73
- package/package.json +27 -8
- package/skills/ab-testing/trigger-cases.json +30 -0
- package/skills/academic-writing/trigger-cases.json +30 -0
- package/skills/accounting-ops/trigger-cases.json +30 -0
- package/skills/ai-agent/trigger-cases.json +30 -0
- package/skills/ai-aiops/trigger-cases.json +30 -0
- package/skills/ai-coding/trigger-cases.json +30 -0
- package/skills/ai-compliance/trigger-cases.json +30 -0
- package/skills/ai-eval/trigger-cases.json +30 -0
- package/skills/ai-llm-app/trigger-cases.json +30 -0
- package/skills/ai-mlops/trigger-cases.json +30 -0
- package/skills/ai-multimodal/trigger-cases.json +30 -0
- package/skills/ai-prompt/trigger-cases.json +30 -0
- package/skills/ai-rag/trigger-cases.json +30 -0
- package/skills/anomaly-detection/trigger-cases.json +30 -0
- package/skills/api-design/trigger-cases.json +31 -0
- package/skills/api-integration/trigger-cases.json +31 -0
- package/skills/arch-design/trigger-cases.json +12 -0
- package/skills/arch-diagram/SKILL.md +118 -0
- package/skills/arch-diagram/trigger-cases.json +30 -0
- package/skills/arch-distributed/trigger-cases.json +30 -0
- package/skills/arch-multitenant/trigger-cases.json +30 -0
- package/skills/audio-brief/SKILL.md +65 -0
- package/skills/audio-brief/trigger-cases.json +30 -0
- package/skills/audit-assist/trigger-cases.json +30 -0
- package/skills/audit-chain/trigger-cases.json +31 -0
- package/skills/banking-credit/trigger-cases.json +30 -0
- package/skills/brand-design/trigger-cases.json +30 -0
- package/skills/business-plan/trigger-cases.json +30 -0
- package/skills/capital-ops/trigger-cases.json +30 -0
- package/skills/cicd-pipeline/trigger-cases.json +31 -0
- package/skills/code-archaeology/SKILL.md +55 -0
- package/skills/code-archaeology/trigger-cases.json +9 -0
- package/skills/code-quality/SKILL.md +138 -0
- package/skills/code-quality/references/languages.md +107 -0
- package/skills/code-quality/scripts/__pycache__/code_battery.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_check.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_check.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_fix.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_fix.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_java.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_java.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_lens.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_lens.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_python.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_python.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_testplan.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/code_battery.py +1072 -0
- package/skills/code-quality/scripts/code_check.py +1269 -0
- package/skills/code-quality/scripts/code_csharp.py +371 -0
- package/skills/code-quality/scripts/code_fix.py +201 -0
- package/skills/code-quality/scripts/code_java.py +695 -0
- package/skills/code-quality/scripts/code_jsts.py +313 -0
- package/skills/code-quality/scripts/code_lens.py +1079 -0
- package/skills/code-quality/scripts/code_python.py +536 -0
- package/skills/code-quality/scripts/code_testplan.py +239 -0
- package/skills/code-quality/trigger-cases.json +45 -0
- package/skills/code-review/SKILL.md +24 -0
- package/skills/code-review/trigger-cases.json +35 -0
- package/skills/commit-and-pr/trigger-cases.json +31 -0
- package/skills/content-creation/trigger-cases.json +30 -0
- package/skills/corporate-governance/trigger-cases.json +30 -0
- package/skills/crawl-chain/trigger-cases.json +31 -0
- package/skills/customer-service/trigger-cases.json +30 -0
- package/skills/data-analysis/trigger-cases.json +30 -0
- package/skills/data-chain/SKILL.md +1 -1
- package/skills/data-chain/trigger-cases.json +31 -0
- package/skills/data-governance/trigger-cases.json +30 -0
- package/skills/data-report-pro/trigger-cases.json +30 -0
- package/skills/data-streaming/trigger-cases.json +30 -0
- package/skills/data-wrangling/SKILL.md +1 -1
- package/skills/data-wrangling/trigger-cases.json +30 -0
- package/skills/db-es/trigger-cases.json +30 -0
- package/skills/db-migration/trigger-cases.json +30 -0
- package/skills/db-mysql/trigger-cases.json +30 -0
- package/skills/db-postgres/trigger-cases.json +30 -0
- package/skills/db-redis/trigger-cases.json +30 -0
- package/skills/debug-root-cause/trigger-cases.json +30 -0
- package/skills/dependency-audit/SKILL.md +56 -0
- package/skills/dependency-audit/trigger-cases.json +7 -0
- package/skills/dependency-upgrade/trigger-cases.json +31 -0
- package/skills/dev-cpp/trigger-cases.json +30 -0
- package/skills/dev-cross-platform/trigger-cases.json +30 -0
- package/skills/dev-csharp/SKILL.md +23 -0
- package/skills/dev-csharp/trigger-cases.json +30 -0
- package/skills/dev-frontend/SKILL.md +19 -0
- package/skills/dev-frontend/trigger-cases.json +30 -0
- package/skills/dev-go/trigger-cases.json +30 -0
- package/skills/dev-harmony/trigger-cases.json +30 -0
- package/skills/dev-java/SKILL.md +20 -0
- package/skills/dev-java/trigger-cases.json +30 -0
- package/skills/dev-mobile/trigger-cases.json +30 -0
- package/skills/dev-node/trigger-cases.json +30 -0
- package/skills/dev-php/trigger-cases.json +30 -0
- package/skills/dev-python/SKILL.md +21 -0
- package/skills/dev-python/trigger-cases.json +30 -0
- package/skills/dev-rust/trigger-cases.json +30 -0
- package/skills/dev-toolchain/trigger-cases.json +31 -0
- package/skills/doc-chain/trigger-cases.json +31 -0
- package/skills/doc-compare/trigger-cases.json +30 -0
- package/skills/docx-author/SKILL.md +127 -25
- package/skills/docx-author/scripts/__pycache__/docx_battery.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_build.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_check.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_layout.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/zh_prose.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/docx_battery.py +586 -0
- package/skills/docx-author/scripts/docx_build.py +995 -0
- package/skills/docx-author/scripts/docx_check.py +1306 -0
- package/skills/docx-author/scripts/docx_doc.py +14 -154
- package/skills/docx-author/scripts/docx_layout.py +490 -0
- package/skills/docx-author/scripts/zh_prose.py +639 -0
- package/skills/docx-author/trigger-cases.json +58 -0
- package/skills/echo-brief/SKILL.md +84 -0
- package/skills/echo-brief/references/echo-format.md +68 -0
- package/skills/echo-brief/trigger-cases.json +40 -0
- package/skills/ecommerce-ops/trigger-cases.json +30 -0
- package/skills/education-training/trigger-cases.json +30 -0
- package/skills/ehs-safety/trigger-cases.json +30 -0
- package/skills/email-writing/trigger-cases.json +30 -0
- package/skills/eng-ai/trigger-cases.json +30 -0
- package/skills/eng-architecture/trigger-cases.json +31 -0
- package/skills/eng-automation/trigger-cases.json +31 -0
- package/skills/eng-collaboration/trigger-cases.json +30 -0
- package/skills/eng-complex-env/trigger-cases.json +30 -0
- package/skills/eng-data-platform/trigger-cases.json +31 -0
- package/skills/eng-database/trigger-cases.json +31 -0
- package/skills/eng-develop/trigger-cases.json +30 -0
- package/skills/eng-docs/SKILL.md +24 -0
- package/skills/eng-docs/trigger-cases.json +30 -0
- package/skills/eng-efficiency/trigger-cases.json +30 -0
- package/skills/eng-infra/trigger-cases.json +30 -0
- package/skills/eng-integration/trigger-cases.json +31 -0
- package/skills/eng-legacy/trigger-cases.json +31 -0
- package/skills/eng-observability/trigger-cases.json +31 -0
- package/skills/eng-ops/trigger-cases.json +31 -0
- package/skills/eng-performance/trigger-cases.json +31 -0
- package/skills/eng-planning/trigger-cases.json +31 -0
- package/skills/eng-quality/SKILL.md +8 -0
- package/skills/eng-quality/trigger-cases.json +31 -0
- package/skills/eng-release/trigger-cases.json +31 -0
- package/skills/eng-reliability/trigger-cases.json +31 -0
- package/skills/eng-requirements/trigger-cases.json +12 -0
- package/skills/eng-research/trigger-cases.json +30 -0
- package/skills/eng-security/trigger-cases.json +31 -0
- package/skills/eng-ux/trigger-cases.json +30 -0
- package/skills/engineering-chain/trigger-cases.json +30 -0
- package/skills/env-offline/trigger-cases.json +31 -0
- package/skills/env-private-deploy/trigger-cases.json +31 -0
- package/skills/env-xinchuang/trigger-cases.json +31 -0
- package/skills/equipment-tpm/trigger-cases.json +30 -0
- package/skills/esg-report/trigger-cases.json +30 -0
- package/skills/event-management/trigger-cases.json +30 -0
- package/skills/excel-analysis-chain/SKILL.md +17 -4
- package/skills/excel-analysis-chain/trigger-cases.json +31 -0
- package/skills/explain-codebase/trigger-cases.json +11 -0
- package/skills/file-ingest/trigger-cases.json +30 -0
- package/skills/finance-analysis/trigger-cases.json +31 -0
- package/skills/finance-chain/trigger-cases.json +31 -0
- package/skills/financial-modeling/trigger-cases.json +31 -0
- package/skills/find-simplifications/SKILL.md +43 -0
- package/skills/find-simplifications/trigger-cases.json +23 -0
- package/skills/foreign-trade/trigger-cases.json +30 -0
- package/skills/funnel-cohort/trigger-cases.json +30 -0
- package/skills/gov-document/trigger-cases.json +30 -0
- package/skills/gui-change-evidence/SKILL.md +47 -0
- package/skills/gui-change-evidence/trigger-cases.json +23 -0
- package/skills/hr-chain/trigger-cases.json +31 -0
- package/skills/hr-operations/trigger-cases.json +30 -0
- package/skills/info-disclosure/trigger-cases.json +31 -0
- package/skills/infra-gateway/trigger-cases.json +31 -0
- package/skills/infra-k8s/trigger-cases.json +31 -0
- package/skills/infra-mq/trigger-cases.json +30 -0
- package/skills/insurance-advisory/trigger-cases.json +30 -0
- package/skills/internal-audit/trigger-cases.json +30 -0
- package/skills/ip-management/trigger-cases.json +31 -0
- package/skills/ipo-listing/trigger-cases.json +31 -0
- package/skills/it-audit/trigger-cases.json +31 -0
- package/skills/itsm-ops/trigger-cases.json +31 -0
- package/skills/knowledge-sop/trigger-cases.json +31 -0
- package/skills/legal-chain/trigger-cases.json +31 -0
- package/skills/legal-contract/trigger-cases.json +30 -0
- package/skills/litigation-support/trigger-cases.json +31 -0
- package/skills/logistics-warehouse/trigger-cases.json +30 -0
- package/skills/long-doc-analysis/trigger-cases.json +31 -0
- package/skills/management-accounting/trigger-cases.json +30 -0
- package/skills/manufacturing-chain/trigger-cases.json +31 -0
- package/skills/manufacturing-quality/trigger-cases.json +30 -0
- package/skills/marketing-analytics/trigger-cases.json +31 -0
- package/skills/marketing-chain/trigger-cases.json +31 -0
- package/skills/meeting-minutes/trigger-cases.json +30 -0
- package/skills/metrics-system/trigger-cases.json +31 -0
- package/skills/negotiation-prep/trigger-cases.json +30 -0
- package/skills/obs-prometheus/trigger-cases.json +31 -0
- package/skills/office-automation/trigger-cases.json +31 -0
- package/skills/okr-review/trigger-cases.json +30 -0
- package/skills/ops-fleet/trigger-cases.json +31 -0
- package/skills/ops-linux/trigger-cases.json +31 -0
- package/skills/optimization-solver/trigger-cases.json +30 -0
- package/skills/patent-disclosure/SKILL.md +105 -0
- package/skills/patent-disclosure/references/figures.md +110 -0
- package/skills/patent-disclosure/references/outline.md +162 -0
- package/skills/patent-disclosure/scripts/patent_docx.py +230 -0
- package/skills/patent-disclosure/trigger-cases.json +31 -0
- package/skills/patent-drafting/SKILL.md +108 -0
- package/skills/patent-drafting/trigger-cases.json +31 -0
- package/skills/patent-portfolio/SKILL.md +81 -0
- package/skills/patent-portfolio/trigger-cases.json +31 -0
- package/skills/pdf-toolkit/trigger-cases.json +31 -0
- package/skills/perf-optimize/trigger-cases.json +31 -0
- package/skills/pivot-crosstab/trigger-cases.json +30 -0
- package/skills/policy-analysis/trigger-cases.json +31 -0
- package/skills/portfolio-analysis/trigger-cases.json +31 -0
- package/skills/ppt-chain/SKILL.md +14 -6
- package/skills/ppt-chain/trigger-cases.json +31 -0
- package/skills/pptx-author/SKILL.md +86 -33
- package/skills/pptx-author/scripts/__pycache__/pptx_check.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/__pycache__/pptx_deck.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/__pycache__/pptx_layout.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/pptx_battery.py +178 -0
- package/skills/pptx-author/scripts/pptx_build.py +119 -0
- package/skills/pptx-author/scripts/pptx_check.py +1182 -0
- package/skills/pptx-author/scripts/pptx_deck.py +736 -145
- package/skills/pptx-author/scripts/pptx_layout.py +384 -0
- package/skills/pptx-author/trigger-cases.json +15 -0
- package/skills/pr-comms/trigger-cases.json +30 -0
- package/skills/procurement-supply/trigger-cases.json +31 -0
- package/skills/product-design/trigger-cases.json +31 -0
- package/skills/production-planning/trigger-cases.json +31 -0
- package/skills/project-management/trigger-cases.json +31 -0
- package/skills/project-proposal/trigger-cases.json +31 -0
- package/skills/prose-standard/SKILL.md +53 -0
- package/skills/prose-standard/trigger-cases.json +23 -0
- package/skills/qa-api-automation/trigger-cases.json +31 -0
- package/skills/qa-mobile-automation/trigger-cases.json +31 -0
- package/skills/qa-ui-automation/trigger-cases.json +31 -0
- package/skills/quant-analysis/trigger-cases.json +31 -0
- package/skills/rag-triage/SKILL.md +64 -0
- package/skills/rag-triage/evals.json +57 -0
- package/skills/rag-triage/references/triage-playbook.md +75 -0
- package/skills/rag-triage/trigger-cases.json +31 -0
- package/skills/real-estate-property/trigger-cases.json +30 -0
- package/skills/refactor-safely/SKILL.md +1 -1
- package/skills/refactor-safely/trigger-cases.json +30 -0
- package/skills/regression-forecast/trigger-cases.json +31 -0
- package/skills/release-checklist/trigger-cases.json +31 -0
- package/skills/report-craft/SKILL.md +76 -0
- package/skills/report-craft/references/feasibility.md +118 -0
- package/skills/report-craft/references/review-checklist.md +51 -0
- package/skills/report-craft/references/visuals.md +114 -0
- package/skills/report-craft/references/writing.md +80 -0
- package/skills/report-craft/scripts/__pycache__/chart_render.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/__pycache__/fin_model.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/__pycache__/project_facts.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/chart_render.py +1222 -0
- package/skills/report-craft/scripts/fin_model.py +602 -0
- package/skills/report-craft/scripts/project_facts.py +580 -0
- package/skills/report-craft/scripts/report_battery.py +349 -0
- package/skills/report-craft/trigger-cases.json +15 -0
- package/skills/retail-store-ops/trigger-cases.json +30 -0
- package/skills/risk-compliance/trigger-cases.json +31 -0
- package/skills/scope-creep/SKILL.md +45 -0
- package/skills/scope-creep/trigger-cases.json +7 -0
- package/skills/sec-auth/trigger-cases.json +31 -0
- package/skills/sec-compliance/trigger-cases.json +31 -0
- package/skills/securities-chain/trigger-cases.json +31 -0
- package/skills/securities-research/trigger-cases.json +31 -0
- package/skills/security-audit/trigger-cases.json +31 -0
- package/skills/slides-html/SKILL.md +25 -10
- package/skills/slides-html/trigger-cases.json +11 -0
- package/skills/sql-analysis/SKILL.md +110 -0
- package/skills/sql-analysis/trigger-cases.json +34 -0
- package/skills/sql-tuning/trigger-cases.json +31 -0
- package/skills/statistical-testing/trigger-cases.json +31 -0
- package/skills/strategy-planning/trigger-cases.json +30 -0
- package/skills/supply-chain/trigger-cases.json +31 -0
- package/skills/tax-compliance/trigger-cases.json +30 -0
- package/skills/tech-doc/SKILL.md +262 -0
- package/skills/tech-doc/scripts/__pycache__/archive_battery.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_build.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_check.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_spec.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/doc_templates.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/archive_battery.py +860 -0
- package/skills/tech-doc/scripts/archive_build.py +599 -0
- package/skills/tech-doc/scripts/archive_check.py +1307 -0
- package/skills/tech-doc/scripts/archive_spec.py +499 -0
- package/skills/tech-doc/scripts/doc_templates.py +624 -0
- package/skills/tech-doc/trigger-cases.json +57 -0
- package/skills/tech-research/trigger-cases.json +31 -0
- package/skills/tender-bidding/trigger-cases.json +31 -0
- package/skills/translation-pro/trigger-cases.json +30 -0
- package/skills/treasury-mgmt/trigger-cases.json +31 -0
- package/skills/trim-cot-leakage/SKILL.md +58 -0
- package/skills/trim-cot-leakage/trigger-cases.json +23 -0
- package/skills/web-craft/SKILL.md +103 -0
- package/skills/web-craft/references/craft-checklist.md +75 -0
- package/skills/web-craft/references/design-direction.md +122 -0
- package/skills/web-craft/references/h5-mobile.md +100 -0
- package/skills/web-craft/references/motion.md +136 -0
- package/skills/web-craft/references/recipes.md +74 -0
- package/skills/web-craft/templates/wedding-invitation.html +1180 -0
- package/skills/web-craft/trigger-cases.json +14 -0
- package/skills/web-scraping/trigger-cases.json +34 -0
- package/skills/web-search/SKILL.md +11 -10
- package/skills/web-search/trigger-cases.json +31 -0
- package/skills/weekly-report/SKILL.md +134 -0
- package/skills/weekly-report/trigger-cases.json +11 -0
- package/skills/write-tests/trigger-cases.json +31 -0
- package/skills/xlsx-analysis/SKILL.md +143 -77
- package/skills/xlsx-analysis/scripts/__pycache__/data_profile.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_build.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_check.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_layout.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_toolkit.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/data_profile.py +391 -0
- package/skills/xlsx-analysis/scripts/xlsx_battery.py +389 -0
- package/skills/xlsx-analysis/scripts/xlsx_build.py +522 -0
- package/skills/xlsx-analysis/scripts/xlsx_check.py +924 -0
- package/skills/xlsx-analysis/scripts/xlsx_layout.py +235 -0
- package/skills/xlsx-analysis/scripts/xlsx_style.py +28 -66
- package/skills/xlsx-analysis/scripts/xlsx_toolkit.py +61 -5
- package/skills/xlsx-analysis/trigger-cases.json +59 -0
- package/squads/hc-defense.md +47 -0
- package/squads/hc-rdarchive.md +110 -0
- package/squads/hc-redteam.md +50 -0
- package/squads/hc-weekly.md +125 -0
- package/src/account.js +1 -0
- package/src/acp.js +1 -0
- package/src/advisor.js +1 -1
- package/src/agent.js +1 -1
- package/src/api.js +1 -1
- package/src/archaeology.js +1 -0
- package/src/ask-ui.js +1 -0
- package/src/assets-import.js +1 -1
- package/src/autofix.js +1 -1
- package/src/automation-tools.js +1 -0
- package/src/automation.js +1 -1
- package/src/autorun.js +1 -1
- package/src/banner.js +1 -1
- package/src/capability-probe.js +1 -0
- package/src/checkpoints.js +1 -1
- package/src/code-tools.js +1 -0
- package/src/codeindex.js +1 -1
- package/src/codemode.js +1 -0
- package/src/command-bridge.js +1 -0
- package/src/command-ui.js +1 -0
- package/src/commands.js +1 -1
- package/src/compaction.js +1 -1
- package/src/computer.js +1 -0
- package/src/config.js +1 -1
- package/src/consult.js +1 -1
- package/src/context-report.js +1 -1
- package/src/context-window.js +1 -0
- package/src/context.js +1 -1
- package/src/crew.js +1 -0
- package/src/crush.js +1 -0
- package/src/daemon.js +1 -1
- package/src/decisions.js +1 -1
- package/src/depaudit.js +1 -0
- package/src/device.js +1 -1
- package/src/diagram-draw.js +1 -0
- package/src/diagram-layouts.js +1 -0
- package/src/diagram-raster.js +1 -0
- package/src/diagram.js +1 -0
- package/src/dictation.js +1 -0
- package/src/diffview.js +1 -1
- package/src/doctor.js +1 -1
- package/src/docx-tools.js +1 -0
- package/src/errkind.js +1 -0
- package/src/evidence.js +1 -0
- package/src/executors.js +1 -1
- package/src/expert-catalog.js +1 -1
- package/src/experts-store.js +1 -1
- package/src/experts.js +1 -1
- package/src/external-agents.js +1 -0
- package/src/forks.js +1 -1
- package/src/fs-observe.js +1 -0
- package/src/gitflow.js +1 -1
- package/src/goalround.js +1 -0
- package/src/hooks.js +1 -1
- package/src/input.js +1 -1
- package/src/insights.js +1 -1
- package/src/jobs.js +1 -0
- package/src/kbsync.js +1 -1
- package/src/lsp.js +1 -0
- package/src/mcp-apps.js +1 -0
- package/src/mcp.js +1 -1
- package/src/memory.js +1 -1
- package/src/notify.js +1 -0
- package/src/permissions.js +1 -1
- package/src/plan-limits.js +1 -0
- package/src/policy.js +1 -0
- package/src/pptx-tools.js +1 -0
- package/src/presence.js +1 -0
- package/src/privatedir.js +1 -0
- package/src/pyruntime.js +1 -1
- package/src/radar.js +1 -0
- package/src/render.js +1 -1
- package/src/repl.js +1 -1
- package/src/report-tools.js +1 -0
- package/src/routing.js +1 -0
- package/src/sandbox-probe.js +1 -0
- package/src/sandbox-win-launcher.mjs +156 -0
- package/src/sandbox.js +1 -0
- package/src/scan.js +1 -1
- package/src/schedule.js +1 -1
- package/src/scopecreep.js +1 -0
- package/src/sdk.js +1 -0
- package/src/selection.js +1 -0
- package/src/session-events.js +1 -0
- package/src/session.js +1 -1
- package/src/sessionindex.js +1 -0
- package/src/sessionlog.js +1 -0
- package/src/skill-catalog.js +1 -0
- package/src/skill-lint.js +1 -0
- package/src/skill-optimize.js +1 -0
- package/src/skill-scan.js +1 -0
- package/src/skills.js +1 -1
- package/src/spill.js +1 -0
- package/src/squad.js +1 -1
- package/src/subagent.js +1 -1
- package/src/surface-state.js +1 -0
- package/src/tapd.js +1 -0
- package/src/terminal-select.js +1 -0
- package/src/terminal.js +1 -0
- package/src/test-runner.js +1 -0
- package/src/todo.js +1 -1
- package/src/toolpipe.js +1 -0
- package/src/tools.js +1 -1
- package/src/toon.js +1 -0
- package/src/transcript.js +1 -1
- package/src/tui/app.js +1 -1
- package/src/tui/command-menu.js +1 -0
- package/src/tui/progress.js +1 -0
- package/src/tui/scroll.js +1 -0
- package/src/tui/welcome.js +1 -0
- package/src/tui/workbench.js +1 -0
- package/src/ui-spec.js +1 -0
- package/src/uitree.js +1 -0
- package/src/uninstall.js +1 -1
- package/src/vision.js +1 -0
- package/src/visual.js +1 -0
- package/src/voice-form.js +1 -0
- package/src/voice.js +1 -0
- package/src/watch.js +1 -1
- package/src/web-audit.js +1 -0
- package/src/web-cdp.js +1 -0
- package/src/web-inspect.js +1 -0
- package/src/websearch.js +1 -0
- package/src/weekly.js +1 -0
- package/src/workflow.js +1 -0
- package/src/worklog.js +1 -0
- package/src/xlsx-tools.js +1 -0
- package/ui/command-picker.js +79 -0
- package/skills/ab-testing/scripts/__pycache__/ab_test.cpython-312.pyc +0 -0
- package/skills/accounting-ops/scripts/__pycache__/acct_tools.cpython-312.pyc +0 -0
- package/skills/anomaly-detection/scripts/__pycache__/anomaly.cpython-312.pyc +0 -0
- package/skills/audit-assist/scripts/__pycache__/audit_sampling.cpython-312.pyc +0 -0
- package/skills/banking-credit/scripts/__pycache__/credit_metrics.cpython-312.pyc +0 -0
- package/skills/data-report-pro/scripts/__pycache__/bigdata.cpython-312.pyc +0 -0
- package/skills/data-wrangling/scripts/__pycache__/join_tools.cpython-312.pyc +0 -0
- package/skills/ecommerce-ops/scripts/__pycache__/gmv_decompose.cpython-312.pyc +0 -0
- package/skills/funnel-cohort/scripts/__pycache__/funnel.cpython-312.pyc +0 -0
- package/skills/management-accounting/scripts/__pycache__/costing.cpython-312.pyc +0 -0
- package/skills/manufacturing-quality/scripts/__pycache__/spc.cpython-312.pyc +0 -0
- package/skills/marketing-analytics/scripts/__pycache__/rfm.cpython-312.pyc +0 -0
- package/skills/optimization-solver/scripts/__pycache__/solver.cpython-312.pyc +0 -0
- package/skills/pivot-crosstab/scripts/__pycache__/pivot_tools.cpython-312.pyc +0 -0
- package/skills/portfolio-analysis/scripts/__pycache__/portfolio.cpython-312.pyc +0 -0
- package/skills/procurement-supply/scripts/__pycache__/supply.cpython-312.pyc +0 -0
- package/skills/production-planning/scripts/__pycache__/mrp_capacity.cpython-312.pyc +0 -0
- package/skills/project-management/scripts/__pycache__/cpm.cpython-312.pyc +0 -0
- package/skills/quant-analysis/scripts/__pycache__/quant.cpython-312.pyc +0 -0
- package/skills/real-estate-property/scripts/__pycache__/property_calc.cpython-312.pyc +0 -0
- package/skills/regression-forecast/scripts/__pycache__/regress.cpython-312.pyc +0 -0
- package/skills/securities-research/scripts/__pycache__/valuation.cpython-312.pyc +0 -0
- package/skills/statistical-testing/scripts/__pycache__/stat_tests.cpython-312.pyc +0 -0
- package/skills/treasury-mgmt/scripts/__pycache__/cashflow_forecast.cpython-312.pyc +0 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# 项目事实清单:扫一遍代码库,给「可研 / 概要设计 / 项目总结 / 结题报告」这类文档取证。只用标准库。
|
|
3
|
+
#
|
|
4
|
+
# 为什么要有它:写项目自己的报告时,模型最容易做的是凭印象报数——「微服务若干个」「代码约十万行」
|
|
5
|
+
# 「支持数十种模型」。评审一问来源就露馅,前后章节报的数还对不上。这里把能从代码里数出来的事实一次数清,
|
|
6
|
+
# 每条都带来源路径,正文引用时写得出「据 huicai-backend/pom.xml」。
|
|
7
|
+
#
|
|
8
|
+
# 纪律:
|
|
9
|
+
# · 只出计数、名称、版本、端口和来源路径;**配置值一律不读出**(application.yml 里常有默认口令、密钥),
|
|
10
|
+
# 端口与服务名走白名单键;compose 只取服务名、镜像、端口映射,环境变量一个字都不出;
|
|
11
|
+
# · 同步副本只算一次:多端工程常把同一份核心代码拷到几个子工程里,按内容哈希去重,
|
|
12
|
+
# 否则代码行数会被重复计入好几倍;
|
|
13
|
+
# · 人名不出:git 只报提交数、起止日期与贡献者**人数**。
|
|
14
|
+
#
|
|
15
|
+
# 用法:python project_facts.py <根目录> 最后一行输出 JSON(含 markdown 事实表与模块树 spec)。
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import xml.etree.ElementTree as ET
|
|
24
|
+
|
|
25
|
+
SKIP_DIRS = {
|
|
26
|
+
'.git', '.svn', '.hg', 'node_modules', 'target', 'dist', 'build', 'out', '.next', '.nuxt', 'coverage',
|
|
27
|
+
'.venv', 'venv', 'env', '__pycache__', '.idea', '.vscode', '.gradle', 'vendor', '.cache', '.turbo',
|
|
28
|
+
'.parcel-cache', '.huicai', '.pytest_cache', '.mypy_cache', 'bower_components', '.terraform', 'Pods',
|
|
29
|
+
'release', 'releases', '.output', 'tmp', '.tmp', 'logs',
|
|
30
|
+
}
|
|
31
|
+
LANGS = {
|
|
32
|
+
'.java': 'Java', '.kt': 'Kotlin', '.kts': 'Kotlin', '.scala': 'Scala', '.groovy': 'Groovy',
|
|
33
|
+
'.js': 'JavaScript', '.mjs': 'JavaScript', '.cjs': 'JavaScript', '.jsx': 'JavaScript',
|
|
34
|
+
'.ts': 'TypeScript', '.tsx': 'TypeScript', '.mts': 'TypeScript', '.vue': 'Vue', '.svelte': 'Svelte',
|
|
35
|
+
'.py': 'Python', '.go': 'Go', '.rs': 'Rust', '.cs': 'C#', '.c': 'C/C++', '.cc': 'C/C++', '.cpp': 'C/C++',
|
|
36
|
+
'.h': 'C/C++', '.hpp': 'C/C++', '.swift': 'Swift', '.m': 'Objective-C', '.php': 'PHP', '.rb': 'Ruby',
|
|
37
|
+
'.dart': 'Dart', '.lua': 'Lua', '.sql': 'SQL', '.sh': 'Shell', '.bash': 'Shell', '.ps1': 'PowerShell',
|
|
38
|
+
'.css': 'CSS/样式', '.scss': 'CSS/样式', '.less': 'CSS/样式', '.html': 'HTML',
|
|
39
|
+
}
|
|
40
|
+
MAX_FILE = 2 * 1024 * 1024
|
|
41
|
+
MAX_FILES = 120000
|
|
42
|
+
|
|
43
|
+
TEST_PATH = re.compile(r'(^|/)(test|tests|__tests__|spec|e2e)(/|$)', re.I)
|
|
44
|
+
TEST_NAME = re.compile(r'(Tests?\.(java|kt|cs)$|\.(test|spec)\.[cm]?[jt]sx?$|^test_.*\.py$|_test\.(py|go)$)')
|
|
45
|
+
CASE_PAT = {
|
|
46
|
+
'Java': re.compile(r'@(Test|ParameterizedTest)\b'), 'Kotlin': re.compile(r'@Test\b'),
|
|
47
|
+
'JavaScript': re.compile(r'^\s*(?:it|test)(?:\.each\([^)]*\))?\s*\(', re.M),
|
|
48
|
+
'TypeScript': re.compile(r'^\s*(?:it|test)(?:\.each\([^)]*\))?\s*\(', re.M),
|
|
49
|
+
'Python': re.compile(r'^\s*(?:async\s+)?def\s+test_\w*\s*\(', re.M), 'Go': re.compile(r'^func\s+Test\w*\(', re.M),
|
|
50
|
+
'C#': re.compile(r'\[(Fact|Theory|Test|TestMethod)\b'),
|
|
51
|
+
}
|
|
52
|
+
JAVA_MAP = re.compile(r'@(Get|Post|Put|Delete|Patch)Mapping\b|@RequestMapping\s*\([^)]*method\s*=')
|
|
53
|
+
JS_ROUTE = re.compile(r'\b(?:app|router|server|fastify)\.(?:get|post|put|delete|patch)\s*\(\s*[\'"`]/')
|
|
54
|
+
PY_ROUTE = re.compile(r'@\w+\.(?:get|post|put|delete|patch|route)\s*\(\s*[\'"]/')
|
|
55
|
+
CREATE_TABLE = re.compile(r'\bcreate\s+(?:unlogged\s+|temporary\s+|temp\s+)?table\s+(?:if\s+not\s+exists\s+)?'
|
|
56
|
+
r'((?:[`"\[]?\w+[`"\]]?\.)?[`"\[]?\w+[`"\]]?)', re.I)
|
|
57
|
+
ROUTE_PATH = re.compile(r'\bpath\s*:\s*[\'"`]')
|
|
58
|
+
|
|
59
|
+
FRAMEWORKS_JS = {
|
|
60
|
+
'vue': 'Vue', 'react': 'React', 'electron': 'Electron', 'vite': 'Vite', 'element-plus': 'Element Plus',
|
|
61
|
+
'ant-design-vue': 'Ant Design Vue', 'antd': 'Ant Design', 'express': 'Express', 'koa': 'Koa',
|
|
62
|
+
'fastify': 'Fastify', 'typescript': 'TypeScript', 'pinia': 'Pinia', 'vuex': 'Vuex', 'vue-router': 'Vue Router',
|
|
63
|
+
'echarts': 'ECharts', 'three': 'three.js', 'axios': 'Axios', 'webpack': 'webpack', 'next': 'Next.js',
|
|
64
|
+
'nuxt': 'Nuxt', '@nestjs/core': 'NestJS', 'playwright': 'Playwright', 'jest': 'Jest', 'vitest': 'Vitest',
|
|
65
|
+
'tailwindcss': 'Tailwind CSS', 'monaco-editor': 'Monaco Editor', 'xterm': 'xterm.js', '@xterm/xterm': 'xterm.js',
|
|
66
|
+
'node-pty': 'node-pty', 'electron-builder': 'electron-builder', 'socket.io': 'Socket.IO', 'openai': 'OpenAI SDK',
|
|
67
|
+
'@modelcontextprotocol/sdk': 'MCP SDK', 'better-sqlite3': 'SQLite', 'markdown-it': 'markdown-it',
|
|
68
|
+
'ink': 'Ink(终端界面)', 'commander': 'commander', 'yargs': 'yargs',
|
|
69
|
+
}
|
|
70
|
+
FRAMEWORKS_JAVA = {
|
|
71
|
+
'spring-cloud-starter-gateway': 'Spring Cloud Gateway', 'spring-cloud-starter-openfeign': 'OpenFeign',
|
|
72
|
+
'spring-cloud-starter-alibaba-nacos-discovery': 'Nacos 注册中心', 'spring-cloud-starter-alibaba-nacos-config': 'Nacos 配置中心',
|
|
73
|
+
'spring-cloud-starter-alibaba-sentinel': 'Sentinel', 'mybatis-plus-spring-boot3-starter': 'MyBatis-Plus',
|
|
74
|
+
'mybatis-plus-boot-starter': 'MyBatis-Plus', 'spring-boot-starter-data-redis': 'Redis',
|
|
75
|
+
'postgresql': 'PostgreSQL', 'mysql-connector-j': 'MySQL', 'mysql-connector-java': 'MySQL',
|
|
76
|
+
'shardingsphere-jdbc-core': 'ShardingSphere', 'shardingsphere-jdbc': 'ShardingSphere',
|
|
77
|
+
'spring-kafka': 'Kafka', 'rocketmq-spring-boot-starter': 'RocketMQ', 'spring-boot-starter-amqp': 'RabbitMQ',
|
|
78
|
+
'elasticsearch-java': 'Elasticsearch', 'minio': 'MinIO', 'seata-spring-boot-starter': 'Seata',
|
|
79
|
+
'knife4j-openapi3-jakarta-spring-boot-starter': 'Knife4j', 'springdoc-openapi-starter-webmvc-ui': 'springdoc',
|
|
80
|
+
'spring-boot-starter-security': 'Spring Security', 'jjwt-api': 'JWT', 'spring-boot-starter-webflux': 'WebFlux',
|
|
81
|
+
'spring-ai-core': 'Spring AI', 'langchain4j': 'LangChain4j', 'pgvector': 'pgvector',
|
|
82
|
+
'spring-boot-starter-actuator': 'Actuator', 'micrometer-registry-prometheus': 'Prometheus',
|
|
83
|
+
'caffeine': 'Caffeine', 'redisson-spring-boot-starter': 'Redisson', 'xxl-job-core': 'XXL-JOB',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def rel(root, p):
|
|
88
|
+
return os.path.relpath(p, root).replace(os.sep, '/')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def read_text(p, limit=MAX_FILE):
|
|
92
|
+
try:
|
|
93
|
+
if os.path.getsize(p) > limit:
|
|
94
|
+
return None
|
|
95
|
+
with open(p, 'rb') as f:
|
|
96
|
+
raw = f.read()
|
|
97
|
+
if b'\0' in raw[:4096]:
|
|
98
|
+
return None
|
|
99
|
+
return raw.decode('utf-8', errors='replace')
|
|
100
|
+
except OSError:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def walk(root):
|
|
105
|
+
n = 0
|
|
106
|
+
for d, dirs, files in os.walk(root):
|
|
107
|
+
dirs[:] = sorted(x for x in dirs if x not in SKIP_DIRS and not x.startswith('.')
|
|
108
|
+
and not (x == 'node' and d.replace(os.sep, '/').endswith('/resources')))
|
|
109
|
+
for f in sorted(files):
|
|
110
|
+
n += 1
|
|
111
|
+
if n > MAX_FILES:
|
|
112
|
+
return
|
|
113
|
+
yield os.path.join(d, f)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ── 小型 YAML 读取:只取白名单键,绝不整体解析输出 ────────────────────────────
|
|
117
|
+
def yaml_scalars(text, wanted):
|
|
118
|
+
out, stack = {}, []
|
|
119
|
+
for raw in text.splitlines():
|
|
120
|
+
s = raw.strip()
|
|
121
|
+
if s == '---':
|
|
122
|
+
stack = []
|
|
123
|
+
continue
|
|
124
|
+
if not s or s.startswith('#') or s.startswith('- '):
|
|
125
|
+
continue
|
|
126
|
+
line = re.sub(r'\s+#.*$', '', raw.rstrip())
|
|
127
|
+
m = re.match(r'^(\s*)([\w.\-]+)\s*:\s*(.*)$', line)
|
|
128
|
+
if not m:
|
|
129
|
+
continue
|
|
130
|
+
indent, key, val = len(m.group(1)), m.group(2), m.group(3).strip()
|
|
131
|
+
while stack and stack[-1][0] >= indent:
|
|
132
|
+
stack.pop()
|
|
133
|
+
path = '.'.join([k for _, k in stack] + [key])
|
|
134
|
+
if val in ('', '|', '>', '|-', '>-'):
|
|
135
|
+
stack.append((indent, key))
|
|
136
|
+
elif path in wanted and path not in out:
|
|
137
|
+
out[path] = val.strip('\'"')
|
|
138
|
+
return out
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def placeholder_default(v):
|
|
142
|
+
"""${SERVER_PORT:37005} → 37005(环境变量占位取默认值);没有默认值返回原样。"""
|
|
143
|
+
m = re.fullmatch(r'\$\{[^:}]+:([^}]*)\}', v or '')
|
|
144
|
+
return m.group(1) if m else v
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def compose_services(text):
|
|
148
|
+
"""docker-compose:只取服务名、镜像、端口映射。环境变量、卷、命令一律不读。"""
|
|
149
|
+
out, in_services, svc_indent, cur, in_ports = [], False, None, None, False
|
|
150
|
+
for raw in text.splitlines():
|
|
151
|
+
if not raw.strip() or raw.strip().startswith('#'):
|
|
152
|
+
continue
|
|
153
|
+
indent = len(raw) - len(raw.lstrip(' '))
|
|
154
|
+
s = raw.strip()
|
|
155
|
+
if indent == 0:
|
|
156
|
+
in_services = s.startswith('services:')
|
|
157
|
+
cur, in_ports = None, False
|
|
158
|
+
continue
|
|
159
|
+
if not in_services:
|
|
160
|
+
continue
|
|
161
|
+
if svc_indent is None:
|
|
162
|
+
svc_indent = indent
|
|
163
|
+
if indent == svc_indent and re.match(r'^[\w.\-]+\s*:\s*$', s):
|
|
164
|
+
cur = {'name': s.rstrip(':').strip(), 'image': '', 'ports': []}
|
|
165
|
+
out.append(cur)
|
|
166
|
+
in_ports = False
|
|
167
|
+
continue
|
|
168
|
+
if cur is None:
|
|
169
|
+
continue
|
|
170
|
+
if indent > svc_indent:
|
|
171
|
+
m = re.match(r'^image\s*:\s*(.+)$', s)
|
|
172
|
+
if m:
|
|
173
|
+
cur['image'] = m.group(1).strip().strip('\'"')
|
|
174
|
+
in_ports = False
|
|
175
|
+
continue
|
|
176
|
+
if re.match(r'^ports\s*:\s*$', s):
|
|
177
|
+
in_ports = True
|
|
178
|
+
continue
|
|
179
|
+
if in_ports and s.startswith('- '):
|
|
180
|
+
cur['ports'].append(s[2:].strip().strip('\'"'))
|
|
181
|
+
continue
|
|
182
|
+
if re.match(r'^[\w.\-]+\s*:', s):
|
|
183
|
+
in_ports = False
|
|
184
|
+
return out
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def pom_info(p, root):
|
|
188
|
+
text = read_text(p)
|
|
189
|
+
if text is None:
|
|
190
|
+
return None
|
|
191
|
+
try:
|
|
192
|
+
t = ET.fromstring(text)
|
|
193
|
+
except ET.ParseError:
|
|
194
|
+
return None
|
|
195
|
+
ns = ''
|
|
196
|
+
m = re.match(r'\{(.*)\}', t.tag)
|
|
197
|
+
if m:
|
|
198
|
+
ns = '{' + m.group(1) + '}'
|
|
199
|
+
|
|
200
|
+
def g(el, path):
|
|
201
|
+
x = el.find('/'.join(ns + seg for seg in path.split('/')))
|
|
202
|
+
return x.text.strip() if x is not None and x.text else ''
|
|
203
|
+
info = {
|
|
204
|
+
'path': rel(root, os.path.dirname(p)) or '.', 'kind': 'maven', 'source': rel(root, p),
|
|
205
|
+
'artifactId': g(t, 'artifactId'), 'name': g(t, 'name'), 'description': g(t, 'description'),
|
|
206
|
+
'version': g(t, 'version') or g(t, 'parent/version'),
|
|
207
|
+
'parent': g(t, 'parent/artifactId'), 'parent_version': g(t, 'parent/version'),
|
|
208
|
+
}
|
|
209
|
+
props = t.find(ns + 'properties')
|
|
210
|
+
pv = {}
|
|
211
|
+
if props is not None:
|
|
212
|
+
for c in props:
|
|
213
|
+
k = c.tag.replace(ns, '')
|
|
214
|
+
if k in ('java.version', 'maven.compiler.source', 'spring-cloud.version', 'spring-cloud-alibaba.version',
|
|
215
|
+
'spring-boot.version', 'mybatis-plus.version', 'kotlin.version'):
|
|
216
|
+
pv[k] = (c.text or '').strip()
|
|
217
|
+
info['properties'] = pv
|
|
218
|
+
mods = []
|
|
219
|
+
for mm in re.finditer(r'<module>\s*([^<\s]+)\s*</module>[ \t]*(?:<!--\s*(.*?)\s*-->)?', text):
|
|
220
|
+
mods.append({'module': mm.group(1), 'comment': (mm.group(2) or '').strip()})
|
|
221
|
+
info['modules'] = mods
|
|
222
|
+
deps = set()
|
|
223
|
+
for dep in t.iter(ns + 'dependency'):
|
|
224
|
+
a = dep.find(ns + 'artifactId')
|
|
225
|
+
if a is not None and a.text:
|
|
226
|
+
deps.add(a.text.strip())
|
|
227
|
+
info['frameworks'] = sorted({FRAMEWORKS_JAVA[d] for d in deps if d in FRAMEWORKS_JAVA})
|
|
228
|
+
return info
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def pkg_info(p, root):
|
|
232
|
+
text = read_text(p)
|
|
233
|
+
try:
|
|
234
|
+
j = json.loads(text or '')
|
|
235
|
+
except ValueError:
|
|
236
|
+
return None
|
|
237
|
+
deps = {}
|
|
238
|
+
for k in ('dependencies', 'devDependencies', 'peerDependencies'):
|
|
239
|
+
deps.update(j.get(k) or {})
|
|
240
|
+
fw = {}
|
|
241
|
+
for name, label in FRAMEWORKS_JS.items():
|
|
242
|
+
if name in deps:
|
|
243
|
+
fw[label] = re.sub(r'^[\^~>=<\s]+', '', str(deps[name]))
|
|
244
|
+
eng = (j.get('engines') or {}).get('node')
|
|
245
|
+
return {'path': rel(root, os.path.dirname(p)) or '.', 'kind': 'npm', 'source': rel(root, p),
|
|
246
|
+
'name': j.get('name', ''), 'version': j.get('version', ''), 'description': j.get('description', ''),
|
|
247
|
+
'frameworks': fw, 'node': eng or '', 'bin': sorted((j.get('bin') or {}).keys()) if isinstance(j.get('bin'), dict) else []}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def gradle_info(p, root):
|
|
251
|
+
text = read_text(p) or ''
|
|
252
|
+
plugins = sorted(set(re.findall(r'id\s*\(?\s*["\']([\w.\-]+)["\']\s*\)?(?:\s*version\s*["\']([\w.\-]+)["\'])?', text)))
|
|
253
|
+
kv = re.search(r'kotlin\s*\(\s*["\']jvm["\']\s*\)\s*version\s*["\']([\w.\-]+)', text)
|
|
254
|
+
ver = re.search(r'^\s*version\s*=\s*["\']([^"\']+)', text, re.M)
|
|
255
|
+
grp = re.search(r'^\s*group\s*=\s*["\']([^"\']+)', text, re.M)
|
|
256
|
+
return {'path': rel(root, os.path.dirname(p)) or '.', 'kind': 'gradle', 'source': rel(root, p),
|
|
257
|
+
'name': os.path.basename(os.path.dirname(p)), 'version': ver.group(1) if ver else '',
|
|
258
|
+
'group': grp.group(1) if grp else '', 'description': '',
|
|
259
|
+
'frameworks': {pid: v for pid, v in plugins}, 'kotlin': kv.group(1) if kv else ''}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def git_stats(repo):
|
|
263
|
+
def run(*args):
|
|
264
|
+
try:
|
|
265
|
+
r = subprocess.run(['git', '-C', repo] + list(args), capture_output=True, text=True, timeout=20)
|
|
266
|
+
return r.stdout.strip() if r.returncode == 0 else ''
|
|
267
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
268
|
+
return ''
|
|
269
|
+
commits = run('rev-list', '--count', 'HEAD')
|
|
270
|
+
if not commits:
|
|
271
|
+
return None
|
|
272
|
+
first = run('log', '--reverse', '--format=%ad', '--date=short')
|
|
273
|
+
first = first.splitlines()[0] if first else ''
|
|
274
|
+
authors = run('log', '--format=%ae')
|
|
275
|
+
return {'commits': int(commits), 'first_commit': first, 'last_commit': run('log', '-1', '--format=%ad', '--date=short'),
|
|
276
|
+
'contributors': len(set(a.lower() for a in authors.splitlines() if a.strip())),
|
|
277
|
+
'tags': len([x for x in run('tag').splitlines() if x.strip()])}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def md_title(p):
|
|
281
|
+
text = read_text(p, 512 * 1024) or ''
|
|
282
|
+
for line in text.splitlines()[:40]:
|
|
283
|
+
m = re.match(r'^#\s+(.+?)\s*#*\s*$', line)
|
|
284
|
+
if m:
|
|
285
|
+
return m.group(1).strip()
|
|
286
|
+
return os.path.splitext(os.path.basename(p))[0]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def scan(root):
|
|
290
|
+
root = os.path.abspath(root)
|
|
291
|
+
if not os.path.isdir(root):
|
|
292
|
+
raise ValueError(f'目录不存在:{root}')
|
|
293
|
+
lang = {}
|
|
294
|
+
test = {'files': 0, 'loc': 0, 'cases': 0, 'by_lang': {}}
|
|
295
|
+
dup = {'files': 0, 'loc': 0}
|
|
296
|
+
seen = {}
|
|
297
|
+
api = {'total': 0, 'controllers': 0, 'by_module': {}}
|
|
298
|
+
fe = {'routes': 0, 'views': 0, 'by_project': {}}
|
|
299
|
+
tables, sql_files = {}, 0
|
|
300
|
+
manifests, services, composes, docs = [], [], [], []
|
|
301
|
+
files_seen = 0
|
|
302
|
+
truncated = False
|
|
303
|
+
|
|
304
|
+
top_of = lambda rp: rp.split('/')[0] if '/' in rp else '.'
|
|
305
|
+
|
|
306
|
+
for p in walk(root):
|
|
307
|
+
files_seen += 1
|
|
308
|
+
rp = rel(root, p)
|
|
309
|
+
base = os.path.basename(p)
|
|
310
|
+
ext = os.path.splitext(base)[1].lower()
|
|
311
|
+
# 清单文件
|
|
312
|
+
if base == 'pom.xml':
|
|
313
|
+
i = pom_info(p, root)
|
|
314
|
+
if i:
|
|
315
|
+
manifests.append(i)
|
|
316
|
+
elif base == 'package.json':
|
|
317
|
+
i = pkg_info(p, root)
|
|
318
|
+
if i:
|
|
319
|
+
manifests.append(i)
|
|
320
|
+
elif base in ('build.gradle', 'build.gradle.kts'):
|
|
321
|
+
manifests.append(gradle_info(p, root))
|
|
322
|
+
elif re.fullmatch(r'(docker-)?compose(\.[\w-]+)?\.ya?ml|docker-compose(\.[\w-]+)?\.ya?ml', base):
|
|
323
|
+
t = read_text(p) or ''
|
|
324
|
+
sv = compose_services(t)
|
|
325
|
+
if sv:
|
|
326
|
+
composes.append({'file': rp, 'services': sv})
|
|
327
|
+
elif re.fullmatch(r'(application|bootstrap)(-[\w]+)?\.(ya?ml|properties)', base) and '/src/main/resources/' in '/' + rp:
|
|
328
|
+
t = read_text(p) or ''
|
|
329
|
+
if base.endswith('.properties'):
|
|
330
|
+
kv = dict(re.findall(r'^\s*(server\.port|spring\.application\.name)\s*[=:]\s*(\S+)', t, re.M))
|
|
331
|
+
else:
|
|
332
|
+
kv = yaml_scalars(t, {'server.port', 'spring.application.name'})
|
|
333
|
+
if kv:
|
|
334
|
+
services.append({'name': placeholder_default(kv.get('spring.application.name', '')),
|
|
335
|
+
'port': placeholder_default(kv.get('server.port', '')), 'source': rp,
|
|
336
|
+
'profile': (re.search(r'-(\w+)\.', base) or [None, ''])[1]})
|
|
337
|
+
if ext == '.md':
|
|
338
|
+
if re.search(r'(^|/)docs?(/|$)', rp, re.I) or '/' not in rp:
|
|
339
|
+
docs.append({'path': rp, 'title': md_title(p)})
|
|
340
|
+
continue
|
|
341
|
+
L = LANGS.get(ext)
|
|
342
|
+
if not L:
|
|
343
|
+
continue
|
|
344
|
+
if base.endswith(('.min.js', '.min.css', '.bundle.js')) or base in ('package-lock.json',):
|
|
345
|
+
continue
|
|
346
|
+
text = read_text(p)
|
|
347
|
+
if text is None:
|
|
348
|
+
continue
|
|
349
|
+
loc = sum(1 for line in text.splitlines() if line.strip())
|
|
350
|
+
if loc and len(text) / max(1, text.count('\n') + 1) > 400: # 平均每行 400+ 字符:压缩/生成物
|
|
351
|
+
continue
|
|
352
|
+
h = hashlib.md5(text.encode('utf-8', errors='replace')).hexdigest()
|
|
353
|
+
if h in seen: # 同步副本、拷贝:只算一次
|
|
354
|
+
dup['files'] += 1
|
|
355
|
+
dup['loc'] += loc
|
|
356
|
+
continue
|
|
357
|
+
seen[h] = rp
|
|
358
|
+
st = lang.setdefault(L, {'files': 0, 'loc': 0})
|
|
359
|
+
st['files'] += 1
|
|
360
|
+
st['loc'] += loc
|
|
361
|
+
is_test = bool(TEST_PATH.search(rp) or TEST_NAME.search(base))
|
|
362
|
+
if is_test:
|
|
363
|
+
test['files'] += 1
|
|
364
|
+
test['loc'] += loc
|
|
365
|
+
pat = CASE_PAT.get(L)
|
|
366
|
+
if pat:
|
|
367
|
+
c = len(pat.findall(text))
|
|
368
|
+
test['cases'] += c
|
|
369
|
+
test['by_lang'][L] = test['by_lang'].get(L, 0) + c
|
|
370
|
+
continue
|
|
371
|
+
mod = top_of(rp)
|
|
372
|
+
if L == 'Java':
|
|
373
|
+
if '@RestController' in text or re.search(r'@Controller\b', text):
|
|
374
|
+
api['controllers'] += 1
|
|
375
|
+
n = len(JAVA_MAP.findall(text))
|
|
376
|
+
if n:
|
|
377
|
+
key = rp.split('/src/')[0] if '/src/' in rp else mod
|
|
378
|
+
api['total'] += n
|
|
379
|
+
api['by_module'][key] = api['by_module'].get(key, 0) + n
|
|
380
|
+
elif L in ('JavaScript', 'TypeScript', 'Python'):
|
|
381
|
+
n = len((PY_ROUTE if L == 'Python' else JS_ROUTE).findall(text))
|
|
382
|
+
if n:
|
|
383
|
+
api['total'] += n
|
|
384
|
+
api['by_module'][mod] = api['by_module'].get(mod, 0) + n
|
|
385
|
+
if L in ('JavaScript', 'TypeScript') and re.search(r'(^|/)(router|routes)(/|\.)', rp):
|
|
386
|
+
n = len(ROUTE_PATH.findall(text))
|
|
387
|
+
if n and ('createRouter' in text or 'VueRouter' in text or 'createBrowserRouter' in text or 'routes' in text):
|
|
388
|
+
fe['routes'] += n
|
|
389
|
+
fe['by_project'].setdefault(mod, {'routes': 0, 'views': 0})['routes'] += n
|
|
390
|
+
if ext in ('.vue', '.tsx', '.jsx') and re.search(r'(^|/)(views|pages)/', rp):
|
|
391
|
+
fe['views'] += 1
|
|
392
|
+
fe['by_project'].setdefault(mod, {'routes': 0, 'views': 0})['views'] += 1
|
|
393
|
+
if L == 'SQL':
|
|
394
|
+
sql_files += 1
|
|
395
|
+
for mt in CREATE_TABLE.finditer(text):
|
|
396
|
+
name = re.sub(r'[`"\[\]]', '', mt.group(1)).split('.')[-1].lower()
|
|
397
|
+
tables.setdefault(name, rp)
|
|
398
|
+
if files_seen >= MAX_FILES:
|
|
399
|
+
truncated = True
|
|
400
|
+
|
|
401
|
+
# git:根目录与一、二级子目录里的仓库
|
|
402
|
+
repos = []
|
|
403
|
+
cands = [root] + [os.path.join(root, d) for d in sorted(os.listdir(root)) if os.path.isdir(os.path.join(root, d))]
|
|
404
|
+
for c in cands:
|
|
405
|
+
if os.path.exists(os.path.join(c, '.git')):
|
|
406
|
+
s = git_stats(c)
|
|
407
|
+
if s:
|
|
408
|
+
s['repo'] = rel(root, c) or '.'
|
|
409
|
+
repos.append(s)
|
|
410
|
+
|
|
411
|
+
return {'root': root, 'files_scanned': files_seen, 'truncated': truncated, 'lang': lang, 'test': test, 'dup': dup,
|
|
412
|
+
'api': api, 'frontend': fe, 'tables': tables, 'sql_files': sql_files, 'manifests': manifests,
|
|
413
|
+
'services': services, 'compose': composes, 'docs': docs, 'git': repos}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def build(root):
|
|
417
|
+
S = scan(root)
|
|
418
|
+
warnings = []
|
|
419
|
+
by_lang = sorted(({'lang': k, 'files': v['files'], 'loc': v['loc']} for k, v in S['lang'].items()),
|
|
420
|
+
key=lambda x: -x['loc'])
|
|
421
|
+
total_loc = sum(x['loc'] for x in by_lang)
|
|
422
|
+
prod_loc = total_loc - S['test']['loc']
|
|
423
|
+
poms = [m for m in S['manifests'] if m['kind'] == 'maven']
|
|
424
|
+
parents = [m for m in poms if m['modules']]
|
|
425
|
+
modules = []
|
|
426
|
+
for par in parents:
|
|
427
|
+
for mm in par['modules']:
|
|
428
|
+
child = next((c for c in poms if c['path'] == (par['path'] + '/' + mm['module'] if par['path'] != '.' else mm['module'])), None)
|
|
429
|
+
desc = mm['comment'] or (child or {}).get('description') or (child or {}).get('name') or ''
|
|
430
|
+
modules.append({'parent': par['path'], 'module': mm['module'], 'description': desc,
|
|
431
|
+
'frameworks': (child or {}).get('frameworks', []),
|
|
432
|
+
'source': (child or par)['source']})
|
|
433
|
+
# 同一服务名可能有 application.yml 与 application-dev.yml 等多份,取不带 profile 的那份
|
|
434
|
+
svc = {}
|
|
435
|
+
for s in S['services']:
|
|
436
|
+
k = s['name'] or s['source']
|
|
437
|
+
if k not in svc or not s['profile']:
|
|
438
|
+
svc[k] = s
|
|
439
|
+
services = sorted(svc.values(), key=lambda s: (s['port'] or 'zzzz', s['name']))
|
|
440
|
+
top_subprojects, by_path = [], {}
|
|
441
|
+
for m in S['manifests']:
|
|
442
|
+
depth = 0 if m['path'] == '.' else m['path'].count('/') + 1
|
|
443
|
+
if depth > 1 or (m['kind'] == 'maven' and m.get('parent') and any(
|
|
444
|
+
p['path'] != m['path'] and m['path'].startswith(p['path'] + '/') for p in parents)):
|
|
445
|
+
continue
|
|
446
|
+
if m['path'] in by_path: # 同一目录既有 build.gradle 又有 package.json:合并
|
|
447
|
+
t = by_path[m['path']]
|
|
448
|
+
t['kind'] = t['kind'] + '+' + m['kind']
|
|
449
|
+
t['source'] = t['source'] + '、' + m['source']
|
|
450
|
+
t['description'] = t.get('description') or m.get('description', '')
|
|
451
|
+
fw = dict(t['frameworks']) if isinstance(t['frameworks'], dict) else {k: '' for k in t['frameworks']}
|
|
452
|
+
fw.update(m['frameworks'] if isinstance(m['frameworks'], dict) else {k: '' for k in m['frameworks']})
|
|
453
|
+
t['frameworks'] = fw
|
|
454
|
+
t['node'] = t.get('node') or m.get('node', '')
|
|
455
|
+
continue
|
|
456
|
+
m = dict(m)
|
|
457
|
+
by_path[m['path']] = m
|
|
458
|
+
top_subprojects.append(m)
|
|
459
|
+
|
|
460
|
+
# ── markdown 事实表(每条带来源)──
|
|
461
|
+
rows = []
|
|
462
|
+
|
|
463
|
+
def add(k, v, src):
|
|
464
|
+
rows.append(f'| {k} | {v} | {src} |')
|
|
465
|
+
if top_subprojects:
|
|
466
|
+
add('子工程', f'{len(top_subprojects)} 个:' + '、'.join(f'{m["path"]}({m["kind"]})' for m in top_subprojects),
|
|
467
|
+
'各子工程的 pom.xml / package.json / build.gradle')
|
|
468
|
+
if modules:
|
|
469
|
+
add('后端模块(Maven)', f'{len(modules)} 个:' + '、'.join(x['module'] for x in modules),
|
|
470
|
+
'、'.join(sorted({p['source'] for p in parents})))
|
|
471
|
+
if services:
|
|
472
|
+
add('可独立部署的服务(含端口)', f'{len(services)} 个:' + '、'.join(
|
|
473
|
+
f'{s["name"] or "?"}{("(" + s["port"] + ")") if s["port"] else ""}' for s in services),
|
|
474
|
+
'各服务 src/main/resources/application.yml(只读 server.port 与 spring.application.name)')
|
|
475
|
+
for par in parents:
|
|
476
|
+
fw = []
|
|
477
|
+
if par.get('parent') == 'spring-boot-starter-parent' and par.get('parent_version'):
|
|
478
|
+
fw.append(f'Spring Boot {par["parent_version"]}')
|
|
479
|
+
pv = par.get('properties') or {}
|
|
480
|
+
for k, lab in (('spring-cloud.version', 'Spring Cloud'), ('spring-cloud-alibaba.version', 'Spring Cloud Alibaba'),
|
|
481
|
+
('java.version', 'Java')):
|
|
482
|
+
if pv.get(k):
|
|
483
|
+
fw.append(f'{lab} {pv[k]}')
|
|
484
|
+
if fw:
|
|
485
|
+
add(f'后端技术栈({par["path"]})', '、'.join(fw), par['source'])
|
|
486
|
+
java_fw = sorted({f for m in poms for f in m.get('frameworks', [])})
|
|
487
|
+
if java_fw:
|
|
488
|
+
add('后端中间件与组件', '、'.join(java_fw), '各模块 pom.xml 依赖')
|
|
489
|
+
for m in top_subprojects:
|
|
490
|
+
if m['kind'] == 'maven' or not m['frameworks']:
|
|
491
|
+
continue
|
|
492
|
+
items = [f'{k} {v}'.strip() for k, v in list(m['frameworks'].items())[:8]]
|
|
493
|
+
if m.get('node'):
|
|
494
|
+
items.insert(0, f'Node.js {m["node"]}')
|
|
495
|
+
add(f'{m["path"]} 技术栈', '、'.join(items), m['source'])
|
|
496
|
+
add('代码规模(非空行,去重后)', f'{total_loc:,} 行,其中业务代码 {prod_loc:,} 行、测试代码 {S["test"]["loc"]:,} 行',
|
|
497
|
+
f'扫描 {S["files_scanned"]:,} 个文件')
|
|
498
|
+
add('主要语言', '、'.join(f'{x["lang"]} {x["loc"]:,} 行' for x in by_lang[:6]), '按扩展名统计')
|
|
499
|
+
if S['dup']['files']:
|
|
500
|
+
add('同步副本(未重复计入)', f'{S["dup"]["files"]:,} 个文件 / {S["dup"]["loc"]:,} 行', '内容哈希完全相同的文件')
|
|
501
|
+
if S['api']['total']:
|
|
502
|
+
add('HTTP 接口', f'约 {S["api"]["total"]:,} 个({S["api"]["controllers"]} 个控制器)',
|
|
503
|
+
'按 @Get/Post/Put/Delete/PatchMapping 与路由注册语句计数')
|
|
504
|
+
if S['frontend']['routes'] or S['frontend']['views']:
|
|
505
|
+
add('前端页面', f'路由约 {S["frontend"]["routes"]} 条、页面组件 {S["frontend"]["views"]} 个', 'router/ 与 views|pages/ 目录')
|
|
506
|
+
if S['tables']:
|
|
507
|
+
add('数据库表', f'{len(S["tables"])} 张({S["sql_files"]} 个 SQL 文件)', 'CREATE TABLE 语句去重')
|
|
508
|
+
if S['test']['files']:
|
|
509
|
+
add('自动化测试', f'{S["test"]["files"]:,} 个测试文件、约 {S["test"]["cases"]:,} 个用例', '测试目录与 *Test / *.test.* / test_*.py')
|
|
510
|
+
if S['compose']:
|
|
511
|
+
n = sum(len(c['services']) for c in S['compose'])
|
|
512
|
+
add('容器编排', f'{len(S["compose"])} 个 compose 文件、{n} 个服务定义', '、'.join(c['file'] for c in S['compose'][:4])
|
|
513
|
+
+ ('…' if len(S['compose']) > 4 else ''))
|
|
514
|
+
for g in S['git']:
|
|
515
|
+
add(f'版本库 {g["repo"]}', f'{g["commits"]:,} 次提交({g["first_commit"]} 至 {g["last_commit"]}),{g["contributors"]} 位贡献者',
|
|
516
|
+
'git log')
|
|
517
|
+
if S['docs']:
|
|
518
|
+
add('项目文档', f'{len(S["docs"])} 篇 Markdown', 'docs/ 目录与根目录')
|
|
519
|
+
md = '| 事实 | 数值 | 来源 |\n|---|---|---|\n' + '\n'.join(rows)
|
|
520
|
+
|
|
521
|
+
def short(desc, cap=18):
|
|
522
|
+
d = (desc or '').strip()
|
|
523
|
+
if ' - ' in d:
|
|
524
|
+
d = d.split(' - ')[-1]
|
|
525
|
+
d = re.split(r'[::;;。,,((]|——|—', d)[0].strip()
|
|
526
|
+
return d if len(d) <= cap else d[:cap - 1] + '…'
|
|
527
|
+
|
|
528
|
+
# ── 模块树(交给 render_diagram 的 tree 图型)──
|
|
529
|
+
name = os.path.basename(S['root'])
|
|
530
|
+
root_desc = next((p['description'] for p in parents if p['path'] == '.' and p['description']), '')
|
|
531
|
+
children = []
|
|
532
|
+
for m in top_subprojects:
|
|
533
|
+
if m['path'] == '.':
|
|
534
|
+
continue
|
|
535
|
+
node = {'name': m['path'], 'desc': short(m.get('description'))}
|
|
536
|
+
subs = [x for x in modules if x['parent'] == m['path']]
|
|
537
|
+
if subs:
|
|
538
|
+
node['children'] = [{'name': x['module'], 'desc': short(x['description'])} for x in subs]
|
|
539
|
+
children.append(node)
|
|
540
|
+
if not children and modules:
|
|
541
|
+
children = [{'name': x['module'], 'desc': short(x['description'])} for x in modules]
|
|
542
|
+
# 自左向右 + 不画标题块:这份 spec 多半要进 Word,从上往下画的宽树插进 A4 字只剩 3 磅
|
|
543
|
+
tree = {'type': 'tree', 'title': f'{name} 模块组成', 'direction': 'LR', 'header': False,
|
|
544
|
+
'root': {'name': name, 'desc': short(root_desc, 24), 'children': children}} \
|
|
545
|
+
if children else None
|
|
546
|
+
|
|
547
|
+
if S['truncated']:
|
|
548
|
+
warnings.append(f'文件数超过 {MAX_FILES:,},只统计了前 {MAX_FILES:,} 个:代码规模偏小,引用时注明「不少于」')
|
|
549
|
+
if not S['git']:
|
|
550
|
+
warnings.append('没有找到 git 仓库:研发周期、提交次数等过程数据要从别处取证')
|
|
551
|
+
if not top_subprojects:
|
|
552
|
+
warnings.append('没有识别到 pom.xml / package.json / build.gradle:技术栈要人工确认')
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
'ok': True, 'root': S['root'], 'files_scanned': S['files_scanned'],
|
|
556
|
+
'code': {'total_loc': total_loc, 'prod_loc': prod_loc, 'test_loc': S['test']['loc'], 'by_language': by_lang,
|
|
557
|
+
'duplicates': S['dup']},
|
|
558
|
+
'subprojects': [{k: v for k, v in m.items() if k in ('path', 'kind', 'name', 'version', 'description', 'frameworks', 'source')}
|
|
559
|
+
for m in top_subprojects],
|
|
560
|
+
'maven_modules': modules, 'services': services, 'compose': S['compose'][:12],
|
|
561
|
+
'api': S['api'], 'frontend': S['frontend'],
|
|
562
|
+
'database': {'tables': len(S['tables']), 'sql_files': S['sql_files'], 'names': sorted(S['tables'])[:300]},
|
|
563
|
+
'tests': S['test'], 'git': S['git'],
|
|
564
|
+
'docs': {'count': len(S['docs']), 'items': S['docs'][:150]},
|
|
565
|
+
'markdown': md, 'diagram_tree': tree, 'warnings': warnings,
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def main(argv):
|
|
570
|
+
root = argv[1] if len(argv) > 1 else '.'
|
|
571
|
+
try:
|
|
572
|
+
res = build(root)
|
|
573
|
+
except Exception as e: # 如实上报
|
|
574
|
+
res = {'ok': False, 'error': f'{type(e).__name__}: {e}'}
|
|
575
|
+
print(json.dumps(res, ensure_ascii=False))
|
|
576
|
+
return 0 if res.get('ok') else 1
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
if __name__ == '__main__':
|
|
580
|
+
sys.exit(main(sys.argv))
|