@whyshu/argent-engine 0.0.0-stage → 0.6.1
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/MANIFEST.json +341 -0
- package/engine/VERSION +1 -0
- package/engine/agents/catalog.json +454 -0
- package/engine/configs/_aliases.proposed.json +651 -0
- package/engine/configs/_migration-plan.proposed.json +899 -0
- package/engine/configs/account.json +272 -0
- package/engine/configs/advanced.json +205 -0
- package/engine/configs/host.json +149 -0
- package/engine/configs/hr.json +27 -0
- package/engine/configs/notify.json +264 -0
- package/engine/core/cli/argent.mjs +277 -0
- package/engine/core/contracts/argent.contracts.json +1065 -0
- package/engine/core/contracts/verbs.schema.json +2539 -0
- package/engine/core/lib/argent-datadir.mjs +223 -0
- package/engine/core/lib/audit.mjs +187 -0
- package/engine/core/lib/claim.mjs +258 -0
- package/engine/core/lib/config-catalog.mjs +1172 -0
- package/engine/core/lib/doctor-customer.mjs +577 -0
- package/engine/core/lib/flow-list.mjs +138 -0
- package/engine/core/lib/flow-registry.mjs +48 -0
- package/engine/core/lib/init.mjs +247 -0
- package/engine/core/lib/ledger.mjs +124 -0
- package/engine/core/lib/license-activate.mjs +813 -0
- package/engine/core/lib/license-device.mjs +314 -0
- package/engine/core/lib/license-file.mjs +349 -0
- package/engine/core/lib/license-guard.mjs +123 -0
- package/engine/core/lib/license-ops.mjs +1645 -0
- package/engine/core/lib/license-verdict.mjs +133 -0
- package/engine/core/lib/license.mjs +577 -0
- package/engine/core/lib/notify/dedup.mjs +101 -0
- package/engine/core/lib/notify/feishu.mjs +38 -0
- package/engine/core/lib/notify/index.mjs +95 -0
- package/engine/core/lib/notify/verb.mjs +327 -0
- package/engine/core/lib/notify/webhook.mjs +135 -0
- package/engine/core/lib/notify/wecom.mjs +36 -0
- package/engine/core/lib/python.mjs +145 -0
- package/engine/core/lib/sop.mjs +116 -0
- package/engine/core/lib/versions.mjs +205 -0
- package/engine/core/mcp/client.mjs +67 -0
- package/engine/core/mcp/http.mjs +138 -0
- package/engine/core/mcp/server.mjs +702 -0
- package/engine/scripts/_engine_rules_probe.py +15 -0
- package/engine/scripts/analyze-full-day.py +568 -0
- package/engine/scripts/archive/feishu-bridge.js +246 -0
- package/engine/scripts/argent-agents.mjs +527 -0
- package/engine/scripts/argent-capabilities.mjs +776 -0
- package/engine/scripts/argent-catalog.mjs +186 -0
- package/engine/scripts/argent-datadir.py +62 -0
- package/engine/scripts/argent-doctor.mjs +624 -0
- package/engine/scripts/argent-env-write.mjs +302 -0
- package/engine/scripts/argent-schedule.mjs +620 -0
- package/engine/scripts/argent-setup-check.mjs +369 -0
- package/engine/scripts/argent-upgrade.mjs +1046 -0
- package/engine/scripts/base-bootstrap.sh +576 -0
- package/engine/scripts/billing-mode-daily.sh +13 -0
- package/engine/scripts/build-engine-package.mjs +126 -0
- package/engine/scripts/capability-packs.lock.json +484 -0
- package/engine/scripts/capability_pack.py +1242 -0
- package/engine/scripts/check-1.0-criteria.mjs +355 -0
- package/engine/scripts/check-agent-catalog.mjs +141 -0
- package/engine/scripts/check-billing-mode.sh +378 -0
- package/engine/scripts/check-child-sessions.mjs +83 -0
- package/engine/scripts/check-config-catalog.mjs +677 -0
- package/engine/scripts/check-docs-drift.mjs +522 -0
- package/engine/scripts/check-dsh-web-expose.sh +98 -0
- package/engine/scripts/check-judgment-drift.mjs +403 -0
- package/engine/scripts/check-license.mjs +199 -0
- package/engine/scripts/check-model-auth.py +443 -0
- package/engine/scripts/check-pack-closure.mjs +661 -0
- package/engine/scripts/check-portable-symbols.mjs +116 -0
- package/engine/scripts/check-preset-landing.mjs +124 -0
- package/engine/scripts/check-production-home.mjs +602 -0
- package/engine/scripts/check-role-mapping.mjs +461 -0
- package/engine/scripts/check-sample-redaction.mjs +320 -0
- package/engine/scripts/check-skill-classification.mjs +188 -0
- package/engine/scripts/check-sop-consistency.mjs +431 -0
- package/engine/scripts/check-test-env-safety.mjs +627 -0
- package/engine/scripts/check-version-consistency.mjs +452 -0
- package/engine/scripts/ci-run.sh +558 -0
- package/engine/scripts/collect-evidence.mjs +42 -0
- package/engine/scripts/config-explain.mjs +278 -0
- package/engine/scripts/configure-provider.rb +29 -0
- package/engine/scripts/customer-config-migrate.mjs +179 -0
- package/engine/scripts/customer-preflight.py +892 -0
- package/engine/scripts/customer-readiness.sh +135 -0
- package/engine/scripts/daily-report-inbox.py +242 -0
- package/engine/scripts/daily-report-reconcile.mjs +312 -0
- package/engine/scripts/daily-report.mjs +1310 -0
- package/engine/scripts/daily-report.sh +47 -0
- package/engine/scripts/data/lip-product-dict.json +306 -0
- package/engine/scripts/deliver-customer.mjs +932 -0
- package/engine/scripts/delivery-faces.txt +31 -0
- package/engine/scripts/desktop-trial.sh +146 -0
- package/engine/scripts/dev-window.sh +261 -0
- package/engine/scripts/drill-clean-machine.sh +315 -0
- package/engine/scripts/dsh-api-client.mjs +215 -0
- package/engine/scripts/dsh-backup-scope.sh +83 -0
- package/engine/scripts/dsh-offline-pack.mjs +77 -0
- package/engine/scripts/dsh-overlay-pin.sh +87 -0
- package/engine/scripts/dsh-plane.mjs +153 -0
- package/engine/scripts/dsh-portforward.py +223 -0
- package/engine/scripts/dsh-preflight.mjs +179 -0
- package/engine/scripts/dsh-release-watch-daily.sh +19 -0
- package/engine/scripts/dsh-release-watch.mjs +458 -0
- package/engine/scripts/dsh-remote-check.sh +99 -0
- package/engine/scripts/dsh-restart.sh +179 -0
- package/engine/scripts/dsh-restore.sh +111 -0
- package/engine/scripts/dsh-scope-rewire.mjs +803 -0
- package/engine/scripts/dsh-snapshot.mjs +116 -0
- package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
- package/engine/scripts/dsh-upgrade.mjs +612 -0
- package/engine/scripts/dsh-url.sh +86 -0
- package/engine/scripts/dsh-web-launch.mjs +155 -0
- package/engine/scripts/ensure-store-windows.py +509 -0
- package/engine/scripts/entitlements.py +458 -0
- package/engine/scripts/env_probe.py +411 -0
- package/engine/scripts/feishu-card.mjs +252 -0
- package/engine/scripts/final-check.mjs +43 -0
- package/engine/scripts/fix-skill-frontmatter.py +209 -0
- package/engine/scripts/flow-auto-retry.py +454 -0
- package/engine/scripts/flow-requirements.json +735 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +87 -0
- package/engine/scripts/flow-timers.sh +184 -0
- package/engine/scripts/flow-trigger-wake.mjs +284 -0
- package/engine/scripts/flow-trigger.sh +571 -0
- package/engine/scripts/flow_billing.py +354 -0
- package/engine/scripts/flow_capability_stats.py +314 -0
- package/engine/scripts/flow_case.py +194 -0
- package/engine/scripts/flow_claim.py +497 -0
- package/engine/scripts/flow_config.py +1025 -0
- package/engine/scripts/flow_failure.py +588 -0
- package/engine/scripts/flow_learn.py +105 -0
- package/engine/scripts/flow_ledger.py +235 -0
- package/engine/scripts/flow_policy.py +238 -0
- package/engine/scripts/flow_preflight.py +339 -0
- package/engine/scripts/flow_registry.py +341 -0
- package/engine/scripts/flow_retry.py +304 -0
- package/engine/scripts/flow_return_to_ops.py +191 -0
- package/engine/scripts/flow_source.py +315 -0
- package/engine/scripts/flow_sources/__init__.py +53 -0
- package/engine/scripts/flow_sources/cases_json.py +106 -0
- package/engine/scripts/flow_sources/common.py +428 -0
- package/engine/scripts/flow_sources/feishu_base.py +273 -0
- package/engine/scripts/flow_stale.py +185 -0
- package/engine/scripts/flow_status_reconcile.py +344 -0
- package/engine/scripts/flow_triage.py +849 -0
- package/engine/scripts/flow_write_verify.py +496 -0
- package/engine/scripts/fulfillment.js +586 -0
- package/engine/scripts/gen-agent-bundles.mjs +276 -0
- package/engine/scripts/gen-preset-rows.mjs +259 -0
- package/engine/scripts/init-ml.sh +2278 -0
- package/engine/scripts/install-internal-role.sh +190 -0
- package/engine/scripts/install-license.mjs +641 -0
- package/engine/scripts/install-one-shot.mjs +665 -0
- package/engine/scripts/integration_contract.py +220 -0
- package/engine/scripts/integrations/wenyao/adapter.py +98 -0
- package/engine/scripts/integrations/xzh/adapter.py +75 -0
- package/engine/scripts/invoice-mapping.example.json +40 -0
- package/engine/scripts/invoice-table.sh +195 -0
- package/engine/scripts/invoice_carrier_notice.py +279 -0
- package/engine/scripts/invoice_fail_closeout.py +297 -0
- package/engine/scripts/invoice_lip_runner.py +1508 -0
- package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
- package/engine/scripts/invoice_ready_audit.py +191 -0
- package/engine/scripts/invoice_runner.py +675 -0
- package/engine/scripts/invoice_stageb.py +516 -0
- package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
- package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
- package/engine/scripts/lease.py +377 -0
- package/engine/scripts/lib/agent-install.mjs +649 -0
- package/engine/scripts/lib/argent-datadir.mjs +16 -0
- package/engine/scripts/lib/argent-datadir.sh +18 -0
- package/engine/scripts/lib/argent_datadir.py +285 -0
- package/engine/scripts/lib/argent_filelock.py +507 -0
- package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
- package/engine/scripts/lib/cdp_ports.py +197 -0
- package/engine/scripts/lib/customer-env.mjs +102 -0
- package/engine/scripts/lib/daily-report-core.mjs +517 -0
- package/engine/scripts/lib/dep-declaration.mjs +81 -0
- package/engine/scripts/lib/deps-migrate.mjs +310 -0
- package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
- package/engine/scripts/lib/dsh-scope.mjs +500 -0
- package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
- package/engine/scripts/lib/dsh-versions.mjs +96 -0
- package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
- package/engine/scripts/lib/is-direct-run.mjs +61 -0
- package/engine/scripts/lib/patch-compose.mjs +241 -0
- package/engine/scripts/lib/patch-target.mjs +35 -0
- package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
- package/engine/scripts/lib/plugin-packages.mjs +332 -0
- package/engine/scripts/lib/preset-compat.mjs +127 -0
- package/engine/scripts/lib/preset-install.mjs +575 -0
- package/engine/scripts/lib/preset-mechanism.mjs +56 -0
- package/engine/scripts/lib/preset-sync.mjs +59 -0
- package/engine/scripts/lib/profile-retire.mjs +97 -0
- package/engine/scripts/lib/python-runtime.mjs +190 -0
- package/engine/scripts/lib/registry.mjs +72 -0
- package/engine/scripts/lib/repo-root.sh +56 -0
- package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
- package/engine/scripts/lib/schedule-probe.mjs +835 -0
- package/engine/scripts/lib/scope-pointer.mjs +164 -0
- package/engine/scripts/lib/settings-panels.mjs +816 -0
- package/engine/scripts/lib/stdio_utf8.py +86 -0
- package/engine/scripts/lib/wake-auth.mjs +392 -0
- package/engine/scripts/lib/webdriver_ipc.py +264 -0
- package/engine/scripts/lib/zstd-frames.mjs +104 -0
- package/engine/scripts/license-e2e-drill.mjs +426 -0
- package/engine/scripts/license-gate.mjs +245 -0
- package/engine/scripts/lip_backlog.py +171 -0
- package/engine/scripts/lip_table.py +185 -0
- package/engine/scripts/local-usage.mjs +214 -0
- package/engine/scripts/migrate-providers.mjs +127 -0
- package/engine/scripts/migrate-skills.sh +65 -0
- package/engine/scripts/ml-oauth-setup.mjs +550 -0
- package/engine/scripts/ml-store-ids.json +26 -0
- package/engine/scripts/ml_artifact_attribution.py +333 -0
- package/engine/scripts/ml_capacity_probe.py +1158 -0
- package/engine/scripts/ml_hub_status.py +1556 -0
- package/engine/scripts/ml_schema_check.py +115 -0
- package/engine/scripts/ml_store_ids.py +139 -0
- package/engine/scripts/npm-publish-argent.mjs +499 -0
- package/engine/scripts/npm-publish-argent.sh +20 -0
- package/engine/scripts/orchestrator.py +13811 -0
- package/engine/scripts/pack-plugins.mjs +140 -0
- package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
- package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
- package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
- package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
- package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
- package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
- package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
- package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
- package/engine/scripts/patches/verify-hardening.mjs +167 -0
- package/engine/scripts/patches/verify-session-migration.mjs +149 -0
- package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
- package/engine/scripts/plan-role-rename.mjs +125 -0
- package/engine/scripts/plugin-bootcheck.mjs +451 -0
- package/engine/scripts/plugin-composition.json +15 -0
- package/engine/scripts/poc-mcp-host.mjs +455 -0
- package/engine/scripts/poll-fulfillment.sh +295 -0
- package/engine/scripts/poll-invoice.sh +76 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +71 -0
- package/engine/scripts/provider-switch.sh +146 -0
- package/engine/scripts/provider-sync-target.check.mjs +64 -0
- package/engine/scripts/python-runtime.sh +78 -0
- package/engine/scripts/query_runner.py +310 -0
- package/engine/scripts/release-argent.sh +574 -0
- package/engine/scripts/repair-unresolved-tools.mjs +178 -0
- package/engine/scripts/retire-inventory.mjs +40 -0
- package/engine/scripts/run-tests.sh +257 -0
- package/engine/scripts/s1-audit.mjs +85 -0
- package/engine/scripts/self-sign-license.mjs +179 -0
- package/engine/scripts/session-health-scan.mjs +63 -0
- package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
- package/engine/scripts/setup.sh +22 -0
- package/engine/scripts/skill-install.mjs +629 -0
- package/engine/scripts/sku_translate.py +148 -0
- package/engine/scripts/sync-plugins.sh +90 -0
- package/engine/scripts/sync-presets.sh +27 -0
- package/engine/scripts/sync-scripts.sh +445 -0
- package/engine/scripts/telemetry.py +873 -0
- package/engine/scripts/ui-copy-count.mjs +220 -0
- package/engine/scripts/usage-anchor.sh +137 -0
- package/engine/scripts/usage_report.py +531 -0
- package/engine/scripts/validate-preset-schema.mjs +208 -0
- package/engine/scripts/vertical-init.mjs +323 -0
- package/engine/scripts/vertical-lint.mjs +482 -0
- package/engine/scripts/wenyao-browser.sh +74 -0
- package/engine/scripts/wenyao-fixture-export.py +116 -0
- package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
- package/engine/scripts/wenyao-login.sh +35 -0
- package/engine/scripts/wenyao-template-map.json +292 -0
- package/engine/scripts/wenyao_attach.py +131 -0
- package/engine/scripts/wenyao_download_template.py +144 -0
- package/engine/scripts/wenyao_fill_template.py +568 -0
- package/engine/scripts/wenyao_login.py +145 -0
- package/engine/scripts/wenyao_portal.py +633 -0
- package/engine/scripts/wenyao_recon.py +230 -0
- package/engine/scripts/wenyao_run.py +503 -0
- package/engine/scripts/wenyao_shadow_diff.py +160 -0
- package/engine/scripts/wenyao_skill_contract.py +156 -0
- package/engine/scripts/wenyao_template_fetch.py +175 -0
- package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
- package/engine/scripts/ziniao-auth-probe.sh +268 -0
- package/engine/scripts/ziniao-keepalive.sh +217 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
- package/engine/scripts/ziniao-windows.sh +154 -0
- package/engine/skills/README.md +86 -0
- package/engine/skills/argent-dev/SKILL.md +41 -0
- package/engine/skills/internal.json +37 -0
- package/engine/skills/lktop-pm/SKILL.md +35 -0
- package/engine/skills/lktopapp-dev/SKILL.md +34 -0
- package/engine/skills/ml-config-guide/SKILL.md +50 -0
- package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
- package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
- package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
- package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
- package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
- package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
- package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
- package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
- package/engine/skills/ml-invoice/SKILL.md +506 -0
- package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
- package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
- package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
- package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
- package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
- package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
- package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
- package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
- package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
- package/engine/skills/pet-dev/SKILL.md +42 -0
- package/engine/skills/whyshu-pm/SKILL.md +35 -0
- package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
- package/engine/version.mjs +1 -0
- package/engine/verticals/README.md +103 -0
- package/engine/verticals/_engine-words.baseline.txt +237 -0
- package/engine/verticals/_engine-words.json +19 -0
- package/engine/verticals/mercadolibre/README.md +43 -0
- package/engine/verticals/mercadolibre/config.json +709 -0
- package/engine/verticals/mercadolibre/flows.json +738 -0
- package/engine/verticals/mercadolibre/manifest.json +538 -0
- package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
- package/engine/verticals/mercadolibre/preflight.py +143 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
- package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
- package/engine/verticals/mercadolibre/prompts/full.md +91 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
- package/package.json +22 -4
- package/requirements.txt +5 -0
- package/README.md +0 -3
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* skill-install.mjs —— **技能外发安装器**(P3,2026-10-03)
|
|
4
|
+
*
|
|
5
|
+
* ## 它解决什么(缺口⑥ / R3)
|
|
6
|
+
* "装进客户已有的 agent"这句话,今天**没有命令入口**:dsh 侧靠 preset 零配置挂载 + 升级器全量同步,
|
|
7
|
+
* 其它宿主(Claude Code / Codex / Cursor)**一个都没有**。本脚本把"按宿主约定装技能"变成一条命令,
|
|
8
|
+
* 并且**只装该装的**:外发集 = 所有 `verticals/<id>/manifest.json#skills` 的**并集**(今天 5 个流程技能)。
|
|
9
|
+
*
|
|
10
|
+
* ## 归类判据**不在这里**(唯一实现在门禁里)
|
|
11
|
+
* `classify()` 从 `scripts/check-skill-classification.mjs` **import**(不是抄一份):
|
|
12
|
+
* `skills/` 下任何既不在业务线声明、也不在 `skills/internal.json` 的目录 ⇒ 门禁红 ⇒ **本脚本拒绝安装**
|
|
13
|
+
* ("外发泄露内部角色"是本阶段唯一的高危项 ⇒ 宁可不装,不可装错)。
|
|
14
|
+
*
|
|
15
|
+
* ## 目标目录:**逐宿主实测/查证**(`HOSTS`;查不到的标 `verified` 并给 `--dir` 兜底)
|
|
16
|
+
* | 宿主 | 目录 | 证据等级 |
|
|
17
|
+
* |---|---|---|
|
|
18
|
+
* | `dsh` | `<dsh home>/skills`(缺省 `~/.dsh/skills`) | **实测 + 仓库判据**:preset 的 `skill-filesystem` 零配置挂载该根;本机该目录有 13 个技能 |
|
|
19
|
+
* | `codex` | `~/.codex/skills/` | **本机实测**:该目录存在,且里面就是 `<name>/SKILL.md` + `name`/`description` frontmatter(与本仓库同形) |
|
|
20
|
+
* | `claude-code` | `~/.claude/skills/` | **查证(官方文档)**:Claude Code 的 personal skills 目录;⚠️ 本机**无** `~/.claude` ⇒ **未实测** |
|
|
21
|
+
* | `cursor` | `~/.cursor/skills/` | **查证(官方文档 "Skill directories")**:user-level 技能目录(另有 `.agents/skills/`);⚠️ 本机**无** `~/.cursor` ⇒ **未实测** |
|
|
22
|
+
*
|
|
23
|
+
* > Cursor 官方文档另写明"为兼容也加载 `~/.claude/skills/` 与 `~/.codex/skills/`" ⇒ 装到 cursor 目录不是唯一路径。
|
|
24
|
+
* > **不确定就用 `--dir <path>` 显式指定**(本脚本的兜底,也是唯一不依赖"约定"的形态)。
|
|
25
|
+
*
|
|
26
|
+
* ## 与 dsh 的关系(§P3① 的实施约束)
|
|
27
|
+
* dsh 侧**仍走原路**(`presets/<preset>/agent.cordis.yml` 的 `skill-filesystem` 挂载 + `scripts/argent-upgrade.mjs`)
|
|
28
|
+
* ⇒ `--host dsh --apply` **被拒绝**(exit 2 + `host_owns_install`),只允许 `--list` / `--dry-run` 做**一致性校验**;
|
|
29
|
+
* 确要装到别处请用 `--host dsh --dir <path>`(显式 = 你的决定)。
|
|
30
|
+
*
|
|
31
|
+
* ## 版本记录与更新/卸载
|
|
32
|
+
* · 装完写 `<target>/.argent-skills.json`:宿主 / 目标 / Argent 版本 / 每个技能的**内容摘要**(sha256)
|
|
33
|
+
* + 逐文件 sha12 + 从 frontmatter 读到的 `version`(有则记);
|
|
34
|
+
* · 更新:与清单比对 —— **sha 一致 ⇒ `skipped`(幂等)**;不一致 ⇒ **先备份**(`<target>/.argent-skills-backup/<name>-<ts>/`)**再覆盖**;
|
|
35
|
+
* · 卸载:`--uninstall` 只删**清单里列过**的技能目录与清单本身 —— 用户自建目录**一个都不碰**。
|
|
36
|
+
*
|
|
37
|
+
* ## `--prune-orphans`:**受守卫的**孤儿技能清理(2026-10-04,客户机 18 项残留的收敛动词)
|
|
38
|
+
* 背景:`scripts/argent-upgrade.mjs` 按纪律**只报告不删**宿主技能根里"多出"的目录 ⇒ 客户机实测
|
|
39
|
+
* `~/.dsh/skills/` 里积了 6 个内部角色技能 + 12 个旧 hermes 期技能(18 项),**没有安全手段**收敛。
|
|
40
|
+
* 本动词是那个手段,但把它做成**人工确认 + 逐项点名**的动作:
|
|
41
|
+
* · **缺省 = dry-run**:只列"将删除 / 拒绝清理 / 未点名"三张清单,零副作用;
|
|
42
|
+
* · 真删要 `--apply` **且** `--allow <name>`(可重复)**逐个点名** —— **未点名的绝不删**;
|
|
43
|
+
* · **只允许删四条件同时满足的目录**(缺一即拒绝,见 `planPrune()`):
|
|
44
|
+
* ① 不在**外发集**(`verticals/<id>/manifest.json#skills` 并集);
|
|
45
|
+
* ② 不在 `skills/internal.json`(内部角色技能 = 宿主上"角色包已装"的判据);
|
|
46
|
+
* ③ 不在**安装器自建清单** `.argent-skills.json` 里(那是本脚本装的 ⇒ 该用 `--uninstall`);
|
|
47
|
+
* ④ 目录内**含 `SKILL.md`**(技能形态的证明 —— 用户自建的普通文件/目录一律不碰)。
|
|
48
|
+
* 额外拒绝:点开头的项(`.argent-skills.json` / 备份目录…本工具自管)、**软链**(删除会越界)。
|
|
49
|
+
* · **先备份再删**:`<target>/.argent-skills-pruned-<ts>/<name>/`(回滚 = `cp -R` 回去);
|
|
50
|
+
* 并写审计 JSONL `<target>/.argent-skills-prune-audit.jsonl`(追加型,一行一次动作);
|
|
51
|
+
* · `--host dsh` **仍不抢安装权**:清理默认拒绝(`host_owns_prune`),
|
|
52
|
+
* 除非给出显式 `--dir <path>`(= 你的决定)或 `--allow-host-dsh-prune`(客户机清理的入口)。
|
|
53
|
+
* · 残留风险(**必须在人这一侧兜住**):一个**用户手写的** `SKILL.md` 技能目录在形态上与旧残留
|
|
54
|
+
* **无法区分** ⇒ 唯一的护栏就是"默认 dry-run 打印清单 + 必须逐项点名"。请**逐个核对名字**再点名。
|
|
55
|
+
*
|
|
56
|
+
* ## A1/A2(与 CLI verb 同口径,便于宿主统一处置)
|
|
57
|
+
* · `0` 成功(含"成功但空":技能一个都没装 = `empty:true`);`1` 运行失败(写不下 / 目标非法 / 归类不过);
|
|
58
|
+
* · `2` 用法错误(未知宿主、未知参数、`--host dsh --apply` 未给 --dir、清单里没有可卸载项却在 `--uninstall`);
|
|
59
|
+
* · `--json` 时 stdout **只有一份 JSON**(人话一律走 stdout 的 human 形态;诊断走 stderr)。
|
|
60
|
+
*
|
|
61
|
+
* ## A3:默认动作 = **dry-run**(2026-10-03 收口;真装要显式 `--apply`)
|
|
62
|
+
* 本脚本此前**默认真装**(`--apply` 只是同义词),这与 P0-A A3"**凡 write 类必须默认 dry-run**、
|
|
63
|
+
* 真写要显式确权"直接冲突 —— 而且它写的是**别人的 agent 目录**(`~/.claude/skills` 之类),
|
|
64
|
+
* 一条 `--host` 敲下去就落盘,没有回头路。现在:
|
|
65
|
+
* · **缺省 = dry-run**(只出计划 + `willWrite` 列表,**零副作用**);
|
|
66
|
+
* · `--apply` = 真装(sha 一致 ⇒ `skipped`;不一致 ⇒ **先备份再覆盖**);
|
|
67
|
+
* · `--dry-run` 保留为**显式同义**(脚本/文档里写全更清楚,行为与缺省一致);
|
|
68
|
+
* · `--list` 行为不变(只看宿主约定目录 + 外发清单 + 目标现状,不读计划)。
|
|
69
|
+
* ⇒ 实施方案 §P3 的三条验收命令**同批补齐 `--apply`**(不带它现在只会预演)。
|
|
70
|
+
*/
|
|
71
|
+
import { appendFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
72
|
+
import { createHash } from 'node:crypto'
|
|
73
|
+
import { homedir } from 'node:os'
|
|
74
|
+
import { dirname, join, resolve, sep } from 'node:path'
|
|
75
|
+
import { fileURLToPath } from 'node:url'
|
|
76
|
+
import { isDirectRun } from './lib/is-direct-run.mjs'
|
|
77
|
+
import { classify, INTERNAL_MANIFEST, SKILL_FILE } from './check-skill-classification.mjs'
|
|
78
|
+
|
|
79
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
80
|
+
const REPO = resolve(HERE, '..')
|
|
81
|
+
|
|
82
|
+
/** 安装清单文件名(**只此一处**:更新/卸载都读它)。 */
|
|
83
|
+
export const MANIFEST_NAME = '.argent-skills.json'
|
|
84
|
+
/** 覆盖前备份目录名。 */
|
|
85
|
+
export const BACKUP_DIRNAME = '.argent-skills-backup'
|
|
86
|
+
/** 清理(`--prune-orphans`)前备份目录名**前缀**(真删前先备份 ⇒ 可回滚;目录名带时间戳)。 */
|
|
87
|
+
export const PRUNE_BACKUP_PREFIX = '.argent-skills-pruned-'
|
|
88
|
+
/** 清理审计 JSONL(**追加型**,一行一次动作;与 `.argent-skills.json` 同族命名,随目标目录走)。 */
|
|
89
|
+
export const PRUNE_AUDIT_NAME = '.argent-skills-prune-audit.jsonl'
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 宿主 → 技能目录约定(**数据表**,路径不写死在逻辑里 —— 约定会变,改这里一行)。
|
|
93
|
+
* `verified`:证据等级(实测 / 查证 / 待实测),`--list` 会**原样打给人看**(不许把"约定"说成"实测")。
|
|
94
|
+
*/
|
|
95
|
+
export function hostTable({ home = homedir(), dshHome = '' } = {}) {
|
|
96
|
+
const dsh = dshHome || join(home, '.dsh')
|
|
97
|
+
return {
|
|
98
|
+
dsh: {
|
|
99
|
+
id: 'dsh', dir: join(dsh, 'skills'), ownsInstall: true,
|
|
100
|
+
verified: '实测 + 仓库判据',
|
|
101
|
+
evidence: '`presets/*/agent.cordis.yml` 的 `skill-filesystem` 零配置挂载 `<dsh home>/skills`;本机实测该目录在册(13 个技能)',
|
|
102
|
+
},
|
|
103
|
+
'claude-code': {
|
|
104
|
+
id: 'claude-code', dir: join(home, '.claude', 'skills'), ownsInstall: false,
|
|
105
|
+
verified: '查证(官方文档);⛔ 本机无 ~/.claude ⇒ 未实测',
|
|
106
|
+
evidence: 'Claude Code 的 personal skills 目录 = `~/.claude/skills/<name>/SKILL.md`(Agent Skills 形态,与本仓库同形);Cursor 官方文档亦把该目录列为兼容载入点',
|
|
107
|
+
},
|
|
108
|
+
codex: {
|
|
109
|
+
id: 'codex', dir: join(home, '.codex', 'skills'), ownsInstall: false,
|
|
110
|
+
verified: '本机实测',
|
|
111
|
+
evidence: '本机 `~/.codex/skills/` 实际存在,内部为 `<name>/SKILL.md` + `name`/`description` frontmatter(与本仓库格式同形)',
|
|
112
|
+
},
|
|
113
|
+
cursor: {
|
|
114
|
+
id: 'cursor', dir: join(home, '.cursor', 'skills'), ownsInstall: false,
|
|
115
|
+
verified: '查证(官方文档);⛔ 本机无 ~/.cursor ⇒ 未实测',
|
|
116
|
+
evidence: 'Cursor 官方文档「Skill directories」:user-level = `~/.cursor/skills/`(另有 `~/.agents/skills/`);并注明为兼容也载入 `~/.claude/skills/`、`~/.codex/skills/`',
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export const HOST_IDS = Object.keys(hostTable())
|
|
121
|
+
|
|
122
|
+
const sha256 = (buf) => createHash('sha256').update(buf).digest('hex')
|
|
123
|
+
const sha12 = (buf) => sha256(buf).slice(0, 12)
|
|
124
|
+
|
|
125
|
+
/** 技能目录 → 逐文件 sha12(相对路径排序,**内容寻址**;改一个字节就变)。 */
|
|
126
|
+
export function skillFiles(skillDir) {
|
|
127
|
+
const files = {}
|
|
128
|
+
const walk = (dir, base = dir) => {
|
|
129
|
+
for (const e of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
130
|
+
if (e.name === '.DS_Store') continue
|
|
131
|
+
const p = join(dir, e.name)
|
|
132
|
+
const rel = p.slice(base.length + 1).split(sep).join('/')
|
|
133
|
+
if (statSync(p).isDirectory()) walk(p, base)
|
|
134
|
+
else if (statSync(p).isFile()) files[rel] = sha12(readFileSync(p))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
walk(skillDir)
|
|
138
|
+
return files
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 技能内容摘要:`路径:sha12` 排序后求 sha256(与 `capability_pack.py#pack_digest` 同形态)。 */
|
|
142
|
+
export function skillDigest(skillDir) {
|
|
143
|
+
const files = skillFiles(skillDir)
|
|
144
|
+
const lines = Object.entries(files).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([f, s]) => `${f}:${s}`)
|
|
145
|
+
return { digest: sha256(lines.join('\n')), files, bytes: lines.reduce((n, l) => n + l.length, 0) }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 从 SKILL.md 的 frontmatter 里取 `version`(有则记;**不解析/不校验 frontmatter 合法性** —— 那是 fix-skill-frontmatter.py 的职责)。 */
|
|
149
|
+
export function frontmatterVersion(text) {
|
|
150
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(String(text || ''))
|
|
151
|
+
if (!m) return null
|
|
152
|
+
const v = /^\s*version\s*:\s*(.+?)\s*$/m.exec(m[1])
|
|
153
|
+
return v ? v[1].replace(/^["']|["']$/g, '') : null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 目标目录 → 当前清单(不存在/坏文件 ⇒ null)。 */
|
|
157
|
+
export function readManifest(target) {
|
|
158
|
+
const p = join(target, MANIFEST_NAME)
|
|
159
|
+
if (!existsSync(p)) return null
|
|
160
|
+
try { return JSON.parse(readFileSync(p, 'utf8')) } catch { return { _corrupt: true, _path: p } }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 外发技能清单(来自业务线声明;**由 classify() 给出,安装器不自己读 manifest**)。 */
|
|
164
|
+
export function externalSkills(root = REPO) {
|
|
165
|
+
const c = classify({ root })
|
|
166
|
+
return { ...c, skills: c.external }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 计划:对每个外发技能算出 `install` / `skip`(sha 一致)/ `update`(不一致,先备份再覆盖)。
|
|
171
|
+
*/
|
|
172
|
+
export function planInstall({ root = REPO, target, skills }) {
|
|
173
|
+
const man = readManifest(target)
|
|
174
|
+
const prev = (man && !man._corrupt && man.skills) || {}
|
|
175
|
+
const rows = []
|
|
176
|
+
for (const name of skills) {
|
|
177
|
+
const src = join(root, 'skills', name)
|
|
178
|
+
if (!existsSync(join(src, SKILL_FILE))) { rows.push({ name, action: 'missing_source', src }); continue }
|
|
179
|
+
const cur = skillDigest(src)
|
|
180
|
+
const dst = join(target, name)
|
|
181
|
+
const exists = existsSync(dst) && existsSync(join(dst, SKILL_FILE))
|
|
182
|
+
// ⚠️ 判据用**目标目录的实际内容**(不是清单里记的摘要):客户/用户手改过、上游更新过,
|
|
183
|
+
// 都必须被认成 update(否则"改了却没生效"——那正是 §P3① 要的"sha 一致跳过、不一致先备份再覆盖")。
|
|
184
|
+
const dstDigest = exists ? skillDigest(dst).digest : null
|
|
185
|
+
const recorded = prev[name] && prev[name].digest ? prev[name].digest : null
|
|
186
|
+
let action = 'install'
|
|
187
|
+
if (exists && dstDigest === cur.digest) action = 'skip'
|
|
188
|
+
else if (exists) action = 'update'
|
|
189
|
+
rows.push({
|
|
190
|
+
name, action, src, dst, digest: cur.digest.slice(0, 16), files: Object.keys(cur.files).length,
|
|
191
|
+
targetDigest: dstDigest ? dstDigest.slice(0, 16) : null, recordedDigest: recorded ? recorded.slice(0, 16) : null,
|
|
192
|
+
targetDrift: Boolean(recorded && dstDigest && recorded !== dstDigest),
|
|
193
|
+
version: frontmatterVersion(readFileSync(join(src, SKILL_FILE), 'utf8')),
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
return { rows, manifest: man, previous: prev }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 执行安装(先备份后覆盖;幂等)。 */
|
|
200
|
+
export function applyInstall({ root = REPO, target, rows, host, argentVersion, now = new Date() }) {
|
|
201
|
+
const done = []
|
|
202
|
+
const backupRoot = join(target, BACKUP_DIRNAME)
|
|
203
|
+
const stamp = now.toISOString().replace(/[:.]/g, '-')
|
|
204
|
+
mkdirSync(target, { recursive: true })
|
|
205
|
+
const skills = {}
|
|
206
|
+
for (const r of rows) {
|
|
207
|
+
if (r.action === 'missing_source') { done.push({ ...r, ok: false, note: '源目录不存在(归类门禁本该拦住)' }); continue }
|
|
208
|
+
if (r.action === 'skip') {
|
|
209
|
+
const d = skillDigest(r.src)
|
|
210
|
+
skills[r.name] = { digest: d.digest, files: d.files, version: r.version, installedAt: (readManifest(target)?.skills?.[r.name]?.installedAt) || now.toISOString(), updatedAt: now.toISOString() }
|
|
211
|
+
done.push({ ...r, ok: true, note: 'skipped(内容 sha 一致,幂等)' })
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
let backup = null
|
|
215
|
+
if (r.action === 'update') { // 覆盖前先备份(§P3① 的硬要求)
|
|
216
|
+
backup = join(backupRoot, `${r.name}-${stamp}`)
|
|
217
|
+
mkdirSync(backup, { recursive: true })
|
|
218
|
+
cpSync(join(target, r.name), backup, { recursive: true })
|
|
219
|
+
}
|
|
220
|
+
rmSync(join(target, r.name), { recursive: true, force: true })
|
|
221
|
+
cpSync(r.src, join(target, r.name), { recursive: true })
|
|
222
|
+
const d = skillDigest(join(target, r.name))
|
|
223
|
+
const prev = readManifest(target)?.skills?.[r.name]
|
|
224
|
+
skills[r.name] = { digest: d.digest, files: d.files, version: r.version, installedAt: prev?.installedAt || now.toISOString(), updatedAt: now.toISOString() }
|
|
225
|
+
done.push({ ...r, ok: true, backup, note: r.action === 'update' ? 'updated(已先备份再覆盖)' : 'installed' })
|
|
226
|
+
}
|
|
227
|
+
// 清单:保留**不在本次外发集**里但上次装过的条目?——不保留:它们已不该外发(归类门禁会先拦),
|
|
228
|
+
// 但卸载要能清掉 ⇒ 把"上次有、这次没有"的记进 `orphans` 供人决定(不静默删用户目录)。
|
|
229
|
+
const prevAll = readManifest(target)?.skills || {}
|
|
230
|
+
const orphans = Object.keys(prevAll).filter((k) => !skills[k])
|
|
231
|
+
const manifest = {
|
|
232
|
+
_doc: '本文件由 `node scripts/skill-install.mjs` 写入:外发技能清单 + 内容摘要(更新/卸载的判据)。删除它 = 卸载器找不到自己装过什么。',
|
|
233
|
+
host, target, argent: argentVersion, updatedAt: now.toISOString(),
|
|
234
|
+
declaration: { source: 'verticals/<id>/manifest.json#skills(唯一真源;安装器不自己维护名单)' },
|
|
235
|
+
skills, orphans,
|
|
236
|
+
}
|
|
237
|
+
writeFileSync(join(target, MANIFEST_NAME), `${JSON.stringify(manifest, null, 2)}\n`)
|
|
238
|
+
return { done, manifest }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 卸载:只删清单里列过的目录 + 清单本身(用户自建目录一个都不碰)。 */
|
|
242
|
+
export function applyUninstall({ target, now = new Date() }) {
|
|
243
|
+
const man = readManifest(target)
|
|
244
|
+
if (!man) return { ok: false, reason: 'no_manifest', error: `目标目录里没有 ${MANIFEST_NAME}:${target}(没装过,或清单被删)⇒ 不敢猜哪些该删` }
|
|
245
|
+
if (man._corrupt) return { ok: false, reason: 'bad_manifest', error: `${MANIFEST_NAME} 不可解析 ⇒ 不敢猜哪些该删(人工确认后再删)` }
|
|
246
|
+
const removed = []
|
|
247
|
+
const kept = []
|
|
248
|
+
for (const name of Object.keys(man.skills || {})) {
|
|
249
|
+
const p = join(target, name)
|
|
250
|
+
if (existsSync(p)) { rmSync(p, { recursive: true, force: true }); removed.push(name) } else kept.push(`${name}(目录不在,仅清清单)`)
|
|
251
|
+
}
|
|
252
|
+
rmSync(join(target, MANIFEST_NAME), { force: true })
|
|
253
|
+
const leftovers = readdirSync(target).filter((n) => n !== BACKUP_DIRNAME)
|
|
254
|
+
return { ok: true, removed, kept, leftovers, at: now.toISOString(), backupDir: existsSync(join(target, BACKUP_DIRNAME)) ? join(target, BACKUP_DIRNAME) : null }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── 孤儿技能清理(--prune-orphans;**受守卫、默认 dry-run、逐项点名**)────────────────
|
|
258
|
+
/** SKILL.md 的 frontmatter `name`(只用于**给人看**的提示;判据不依赖它 —— 它不是可信信号)。 */
|
|
259
|
+
function frontmatterName(text) {
|
|
260
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(String(text || ''))
|
|
261
|
+
if (!m) return null
|
|
262
|
+
const v = /^\s*name\s*:\s*(.+?)\s*$/m.exec(m[1])
|
|
263
|
+
return v ? v[1].replace(/^["']|["']$/g, '') : null
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* **清理候选评估**(只读,纯路径参数)。四条件**同时**满足才进 `candidates`,缺一即进 `refused`。
|
|
268
|
+
*
|
|
269
|
+
* @param {{root?:string, target:string, allow?:string[]}} o
|
|
270
|
+
* @returns {{candidates:Array, refused:Array, notAllowed:Array, skipped:Array,
|
|
271
|
+
* namedRefused:string[], missingAllow:string[], external:string[], internal:string[],
|
|
272
|
+
* recorded:string[], targetExists:boolean}}
|
|
273
|
+
* · `candidates` = 满足四条件**且已点名**(`--allow`)⇒ 真删时会被删的那些;
|
|
274
|
+
* · `notAllowed` = 满足四条件但**没点名** ⇒ 一律不删("未点名的绝不删");
|
|
275
|
+
* · `refused` = 四条件缺一 / 点开头 / 软链 ⇒ **连点名都不删**,逐条给原因;
|
|
276
|
+
* · `namedRefused` = 你**点名了**但被拒绝(或根本不存在)的名字 ⇒ 命令无法被满足(非零退出)。
|
|
277
|
+
*/
|
|
278
|
+
export function planPrune({ root = REPO, target, allow = [] } = {}) {
|
|
279
|
+
const cls = externalSkills(root)
|
|
280
|
+
const ext = new Set(cls.external)
|
|
281
|
+
const int = new Set(cls.internal)
|
|
282
|
+
const man = readManifest(target)
|
|
283
|
+
const recorded = new Set(Object.keys((man && !man._corrupt && man.skills) || {}))
|
|
284
|
+
const allowSet = new Set(allow)
|
|
285
|
+
const candidates = []
|
|
286
|
+
const notAllowed = []
|
|
287
|
+
const refused = []
|
|
288
|
+
const skipped = []
|
|
289
|
+
const seen = new Set()
|
|
290
|
+
const targetExists = existsSync(target)
|
|
291
|
+
for (const e of targetExists ? readdirSync(target, { withFileTypes: true }) : []) {
|
|
292
|
+
const name = e.name
|
|
293
|
+
seen.add(name)
|
|
294
|
+
if (name.startsWith('.')) { skipped.push({ name, reason: '点开头(工具自管:清单 / 备份 / 审计)⇒ 不碰' }); continue }
|
|
295
|
+
const p = join(target, name)
|
|
296
|
+
// 软链**单独一档**:删它要么删链接、要么越界删到目标树 —— 两种都不是本动词该干的事
|
|
297
|
+
if (e.isSymbolicLink()) { refused.push({ name, path: p, reasons: ['软链:删除会越界或语义不明 ⇒ 一律不碰'] }); continue }
|
|
298
|
+
if (!e.isDirectory()) { refused.push({ name, path: p, reasons: ['非技能形态:不是目录(用户自建文件)⇒ 不碰'] }); continue }
|
|
299
|
+
const reasons = []
|
|
300
|
+
if (ext.has(name)) reasons.push('① 在外发集(业务线声明的流程技能 ⇒ 归同步/安装管,清理不碰)')
|
|
301
|
+
if (int.has(name)) reasons.push(`② 在 ${INTERNAL_MANIFEST}(内部角色技能 ⇒ 宿主上它还是"角色包已装"的判据)`)
|
|
302
|
+
if (recorded.has(name)) reasons.push(`③ 在安装器清单 ${MANIFEST_NAME} 里(本脚本装的 ⇒ 请用 --uninstall 收敛)`)
|
|
303
|
+
const hasSkillFile = existsSync(join(p, SKILL_FILE))
|
|
304
|
+
if (!hasSkillFile) reasons.push(`④ 目录内没有 ${SKILL_FILE}(不是技能形态 ⇒ 可能是用户自建,一律不碰)`)
|
|
305
|
+
if (reasons.length) { refused.push({ name, path: p, reasons }); continue }
|
|
306
|
+
const row = {
|
|
307
|
+
name, path: p, allowed: allowSet.has(name),
|
|
308
|
+
files: Object.keys(skillFiles(p)).length,
|
|
309
|
+
skillName: frontmatterName(readFileSync(join(p, SKILL_FILE), 'utf8')),
|
|
310
|
+
}
|
|
311
|
+
if (row.allowed) candidates.push(row); else notAllowed.push(row)
|
|
312
|
+
}
|
|
313
|
+
const refusedNames = new Set(refused.map((r) => r.name))
|
|
314
|
+
const namedRefused = allow.filter((n) => refusedNames.has(n))
|
|
315
|
+
const missingAllow = allow.filter((n) => !seen.has(n))
|
|
316
|
+
return {
|
|
317
|
+
candidates, refused, notAllowed, skipped, namedRefused, missingAllow, targetExists,
|
|
318
|
+
external: cls.external, internal: cls.internal, recorded: [...recorded].sort(),
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* **执行清理**(真删):逐个 **先备份到 `<target>/.argent-skills-pruned-<ts>/<name>/` 再删**,
|
|
324
|
+
* 完成后追加一行审计 JSONL。只接受 `planPrune()` 给出的 `candidates`(调用方不得自己拼名字)。
|
|
325
|
+
*
|
|
326
|
+
* @returns {{removed:string[], failed:Array, backupDir:string|null, auditPath:string|null, at:string}}
|
|
327
|
+
*/
|
|
328
|
+
export function applyPrune({ target, candidates, host, now = new Date() }) {
|
|
329
|
+
const stamp = now.toISOString().replace(/[:.]/g, '-')
|
|
330
|
+
const rows = []
|
|
331
|
+
const removed = []
|
|
332
|
+
const failed = []
|
|
333
|
+
let backupDir = null
|
|
334
|
+
if (candidates.length) {
|
|
335
|
+
backupDir = join(target, `${PRUNE_BACKUP_PREFIX}${stamp}`)
|
|
336
|
+
mkdirSync(backupDir, { recursive: true })
|
|
337
|
+
for (const c of candidates) {
|
|
338
|
+
const dst = join(backupDir, c.name)
|
|
339
|
+
try {
|
|
340
|
+
cpSync(c.path, dst, { recursive: true }) // ① 先备份(可回滚)
|
|
341
|
+
rmSync(c.path, { recursive: true, force: true }) // ② 再删
|
|
342
|
+
removed.push(c.name)
|
|
343
|
+
rows.push({ name: c.name, ok: true, backup: dst })
|
|
344
|
+
} catch (e) {
|
|
345
|
+
failed.push({ name: c.name, error: e && e.message ? e.message : String(e) })
|
|
346
|
+
rows.push({ name: c.name, ok: false, error: e && e.message ? e.message : String(e) })
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (!removed.length) rmSync(backupDir, { recursive: true, force: true }) // 全失败 ⇒ 不留空备份目录
|
|
350
|
+
}
|
|
351
|
+
// 审计(沿用既有 JSONL 追加风格:一行一次动作,含目标/备份/删除/失败)
|
|
352
|
+
let auditPath = null
|
|
353
|
+
if (removed.length || failed.length) {
|
|
354
|
+
auditPath = join(target, PRUNE_AUDIT_NAME)
|
|
355
|
+
const row = {
|
|
356
|
+
at: now.toISOString(), verb: 'skill-install', action: 'prune-orphans', host: host || null, target,
|
|
357
|
+
removed, failed, backupDir,
|
|
358
|
+
rollback: backupDir ? `cp -R ${backupDir}/<name> ${target}/<name>` : null,
|
|
359
|
+
}
|
|
360
|
+
try { appendFileSync(auditPath, `${JSON.stringify(row)}\n`, { mode: 0o600 }) } catch { auditPath = null }
|
|
361
|
+
}
|
|
362
|
+
return { rows, removed, failed, backupDir, auditPath, at: now.toISOString() }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** 清理结果的 JSON 载荷(dry-run 与真删共用一套字段名)。 */
|
|
366
|
+
function pruneJson(plan, { refusedCount, unhonored, applied = null }) {
|
|
367
|
+
return {
|
|
368
|
+
dryRun: !applied,
|
|
369
|
+
candidates: plan.candidates.map((c) => c.name),
|
|
370
|
+
candidatesDetail: plan.candidates,
|
|
371
|
+
notAllowed: plan.notAllowed.map((c) => c.name),
|
|
372
|
+
notAllowedDetail: plan.notAllowed,
|
|
373
|
+
refused: plan.refused,
|
|
374
|
+
skipped: plan.skipped,
|
|
375
|
+
refusedCount,
|
|
376
|
+
namedRefused: plan.namedRefused,
|
|
377
|
+
missingAllow: plan.missingAllow,
|
|
378
|
+
unhonored,
|
|
379
|
+
external: plan.external, internal: plan.internal, recorded: plan.recorded,
|
|
380
|
+
removed: applied ? applied.removed : [],
|
|
381
|
+
failed: applied ? applied.failed : [],
|
|
382
|
+
backupDir: applied ? applied.backupDir : null,
|
|
383
|
+
auditPath: applied ? applied.auditPath : null,
|
|
384
|
+
criteria: [
|
|
385
|
+
'① 不在外发集(verticals/<id>/manifest.json#skills 并集)',
|
|
386
|
+
'② 不在 skills/internal.json(内部角色技能)',
|
|
387
|
+
`③ 不在安装器清单 ${MANIFEST_NAME}`,
|
|
388
|
+
`④ 目录内含 ${SKILL_FILE}`,
|
|
389
|
+
],
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** 清理的人话输出(dry-run / 真删共用;`将删除 N 项(点名)/ 拒绝清理 M 项(原因)` 是固定口径)。 */
|
|
394
|
+
function pruneHuman({ plan, target, host, refusedCount, dryRun, explicit = false, unhonored = [], applied = null }) {
|
|
395
|
+
const lines = [dryRun
|
|
396
|
+
? `🧪 [dry-run${explicit ? '' : '(缺省动作:A3 安全态)'}] 技能清理预演(宿主 ${host} → ${target})`
|
|
397
|
+
: `🧹 技能清理(宿主 ${host} → ${target})`]
|
|
398
|
+
lines.push(` 将删除 ${plan.candidates.length} 项(点名)${plan.candidates.length ? `:${plan.candidates.map((c) => c.name).join('、')}` : '(未点名,或没有符合条件的项)'}`)
|
|
399
|
+
for (const c of plan.candidates) {
|
|
400
|
+
lines.push(` · ${c.name}(${c.files} 个文件${c.skillName ? `;SKILL.md name=${c.skillName}` : ';SKILL.md 无 name'})`)
|
|
401
|
+
}
|
|
402
|
+
if (plan.notAllowed.length) {
|
|
403
|
+
lines.push(` 未点名 ${plan.notAllowed.length} 项(**不删**;四条件都满足,要删请用 --allow 逐个点名):${plan.notAllowed.map((c) => c.name).join('、')}`)
|
|
404
|
+
}
|
|
405
|
+
lines.push(` 拒绝清理 ${refusedCount} 项(原因):`)
|
|
406
|
+
for (const c of plan.refused) lines.push(` · ${c.name} — ${c.reasons.join(';')}`)
|
|
407
|
+
for (const c of plan.skipped) lines.push(` · ${c.name} — ${c.reason}`)
|
|
408
|
+
if (refusedCount === 0) lines.push(' (无)')
|
|
409
|
+
if (unhonored.length) {
|
|
410
|
+
lines.push(` ⚠️ 你点名了但**没有删**(四条件不满足或目录不存在):${unhonored.join('、')}`)
|
|
411
|
+
}
|
|
412
|
+
if (applied) {
|
|
413
|
+
lines.push(` 已删除 ${applied.removed.length} 项${applied.removed.length ? `:${applied.removed.join('、')}` : ''}`)
|
|
414
|
+
if (applied.failed.length) lines.push(` ❌ 失败 ${applied.failed.length} 项:${applied.failed.map((f) => `${f.name}(${f.error})`).join(';')}`)
|
|
415
|
+
if (applied.backupDir) lines.push(` 备份(可回滚):${applied.backupDir} 回滚 = \`cp -R <备份>/<name> <目标>/<name>\``)
|
|
416
|
+
if (applied.auditPath) lines.push(` 审计:${applied.auditPath}(JSONL,追加型)`)
|
|
417
|
+
lines.push(` 小结:已删除 ${applied.removed.length} 项(点名)/ 拒绝清理 ${refusedCount} 项(原因)`)
|
|
418
|
+
} else {
|
|
419
|
+
lines.push(' ⛔ 未删任何目录(真删:`--apply --allow <name>`,逐个点名)')
|
|
420
|
+
lines.push(` 小结:将删除 ${plan.candidates.length} 项(点名)/ 拒绝清理 ${refusedCount} 项(原因)`)
|
|
421
|
+
}
|
|
422
|
+
return lines.join('\n')
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── CLI ─────────────────────────────────────────────────────────────────────
|
|
426
|
+
const USAGE = [
|
|
427
|
+
'用法:node scripts/skill-install.mjs --host <dsh|claude-code|codex|cursor> [--list|--dry-run|--apply|--uninstall]',
|
|
428
|
+
' [--dir <path>] [--json]',
|
|
429
|
+
' node scripts/skill-install.mjs --host <宿主> --prune-orphans [--apply --allow <name> …] [--dir <path>] [--json]',
|
|
430
|
+
'',
|
|
431
|
+
'只装**外发白名单**技能(= 所有 verticals/<id>/manifest.json#skills 的并集,今天 5 个流程技能)。',
|
|
432
|
+
' · --list 只看:宿主约定目录 + 外发清单 + 目标目录现状(不写盘)',
|
|
433
|
+
' · --dry-run / **缺省** 只看计划(install/skip/update),不写盘(A3:write 类默认安全态)',
|
|
434
|
+
' · --apply 执行安装(sha 一致 ⇒ skipped;不一致 ⇒ 先备份再覆盖)',
|
|
435
|
+
' · --uninstall 只删清单里列过的技能目录与清单本身(用户自建目录不碰)',
|
|
436
|
+
' · --dir <path> 覆盖宿主约定目录(约定查不到/不适用时的兜底)',
|
|
437
|
+
'',
|
|
438
|
+
'孤儿技能清理(**人工确认**;客户机 6 内部 + 12 旧 hermes 期那类残留):',
|
|
439
|
+
' · --prune-orphans 列出「将删除(点名)/ 未点名(不删)/ 拒绝清理」三张清单;**缺省 dry-run**',
|
|
440
|
+
' · --apply --allow <n> 真删:**必须逐个点名**(可重复);未点名的**绝不删**;先备份再删 + 写审计',
|
|
441
|
+
' · --allow-host-dsh-prune `--host dsh` 下清理的显式放行(或改用 `--dir <path>` 显式给目标)',
|
|
442
|
+
' · 只删**四条件同时满足**的目录:① 不在外发集 ② 不在 skills/internal.json ③ 不在安装器清单',
|
|
443
|
+
' ④ 目录内含 SKILL.md;用户自建(非技能形态 / 点开头 / 软链)一律不碰并列入"拒绝清理"',
|
|
444
|
+
'',
|
|
445
|
+
'⛔ `--host dsh --apply`(装)被拒绝(dsh 侧走原路:preset 挂载 + scripts/argent-upgrade.mjs);',
|
|
446
|
+
' 确要装到别处请用 `--host dsh --dir <path>`。清理同理:加 `--dir` 或 `--allow-host-dsh-prune`。',
|
|
447
|
+
].join('\n')
|
|
448
|
+
|
|
449
|
+
function parseArgs(argv) {
|
|
450
|
+
// A3:**缺省即 dry-run**(真装要显式 --apply);`--dry-run` 是它的显式同义。
|
|
451
|
+
const o = { host: null, dir: null, action: 'dry-run', json: false, help: false, pruneOrphans: false, allow: [], allowHostDshPrune: false }
|
|
452
|
+
for (let i = 0; i < argv.length; i++) {
|
|
453
|
+
const a = argv[i]
|
|
454
|
+
if (a === '--json') { o.json = true; continue }
|
|
455
|
+
if (a === '-h' || a === '--help') { o.help = true; continue }
|
|
456
|
+
if (a === '--host' || a === '--host=') { const v = argv[++i]; if (!v || v.startsWith('--')) return { ok: false, error: '--host 缺值' }; o.host = v; continue }
|
|
457
|
+
if (a === '--dir' || a === '--target') { const v = argv[++i]; if (!v || v.startsWith('--')) return { ok: false, error: `${a} 缺值` }; o.dir = v; continue }
|
|
458
|
+
if (a === '--list') { o.action = 'list'; continue }
|
|
459
|
+
if (a === '--dry-run') { o.action = 'dry-run'; o.explicitDryRun = true; continue }
|
|
460
|
+
if (a === '--apply') { o.action = 'install'; continue }
|
|
461
|
+
if (a === '--uninstall' || a === 'remove' || a === '--remove') { o.action = 'uninstall'; continue }
|
|
462
|
+
if (a === '--prune-orphans' || a === '--prune') { o.pruneOrphans = true; continue }
|
|
463
|
+
if (a === '--allow-host-dsh-prune') { o.allowHostDshPrune = true; continue }
|
|
464
|
+
if (a === '--allow' || a === '--allow=') {
|
|
465
|
+
const v = argv[++i]
|
|
466
|
+
if (!v || v.startsWith('--')) return { ok: false, error: '--allow 缺值(要逐个点名,可重复)' }
|
|
467
|
+
// 允许 `--allow a,b` 逗号形态(人懒得敲多次),但仍**逐个**进名单 ⇒ 每个都显式
|
|
468
|
+
for (const n of String(v).split(',').map((s) => s.trim()).filter(Boolean)) o.allow.push(n)
|
|
469
|
+
continue
|
|
470
|
+
}
|
|
471
|
+
return { ok: false, error: `未知参数:${a}` }
|
|
472
|
+
}
|
|
473
|
+
return { ok: true, ...o }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function main(argv) {
|
|
477
|
+
const p = parseArgs(argv)
|
|
478
|
+
if (!p.ok) return { code: 2, human: `❌ usage_error:${p.error}\n${USAGE}`, json: { ok: false, empty: false, code: 2, reason: 'usage_error', error: p.error, verb: 'skill-install' } }
|
|
479
|
+
if (p.help) return { code: 0, human: USAGE, json: { ok: true, empty: false, code: 0, reason: null, verb: 'skill-install', usage: USAGE } }
|
|
480
|
+
if (!p.host) return { code: 2, human: `❌ usage_error:缺必填参数 --host(可用:${HOST_IDS.join('、')})\n${USAGE}`, json: { ok: false, empty: false, code: 2, reason: 'usage_error', error: '缺 --host', hosts: HOST_IDS, verb: 'skill-install' } }
|
|
481
|
+
if (!HOST_IDS.includes(p.host)) return { code: 2, human: `❌ unknown_host:未知宿主 ${p.host}(可用:${HOST_IDS.join('、')})`, json: { ok: false, empty: false, code: 2, reason: 'unknown_host', error: `未知宿主 ${p.host}`, hosts: HOST_IDS, verb: 'skill-install' } }
|
|
482
|
+
|
|
483
|
+
const table = hostTable({ dshHome: process.env.DSH_HOME || '' })
|
|
484
|
+
const host = table[p.host]
|
|
485
|
+
const target = p.dir ? resolve(p.dir) : host.dir
|
|
486
|
+
const cls = externalSkills(REPO)
|
|
487
|
+
const argent = (() => { try { return readFileSync(join(REPO, 'VERSION'), 'utf8').trim() } catch { return null } })()
|
|
488
|
+
|
|
489
|
+
const base = {
|
|
490
|
+
host: p.host, target, dirSource: p.dir ? 'explicit(--dir)' : 'host-convention',
|
|
491
|
+
hostVerified: host.verified, hostEvidence: host.evidence,
|
|
492
|
+
external: cls.external, internal: cls.internal, unclassified: cls.unclassified,
|
|
493
|
+
argent, action: p.action,
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 归类门禁:不过 ⇒ **拒绝安装**(外发泄露内部角色是本阶段唯一高危项)
|
|
497
|
+
if (!cls.ok) {
|
|
498
|
+
return {
|
|
499
|
+
code: 1,
|
|
500
|
+
human: `❌ 归类门禁不通过 ⇒ 拒绝安装(宁可不装,不可装错):\n${cls.errors.map((e) => ` · [${e.rule}] ${e.message}`).join('\n')}`,
|
|
501
|
+
json: { ok: false, empty: false, code: 1, reason: 'skill_unclassified', error: cls.errors.map((e) => `[${e.rule}] ${e.message}`).join(';'), verb: 'skill-install', ...base },
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (!cls.external.length) {
|
|
505
|
+
return {
|
|
506
|
+
code: 0,
|
|
507
|
+
human: '(外发集为空:没有任何业务线声明流程技能 ⇒ 无事可做)',
|
|
508
|
+
json: { ok: true, empty: true, code: 0, reason: 'no_skill_declared', verb: 'skill-install', ...base, skills: [] },
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ── 孤儿技能清理(--prune-orphans):受守卫、**缺省 dry-run**、真删要逐个点名 ──────────
|
|
513
|
+
if (p.pruneOrphans) {
|
|
514
|
+
if (p.action === 'list' || p.action === 'uninstall') {
|
|
515
|
+
const err = `--prune-orphans 与 --list/--uninstall 互斥(清理有自己的清单口径,混用会让人读错)`
|
|
516
|
+
return { code: 2, human: `❌ usage_error:${err}`, json: { ok: false, empty: false, code: 2, reason: 'usage_error', error: err, verb: 'skill-install', ...base } }
|
|
517
|
+
}
|
|
518
|
+
if (host.ownsInstall && !p.dir && !p.allowHostDshPrune) {
|
|
519
|
+
const err = '`--host dsh` 的技能**清理**同样不抢原路(技能根归 presets + `scripts/argent-upgrade.mjs`)'
|
|
520
|
+
+ ' ⇒ 客户机清理请显式二选一:`--dir <path>`(推荐:目标写死、看得见)或 `--allow-host-dsh-prune`(承认你在动宿主技能根)'
|
|
521
|
+
return { code: 2, human: `⛔ host_owns_prune:${err}`, json: { ok: false, empty: false, code: 2, reason: 'host_owns_prune', error: err, verb: 'skill-install', ...base } }
|
|
522
|
+
}
|
|
523
|
+
const applyIt = p.action === 'install' // `--apply`(清理语境 = 真删)
|
|
524
|
+
if (applyIt && !p.allow.length) {
|
|
525
|
+
const err = '真删必须**逐个点名**:`--apply --allow <name>`(可重复);未点名的一律不删(防"一条命令清一片")'
|
|
526
|
+
return { code: 2, human: `❌ allow_required:${err}`, json: { ok: false, empty: false, code: 2, reason: 'allow_required', error: err, verb: 'skill-install', ...base } }
|
|
527
|
+
}
|
|
528
|
+
if (!existsSync(target)) {
|
|
529
|
+
return { code: 2, human: `❌ target_missing:目标目录不存在:${target}`, json: { ok: false, empty: true, code: 2, reason: 'target_missing', error: `目标目录不存在:${target}`, verb: 'skill-install', ...base } }
|
|
530
|
+
}
|
|
531
|
+
const plan = planPrune({ root: REPO, target, allow: p.allow })
|
|
532
|
+
const refusedCount = plan.refused.length + plan.skipped.length
|
|
533
|
+
const unhonored = [...plan.namedRefused, ...plan.missingAllow]
|
|
534
|
+
if (!applyIt) {
|
|
535
|
+
return {
|
|
536
|
+
code: unhonored.length ? 1 : 0,
|
|
537
|
+
human: pruneHuman({ plan, target, host: p.host, refusedCount, dryRun: true, explicit: p.explicitDryRun, unhonored }),
|
|
538
|
+
json: {
|
|
539
|
+
ok: true, empty: plan.candidates.length === 0, code: unhonored.length ? 1 : 0,
|
|
540
|
+
reason: unhonored.length ? 'named_not_prunable' : null, verb: 'skill-install', ...base,
|
|
541
|
+
dryRun: true,
|
|
542
|
+
prune: pruneJson(plan, { refusedCount, unhonored }),
|
|
543
|
+
},
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const r = applyPrune({ target, candidates: plan.candidates, host: p.host })
|
|
547
|
+
const code = (r.failed.length || unhonored.length) ? 1 : 0
|
|
548
|
+
return {
|
|
549
|
+
code,
|
|
550
|
+
human: pruneHuman({ plan, target, host: p.host, refusedCount, dryRun: false, unhonored, applied: r }),
|
|
551
|
+
json: {
|
|
552
|
+
ok: code === 0, empty: plan.candidates.length === 0, code,
|
|
553
|
+
reason: r.failed.length ? 'prune_failed' : (unhonored.length ? 'named_not_prunable' : null),
|
|
554
|
+
verb: 'skill-install', ...base,
|
|
555
|
+
dryRun: false,
|
|
556
|
+
prune: pruneJson(plan, { refusedCount, unhonored, applied: r }),
|
|
557
|
+
removed: r.removed, failed: r.failed.map((f) => f.name), backupDir: r.backupDir, auditPath: r.auditPath,
|
|
558
|
+
},
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (p.action === 'list') {
|
|
563
|
+
const lines = ['📦 技能外发清单(P3)', ` 宿主:${p.host}(目标 ${target};目录来源=${base.dirSource};证据:${host.verified})`,
|
|
564
|
+
` · ${host.evidence}`, ` 外发 ${cls.external.length} 个(真源:${cls.sources.map((s) => `verticals/${s.vertical}/manifest.json#skills`).join('、')}):`]
|
|
565
|
+
for (const name of cls.external) {
|
|
566
|
+
const v = frontmatterVersion(readFileSync(join(REPO, 'skills', name, SKILL_FILE), 'utf8'))
|
|
567
|
+
lines.push(` · ${name}${v ? `(v${v})` : ''} —— skills/${name}/`)
|
|
568
|
+
}
|
|
569
|
+
lines.push(` 内部 ${cls.internal.length} 个**不外发**(${INTERNAL_MANIFEST}):${cls.internal.join('、')}`)
|
|
570
|
+
const man = readManifest(target)
|
|
571
|
+
lines.push(` 目标目录现状:${existsSync(target) ? `${readdirSync(target).length} 项` : '不存在'}${man && !man._corrupt ? `|已装 ${Object.keys(man.skills || {}).length} 个(${MANIFEST_NAME})` : `|无 ${MANIFEST_NAME}`}`)
|
|
572
|
+
return { code: 0, human: lines.join('\n'), json: { ok: true, empty: false, code: 0, reason: null, verb: 'skill-install', ...base, skills: cls.external, manifestPresent: Boolean(man) } }
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (p.action === 'uninstall') {
|
|
576
|
+
if (!existsSync(target)) return { code: 2, human: `❌ target_missing:目标目录不存在:${target}(没装过 ⇒ 无可卸载)`, json: { ok: false, empty: true, code: 2, reason: 'target_missing', error: `目标目录不存在:${target}`, verb: 'skill-install', ...base } }
|
|
577
|
+
const r = applyUninstall({ target })
|
|
578
|
+
if (!r.ok) return { code: 2, human: `❌ ${r.reason}:${r.error}`, json: { ok: false, empty: false, code: 2, reason: r.reason, error: r.error, verb: 'skill-install', ...base } }
|
|
579
|
+
const human = [`🧹 已卸载 ${r.removed.length} 个技能(只删清单里列过的):${r.removed.join('、') || '(无)'}`,
|
|
580
|
+
r.kept.length ? ` 清单里但目录不在:${r.kept.join('、')}` : null,
|
|
581
|
+
` 目标目录剩余 ${r.leftovers.length} 项(用户自建文件/目录**一个都没碰**):${r.leftovers.join('、') || '(空)'}`,
|
|
582
|
+
r.backupDir ? ` 备份目录保留(如需回滚可用):${r.backupDir}` : null,
|
|
583
|
+
].filter(Boolean).join('\n')
|
|
584
|
+
return { code: 0, human, json: { ok: true, empty: r.removed.length === 0, code: 0, reason: r.removed.length ? null : 'no_match', verb: 'skill-install', ...base, removed: r.removed, kept: r.kept, leftovers: r.leftovers, backupDir: r.backupDir } }
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// dsh 宿主的安装权归原路(§P3① 的实施约束)——除非显式 --dir
|
|
588
|
+
if (host.ownsInstall && !p.dir && p.action === 'install') {
|
|
589
|
+
const err = 'dsh 宿主的技能安装权归**原路**(`presets/*/agent.cordis.yml` 的 skill-filesystem 挂载 + `scripts/argent-upgrade.mjs`)'
|
|
590
|
+
+ ' ⇒ 本脚本不抢安装权,只做 `--list` / `--dry-run` 一致性校验;确要装到别处请用 `--host dsh --dir <path>`'
|
|
591
|
+
return { code: 2, human: `⛔ host_owns_install:${err}`, json: { ok: false, empty: false, code: 2, reason: 'host_owns_install', error: err, verb: 'skill-install', ...base } }
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const plan = planInstall({ root: REPO, target, skills: cls.external })
|
|
595
|
+
if (p.action === 'dry-run') {
|
|
596
|
+
const human = [`🧪 [dry-run${p.explicitDryRun ? '' : '(缺省动作:A3 安全态)'}] 技能安装计划(宿主 ${p.host} → ${target})`,
|
|
597
|
+
...plan.rows.map((r) => ` · ${r.name.padEnd(24)} ${r.action}${r.action === 'skip' ? '(内容 sha 一致,幂等跳过)' : r.action === 'update' ? '(不一致 ⇒ 先备份再覆盖)' : ''}`),
|
|
598
|
+
' ⛔ 未写任何文件(真装加 --apply)'].join('\n')
|
|
599
|
+
return { code: 0, human, json: { ok: true, empty: false, code: 0, reason: null, verb: 'skill-install', ...base, dryRun: true, plan: plan.rows, willWrite: plan.rows.filter((r) => r.action !== 'skip').map((r) => r.dst) } }
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const { done, manifest } = applyInstall({ root: REPO, target, rows: plan.rows, host: p.host, argentVersion: argent })
|
|
603
|
+
const failed = done.filter((d) => !d.ok)
|
|
604
|
+
const human = [`📦 技能安装(宿主 ${p.host} → ${target})`,
|
|
605
|
+
...done.map((d) => ` · ${d.name.padEnd(24)} ${d.ok ? d.action : '❌ failed'}${d.backup ? `(备份 ${d.backup})` : ''} ${d.note}`),
|
|
606
|
+
` 清单:${join(target, MANIFEST_NAME)}(记录 ${Object.keys(manifest.skills).length} 个技能的内容摘要;更新/卸载读它)`,
|
|
607
|
+
` ⛔ 未触碰目标目录里用户自建的文件/目录`].join('\n')
|
|
608
|
+
return {
|
|
609
|
+
code: failed.length ? 1 : 0,
|
|
610
|
+
human,
|
|
611
|
+
json: {
|
|
612
|
+
ok: failed.length === 0, empty: done.length === 0, code: failed.length ? 1 : 0,
|
|
613
|
+
reason: failed.length ? 'install_failed' : null, verb: 'skill-install', ...base,
|
|
614
|
+
installed: done.filter((d) => d.action === 'install' && d.ok).map((d) => d.name),
|
|
615
|
+
updated: done.filter((d) => d.action === 'update' && d.ok).map((d) => d.name),
|
|
616
|
+
skipped: done.filter((d) => d.action === 'skip').map((d) => d.name),
|
|
617
|
+
failed: failed.map((d) => ({ name: d.name, note: d.note })),
|
|
618
|
+
manifest: { path: join(target, MANIFEST_NAME), skills: Object.keys(manifest.skills), orphans: manifest.orphans },
|
|
619
|
+
results: done,
|
|
620
|
+
},
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (isDirectRun(import.meta.url)) {
|
|
625
|
+
const { code, human, json } = main(process.argv.slice(2))
|
|
626
|
+
if (process.argv.includes('--json')) console.log(JSON.stringify(json, null, 2))
|
|
627
|
+
else console.log(human)
|
|
628
|
+
process.exit(code)
|
|
629
|
+
}
|