@gong-ym/ai-spec-auto 0.2.13 → 0.2.15
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/.agents/commands/README.md +33 -33
- package/.agents/commands/claude/spec-start-review.md +88 -88
- package/.agents/commands/codex/spec-continue.md +74 -74
- package/.agents/commands/codex/spec-orchestrate.md +35 -35
- package/.agents/commands/codex/spec-start-review.md +88 -88
- package/.agents/commands/codex/spec-start.md +67 -67
- package/.agents/commands/codex/spec-status.md +22 -22
- package/.agents/commands/codex/spec-stop.md +29 -29
- package/.agents/commands/codex/spec-update.md +40 -40
- package/.agents/commands/common/branch-review.md +117 -117
- package/.agents/commands/common/project-init.md +25 -25
- package/.agents/commands/common/spec-continue.md +74 -74
- package/.agents/commands/common/spec-orchestrate.md +35 -35
- package/.agents/commands/common/spec-start-review.md +82 -82
- package/.agents/commands/common/spec-start.md +67 -67
- package/.agents/commands/common/spec-status.md +22 -22
- package/.agents/commands/common/spec-stop.md +29 -29
- package/.agents/commands/common/spec-update.md +60 -40
- package/.agents/commands/cursor/opsx-apply.md +55 -55
- package/.agents/commands/cursor/opsx-archive.md +48 -48
- package/.agents/commands/cursor/opsx-explore.md +45 -45
- package/.agents/commands/cursor/opsx-propose.md +59 -59
- package/.agents/commands/cursor/spec-continue.md +63 -63
- package/.agents/commands/cursor/spec-orchestrate.md +53 -53
- package/.agents/commands/cursor/spec-start-review.md +78 -78
- package/.agents/commands/cursor/spec-start.md +59 -59
- package/.agents/commands/cursor/spec-status.md +30 -30
- package/.agents/commands/cursor/spec-stop.md +29 -29
- package/.agents/commands/cursor/spec-update.md +41 -41
- package/.agents/flows/FRONTMATTER.md +263 -263
- package/.agents/flows/RUN_OUTPUT.md +263 -263
- package/.agents/flows/common/README.md +29 -29
- package/.agents/flows/common/bugfix-to-verification.md +95 -95
- package/.agents/flows/common/change-to-architecture-review.md +89 -89
- package/.agents/flows/common/change-to-release.md +94 -94
- package/.agents/flows/common/prd-to-delivery.md +184 -184
- package/.agents/flows/common/requirement-to-observability.md +97 -97
- package/.agents/orchestration/README.md +22 -22
- package/.agents/orchestration/expert-dispatch-spec.md +155 -155
- package/.agents/orchestration/expert-executor-spec.md +84 -84
- package/.agents/orchestration/expert-runtime-action-spec.md +73 -73
- package/.agents/orchestration/runtime-state-handoff-spec.md +264 -264
- package/.agents/orchestration/task-anchor-spec.md +212 -212
- package/.agents/orchestration/task-orchestrator-adapter-payload.md +153 -153
- package/.agents/orchestration/task-orchestrator-bootstrap-payload.md +145 -145
- package/.agents/orchestration/task-orchestrator-output-extractor-spec.md +93 -93
- package/.agents/orchestration/task-orchestrator-run-plan-template.md +312 -312
- package/.agents/orchestration/task-orchestrator-runtime-hooks.md +214 -214
- package/.agents/registry/README.md +63 -63
- package/.agents/registry/flows.json +125 -125
- package/.agents/registry/profiles.json +101 -101
- package/.agents/registry/roles.json +1265 -1265
- package/.agents/registry/rules.json +148 -148
- package/.agents/registry/scenario-packages.json +123 -123
- package/.agents/registry/skills.json +130 -130
- package/.agents/roles/INDEX.md +346 -346
- package/.agents/roles/common/README.md +76 -76
- package/.agents/roles/common/archive-change.md +80 -80
- package/.agents/roles/common/backend-implementer.md +92 -92
- package/.agents/roles/common/code-guardian.md +151 -151
- package/.agents/roles/common/frontend-implementer.md +146 -146
- package/.agents/roles/common/requirement-analyst.md +138 -138
- package/.agents/roles/common/task-orchestrator-routing.md +301 -301
- package/.agents/roles/common/task-orchestrator.md +224 -224
- package/.agents/roles/common/tooling-implementer.md +92 -92
- package/.agents/roles/domains/README.md +35 -35
- package/.agents/roles/domains/delivery/README.md +11 -11
- package/.agents/roles/domains/delivery/container-specialist.md +50 -50
- package/.agents/roles/domains/delivery/deployment-specialist.md +50 -50
- package/.agents/roles/domains/delivery/pipeline-specialist.md +50 -50
- package/.agents/roles/domains/demand-design/README.md +16 -16
- package/.agents/roles/domains/demand-design/api-contract-specialist.md +52 -52
- package/.agents/roles/domains/demand-design/design-collaborator.md +58 -58
- package/.agents/roles/domains/documentation/README.md +11 -11
- package/.agents/roles/domains/documentation/api-doc-specialist.md +50 -50
- package/.agents/roles/domains/documentation/component-doc-specialist.md +49 -49
- package/.agents/roles/domains/documentation/technical-writing-specialist.md +48 -48
- package/.agents/roles/domains/engineering/README.md +17 -17
- package/.agents/roles/domains/engineering/architecture-advisor.md +53 -53
- package/.agents/roles/domains/engineering/build-specialist.md +51 -51
- package/.agents/roles/domains/engineering/dependency-governor.md +52 -52
- package/.agents/roles/domains/governance/README.md +17 -17
- package/.agents/roles/domains/governance/api-governance-specialist.md +51 -51
- package/.agents/roles/domains/governance/lint-policy-specialist.md +49 -49
- package/.agents/roles/domains/governance/route-governance-specialist.md +52 -52
- package/.agents/roles/domains/observability/README.md +11 -11
- package/.agents/roles/domains/observability/error-tracker.md +50 -50
- package/.agents/roles/domains/observability/event-instrumentation-specialist.md +51 -51
- package/.agents/roles/domains/observability/rum-analyst.md +50 -50
- package/.agents/roles/domains/performance/README.md +11 -11
- package/.agents/roles/domains/performance/asset-optimizer.md +50 -50
- package/.agents/roles/domains/performance/performance-auditor.md +56 -56
- package/.agents/roles/domains/performance/vitals-analyst.md +50 -50
- package/.agents/roles/domains/security-a11y/README.md +11 -11
- package/.agents/roles/domains/security-a11y/a11y-auditor.md +50 -50
- package/.agents/roles/domains/security-a11y/aria-specialist.md +51 -51
- package/.agents/roles/domains/security-a11y/security-reviewer.md +49 -49
- package/.agents/roles/domains/testing/README.md +12 -12
- package/.agents/roles/domains/testing/coverage-analyst.md +50 -50
- package/.agents/roles/domains/testing/e2e-test-specialist.md +51 -51
- package/.agents/roles/domains/testing/unit-test-specialist.md +56 -56
- package/.agents/roles/domains/testing/verification-reviewer.md +67 -67
- package/.agents/rules/README.md +87 -87
- package/.agents/rules/common/02-/347/274/226/347/240/201/350/247/204/350/214/203.md +45 -45
- package/.agents/rules/common/08-/351/200/232/347/224/250/347/272/246/346/235/237.md +63 -63
- package/.agents/rules/common/10-/346/226/207/346/241/243/350/247/204/350/214/203.md +101 -101
- package/.agents/rules/common/12-Superpowers/346/211/247/350/241/214/350/247/204/350/214/203.md +46 -46
- package/.agents/rules/common/14-/345/256/241/350/256/241/346/261/207/346/212/245/350/247/204/350/214/203.md +107 -107
- package/.agents/rules/common/15-visual-gate-wait.md +90 -90
- package/.agents/rules/profiles/nestjs/01-/351/241/271/347/233/256/346/246/202/350/277/260.md +27 -27
- package/.agents/rules/profiles/nestjs/03-/351/241/271/347/233/256/347/273/223/346/236/204.md +20 -20
- package/.agents/rules/profiles/nestjs/04-/346/250/241/345/235/227/347/273/223/346/236/204/350/247/204/350/214/203.md +24 -24
- package/.agents/rules/profiles/nestjs/05-/346/216/245/345/217/243/344/270/216/345/245/221/347/272/246/350/247/204/350/214/203.md +24 -24
- package/.agents/rules/profiles/nestjs/06-/346/225/260/346/215/256/350/256/277/351/227/256/350/247/204/350/214/203.md +24 -24
- package/.agents/rules/profiles/nestjs/07-/351/205/215/347/275/256/344/270/216/350/277/220/350/241/214/346/227/266/350/247/204/350/214/203.md +20 -20
- package/.agents/rules/profiles/nestjs/09-/345/274/202/345/270/270/344/270/216/346/227/245/345/277/227/350/247/204/350/214/203.md +20 -20
- package/.agents/rules/profiles/nestjs/11-/346/265/213/350/257/225/350/247/204/350/214/203.md +24 -24
- package/.agents/rules/profiles/nestjs/13-/344/273/243/347/240/201/346/240/274/345/274/217/345/214/226/344/270/216/346/243/200/346/237/245.md +20 -20
- package/.agents/rules/profiles/node-tooling/01-/351/241/271/347/233/256/346/246/202/350/277/260.md +30 -30
- package/.agents/rules/profiles/node-tooling/03-/351/241/271/347/233/256/347/273/223/346/236/204.md +37 -37
- package/.agents/rules/profiles/node-tooling/04-CLI/344/270/216/346/250/241/345/235/227/350/247/204/350/214/203.md +42 -42
- package/.agents/rules/profiles/node-tooling/05-Contract/344/270/216Schema/350/247/204/350/214/203.md +42 -42
- package/.agents/rules/profiles/node-tooling/06-/350/277/220/350/241/214/346/227/266/346/226/207/344/273/266/350/247/204/350/214/203.md +30 -30
- package/.agents/rules/profiles/node-tooling/07-/346/227/245/345/277/227/344/270/216/351/224/231/350/257/257/345/244/204/347/220/206/350/247/204/350/214/203.md +60 -60
- package/.agents/rules/profiles/node-tooling/09-/350/204/232/346/234/254/344/270/216/345/205/245/345/217/243/350/247/204/350/214/203.md +45 -45
- package/.agents/rules/profiles/node-tooling/11-/346/265/213/350/257/225/350/247/204/350/214/203.md +41 -41
- package/.agents/rules/profiles/node-tooling/13-/344/273/243/347/240/201/346/240/274/345/274/217/345/214/226/344/270/216/346/243/200/346/237/245.md +55 -55
- package/.agents/rules/profiles/react/01-/351/241/271/347/233/256/346/246/202/350/277/260.md +29 -29
- package/.agents/rules/profiles/react/03-/351/241/271/347/233/256/347/273/223/346/236/204.md +104 -104
- package/.agents/rules/profiles/react/04-/347/273/204/344/273/266/350/247/204/350/214/203.md +46 -46
- package/.agents/rules/profiles/react/05-API/350/247/204/350/214/203.md +67 -67
- package/.agents/rules/profiles/react/06-/350/267/257/347/224/261/350/247/204/350/214/203.md +54 -54
- package/.agents/rules/profiles/react/07-/347/212/266/346/200/201/347/256/241/347/220/206.md +226 -226
- package/.agents/rules/profiles/react/09-/346/240/267/345/274/217/350/247/204/350/214/203.md +71 -71
- package/.agents/rules/profiles/react/11-/346/265/213/350/257/225/350/247/204/350/214/203.md +80 -80
- package/.agents/rules/profiles/react/13-/344/273/243/347/240/201/346/240/274/345/274/217/345/214/226/344/270/216/346/243/200/346/237/245.md +159 -159
- package/.agents/rules/profiles/springboot/01-/351/241/271/347/233/256/346/246/202/350/277/260.md +31 -31
- package/.agents/rules/profiles/springboot/03-/351/241/271/347/233/256/347/273/223/346/236/204.md +37 -37
- package/.agents/rules/profiles/springboot/04-/345/210/206/345/261/202/350/247/204/350/214/203.md +33 -33
- package/.agents/rules/profiles/springboot/05-/346/216/245/345/217/243/344/270/216/345/245/221/347/272/246/350/247/204/350/214/203.md +51 -51
- package/.agents/rules/profiles/springboot/06-/346/225/260/346/215/256/350/256/277/351/227/256/350/247/204/350/214/203.md +34 -34
- package/.agents/rules/profiles/springboot/07-/351/205/215/347/275/256/344/270/216/350/277/220/350/241/214/346/227/266/350/247/204/350/214/203.md +38 -38
- package/.agents/rules/profiles/springboot/09-/345/274/202/345/270/270/344/270/216/346/227/245/345/277/227/350/247/204/350/214/203.md +48 -48
- package/.agents/rules/profiles/springboot/11-/346/265/213/350/257/225/350/247/204/350/214/203.md +43 -43
- package/.agents/rules/profiles/springboot/13-/344/273/243/347/240/201/346/240/274/345/274/217/345/214/226/344/270/216/346/243/200/346/237/245.md +48 -48
- package/.agents/rules/profiles/vue/01-/351/241/271/347/233/256/346/246/202/350/277/260.md +47 -47
- package/.agents/rules/profiles/vue/03-/351/241/271/347/233/256/347/273/223/346/236/204.md +106 -106
- package/.agents/rules/profiles/vue/04-/347/273/204/344/273/266/350/247/204/350/214/203.md +61 -61
- package/.agents/rules/profiles/vue/05-API/350/247/204/350/214/203.md +67 -67
- package/.agents/rules/profiles/vue/06-/350/267/257/347/224/261/350/247/204/350/214/203.md +69 -69
- package/.agents/rules/profiles/vue/07-/347/212/266/346/200/201/347/256/241/347/220/206.md +93 -93
- package/.agents/rules/profiles/vue/09-/346/240/267/345/274/217/350/247/204/350/214/203.md +67 -67
- package/.agents/rules/profiles/vue/11-/346/265/213/350/257/225/350/247/204/350/214/203.md +80 -80
- package/.agents/rules/profiles/vue/13-/344/273/243/347/240/201/346/240/274/345/274/217/345/214/226/344/270/216/346/243/200/346/237/245.md +159 -159
- package/.agents/skills/README.md +171 -171
- package/.agents/skills/common/archive-change/SKILL.md +180 -180
- package/.agents/skills/common/branch-code-reviewer/SKILL.md +533 -459
- package/.agents/skills/common/branch-code-reviewer/references/business-risk-guide.md +293 -293
- package/.agents/skills/common/branch-code-reviewer/references/html-template-guide.md +121 -121
- package/.agents/skills/common/config-and-secret-scan/SKILL.md +99 -99
- package/.agents/skills/common/create-proposal/SKILL.md +192 -192
- package/.agents/skills/common/create-proposal/evals/evals.json +16 -16
- package/.agents/skills/common/create-proposal/evals/train_queries.json +18 -18
- package/.agents/skills/common/create-proposal/evals/validation_queries.json +18 -18
- package/.agents/skills/common/create-proposal/references/interaction-spec-template.md +42 -42
- package/.agents/skills/common/create-test/SKILL.md +292 -292
- package/.agents/skills/common/dependency-impact-graph/SKILL.md +80 -80
- package/.agents/skills/common/execute-task/SKILL.md +206 -206
- package/.agents/skills/common/execute-task/evals/evals.json +16 -16
- package/.agents/skills/common/execute-task/evals/train_queries.json +18 -18
- package/.agents/skills/common/execute-task/evals/validation_queries.json +18 -18
- package/.agents/skills/common/find-skills/SKILL.md +144 -144
- package/.agents/skills/common/install-ai-spec-auto/SKILL.md +260 -260
- package/.agents/skills/common/install-ai-spec-auto/evals/evals.json +17 -17
- package/.agents/skills/common/install-ai-spec-auto/evals/train_queries.json +18 -18
- package/.agents/skills/common/install-ai-spec-auto/evals/validation_queries.json +18 -18
- package/.agents/skills/common/project-init/SKILL.md +178 -178
- package/.agents/skills/common/project-init/evals/evals.json +16 -16
- package/.agents/skills/common/project-init/evals/train_queries.json +18 -18
- package/.agents/skills/common/project-init/evals/validation_queries.json +18 -18
- package/.agents/skills/common/project-init/references/custom-rule-generation.md +89 -89
- package/.agents/skills/common/project-init/references/deep-scan-rules.md +67 -67
- package/.agents/skills/common/project-init/references/output-contracts.md +71 -71
- package/.agents/skills/common/project-init/references/repo-fact-gathering.md +83 -83
- package/.agents/skills/common/project-init/references/scope-resolution.md +76 -76
- package/.agents/skills/common/project-init/scripts/inspect-project.js +112 -112
- package/.agents/skills/common/skill-creator/LICENSE.txt +201 -201
- package/.agents/skills/common/skill-creator/SKILL.md +370 -370
- package/.agents/skills/common/skill-creator/evals/evals.json +16 -16
- package/.agents/skills/common/skill-creator/evals/train_queries.json +18 -18
- package/.agents/skills/common/skill-creator/evals/validation_queries.json +18 -18
- package/.agents/skills/common/skill-creator/references/output-patterns.md +82 -82
- package/.agents/skills/common/skill-creator/references/workflows.md +27 -27
- package/.agents/skills/common/skill-creator/scripts/init_skill.py +209 -209
- package/.agents/skills/common/skill-creator/scripts/package_skill.py +110 -110
- package/.agents/skills/common/skill-creator/scripts/quick_validate.py +51 -51
- package/.agents/skills/common/skill-optimizer/SKILL.md +102 -102
- package/.agents/skills/common/skill-optimizer/evals/evals.json +16 -16
- package/.agents/skills/common/skill-optimizer/evals/train_queries.json +18 -18
- package/.agents/skills/common/skill-optimizer/evals/validation_queries.json +18 -18
- package/.agents/skills/common/skill-optimizer/references/design-patterns.md +26 -26
- package/.agents/skills/common/skill-optimizer/references/review-checklist.md +22 -22
- package/.agents/skills/common/using-superpowers/SKILL.md +151 -151
- package/.agents/skills/common/wait-for-gate-signal/SKILL.md +85 -85
- package/.agents/skills/domains/README.md +19 -19
- package/.agents/skills/domains/ui-ux-pro-max/SKILL.md +58 -58
- package/.agents/skills/domains/web/design-analysis/SKILL.md +89 -89
- package/.agents/skills/domains/web/design-analysis/rules/analysis-order.md +61 -61
- package/.agents/skills/domains/web/design-analysis/rules/analysis-priorities.md +136 -136
- package/.agents/skills/domains/web/design-analysis/rules/checklist-common-misses.md +107 -107
- package/.agents/skills/domains/web/design-analysis/rules/implementation-common-errors.md +204 -204
- package/.agents/skills/domains/web/design-analysis/rules/implementation-guidelines.md +211 -211
- package/.agents/skills/domains/web/design-analysis/rules/output-analysis-checklist.md +247 -247
- package/.agents/skills/domains/web/design-analysis/rules/tools-design-guidelines.md +108 -108
- package/.agents/skills/domains/web/design-analysis/rules/workflow-element-extraction.md +162 -162
- package/.agents/skills/domains/web/design-analysis/rules/workflow-layout-map.md +131 -131
- package/.agents/skills/domains/web/design-analysis/rules/workflow-output-checklist.md +70 -70
- package/.agents/skills/domains/web/design-analysis/rules/workflow-style-summary.md +91 -91
- package/.agents/skills/domains/web/route-permission-map/SKILL.md +103 -103
- package/.agents/skills/domains/web/ui-verification/SKILL.md +114 -114
- package/.agents/skills/domains/web/ui-verification/evals/evals.json +16 -16
- package/.agents/skills/domains/web/ui-verification/evals/train_queries.json +18 -18
- package/.agents/skills/domains/web/ui-verification/evals/validation_queries.json +18 -18
- package/.agents/skills/domains/web/ui-verification/rules/comparison-content-image.md +34 -34
- package/.agents/skills/domains/web/ui-verification/rules/comparison-content-text.md +30 -30
- package/.agents/skills/domains/web/ui-verification/rules/comparison-hierarchy.md +33 -33
- package/.agents/skills/domains/web/ui-verification/rules/comparison-layout.md +35 -35
- package/.agents/skills/domains/web/ui-verification/rules/errors-alignment.md +42 -42
- package/.agents/skills/domains/web/ui-verification/rules/errors-button-dimensions.md +28 -28
- package/.agents/skills/domains/web/ui-verification/rules/errors-button-position.md +25 -25
- package/.agents/skills/domains/web/ui-verification/rules/errors-css-priority.md +50 -50
- package/.agents/skills/domains/web/ui-verification/rules/errors-flex-column-width.md +46 -46
- package/.agents/skills/domains/web/ui-verification/rules/errors-flex-layout.md +46 -46
- package/.agents/skills/domains/web/ui-verification/rules/errors-grid-container-width.md +44 -44
- package/.agents/skills/domains/web/ui-verification/rules/errors-page-container-width.md +39 -39
- package/.agents/skills/domains/web/ui-verification/rules/tools-browser-navigation.md +53 -53
- package/.agents/skills/domains/web/ui-verification/rules/tools-design-guidelines.md +53 -53
- package/.agents/skills/domains/web/ui-verification/rules/workflow-checklist.md +27 -27
- package/.agents/skills/domains/web/ui-verification/rules/workflow-problem-list.md +56 -56
- package/.agents/skills/domains/web/ui-verification/rules/workflow-reflection.md +44 -44
- package/.agents/skills/domains/web/ui-verification/rules/writing-alignment.md +44 -44
- package/.agents/skills/domains/web/ui-verification/rules/writing-element-completeness.md +63 -63
- package/.agents/skills/domains/web/ui-verification/rules/writing-list-layout.md +75 -75
- package/.agents/skills/domains/web/ui-verification/rules/writing-page-container-width.md +37 -37
- package/.agents/skills/domains/web/web-design-guidelines/SKILL.md +40 -40
- package/.agents/skills/profiles/nestjs/README.md +4 -4
- package/.agents/skills/profiles/node-tooling/README.md +9 -9
- package/.agents/skills/profiles/react/create-api/SKILL.md +145 -145
- package/.agents/skills/profiles/react/create-component/SKILL.md +160 -160
- package/.agents/skills/profiles/react/create-route/SKILL.md +168 -168
- package/.agents/skills/profiles/react/create-store/SKILL.md +262 -262
- package/.agents/skills/profiles/react/theme-variables/SKILL.md +82 -82
- package/.agents/skills/profiles/react/vercel-composition-patterns/AGENTS.md +899 -899
- package/.agents/skills/profiles/react/vercel-composition-patterns/SKILL.md +81 -81
- package/.agents/skills/profiles/react/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md +100 -100
- package/.agents/skills/profiles/react/vercel-composition-patterns/rules/architecture-compound-components.md +112 -112
- package/.agents/skills/profiles/react/vercel-composition-patterns/rules/patterns-children-over-render-props.md +87 -87
- package/.agents/skills/profiles/react/vercel-composition-patterns/rules/patterns-explicit-variants.md +100 -100
- package/.agents/skills/profiles/react/vercel-composition-patterns/rules/state-context-interface.md +191 -191
- package/.agents/skills/profiles/react/vercel-composition-patterns/rules/state-decouple-implementation.md +113 -113
- package/.agents/skills/profiles/react/vercel-composition-patterns/rules/state-lift-state.md +125 -125
- package/.agents/skills/profiles/react/vercel-react-best-practices/AGENTS.md +2934 -2934
- package/.agents/skills/profiles/react/vercel-react-best-practices/SKILL.md +136 -136
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -55
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/advanced-init-once.md +42 -42
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/advanced-use-latest.md +39 -39
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/async-api-routes.md +38 -38
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/async-defer-await.md +80 -80
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/async-dependencies.md +51 -51
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/async-parallel.md +28 -28
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -99
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -59
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/bundle-conditional.md +31 -31
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -49
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -35
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/bundle-preload.md +50 -50
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/client-event-listeners.md +74 -74
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -71
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -48
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/client-swr-dedup.md +56 -56
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -107
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-cache-function-results.md +80 -80
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-cache-property-access.md +28 -28
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-cache-storage.md +70 -70
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-combine-iterations.md +32 -32
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-early-exit.md +50 -50
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -45
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-index-maps.md +37 -37
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-length-check-first.md +49 -49
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-min-max-loop.md +82 -82
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -24
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -57
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-activity.md +26 -26
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -47
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -40
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -38
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -46
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -82
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -30
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -28
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -75
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -39
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-dependencies.md +45 -45
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -40
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-derived-state.md +29 -29
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -74
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -58
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -38
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-memo.md +44 -44
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -45
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -35
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-transitions.md +40 -40
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -73
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -73
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/server-auth-actions.md +96 -96
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/server-cache-lru.md +41 -41
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/server-cache-react.md +76 -76
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/server-dedup-props.md +65 -65
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -83
- package/.agents/skills/profiles/react/vercel-react-best-practices/rules/server-serialization.md +38 -38
- package/.agents/skills/profiles/springboot/README.md +10 -10
- package/.agents/skills/profiles/vue/create-api/SKILL.md +105 -105
- package/.agents/skills/profiles/vue/create-component/SKILL.md +76 -76
- package/.agents/skills/profiles/vue/create-route/SKILL.md +141 -141
- package/.agents/skills/profiles/vue/create-store/SKILL.md +97 -97
- package/.agents/skills/profiles/vue/create-view/SKILL.md +81 -81
- package/.agents/skills/profiles/vue/theme-variables/SKILL.md +73 -73
- package/.agents/skills/profiles/vue/vue-best-practices/SKILL.md +166 -166
- package/.agents/skills/profiles/vue/vue-best-practices/references/animation-class-based-technique.md +254 -254
- package/.agents/skills/profiles/vue/vue-best-practices/references/animation-state-driven-technique.md +291 -291
- package/.agents/skills/profiles/vue/vue-best-practices/references/component-async.md +97 -97
- package/.agents/skills/profiles/vue/vue-best-practices/references/component-data-flow.md +307 -307
- package/.agents/skills/profiles/vue/vue-best-practices/references/component-fallthrough-attrs.md +174 -174
- package/.agents/skills/profiles/vue/vue-best-practices/references/component-keep-alive.md +137 -137
- package/.agents/skills/profiles/vue/vue-best-practices/references/component-slots.md +216 -216
- package/.agents/skills/profiles/vue/vue-best-practices/references/component-suspense.md +228 -228
- package/.agents/skills/profiles/vue/vue-best-practices/references/component-teleport.md +108 -108
- package/.agents/skills/profiles/vue/vue-best-practices/references/component-transition-group.md +128 -128
- package/.agents/skills/profiles/vue/vue-best-practices/references/component-transition.md +125 -125
- package/.agents/skills/profiles/vue/vue-best-practices/references/composables.md +290 -290
- package/.agents/skills/profiles/vue/vue-best-practices/references/directives.md +162 -162
- package/.agents/skills/profiles/vue/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +159 -159
- package/.agents/skills/profiles/vue/vue-best-practices/references/perf-v-once-v-memo-directives.md +182 -182
- package/.agents/skills/profiles/vue/vue-best-practices/references/perf-virtualize-large-lists.md +187 -187
- package/.agents/skills/profiles/vue/vue-best-practices/references/plugins.md +166 -166
- package/.agents/skills/profiles/vue/vue-best-practices/references/reactivity.md +344 -344
- package/.agents/skills/profiles/vue/vue-best-practices/references/render-functions.md +201 -201
- package/.agents/skills/profiles/vue/vue-best-practices/references/sfc.md +310 -310
- package/.agents/skills/profiles/vue/vue-best-practices/references/state-management.md +135 -135
- package/.agents/skills/profiles/vue/vue-best-practices/references/updated-hook-performance.md +187 -187
- package/.agents/templates/common/README.md +23 -23
- package/.agents/templates/common/bugfix.md +22 -22
- package/.agents/templates/common/create-expert-package.md +458 -458
- package/.agents/templates/common/mock-page.md +28 -28
- package/.agents/templates/common/new-component.md +25 -25
- package/.agents/templates/common/new-page.md +31 -31
- package/.cursor/mcp.json +35 -35
- package/.qoder/mcp.json +26 -26
- package/bin/archive-change.js +560 -474
- package/bin/check-command.js +62 -62
- package/bin/cli.js +0 -0
- package/bin/command-template-renderer.js +40 -40
- package/bin/context-command.js +102 -102
- package/bin/demo-runtime-smoke.js +760 -760
- package/bin/execution-semantics.js +821 -821
- package/bin/executor-command.js +93 -93
- package/bin/expert-dispatch.js +334 -334
- package/bin/expert-executor.js +1148 -1148
- package/bin/guard-command.js +52 -52
- package/bin/hub-command.js +876 -876
- package/bin/ide-command.js +242 -242
- package/bin/init-command.js +193 -193
- package/bin/install-workflow.js +35 -3
- package/bin/manifest-export.js +34 -34
- package/bin/profile-registry.js +90 -90
- package/bin/protocol-workflow.js +452 -446
- package/bin/repair-command.js +161 -161
- package/bin/repo-map.js +177 -177
- package/bin/report-command.js +236 -236
- package/bin/runtime-bootstrap.js +428 -428
- package/bin/runtime-embedded.js +101 -101
- package/bin/runtime-fallback.js +106 -106
- package/bin/runtime-launcher.js +116 -116
- package/bin/runtime-paths.js +177 -177
- package/bin/runtime-registry.js +289 -289
- package/bin/runtime-state.js +2541 -2541
- package/bin/scan.js +96 -96
- package/bin/self-upgrade.js +206 -206
- package/bin/skill-spec-validator.js +457 -457
- package/bin/spec-command.js +366 -366
- package/bin/superpowers.js +384 -384
- package/bin/sync-command.js +59 -59
- package/bin/sync.js +1904 -1904
- package/bin/task-orchestrator-adapter.js +341 -341
- package/bin/task-orchestrator-extractor.js +274 -274
- package/bin/task-orchestrator-runner.js +1208 -1208
- package/bin/telemetry/README.md +66 -66
- package/bin/telemetry/aspect.js +153 -153
- package/bin/telemetry/collect.js +67 -67
- package/bin/telemetry/config.js +114 -114
- package/bin/telemetry/defaults.json +5 -5
- package/bin/telemetry/healthcheck.js +195 -195
- package/bin/telemetry/identity.js +53 -53
- package/bin/telemetry/index.js +25 -25
- package/bin/telemetry/reporter.js +83 -83
- package/bin/telemetry/safe.js +39 -39
- package/bin/validate-registry.js +740 -740
- package/bin/visual-bridge-config.js +117 -117
- package/bin/visual-bridge.js +287 -287
- package/bin/visual-command.js +432 -432
- package/bin/worktree-command.js +194 -194
- package/configs/common/.editorconfig +15 -15
- package/configs/common/.husky/commit-msg +4 -4
- package/configs/common/.husky/pre-commit +4 -4
- package/configs/common/.lintstagedrc +11 -11
- package/configs/common/.prettierignore +11 -11
- package/configs/common/.prettierrc.json +11 -11
- package/configs/common/.stylelintignore +14 -14
- package/configs/common/.stylelintrc.json +21 -21
- package/configs/common/commitlint.config.js +3 -3
- package/configs/profiles/nestjs/.gitkeep +1 -1
- package/configs/profiles/node-tooling/.gitkeep +1 -1
- package/configs/profiles/react/.eslintignore +6 -6
- package/configs/profiles/react/.eslintrc.js +16 -16
- package/configs/profiles/react/.stylelintrc.json +18 -18
- package/configs/profiles/springboot/.gitkeep +1 -1
- package/configs/profiles/vue/.eslintignore +6 -6
- package/configs/profiles/vue/.eslintrc.cjs +17 -17
- package/contracts/README.md +28 -28
- package/contracts/fixtures/asset-package.fixture.json +26 -26
- package/contracts/fixtures/asset-usage-feedback.fixture.json +14 -14
- package/contracts/fixtures/evidence-report.fixture.json +28 -28
- package/contracts/fixtures/manifest.fixture.json +20 -20
- package/contracts/fixtures/run-event.fixture.json +15 -15
- package/contracts/schemas/asset-package.schema.json +76 -76
- package/contracts/schemas/asset-usage-feedback.schema.json +57 -57
- package/contracts/schemas/evidence-report.schema.json +60 -60
- package/contracts/schemas/manifest.schema.json +63 -63
- package/contracts/schemas/run-event.schema.json +72 -72
- package/install.ps1 +35 -35
- package/install.sh +17 -17
- package/internal/ai-protocol-workflow.js +5824 -5600
- package/internal/hub-client.js +98 -98
- package/internal/hub-sync-selection.js +69 -69
- package/internal/visual-hooks/README.md +481 -481
- package/internal/visual-hooks/config-loader.js +218 -218
- package/internal/visual-hooks/control-puller.js +206 -206
- package/internal/visual-hooks/gate-signal.js +150 -150
- package/internal/visual-hooks/inbox-consumer.js +469 -469
- package/internal/visual-hooks/index.js +197 -197
- package/internal/visual-hooks/push-client.js +189 -189
- package/internal/visual-hooks/receipt-pusher.js +176 -176
- package/internal/visual-hooks/runtime-state-pusher.js +128 -128
- package/openspec/config.yaml.template +52 -52
- package/openspec/schemas/expert-delivery/schema.yaml +68 -68
- package/openspec/schemas/expert-delivery/templates/checklist.md +39 -39
- package/openspec/schemas/expert-delivery/templates/design.md +61 -61
- package/openspec/schemas/expert-delivery/templates/iterations.md +25 -25
- package/openspec/schemas/expert-delivery/templates/proposal.md +45 -45
- package/openspec/schemas/expert-delivery/templates/spec.md +29 -29
- package/openspec/schemas/expert-delivery/templates/tasks.md +24 -24
- package/package.json +1 -1
- package/scripts/acceptance-zero-intrusion.sh +168 -168
- package/scripts/hub-sync-assets.config.example.json +296 -296
- package/scripts/hub-sync-assets.js +2038 -2038
- package/scripts/local-verify.sh +280 -280
- package/scripts/post-publish-auto-fix-check.js +404 -404
- package/scripts/post-publish-verify.sh +175 -175
- package/scripts/setup-cursor-manual-test.sh +107 -107
- package/scripts/setup-cursor-spec-archive-test.sh +111 -111
- package/scripts/setup-visual-integration.sh +225 -225
- package/scripts/test-integration.sh +176 -176
- package/scripts/update-test-project.sh +93 -93
- package/scripts/upload-four-web.sh +57 -57
- package/scripts/verify-install-ps1-bom.js +26 -26
- package/src/agent/agent-context.js +259 -259
- package/src/agent/agent-profile.js +185 -185
- package/src/agent/agent-templates.js +161 -161
- package/src/agent/agent-types.js +108 -108
- package/src/agent/collaboration-protocol.js +333 -333
- package/src/agent/conflict-handler.js +364 -364
- package/src/agent/file-permission.js +121 -121
- package/src/agent/index.js +38 -38
- package/src/agent/permission-audit.js +151 -151
- package/src/agent/review-repair-loop.js +270 -270
- package/src/agent/tool-permission.js +101 -101
- package/src/asset/asset-dependency.js +322 -322
- package/src/asset/asset-feedback.js +350 -350
- package/src/asset/asset-fork.js +300 -300
- package/src/asset/asset-install.js +278 -278
- package/src/asset/asset-installer.js +497 -497
- package/src/asset/asset-lifecycle.js +324 -324
- package/src/asset/asset-manager.js +245 -245
- package/src/asset/asset-package-manager.js +349 -349
- package/src/asset/asset-package.js +186 -186
- package/src/asset/asset-quality.js +262 -262
- package/src/asset/asset-registry.js +387 -387
- package/src/asset/asset-version.js +293 -293
- package/src/asset/index.js +86 -86
- package/src/cache/agent-profile-cache.js +59 -59
- package/src/cache/asset-cache.js +63 -63
- package/src/cache/global-cache.js +61 -61
- package/src/cache/manifest-cache.js +30 -30
- package/src/check/check-service.js +32 -32
- package/src/config/config-layer.js +343 -343
- package/src/config/config-loader.js +60 -60
- package/src/config/defaults.js +49 -49
- package/src/connectors/hub/asset-package.js +72 -72
- package/src/connectors/hub/asset-usage-feedback.js +46 -46
- package/src/connectors/hub/hub-connector.js +44 -44
- package/src/connectors/hub/index.js +21 -21
- package/src/connectors/visual/evidence-report.js +49 -49
- package/src/connectors/visual/index.js +15 -15
- package/src/connectors/visual/queue.js +41 -41
- package/src/connectors/visual/run-event.js +81 -81
- package/src/connectors/visual/visual-connector.js +77 -77
- package/src/context/context-budget.js +59 -59
- package/src/context/context-builder.js +285 -285
- package/src/context/context-loader.js +116 -116
- package/src/context/context-planner.js +158 -158
- package/src/context/types.js +96 -96
- package/src/contracts/index.js +63 -63
- package/src/executor/executor-registry.js +78 -78
- package/src/executor/executor-result-parser.js +44 -44
- package/src/executor/executor-runner.js +141 -141
- package/src/executor/executor-selector.js +139 -139
- package/src/executor/executor-timeout.js +36 -36
- package/src/executor/providers/base-provider-utils.js +189 -189
- package/src/executor/providers/claude-code-executor-provider.js +128 -128
- package/src/executor/providers/codex-executor-provider.js +126 -126
- package/src/executor/providers/cursor-executor-provider.js +99 -99
- package/src/executor/types.js +137 -137
- package/src/git/branch-manager.js +71 -71
- package/src/git/dirty-checker.js +43 -43
- package/src/git/dirty-strategy-handler.js +29 -29
- package/src/git/git-command.js +37 -37
- package/src/git/git-repository-detector.js +45 -45
- package/src/git/multi-repo-worktree-planner.js +88 -88
- package/src/git/policy.js +19 -19
- package/src/git/strategies/block-dirty-strategy.js +34 -34
- package/src/git/strategies/ignore-dirty-strategy.js +33 -33
- package/src/git/strategies/patch-snapshot-strategy.js +53 -53
- package/src/git/strategies/wip-commit-strategy.js +38 -38
- package/src/git/types.js +71 -71
- package/src/git/worktree-manager.js +85 -85
- package/src/governance/asset-review.js +351 -351
- package/src/governance/audit-log.js +368 -368
- package/src/governance/gray-release.js +312 -312
- package/src/governance/index.js +31 -31
- package/src/governance/policy-types.js +56 -56
- package/src/governance/rbac-types.js +171 -171
- package/src/governance/rbac.js +382 -382
- package/src/governance/rollback.js +360 -360
- package/src/governance/security-policy.js +354 -354
- package/src/hook/hook-config-writer.js +125 -125
- package/src/hub/hub-client.js +186 -186
- package/src/hub/hub-config.js +39 -39
- package/src/hub/project-facts.js +31 -31
- package/src/hub/runtime-feedback-reporter.js +55 -55
- package/src/ide/adapters/adapter-protocol.js +385 -385
- package/src/ide/adapters/claude-adapter.js +419 -419
- package/src/ide/adapters/codex-adapter.js +60 -60
- package/src/ide/adapters/cursor-adapter.js +484 -484
- package/src/ide/adapters/index.js +24 -24
- package/src/ide/anchors/markdown-anchor-writer.js +152 -152
- package/src/ide/ide-service.js +270 -270
- package/src/ide/ide-types.js +94 -94
- package/src/ide/links/link-mode-resolver.js +160 -160
- package/src/ide/registry/ide-registry-builder.js +165 -165
- package/src/incident/incident-writer.js +47 -47
- package/src/incident/types.js +22 -22
- package/src/init/ide-linker.js +126 -126
- package/src/init/ide-pointer-injector.js +75 -75
- package/src/init/init-applier.js +197 -197
- package/src/init/init-plan.js +294 -294
- package/src/init/init-service.js +65 -65
- package/src/init/manifest-installer.js +302 -302
- package/src/init/types.js +26 -26
- package/src/project/config-writer.js +83 -83
- package/src/project/context-index-writer.js +82 -82
- package/src/project/json-utils.js +72 -72
- package/src/project/local-state-writer.js +50 -50
- package/src/project/lock-file-writer.js +98 -98
- package/src/project/manifest-writer.js +126 -126
- package/src/project/policy-config-writer.js +91 -91
- package/src/project/project-config-writer.js +74 -74
- package/src/project/project-files.js +39 -39
- package/src/project/registry-index-writer.js +43 -43
- package/src/project/workspace-config-writer.js +63 -63
- package/src/run/index.js +11 -11
- package/src/run/run-id.js +32 -32
- package/src/run/run-service.js +269 -269
- package/src/run/run-store.js +80 -80
- package/src/scanner/aggregator/detection-aggregator.js +23 -23
- package/src/scanner/boundary/boundary-resolver.js +229 -229
- package/src/scanner/detectors/detector-registry.js +44 -44
- package/src/scanner/detectors/fastapi-detector.js +46 -46
- package/src/scanner/detectors/go-detector.js +46 -46
- package/src/scanner/detectors/nestjs-detector.js +57 -57
- package/src/scanner/detectors/nextjs-detector.js +52 -52
- package/src/scanner/detectors/react-vite-detector.js +52 -52
- package/src/scanner/detectors/react-webpack-detector.js +57 -57
- package/src/scanner/detectors/springboot-detector.js +46 -46
- package/src/scanner/detectors/springcloud-detector.js +46 -46
- package/src/scanner/detectors/springmvc-detector.js +46 -46
- package/src/scanner/detectors/vue-vite-detector.js +52 -52
- package/src/scanner/engine.js +72 -72
- package/src/scanner/facts/fact-extractor.js +211 -211
- package/src/scanner/types.js +30 -30
- package/src/security/asset-tamper-checker.js +188 -188
- package/src/security/checksum.js +40 -40
- package/src/spec/spec-writer.js +302 -302
- package/src/state-machine/circuit-breaker.js +112 -112
- package/src/state-machine/escape-hatch.js +49 -49
- package/src/state-machine/stage-runner.js +281 -281
- package/src/state-machine/state-machine.js +24 -24
- package/src/state-machine/transition-guard.js +36 -36
- package/src/state-machine/types.js +37 -37
- package/src/sync/sync-service.js +192 -192
- package/src/visual/agent-visual.js +142 -142
- package/src/visual/event-gateway.js +357 -357
- package/src/visual/event-mapper.js +128 -128
- package/src/visual/hook-dashboard.js +216 -216
- package/src/visual/index.js +27 -27
- package/src/visual/metrics.js +287 -287
- package/src/visual/privacy-filter.js +100 -100
- package/src/visual/risk-board.js +252 -252
- package/src/visual/timeline.js +245 -245
- package/src/visual/visual-client.js +94 -94
- package/src/visual/visual-config.js +40 -40
- package/src/visual/visual-reporter.js +88 -88
|
@@ -1,2038 +1,2038 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const {
|
|
6
|
-
collectRelatedAssetIdsFromScenarios,
|
|
7
|
-
mergeSelectionWithDerivedIds,
|
|
8
|
-
} = require("../internal/hub-sync-selection");
|
|
9
|
-
|
|
10
|
-
const PROJECT_ROOT = process.cwd();
|
|
11
|
-
const DEFAULT_BASE_URL = "http://localhost:3000";
|
|
12
|
-
const DEFAULT_HUB_PROJECT = path.resolve(PROJECT_ROOT, "../skill-q-platform");
|
|
13
|
-
const DEFAULT_CONFIG_PATH = path.resolve(PROJECT_ROOT, "scripts/hub-sync-assets.config.json");
|
|
14
|
-
const DEFAULT_CONFIG_EXAMPLE_PATH = path.resolve(
|
|
15
|
-
PROJECT_ROOT,
|
|
16
|
-
"scripts/hub-sync-assets.config.example.json",
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
const TEXT_FILE_EXTENSIONS = new Set([
|
|
20
|
-
".md",
|
|
21
|
-
".mdx",
|
|
22
|
-
".txt",
|
|
23
|
-
".json",
|
|
24
|
-
".jsonc",
|
|
25
|
-
".yaml",
|
|
26
|
-
".yml",
|
|
27
|
-
".js",
|
|
28
|
-
".cjs",
|
|
29
|
-
".mjs",
|
|
30
|
-
".ts",
|
|
31
|
-
".tsx",
|
|
32
|
-
".jsx",
|
|
33
|
-
".css",
|
|
34
|
-
".scss",
|
|
35
|
-
".sass",
|
|
36
|
-
".less",
|
|
37
|
-
".html",
|
|
38
|
-
".xml",
|
|
39
|
-
".svg",
|
|
40
|
-
".sh",
|
|
41
|
-
".ps1",
|
|
42
|
-
".py",
|
|
43
|
-
".sql",
|
|
44
|
-
".toml",
|
|
45
|
-
".env",
|
|
46
|
-
".gitignore",
|
|
47
|
-
".npmrc",
|
|
48
|
-
]);
|
|
49
|
-
|
|
50
|
-
function main() {
|
|
51
|
-
const options = parseArgs(process.argv.slice(2));
|
|
52
|
-
if (options.help) {
|
|
53
|
-
printHelp();
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
run(options).catch((error) => {
|
|
58
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
59
|
-
console.error(`[hub-sync] failed: ${message}`);
|
|
60
|
-
process.exitCode = 1;
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function run(cliOptions) {
|
|
65
|
-
const config = loadConfig(cliOptions.configPath);
|
|
66
|
-
const resolved = resolveRuntimeOptions(cliOptions, config);
|
|
67
|
-
const client = new HubClient(resolved);
|
|
68
|
-
|
|
69
|
-
const shouldUseAdminSession =
|
|
70
|
-
!resolved.skipRoles ||
|
|
71
|
-
!resolved.skipScenarios ||
|
|
72
|
-
resolved.hasAdminAuthInput;
|
|
73
|
-
if (shouldUseAdminSession && !client.hasAdminAccess()) {
|
|
74
|
-
await client.ensureAdminSession();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const categories = resolved.skipSkills && resolved.skipRules
|
|
78
|
-
? { skill: [], rule: [] }
|
|
79
|
-
: client.hasAdminAccess()
|
|
80
|
-
? await loadCategories(client, resolved)
|
|
81
|
-
: { skill: [], rule: [] };
|
|
82
|
-
const skillBrowseItems = !client.hasAdminAccess() || (resolved.skipSkills && resolved.skipRoles && resolved.skipScenarios)
|
|
83
|
-
? []
|
|
84
|
-
: await loadBrowseItems(client, "skill", resolved);
|
|
85
|
-
const ruleBrowseItems = !client.hasAdminAccess() || (resolved.skipRules && resolved.skipRoles && resolved.skipScenarios)
|
|
86
|
-
? []
|
|
87
|
-
: await loadBrowseItems(client, "rule", resolved);
|
|
88
|
-
const roleResponse = resolved.skipRoles && resolved.skipScenarios
|
|
89
|
-
? { items: [] }
|
|
90
|
-
: await client.getJson("/api/admin/roles");
|
|
91
|
-
const scenarioResponse = resolved.skipScenarios
|
|
92
|
-
? { items: [] }
|
|
93
|
-
: await client.getJson("/api/admin/scenarios");
|
|
94
|
-
|
|
95
|
-
const localRegistries = {
|
|
96
|
-
skills: readJson(path.resolve(PROJECT_ROOT, ".agents/registry/skills.json")).skills || {},
|
|
97
|
-
rules: readJson(path.resolve(PROJECT_ROOT, ".agents/registry/rules.json")).rules || {},
|
|
98
|
-
roles: readJson(path.resolve(PROJECT_ROOT, ".agents/registry/roles.json")).roles || {},
|
|
99
|
-
scenarios:
|
|
100
|
-
readJson(path.resolve(PROJECT_ROOT, ".agents/registry/scenario-packages.json")).scenario_packages || {},
|
|
101
|
-
};
|
|
102
|
-
const relatedScenarioIds = selectResourceIds(
|
|
103
|
-
localRegistries.scenarios,
|
|
104
|
-
resolved.fromScenarioSelection,
|
|
105
|
-
);
|
|
106
|
-
const relatedAssets = collectRelatedAssetIdsFromScenarios({
|
|
107
|
-
scenarioIds: relatedScenarioIds,
|
|
108
|
-
localScenarios: localRegistries.scenarios,
|
|
109
|
-
localRoles: localRegistries.roles,
|
|
110
|
-
});
|
|
111
|
-
resolved.roleSelection = mergeSelectionWithDerivedIds({
|
|
112
|
-
selection: resolved.roleSelection,
|
|
113
|
-
selectionSpecified: resolved.roleSelectionSpecified,
|
|
114
|
-
derivedIds: relatedAssets.roleIds,
|
|
115
|
-
preferDerivedWhenImplicitAll: relatedScenarioIds.length > 0,
|
|
116
|
-
});
|
|
117
|
-
resolved.skillSelection = mergeSelectionWithDerivedIds({
|
|
118
|
-
selection: resolved.skillSelection,
|
|
119
|
-
selectionSpecified: resolved.skillSelectionSpecified,
|
|
120
|
-
derivedIds: relatedAssets.skillIds,
|
|
121
|
-
preferDerivedWhenImplicitAll: relatedScenarioIds.length > 0,
|
|
122
|
-
});
|
|
123
|
-
resolved.skipRoles = isSelectionNone(resolved.roleSelection);
|
|
124
|
-
resolved.skipSkills = isSelectionNone(resolved.skillSelection);
|
|
125
|
-
|
|
126
|
-
const hubState = {
|
|
127
|
-
categories,
|
|
128
|
-
skillsBySlug: indexBy(skillBrowseItems.items || [], "slug"),
|
|
129
|
-
rulesBySlug: indexBy(ruleBrowseItems.items || [], "slug"),
|
|
130
|
-
rolesBySlug: indexBy(roleResponse.items || [], "slug"),
|
|
131
|
-
scenariosBySlug: indexBy(scenarioResponse.items || [], "slug"),
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const summary = {
|
|
135
|
-
skill: { created: 0, updated: 0, versioned: 0, skipped: 0 },
|
|
136
|
-
rule: { created: 0, updated: 0, versioned: 0, skipped: 0 },
|
|
137
|
-
role: { created: 0, updated: 0, versioned: 0, skipped: 0 },
|
|
138
|
-
scenario: { created: 0, updated: 0, skipped: 0 },
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
if (!resolved.skipRules) {
|
|
142
|
-
await syncRules({
|
|
143
|
-
client,
|
|
144
|
-
resolved,
|
|
145
|
-
config,
|
|
146
|
-
localRules: localRegistries.rules,
|
|
147
|
-
hubState,
|
|
148
|
-
summary,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (!resolved.skipSkills) {
|
|
153
|
-
await syncSkills({
|
|
154
|
-
client,
|
|
155
|
-
resolved,
|
|
156
|
-
config,
|
|
157
|
-
localSkills: localRegistries.skills,
|
|
158
|
-
hubState,
|
|
159
|
-
summary,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (!resolved.skipRoles) {
|
|
164
|
-
await syncRoles({
|
|
165
|
-
client,
|
|
166
|
-
resolved,
|
|
167
|
-
config,
|
|
168
|
-
localRoles: localRegistries.roles,
|
|
169
|
-
hubState,
|
|
170
|
-
summary,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (!resolved.skipScenarios) {
|
|
175
|
-
await syncScenarios({
|
|
176
|
-
client,
|
|
177
|
-
resolved,
|
|
178
|
-
config,
|
|
179
|
-
localScenarios: localRegistries.scenarios,
|
|
180
|
-
hubState,
|
|
181
|
-
summary,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
printSummary(summary, resolved.dryRun);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function parseArgs(argv) {
|
|
189
|
-
const args = {
|
|
190
|
-
help: false,
|
|
191
|
-
dryRun: false,
|
|
192
|
-
baseUrl: undefined,
|
|
193
|
-
hubProject: undefined,
|
|
194
|
-
configPath: DEFAULT_CONFIG_PATH,
|
|
195
|
-
adminEmail: undefined,
|
|
196
|
-
adminPassword: undefined,
|
|
197
|
-
adminCookie: undefined,
|
|
198
|
-
adminSecret: undefined,
|
|
199
|
-
agentApiKey: undefined,
|
|
200
|
-
skills: undefined,
|
|
201
|
-
rules: undefined,
|
|
202
|
-
roles: undefined,
|
|
203
|
-
scenarios: undefined,
|
|
204
|
-
fromScenarios: undefined,
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
208
|
-
const current = argv[index];
|
|
209
|
-
if (current === "--help" || current === "-h") {
|
|
210
|
-
args.help = true;
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
if (current === "--dry-run") {
|
|
214
|
-
args.dryRun = true;
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
const next = argv[index + 1];
|
|
218
|
-
if (current === "--base-url") {
|
|
219
|
-
args.baseUrl = next;
|
|
220
|
-
index += 1;
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (current === "--hub-project") {
|
|
224
|
-
args.hubProject = next;
|
|
225
|
-
index += 1;
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
if (current === "--config") {
|
|
229
|
-
args.configPath = next ? path.resolve(PROJECT_ROOT, next) : DEFAULT_CONFIG_PATH;
|
|
230
|
-
index += 1;
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
if (current === "--admin-email") {
|
|
234
|
-
args.adminEmail = next;
|
|
235
|
-
index += 1;
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
if (current === "--admin-password") {
|
|
239
|
-
args.adminPassword = next;
|
|
240
|
-
index += 1;
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
if (current === "--admin-cookie") {
|
|
244
|
-
args.adminCookie = next;
|
|
245
|
-
index += 1;
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
if (current === "--admin-secret") {
|
|
249
|
-
args.adminSecret = next;
|
|
250
|
-
index += 1;
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
if (current === "--agent-api-key") {
|
|
254
|
-
args.agentApiKey = next;
|
|
255
|
-
index += 1;
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
if (current === "--skills") {
|
|
259
|
-
args.skills = next;
|
|
260
|
-
index += 1;
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
if (current === "--rules") {
|
|
264
|
-
args.rules = next;
|
|
265
|
-
index += 1;
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
if (current === "--roles") {
|
|
269
|
-
args.roles = next;
|
|
270
|
-
index += 1;
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
if (current === "--scenarios") {
|
|
274
|
-
args.scenarios = next;
|
|
275
|
-
index += 1;
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
if (current === "--from-scenarios") {
|
|
279
|
-
args.fromScenarios = next;
|
|
280
|
-
index += 1;
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
throw new Error(`unknown argument: ${current}`);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return args;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function printHelp() {
|
|
290
|
-
console.log(`
|
|
291
|
-
Usage:
|
|
292
|
-
node ./scripts/hub-sync-assets.js [options]
|
|
293
|
-
|
|
294
|
-
Options:
|
|
295
|
-
--dry-run Only print planned operations
|
|
296
|
-
--base-url <url> Hub base url, default http://localhost:3000
|
|
297
|
-
--hub-project <path> Hub project path, default ../skill-q-platform
|
|
298
|
-
--config <path> Private config path, default scripts/hub-sync-assets.config.json
|
|
299
|
-
--admin-email <email> Hub admin email for login
|
|
300
|
-
--admin-password <pwd> Hub admin password for login
|
|
301
|
-
--admin-cookie <cookie> Existing admin_session cookie
|
|
302
|
-
--admin-secret <secret> HUB_ADMIN_SECRET, used for skill/rule author bypass and admin API bypass
|
|
303
|
-
--agent-api-key <key> Agent API key, required when Hub enforces upload login for skill/rule version updates
|
|
304
|
-
--skills <all|csv|none> Sync selected skills
|
|
305
|
-
--rules <all|csv|none> Sync selected rules
|
|
306
|
-
--roles <all|csv|none> Sync selected roles
|
|
307
|
-
--scenarios <all|csv|none> Sync selected scenarios
|
|
308
|
-
--from-scenarios <all|csv|none>
|
|
309
|
-
Expand related roles and skills from scenario packages
|
|
310
|
-
--help Show help
|
|
311
|
-
|
|
312
|
-
Examples:
|
|
313
|
-
node ./scripts/hub-sync-assets.js --dry-run
|
|
314
|
-
node ./scripts/hub-sync-assets.js --skills create-api,create-route --rules none
|
|
315
|
-
node ./scripts/hub-sync-assets.js --from-scenarios change-to-release --rules none --scenarios none
|
|
316
|
-
node ./scripts/hub-sync-assets.js --config scripts/hub-sync-assets.config.json
|
|
317
|
-
|
|
318
|
-
Notes:
|
|
319
|
-
- If you pass http://localhost:3000/admin, the script will normalize it to http://localhost:3000.
|
|
320
|
-
- skill/rule can run without admin login when your local Hub allows direct upload APIs.
|
|
321
|
-
- Existing skill/rule resources need version publishing for file changes. If Hub requires upload login,
|
|
322
|
-
you must provide --agent-api-key or config hub.agentApiKey for those version updates.
|
|
323
|
-
- existing skill/rule updates usually still need --admin-secret or --agent-api-key.
|
|
324
|
-
- if your local Hub lets requireAdminJson accept HUB_ADMIN_SECRET, roles/scenarios can also use --admin-secret.
|
|
325
|
-
- otherwise roles/scenarios still require the admin session.
|
|
326
|
-
- A config example is available at ${path.relative(PROJECT_ROOT, DEFAULT_CONFIG_EXAMPLE_PATH)}.
|
|
327
|
-
`.trim());
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function loadConfig(configPath) {
|
|
331
|
-
if (!configPath || !fs.existsSync(configPath)) {
|
|
332
|
-
return {};
|
|
333
|
-
}
|
|
334
|
-
return readJson(configPath);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function resolveRuntimeOptions(cliOptions, config) {
|
|
338
|
-
const hubProjectDir = path.resolve(
|
|
339
|
-
PROJECT_ROOT,
|
|
340
|
-
cliOptions.hubProject || config?.hub?.projectDir || DEFAULT_HUB_PROJECT,
|
|
341
|
-
);
|
|
342
|
-
const envFileValues = loadEnvOverrides(hubProjectDir);
|
|
343
|
-
|
|
344
|
-
const baseUrl = normalizeBaseUrl(
|
|
345
|
-
cliOptions.baseUrl ||
|
|
346
|
-
process.env.HUB_SYNC_BASE_URL ||
|
|
347
|
-
config?.hub?.baseUrl ||
|
|
348
|
-
DEFAULT_BASE_URL,
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
const adminSecret =
|
|
352
|
-
cliOptions.adminSecret ||
|
|
353
|
-
process.env.HUB_ADMIN_SECRET ||
|
|
354
|
-
process.env.HUB_SYNC_ADMIN_SECRET ||
|
|
355
|
-
config?.hub?.adminSecret ||
|
|
356
|
-
envFileValues.HUB_ADMIN_SECRET ||
|
|
357
|
-
"";
|
|
358
|
-
|
|
359
|
-
return {
|
|
360
|
-
baseUrl,
|
|
361
|
-
hubProjectDir,
|
|
362
|
-
adminEmail:
|
|
363
|
-
cliOptions.adminEmail ||
|
|
364
|
-
process.env.HUB_SYNC_ADMIN_EMAIL ||
|
|
365
|
-
config?.hub?.adminEmail ||
|
|
366
|
-
"",
|
|
367
|
-
adminPassword:
|
|
368
|
-
cliOptions.adminPassword ||
|
|
369
|
-
process.env.HUB_SYNC_ADMIN_PASSWORD ||
|
|
370
|
-
config?.hub?.adminPassword ||
|
|
371
|
-
"",
|
|
372
|
-
adminCookie:
|
|
373
|
-
cliOptions.adminCookie ||
|
|
374
|
-
process.env.HUB_SYNC_ADMIN_COOKIE ||
|
|
375
|
-
config?.hub?.adminSessionCookie ||
|
|
376
|
-
"",
|
|
377
|
-
adminSecret,
|
|
378
|
-
agentApiKey:
|
|
379
|
-
cliOptions.agentApiKey ||
|
|
380
|
-
process.env.HUB_SYNC_AGENT_API_KEY ||
|
|
381
|
-
config?.hub?.agentApiKey ||
|
|
382
|
-
"",
|
|
383
|
-
hasAdminAuthInput: Boolean(
|
|
384
|
-
cliOptions.adminCookie ||
|
|
385
|
-
process.env.HUB_SYNC_ADMIN_COOKIE ||
|
|
386
|
-
config?.hub?.adminSessionCookie ||
|
|
387
|
-
adminSecret ||
|
|
388
|
-
((cliOptions.adminEmail ||
|
|
389
|
-
process.env.HUB_SYNC_ADMIN_EMAIL ||
|
|
390
|
-
config?.hub?.adminEmail) &&
|
|
391
|
-
(cliOptions.adminPassword ||
|
|
392
|
-
process.env.HUB_SYNC_ADMIN_PASSWORD ||
|
|
393
|
-
config?.hub?.adminPassword)),
|
|
394
|
-
),
|
|
395
|
-
dryRun: Boolean(cliOptions.dryRun),
|
|
396
|
-
config,
|
|
397
|
-
skillSelectionSpecified: typeof cliOptions.skills !== "undefined",
|
|
398
|
-
roleSelectionSpecified: typeof cliOptions.roles !== "undefined",
|
|
399
|
-
skillSelection: normalizeSelection(cliOptions.skills),
|
|
400
|
-
ruleSelection: normalizeSelection(cliOptions.rules),
|
|
401
|
-
roleSelection: normalizeSelection(cliOptions.roles),
|
|
402
|
-
scenarioSelection: normalizeSelection(cliOptions.scenarios),
|
|
403
|
-
fromScenarioSelection: normalizeSelection(
|
|
404
|
-
typeof cliOptions.fromScenarios === "undefined" ? "none" : cliOptions.fromScenarios,
|
|
405
|
-
),
|
|
406
|
-
skipSkills: isSelectionNone(normalizeSelection(cliOptions.skills)),
|
|
407
|
-
skipRules: isSelectionNone(normalizeSelection(cliOptions.rules)),
|
|
408
|
-
skipRoles: isSelectionNone(normalizeSelection(cliOptions.roles)),
|
|
409
|
-
skipScenarios: isSelectionNone(normalizeSelection(cliOptions.scenarios)),
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function normalizeSelection(value) {
|
|
414
|
-
if (!value) return { mode: "all", values: new Set() };
|
|
415
|
-
const trimmed = String(value).trim();
|
|
416
|
-
if (!trimmed || trimmed === "all") {
|
|
417
|
-
return { mode: "all", values: new Set() };
|
|
418
|
-
}
|
|
419
|
-
if (trimmed === "none") {
|
|
420
|
-
return { mode: "none", values: new Set() };
|
|
421
|
-
}
|
|
422
|
-
return {
|
|
423
|
-
mode: "pick",
|
|
424
|
-
values: new Set(
|
|
425
|
-
trimmed
|
|
426
|
-
.split(",")
|
|
427
|
-
.map((item) => item.trim())
|
|
428
|
-
.filter(Boolean),
|
|
429
|
-
),
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function isSelectionNone(selection) {
|
|
434
|
-
return selection.mode === "none";
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function loadEnvOverrides(hubProjectDir) {
|
|
438
|
-
const files = [".env.local", ".env.development.local", ".env", ".env.development"];
|
|
439
|
-
const merged = {};
|
|
440
|
-
for (const filename of files) {
|
|
441
|
-
const filePath = path.join(hubProjectDir, filename);
|
|
442
|
-
if (!fs.existsSync(filePath)) continue;
|
|
443
|
-
Object.assign(merged, parseEnvLikeFile(fs.readFileSync(filePath, "utf8")));
|
|
444
|
-
}
|
|
445
|
-
return merged;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function parseEnvLikeFile(content) {
|
|
449
|
-
const output = {};
|
|
450
|
-
for (const rawLine of content.split(/\r?\n/)) {
|
|
451
|
-
const line = rawLine.trim();
|
|
452
|
-
if (!line || line.startsWith("#")) continue;
|
|
453
|
-
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
454
|
-
if (!match) continue;
|
|
455
|
-
let value = match[2].trim();
|
|
456
|
-
if (
|
|
457
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
458
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
459
|
-
) {
|
|
460
|
-
value = value.slice(1, -1);
|
|
461
|
-
}
|
|
462
|
-
output[match[1]] = value;
|
|
463
|
-
}
|
|
464
|
-
return output;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function normalizeBaseUrl(input) {
|
|
468
|
-
const url = new URL(String(input));
|
|
469
|
-
url.pathname = "";
|
|
470
|
-
url.search = "";
|
|
471
|
-
url.hash = "";
|
|
472
|
-
return url.toString().replace(/\/$/, "");
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
class HubClient {
|
|
476
|
-
constructor(options) {
|
|
477
|
-
this.baseUrl = options.baseUrl;
|
|
478
|
-
this.adminEmail = options.adminEmail;
|
|
479
|
-
this.adminPassword = options.adminPassword;
|
|
480
|
-
this.cookie = options.adminCookie || "";
|
|
481
|
-
this.adminSecret = options.adminSecret || "";
|
|
482
|
-
this.agentApiKey = options.agentApiKey || "";
|
|
483
|
-
this.dryRun = options.dryRun;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
hasAdminSession() {
|
|
487
|
-
return Boolean(this.cookie);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
hasAdminAccess() {
|
|
491
|
-
return Boolean(this.cookie || this.adminSecret);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
async ensureAdminSession() {
|
|
495
|
-
if (!this.cookie) {
|
|
496
|
-
if (!this.adminEmail || !this.adminPassword) {
|
|
497
|
-
throw new Error(
|
|
498
|
-
"missing admin auth: provide --admin-email/--admin-password, --admin-cookie, or hub config",
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
await this.login();
|
|
502
|
-
}
|
|
503
|
-
await this.getJson("/api/admin/auth/me");
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
async login() {
|
|
507
|
-
const response = await fetch(`${this.baseUrl}/api/admin/auth/login`, {
|
|
508
|
-
method: "POST",
|
|
509
|
-
headers: {
|
|
510
|
-
"content-type": "application/json",
|
|
511
|
-
accept: "application/json",
|
|
512
|
-
},
|
|
513
|
-
body: JSON.stringify({
|
|
514
|
-
email: this.adminEmail,
|
|
515
|
-
password: this.adminPassword,
|
|
516
|
-
}),
|
|
517
|
-
});
|
|
518
|
-
if (!response.ok) {
|
|
519
|
-
throw new Error(`admin login failed: ${await readErrorText(response)}`);
|
|
520
|
-
}
|
|
521
|
-
const cookies = getResponseCookies(response);
|
|
522
|
-
const adminSession = cookies.find((cookie) => cookie.startsWith("admin_session="));
|
|
523
|
-
if (!adminSession) {
|
|
524
|
-
throw new Error("admin login succeeded but no admin_session cookie was returned");
|
|
525
|
-
}
|
|
526
|
-
this.cookie = adminSession;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
async getJson(pathname) {
|
|
530
|
-
return this.requestJson(pathname, { method: "GET" });
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
async postJson(pathname, body) {
|
|
534
|
-
return this.requestJson(pathname, {
|
|
535
|
-
method: "POST",
|
|
536
|
-
headers: {
|
|
537
|
-
"content-type": "application/json",
|
|
538
|
-
},
|
|
539
|
-
body: JSON.stringify(body),
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
async postForm(pathname, formData) {
|
|
544
|
-
const response = await fetch(`${this.baseUrl}${pathname}`, {
|
|
545
|
-
method: "POST",
|
|
546
|
-
headers: this.buildHeaders({}),
|
|
547
|
-
body: formData,
|
|
548
|
-
});
|
|
549
|
-
if (!response.ok) {
|
|
550
|
-
throw new Error(await readErrorText(response));
|
|
551
|
-
}
|
|
552
|
-
return unwrapApiResponse(await response.json());
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
async requestJson(pathname, init) {
|
|
556
|
-
const response = await fetch(`${this.baseUrl}${pathname}`, {
|
|
557
|
-
...init,
|
|
558
|
-
headers: this.buildHeaders(init.headers || {}),
|
|
559
|
-
});
|
|
560
|
-
if (!response.ok) {
|
|
561
|
-
throw new Error(await readErrorText(response));
|
|
562
|
-
}
|
|
563
|
-
return unwrapApiResponse(await response.json());
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
buildHeaders(headers) {
|
|
567
|
-
const next = {
|
|
568
|
-
accept: "application/json",
|
|
569
|
-
...headers,
|
|
570
|
-
};
|
|
571
|
-
if (this.cookie) {
|
|
572
|
-
next.cookie = this.cookie;
|
|
573
|
-
}
|
|
574
|
-
if (this.adminSecret) {
|
|
575
|
-
next["x-hub-admin-secret"] = this.adminSecret;
|
|
576
|
-
}
|
|
577
|
-
if (this.agentApiKey) {
|
|
578
|
-
next.authorization = `Bearer ${this.agentApiKey}`;
|
|
579
|
-
}
|
|
580
|
-
return next;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
async function loadCategories(client) {
|
|
585
|
-
const [skill, rule] = await Promise.all([
|
|
586
|
-
client.getJson("/api/admin/categories?resourceType=skill"),
|
|
587
|
-
client.getJson("/api/admin/categories?resourceType=rule"),
|
|
588
|
-
]);
|
|
589
|
-
return {
|
|
590
|
-
skill: skill.items || [],
|
|
591
|
-
rule: rule.items || [],
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
async function loadBrowseItems(client, resourceType) {
|
|
596
|
-
const pageSize = 100;
|
|
597
|
-
let page = 1;
|
|
598
|
-
let total = 0;
|
|
599
|
-
const items = [];
|
|
600
|
-
do {
|
|
601
|
-
const response = await client.getJson(
|
|
602
|
-
`/api/admin/resources/browse?resourceType=${resourceType}&page=${page}&pageSize=${pageSize}`,
|
|
603
|
-
);
|
|
604
|
-
total = Number(response.total || 0);
|
|
605
|
-
items.push(...(response.items || []));
|
|
606
|
-
page += 1;
|
|
607
|
-
} while (items.length < total);
|
|
608
|
-
return { items };
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
async function syncRules(context) {
|
|
612
|
-
const ids = selectResourceIds(context.localRules, context.resolved.ruleSelection);
|
|
613
|
-
for (const ruleId of ids) {
|
|
614
|
-
const local = context.localRules[ruleId];
|
|
615
|
-
const desiredAssets = buildRuleAssets(ruleId, local, context);
|
|
616
|
-
if (desiredAssets.length === 0) {
|
|
617
|
-
context.summary.rule.skipped += 1;
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
|
-
for (const desired of desiredAssets) {
|
|
621
|
-
if (!desired) {
|
|
622
|
-
context.summary.rule.skipped += 1;
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
let existing = context.hubState.rulesBySlug[desired.slug];
|
|
627
|
-
if (!existing) {
|
|
628
|
-
const publicExisting = await fetchPublicResource(context.client, "rule", desired.slug);
|
|
629
|
-
if (publicExisting) {
|
|
630
|
-
existing = publicExisting;
|
|
631
|
-
context.hubState.rulesBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
632
|
-
"rule",
|
|
633
|
-
publicExisting,
|
|
634
|
-
desired,
|
|
635
|
-
);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
if (!existing) {
|
|
639
|
-
if (!desired.categorySlug) {
|
|
640
|
-
warn(`rule ${ruleId}: missing categorySlug, skip create`);
|
|
641
|
-
context.summary.rule.skipped += 1;
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
if (context.resolved.dryRun) {
|
|
645
|
-
info(`rule ${desired.slug}: create`);
|
|
646
|
-
context.summary.rule.created += 1;
|
|
647
|
-
context.hubState.rulesBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
648
|
-
"rule",
|
|
649
|
-
null,
|
|
650
|
-
desired,
|
|
651
|
-
);
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const createResult = await createSkillRuleOrNull({
|
|
656
|
-
type: "rule",
|
|
657
|
-
desired,
|
|
658
|
-
client: context.client,
|
|
659
|
-
});
|
|
660
|
-
if (createResult?.created) {
|
|
661
|
-
info(`rule ${desired.slug}: created`);
|
|
662
|
-
context.summary.rule.created += 1;
|
|
663
|
-
context.hubState.rulesBySlug[desired.slug] = {
|
|
664
|
-
id: createResult.resource?.id || desired.slug,
|
|
665
|
-
slug: desired.slug,
|
|
666
|
-
name: desired.name,
|
|
667
|
-
registryId: desired.registryId,
|
|
668
|
-
manifestId: desired.manifestId,
|
|
669
|
-
tags: desired.tags,
|
|
670
|
-
supportedProfiles: desired.supportedProfiles,
|
|
671
|
-
categoryName: createResult.resource?.category?.name || desired.categorySlug,
|
|
672
|
-
};
|
|
673
|
-
continue;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const conflictExisting = await fetchConflictResource({
|
|
677
|
-
type: "rule",
|
|
678
|
-
desiredSlug: desired.slug,
|
|
679
|
-
conflictSlugs: createResult?.conflictSlugs || [],
|
|
680
|
-
client: context.client,
|
|
681
|
-
});
|
|
682
|
-
if (!conflictExisting) {
|
|
683
|
-
throw new Error(`rule ${ruleId}: resource create conflicted but public resource was not found`);
|
|
684
|
-
}
|
|
685
|
-
existing = conflictExisting;
|
|
686
|
-
context.hubState.rulesBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
687
|
-
"rule",
|
|
688
|
-
conflictExisting,
|
|
689
|
-
desired,
|
|
690
|
-
);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const existingDetails = existing
|
|
694
|
-
? await fetchPublicResource(context.client, "rule", existing.slug || desired.slug) || existing
|
|
695
|
-
: null;
|
|
696
|
-
const metadataPatch = existingDetails
|
|
697
|
-
? buildSkillRuleMetadataPatch("rule", desired, existingDetails)
|
|
698
|
-
: buildSkillRuleFullPatch(desired);
|
|
699
|
-
if (metadataPatch) {
|
|
700
|
-
if (context.resolved.dryRun) {
|
|
701
|
-
info(`rule ${desired.slug}: update metadata`);
|
|
702
|
-
} else {
|
|
703
|
-
await context.client.postJson(
|
|
704
|
-
`/api/rules/${encodeURIComponent(existingDetails?.slug || existing?.slug || desired.slug)}`,
|
|
705
|
-
metadataPatch,
|
|
706
|
-
);
|
|
707
|
-
info(`rule ${desired.slug}: metadata updated`);
|
|
708
|
-
}
|
|
709
|
-
context.summary.rule.updated += 1;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const versionChanged = await ensureSkillRuleVersion({
|
|
713
|
-
type: "rule",
|
|
714
|
-
desired,
|
|
715
|
-
slug: desired.slug,
|
|
716
|
-
client: context.client,
|
|
717
|
-
dryRun: context.resolved.dryRun,
|
|
718
|
-
});
|
|
719
|
-
if (versionChanged === "versioned") {
|
|
720
|
-
context.summary.rule.versioned += 1;
|
|
721
|
-
} else if (!metadataPatch) {
|
|
722
|
-
context.summary.rule.skipped += 1;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
context.hubState.rulesBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
726
|
-
"rule",
|
|
727
|
-
existing,
|
|
728
|
-
desired,
|
|
729
|
-
);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
async function syncSkills(context) {
|
|
735
|
-
const ids = selectResourceIds(context.localSkills, context.resolved.skillSelection);
|
|
736
|
-
for (const skillId of ids) {
|
|
737
|
-
const local = context.localSkills[skillId];
|
|
738
|
-
const desiredAssets = buildSkillAssets(skillId, local, context);
|
|
739
|
-
if (desiredAssets.length === 0) {
|
|
740
|
-
context.summary.skill.skipped += 1;
|
|
741
|
-
continue;
|
|
742
|
-
}
|
|
743
|
-
for (const desired of desiredAssets) {
|
|
744
|
-
if (!desired) {
|
|
745
|
-
context.summary.skill.skipped += 1;
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
let existing = context.hubState.skillsBySlug[desired.slug];
|
|
750
|
-
if (!existing) {
|
|
751
|
-
const publicExisting = await fetchPublicResource(context.client, "skill", desired.slug);
|
|
752
|
-
if (publicExisting) {
|
|
753
|
-
existing = publicExisting;
|
|
754
|
-
context.hubState.skillsBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
755
|
-
"skill",
|
|
756
|
-
publicExisting,
|
|
757
|
-
desired,
|
|
758
|
-
);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
if (!existing) {
|
|
762
|
-
if (!desired.categorySlug) {
|
|
763
|
-
warn(`skill ${skillId}: missing categorySlug, skip create`);
|
|
764
|
-
context.summary.skill.skipped += 1;
|
|
765
|
-
continue;
|
|
766
|
-
}
|
|
767
|
-
if (context.resolved.dryRun) {
|
|
768
|
-
info(`skill ${desired.slug}: create`);
|
|
769
|
-
context.summary.skill.created += 1;
|
|
770
|
-
context.hubState.skillsBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
771
|
-
"skill",
|
|
772
|
-
null,
|
|
773
|
-
desired,
|
|
774
|
-
);
|
|
775
|
-
continue;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
const createResult = await createSkillRuleOrNull({
|
|
779
|
-
type: "skill",
|
|
780
|
-
desired,
|
|
781
|
-
client: context.client,
|
|
782
|
-
});
|
|
783
|
-
if (createResult?.created) {
|
|
784
|
-
info(`skill ${desired.slug}: created`);
|
|
785
|
-
context.summary.skill.created += 1;
|
|
786
|
-
context.hubState.skillsBySlug[desired.slug] = {
|
|
787
|
-
id: createResult.resource?.id || desired.slug,
|
|
788
|
-
slug: desired.slug,
|
|
789
|
-
name: desired.name,
|
|
790
|
-
registryId: desired.registryId,
|
|
791
|
-
manifestId: desired.manifestId,
|
|
792
|
-
tags: desired.tags,
|
|
793
|
-
supportedProfiles: desired.supportedProfiles,
|
|
794
|
-
categoryName: createResult.resource?.category?.name || desired.categorySlug,
|
|
795
|
-
};
|
|
796
|
-
continue;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
const conflictExisting = await fetchConflictResource({
|
|
800
|
-
type: "skill",
|
|
801
|
-
desiredSlug: desired.slug,
|
|
802
|
-
conflictSlugs: createResult?.conflictSlugs || [],
|
|
803
|
-
client: context.client,
|
|
804
|
-
});
|
|
805
|
-
if (!conflictExisting) {
|
|
806
|
-
throw new Error(`skill ${skillId}: resource create conflicted but public resource was not found`);
|
|
807
|
-
}
|
|
808
|
-
existing = conflictExisting;
|
|
809
|
-
context.hubState.skillsBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
810
|
-
"skill",
|
|
811
|
-
conflictExisting,
|
|
812
|
-
desired,
|
|
813
|
-
);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const existingDetails = existing
|
|
817
|
-
? await fetchPublicResource(context.client, "skill", existing.slug || desired.slug) || existing
|
|
818
|
-
: null;
|
|
819
|
-
const metadataPatch = existingDetails
|
|
820
|
-
? buildSkillRuleMetadataPatch("skill", desired, existingDetails)
|
|
821
|
-
: buildSkillRuleFullPatch(desired);
|
|
822
|
-
if (metadataPatch) {
|
|
823
|
-
if (context.resolved.dryRun) {
|
|
824
|
-
info(`skill ${desired.slug}: update metadata`);
|
|
825
|
-
} else {
|
|
826
|
-
await context.client.postJson(
|
|
827
|
-
`/api/skills/${encodeURIComponent(existingDetails?.slug || existing?.slug || desired.slug)}`,
|
|
828
|
-
metadataPatch,
|
|
829
|
-
);
|
|
830
|
-
info(`skill ${desired.slug}: metadata updated`);
|
|
831
|
-
}
|
|
832
|
-
context.summary.skill.updated += 1;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
const versionChanged = await ensureSkillRuleVersion({
|
|
836
|
-
type: "skill",
|
|
837
|
-
desired,
|
|
838
|
-
slug: desired.slug,
|
|
839
|
-
client: context.client,
|
|
840
|
-
dryRun: context.resolved.dryRun,
|
|
841
|
-
});
|
|
842
|
-
if (versionChanged === "versioned") {
|
|
843
|
-
context.summary.skill.versioned += 1;
|
|
844
|
-
} else if (!metadataPatch) {
|
|
845
|
-
context.summary.skill.skipped += 1;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
context.hubState.skillsBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
849
|
-
"skill",
|
|
850
|
-
existing,
|
|
851
|
-
desired,
|
|
852
|
-
);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
async function syncRoles(context) {
|
|
858
|
-
const ids = selectResourceIds(context.localRoles, context.resolved.roleSelection);
|
|
859
|
-
for (const roleId of ids) {
|
|
860
|
-
const local = context.localRoles[roleId];
|
|
861
|
-
const desired = await buildRoleAsset(roleId, local, context);
|
|
862
|
-
if (!desired) {
|
|
863
|
-
context.summary.role.skipped += 1;
|
|
864
|
-
continue;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
const existing = context.hubState.rolesBySlug[desired.slug];
|
|
868
|
-
if (!existing) {
|
|
869
|
-
if (context.resolved.dryRun) {
|
|
870
|
-
info(`role ${roleId}: create`);
|
|
871
|
-
context.summary.role.created += 1;
|
|
872
|
-
context.hubState.rolesBySlug[desired.slug] = buildPreviewRoleState(null, desired);
|
|
873
|
-
} else {
|
|
874
|
-
await context.client.postJson("/api/admin/roles", desired.payload);
|
|
875
|
-
info(`role ${roleId}: created`);
|
|
876
|
-
context.summary.role.created += 1;
|
|
877
|
-
const refreshed = await context.client.getJson("/api/admin/roles");
|
|
878
|
-
context.hubState.rolesBySlug = indexBy(refreshed.items || [], "slug");
|
|
879
|
-
}
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
const existingPayload = normalizeRoleResponseToPayload(existing);
|
|
884
|
-
if (deepEqual(existingPayload, desired.payload)) {
|
|
885
|
-
context.summary.role.skipped += 1;
|
|
886
|
-
info(`role ${roleId}: no changes`);
|
|
887
|
-
continue;
|
|
888
|
-
}
|
|
889
|
-
const needsVersion = await roleVersionWouldChange({
|
|
890
|
-
client: context.client,
|
|
891
|
-
slug: existing.slug,
|
|
892
|
-
desiredVersionFiles: desired.versionFiles,
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
if (context.resolved.dryRun) {
|
|
896
|
-
info(`role ${roleId}: update${needsVersion ? " + version" : ""}`);
|
|
897
|
-
context.summary.role.updated += 1;
|
|
898
|
-
if (needsVersion) {
|
|
899
|
-
context.summary.role.versioned += 1;
|
|
900
|
-
}
|
|
901
|
-
context.hubState.rolesBySlug[desired.slug] = buildPreviewRoleState(existing, desired);
|
|
902
|
-
continue;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
await context.client.postJson("/api/admin/roles/update", {
|
|
906
|
-
id: existing.id,
|
|
907
|
-
...desired.payload,
|
|
908
|
-
});
|
|
909
|
-
await ensureRoleVersion({
|
|
910
|
-
client: context.client,
|
|
911
|
-
slug: existing.slug,
|
|
912
|
-
desiredVersionFiles: desired.versionFiles,
|
|
913
|
-
dryRun: false,
|
|
914
|
-
});
|
|
915
|
-
info(`role ${roleId}: updated`);
|
|
916
|
-
context.summary.role.updated += 1;
|
|
917
|
-
if (needsVersion) {
|
|
918
|
-
context.summary.role.versioned += 1;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
const refreshed = await context.client.getJson("/api/admin/roles");
|
|
922
|
-
context.hubState.rolesBySlug = indexBy(refreshed.items || [], "slug");
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
async function syncScenarios(context) {
|
|
927
|
-
const ids = selectResourceIds(context.localScenarios, context.resolved.scenarioSelection);
|
|
928
|
-
for (const scenarioId of ids) {
|
|
929
|
-
const local = context.localScenarios[scenarioId];
|
|
930
|
-
const desired = buildScenarioAsset(scenarioId, local, context);
|
|
931
|
-
if (!desired) {
|
|
932
|
-
context.summary.scenario.skipped += 1;
|
|
933
|
-
continue;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
const existing = context.hubState.scenariosBySlug[desired.slug];
|
|
937
|
-
if (!existing) {
|
|
938
|
-
if (context.resolved.dryRun) {
|
|
939
|
-
info(`scenario ${scenarioId}: create`);
|
|
940
|
-
context.summary.scenario.created += 1;
|
|
941
|
-
context.hubState.scenariosBySlug[desired.slug] = buildPreviewScenarioState(null, desired);
|
|
942
|
-
} else {
|
|
943
|
-
await context.client.postJson("/api/admin/scenarios", desired.payload);
|
|
944
|
-
info(`scenario ${scenarioId}: created`);
|
|
945
|
-
context.summary.scenario.created += 1;
|
|
946
|
-
const refreshed = await context.client.getJson("/api/admin/scenarios");
|
|
947
|
-
context.hubState.scenariosBySlug = indexBy(refreshed.items || [], "slug");
|
|
948
|
-
}
|
|
949
|
-
continue;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
const existingPayload = normalizeScenarioResponseToPayload(existing);
|
|
953
|
-
if (deepEqual(existingPayload, desired.payload)) {
|
|
954
|
-
context.summary.scenario.skipped += 1;
|
|
955
|
-
info(`scenario ${scenarioId}: no changes`);
|
|
956
|
-
continue;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
if (context.resolved.dryRun) {
|
|
960
|
-
info(`scenario ${scenarioId}: update`);
|
|
961
|
-
context.summary.scenario.updated += 1;
|
|
962
|
-
context.hubState.scenariosBySlug[desired.slug] = buildPreviewScenarioState(existing, desired);
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
await context.client.postJson("/api/admin/scenarios/update", {
|
|
967
|
-
id: existing.id,
|
|
968
|
-
...desired.payload,
|
|
969
|
-
});
|
|
970
|
-
info(`scenario ${scenarioId}: updated`);
|
|
971
|
-
context.summary.scenario.updated += 1;
|
|
972
|
-
|
|
973
|
-
const refreshed = await context.client.getJson("/api/admin/scenarios");
|
|
974
|
-
context.hubState.scenariosBySlug = indexBy(refreshed.items || [], "slug");
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
function buildSkillAssets(skillId, local, context) {
|
|
979
|
-
const variants = buildProfileVariantSpecs({
|
|
980
|
-
type: "skill",
|
|
981
|
-
resourceId: skillId,
|
|
982
|
-
local,
|
|
983
|
-
hubState: context.hubState,
|
|
984
|
-
});
|
|
985
|
-
if (variants.length === 0) {
|
|
986
|
-
return [buildSkillAsset(skillId, local, context, null)].filter(Boolean);
|
|
987
|
-
}
|
|
988
|
-
return variants
|
|
989
|
-
.map((variant) => buildSkillAsset(skillId, local, context, variant))
|
|
990
|
-
.filter(Boolean);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function buildRuleAssets(ruleId, local, context) {
|
|
994
|
-
const variants = buildProfileVariantSpecs({
|
|
995
|
-
type: "rule",
|
|
996
|
-
resourceId: ruleId,
|
|
997
|
-
local,
|
|
998
|
-
hubState: context.hubState,
|
|
999
|
-
});
|
|
1000
|
-
if (variants.length === 0) {
|
|
1001
|
-
return [buildRuleAsset(ruleId, local, context, null)].filter(Boolean);
|
|
1002
|
-
}
|
|
1003
|
-
return variants
|
|
1004
|
-
.map((variant) => buildRuleAsset(ruleId, local, context, variant))
|
|
1005
|
-
.filter(Boolean);
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
function buildSkillAsset(skillId, local, context, variant) {
|
|
1009
|
-
const override = resolveResourceOverride({
|
|
1010
|
-
config: context.config,
|
|
1011
|
-
type: "skills",
|
|
1012
|
-
resourceId: skillId,
|
|
1013
|
-
variantSlug: variant?.slug,
|
|
1014
|
-
});
|
|
1015
|
-
const files = collectSkillFiles(local, skillId, variant?.sourcePaths);
|
|
1016
|
-
if (files.length === 0) {
|
|
1017
|
-
warn(`skill ${skillId}: no files collected`);
|
|
1018
|
-
return null;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
const primaryFile = pickPrimaryTextFile(files, "SKILL.md") || files[0];
|
|
1022
|
-
const parsed = parseFrontmatterFile(primaryFile.content, "skill");
|
|
1023
|
-
const name = override.name || variant?.existing?.name || parsed.name || skillId;
|
|
1024
|
-
const description =
|
|
1025
|
-
override.description ||
|
|
1026
|
-
parsed.description ||
|
|
1027
|
-
variant?.existing?.description ||
|
|
1028
|
-
`Sync from local skill ${skillId}`;
|
|
1029
|
-
const supportedProfiles =
|
|
1030
|
-
override.supportedProfiles ||
|
|
1031
|
-
variant?.supportedProfiles ||
|
|
1032
|
-
Object.keys(local.sourceByProfile || {});
|
|
1033
|
-
const domains = Array.isArray(local.domains) ? local.domains : [];
|
|
1034
|
-
const categorySlug = resolveCategorySlug({
|
|
1035
|
-
type: "skill",
|
|
1036
|
-
resourceId: skillId,
|
|
1037
|
-
override,
|
|
1038
|
-
domains,
|
|
1039
|
-
categories: context.hubState.categories.skill,
|
|
1040
|
-
config: context.config,
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
return {
|
|
1044
|
-
slug: variant?.slug || override.slug || skillId,
|
|
1045
|
-
registryId: override.registryId || skillId,
|
|
1046
|
-
manifestId: override.manifestId || override.registryId || skillId,
|
|
1047
|
-
name,
|
|
1048
|
-
description,
|
|
1049
|
-
longDescription: override.longDescription || "",
|
|
1050
|
-
author: override.author || context.config?.defaults?.author || "Hub Admin",
|
|
1051
|
-
categorySlug,
|
|
1052
|
-
tags: uniqueKeepOrder(override.tags || domains),
|
|
1053
|
-
supportedProfiles: uniqueKeepOrder(supportedProfiles),
|
|
1054
|
-
downloadPolicy: override.downloadPolicy || context.config?.defaults?.downloadPolicy || "login",
|
|
1055
|
-
files,
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
function buildRuleAsset(ruleId, local, context, variant) {
|
|
1060
|
-
const override = resolveResourceOverride({
|
|
1061
|
-
config: context.config,
|
|
1062
|
-
type: "rules",
|
|
1063
|
-
resourceId: ruleId,
|
|
1064
|
-
variantSlug: variant?.slug,
|
|
1065
|
-
});
|
|
1066
|
-
const files = collectRuleFiles(local, variant?.sourcePaths);
|
|
1067
|
-
if (files.length === 0) {
|
|
1068
|
-
warn(`rule ${ruleId}: no files collected`);
|
|
1069
|
-
return null;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
const primaryFile = files[0];
|
|
1073
|
-
const parsed = parseFrontmatterFile(primaryFile.content, "rule");
|
|
1074
|
-
const name = override.name || variant?.existing?.name || parsed.name || ruleId;
|
|
1075
|
-
const description =
|
|
1076
|
-
override.description ||
|
|
1077
|
-
parsed.description ||
|
|
1078
|
-
variant?.existing?.description ||
|
|
1079
|
-
`Sync from local rule ${ruleId}`;
|
|
1080
|
-
const supportedProfiles =
|
|
1081
|
-
override.supportedProfiles ||
|
|
1082
|
-
variant?.supportedProfiles ||
|
|
1083
|
-
Object.keys(local.sourceByProfile || {});
|
|
1084
|
-
const domains = Array.isArray(local.domains) ? local.domains : [];
|
|
1085
|
-
const categorySlug = resolveCategorySlug({
|
|
1086
|
-
type: "rule",
|
|
1087
|
-
resourceId: ruleId,
|
|
1088
|
-
override,
|
|
1089
|
-
domains,
|
|
1090
|
-
categories: context.hubState.categories.rule,
|
|
1091
|
-
config: context.config,
|
|
1092
|
-
});
|
|
1093
|
-
|
|
1094
|
-
return {
|
|
1095
|
-
slug: variant?.slug || override.slug || ruleId,
|
|
1096
|
-
registryId: override.registryId || ruleId,
|
|
1097
|
-
manifestId: override.manifestId || override.registryId || ruleId,
|
|
1098
|
-
name,
|
|
1099
|
-
description,
|
|
1100
|
-
longDescription: override.longDescription || "",
|
|
1101
|
-
author: override.author || context.config?.defaults?.author || "Hub Admin",
|
|
1102
|
-
categorySlug,
|
|
1103
|
-
tags: uniqueKeepOrder(override.tags || domains),
|
|
1104
|
-
supportedProfiles: uniqueKeepOrder(supportedProfiles),
|
|
1105
|
-
downloadPolicy: override.downloadPolicy || context.config?.defaults?.downloadPolicy || "login",
|
|
1106
|
-
files,
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
async function buildRoleAsset(roleId, local, context) {
|
|
1111
|
-
const override = resolveResourceOverride({
|
|
1112
|
-
config: context.config,
|
|
1113
|
-
type: "roles",
|
|
1114
|
-
resourceId: roleId,
|
|
1115
|
-
});
|
|
1116
|
-
const sourcePath = path.resolve(PROJECT_ROOT, local.source);
|
|
1117
|
-
if (!fs.existsSync(sourcePath)) {
|
|
1118
|
-
warn(`role ${roleId}: source not found ${local.source}`);
|
|
1119
|
-
return null;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
const uploadParsed = await parseRoleWithHub(context.client, sourcePath);
|
|
1123
|
-
const registrySkillSlugs = uniqueKeepOrder([
|
|
1124
|
-
...(Array.isArray(local.skill_priority) ? local.skill_priority : []),
|
|
1125
|
-
...(Array.isArray(local.micro_skill_allowlist) ? local.micro_skill_allowlist : []),
|
|
1126
|
-
...(Array.isArray(uploadParsed.roleData.preferredSkills) ? uploadParsed.roleData.preferredSkills : []),
|
|
1127
|
-
]);
|
|
1128
|
-
const registryRuleSlugs = uniqueKeepOrder(Array.isArray(local.rule_ids) ? local.rule_ids : []);
|
|
1129
|
-
const skillIds = uniqueKeepOrder(
|
|
1130
|
-
registrySkillSlugs.flatMap((slug) => resolveLinkedResourceIds("skill", slug, context.hubState)),
|
|
1131
|
-
);
|
|
1132
|
-
const ruleIds = uniqueKeepOrder(
|
|
1133
|
-
registryRuleSlugs.flatMap((slug) => resolveLinkedResourceIds("rule", slug, context.hubState)),
|
|
1134
|
-
);
|
|
1135
|
-
const domainIds = resolveRoleDomainIds({
|
|
1136
|
-
override,
|
|
1137
|
-
local,
|
|
1138
|
-
uploadParsed,
|
|
1139
|
-
config: context.config,
|
|
1140
|
-
});
|
|
1141
|
-
const name = override.name || uploadParsed.roleData.name || local.name || roleId;
|
|
1142
|
-
const slug = override.slug || uploadParsed.roleData.slug || roleId;
|
|
1143
|
-
const payload = {
|
|
1144
|
-
name,
|
|
1145
|
-
slug,
|
|
1146
|
-
registryId: override.registryId || roleId,
|
|
1147
|
-
manifestId: override.manifestId || override.registryId || roleId,
|
|
1148
|
-
author: override.author || context.config?.defaults?.author || "Hub Admin",
|
|
1149
|
-
description: override.description || uploadParsed.roleData.description || `${name} role`,
|
|
1150
|
-
longDescription: override.longDescription || null,
|
|
1151
|
-
publishStatus: override.publishStatus || context.config?.defaults?.rolePublishStatus || "draft",
|
|
1152
|
-
roleStatus: override.roleStatus || local.status || uploadParsed.roleData.roleStatus || "draft",
|
|
1153
|
-
tags: uniqueKeepOrder(override.tags || local.domains || []),
|
|
1154
|
-
supportedProfiles: uniqueKeepOrder(override.supportedProfiles || local.profiles || []),
|
|
1155
|
-
triggers: uniqueKeepOrder(override.triggers || uploadParsed.roleData.triggers || []),
|
|
1156
|
-
preferredSkills: uniqueKeepOrder(override.preferredSkills || registrySkillSlugs),
|
|
1157
|
-
reads: uniqueKeepOrder(override.reads || uploadParsed.roleData.reads || []),
|
|
1158
|
-
writes: uniqueKeepOrder(override.writes || uploadParsed.roleData.writes || []),
|
|
1159
|
-
handoffTo: uniqueKeepOrder(override.handoffTo || uploadParsed.roleData.handoffTo || []),
|
|
1160
|
-
rolePositioning: override.rolePositioning || uploadParsed.sections.rolePositioning || null,
|
|
1161
|
-
workingPrinciples: uniqueKeepOrder(
|
|
1162
|
-
override.workingPrinciples || uploadParsed.sections.workingPrinciples || [],
|
|
1163
|
-
),
|
|
1164
|
-
requiredSteps: uniqueKeepOrder(override.requiredSteps || uploadParsed.sections.requiredSteps || []),
|
|
1165
|
-
executionContract: override.executionContract || uploadParsed.sections.executionContract || null,
|
|
1166
|
-
outputStandard: override.outputStandard || uploadParsed.sections.outputStandard || null,
|
|
1167
|
-
prohibitedActions: uniqueKeepOrder(
|
|
1168
|
-
override.prohibitedActions || uploadParsed.sections.prohibitedActions || [],
|
|
1169
|
-
),
|
|
1170
|
-
handoffNotes: override.handoffNotes || uploadParsed.sections.handoffNotes || null,
|
|
1171
|
-
skillIds,
|
|
1172
|
-
ruleIds,
|
|
1173
|
-
domainIds,
|
|
1174
|
-
};
|
|
1175
|
-
|
|
1176
|
-
const versionFiles = buildRoleVersionFiles({
|
|
1177
|
-
...payload,
|
|
1178
|
-
skillSlugs: registrySkillSlugs,
|
|
1179
|
-
ruleSlugs: registryRuleSlugs,
|
|
1180
|
-
domainSlugs: uniqueKeepOrder(override.domainSlugs || local.domains || uploadParsed.roleData.domains || []),
|
|
1181
|
-
});
|
|
1182
|
-
|
|
1183
|
-
return {
|
|
1184
|
-
slug,
|
|
1185
|
-
payload,
|
|
1186
|
-
versionFiles,
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
function buildScenarioAsset(scenarioId, local, context) {
|
|
1191
|
-
const override = resolveResourceOverride({
|
|
1192
|
-
config: context.config,
|
|
1193
|
-
type: "scenarios",
|
|
1194
|
-
resourceId: scenarioId,
|
|
1195
|
-
});
|
|
1196
|
-
const roleItems = [];
|
|
1197
|
-
for (const roleSlug of local.roles || []) {
|
|
1198
|
-
const role = context.hubState.rolesBySlug[roleSlug];
|
|
1199
|
-
if (!role) {
|
|
1200
|
-
warn(`scenario ${scenarioId}: role ${roleSlug} not found in Hub, skip scenario`);
|
|
1201
|
-
return null;
|
|
1202
|
-
}
|
|
1203
|
-
roleItems.push({
|
|
1204
|
-
id: role.id,
|
|
1205
|
-
isOptional: Array.isArray(override.optionalRoles) && override.optionalRoles.includes(roleSlug),
|
|
1206
|
-
});
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
const explicitSkillIds = uniqueKeepOrder(
|
|
1210
|
-
(local.skills || []).flatMap((slug) => resolveLinkedResourceIds("skill", slug, context.hubState)),
|
|
1211
|
-
);
|
|
1212
|
-
const explicitRuleIds = uniqueKeepOrder(
|
|
1213
|
-
(local.rules || []).flatMap((slug) => resolveLinkedResourceIds("rule", slug, context.hubState)),
|
|
1214
|
-
);
|
|
1215
|
-
const roleSkillIds = roleItems.flatMap((item) => {
|
|
1216
|
-
const role = findRoleById(context.hubState, item.id);
|
|
1217
|
-
return role ? (role.skillLinks || []).map((link) => link.skillId).filter(Boolean) : [];
|
|
1218
|
-
});
|
|
1219
|
-
const roleRuleIds = roleItems.flatMap((item) => {
|
|
1220
|
-
const role = findRoleById(context.hubState, item.id);
|
|
1221
|
-
return role ? (role.ruleLinks || []).map((link) => link.ruleId).filter(Boolean) : [];
|
|
1222
|
-
});
|
|
1223
|
-
const skillIds = uniqueKeepOrder([...explicitSkillIds, ...roleSkillIds]);
|
|
1224
|
-
const ruleIds = uniqueKeepOrder([...explicitRuleIds, ...roleRuleIds]);
|
|
1225
|
-
const domainIds = resolveScenarioDomainIds({
|
|
1226
|
-
scenario: local,
|
|
1227
|
-
override,
|
|
1228
|
-
roleItems,
|
|
1229
|
-
hubState: context.hubState,
|
|
1230
|
-
config: context.config,
|
|
1231
|
-
});
|
|
1232
|
-
const supportedProfiles = uniqueKeepOrder(
|
|
1233
|
-
override.supportedProfiles ||
|
|
1234
|
-
local.profiles ||
|
|
1235
|
-
context.config?.defaults?.scenarioSupportedProfiles ||
|
|
1236
|
-
["vue", "react"],
|
|
1237
|
-
);
|
|
1238
|
-
const name = override.name || scenarioId;
|
|
1239
|
-
const payload = {
|
|
1240
|
-
name,
|
|
1241
|
-
slug: override.slug || scenarioId,
|
|
1242
|
-
description:
|
|
1243
|
-
override.description ||
|
|
1244
|
-
`自动同步场景方案,入口 ${override.entryRoleSlug || local.roles?.[0] || "unknown"},角色链路:${(local.roles || []).join(" -> ")}`,
|
|
1245
|
-
longDescription: override.longDescription || null,
|
|
1246
|
-
publishStatus: override.publishStatus || context.config?.defaults?.scenarioPublishStatus || "draft",
|
|
1247
|
-
tags: uniqueKeepOrder(override.tags || local.domains || []),
|
|
1248
|
-
supportedProfiles,
|
|
1249
|
-
recommendedIdes: uniqueKeepOrder(
|
|
1250
|
-
override.recommendedIdes || context.config?.defaults?.scenarioRecommendedIdes || ["cursor"],
|
|
1251
|
-
),
|
|
1252
|
-
entryRoleId: resolveScenarioEntryRoleId(override, local, context.hubState),
|
|
1253
|
-
isFeatured:
|
|
1254
|
-
typeof override.isFeatured === "boolean"
|
|
1255
|
-
? override.isFeatured
|
|
1256
|
-
: Boolean(context.config?.defaults?.scenarioFeatured),
|
|
1257
|
-
roles: sortByKey(roleItems, (row) => `${row.id}:${row.isOptional ? 1 : 0}`),
|
|
1258
|
-
skillIds: sortStrings(skillIds),
|
|
1259
|
-
ruleIds: sortStrings(ruleIds),
|
|
1260
|
-
domainIds: sortStrings(domainIds),
|
|
1261
|
-
};
|
|
1262
|
-
|
|
1263
|
-
if (!payload.entryRoleId && roleItems.length > 0) {
|
|
1264
|
-
payload.entryRoleId = roleItems[0].id;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
return {
|
|
1268
|
-
slug: payload.slug,
|
|
1269
|
-
payload,
|
|
1270
|
-
};
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
async function ensureSkillRuleVersion({ type, desired, slug, client, dryRun }) {
|
|
1274
|
-
const versions = await client.getJson(`/api/${type === "skill" ? "skills" : "rules"}/${encodeURIComponent(slug)}/versions`);
|
|
1275
|
-
const latest = Array.isArray(versions)
|
|
1276
|
-
? versions.find((item) => item && item.isLatest) || versions[0]
|
|
1277
|
-
: null;
|
|
1278
|
-
const currentFiles = normalizeFiles(latest?.files || []);
|
|
1279
|
-
const desiredFiles = normalizeFiles(desired.files || []);
|
|
1280
|
-
if (deepEqual(currentFiles, desiredFiles)) {
|
|
1281
|
-
info(`${type} ${slug}: version files unchanged`);
|
|
1282
|
-
return "unchanged";
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
if (dryRun) {
|
|
1286
|
-
info(`${type} ${slug}: publish version`);
|
|
1287
|
-
return "versioned";
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
const nextVersion = suggestNextPatchVersion(
|
|
1291
|
-
Array.isArray(versions) ? versions.map((item) => item.version).filter(Boolean) : [],
|
|
1292
|
-
);
|
|
1293
|
-
try {
|
|
1294
|
-
await client.postJson(`/api/${type === "skill" ? "skills" : "rules"}/${encodeURIComponent(slug)}/versions`, {
|
|
1295
|
-
version: nextVersion,
|
|
1296
|
-
changelog: "sync from local registry",
|
|
1297
|
-
files: desired.files,
|
|
1298
|
-
isLatest: true,
|
|
1299
|
-
});
|
|
1300
|
-
} catch (error) {
|
|
1301
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1302
|
-
if (message.includes("请先登录后再上传")) {
|
|
1303
|
-
throw new Error(
|
|
1304
|
-
`${type} ${slug}: version update requires agent login. Provide --agent-api-key or hub.agentApiKey.`,
|
|
1305
|
-
);
|
|
1306
|
-
}
|
|
1307
|
-
throw error;
|
|
1308
|
-
}
|
|
1309
|
-
info(`${type} ${slug}: version ${nextVersion} created`);
|
|
1310
|
-
return "versioned";
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
async function ensureRoleVersion({ client, slug, desiredVersionFiles, dryRun }) {
|
|
1314
|
-
const needsChange = await roleVersionWouldChange({
|
|
1315
|
-
client,
|
|
1316
|
-
slug,
|
|
1317
|
-
desiredVersionFiles,
|
|
1318
|
-
});
|
|
1319
|
-
if (!needsChange) {
|
|
1320
|
-
info(`role ${slug}: version files unchanged`);
|
|
1321
|
-
return;
|
|
1322
|
-
}
|
|
1323
|
-
if (dryRun) {
|
|
1324
|
-
info(`role ${slug}: publish version`);
|
|
1325
|
-
return;
|
|
1326
|
-
}
|
|
1327
|
-
const versions = await client.getJson(`/api/roles/${encodeURIComponent(slug)}/versions`);
|
|
1328
|
-
const nextVersion = suggestNextPatchVersion(
|
|
1329
|
-
Array.isArray(versions) ? versions.map((item) => item.version).filter(Boolean) : [],
|
|
1330
|
-
);
|
|
1331
|
-
await client.postJson(`/api/roles/${encodeURIComponent(slug)}/versions`, {
|
|
1332
|
-
version: nextVersion,
|
|
1333
|
-
changelog: "sync from local registry",
|
|
1334
|
-
isLatest: true,
|
|
1335
|
-
});
|
|
1336
|
-
info(`role ${slug}: version ${nextVersion} created`);
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
async function roleVersionWouldChange({ client, slug, desiredVersionFiles }) {
|
|
1340
|
-
const versions = await client.getJson(`/api/roles/${encodeURIComponent(slug)}/versions`);
|
|
1341
|
-
const latest = Array.isArray(versions)
|
|
1342
|
-
? versions.find((item) => item && item.isLatest) || versions[0]
|
|
1343
|
-
: null;
|
|
1344
|
-
const currentFiles = normalizeFiles(latest?.files || []);
|
|
1345
|
-
const desiredFiles = normalizeFiles(desiredVersionFiles || []);
|
|
1346
|
-
return !deepEqual(currentFiles, desiredFiles);
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
function buildProfileVariantSpecs({ type, resourceId, local, hubState }) {
|
|
1350
|
-
if (!local?.sourceByProfile || typeof local.sourceByProfile !== "object") {
|
|
1351
|
-
return [];
|
|
1352
|
-
}
|
|
1353
|
-
const profiles = Object.keys(local.sourceByProfile).sort();
|
|
1354
|
-
if (profiles.length <= 1) {
|
|
1355
|
-
return [];
|
|
1356
|
-
}
|
|
1357
|
-
const existingMatches = findResourcesByRegistryKey(type, resourceId, hubState);
|
|
1358
|
-
if (existingMatches.length === 0) {
|
|
1359
|
-
return [];
|
|
1360
|
-
}
|
|
1361
|
-
return profiles.map((profile) => {
|
|
1362
|
-
const existing = pickProfileResourceVariant(type, resourceId, profile, existingMatches);
|
|
1363
|
-
return {
|
|
1364
|
-
profile,
|
|
1365
|
-
slug: existing?.slug || defaultSplitResourceSlug(type, resourceId, profile),
|
|
1366
|
-
sourcePaths: [local.sourceByProfile[profile]].filter(Boolean),
|
|
1367
|
-
supportedProfiles: [profile],
|
|
1368
|
-
existing,
|
|
1369
|
-
};
|
|
1370
|
-
});
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
function findResourcesByRegistryKey(type, resourceId, hubState) {
|
|
1374
|
-
const collection = type === "skill" ? hubState.skillsBySlug : hubState.rulesBySlug;
|
|
1375
|
-
return Object.values(collection || {}).filter(
|
|
1376
|
-
(item) =>
|
|
1377
|
-
item &&
|
|
1378
|
-
(item.registryId === resourceId || item.manifestId === resourceId),
|
|
1379
|
-
);
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
function pickProfileResourceVariant(type, resourceId, profile, items) {
|
|
1383
|
-
const fallbackSlug = defaultSplitResourceSlug(type, resourceId, profile);
|
|
1384
|
-
return (
|
|
1385
|
-
items.find((item) => normalizeStringArray(item.supportedProfiles).includes(profile)) ||
|
|
1386
|
-
items.find((item) => item.slug === fallbackSlug) ||
|
|
1387
|
-
null
|
|
1388
|
-
);
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
function defaultSplitResourceSlug(type, resourceId, profile) {
|
|
1392
|
-
return type === "rule" ? `${profile}-${resourceId}` : `${resourceId}-${profile}`;
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
function resolveLinkedResourceIds(type, resourceId, hubState) {
|
|
1396
|
-
const collection = type === "skill" ? hubState.skillsBySlug : hubState.rulesBySlug;
|
|
1397
|
-
const direct = collection?.[resourceId];
|
|
1398
|
-
if (direct?.id) {
|
|
1399
|
-
return [direct.id];
|
|
1400
|
-
}
|
|
1401
|
-
return findResourcesByRegistryKey(type, resourceId, hubState)
|
|
1402
|
-
.map((item) => item.id)
|
|
1403
|
-
.filter(Boolean);
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
function collectSkillFiles(local, skillId, forcedSourcePaths) {
|
|
1407
|
-
const sourcePaths = forcedSourcePaths || resolveSourcePaths(local);
|
|
1408
|
-
if (sourcePaths.length === 0) return [];
|
|
1409
|
-
const absoluteSkillDirs = uniqueKeepOrder(
|
|
1410
|
-
sourcePaths.map((relativePath) => path.dirname(path.resolve(PROJECT_ROOT, relativePath))),
|
|
1411
|
-
);
|
|
1412
|
-
const baseDir = commonAncestor(absoluteSkillDirs);
|
|
1413
|
-
const fileEntries = [];
|
|
1414
|
-
for (const skillDir of absoluteSkillDirs) {
|
|
1415
|
-
for (const absoluteFile of walkFiles(skillDir)) {
|
|
1416
|
-
const buffer = fs.readFileSync(absoluteFile);
|
|
1417
|
-
if (looksBinary(buffer, absoluteFile)) {
|
|
1418
|
-
warn(`skill ${skillId}: skipped binary file ${path.relative(PROJECT_ROOT, absoluteFile)}`);
|
|
1419
|
-
continue;
|
|
1420
|
-
}
|
|
1421
|
-
const relativePath = toPosixPath(path.relative(baseDir, absoluteFile));
|
|
1422
|
-
fileEntries.push({
|
|
1423
|
-
name: path.basename(absoluteFile),
|
|
1424
|
-
path: relativePath,
|
|
1425
|
-
content: buffer.toString("utf8"),
|
|
1426
|
-
});
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
return normalizeFiles(fileEntries);
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
function collectRuleFiles(local, forcedSourcePaths) {
|
|
1433
|
-
const sourcePaths = forcedSourcePaths || resolveSourcePaths(local);
|
|
1434
|
-
if (sourcePaths.length === 0) return [];
|
|
1435
|
-
const absoluteFiles = uniqueKeepOrder(sourcePaths.map((relativePath) => path.resolve(PROJECT_ROOT, relativePath)));
|
|
1436
|
-
const baseDir = commonAncestor(absoluteFiles.map((absoluteFile) => path.dirname(absoluteFile)));
|
|
1437
|
-
return normalizeFiles(
|
|
1438
|
-
absoluteFiles.map((absoluteFile) => ({
|
|
1439
|
-
name: path.basename(absoluteFile),
|
|
1440
|
-
path: toPosixPath(path.relative(baseDir, absoluteFile)),
|
|
1441
|
-
content: fs.readFileSync(absoluteFile, "utf8"),
|
|
1442
|
-
})),
|
|
1443
|
-
);
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
function resolveSourcePaths(local) {
|
|
1447
|
-
if (local.source) {
|
|
1448
|
-
return [local.source];
|
|
1449
|
-
}
|
|
1450
|
-
if (local.sourceByProfile && typeof local.sourceByProfile === "object") {
|
|
1451
|
-
return Object.keys(local.sourceByProfile)
|
|
1452
|
-
.sort()
|
|
1453
|
-
.map((key) => local.sourceByProfile[key])
|
|
1454
|
-
.filter(Boolean);
|
|
1455
|
-
}
|
|
1456
|
-
return [];
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
async function parseRoleWithHub(client, sourcePath) {
|
|
1460
|
-
const buffer = fs.readFileSync(sourcePath);
|
|
1461
|
-
const form = new FormData();
|
|
1462
|
-
form.set("kind", "role");
|
|
1463
|
-
form.set("mode", "zip");
|
|
1464
|
-
form.set("file", new Blob([buffer]), path.basename(sourcePath));
|
|
1465
|
-
return client.postForm("/api/upload", form);
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
function resolveCategorySlug({ type, resourceId, override, domains, categories, config }) {
|
|
1469
|
-
if (override.categorySlug) return override.categorySlug;
|
|
1470
|
-
const categoryMap = config?.categoryMap?.[type] || {};
|
|
1471
|
-
if (categoryMap[resourceId]) return categoryMap[resourceId];
|
|
1472
|
-
for (const domain of domains || []) {
|
|
1473
|
-
if (categoryMap[`domain:${domain}`]) {
|
|
1474
|
-
return categoryMap[`domain:${domain}`];
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
const defaultKey = type === "skill" ? "skillCategorySlug" : "ruleCategorySlug";
|
|
1478
|
-
if (config?.defaults?.[defaultKey]) return config.defaults[defaultKey];
|
|
1479
|
-
if (Array.isArray(categories) && categories.length === 1) {
|
|
1480
|
-
return categories[0].slug;
|
|
1481
|
-
}
|
|
1482
|
-
return null;
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
function resolveResourceOverride({ config, type, resourceId, variantSlug }) {
|
|
1486
|
-
const resources = config?.resources?.[type] || {};
|
|
1487
|
-
const base = resources?.[resourceId] || {};
|
|
1488
|
-
const variant = variantSlug ? resources?.[variantSlug] || {} : {};
|
|
1489
|
-
return {
|
|
1490
|
-
...base,
|
|
1491
|
-
...variant,
|
|
1492
|
-
};
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
function buildSkillRuleMetadataPatch(type, desired, existing) {
|
|
1496
|
-
const patch = {};
|
|
1497
|
-
const existingCategorySlug = existing.categorySlug || existing.category?.slug || null;
|
|
1498
|
-
if (desired.name && desired.name !== existing.name) patch.name = desired.name;
|
|
1499
|
-
if (desired.slug && desired.slug !== existing.slug) patch.slug = desired.slug;
|
|
1500
|
-
if (desired.registryId !== undefined && desired.registryId !== existing.registryId) {
|
|
1501
|
-
patch.registryId = desired.registryId;
|
|
1502
|
-
}
|
|
1503
|
-
if (desired.manifestId !== undefined && desired.manifestId !== existing.manifestId) {
|
|
1504
|
-
patch.manifestId = desired.manifestId;
|
|
1505
|
-
}
|
|
1506
|
-
if (desired.description !== existing.description) {
|
|
1507
|
-
patch.description = desired.description;
|
|
1508
|
-
}
|
|
1509
|
-
if ((desired.longDescription || null) !== (existing.longDescription || null)) {
|
|
1510
|
-
patch.longDescription = desired.longDescription || null;
|
|
1511
|
-
}
|
|
1512
|
-
if (desired.author !== existing.author) {
|
|
1513
|
-
patch.author = desired.author;
|
|
1514
|
-
}
|
|
1515
|
-
if (desired.categorySlug && desired.categorySlug !== existingCategorySlug) {
|
|
1516
|
-
patch.categorySlug = desired.categorySlug;
|
|
1517
|
-
}
|
|
1518
|
-
if (!sameStringArray(desired.tags, existing.tags)) {
|
|
1519
|
-
patch.tags = desired.tags;
|
|
1520
|
-
}
|
|
1521
|
-
if (!sameStringArray(desired.supportedProfiles, existing.supportedProfiles)) {
|
|
1522
|
-
patch.supportedProfiles = desired.supportedProfiles;
|
|
1523
|
-
}
|
|
1524
|
-
if (desired.downloadPolicy !== existing.downloadPolicy) {
|
|
1525
|
-
patch.downloadPolicy = desired.downloadPolicy;
|
|
1526
|
-
}
|
|
1527
|
-
return Object.keys(patch).length > 0 ? patch : null;
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
function buildSkillRuleFullPatch(desired) {
|
|
1531
|
-
return {
|
|
1532
|
-
name: desired.name,
|
|
1533
|
-
slug: desired.slug,
|
|
1534
|
-
registryId: desired.registryId,
|
|
1535
|
-
manifestId: desired.manifestId,
|
|
1536
|
-
description: desired.description,
|
|
1537
|
-
longDescription: desired.longDescription || null,
|
|
1538
|
-
author: desired.author,
|
|
1539
|
-
categorySlug: desired.categorySlug,
|
|
1540
|
-
tags: desired.tags,
|
|
1541
|
-
supportedProfiles: desired.supportedProfiles,
|
|
1542
|
-
downloadPolicy: desired.downloadPolicy,
|
|
1543
|
-
};
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
async function createSkillRuleOrNull({ type, desired, client }) {
|
|
1547
|
-
try {
|
|
1548
|
-
const response = await client.postJson(`/${type === "skill" ? "api/skills" : "api/rules"}`, {
|
|
1549
|
-
name: desired.name,
|
|
1550
|
-
slug: desired.slug,
|
|
1551
|
-
registryId: desired.registryId,
|
|
1552
|
-
manifestId: desired.manifestId,
|
|
1553
|
-
description: desired.description,
|
|
1554
|
-
longDescription: desired.longDescription,
|
|
1555
|
-
author: desired.author,
|
|
1556
|
-
categorySlug: desired.categorySlug,
|
|
1557
|
-
tags: desired.tags,
|
|
1558
|
-
supportedProfiles: desired.supportedProfiles,
|
|
1559
|
-
downloadPolicy: desired.downloadPolicy,
|
|
1560
|
-
initialFiles: desired.files,
|
|
1561
|
-
});
|
|
1562
|
-
return {
|
|
1563
|
-
created: true,
|
|
1564
|
-
resource: response?.[type] || response || null,
|
|
1565
|
-
};
|
|
1566
|
-
} catch (error) {
|
|
1567
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1568
|
-
if (isConflictMessage(message)) {
|
|
1569
|
-
return {
|
|
1570
|
-
created: false,
|
|
1571
|
-
resource: null,
|
|
1572
|
-
conflictSlugs: extractConflictResourceSlugs(message),
|
|
1573
|
-
};
|
|
1574
|
-
}
|
|
1575
|
-
throw error;
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
async function fetchPublicResource(client, type, slug) {
|
|
1580
|
-
try {
|
|
1581
|
-
return await client.getJson(`/api/${type === "skill" ? "skills" : "rules"}/${encodeURIComponent(slug)}`);
|
|
1582
|
-
} catch {
|
|
1583
|
-
return null;
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
async function fetchConflictResource({ type, desiredSlug, conflictSlugs, client }) {
|
|
1588
|
-
const candidates = uniqueKeepOrder([desiredSlug, ...(conflictSlugs || [])]);
|
|
1589
|
-
for (const candidate of candidates) {
|
|
1590
|
-
const resource = await fetchPublicResource(client, type, candidate);
|
|
1591
|
-
if (resource) {
|
|
1592
|
-
return resource;
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
return null;
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
function resolveRoleDomainIds({ override, local, uploadParsed, config }) {
|
|
1599
|
-
const explicit = Array.isArray(override.domainIds) ? override.domainIds : [];
|
|
1600
|
-
if (explicit.length > 0) {
|
|
1601
|
-
return explicit;
|
|
1602
|
-
}
|
|
1603
|
-
const configMap = config?.domainIdMap || {};
|
|
1604
|
-
const mapped = uniqueKeepOrder([
|
|
1605
|
-
...(Array.isArray(local.domains) ? local.domains : []),
|
|
1606
|
-
...(Array.isArray(uploadParsed.roleData.domains) ? uploadParsed.roleData.domains : []),
|
|
1607
|
-
])
|
|
1608
|
-
.map((slug) => configMap[slug])
|
|
1609
|
-
.filter(Boolean);
|
|
1610
|
-
if (mapped.length > 0) {
|
|
1611
|
-
return mapped;
|
|
1612
|
-
}
|
|
1613
|
-
if (Array.isArray(uploadParsed.mappedDomainIds) && uploadParsed.mappedDomainIds.length > 0) {
|
|
1614
|
-
return uniqueKeepOrder(uploadParsed.mappedDomainIds);
|
|
1615
|
-
}
|
|
1616
|
-
return [];
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
function resolveScenarioDomainIds({ scenario, override, roleItems, hubState, config }) {
|
|
1620
|
-
if (Array.isArray(override.domainIds) && override.domainIds.length > 0) {
|
|
1621
|
-
return uniqueKeepOrder(override.domainIds);
|
|
1622
|
-
}
|
|
1623
|
-
const domainMap = config?.domainIdMap || {};
|
|
1624
|
-
const mapped = uniqueKeepOrder(Array.isArray(scenario.domains) ? scenario.domains : [])
|
|
1625
|
-
.map((slug) => domainMap[slug])
|
|
1626
|
-
.filter(Boolean);
|
|
1627
|
-
if (mapped.length > 0) {
|
|
1628
|
-
return mapped;
|
|
1629
|
-
}
|
|
1630
|
-
const roleDomainIds = [];
|
|
1631
|
-
for (const roleItem of roleItems) {
|
|
1632
|
-
const role = Object.values(hubState.rolesBySlug).find((item) => item.id === roleItem.id);
|
|
1633
|
-
if (role && Array.isArray(role.domainLinks)) {
|
|
1634
|
-
roleDomainIds.push(...role.domainLinks.map((link) => link.domainId).filter(Boolean));
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
return uniqueKeepOrder(roleDomainIds);
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
function resolveScenarioEntryRoleId(override, local, hubState) {
|
|
1641
|
-
if (override.entryRoleId) return override.entryRoleId;
|
|
1642
|
-
if (override.entryRoleSlug && hubState.rolesBySlug[override.entryRoleSlug]) {
|
|
1643
|
-
return hubState.rolesBySlug[override.entryRoleSlug].id;
|
|
1644
|
-
}
|
|
1645
|
-
const firstRoleSlug = Array.isArray(local.roles) ? local.roles[0] : null;
|
|
1646
|
-
return firstRoleSlug && hubState.rolesBySlug[firstRoleSlug]
|
|
1647
|
-
? hubState.rolesBySlug[firstRoleSlug].id
|
|
1648
|
-
: null;
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
function normalizeRoleResponseToPayload(item) {
|
|
1652
|
-
return {
|
|
1653
|
-
name: item.name,
|
|
1654
|
-
slug: item.slug,
|
|
1655
|
-
registryId: item.registryId || null,
|
|
1656
|
-
manifestId: item.manifestId || null,
|
|
1657
|
-
author: item.author,
|
|
1658
|
-
description: item.description,
|
|
1659
|
-
longDescription: item.longDescription || null,
|
|
1660
|
-
publishStatus: item.publishStatus,
|
|
1661
|
-
roleStatus: item.roleStatus,
|
|
1662
|
-
tags: normalizeStringArray(item.tags),
|
|
1663
|
-
supportedProfiles: normalizeStringArray(item.supportedProfiles),
|
|
1664
|
-
triggers: normalizeStringArray(item.triggers),
|
|
1665
|
-
preferredSkills: normalizeStringArray(item.preferredSkills),
|
|
1666
|
-
reads: normalizeStringArray(item.reads),
|
|
1667
|
-
writes: normalizeStringArray(item.writes),
|
|
1668
|
-
handoffTo: normalizeStringArray(item.handoffTo),
|
|
1669
|
-
rolePositioning: item.rolePositioning || null,
|
|
1670
|
-
workingPrinciples: normalizeStringArray(item.workingPrinciples),
|
|
1671
|
-
requiredSteps: normalizeStringArray(item.requiredSteps),
|
|
1672
|
-
executionContract: item.executionContract || null,
|
|
1673
|
-
outputStandard: item.outputStandard || null,
|
|
1674
|
-
prohibitedActions: normalizeStringArray(item.prohibitedActions),
|
|
1675
|
-
handoffNotes: item.handoffNotes || null,
|
|
1676
|
-
skillIds: (item.skillLinks || []).map((link) => link.skillId),
|
|
1677
|
-
ruleIds: (item.ruleLinks || []).map((link) => link.ruleId),
|
|
1678
|
-
domainIds: (item.domainLinks || []).map((link) => link.domainId),
|
|
1679
|
-
};
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
function buildPreviewSkillRuleState(type, existing, desired) {
|
|
1683
|
-
return {
|
|
1684
|
-
...(existing || {}),
|
|
1685
|
-
id: existing?.id || previewResourceId(type, desired.slug),
|
|
1686
|
-
slug: desired.slug,
|
|
1687
|
-
name: desired.name,
|
|
1688
|
-
registryId: desired.registryId ?? existing?.registryId ?? null,
|
|
1689
|
-
manifestId: desired.manifestId ?? existing?.manifestId ?? null,
|
|
1690
|
-
description: desired.description,
|
|
1691
|
-
longDescription: desired.longDescription || null,
|
|
1692
|
-
author: desired.author,
|
|
1693
|
-
tags: desired.tags,
|
|
1694
|
-
supportedProfiles: desired.supportedProfiles,
|
|
1695
|
-
categorySlug: desired.categorySlug || existing?.categorySlug || null,
|
|
1696
|
-
categoryName: existing?.categoryName || desired.categorySlug || null,
|
|
1697
|
-
downloadPolicy: desired.downloadPolicy,
|
|
1698
|
-
};
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
function buildPreviewRoleState(existing, desired) {
|
|
1702
|
-
return {
|
|
1703
|
-
...(existing || {}),
|
|
1704
|
-
id: existing?.id || previewResourceId("role", desired.slug),
|
|
1705
|
-
...desired.payload,
|
|
1706
|
-
skillLinks: (desired.payload.skillIds || []).map((skillId) => ({ skillId })),
|
|
1707
|
-
ruleLinks: (desired.payload.ruleIds || []).map((ruleId) => ({ ruleId })),
|
|
1708
|
-
domainLinks: (desired.payload.domainIds || []).map((domainId) => ({ domainId })),
|
|
1709
|
-
};
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
function normalizeScenarioResponseToPayload(item) {
|
|
1713
|
-
return {
|
|
1714
|
-
name: item.name,
|
|
1715
|
-
slug: item.slug,
|
|
1716
|
-
description: item.description,
|
|
1717
|
-
longDescription: item.longDescription || null,
|
|
1718
|
-
publishStatus: item.publishStatus,
|
|
1719
|
-
tags: normalizeStringArray(item.tags),
|
|
1720
|
-
supportedProfiles: normalizeStringArray(item.supportedProfiles),
|
|
1721
|
-
recommendedIdes: normalizeStringArray(item.recommendedIdes),
|
|
1722
|
-
entryRoleId: item.entryRoleId || null,
|
|
1723
|
-
isFeatured: Boolean(item.isFeatured),
|
|
1724
|
-
roles: sortByKey(
|
|
1725
|
-
(item.roles || []).map((link) => ({
|
|
1726
|
-
id: link.roleId,
|
|
1727
|
-
isOptional: Boolean(link.isOptional),
|
|
1728
|
-
})),
|
|
1729
|
-
(row) => `${row.id}:${row.isOptional ? 1 : 0}`,
|
|
1730
|
-
),
|
|
1731
|
-
skillIds: sortStrings((item.skills || []).map((link) => link.skillId)),
|
|
1732
|
-
ruleIds: sortStrings((item.rules || []).map((link) => link.ruleId)),
|
|
1733
|
-
domainIds: sortStrings((item.domainLinks || []).map((link) => link.domainId)),
|
|
1734
|
-
};
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
function buildPreviewScenarioState(existing, desired) {
|
|
1738
|
-
return {
|
|
1739
|
-
...(existing || {}),
|
|
1740
|
-
id: existing?.id || previewResourceId("scenario", desired.slug),
|
|
1741
|
-
...desired.payload,
|
|
1742
|
-
roles: (desired.payload.roles || []).map((link) => ({
|
|
1743
|
-
roleId: link.id,
|
|
1744
|
-
isOptional: Boolean(link.isOptional),
|
|
1745
|
-
})),
|
|
1746
|
-
skills: (desired.payload.skillIds || []).map((skillId) => ({ skillId })),
|
|
1747
|
-
rules: (desired.payload.ruleIds || []).map((ruleId) => ({ ruleId })),
|
|
1748
|
-
domainLinks: (desired.payload.domainIds || []).map((domainId) => ({ domainId })),
|
|
1749
|
-
};
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
function buildRoleVersionFiles(input) {
|
|
1753
|
-
return normalizeFiles([
|
|
1754
|
-
{
|
|
1755
|
-
name: `${input.slug}.role.json`,
|
|
1756
|
-
path: `.hub/roles/${input.slug}.role.json`,
|
|
1757
|
-
content: JSON.stringify(
|
|
1758
|
-
{
|
|
1759
|
-
name: input.name,
|
|
1760
|
-
slug: input.slug,
|
|
1761
|
-
author: input.author,
|
|
1762
|
-
description: input.description,
|
|
1763
|
-
longDescription: input.longDescription ?? null,
|
|
1764
|
-
publishStatus: input.publishStatus,
|
|
1765
|
-
roleStatus: input.roleStatus,
|
|
1766
|
-
supportedProfiles: input.supportedProfiles,
|
|
1767
|
-
tags: input.tags,
|
|
1768
|
-
triggers: input.triggers,
|
|
1769
|
-
preferredSkills: input.preferredSkills,
|
|
1770
|
-
reads: input.reads,
|
|
1771
|
-
writes: input.writes,
|
|
1772
|
-
handoffTo: input.handoffTo,
|
|
1773
|
-
skills: input.skillSlugs,
|
|
1774
|
-
rules: input.ruleSlugs,
|
|
1775
|
-
capabilityDomains: input.domainSlugs,
|
|
1776
|
-
sections: {
|
|
1777
|
-
rolePositioning: input.rolePositioning ?? null,
|
|
1778
|
-
workingPrinciples: input.workingPrinciples,
|
|
1779
|
-
requiredSteps: input.requiredSteps,
|
|
1780
|
-
executionContract: input.executionContract ?? null,
|
|
1781
|
-
outputStandard: input.outputStandard ?? null,
|
|
1782
|
-
prohibitedActions: input.prohibitedActions,
|
|
1783
|
-
handoffNotes: input.handoffNotes ?? null,
|
|
1784
|
-
},
|
|
1785
|
-
},
|
|
1786
|
-
null,
|
|
1787
|
-
2,
|
|
1788
|
-
),
|
|
1789
|
-
},
|
|
1790
|
-
]);
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
function walkFiles(rootDir) {
|
|
1794
|
-
const results = [];
|
|
1795
|
-
const stack = [rootDir];
|
|
1796
|
-
while (stack.length > 0) {
|
|
1797
|
-
const current = stack.pop();
|
|
1798
|
-
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
1799
|
-
for (const entry of entries) {
|
|
1800
|
-
if (entry.name === ".DS_Store") continue;
|
|
1801
|
-
const absolutePath = path.join(current, entry.name);
|
|
1802
|
-
if (entry.isDirectory()) {
|
|
1803
|
-
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
1804
|
-
stack.push(absolutePath);
|
|
1805
|
-
continue;
|
|
1806
|
-
}
|
|
1807
|
-
results.push(absolutePath);
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
results.sort();
|
|
1811
|
-
return results;
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
function commonAncestor(paths) {
|
|
1815
|
-
if (!Array.isArray(paths) || paths.length === 0) {
|
|
1816
|
-
return PROJECT_ROOT;
|
|
1817
|
-
}
|
|
1818
|
-
const split = paths.map((item) => path.resolve(item).split(path.sep).filter(Boolean));
|
|
1819
|
-
const minLength = Math.min(...split.map((parts) => parts.length));
|
|
1820
|
-
const shared = [];
|
|
1821
|
-
for (let index = 0; index < minLength; index += 1) {
|
|
1822
|
-
const value = split[0][index];
|
|
1823
|
-
if (split.every((parts) => parts[index] === value)) {
|
|
1824
|
-
shared.push(value);
|
|
1825
|
-
} else {
|
|
1826
|
-
break;
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
const prefix = path.isAbsolute(paths[0]) ? path.sep : "";
|
|
1830
|
-
return prefix + shared.join(path.sep);
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
function parseFrontmatterFile(content, type) {
|
|
1834
|
-
const trimmed = content.trimStart();
|
|
1835
|
-
if (!trimmed.startsWith("---")) {
|
|
1836
|
-
return {};
|
|
1837
|
-
}
|
|
1838
|
-
const match = trimmed.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
1839
|
-
if (!match) {
|
|
1840
|
-
return {};
|
|
1841
|
-
}
|
|
1842
|
-
const meta = {};
|
|
1843
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
1844
|
-
const parsed = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
1845
|
-
if (!parsed) continue;
|
|
1846
|
-
let value = parsed[2].trim();
|
|
1847
|
-
if (
|
|
1848
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
1849
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
1850
|
-
) {
|
|
1851
|
-
value = value.slice(1, -1);
|
|
1852
|
-
}
|
|
1853
|
-
meta[parsed[1].toLowerCase()] = value;
|
|
1854
|
-
}
|
|
1855
|
-
const name =
|
|
1856
|
-
meta.name || meta.title || meta["display-name"] || (type === "skill" ? meta.skill : meta.rule);
|
|
1857
|
-
const description = meta.description || meta.summary || meta.desc;
|
|
1858
|
-
return {
|
|
1859
|
-
name: typeof name === "string" ? name : undefined,
|
|
1860
|
-
description: typeof description === "string" ? description : undefined,
|
|
1861
|
-
};
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
function pickPrimaryTextFile(files, filename) {
|
|
1865
|
-
return files.find((file) => path.basename(file.path).toLowerCase() === filename.toLowerCase()) || null;
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
function normalizeFiles(files) {
|
|
1869
|
-
return [...files]
|
|
1870
|
-
.map((file) => ({
|
|
1871
|
-
name: file.name,
|
|
1872
|
-
path: toPosixPath(file.path),
|
|
1873
|
-
...(typeof file.content === "string" ? { content: file.content } : {}),
|
|
1874
|
-
}))
|
|
1875
|
-
.sort((left, right) => left.path.localeCompare(right.path));
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
function normalizeStringArray(value) {
|
|
1879
|
-
if (!Array.isArray(value)) return [];
|
|
1880
|
-
return value.filter((item) => typeof item === "string");
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
function sortStrings(value) {
|
|
1884
|
-
return normalizeStringArray(value).slice().sort((left, right) => left.localeCompare(right));
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
function sortByKey(items, pickKey) {
|
|
1888
|
-
return [...(items || [])].sort((left, right) => pickKey(left).localeCompare(pickKey(right)));
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
function sameStringArray(left, right) {
|
|
1892
|
-
return deepEqual(sortStrings(left), sortStrings(right));
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
function previewResourceId(type, slug) {
|
|
1896
|
-
return `preview-${type}-${slug}`;
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
function looksBinary(buffer, absolutePath) {
|
|
1900
|
-
const extension = path.extname(absolutePath).toLowerCase();
|
|
1901
|
-
if (TEXT_FILE_EXTENSIONS.has(extension)) {
|
|
1902
|
-
return false;
|
|
1903
|
-
}
|
|
1904
|
-
if (!extension && path.basename(absolutePath).startsWith(".")) {
|
|
1905
|
-
return false;
|
|
1906
|
-
}
|
|
1907
|
-
return buffer.includes(0);
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
function suggestNextPatchVersion(currentVersions) {
|
|
1911
|
-
const parsed = currentVersions
|
|
1912
|
-
.map((value) => {
|
|
1913
|
-
const match = String(value).trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
1914
|
-
if (!match) return null;
|
|
1915
|
-
return {
|
|
1916
|
-
major: Number(match[1]),
|
|
1917
|
-
minor: Number(match[2]),
|
|
1918
|
-
patch: Number(match[3]),
|
|
1919
|
-
};
|
|
1920
|
-
})
|
|
1921
|
-
.filter(Boolean);
|
|
1922
|
-
if (parsed.length === 0) return "1.0.0";
|
|
1923
|
-
parsed.sort((left, right) => {
|
|
1924
|
-
if (left.major !== right.major) return right.major - left.major;
|
|
1925
|
-
if (left.minor !== right.minor) return right.minor - left.minor;
|
|
1926
|
-
return right.patch - left.patch;
|
|
1927
|
-
});
|
|
1928
|
-
const latest = parsed[0];
|
|
1929
|
-
return `${latest.major}.${latest.minor}.${latest.patch + 1}`;
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
function selectResourceIds(registry, selection) {
|
|
1933
|
-
const all = Object.keys(registry);
|
|
1934
|
-
if (selection.mode === "all") return all;
|
|
1935
|
-
if (selection.mode === "none") return [];
|
|
1936
|
-
return all.filter((id) => selection.values.has(id));
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
function uniqueKeepOrder(items) {
|
|
1940
|
-
const output = [];
|
|
1941
|
-
const seen = new Set();
|
|
1942
|
-
for (const item of items || []) {
|
|
1943
|
-
if (!item) continue;
|
|
1944
|
-
if (seen.has(item)) continue;
|
|
1945
|
-
seen.add(item);
|
|
1946
|
-
output.push(item);
|
|
1947
|
-
}
|
|
1948
|
-
return output;
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
function toPosixPath(filePath) {
|
|
1952
|
-
return filePath.split(path.sep).join("/");
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
function deepEqual(left, right) {
|
|
1956
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
function isConflictMessage(message) {
|
|
1960
|
-
return (
|
|
1961
|
-
typeof message === "string" &&
|
|
1962
|
-
(message.includes("已存在") ||
|
|
1963
|
-
message.includes("409") ||
|
|
1964
|
-
message.includes("duplicate") ||
|
|
1965
|
-
message.includes("Unique"))
|
|
1966
|
-
);
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
function extractConflictResourceSlugs(message) {
|
|
1970
|
-
if (typeof message !== "string") return [];
|
|
1971
|
-
const matches = [...message.matchAll(/对应\s*(?:Rule|Skill)\s*:([a-z0-9._-]+)/gi)];
|
|
1972
|
-
return uniqueKeepOrder(matches.map((match) => match[1]).filter(Boolean));
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
function indexBy(items, key) {
|
|
1976
|
-
const output = {};
|
|
1977
|
-
for (const item of items || []) {
|
|
1978
|
-
if (!item || !item[key]) continue;
|
|
1979
|
-
output[item[key]] = item;
|
|
1980
|
-
}
|
|
1981
|
-
return output;
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
function findRoleById(hubState, id) {
|
|
1985
|
-
return Object.values(hubState.rolesBySlug || {}).find((item) => item.id === id) || null;
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
function unwrapApiResponse(payload) {
|
|
1989
|
-
if (
|
|
1990
|
-
payload &&
|
|
1991
|
-
typeof payload === "object" &&
|
|
1992
|
-
Object.prototype.hasOwnProperty.call(payload, "data") &&
|
|
1993
|
-
Object.prototype.hasOwnProperty.call(payload, "code")
|
|
1994
|
-
) {
|
|
1995
|
-
return payload.data;
|
|
1996
|
-
}
|
|
1997
|
-
return payload;
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
function getResponseCookies(response) {
|
|
2001
|
-
if (typeof response.headers.getSetCookie === "function") {
|
|
2002
|
-
return response.headers.getSetCookie().map((cookie) => cookie.split(";")[0]);
|
|
2003
|
-
}
|
|
2004
|
-
const cookie = response.headers.get("set-cookie");
|
|
2005
|
-
return cookie ? [cookie.split(";")[0]] : [];
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
async function readErrorText(response) {
|
|
2009
|
-
const text = await response.text();
|
|
2010
|
-
try {
|
|
2011
|
-
const parsed = JSON.parse(text);
|
|
2012
|
-
return parsed?.error || parsed?.message || text;
|
|
2013
|
-
} catch {
|
|
2014
|
-
return text || `${response.status} ${response.statusText}`;
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
function readJson(filePath) {
|
|
2019
|
-
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
function warn(message) {
|
|
2023
|
-
console.warn(`[hub-sync] warn: ${message}`);
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
function info(message) {
|
|
2027
|
-
console.log(`[hub-sync] ${message}`);
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
function printSummary(summary, dryRun) {
|
|
2031
|
-
console.log("");
|
|
2032
|
-
console.log(`[hub-sync] ${dryRun ? "dry-run summary" : "summary"}`);
|
|
2033
|
-
console.log(
|
|
2034
|
-
JSON.stringify(summary, null, 2),
|
|
2035
|
-
);
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const {
|
|
6
|
+
collectRelatedAssetIdsFromScenarios,
|
|
7
|
+
mergeSelectionWithDerivedIds,
|
|
8
|
+
} = require("../internal/hub-sync-selection");
|
|
9
|
+
|
|
10
|
+
const PROJECT_ROOT = process.cwd();
|
|
11
|
+
const DEFAULT_BASE_URL = "http://localhost:3000";
|
|
12
|
+
const DEFAULT_HUB_PROJECT = path.resolve(PROJECT_ROOT, "../skill-q-platform");
|
|
13
|
+
const DEFAULT_CONFIG_PATH = path.resolve(PROJECT_ROOT, "scripts/hub-sync-assets.config.json");
|
|
14
|
+
const DEFAULT_CONFIG_EXAMPLE_PATH = path.resolve(
|
|
15
|
+
PROJECT_ROOT,
|
|
16
|
+
"scripts/hub-sync-assets.config.example.json",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const TEXT_FILE_EXTENSIONS = new Set([
|
|
20
|
+
".md",
|
|
21
|
+
".mdx",
|
|
22
|
+
".txt",
|
|
23
|
+
".json",
|
|
24
|
+
".jsonc",
|
|
25
|
+
".yaml",
|
|
26
|
+
".yml",
|
|
27
|
+
".js",
|
|
28
|
+
".cjs",
|
|
29
|
+
".mjs",
|
|
30
|
+
".ts",
|
|
31
|
+
".tsx",
|
|
32
|
+
".jsx",
|
|
33
|
+
".css",
|
|
34
|
+
".scss",
|
|
35
|
+
".sass",
|
|
36
|
+
".less",
|
|
37
|
+
".html",
|
|
38
|
+
".xml",
|
|
39
|
+
".svg",
|
|
40
|
+
".sh",
|
|
41
|
+
".ps1",
|
|
42
|
+
".py",
|
|
43
|
+
".sql",
|
|
44
|
+
".toml",
|
|
45
|
+
".env",
|
|
46
|
+
".gitignore",
|
|
47
|
+
".npmrc",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
function main() {
|
|
51
|
+
const options = parseArgs(process.argv.slice(2));
|
|
52
|
+
if (options.help) {
|
|
53
|
+
printHelp();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
run(options).catch((error) => {
|
|
58
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
59
|
+
console.error(`[hub-sync] failed: ${message}`);
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function run(cliOptions) {
|
|
65
|
+
const config = loadConfig(cliOptions.configPath);
|
|
66
|
+
const resolved = resolveRuntimeOptions(cliOptions, config);
|
|
67
|
+
const client = new HubClient(resolved);
|
|
68
|
+
|
|
69
|
+
const shouldUseAdminSession =
|
|
70
|
+
!resolved.skipRoles ||
|
|
71
|
+
!resolved.skipScenarios ||
|
|
72
|
+
resolved.hasAdminAuthInput;
|
|
73
|
+
if (shouldUseAdminSession && !client.hasAdminAccess()) {
|
|
74
|
+
await client.ensureAdminSession();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const categories = resolved.skipSkills && resolved.skipRules
|
|
78
|
+
? { skill: [], rule: [] }
|
|
79
|
+
: client.hasAdminAccess()
|
|
80
|
+
? await loadCategories(client, resolved)
|
|
81
|
+
: { skill: [], rule: [] };
|
|
82
|
+
const skillBrowseItems = !client.hasAdminAccess() || (resolved.skipSkills && resolved.skipRoles && resolved.skipScenarios)
|
|
83
|
+
? []
|
|
84
|
+
: await loadBrowseItems(client, "skill", resolved);
|
|
85
|
+
const ruleBrowseItems = !client.hasAdminAccess() || (resolved.skipRules && resolved.skipRoles && resolved.skipScenarios)
|
|
86
|
+
? []
|
|
87
|
+
: await loadBrowseItems(client, "rule", resolved);
|
|
88
|
+
const roleResponse = resolved.skipRoles && resolved.skipScenarios
|
|
89
|
+
? { items: [] }
|
|
90
|
+
: await client.getJson("/api/admin/roles");
|
|
91
|
+
const scenarioResponse = resolved.skipScenarios
|
|
92
|
+
? { items: [] }
|
|
93
|
+
: await client.getJson("/api/admin/scenarios");
|
|
94
|
+
|
|
95
|
+
const localRegistries = {
|
|
96
|
+
skills: readJson(path.resolve(PROJECT_ROOT, ".agents/registry/skills.json")).skills || {},
|
|
97
|
+
rules: readJson(path.resolve(PROJECT_ROOT, ".agents/registry/rules.json")).rules || {},
|
|
98
|
+
roles: readJson(path.resolve(PROJECT_ROOT, ".agents/registry/roles.json")).roles || {},
|
|
99
|
+
scenarios:
|
|
100
|
+
readJson(path.resolve(PROJECT_ROOT, ".agents/registry/scenario-packages.json")).scenario_packages || {},
|
|
101
|
+
};
|
|
102
|
+
const relatedScenarioIds = selectResourceIds(
|
|
103
|
+
localRegistries.scenarios,
|
|
104
|
+
resolved.fromScenarioSelection,
|
|
105
|
+
);
|
|
106
|
+
const relatedAssets = collectRelatedAssetIdsFromScenarios({
|
|
107
|
+
scenarioIds: relatedScenarioIds,
|
|
108
|
+
localScenarios: localRegistries.scenarios,
|
|
109
|
+
localRoles: localRegistries.roles,
|
|
110
|
+
});
|
|
111
|
+
resolved.roleSelection = mergeSelectionWithDerivedIds({
|
|
112
|
+
selection: resolved.roleSelection,
|
|
113
|
+
selectionSpecified: resolved.roleSelectionSpecified,
|
|
114
|
+
derivedIds: relatedAssets.roleIds,
|
|
115
|
+
preferDerivedWhenImplicitAll: relatedScenarioIds.length > 0,
|
|
116
|
+
});
|
|
117
|
+
resolved.skillSelection = mergeSelectionWithDerivedIds({
|
|
118
|
+
selection: resolved.skillSelection,
|
|
119
|
+
selectionSpecified: resolved.skillSelectionSpecified,
|
|
120
|
+
derivedIds: relatedAssets.skillIds,
|
|
121
|
+
preferDerivedWhenImplicitAll: relatedScenarioIds.length > 0,
|
|
122
|
+
});
|
|
123
|
+
resolved.skipRoles = isSelectionNone(resolved.roleSelection);
|
|
124
|
+
resolved.skipSkills = isSelectionNone(resolved.skillSelection);
|
|
125
|
+
|
|
126
|
+
const hubState = {
|
|
127
|
+
categories,
|
|
128
|
+
skillsBySlug: indexBy(skillBrowseItems.items || [], "slug"),
|
|
129
|
+
rulesBySlug: indexBy(ruleBrowseItems.items || [], "slug"),
|
|
130
|
+
rolesBySlug: indexBy(roleResponse.items || [], "slug"),
|
|
131
|
+
scenariosBySlug: indexBy(scenarioResponse.items || [], "slug"),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const summary = {
|
|
135
|
+
skill: { created: 0, updated: 0, versioned: 0, skipped: 0 },
|
|
136
|
+
rule: { created: 0, updated: 0, versioned: 0, skipped: 0 },
|
|
137
|
+
role: { created: 0, updated: 0, versioned: 0, skipped: 0 },
|
|
138
|
+
scenario: { created: 0, updated: 0, skipped: 0 },
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (!resolved.skipRules) {
|
|
142
|
+
await syncRules({
|
|
143
|
+
client,
|
|
144
|
+
resolved,
|
|
145
|
+
config,
|
|
146
|
+
localRules: localRegistries.rules,
|
|
147
|
+
hubState,
|
|
148
|
+
summary,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!resolved.skipSkills) {
|
|
153
|
+
await syncSkills({
|
|
154
|
+
client,
|
|
155
|
+
resolved,
|
|
156
|
+
config,
|
|
157
|
+
localSkills: localRegistries.skills,
|
|
158
|
+
hubState,
|
|
159
|
+
summary,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!resolved.skipRoles) {
|
|
164
|
+
await syncRoles({
|
|
165
|
+
client,
|
|
166
|
+
resolved,
|
|
167
|
+
config,
|
|
168
|
+
localRoles: localRegistries.roles,
|
|
169
|
+
hubState,
|
|
170
|
+
summary,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!resolved.skipScenarios) {
|
|
175
|
+
await syncScenarios({
|
|
176
|
+
client,
|
|
177
|
+
resolved,
|
|
178
|
+
config,
|
|
179
|
+
localScenarios: localRegistries.scenarios,
|
|
180
|
+
hubState,
|
|
181
|
+
summary,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
printSummary(summary, resolved.dryRun);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseArgs(argv) {
|
|
189
|
+
const args = {
|
|
190
|
+
help: false,
|
|
191
|
+
dryRun: false,
|
|
192
|
+
baseUrl: undefined,
|
|
193
|
+
hubProject: undefined,
|
|
194
|
+
configPath: DEFAULT_CONFIG_PATH,
|
|
195
|
+
adminEmail: undefined,
|
|
196
|
+
adminPassword: undefined,
|
|
197
|
+
adminCookie: undefined,
|
|
198
|
+
adminSecret: undefined,
|
|
199
|
+
agentApiKey: undefined,
|
|
200
|
+
skills: undefined,
|
|
201
|
+
rules: undefined,
|
|
202
|
+
roles: undefined,
|
|
203
|
+
scenarios: undefined,
|
|
204
|
+
fromScenarios: undefined,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
208
|
+
const current = argv[index];
|
|
209
|
+
if (current === "--help" || current === "-h") {
|
|
210
|
+
args.help = true;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (current === "--dry-run") {
|
|
214
|
+
args.dryRun = true;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const next = argv[index + 1];
|
|
218
|
+
if (current === "--base-url") {
|
|
219
|
+
args.baseUrl = next;
|
|
220
|
+
index += 1;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (current === "--hub-project") {
|
|
224
|
+
args.hubProject = next;
|
|
225
|
+
index += 1;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (current === "--config") {
|
|
229
|
+
args.configPath = next ? path.resolve(PROJECT_ROOT, next) : DEFAULT_CONFIG_PATH;
|
|
230
|
+
index += 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (current === "--admin-email") {
|
|
234
|
+
args.adminEmail = next;
|
|
235
|
+
index += 1;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (current === "--admin-password") {
|
|
239
|
+
args.adminPassword = next;
|
|
240
|
+
index += 1;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (current === "--admin-cookie") {
|
|
244
|
+
args.adminCookie = next;
|
|
245
|
+
index += 1;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (current === "--admin-secret") {
|
|
249
|
+
args.adminSecret = next;
|
|
250
|
+
index += 1;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (current === "--agent-api-key") {
|
|
254
|
+
args.agentApiKey = next;
|
|
255
|
+
index += 1;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (current === "--skills") {
|
|
259
|
+
args.skills = next;
|
|
260
|
+
index += 1;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (current === "--rules") {
|
|
264
|
+
args.rules = next;
|
|
265
|
+
index += 1;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (current === "--roles") {
|
|
269
|
+
args.roles = next;
|
|
270
|
+
index += 1;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (current === "--scenarios") {
|
|
274
|
+
args.scenarios = next;
|
|
275
|
+
index += 1;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (current === "--from-scenarios") {
|
|
279
|
+
args.fromScenarios = next;
|
|
280
|
+
index += 1;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
throw new Error(`unknown argument: ${current}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return args;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function printHelp() {
|
|
290
|
+
console.log(`
|
|
291
|
+
Usage:
|
|
292
|
+
node ./scripts/hub-sync-assets.js [options]
|
|
293
|
+
|
|
294
|
+
Options:
|
|
295
|
+
--dry-run Only print planned operations
|
|
296
|
+
--base-url <url> Hub base url, default http://localhost:3000
|
|
297
|
+
--hub-project <path> Hub project path, default ../skill-q-platform
|
|
298
|
+
--config <path> Private config path, default scripts/hub-sync-assets.config.json
|
|
299
|
+
--admin-email <email> Hub admin email for login
|
|
300
|
+
--admin-password <pwd> Hub admin password for login
|
|
301
|
+
--admin-cookie <cookie> Existing admin_session cookie
|
|
302
|
+
--admin-secret <secret> HUB_ADMIN_SECRET, used for skill/rule author bypass and admin API bypass
|
|
303
|
+
--agent-api-key <key> Agent API key, required when Hub enforces upload login for skill/rule version updates
|
|
304
|
+
--skills <all|csv|none> Sync selected skills
|
|
305
|
+
--rules <all|csv|none> Sync selected rules
|
|
306
|
+
--roles <all|csv|none> Sync selected roles
|
|
307
|
+
--scenarios <all|csv|none> Sync selected scenarios
|
|
308
|
+
--from-scenarios <all|csv|none>
|
|
309
|
+
Expand related roles and skills from scenario packages
|
|
310
|
+
--help Show help
|
|
311
|
+
|
|
312
|
+
Examples:
|
|
313
|
+
node ./scripts/hub-sync-assets.js --dry-run
|
|
314
|
+
node ./scripts/hub-sync-assets.js --skills create-api,create-route --rules none
|
|
315
|
+
node ./scripts/hub-sync-assets.js --from-scenarios change-to-release --rules none --scenarios none
|
|
316
|
+
node ./scripts/hub-sync-assets.js --config scripts/hub-sync-assets.config.json
|
|
317
|
+
|
|
318
|
+
Notes:
|
|
319
|
+
- If you pass http://localhost:3000/admin, the script will normalize it to http://localhost:3000.
|
|
320
|
+
- skill/rule can run without admin login when your local Hub allows direct upload APIs.
|
|
321
|
+
- Existing skill/rule resources need version publishing for file changes. If Hub requires upload login,
|
|
322
|
+
you must provide --agent-api-key or config hub.agentApiKey for those version updates.
|
|
323
|
+
- existing skill/rule updates usually still need --admin-secret or --agent-api-key.
|
|
324
|
+
- if your local Hub lets requireAdminJson accept HUB_ADMIN_SECRET, roles/scenarios can also use --admin-secret.
|
|
325
|
+
- otherwise roles/scenarios still require the admin session.
|
|
326
|
+
- A config example is available at ${path.relative(PROJECT_ROOT, DEFAULT_CONFIG_EXAMPLE_PATH)}.
|
|
327
|
+
`.trim());
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function loadConfig(configPath) {
|
|
331
|
+
if (!configPath || !fs.existsSync(configPath)) {
|
|
332
|
+
return {};
|
|
333
|
+
}
|
|
334
|
+
return readJson(configPath);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function resolveRuntimeOptions(cliOptions, config) {
|
|
338
|
+
const hubProjectDir = path.resolve(
|
|
339
|
+
PROJECT_ROOT,
|
|
340
|
+
cliOptions.hubProject || config?.hub?.projectDir || DEFAULT_HUB_PROJECT,
|
|
341
|
+
);
|
|
342
|
+
const envFileValues = loadEnvOverrides(hubProjectDir);
|
|
343
|
+
|
|
344
|
+
const baseUrl = normalizeBaseUrl(
|
|
345
|
+
cliOptions.baseUrl ||
|
|
346
|
+
process.env.HUB_SYNC_BASE_URL ||
|
|
347
|
+
config?.hub?.baseUrl ||
|
|
348
|
+
DEFAULT_BASE_URL,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const adminSecret =
|
|
352
|
+
cliOptions.adminSecret ||
|
|
353
|
+
process.env.HUB_ADMIN_SECRET ||
|
|
354
|
+
process.env.HUB_SYNC_ADMIN_SECRET ||
|
|
355
|
+
config?.hub?.adminSecret ||
|
|
356
|
+
envFileValues.HUB_ADMIN_SECRET ||
|
|
357
|
+
"";
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
baseUrl,
|
|
361
|
+
hubProjectDir,
|
|
362
|
+
adminEmail:
|
|
363
|
+
cliOptions.adminEmail ||
|
|
364
|
+
process.env.HUB_SYNC_ADMIN_EMAIL ||
|
|
365
|
+
config?.hub?.adminEmail ||
|
|
366
|
+
"",
|
|
367
|
+
adminPassword:
|
|
368
|
+
cliOptions.adminPassword ||
|
|
369
|
+
process.env.HUB_SYNC_ADMIN_PASSWORD ||
|
|
370
|
+
config?.hub?.adminPassword ||
|
|
371
|
+
"",
|
|
372
|
+
adminCookie:
|
|
373
|
+
cliOptions.adminCookie ||
|
|
374
|
+
process.env.HUB_SYNC_ADMIN_COOKIE ||
|
|
375
|
+
config?.hub?.adminSessionCookie ||
|
|
376
|
+
"",
|
|
377
|
+
adminSecret,
|
|
378
|
+
agentApiKey:
|
|
379
|
+
cliOptions.agentApiKey ||
|
|
380
|
+
process.env.HUB_SYNC_AGENT_API_KEY ||
|
|
381
|
+
config?.hub?.agentApiKey ||
|
|
382
|
+
"",
|
|
383
|
+
hasAdminAuthInput: Boolean(
|
|
384
|
+
cliOptions.adminCookie ||
|
|
385
|
+
process.env.HUB_SYNC_ADMIN_COOKIE ||
|
|
386
|
+
config?.hub?.adminSessionCookie ||
|
|
387
|
+
adminSecret ||
|
|
388
|
+
((cliOptions.adminEmail ||
|
|
389
|
+
process.env.HUB_SYNC_ADMIN_EMAIL ||
|
|
390
|
+
config?.hub?.adminEmail) &&
|
|
391
|
+
(cliOptions.adminPassword ||
|
|
392
|
+
process.env.HUB_SYNC_ADMIN_PASSWORD ||
|
|
393
|
+
config?.hub?.adminPassword)),
|
|
394
|
+
),
|
|
395
|
+
dryRun: Boolean(cliOptions.dryRun),
|
|
396
|
+
config,
|
|
397
|
+
skillSelectionSpecified: typeof cliOptions.skills !== "undefined",
|
|
398
|
+
roleSelectionSpecified: typeof cliOptions.roles !== "undefined",
|
|
399
|
+
skillSelection: normalizeSelection(cliOptions.skills),
|
|
400
|
+
ruleSelection: normalizeSelection(cliOptions.rules),
|
|
401
|
+
roleSelection: normalizeSelection(cliOptions.roles),
|
|
402
|
+
scenarioSelection: normalizeSelection(cliOptions.scenarios),
|
|
403
|
+
fromScenarioSelection: normalizeSelection(
|
|
404
|
+
typeof cliOptions.fromScenarios === "undefined" ? "none" : cliOptions.fromScenarios,
|
|
405
|
+
),
|
|
406
|
+
skipSkills: isSelectionNone(normalizeSelection(cliOptions.skills)),
|
|
407
|
+
skipRules: isSelectionNone(normalizeSelection(cliOptions.rules)),
|
|
408
|
+
skipRoles: isSelectionNone(normalizeSelection(cliOptions.roles)),
|
|
409
|
+
skipScenarios: isSelectionNone(normalizeSelection(cliOptions.scenarios)),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function normalizeSelection(value) {
|
|
414
|
+
if (!value) return { mode: "all", values: new Set() };
|
|
415
|
+
const trimmed = String(value).trim();
|
|
416
|
+
if (!trimmed || trimmed === "all") {
|
|
417
|
+
return { mode: "all", values: new Set() };
|
|
418
|
+
}
|
|
419
|
+
if (trimmed === "none") {
|
|
420
|
+
return { mode: "none", values: new Set() };
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
mode: "pick",
|
|
424
|
+
values: new Set(
|
|
425
|
+
trimmed
|
|
426
|
+
.split(",")
|
|
427
|
+
.map((item) => item.trim())
|
|
428
|
+
.filter(Boolean),
|
|
429
|
+
),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function isSelectionNone(selection) {
|
|
434
|
+
return selection.mode === "none";
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function loadEnvOverrides(hubProjectDir) {
|
|
438
|
+
const files = [".env.local", ".env.development.local", ".env", ".env.development"];
|
|
439
|
+
const merged = {};
|
|
440
|
+
for (const filename of files) {
|
|
441
|
+
const filePath = path.join(hubProjectDir, filename);
|
|
442
|
+
if (!fs.existsSync(filePath)) continue;
|
|
443
|
+
Object.assign(merged, parseEnvLikeFile(fs.readFileSync(filePath, "utf8")));
|
|
444
|
+
}
|
|
445
|
+
return merged;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function parseEnvLikeFile(content) {
|
|
449
|
+
const output = {};
|
|
450
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
451
|
+
const line = rawLine.trim();
|
|
452
|
+
if (!line || line.startsWith("#")) continue;
|
|
453
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
454
|
+
if (!match) continue;
|
|
455
|
+
let value = match[2].trim();
|
|
456
|
+
if (
|
|
457
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
458
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
459
|
+
) {
|
|
460
|
+
value = value.slice(1, -1);
|
|
461
|
+
}
|
|
462
|
+
output[match[1]] = value;
|
|
463
|
+
}
|
|
464
|
+
return output;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function normalizeBaseUrl(input) {
|
|
468
|
+
const url = new URL(String(input));
|
|
469
|
+
url.pathname = "";
|
|
470
|
+
url.search = "";
|
|
471
|
+
url.hash = "";
|
|
472
|
+
return url.toString().replace(/\/$/, "");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
class HubClient {
|
|
476
|
+
constructor(options) {
|
|
477
|
+
this.baseUrl = options.baseUrl;
|
|
478
|
+
this.adminEmail = options.adminEmail;
|
|
479
|
+
this.adminPassword = options.adminPassword;
|
|
480
|
+
this.cookie = options.adminCookie || "";
|
|
481
|
+
this.adminSecret = options.adminSecret || "";
|
|
482
|
+
this.agentApiKey = options.agentApiKey || "";
|
|
483
|
+
this.dryRun = options.dryRun;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
hasAdminSession() {
|
|
487
|
+
return Boolean(this.cookie);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
hasAdminAccess() {
|
|
491
|
+
return Boolean(this.cookie || this.adminSecret);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async ensureAdminSession() {
|
|
495
|
+
if (!this.cookie) {
|
|
496
|
+
if (!this.adminEmail || !this.adminPassword) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
"missing admin auth: provide --admin-email/--admin-password, --admin-cookie, or hub config",
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
await this.login();
|
|
502
|
+
}
|
|
503
|
+
await this.getJson("/api/admin/auth/me");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async login() {
|
|
507
|
+
const response = await fetch(`${this.baseUrl}/api/admin/auth/login`, {
|
|
508
|
+
method: "POST",
|
|
509
|
+
headers: {
|
|
510
|
+
"content-type": "application/json",
|
|
511
|
+
accept: "application/json",
|
|
512
|
+
},
|
|
513
|
+
body: JSON.stringify({
|
|
514
|
+
email: this.adminEmail,
|
|
515
|
+
password: this.adminPassword,
|
|
516
|
+
}),
|
|
517
|
+
});
|
|
518
|
+
if (!response.ok) {
|
|
519
|
+
throw new Error(`admin login failed: ${await readErrorText(response)}`);
|
|
520
|
+
}
|
|
521
|
+
const cookies = getResponseCookies(response);
|
|
522
|
+
const adminSession = cookies.find((cookie) => cookie.startsWith("admin_session="));
|
|
523
|
+
if (!adminSession) {
|
|
524
|
+
throw new Error("admin login succeeded but no admin_session cookie was returned");
|
|
525
|
+
}
|
|
526
|
+
this.cookie = adminSession;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async getJson(pathname) {
|
|
530
|
+
return this.requestJson(pathname, { method: "GET" });
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async postJson(pathname, body) {
|
|
534
|
+
return this.requestJson(pathname, {
|
|
535
|
+
method: "POST",
|
|
536
|
+
headers: {
|
|
537
|
+
"content-type": "application/json",
|
|
538
|
+
},
|
|
539
|
+
body: JSON.stringify(body),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async postForm(pathname, formData) {
|
|
544
|
+
const response = await fetch(`${this.baseUrl}${pathname}`, {
|
|
545
|
+
method: "POST",
|
|
546
|
+
headers: this.buildHeaders({}),
|
|
547
|
+
body: formData,
|
|
548
|
+
});
|
|
549
|
+
if (!response.ok) {
|
|
550
|
+
throw new Error(await readErrorText(response));
|
|
551
|
+
}
|
|
552
|
+
return unwrapApiResponse(await response.json());
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async requestJson(pathname, init) {
|
|
556
|
+
const response = await fetch(`${this.baseUrl}${pathname}`, {
|
|
557
|
+
...init,
|
|
558
|
+
headers: this.buildHeaders(init.headers || {}),
|
|
559
|
+
});
|
|
560
|
+
if (!response.ok) {
|
|
561
|
+
throw new Error(await readErrorText(response));
|
|
562
|
+
}
|
|
563
|
+
return unwrapApiResponse(await response.json());
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
buildHeaders(headers) {
|
|
567
|
+
const next = {
|
|
568
|
+
accept: "application/json",
|
|
569
|
+
...headers,
|
|
570
|
+
};
|
|
571
|
+
if (this.cookie) {
|
|
572
|
+
next.cookie = this.cookie;
|
|
573
|
+
}
|
|
574
|
+
if (this.adminSecret) {
|
|
575
|
+
next["x-hub-admin-secret"] = this.adminSecret;
|
|
576
|
+
}
|
|
577
|
+
if (this.agentApiKey) {
|
|
578
|
+
next.authorization = `Bearer ${this.agentApiKey}`;
|
|
579
|
+
}
|
|
580
|
+
return next;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function loadCategories(client) {
|
|
585
|
+
const [skill, rule] = await Promise.all([
|
|
586
|
+
client.getJson("/api/admin/categories?resourceType=skill"),
|
|
587
|
+
client.getJson("/api/admin/categories?resourceType=rule"),
|
|
588
|
+
]);
|
|
589
|
+
return {
|
|
590
|
+
skill: skill.items || [],
|
|
591
|
+
rule: rule.items || [],
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function loadBrowseItems(client, resourceType) {
|
|
596
|
+
const pageSize = 100;
|
|
597
|
+
let page = 1;
|
|
598
|
+
let total = 0;
|
|
599
|
+
const items = [];
|
|
600
|
+
do {
|
|
601
|
+
const response = await client.getJson(
|
|
602
|
+
`/api/admin/resources/browse?resourceType=${resourceType}&page=${page}&pageSize=${pageSize}`,
|
|
603
|
+
);
|
|
604
|
+
total = Number(response.total || 0);
|
|
605
|
+
items.push(...(response.items || []));
|
|
606
|
+
page += 1;
|
|
607
|
+
} while (items.length < total);
|
|
608
|
+
return { items };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function syncRules(context) {
|
|
612
|
+
const ids = selectResourceIds(context.localRules, context.resolved.ruleSelection);
|
|
613
|
+
for (const ruleId of ids) {
|
|
614
|
+
const local = context.localRules[ruleId];
|
|
615
|
+
const desiredAssets = buildRuleAssets(ruleId, local, context);
|
|
616
|
+
if (desiredAssets.length === 0) {
|
|
617
|
+
context.summary.rule.skipped += 1;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
for (const desired of desiredAssets) {
|
|
621
|
+
if (!desired) {
|
|
622
|
+
context.summary.rule.skipped += 1;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let existing = context.hubState.rulesBySlug[desired.slug];
|
|
627
|
+
if (!existing) {
|
|
628
|
+
const publicExisting = await fetchPublicResource(context.client, "rule", desired.slug);
|
|
629
|
+
if (publicExisting) {
|
|
630
|
+
existing = publicExisting;
|
|
631
|
+
context.hubState.rulesBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
632
|
+
"rule",
|
|
633
|
+
publicExisting,
|
|
634
|
+
desired,
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (!existing) {
|
|
639
|
+
if (!desired.categorySlug) {
|
|
640
|
+
warn(`rule ${ruleId}: missing categorySlug, skip create`);
|
|
641
|
+
context.summary.rule.skipped += 1;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (context.resolved.dryRun) {
|
|
645
|
+
info(`rule ${desired.slug}: create`);
|
|
646
|
+
context.summary.rule.created += 1;
|
|
647
|
+
context.hubState.rulesBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
648
|
+
"rule",
|
|
649
|
+
null,
|
|
650
|
+
desired,
|
|
651
|
+
);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const createResult = await createSkillRuleOrNull({
|
|
656
|
+
type: "rule",
|
|
657
|
+
desired,
|
|
658
|
+
client: context.client,
|
|
659
|
+
});
|
|
660
|
+
if (createResult?.created) {
|
|
661
|
+
info(`rule ${desired.slug}: created`);
|
|
662
|
+
context.summary.rule.created += 1;
|
|
663
|
+
context.hubState.rulesBySlug[desired.slug] = {
|
|
664
|
+
id: createResult.resource?.id || desired.slug,
|
|
665
|
+
slug: desired.slug,
|
|
666
|
+
name: desired.name,
|
|
667
|
+
registryId: desired.registryId,
|
|
668
|
+
manifestId: desired.manifestId,
|
|
669
|
+
tags: desired.tags,
|
|
670
|
+
supportedProfiles: desired.supportedProfiles,
|
|
671
|
+
categoryName: createResult.resource?.category?.name || desired.categorySlug,
|
|
672
|
+
};
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const conflictExisting = await fetchConflictResource({
|
|
677
|
+
type: "rule",
|
|
678
|
+
desiredSlug: desired.slug,
|
|
679
|
+
conflictSlugs: createResult?.conflictSlugs || [],
|
|
680
|
+
client: context.client,
|
|
681
|
+
});
|
|
682
|
+
if (!conflictExisting) {
|
|
683
|
+
throw new Error(`rule ${ruleId}: resource create conflicted but public resource was not found`);
|
|
684
|
+
}
|
|
685
|
+
existing = conflictExisting;
|
|
686
|
+
context.hubState.rulesBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
687
|
+
"rule",
|
|
688
|
+
conflictExisting,
|
|
689
|
+
desired,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const existingDetails = existing
|
|
694
|
+
? await fetchPublicResource(context.client, "rule", existing.slug || desired.slug) || existing
|
|
695
|
+
: null;
|
|
696
|
+
const metadataPatch = existingDetails
|
|
697
|
+
? buildSkillRuleMetadataPatch("rule", desired, existingDetails)
|
|
698
|
+
: buildSkillRuleFullPatch(desired);
|
|
699
|
+
if (metadataPatch) {
|
|
700
|
+
if (context.resolved.dryRun) {
|
|
701
|
+
info(`rule ${desired.slug}: update metadata`);
|
|
702
|
+
} else {
|
|
703
|
+
await context.client.postJson(
|
|
704
|
+
`/api/rules/${encodeURIComponent(existingDetails?.slug || existing?.slug || desired.slug)}`,
|
|
705
|
+
metadataPatch,
|
|
706
|
+
);
|
|
707
|
+
info(`rule ${desired.slug}: metadata updated`);
|
|
708
|
+
}
|
|
709
|
+
context.summary.rule.updated += 1;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const versionChanged = await ensureSkillRuleVersion({
|
|
713
|
+
type: "rule",
|
|
714
|
+
desired,
|
|
715
|
+
slug: desired.slug,
|
|
716
|
+
client: context.client,
|
|
717
|
+
dryRun: context.resolved.dryRun,
|
|
718
|
+
});
|
|
719
|
+
if (versionChanged === "versioned") {
|
|
720
|
+
context.summary.rule.versioned += 1;
|
|
721
|
+
} else if (!metadataPatch) {
|
|
722
|
+
context.summary.rule.skipped += 1;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
context.hubState.rulesBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
726
|
+
"rule",
|
|
727
|
+
existing,
|
|
728
|
+
desired,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function syncSkills(context) {
|
|
735
|
+
const ids = selectResourceIds(context.localSkills, context.resolved.skillSelection);
|
|
736
|
+
for (const skillId of ids) {
|
|
737
|
+
const local = context.localSkills[skillId];
|
|
738
|
+
const desiredAssets = buildSkillAssets(skillId, local, context);
|
|
739
|
+
if (desiredAssets.length === 0) {
|
|
740
|
+
context.summary.skill.skipped += 1;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
for (const desired of desiredAssets) {
|
|
744
|
+
if (!desired) {
|
|
745
|
+
context.summary.skill.skipped += 1;
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
let existing = context.hubState.skillsBySlug[desired.slug];
|
|
750
|
+
if (!existing) {
|
|
751
|
+
const publicExisting = await fetchPublicResource(context.client, "skill", desired.slug);
|
|
752
|
+
if (publicExisting) {
|
|
753
|
+
existing = publicExisting;
|
|
754
|
+
context.hubState.skillsBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
755
|
+
"skill",
|
|
756
|
+
publicExisting,
|
|
757
|
+
desired,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (!existing) {
|
|
762
|
+
if (!desired.categorySlug) {
|
|
763
|
+
warn(`skill ${skillId}: missing categorySlug, skip create`);
|
|
764
|
+
context.summary.skill.skipped += 1;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
if (context.resolved.dryRun) {
|
|
768
|
+
info(`skill ${desired.slug}: create`);
|
|
769
|
+
context.summary.skill.created += 1;
|
|
770
|
+
context.hubState.skillsBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
771
|
+
"skill",
|
|
772
|
+
null,
|
|
773
|
+
desired,
|
|
774
|
+
);
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const createResult = await createSkillRuleOrNull({
|
|
779
|
+
type: "skill",
|
|
780
|
+
desired,
|
|
781
|
+
client: context.client,
|
|
782
|
+
});
|
|
783
|
+
if (createResult?.created) {
|
|
784
|
+
info(`skill ${desired.slug}: created`);
|
|
785
|
+
context.summary.skill.created += 1;
|
|
786
|
+
context.hubState.skillsBySlug[desired.slug] = {
|
|
787
|
+
id: createResult.resource?.id || desired.slug,
|
|
788
|
+
slug: desired.slug,
|
|
789
|
+
name: desired.name,
|
|
790
|
+
registryId: desired.registryId,
|
|
791
|
+
manifestId: desired.manifestId,
|
|
792
|
+
tags: desired.tags,
|
|
793
|
+
supportedProfiles: desired.supportedProfiles,
|
|
794
|
+
categoryName: createResult.resource?.category?.name || desired.categorySlug,
|
|
795
|
+
};
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const conflictExisting = await fetchConflictResource({
|
|
800
|
+
type: "skill",
|
|
801
|
+
desiredSlug: desired.slug,
|
|
802
|
+
conflictSlugs: createResult?.conflictSlugs || [],
|
|
803
|
+
client: context.client,
|
|
804
|
+
});
|
|
805
|
+
if (!conflictExisting) {
|
|
806
|
+
throw new Error(`skill ${skillId}: resource create conflicted but public resource was not found`);
|
|
807
|
+
}
|
|
808
|
+
existing = conflictExisting;
|
|
809
|
+
context.hubState.skillsBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
810
|
+
"skill",
|
|
811
|
+
conflictExisting,
|
|
812
|
+
desired,
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const existingDetails = existing
|
|
817
|
+
? await fetchPublicResource(context.client, "skill", existing.slug || desired.slug) || existing
|
|
818
|
+
: null;
|
|
819
|
+
const metadataPatch = existingDetails
|
|
820
|
+
? buildSkillRuleMetadataPatch("skill", desired, existingDetails)
|
|
821
|
+
: buildSkillRuleFullPatch(desired);
|
|
822
|
+
if (metadataPatch) {
|
|
823
|
+
if (context.resolved.dryRun) {
|
|
824
|
+
info(`skill ${desired.slug}: update metadata`);
|
|
825
|
+
} else {
|
|
826
|
+
await context.client.postJson(
|
|
827
|
+
`/api/skills/${encodeURIComponent(existingDetails?.slug || existing?.slug || desired.slug)}`,
|
|
828
|
+
metadataPatch,
|
|
829
|
+
);
|
|
830
|
+
info(`skill ${desired.slug}: metadata updated`);
|
|
831
|
+
}
|
|
832
|
+
context.summary.skill.updated += 1;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const versionChanged = await ensureSkillRuleVersion({
|
|
836
|
+
type: "skill",
|
|
837
|
+
desired,
|
|
838
|
+
slug: desired.slug,
|
|
839
|
+
client: context.client,
|
|
840
|
+
dryRun: context.resolved.dryRun,
|
|
841
|
+
});
|
|
842
|
+
if (versionChanged === "versioned") {
|
|
843
|
+
context.summary.skill.versioned += 1;
|
|
844
|
+
} else if (!metadataPatch) {
|
|
845
|
+
context.summary.skill.skipped += 1;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
context.hubState.skillsBySlug[desired.slug] = buildPreviewSkillRuleState(
|
|
849
|
+
"skill",
|
|
850
|
+
existing,
|
|
851
|
+
desired,
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
async function syncRoles(context) {
|
|
858
|
+
const ids = selectResourceIds(context.localRoles, context.resolved.roleSelection);
|
|
859
|
+
for (const roleId of ids) {
|
|
860
|
+
const local = context.localRoles[roleId];
|
|
861
|
+
const desired = await buildRoleAsset(roleId, local, context);
|
|
862
|
+
if (!desired) {
|
|
863
|
+
context.summary.role.skipped += 1;
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const existing = context.hubState.rolesBySlug[desired.slug];
|
|
868
|
+
if (!existing) {
|
|
869
|
+
if (context.resolved.dryRun) {
|
|
870
|
+
info(`role ${roleId}: create`);
|
|
871
|
+
context.summary.role.created += 1;
|
|
872
|
+
context.hubState.rolesBySlug[desired.slug] = buildPreviewRoleState(null, desired);
|
|
873
|
+
} else {
|
|
874
|
+
await context.client.postJson("/api/admin/roles", desired.payload);
|
|
875
|
+
info(`role ${roleId}: created`);
|
|
876
|
+
context.summary.role.created += 1;
|
|
877
|
+
const refreshed = await context.client.getJson("/api/admin/roles");
|
|
878
|
+
context.hubState.rolesBySlug = indexBy(refreshed.items || [], "slug");
|
|
879
|
+
}
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const existingPayload = normalizeRoleResponseToPayload(existing);
|
|
884
|
+
if (deepEqual(existingPayload, desired.payload)) {
|
|
885
|
+
context.summary.role.skipped += 1;
|
|
886
|
+
info(`role ${roleId}: no changes`);
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
const needsVersion = await roleVersionWouldChange({
|
|
890
|
+
client: context.client,
|
|
891
|
+
slug: existing.slug,
|
|
892
|
+
desiredVersionFiles: desired.versionFiles,
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
if (context.resolved.dryRun) {
|
|
896
|
+
info(`role ${roleId}: update${needsVersion ? " + version" : ""}`);
|
|
897
|
+
context.summary.role.updated += 1;
|
|
898
|
+
if (needsVersion) {
|
|
899
|
+
context.summary.role.versioned += 1;
|
|
900
|
+
}
|
|
901
|
+
context.hubState.rolesBySlug[desired.slug] = buildPreviewRoleState(existing, desired);
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
await context.client.postJson("/api/admin/roles/update", {
|
|
906
|
+
id: existing.id,
|
|
907
|
+
...desired.payload,
|
|
908
|
+
});
|
|
909
|
+
await ensureRoleVersion({
|
|
910
|
+
client: context.client,
|
|
911
|
+
slug: existing.slug,
|
|
912
|
+
desiredVersionFiles: desired.versionFiles,
|
|
913
|
+
dryRun: false,
|
|
914
|
+
});
|
|
915
|
+
info(`role ${roleId}: updated`);
|
|
916
|
+
context.summary.role.updated += 1;
|
|
917
|
+
if (needsVersion) {
|
|
918
|
+
context.summary.role.versioned += 1;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const refreshed = await context.client.getJson("/api/admin/roles");
|
|
922
|
+
context.hubState.rolesBySlug = indexBy(refreshed.items || [], "slug");
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async function syncScenarios(context) {
|
|
927
|
+
const ids = selectResourceIds(context.localScenarios, context.resolved.scenarioSelection);
|
|
928
|
+
for (const scenarioId of ids) {
|
|
929
|
+
const local = context.localScenarios[scenarioId];
|
|
930
|
+
const desired = buildScenarioAsset(scenarioId, local, context);
|
|
931
|
+
if (!desired) {
|
|
932
|
+
context.summary.scenario.skipped += 1;
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const existing = context.hubState.scenariosBySlug[desired.slug];
|
|
937
|
+
if (!existing) {
|
|
938
|
+
if (context.resolved.dryRun) {
|
|
939
|
+
info(`scenario ${scenarioId}: create`);
|
|
940
|
+
context.summary.scenario.created += 1;
|
|
941
|
+
context.hubState.scenariosBySlug[desired.slug] = buildPreviewScenarioState(null, desired);
|
|
942
|
+
} else {
|
|
943
|
+
await context.client.postJson("/api/admin/scenarios", desired.payload);
|
|
944
|
+
info(`scenario ${scenarioId}: created`);
|
|
945
|
+
context.summary.scenario.created += 1;
|
|
946
|
+
const refreshed = await context.client.getJson("/api/admin/scenarios");
|
|
947
|
+
context.hubState.scenariosBySlug = indexBy(refreshed.items || [], "slug");
|
|
948
|
+
}
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const existingPayload = normalizeScenarioResponseToPayload(existing);
|
|
953
|
+
if (deepEqual(existingPayload, desired.payload)) {
|
|
954
|
+
context.summary.scenario.skipped += 1;
|
|
955
|
+
info(`scenario ${scenarioId}: no changes`);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (context.resolved.dryRun) {
|
|
960
|
+
info(`scenario ${scenarioId}: update`);
|
|
961
|
+
context.summary.scenario.updated += 1;
|
|
962
|
+
context.hubState.scenariosBySlug[desired.slug] = buildPreviewScenarioState(existing, desired);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
await context.client.postJson("/api/admin/scenarios/update", {
|
|
967
|
+
id: existing.id,
|
|
968
|
+
...desired.payload,
|
|
969
|
+
});
|
|
970
|
+
info(`scenario ${scenarioId}: updated`);
|
|
971
|
+
context.summary.scenario.updated += 1;
|
|
972
|
+
|
|
973
|
+
const refreshed = await context.client.getJson("/api/admin/scenarios");
|
|
974
|
+
context.hubState.scenariosBySlug = indexBy(refreshed.items || [], "slug");
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function buildSkillAssets(skillId, local, context) {
|
|
979
|
+
const variants = buildProfileVariantSpecs({
|
|
980
|
+
type: "skill",
|
|
981
|
+
resourceId: skillId,
|
|
982
|
+
local,
|
|
983
|
+
hubState: context.hubState,
|
|
984
|
+
});
|
|
985
|
+
if (variants.length === 0) {
|
|
986
|
+
return [buildSkillAsset(skillId, local, context, null)].filter(Boolean);
|
|
987
|
+
}
|
|
988
|
+
return variants
|
|
989
|
+
.map((variant) => buildSkillAsset(skillId, local, context, variant))
|
|
990
|
+
.filter(Boolean);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function buildRuleAssets(ruleId, local, context) {
|
|
994
|
+
const variants = buildProfileVariantSpecs({
|
|
995
|
+
type: "rule",
|
|
996
|
+
resourceId: ruleId,
|
|
997
|
+
local,
|
|
998
|
+
hubState: context.hubState,
|
|
999
|
+
});
|
|
1000
|
+
if (variants.length === 0) {
|
|
1001
|
+
return [buildRuleAsset(ruleId, local, context, null)].filter(Boolean);
|
|
1002
|
+
}
|
|
1003
|
+
return variants
|
|
1004
|
+
.map((variant) => buildRuleAsset(ruleId, local, context, variant))
|
|
1005
|
+
.filter(Boolean);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function buildSkillAsset(skillId, local, context, variant) {
|
|
1009
|
+
const override = resolveResourceOverride({
|
|
1010
|
+
config: context.config,
|
|
1011
|
+
type: "skills",
|
|
1012
|
+
resourceId: skillId,
|
|
1013
|
+
variantSlug: variant?.slug,
|
|
1014
|
+
});
|
|
1015
|
+
const files = collectSkillFiles(local, skillId, variant?.sourcePaths);
|
|
1016
|
+
if (files.length === 0) {
|
|
1017
|
+
warn(`skill ${skillId}: no files collected`);
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const primaryFile = pickPrimaryTextFile(files, "SKILL.md") || files[0];
|
|
1022
|
+
const parsed = parseFrontmatterFile(primaryFile.content, "skill");
|
|
1023
|
+
const name = override.name || variant?.existing?.name || parsed.name || skillId;
|
|
1024
|
+
const description =
|
|
1025
|
+
override.description ||
|
|
1026
|
+
parsed.description ||
|
|
1027
|
+
variant?.existing?.description ||
|
|
1028
|
+
`Sync from local skill ${skillId}`;
|
|
1029
|
+
const supportedProfiles =
|
|
1030
|
+
override.supportedProfiles ||
|
|
1031
|
+
variant?.supportedProfiles ||
|
|
1032
|
+
Object.keys(local.sourceByProfile || {});
|
|
1033
|
+
const domains = Array.isArray(local.domains) ? local.domains : [];
|
|
1034
|
+
const categorySlug = resolveCategorySlug({
|
|
1035
|
+
type: "skill",
|
|
1036
|
+
resourceId: skillId,
|
|
1037
|
+
override,
|
|
1038
|
+
domains,
|
|
1039
|
+
categories: context.hubState.categories.skill,
|
|
1040
|
+
config: context.config,
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
return {
|
|
1044
|
+
slug: variant?.slug || override.slug || skillId,
|
|
1045
|
+
registryId: override.registryId || skillId,
|
|
1046
|
+
manifestId: override.manifestId || override.registryId || skillId,
|
|
1047
|
+
name,
|
|
1048
|
+
description,
|
|
1049
|
+
longDescription: override.longDescription || "",
|
|
1050
|
+
author: override.author || context.config?.defaults?.author || "Hub Admin",
|
|
1051
|
+
categorySlug,
|
|
1052
|
+
tags: uniqueKeepOrder(override.tags || domains),
|
|
1053
|
+
supportedProfiles: uniqueKeepOrder(supportedProfiles),
|
|
1054
|
+
downloadPolicy: override.downloadPolicy || context.config?.defaults?.downloadPolicy || "login",
|
|
1055
|
+
files,
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function buildRuleAsset(ruleId, local, context, variant) {
|
|
1060
|
+
const override = resolveResourceOverride({
|
|
1061
|
+
config: context.config,
|
|
1062
|
+
type: "rules",
|
|
1063
|
+
resourceId: ruleId,
|
|
1064
|
+
variantSlug: variant?.slug,
|
|
1065
|
+
});
|
|
1066
|
+
const files = collectRuleFiles(local, variant?.sourcePaths);
|
|
1067
|
+
if (files.length === 0) {
|
|
1068
|
+
warn(`rule ${ruleId}: no files collected`);
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const primaryFile = files[0];
|
|
1073
|
+
const parsed = parseFrontmatterFile(primaryFile.content, "rule");
|
|
1074
|
+
const name = override.name || variant?.existing?.name || parsed.name || ruleId;
|
|
1075
|
+
const description =
|
|
1076
|
+
override.description ||
|
|
1077
|
+
parsed.description ||
|
|
1078
|
+
variant?.existing?.description ||
|
|
1079
|
+
`Sync from local rule ${ruleId}`;
|
|
1080
|
+
const supportedProfiles =
|
|
1081
|
+
override.supportedProfiles ||
|
|
1082
|
+
variant?.supportedProfiles ||
|
|
1083
|
+
Object.keys(local.sourceByProfile || {});
|
|
1084
|
+
const domains = Array.isArray(local.domains) ? local.domains : [];
|
|
1085
|
+
const categorySlug = resolveCategorySlug({
|
|
1086
|
+
type: "rule",
|
|
1087
|
+
resourceId: ruleId,
|
|
1088
|
+
override,
|
|
1089
|
+
domains,
|
|
1090
|
+
categories: context.hubState.categories.rule,
|
|
1091
|
+
config: context.config,
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
return {
|
|
1095
|
+
slug: variant?.slug || override.slug || ruleId,
|
|
1096
|
+
registryId: override.registryId || ruleId,
|
|
1097
|
+
manifestId: override.manifestId || override.registryId || ruleId,
|
|
1098
|
+
name,
|
|
1099
|
+
description,
|
|
1100
|
+
longDescription: override.longDescription || "",
|
|
1101
|
+
author: override.author || context.config?.defaults?.author || "Hub Admin",
|
|
1102
|
+
categorySlug,
|
|
1103
|
+
tags: uniqueKeepOrder(override.tags || domains),
|
|
1104
|
+
supportedProfiles: uniqueKeepOrder(supportedProfiles),
|
|
1105
|
+
downloadPolicy: override.downloadPolicy || context.config?.defaults?.downloadPolicy || "login",
|
|
1106
|
+
files,
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
async function buildRoleAsset(roleId, local, context) {
|
|
1111
|
+
const override = resolveResourceOverride({
|
|
1112
|
+
config: context.config,
|
|
1113
|
+
type: "roles",
|
|
1114
|
+
resourceId: roleId,
|
|
1115
|
+
});
|
|
1116
|
+
const sourcePath = path.resolve(PROJECT_ROOT, local.source);
|
|
1117
|
+
if (!fs.existsSync(sourcePath)) {
|
|
1118
|
+
warn(`role ${roleId}: source not found ${local.source}`);
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const uploadParsed = await parseRoleWithHub(context.client, sourcePath);
|
|
1123
|
+
const registrySkillSlugs = uniqueKeepOrder([
|
|
1124
|
+
...(Array.isArray(local.skill_priority) ? local.skill_priority : []),
|
|
1125
|
+
...(Array.isArray(local.micro_skill_allowlist) ? local.micro_skill_allowlist : []),
|
|
1126
|
+
...(Array.isArray(uploadParsed.roleData.preferredSkills) ? uploadParsed.roleData.preferredSkills : []),
|
|
1127
|
+
]);
|
|
1128
|
+
const registryRuleSlugs = uniqueKeepOrder(Array.isArray(local.rule_ids) ? local.rule_ids : []);
|
|
1129
|
+
const skillIds = uniqueKeepOrder(
|
|
1130
|
+
registrySkillSlugs.flatMap((slug) => resolveLinkedResourceIds("skill", slug, context.hubState)),
|
|
1131
|
+
);
|
|
1132
|
+
const ruleIds = uniqueKeepOrder(
|
|
1133
|
+
registryRuleSlugs.flatMap((slug) => resolveLinkedResourceIds("rule", slug, context.hubState)),
|
|
1134
|
+
);
|
|
1135
|
+
const domainIds = resolveRoleDomainIds({
|
|
1136
|
+
override,
|
|
1137
|
+
local,
|
|
1138
|
+
uploadParsed,
|
|
1139
|
+
config: context.config,
|
|
1140
|
+
});
|
|
1141
|
+
const name = override.name || uploadParsed.roleData.name || local.name || roleId;
|
|
1142
|
+
const slug = override.slug || uploadParsed.roleData.slug || roleId;
|
|
1143
|
+
const payload = {
|
|
1144
|
+
name,
|
|
1145
|
+
slug,
|
|
1146
|
+
registryId: override.registryId || roleId,
|
|
1147
|
+
manifestId: override.manifestId || override.registryId || roleId,
|
|
1148
|
+
author: override.author || context.config?.defaults?.author || "Hub Admin",
|
|
1149
|
+
description: override.description || uploadParsed.roleData.description || `${name} role`,
|
|
1150
|
+
longDescription: override.longDescription || null,
|
|
1151
|
+
publishStatus: override.publishStatus || context.config?.defaults?.rolePublishStatus || "draft",
|
|
1152
|
+
roleStatus: override.roleStatus || local.status || uploadParsed.roleData.roleStatus || "draft",
|
|
1153
|
+
tags: uniqueKeepOrder(override.tags || local.domains || []),
|
|
1154
|
+
supportedProfiles: uniqueKeepOrder(override.supportedProfiles || local.profiles || []),
|
|
1155
|
+
triggers: uniqueKeepOrder(override.triggers || uploadParsed.roleData.triggers || []),
|
|
1156
|
+
preferredSkills: uniqueKeepOrder(override.preferredSkills || registrySkillSlugs),
|
|
1157
|
+
reads: uniqueKeepOrder(override.reads || uploadParsed.roleData.reads || []),
|
|
1158
|
+
writes: uniqueKeepOrder(override.writes || uploadParsed.roleData.writes || []),
|
|
1159
|
+
handoffTo: uniqueKeepOrder(override.handoffTo || uploadParsed.roleData.handoffTo || []),
|
|
1160
|
+
rolePositioning: override.rolePositioning || uploadParsed.sections.rolePositioning || null,
|
|
1161
|
+
workingPrinciples: uniqueKeepOrder(
|
|
1162
|
+
override.workingPrinciples || uploadParsed.sections.workingPrinciples || [],
|
|
1163
|
+
),
|
|
1164
|
+
requiredSteps: uniqueKeepOrder(override.requiredSteps || uploadParsed.sections.requiredSteps || []),
|
|
1165
|
+
executionContract: override.executionContract || uploadParsed.sections.executionContract || null,
|
|
1166
|
+
outputStandard: override.outputStandard || uploadParsed.sections.outputStandard || null,
|
|
1167
|
+
prohibitedActions: uniqueKeepOrder(
|
|
1168
|
+
override.prohibitedActions || uploadParsed.sections.prohibitedActions || [],
|
|
1169
|
+
),
|
|
1170
|
+
handoffNotes: override.handoffNotes || uploadParsed.sections.handoffNotes || null,
|
|
1171
|
+
skillIds,
|
|
1172
|
+
ruleIds,
|
|
1173
|
+
domainIds,
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
const versionFiles = buildRoleVersionFiles({
|
|
1177
|
+
...payload,
|
|
1178
|
+
skillSlugs: registrySkillSlugs,
|
|
1179
|
+
ruleSlugs: registryRuleSlugs,
|
|
1180
|
+
domainSlugs: uniqueKeepOrder(override.domainSlugs || local.domains || uploadParsed.roleData.domains || []),
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
return {
|
|
1184
|
+
slug,
|
|
1185
|
+
payload,
|
|
1186
|
+
versionFiles,
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function buildScenarioAsset(scenarioId, local, context) {
|
|
1191
|
+
const override = resolveResourceOverride({
|
|
1192
|
+
config: context.config,
|
|
1193
|
+
type: "scenarios",
|
|
1194
|
+
resourceId: scenarioId,
|
|
1195
|
+
});
|
|
1196
|
+
const roleItems = [];
|
|
1197
|
+
for (const roleSlug of local.roles || []) {
|
|
1198
|
+
const role = context.hubState.rolesBySlug[roleSlug];
|
|
1199
|
+
if (!role) {
|
|
1200
|
+
warn(`scenario ${scenarioId}: role ${roleSlug} not found in Hub, skip scenario`);
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
roleItems.push({
|
|
1204
|
+
id: role.id,
|
|
1205
|
+
isOptional: Array.isArray(override.optionalRoles) && override.optionalRoles.includes(roleSlug),
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const explicitSkillIds = uniqueKeepOrder(
|
|
1210
|
+
(local.skills || []).flatMap((slug) => resolveLinkedResourceIds("skill", slug, context.hubState)),
|
|
1211
|
+
);
|
|
1212
|
+
const explicitRuleIds = uniqueKeepOrder(
|
|
1213
|
+
(local.rules || []).flatMap((slug) => resolveLinkedResourceIds("rule", slug, context.hubState)),
|
|
1214
|
+
);
|
|
1215
|
+
const roleSkillIds = roleItems.flatMap((item) => {
|
|
1216
|
+
const role = findRoleById(context.hubState, item.id);
|
|
1217
|
+
return role ? (role.skillLinks || []).map((link) => link.skillId).filter(Boolean) : [];
|
|
1218
|
+
});
|
|
1219
|
+
const roleRuleIds = roleItems.flatMap((item) => {
|
|
1220
|
+
const role = findRoleById(context.hubState, item.id);
|
|
1221
|
+
return role ? (role.ruleLinks || []).map((link) => link.ruleId).filter(Boolean) : [];
|
|
1222
|
+
});
|
|
1223
|
+
const skillIds = uniqueKeepOrder([...explicitSkillIds, ...roleSkillIds]);
|
|
1224
|
+
const ruleIds = uniqueKeepOrder([...explicitRuleIds, ...roleRuleIds]);
|
|
1225
|
+
const domainIds = resolveScenarioDomainIds({
|
|
1226
|
+
scenario: local,
|
|
1227
|
+
override,
|
|
1228
|
+
roleItems,
|
|
1229
|
+
hubState: context.hubState,
|
|
1230
|
+
config: context.config,
|
|
1231
|
+
});
|
|
1232
|
+
const supportedProfiles = uniqueKeepOrder(
|
|
1233
|
+
override.supportedProfiles ||
|
|
1234
|
+
local.profiles ||
|
|
1235
|
+
context.config?.defaults?.scenarioSupportedProfiles ||
|
|
1236
|
+
["vue", "react"],
|
|
1237
|
+
);
|
|
1238
|
+
const name = override.name || scenarioId;
|
|
1239
|
+
const payload = {
|
|
1240
|
+
name,
|
|
1241
|
+
slug: override.slug || scenarioId,
|
|
1242
|
+
description:
|
|
1243
|
+
override.description ||
|
|
1244
|
+
`自动同步场景方案,入口 ${override.entryRoleSlug || local.roles?.[0] || "unknown"},角色链路:${(local.roles || []).join(" -> ")}`,
|
|
1245
|
+
longDescription: override.longDescription || null,
|
|
1246
|
+
publishStatus: override.publishStatus || context.config?.defaults?.scenarioPublishStatus || "draft",
|
|
1247
|
+
tags: uniqueKeepOrder(override.tags || local.domains || []),
|
|
1248
|
+
supportedProfiles,
|
|
1249
|
+
recommendedIdes: uniqueKeepOrder(
|
|
1250
|
+
override.recommendedIdes || context.config?.defaults?.scenarioRecommendedIdes || ["cursor"],
|
|
1251
|
+
),
|
|
1252
|
+
entryRoleId: resolveScenarioEntryRoleId(override, local, context.hubState),
|
|
1253
|
+
isFeatured:
|
|
1254
|
+
typeof override.isFeatured === "boolean"
|
|
1255
|
+
? override.isFeatured
|
|
1256
|
+
: Boolean(context.config?.defaults?.scenarioFeatured),
|
|
1257
|
+
roles: sortByKey(roleItems, (row) => `${row.id}:${row.isOptional ? 1 : 0}`),
|
|
1258
|
+
skillIds: sortStrings(skillIds),
|
|
1259
|
+
ruleIds: sortStrings(ruleIds),
|
|
1260
|
+
domainIds: sortStrings(domainIds),
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
if (!payload.entryRoleId && roleItems.length > 0) {
|
|
1264
|
+
payload.entryRoleId = roleItems[0].id;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
return {
|
|
1268
|
+
slug: payload.slug,
|
|
1269
|
+
payload,
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
async function ensureSkillRuleVersion({ type, desired, slug, client, dryRun }) {
|
|
1274
|
+
const versions = await client.getJson(`/api/${type === "skill" ? "skills" : "rules"}/${encodeURIComponent(slug)}/versions`);
|
|
1275
|
+
const latest = Array.isArray(versions)
|
|
1276
|
+
? versions.find((item) => item && item.isLatest) || versions[0]
|
|
1277
|
+
: null;
|
|
1278
|
+
const currentFiles = normalizeFiles(latest?.files || []);
|
|
1279
|
+
const desiredFiles = normalizeFiles(desired.files || []);
|
|
1280
|
+
if (deepEqual(currentFiles, desiredFiles)) {
|
|
1281
|
+
info(`${type} ${slug}: version files unchanged`);
|
|
1282
|
+
return "unchanged";
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if (dryRun) {
|
|
1286
|
+
info(`${type} ${slug}: publish version`);
|
|
1287
|
+
return "versioned";
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const nextVersion = suggestNextPatchVersion(
|
|
1291
|
+
Array.isArray(versions) ? versions.map((item) => item.version).filter(Boolean) : [],
|
|
1292
|
+
);
|
|
1293
|
+
try {
|
|
1294
|
+
await client.postJson(`/api/${type === "skill" ? "skills" : "rules"}/${encodeURIComponent(slug)}/versions`, {
|
|
1295
|
+
version: nextVersion,
|
|
1296
|
+
changelog: "sync from local registry",
|
|
1297
|
+
files: desired.files,
|
|
1298
|
+
isLatest: true,
|
|
1299
|
+
});
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1302
|
+
if (message.includes("请先登录后再上传")) {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
`${type} ${slug}: version update requires agent login. Provide --agent-api-key or hub.agentApiKey.`,
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
throw error;
|
|
1308
|
+
}
|
|
1309
|
+
info(`${type} ${slug}: version ${nextVersion} created`);
|
|
1310
|
+
return "versioned";
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
async function ensureRoleVersion({ client, slug, desiredVersionFiles, dryRun }) {
|
|
1314
|
+
const needsChange = await roleVersionWouldChange({
|
|
1315
|
+
client,
|
|
1316
|
+
slug,
|
|
1317
|
+
desiredVersionFiles,
|
|
1318
|
+
});
|
|
1319
|
+
if (!needsChange) {
|
|
1320
|
+
info(`role ${slug}: version files unchanged`);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
if (dryRun) {
|
|
1324
|
+
info(`role ${slug}: publish version`);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
const versions = await client.getJson(`/api/roles/${encodeURIComponent(slug)}/versions`);
|
|
1328
|
+
const nextVersion = suggestNextPatchVersion(
|
|
1329
|
+
Array.isArray(versions) ? versions.map((item) => item.version).filter(Boolean) : [],
|
|
1330
|
+
);
|
|
1331
|
+
await client.postJson(`/api/roles/${encodeURIComponent(slug)}/versions`, {
|
|
1332
|
+
version: nextVersion,
|
|
1333
|
+
changelog: "sync from local registry",
|
|
1334
|
+
isLatest: true,
|
|
1335
|
+
});
|
|
1336
|
+
info(`role ${slug}: version ${nextVersion} created`);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
async function roleVersionWouldChange({ client, slug, desiredVersionFiles }) {
|
|
1340
|
+
const versions = await client.getJson(`/api/roles/${encodeURIComponent(slug)}/versions`);
|
|
1341
|
+
const latest = Array.isArray(versions)
|
|
1342
|
+
? versions.find((item) => item && item.isLatest) || versions[0]
|
|
1343
|
+
: null;
|
|
1344
|
+
const currentFiles = normalizeFiles(latest?.files || []);
|
|
1345
|
+
const desiredFiles = normalizeFiles(desiredVersionFiles || []);
|
|
1346
|
+
return !deepEqual(currentFiles, desiredFiles);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function buildProfileVariantSpecs({ type, resourceId, local, hubState }) {
|
|
1350
|
+
if (!local?.sourceByProfile || typeof local.sourceByProfile !== "object") {
|
|
1351
|
+
return [];
|
|
1352
|
+
}
|
|
1353
|
+
const profiles = Object.keys(local.sourceByProfile).sort();
|
|
1354
|
+
if (profiles.length <= 1) {
|
|
1355
|
+
return [];
|
|
1356
|
+
}
|
|
1357
|
+
const existingMatches = findResourcesByRegistryKey(type, resourceId, hubState);
|
|
1358
|
+
if (existingMatches.length === 0) {
|
|
1359
|
+
return [];
|
|
1360
|
+
}
|
|
1361
|
+
return profiles.map((profile) => {
|
|
1362
|
+
const existing = pickProfileResourceVariant(type, resourceId, profile, existingMatches);
|
|
1363
|
+
return {
|
|
1364
|
+
profile,
|
|
1365
|
+
slug: existing?.slug || defaultSplitResourceSlug(type, resourceId, profile),
|
|
1366
|
+
sourcePaths: [local.sourceByProfile[profile]].filter(Boolean),
|
|
1367
|
+
supportedProfiles: [profile],
|
|
1368
|
+
existing,
|
|
1369
|
+
};
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function findResourcesByRegistryKey(type, resourceId, hubState) {
|
|
1374
|
+
const collection = type === "skill" ? hubState.skillsBySlug : hubState.rulesBySlug;
|
|
1375
|
+
return Object.values(collection || {}).filter(
|
|
1376
|
+
(item) =>
|
|
1377
|
+
item &&
|
|
1378
|
+
(item.registryId === resourceId || item.manifestId === resourceId),
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function pickProfileResourceVariant(type, resourceId, profile, items) {
|
|
1383
|
+
const fallbackSlug = defaultSplitResourceSlug(type, resourceId, profile);
|
|
1384
|
+
return (
|
|
1385
|
+
items.find((item) => normalizeStringArray(item.supportedProfiles).includes(profile)) ||
|
|
1386
|
+
items.find((item) => item.slug === fallbackSlug) ||
|
|
1387
|
+
null
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function defaultSplitResourceSlug(type, resourceId, profile) {
|
|
1392
|
+
return type === "rule" ? `${profile}-${resourceId}` : `${resourceId}-${profile}`;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function resolveLinkedResourceIds(type, resourceId, hubState) {
|
|
1396
|
+
const collection = type === "skill" ? hubState.skillsBySlug : hubState.rulesBySlug;
|
|
1397
|
+
const direct = collection?.[resourceId];
|
|
1398
|
+
if (direct?.id) {
|
|
1399
|
+
return [direct.id];
|
|
1400
|
+
}
|
|
1401
|
+
return findResourcesByRegistryKey(type, resourceId, hubState)
|
|
1402
|
+
.map((item) => item.id)
|
|
1403
|
+
.filter(Boolean);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function collectSkillFiles(local, skillId, forcedSourcePaths) {
|
|
1407
|
+
const sourcePaths = forcedSourcePaths || resolveSourcePaths(local);
|
|
1408
|
+
if (sourcePaths.length === 0) return [];
|
|
1409
|
+
const absoluteSkillDirs = uniqueKeepOrder(
|
|
1410
|
+
sourcePaths.map((relativePath) => path.dirname(path.resolve(PROJECT_ROOT, relativePath))),
|
|
1411
|
+
);
|
|
1412
|
+
const baseDir = commonAncestor(absoluteSkillDirs);
|
|
1413
|
+
const fileEntries = [];
|
|
1414
|
+
for (const skillDir of absoluteSkillDirs) {
|
|
1415
|
+
for (const absoluteFile of walkFiles(skillDir)) {
|
|
1416
|
+
const buffer = fs.readFileSync(absoluteFile);
|
|
1417
|
+
if (looksBinary(buffer, absoluteFile)) {
|
|
1418
|
+
warn(`skill ${skillId}: skipped binary file ${path.relative(PROJECT_ROOT, absoluteFile)}`);
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
const relativePath = toPosixPath(path.relative(baseDir, absoluteFile));
|
|
1422
|
+
fileEntries.push({
|
|
1423
|
+
name: path.basename(absoluteFile),
|
|
1424
|
+
path: relativePath,
|
|
1425
|
+
content: buffer.toString("utf8"),
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return normalizeFiles(fileEntries);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function collectRuleFiles(local, forcedSourcePaths) {
|
|
1433
|
+
const sourcePaths = forcedSourcePaths || resolveSourcePaths(local);
|
|
1434
|
+
if (sourcePaths.length === 0) return [];
|
|
1435
|
+
const absoluteFiles = uniqueKeepOrder(sourcePaths.map((relativePath) => path.resolve(PROJECT_ROOT, relativePath)));
|
|
1436
|
+
const baseDir = commonAncestor(absoluteFiles.map((absoluteFile) => path.dirname(absoluteFile)));
|
|
1437
|
+
return normalizeFiles(
|
|
1438
|
+
absoluteFiles.map((absoluteFile) => ({
|
|
1439
|
+
name: path.basename(absoluteFile),
|
|
1440
|
+
path: toPosixPath(path.relative(baseDir, absoluteFile)),
|
|
1441
|
+
content: fs.readFileSync(absoluteFile, "utf8"),
|
|
1442
|
+
})),
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function resolveSourcePaths(local) {
|
|
1447
|
+
if (local.source) {
|
|
1448
|
+
return [local.source];
|
|
1449
|
+
}
|
|
1450
|
+
if (local.sourceByProfile && typeof local.sourceByProfile === "object") {
|
|
1451
|
+
return Object.keys(local.sourceByProfile)
|
|
1452
|
+
.sort()
|
|
1453
|
+
.map((key) => local.sourceByProfile[key])
|
|
1454
|
+
.filter(Boolean);
|
|
1455
|
+
}
|
|
1456
|
+
return [];
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
async function parseRoleWithHub(client, sourcePath) {
|
|
1460
|
+
const buffer = fs.readFileSync(sourcePath);
|
|
1461
|
+
const form = new FormData();
|
|
1462
|
+
form.set("kind", "role");
|
|
1463
|
+
form.set("mode", "zip");
|
|
1464
|
+
form.set("file", new Blob([buffer]), path.basename(sourcePath));
|
|
1465
|
+
return client.postForm("/api/upload", form);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
function resolveCategorySlug({ type, resourceId, override, domains, categories, config }) {
|
|
1469
|
+
if (override.categorySlug) return override.categorySlug;
|
|
1470
|
+
const categoryMap = config?.categoryMap?.[type] || {};
|
|
1471
|
+
if (categoryMap[resourceId]) return categoryMap[resourceId];
|
|
1472
|
+
for (const domain of domains || []) {
|
|
1473
|
+
if (categoryMap[`domain:${domain}`]) {
|
|
1474
|
+
return categoryMap[`domain:${domain}`];
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
const defaultKey = type === "skill" ? "skillCategorySlug" : "ruleCategorySlug";
|
|
1478
|
+
if (config?.defaults?.[defaultKey]) return config.defaults[defaultKey];
|
|
1479
|
+
if (Array.isArray(categories) && categories.length === 1) {
|
|
1480
|
+
return categories[0].slug;
|
|
1481
|
+
}
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function resolveResourceOverride({ config, type, resourceId, variantSlug }) {
|
|
1486
|
+
const resources = config?.resources?.[type] || {};
|
|
1487
|
+
const base = resources?.[resourceId] || {};
|
|
1488
|
+
const variant = variantSlug ? resources?.[variantSlug] || {} : {};
|
|
1489
|
+
return {
|
|
1490
|
+
...base,
|
|
1491
|
+
...variant,
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function buildSkillRuleMetadataPatch(type, desired, existing) {
|
|
1496
|
+
const patch = {};
|
|
1497
|
+
const existingCategorySlug = existing.categorySlug || existing.category?.slug || null;
|
|
1498
|
+
if (desired.name && desired.name !== existing.name) patch.name = desired.name;
|
|
1499
|
+
if (desired.slug && desired.slug !== existing.slug) patch.slug = desired.slug;
|
|
1500
|
+
if (desired.registryId !== undefined && desired.registryId !== existing.registryId) {
|
|
1501
|
+
patch.registryId = desired.registryId;
|
|
1502
|
+
}
|
|
1503
|
+
if (desired.manifestId !== undefined && desired.manifestId !== existing.manifestId) {
|
|
1504
|
+
patch.manifestId = desired.manifestId;
|
|
1505
|
+
}
|
|
1506
|
+
if (desired.description !== existing.description) {
|
|
1507
|
+
patch.description = desired.description;
|
|
1508
|
+
}
|
|
1509
|
+
if ((desired.longDescription || null) !== (existing.longDescription || null)) {
|
|
1510
|
+
patch.longDescription = desired.longDescription || null;
|
|
1511
|
+
}
|
|
1512
|
+
if (desired.author !== existing.author) {
|
|
1513
|
+
patch.author = desired.author;
|
|
1514
|
+
}
|
|
1515
|
+
if (desired.categorySlug && desired.categorySlug !== existingCategorySlug) {
|
|
1516
|
+
patch.categorySlug = desired.categorySlug;
|
|
1517
|
+
}
|
|
1518
|
+
if (!sameStringArray(desired.tags, existing.tags)) {
|
|
1519
|
+
patch.tags = desired.tags;
|
|
1520
|
+
}
|
|
1521
|
+
if (!sameStringArray(desired.supportedProfiles, existing.supportedProfiles)) {
|
|
1522
|
+
patch.supportedProfiles = desired.supportedProfiles;
|
|
1523
|
+
}
|
|
1524
|
+
if (desired.downloadPolicy !== existing.downloadPolicy) {
|
|
1525
|
+
patch.downloadPolicy = desired.downloadPolicy;
|
|
1526
|
+
}
|
|
1527
|
+
return Object.keys(patch).length > 0 ? patch : null;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function buildSkillRuleFullPatch(desired) {
|
|
1531
|
+
return {
|
|
1532
|
+
name: desired.name,
|
|
1533
|
+
slug: desired.slug,
|
|
1534
|
+
registryId: desired.registryId,
|
|
1535
|
+
manifestId: desired.manifestId,
|
|
1536
|
+
description: desired.description,
|
|
1537
|
+
longDescription: desired.longDescription || null,
|
|
1538
|
+
author: desired.author,
|
|
1539
|
+
categorySlug: desired.categorySlug,
|
|
1540
|
+
tags: desired.tags,
|
|
1541
|
+
supportedProfiles: desired.supportedProfiles,
|
|
1542
|
+
downloadPolicy: desired.downloadPolicy,
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
async function createSkillRuleOrNull({ type, desired, client }) {
|
|
1547
|
+
try {
|
|
1548
|
+
const response = await client.postJson(`/${type === "skill" ? "api/skills" : "api/rules"}`, {
|
|
1549
|
+
name: desired.name,
|
|
1550
|
+
slug: desired.slug,
|
|
1551
|
+
registryId: desired.registryId,
|
|
1552
|
+
manifestId: desired.manifestId,
|
|
1553
|
+
description: desired.description,
|
|
1554
|
+
longDescription: desired.longDescription,
|
|
1555
|
+
author: desired.author,
|
|
1556
|
+
categorySlug: desired.categorySlug,
|
|
1557
|
+
tags: desired.tags,
|
|
1558
|
+
supportedProfiles: desired.supportedProfiles,
|
|
1559
|
+
downloadPolicy: desired.downloadPolicy,
|
|
1560
|
+
initialFiles: desired.files,
|
|
1561
|
+
});
|
|
1562
|
+
return {
|
|
1563
|
+
created: true,
|
|
1564
|
+
resource: response?.[type] || response || null,
|
|
1565
|
+
};
|
|
1566
|
+
} catch (error) {
|
|
1567
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1568
|
+
if (isConflictMessage(message)) {
|
|
1569
|
+
return {
|
|
1570
|
+
created: false,
|
|
1571
|
+
resource: null,
|
|
1572
|
+
conflictSlugs: extractConflictResourceSlugs(message),
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
throw error;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
async function fetchPublicResource(client, type, slug) {
|
|
1580
|
+
try {
|
|
1581
|
+
return await client.getJson(`/api/${type === "skill" ? "skills" : "rules"}/${encodeURIComponent(slug)}`);
|
|
1582
|
+
} catch {
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
async function fetchConflictResource({ type, desiredSlug, conflictSlugs, client }) {
|
|
1588
|
+
const candidates = uniqueKeepOrder([desiredSlug, ...(conflictSlugs || [])]);
|
|
1589
|
+
for (const candidate of candidates) {
|
|
1590
|
+
const resource = await fetchPublicResource(client, type, candidate);
|
|
1591
|
+
if (resource) {
|
|
1592
|
+
return resource;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function resolveRoleDomainIds({ override, local, uploadParsed, config }) {
|
|
1599
|
+
const explicit = Array.isArray(override.domainIds) ? override.domainIds : [];
|
|
1600
|
+
if (explicit.length > 0) {
|
|
1601
|
+
return explicit;
|
|
1602
|
+
}
|
|
1603
|
+
const configMap = config?.domainIdMap || {};
|
|
1604
|
+
const mapped = uniqueKeepOrder([
|
|
1605
|
+
...(Array.isArray(local.domains) ? local.domains : []),
|
|
1606
|
+
...(Array.isArray(uploadParsed.roleData.domains) ? uploadParsed.roleData.domains : []),
|
|
1607
|
+
])
|
|
1608
|
+
.map((slug) => configMap[slug])
|
|
1609
|
+
.filter(Boolean);
|
|
1610
|
+
if (mapped.length > 0) {
|
|
1611
|
+
return mapped;
|
|
1612
|
+
}
|
|
1613
|
+
if (Array.isArray(uploadParsed.mappedDomainIds) && uploadParsed.mappedDomainIds.length > 0) {
|
|
1614
|
+
return uniqueKeepOrder(uploadParsed.mappedDomainIds);
|
|
1615
|
+
}
|
|
1616
|
+
return [];
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function resolveScenarioDomainIds({ scenario, override, roleItems, hubState, config }) {
|
|
1620
|
+
if (Array.isArray(override.domainIds) && override.domainIds.length > 0) {
|
|
1621
|
+
return uniqueKeepOrder(override.domainIds);
|
|
1622
|
+
}
|
|
1623
|
+
const domainMap = config?.domainIdMap || {};
|
|
1624
|
+
const mapped = uniqueKeepOrder(Array.isArray(scenario.domains) ? scenario.domains : [])
|
|
1625
|
+
.map((slug) => domainMap[slug])
|
|
1626
|
+
.filter(Boolean);
|
|
1627
|
+
if (mapped.length > 0) {
|
|
1628
|
+
return mapped;
|
|
1629
|
+
}
|
|
1630
|
+
const roleDomainIds = [];
|
|
1631
|
+
for (const roleItem of roleItems) {
|
|
1632
|
+
const role = Object.values(hubState.rolesBySlug).find((item) => item.id === roleItem.id);
|
|
1633
|
+
if (role && Array.isArray(role.domainLinks)) {
|
|
1634
|
+
roleDomainIds.push(...role.domainLinks.map((link) => link.domainId).filter(Boolean));
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return uniqueKeepOrder(roleDomainIds);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function resolveScenarioEntryRoleId(override, local, hubState) {
|
|
1641
|
+
if (override.entryRoleId) return override.entryRoleId;
|
|
1642
|
+
if (override.entryRoleSlug && hubState.rolesBySlug[override.entryRoleSlug]) {
|
|
1643
|
+
return hubState.rolesBySlug[override.entryRoleSlug].id;
|
|
1644
|
+
}
|
|
1645
|
+
const firstRoleSlug = Array.isArray(local.roles) ? local.roles[0] : null;
|
|
1646
|
+
return firstRoleSlug && hubState.rolesBySlug[firstRoleSlug]
|
|
1647
|
+
? hubState.rolesBySlug[firstRoleSlug].id
|
|
1648
|
+
: null;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function normalizeRoleResponseToPayload(item) {
|
|
1652
|
+
return {
|
|
1653
|
+
name: item.name,
|
|
1654
|
+
slug: item.slug,
|
|
1655
|
+
registryId: item.registryId || null,
|
|
1656
|
+
manifestId: item.manifestId || null,
|
|
1657
|
+
author: item.author,
|
|
1658
|
+
description: item.description,
|
|
1659
|
+
longDescription: item.longDescription || null,
|
|
1660
|
+
publishStatus: item.publishStatus,
|
|
1661
|
+
roleStatus: item.roleStatus,
|
|
1662
|
+
tags: normalizeStringArray(item.tags),
|
|
1663
|
+
supportedProfiles: normalizeStringArray(item.supportedProfiles),
|
|
1664
|
+
triggers: normalizeStringArray(item.triggers),
|
|
1665
|
+
preferredSkills: normalizeStringArray(item.preferredSkills),
|
|
1666
|
+
reads: normalizeStringArray(item.reads),
|
|
1667
|
+
writes: normalizeStringArray(item.writes),
|
|
1668
|
+
handoffTo: normalizeStringArray(item.handoffTo),
|
|
1669
|
+
rolePositioning: item.rolePositioning || null,
|
|
1670
|
+
workingPrinciples: normalizeStringArray(item.workingPrinciples),
|
|
1671
|
+
requiredSteps: normalizeStringArray(item.requiredSteps),
|
|
1672
|
+
executionContract: item.executionContract || null,
|
|
1673
|
+
outputStandard: item.outputStandard || null,
|
|
1674
|
+
prohibitedActions: normalizeStringArray(item.prohibitedActions),
|
|
1675
|
+
handoffNotes: item.handoffNotes || null,
|
|
1676
|
+
skillIds: (item.skillLinks || []).map((link) => link.skillId),
|
|
1677
|
+
ruleIds: (item.ruleLinks || []).map((link) => link.ruleId),
|
|
1678
|
+
domainIds: (item.domainLinks || []).map((link) => link.domainId),
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function buildPreviewSkillRuleState(type, existing, desired) {
|
|
1683
|
+
return {
|
|
1684
|
+
...(existing || {}),
|
|
1685
|
+
id: existing?.id || previewResourceId(type, desired.slug),
|
|
1686
|
+
slug: desired.slug,
|
|
1687
|
+
name: desired.name,
|
|
1688
|
+
registryId: desired.registryId ?? existing?.registryId ?? null,
|
|
1689
|
+
manifestId: desired.manifestId ?? existing?.manifestId ?? null,
|
|
1690
|
+
description: desired.description,
|
|
1691
|
+
longDescription: desired.longDescription || null,
|
|
1692
|
+
author: desired.author,
|
|
1693
|
+
tags: desired.tags,
|
|
1694
|
+
supportedProfiles: desired.supportedProfiles,
|
|
1695
|
+
categorySlug: desired.categorySlug || existing?.categorySlug || null,
|
|
1696
|
+
categoryName: existing?.categoryName || desired.categorySlug || null,
|
|
1697
|
+
downloadPolicy: desired.downloadPolicy,
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function buildPreviewRoleState(existing, desired) {
|
|
1702
|
+
return {
|
|
1703
|
+
...(existing || {}),
|
|
1704
|
+
id: existing?.id || previewResourceId("role", desired.slug),
|
|
1705
|
+
...desired.payload,
|
|
1706
|
+
skillLinks: (desired.payload.skillIds || []).map((skillId) => ({ skillId })),
|
|
1707
|
+
ruleLinks: (desired.payload.ruleIds || []).map((ruleId) => ({ ruleId })),
|
|
1708
|
+
domainLinks: (desired.payload.domainIds || []).map((domainId) => ({ domainId })),
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function normalizeScenarioResponseToPayload(item) {
|
|
1713
|
+
return {
|
|
1714
|
+
name: item.name,
|
|
1715
|
+
slug: item.slug,
|
|
1716
|
+
description: item.description,
|
|
1717
|
+
longDescription: item.longDescription || null,
|
|
1718
|
+
publishStatus: item.publishStatus,
|
|
1719
|
+
tags: normalizeStringArray(item.tags),
|
|
1720
|
+
supportedProfiles: normalizeStringArray(item.supportedProfiles),
|
|
1721
|
+
recommendedIdes: normalizeStringArray(item.recommendedIdes),
|
|
1722
|
+
entryRoleId: item.entryRoleId || null,
|
|
1723
|
+
isFeatured: Boolean(item.isFeatured),
|
|
1724
|
+
roles: sortByKey(
|
|
1725
|
+
(item.roles || []).map((link) => ({
|
|
1726
|
+
id: link.roleId,
|
|
1727
|
+
isOptional: Boolean(link.isOptional),
|
|
1728
|
+
})),
|
|
1729
|
+
(row) => `${row.id}:${row.isOptional ? 1 : 0}`,
|
|
1730
|
+
),
|
|
1731
|
+
skillIds: sortStrings((item.skills || []).map((link) => link.skillId)),
|
|
1732
|
+
ruleIds: sortStrings((item.rules || []).map((link) => link.ruleId)),
|
|
1733
|
+
domainIds: sortStrings((item.domainLinks || []).map((link) => link.domainId)),
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function buildPreviewScenarioState(existing, desired) {
|
|
1738
|
+
return {
|
|
1739
|
+
...(existing || {}),
|
|
1740
|
+
id: existing?.id || previewResourceId("scenario", desired.slug),
|
|
1741
|
+
...desired.payload,
|
|
1742
|
+
roles: (desired.payload.roles || []).map((link) => ({
|
|
1743
|
+
roleId: link.id,
|
|
1744
|
+
isOptional: Boolean(link.isOptional),
|
|
1745
|
+
})),
|
|
1746
|
+
skills: (desired.payload.skillIds || []).map((skillId) => ({ skillId })),
|
|
1747
|
+
rules: (desired.payload.ruleIds || []).map((ruleId) => ({ ruleId })),
|
|
1748
|
+
domainLinks: (desired.payload.domainIds || []).map((domainId) => ({ domainId })),
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function buildRoleVersionFiles(input) {
|
|
1753
|
+
return normalizeFiles([
|
|
1754
|
+
{
|
|
1755
|
+
name: `${input.slug}.role.json`,
|
|
1756
|
+
path: `.hub/roles/${input.slug}.role.json`,
|
|
1757
|
+
content: JSON.stringify(
|
|
1758
|
+
{
|
|
1759
|
+
name: input.name,
|
|
1760
|
+
slug: input.slug,
|
|
1761
|
+
author: input.author,
|
|
1762
|
+
description: input.description,
|
|
1763
|
+
longDescription: input.longDescription ?? null,
|
|
1764
|
+
publishStatus: input.publishStatus,
|
|
1765
|
+
roleStatus: input.roleStatus,
|
|
1766
|
+
supportedProfiles: input.supportedProfiles,
|
|
1767
|
+
tags: input.tags,
|
|
1768
|
+
triggers: input.triggers,
|
|
1769
|
+
preferredSkills: input.preferredSkills,
|
|
1770
|
+
reads: input.reads,
|
|
1771
|
+
writes: input.writes,
|
|
1772
|
+
handoffTo: input.handoffTo,
|
|
1773
|
+
skills: input.skillSlugs,
|
|
1774
|
+
rules: input.ruleSlugs,
|
|
1775
|
+
capabilityDomains: input.domainSlugs,
|
|
1776
|
+
sections: {
|
|
1777
|
+
rolePositioning: input.rolePositioning ?? null,
|
|
1778
|
+
workingPrinciples: input.workingPrinciples,
|
|
1779
|
+
requiredSteps: input.requiredSteps,
|
|
1780
|
+
executionContract: input.executionContract ?? null,
|
|
1781
|
+
outputStandard: input.outputStandard ?? null,
|
|
1782
|
+
prohibitedActions: input.prohibitedActions,
|
|
1783
|
+
handoffNotes: input.handoffNotes ?? null,
|
|
1784
|
+
},
|
|
1785
|
+
},
|
|
1786
|
+
null,
|
|
1787
|
+
2,
|
|
1788
|
+
),
|
|
1789
|
+
},
|
|
1790
|
+
]);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
function walkFiles(rootDir) {
|
|
1794
|
+
const results = [];
|
|
1795
|
+
const stack = [rootDir];
|
|
1796
|
+
while (stack.length > 0) {
|
|
1797
|
+
const current = stack.pop();
|
|
1798
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
1799
|
+
for (const entry of entries) {
|
|
1800
|
+
if (entry.name === ".DS_Store") continue;
|
|
1801
|
+
const absolutePath = path.join(current, entry.name);
|
|
1802
|
+
if (entry.isDirectory()) {
|
|
1803
|
+
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
1804
|
+
stack.push(absolutePath);
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
results.push(absolutePath);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
results.sort();
|
|
1811
|
+
return results;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function commonAncestor(paths) {
|
|
1815
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
1816
|
+
return PROJECT_ROOT;
|
|
1817
|
+
}
|
|
1818
|
+
const split = paths.map((item) => path.resolve(item).split(path.sep).filter(Boolean));
|
|
1819
|
+
const minLength = Math.min(...split.map((parts) => parts.length));
|
|
1820
|
+
const shared = [];
|
|
1821
|
+
for (let index = 0; index < minLength; index += 1) {
|
|
1822
|
+
const value = split[0][index];
|
|
1823
|
+
if (split.every((parts) => parts[index] === value)) {
|
|
1824
|
+
shared.push(value);
|
|
1825
|
+
} else {
|
|
1826
|
+
break;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
const prefix = path.isAbsolute(paths[0]) ? path.sep : "";
|
|
1830
|
+
return prefix + shared.join(path.sep);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function parseFrontmatterFile(content, type) {
|
|
1834
|
+
const trimmed = content.trimStart();
|
|
1835
|
+
if (!trimmed.startsWith("---")) {
|
|
1836
|
+
return {};
|
|
1837
|
+
}
|
|
1838
|
+
const match = trimmed.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
1839
|
+
if (!match) {
|
|
1840
|
+
return {};
|
|
1841
|
+
}
|
|
1842
|
+
const meta = {};
|
|
1843
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
1844
|
+
const parsed = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
1845
|
+
if (!parsed) continue;
|
|
1846
|
+
let value = parsed[2].trim();
|
|
1847
|
+
if (
|
|
1848
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
1849
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
1850
|
+
) {
|
|
1851
|
+
value = value.slice(1, -1);
|
|
1852
|
+
}
|
|
1853
|
+
meta[parsed[1].toLowerCase()] = value;
|
|
1854
|
+
}
|
|
1855
|
+
const name =
|
|
1856
|
+
meta.name || meta.title || meta["display-name"] || (type === "skill" ? meta.skill : meta.rule);
|
|
1857
|
+
const description = meta.description || meta.summary || meta.desc;
|
|
1858
|
+
return {
|
|
1859
|
+
name: typeof name === "string" ? name : undefined,
|
|
1860
|
+
description: typeof description === "string" ? description : undefined,
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
function pickPrimaryTextFile(files, filename) {
|
|
1865
|
+
return files.find((file) => path.basename(file.path).toLowerCase() === filename.toLowerCase()) || null;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
function normalizeFiles(files) {
|
|
1869
|
+
return [...files]
|
|
1870
|
+
.map((file) => ({
|
|
1871
|
+
name: file.name,
|
|
1872
|
+
path: toPosixPath(file.path),
|
|
1873
|
+
...(typeof file.content === "string" ? { content: file.content } : {}),
|
|
1874
|
+
}))
|
|
1875
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
function normalizeStringArray(value) {
|
|
1879
|
+
if (!Array.isArray(value)) return [];
|
|
1880
|
+
return value.filter((item) => typeof item === "string");
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
function sortStrings(value) {
|
|
1884
|
+
return normalizeStringArray(value).slice().sort((left, right) => left.localeCompare(right));
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
function sortByKey(items, pickKey) {
|
|
1888
|
+
return [...(items || [])].sort((left, right) => pickKey(left).localeCompare(pickKey(right)));
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
function sameStringArray(left, right) {
|
|
1892
|
+
return deepEqual(sortStrings(left), sortStrings(right));
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
function previewResourceId(type, slug) {
|
|
1896
|
+
return `preview-${type}-${slug}`;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function looksBinary(buffer, absolutePath) {
|
|
1900
|
+
const extension = path.extname(absolutePath).toLowerCase();
|
|
1901
|
+
if (TEXT_FILE_EXTENSIONS.has(extension)) {
|
|
1902
|
+
return false;
|
|
1903
|
+
}
|
|
1904
|
+
if (!extension && path.basename(absolutePath).startsWith(".")) {
|
|
1905
|
+
return false;
|
|
1906
|
+
}
|
|
1907
|
+
return buffer.includes(0);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
function suggestNextPatchVersion(currentVersions) {
|
|
1911
|
+
const parsed = currentVersions
|
|
1912
|
+
.map((value) => {
|
|
1913
|
+
const match = String(value).trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
1914
|
+
if (!match) return null;
|
|
1915
|
+
return {
|
|
1916
|
+
major: Number(match[1]),
|
|
1917
|
+
minor: Number(match[2]),
|
|
1918
|
+
patch: Number(match[3]),
|
|
1919
|
+
};
|
|
1920
|
+
})
|
|
1921
|
+
.filter(Boolean);
|
|
1922
|
+
if (parsed.length === 0) return "1.0.0";
|
|
1923
|
+
parsed.sort((left, right) => {
|
|
1924
|
+
if (left.major !== right.major) return right.major - left.major;
|
|
1925
|
+
if (left.minor !== right.minor) return right.minor - left.minor;
|
|
1926
|
+
return right.patch - left.patch;
|
|
1927
|
+
});
|
|
1928
|
+
const latest = parsed[0];
|
|
1929
|
+
return `${latest.major}.${latest.minor}.${latest.patch + 1}`;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function selectResourceIds(registry, selection) {
|
|
1933
|
+
const all = Object.keys(registry);
|
|
1934
|
+
if (selection.mode === "all") return all;
|
|
1935
|
+
if (selection.mode === "none") return [];
|
|
1936
|
+
return all.filter((id) => selection.values.has(id));
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function uniqueKeepOrder(items) {
|
|
1940
|
+
const output = [];
|
|
1941
|
+
const seen = new Set();
|
|
1942
|
+
for (const item of items || []) {
|
|
1943
|
+
if (!item) continue;
|
|
1944
|
+
if (seen.has(item)) continue;
|
|
1945
|
+
seen.add(item);
|
|
1946
|
+
output.push(item);
|
|
1947
|
+
}
|
|
1948
|
+
return output;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
function toPosixPath(filePath) {
|
|
1952
|
+
return filePath.split(path.sep).join("/");
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
function deepEqual(left, right) {
|
|
1956
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
function isConflictMessage(message) {
|
|
1960
|
+
return (
|
|
1961
|
+
typeof message === "string" &&
|
|
1962
|
+
(message.includes("已存在") ||
|
|
1963
|
+
message.includes("409") ||
|
|
1964
|
+
message.includes("duplicate") ||
|
|
1965
|
+
message.includes("Unique"))
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function extractConflictResourceSlugs(message) {
|
|
1970
|
+
if (typeof message !== "string") return [];
|
|
1971
|
+
const matches = [...message.matchAll(/对应\s*(?:Rule|Skill)\s*:([a-z0-9._-]+)/gi)];
|
|
1972
|
+
return uniqueKeepOrder(matches.map((match) => match[1]).filter(Boolean));
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function indexBy(items, key) {
|
|
1976
|
+
const output = {};
|
|
1977
|
+
for (const item of items || []) {
|
|
1978
|
+
if (!item || !item[key]) continue;
|
|
1979
|
+
output[item[key]] = item;
|
|
1980
|
+
}
|
|
1981
|
+
return output;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
function findRoleById(hubState, id) {
|
|
1985
|
+
return Object.values(hubState.rolesBySlug || {}).find((item) => item.id === id) || null;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
function unwrapApiResponse(payload) {
|
|
1989
|
+
if (
|
|
1990
|
+
payload &&
|
|
1991
|
+
typeof payload === "object" &&
|
|
1992
|
+
Object.prototype.hasOwnProperty.call(payload, "data") &&
|
|
1993
|
+
Object.prototype.hasOwnProperty.call(payload, "code")
|
|
1994
|
+
) {
|
|
1995
|
+
return payload.data;
|
|
1996
|
+
}
|
|
1997
|
+
return payload;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function getResponseCookies(response) {
|
|
2001
|
+
if (typeof response.headers.getSetCookie === "function") {
|
|
2002
|
+
return response.headers.getSetCookie().map((cookie) => cookie.split(";")[0]);
|
|
2003
|
+
}
|
|
2004
|
+
const cookie = response.headers.get("set-cookie");
|
|
2005
|
+
return cookie ? [cookie.split(";")[0]] : [];
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
async function readErrorText(response) {
|
|
2009
|
+
const text = await response.text();
|
|
2010
|
+
try {
|
|
2011
|
+
const parsed = JSON.parse(text);
|
|
2012
|
+
return parsed?.error || parsed?.message || text;
|
|
2013
|
+
} catch {
|
|
2014
|
+
return text || `${response.status} ${response.statusText}`;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function readJson(filePath) {
|
|
2019
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
function warn(message) {
|
|
2023
|
+
console.warn(`[hub-sync] warn: ${message}`);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
function info(message) {
|
|
2027
|
+
console.log(`[hub-sync] ${message}`);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
function printSummary(summary, dryRun) {
|
|
2031
|
+
console.log("");
|
|
2032
|
+
console.log(`[hub-sync] ${dryRun ? "dry-run summary" : "summary"}`);
|
|
2033
|
+
console.log(
|
|
2034
|
+
JSON.stringify(summary, null, 2),
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
main();
|