@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.
Files changed (631) hide show
  1. package/.agents/commands/README.md +33 -33
  2. package/.agents/commands/claude/spec-start-review.md +88 -88
  3. package/.agents/commands/codex/spec-continue.md +74 -74
  4. package/.agents/commands/codex/spec-orchestrate.md +35 -35
  5. package/.agents/commands/codex/spec-start-review.md +88 -88
  6. package/.agents/commands/codex/spec-start.md +67 -67
  7. package/.agents/commands/codex/spec-status.md +22 -22
  8. package/.agents/commands/codex/spec-stop.md +29 -29
  9. package/.agents/commands/codex/spec-update.md +40 -40
  10. package/.agents/commands/common/branch-review.md +117 -117
  11. package/.agents/commands/common/project-init.md +25 -25
  12. package/.agents/commands/common/spec-continue.md +74 -74
  13. package/.agents/commands/common/spec-orchestrate.md +35 -35
  14. package/.agents/commands/common/spec-start-review.md +82 -82
  15. package/.agents/commands/common/spec-start.md +67 -67
  16. package/.agents/commands/common/spec-status.md +22 -22
  17. package/.agents/commands/common/spec-stop.md +29 -29
  18. package/.agents/commands/common/spec-update.md +60 -40
  19. package/.agents/commands/cursor/opsx-apply.md +55 -55
  20. package/.agents/commands/cursor/opsx-archive.md +48 -48
  21. package/.agents/commands/cursor/opsx-explore.md +45 -45
  22. package/.agents/commands/cursor/opsx-propose.md +59 -59
  23. package/.agents/commands/cursor/spec-continue.md +63 -63
  24. package/.agents/commands/cursor/spec-orchestrate.md +53 -53
  25. package/.agents/commands/cursor/spec-start-review.md +78 -78
  26. package/.agents/commands/cursor/spec-start.md +59 -59
  27. package/.agents/commands/cursor/spec-status.md +30 -30
  28. package/.agents/commands/cursor/spec-stop.md +29 -29
  29. package/.agents/commands/cursor/spec-update.md +41 -41
  30. package/.agents/flows/FRONTMATTER.md +263 -263
  31. package/.agents/flows/RUN_OUTPUT.md +263 -263
  32. package/.agents/flows/common/README.md +29 -29
  33. package/.agents/flows/common/bugfix-to-verification.md +95 -95
  34. package/.agents/flows/common/change-to-architecture-review.md +89 -89
  35. package/.agents/flows/common/change-to-release.md +94 -94
  36. package/.agents/flows/common/prd-to-delivery.md +184 -184
  37. package/.agents/flows/common/requirement-to-observability.md +97 -97
  38. package/.agents/orchestration/README.md +22 -22
  39. package/.agents/orchestration/expert-dispatch-spec.md +155 -155
  40. package/.agents/orchestration/expert-executor-spec.md +84 -84
  41. package/.agents/orchestration/expert-runtime-action-spec.md +73 -73
  42. package/.agents/orchestration/runtime-state-handoff-spec.md +264 -264
  43. package/.agents/orchestration/task-anchor-spec.md +212 -212
  44. package/.agents/orchestration/task-orchestrator-adapter-payload.md +153 -153
  45. package/.agents/orchestration/task-orchestrator-bootstrap-payload.md +145 -145
  46. package/.agents/orchestration/task-orchestrator-output-extractor-spec.md +93 -93
  47. package/.agents/orchestration/task-orchestrator-run-plan-template.md +312 -312
  48. package/.agents/orchestration/task-orchestrator-runtime-hooks.md +214 -214
  49. package/.agents/registry/README.md +63 -63
  50. package/.agents/registry/flows.json +125 -125
  51. package/.agents/registry/profiles.json +101 -101
  52. package/.agents/registry/roles.json +1265 -1265
  53. package/.agents/registry/rules.json +148 -148
  54. package/.agents/registry/scenario-packages.json +123 -123
  55. package/.agents/registry/skills.json +130 -130
  56. package/.agents/roles/INDEX.md +346 -346
  57. package/.agents/roles/common/README.md +76 -76
  58. package/.agents/roles/common/archive-change.md +80 -80
  59. package/.agents/roles/common/backend-implementer.md +92 -92
  60. package/.agents/roles/common/code-guardian.md +151 -151
  61. package/.agents/roles/common/frontend-implementer.md +146 -146
  62. package/.agents/roles/common/requirement-analyst.md +138 -138
  63. package/.agents/roles/common/task-orchestrator-routing.md +301 -301
  64. package/.agents/roles/common/task-orchestrator.md +224 -224
  65. package/.agents/roles/common/tooling-implementer.md +92 -92
  66. package/.agents/roles/domains/README.md +35 -35
  67. package/.agents/roles/domains/delivery/README.md +11 -11
  68. package/.agents/roles/domains/delivery/container-specialist.md +50 -50
  69. package/.agents/roles/domains/delivery/deployment-specialist.md +50 -50
  70. package/.agents/roles/domains/delivery/pipeline-specialist.md +50 -50
  71. package/.agents/roles/domains/demand-design/README.md +16 -16
  72. package/.agents/roles/domains/demand-design/api-contract-specialist.md +52 -52
  73. package/.agents/roles/domains/demand-design/design-collaborator.md +58 -58
  74. package/.agents/roles/domains/documentation/README.md +11 -11
  75. package/.agents/roles/domains/documentation/api-doc-specialist.md +50 -50
  76. package/.agents/roles/domains/documentation/component-doc-specialist.md +49 -49
  77. package/.agents/roles/domains/documentation/technical-writing-specialist.md +48 -48
  78. package/.agents/roles/domains/engineering/README.md +17 -17
  79. package/.agents/roles/domains/engineering/architecture-advisor.md +53 -53
  80. package/.agents/roles/domains/engineering/build-specialist.md +51 -51
  81. package/.agents/roles/domains/engineering/dependency-governor.md +52 -52
  82. package/.agents/roles/domains/governance/README.md +17 -17
  83. package/.agents/roles/domains/governance/api-governance-specialist.md +51 -51
  84. package/.agents/roles/domains/governance/lint-policy-specialist.md +49 -49
  85. package/.agents/roles/domains/governance/route-governance-specialist.md +52 -52
  86. package/.agents/roles/domains/observability/README.md +11 -11
  87. package/.agents/roles/domains/observability/error-tracker.md +50 -50
  88. package/.agents/roles/domains/observability/event-instrumentation-specialist.md +51 -51
  89. package/.agents/roles/domains/observability/rum-analyst.md +50 -50
  90. package/.agents/roles/domains/performance/README.md +11 -11
  91. package/.agents/roles/domains/performance/asset-optimizer.md +50 -50
  92. package/.agents/roles/domains/performance/performance-auditor.md +56 -56
  93. package/.agents/roles/domains/performance/vitals-analyst.md +50 -50
  94. package/.agents/roles/domains/security-a11y/README.md +11 -11
  95. package/.agents/roles/domains/security-a11y/a11y-auditor.md +50 -50
  96. package/.agents/roles/domains/security-a11y/aria-specialist.md +51 -51
  97. package/.agents/roles/domains/security-a11y/security-reviewer.md +49 -49
  98. package/.agents/roles/domains/testing/README.md +12 -12
  99. package/.agents/roles/domains/testing/coverage-analyst.md +50 -50
  100. package/.agents/roles/domains/testing/e2e-test-specialist.md +51 -51
  101. package/.agents/roles/domains/testing/unit-test-specialist.md +56 -56
  102. package/.agents/roles/domains/testing/verification-reviewer.md +67 -67
  103. package/.agents/rules/README.md +87 -87
  104. package/.agents/rules/common/02-/347/274/226/347/240/201/350/247/204/350/214/203.md +45 -45
  105. package/.agents/rules/common/08-/351/200/232/347/224/250/347/272/246/346/235/237.md +63 -63
  106. package/.agents/rules/common/10-/346/226/207/346/241/243/350/247/204/350/214/203.md +101 -101
  107. package/.agents/rules/common/12-Superpowers/346/211/247/350/241/214/350/247/204/350/214/203.md +46 -46
  108. 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
  109. package/.agents/rules/common/15-visual-gate-wait.md +90 -90
  110. package/.agents/rules/profiles/nestjs/01-/351/241/271/347/233/256/346/246/202/350/277/260.md +27 -27
  111. package/.agents/rules/profiles/nestjs/03-/351/241/271/347/233/256/347/273/223/346/236/204.md +20 -20
  112. 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
  113. 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
  114. 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
  115. 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
  116. 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
  117. package/.agents/rules/profiles/nestjs/11-/346/265/213/350/257/225/350/247/204/350/214/203.md +24 -24
  118. 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
  119. package/.agents/rules/profiles/node-tooling/01-/351/241/271/347/233/256/346/246/202/350/277/260.md +30 -30
  120. package/.agents/rules/profiles/node-tooling/03-/351/241/271/347/233/256/347/273/223/346/236/204.md +37 -37
  121. 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
  122. package/.agents/rules/profiles/node-tooling/05-Contract/344/270/216Schema/350/247/204/350/214/203.md +42 -42
  123. 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
  124. 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
  125. 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
  126. package/.agents/rules/profiles/node-tooling/11-/346/265/213/350/257/225/350/247/204/350/214/203.md +41 -41
  127. 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
  128. package/.agents/rules/profiles/react/01-/351/241/271/347/233/256/346/246/202/350/277/260.md +29 -29
  129. package/.agents/rules/profiles/react/03-/351/241/271/347/233/256/347/273/223/346/236/204.md +104 -104
  130. package/.agents/rules/profiles/react/04-/347/273/204/344/273/266/350/247/204/350/214/203.md +46 -46
  131. package/.agents/rules/profiles/react/05-API/350/247/204/350/214/203.md +67 -67
  132. package/.agents/rules/profiles/react/06-/350/267/257/347/224/261/350/247/204/350/214/203.md +54 -54
  133. package/.agents/rules/profiles/react/07-/347/212/266/346/200/201/347/256/241/347/220/206.md +226 -226
  134. package/.agents/rules/profiles/react/09-/346/240/267/345/274/217/350/247/204/350/214/203.md +71 -71
  135. package/.agents/rules/profiles/react/11-/346/265/213/350/257/225/350/247/204/350/214/203.md +80 -80
  136. 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
  137. package/.agents/rules/profiles/springboot/01-/351/241/271/347/233/256/346/246/202/350/277/260.md +31 -31
  138. package/.agents/rules/profiles/springboot/03-/351/241/271/347/233/256/347/273/223/346/236/204.md +37 -37
  139. package/.agents/rules/profiles/springboot/04-/345/210/206/345/261/202/350/247/204/350/214/203.md +33 -33
  140. 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
  141. 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
  142. 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
  143. 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
  144. package/.agents/rules/profiles/springboot/11-/346/265/213/350/257/225/350/247/204/350/214/203.md +43 -43
  145. 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
  146. package/.agents/rules/profiles/vue/01-/351/241/271/347/233/256/346/246/202/350/277/260.md +47 -47
  147. package/.agents/rules/profiles/vue/03-/351/241/271/347/233/256/347/273/223/346/236/204.md +106 -106
  148. package/.agents/rules/profiles/vue/04-/347/273/204/344/273/266/350/247/204/350/214/203.md +61 -61
  149. package/.agents/rules/profiles/vue/05-API/350/247/204/350/214/203.md +67 -67
  150. package/.agents/rules/profiles/vue/06-/350/267/257/347/224/261/350/247/204/350/214/203.md +69 -69
  151. package/.agents/rules/profiles/vue/07-/347/212/266/346/200/201/347/256/241/347/220/206.md +93 -93
  152. package/.agents/rules/profiles/vue/09-/346/240/267/345/274/217/350/247/204/350/214/203.md +67 -67
  153. package/.agents/rules/profiles/vue/11-/346/265/213/350/257/225/350/247/204/350/214/203.md +80 -80
  154. 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
  155. package/.agents/skills/README.md +171 -171
  156. package/.agents/skills/common/archive-change/SKILL.md +180 -180
  157. package/.agents/skills/common/branch-code-reviewer/SKILL.md +533 -459
  158. package/.agents/skills/common/branch-code-reviewer/references/business-risk-guide.md +293 -293
  159. package/.agents/skills/common/branch-code-reviewer/references/html-template-guide.md +121 -121
  160. package/.agents/skills/common/config-and-secret-scan/SKILL.md +99 -99
  161. package/.agents/skills/common/create-proposal/SKILL.md +192 -192
  162. package/.agents/skills/common/create-proposal/evals/evals.json +16 -16
  163. package/.agents/skills/common/create-proposal/evals/train_queries.json +18 -18
  164. package/.agents/skills/common/create-proposal/evals/validation_queries.json +18 -18
  165. package/.agents/skills/common/create-proposal/references/interaction-spec-template.md +42 -42
  166. package/.agents/skills/common/create-test/SKILL.md +292 -292
  167. package/.agents/skills/common/dependency-impact-graph/SKILL.md +80 -80
  168. package/.agents/skills/common/execute-task/SKILL.md +206 -206
  169. package/.agents/skills/common/execute-task/evals/evals.json +16 -16
  170. package/.agents/skills/common/execute-task/evals/train_queries.json +18 -18
  171. package/.agents/skills/common/execute-task/evals/validation_queries.json +18 -18
  172. package/.agents/skills/common/find-skills/SKILL.md +144 -144
  173. package/.agents/skills/common/install-ai-spec-auto/SKILL.md +260 -260
  174. package/.agents/skills/common/install-ai-spec-auto/evals/evals.json +17 -17
  175. package/.agents/skills/common/install-ai-spec-auto/evals/train_queries.json +18 -18
  176. package/.agents/skills/common/install-ai-spec-auto/evals/validation_queries.json +18 -18
  177. package/.agents/skills/common/project-init/SKILL.md +178 -178
  178. package/.agents/skills/common/project-init/evals/evals.json +16 -16
  179. package/.agents/skills/common/project-init/evals/train_queries.json +18 -18
  180. package/.agents/skills/common/project-init/evals/validation_queries.json +18 -18
  181. package/.agents/skills/common/project-init/references/custom-rule-generation.md +89 -89
  182. package/.agents/skills/common/project-init/references/deep-scan-rules.md +67 -67
  183. package/.agents/skills/common/project-init/references/output-contracts.md +71 -71
  184. package/.agents/skills/common/project-init/references/repo-fact-gathering.md +83 -83
  185. package/.agents/skills/common/project-init/references/scope-resolution.md +76 -76
  186. package/.agents/skills/common/project-init/scripts/inspect-project.js +112 -112
  187. package/.agents/skills/common/skill-creator/LICENSE.txt +201 -201
  188. package/.agents/skills/common/skill-creator/SKILL.md +370 -370
  189. package/.agents/skills/common/skill-creator/evals/evals.json +16 -16
  190. package/.agents/skills/common/skill-creator/evals/train_queries.json +18 -18
  191. package/.agents/skills/common/skill-creator/evals/validation_queries.json +18 -18
  192. package/.agents/skills/common/skill-creator/references/output-patterns.md +82 -82
  193. package/.agents/skills/common/skill-creator/references/workflows.md +27 -27
  194. package/.agents/skills/common/skill-creator/scripts/init_skill.py +209 -209
  195. package/.agents/skills/common/skill-creator/scripts/package_skill.py +110 -110
  196. package/.agents/skills/common/skill-creator/scripts/quick_validate.py +51 -51
  197. package/.agents/skills/common/skill-optimizer/SKILL.md +102 -102
  198. package/.agents/skills/common/skill-optimizer/evals/evals.json +16 -16
  199. package/.agents/skills/common/skill-optimizer/evals/train_queries.json +18 -18
  200. package/.agents/skills/common/skill-optimizer/evals/validation_queries.json +18 -18
  201. package/.agents/skills/common/skill-optimizer/references/design-patterns.md +26 -26
  202. package/.agents/skills/common/skill-optimizer/references/review-checklist.md +22 -22
  203. package/.agents/skills/common/using-superpowers/SKILL.md +151 -151
  204. package/.agents/skills/common/wait-for-gate-signal/SKILL.md +85 -85
  205. package/.agents/skills/domains/README.md +19 -19
  206. package/.agents/skills/domains/ui-ux-pro-max/SKILL.md +58 -58
  207. package/.agents/skills/domains/web/design-analysis/SKILL.md +89 -89
  208. package/.agents/skills/domains/web/design-analysis/rules/analysis-order.md +61 -61
  209. package/.agents/skills/domains/web/design-analysis/rules/analysis-priorities.md +136 -136
  210. package/.agents/skills/domains/web/design-analysis/rules/checklist-common-misses.md +107 -107
  211. package/.agents/skills/domains/web/design-analysis/rules/implementation-common-errors.md +204 -204
  212. package/.agents/skills/domains/web/design-analysis/rules/implementation-guidelines.md +211 -211
  213. package/.agents/skills/domains/web/design-analysis/rules/output-analysis-checklist.md +247 -247
  214. package/.agents/skills/domains/web/design-analysis/rules/tools-design-guidelines.md +108 -108
  215. package/.agents/skills/domains/web/design-analysis/rules/workflow-element-extraction.md +162 -162
  216. package/.agents/skills/domains/web/design-analysis/rules/workflow-layout-map.md +131 -131
  217. package/.agents/skills/domains/web/design-analysis/rules/workflow-output-checklist.md +70 -70
  218. package/.agents/skills/domains/web/design-analysis/rules/workflow-style-summary.md +91 -91
  219. package/.agents/skills/domains/web/route-permission-map/SKILL.md +103 -103
  220. package/.agents/skills/domains/web/ui-verification/SKILL.md +114 -114
  221. package/.agents/skills/domains/web/ui-verification/evals/evals.json +16 -16
  222. package/.agents/skills/domains/web/ui-verification/evals/train_queries.json +18 -18
  223. package/.agents/skills/domains/web/ui-verification/evals/validation_queries.json +18 -18
  224. package/.agents/skills/domains/web/ui-verification/rules/comparison-content-image.md +34 -34
  225. package/.agents/skills/domains/web/ui-verification/rules/comparison-content-text.md +30 -30
  226. package/.agents/skills/domains/web/ui-verification/rules/comparison-hierarchy.md +33 -33
  227. package/.agents/skills/domains/web/ui-verification/rules/comparison-layout.md +35 -35
  228. package/.agents/skills/domains/web/ui-verification/rules/errors-alignment.md +42 -42
  229. package/.agents/skills/domains/web/ui-verification/rules/errors-button-dimensions.md +28 -28
  230. package/.agents/skills/domains/web/ui-verification/rules/errors-button-position.md +25 -25
  231. package/.agents/skills/domains/web/ui-verification/rules/errors-css-priority.md +50 -50
  232. package/.agents/skills/domains/web/ui-verification/rules/errors-flex-column-width.md +46 -46
  233. package/.agents/skills/domains/web/ui-verification/rules/errors-flex-layout.md +46 -46
  234. package/.agents/skills/domains/web/ui-verification/rules/errors-grid-container-width.md +44 -44
  235. package/.agents/skills/domains/web/ui-verification/rules/errors-page-container-width.md +39 -39
  236. package/.agents/skills/domains/web/ui-verification/rules/tools-browser-navigation.md +53 -53
  237. package/.agents/skills/domains/web/ui-verification/rules/tools-design-guidelines.md +53 -53
  238. package/.agents/skills/domains/web/ui-verification/rules/workflow-checklist.md +27 -27
  239. package/.agents/skills/domains/web/ui-verification/rules/workflow-problem-list.md +56 -56
  240. package/.agents/skills/domains/web/ui-verification/rules/workflow-reflection.md +44 -44
  241. package/.agents/skills/domains/web/ui-verification/rules/writing-alignment.md +44 -44
  242. package/.agents/skills/domains/web/ui-verification/rules/writing-element-completeness.md +63 -63
  243. package/.agents/skills/domains/web/ui-verification/rules/writing-list-layout.md +75 -75
  244. package/.agents/skills/domains/web/ui-verification/rules/writing-page-container-width.md +37 -37
  245. package/.agents/skills/domains/web/web-design-guidelines/SKILL.md +40 -40
  246. package/.agents/skills/profiles/nestjs/README.md +4 -4
  247. package/.agents/skills/profiles/node-tooling/README.md +9 -9
  248. package/.agents/skills/profiles/react/create-api/SKILL.md +145 -145
  249. package/.agents/skills/profiles/react/create-component/SKILL.md +160 -160
  250. package/.agents/skills/profiles/react/create-route/SKILL.md +168 -168
  251. package/.agents/skills/profiles/react/create-store/SKILL.md +262 -262
  252. package/.agents/skills/profiles/react/theme-variables/SKILL.md +82 -82
  253. package/.agents/skills/profiles/react/vercel-composition-patterns/AGENTS.md +899 -899
  254. package/.agents/skills/profiles/react/vercel-composition-patterns/SKILL.md +81 -81
  255. package/.agents/skills/profiles/react/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md +100 -100
  256. package/.agents/skills/profiles/react/vercel-composition-patterns/rules/architecture-compound-components.md +112 -112
  257. package/.agents/skills/profiles/react/vercel-composition-patterns/rules/patterns-children-over-render-props.md +87 -87
  258. package/.agents/skills/profiles/react/vercel-composition-patterns/rules/patterns-explicit-variants.md +100 -100
  259. package/.agents/skills/profiles/react/vercel-composition-patterns/rules/state-context-interface.md +191 -191
  260. package/.agents/skills/profiles/react/vercel-composition-patterns/rules/state-decouple-implementation.md +113 -113
  261. package/.agents/skills/profiles/react/vercel-composition-patterns/rules/state-lift-state.md +125 -125
  262. package/.agents/skills/profiles/react/vercel-react-best-practices/AGENTS.md +2934 -2934
  263. package/.agents/skills/profiles/react/vercel-react-best-practices/SKILL.md +136 -136
  264. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -55
  265. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/advanced-init-once.md +42 -42
  266. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/advanced-use-latest.md +39 -39
  267. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/async-api-routes.md +38 -38
  268. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/async-defer-await.md +80 -80
  269. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/async-dependencies.md +51 -51
  270. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/async-parallel.md +28 -28
  271. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -99
  272. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -59
  273. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/bundle-conditional.md +31 -31
  274. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -49
  275. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -35
  276. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/bundle-preload.md +50 -50
  277. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/client-event-listeners.md +74 -74
  278. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -71
  279. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -48
  280. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/client-swr-dedup.md +56 -56
  281. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -107
  282. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-cache-function-results.md +80 -80
  283. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-cache-property-access.md +28 -28
  284. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-cache-storage.md +70 -70
  285. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-combine-iterations.md +32 -32
  286. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-early-exit.md +50 -50
  287. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -45
  288. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-index-maps.md +37 -37
  289. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-length-check-first.md +49 -49
  290. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-min-max-loop.md +82 -82
  291. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -24
  292. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -57
  293. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-activity.md +26 -26
  294. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -47
  295. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -40
  296. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -38
  297. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -46
  298. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -82
  299. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -30
  300. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -28
  301. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -75
  302. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -39
  303. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-dependencies.md +45 -45
  304. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -40
  305. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-derived-state.md +29 -29
  306. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -74
  307. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -58
  308. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -38
  309. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-memo.md +44 -44
  310. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -45
  311. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -35
  312. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-transitions.md +40 -40
  313. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -73
  314. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -73
  315. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/server-auth-actions.md +96 -96
  316. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/server-cache-lru.md +41 -41
  317. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/server-cache-react.md +76 -76
  318. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/server-dedup-props.md +65 -65
  319. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -83
  320. package/.agents/skills/profiles/react/vercel-react-best-practices/rules/server-serialization.md +38 -38
  321. package/.agents/skills/profiles/springboot/README.md +10 -10
  322. package/.agents/skills/profiles/vue/create-api/SKILL.md +105 -105
  323. package/.agents/skills/profiles/vue/create-component/SKILL.md +76 -76
  324. package/.agents/skills/profiles/vue/create-route/SKILL.md +141 -141
  325. package/.agents/skills/profiles/vue/create-store/SKILL.md +97 -97
  326. package/.agents/skills/profiles/vue/create-view/SKILL.md +81 -81
  327. package/.agents/skills/profiles/vue/theme-variables/SKILL.md +73 -73
  328. package/.agents/skills/profiles/vue/vue-best-practices/SKILL.md +166 -166
  329. package/.agents/skills/profiles/vue/vue-best-practices/references/animation-class-based-technique.md +254 -254
  330. package/.agents/skills/profiles/vue/vue-best-practices/references/animation-state-driven-technique.md +291 -291
  331. package/.agents/skills/profiles/vue/vue-best-practices/references/component-async.md +97 -97
  332. package/.agents/skills/profiles/vue/vue-best-practices/references/component-data-flow.md +307 -307
  333. package/.agents/skills/profiles/vue/vue-best-practices/references/component-fallthrough-attrs.md +174 -174
  334. package/.agents/skills/profiles/vue/vue-best-practices/references/component-keep-alive.md +137 -137
  335. package/.agents/skills/profiles/vue/vue-best-practices/references/component-slots.md +216 -216
  336. package/.agents/skills/profiles/vue/vue-best-practices/references/component-suspense.md +228 -228
  337. package/.agents/skills/profiles/vue/vue-best-practices/references/component-teleport.md +108 -108
  338. package/.agents/skills/profiles/vue/vue-best-practices/references/component-transition-group.md +128 -128
  339. package/.agents/skills/profiles/vue/vue-best-practices/references/component-transition.md +125 -125
  340. package/.agents/skills/profiles/vue/vue-best-practices/references/composables.md +290 -290
  341. package/.agents/skills/profiles/vue/vue-best-practices/references/directives.md +162 -162
  342. package/.agents/skills/profiles/vue/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +159 -159
  343. package/.agents/skills/profiles/vue/vue-best-practices/references/perf-v-once-v-memo-directives.md +182 -182
  344. package/.agents/skills/profiles/vue/vue-best-practices/references/perf-virtualize-large-lists.md +187 -187
  345. package/.agents/skills/profiles/vue/vue-best-practices/references/plugins.md +166 -166
  346. package/.agents/skills/profiles/vue/vue-best-practices/references/reactivity.md +344 -344
  347. package/.agents/skills/profiles/vue/vue-best-practices/references/render-functions.md +201 -201
  348. package/.agents/skills/profiles/vue/vue-best-practices/references/sfc.md +310 -310
  349. package/.agents/skills/profiles/vue/vue-best-practices/references/state-management.md +135 -135
  350. package/.agents/skills/profiles/vue/vue-best-practices/references/updated-hook-performance.md +187 -187
  351. package/.agents/templates/common/README.md +23 -23
  352. package/.agents/templates/common/bugfix.md +22 -22
  353. package/.agents/templates/common/create-expert-package.md +458 -458
  354. package/.agents/templates/common/mock-page.md +28 -28
  355. package/.agents/templates/common/new-component.md +25 -25
  356. package/.agents/templates/common/new-page.md +31 -31
  357. package/.cursor/mcp.json +35 -35
  358. package/.qoder/mcp.json +26 -26
  359. package/bin/archive-change.js +560 -474
  360. package/bin/check-command.js +62 -62
  361. package/bin/cli.js +0 -0
  362. package/bin/command-template-renderer.js +40 -40
  363. package/bin/context-command.js +102 -102
  364. package/bin/demo-runtime-smoke.js +760 -760
  365. package/bin/execution-semantics.js +821 -821
  366. package/bin/executor-command.js +93 -93
  367. package/bin/expert-dispatch.js +334 -334
  368. package/bin/expert-executor.js +1148 -1148
  369. package/bin/guard-command.js +52 -52
  370. package/bin/hub-command.js +876 -876
  371. package/bin/ide-command.js +242 -242
  372. package/bin/init-command.js +193 -193
  373. package/bin/install-workflow.js +35 -3
  374. package/bin/manifest-export.js +34 -34
  375. package/bin/profile-registry.js +90 -90
  376. package/bin/protocol-workflow.js +452 -446
  377. package/bin/repair-command.js +161 -161
  378. package/bin/repo-map.js +177 -177
  379. package/bin/report-command.js +236 -236
  380. package/bin/runtime-bootstrap.js +428 -428
  381. package/bin/runtime-embedded.js +101 -101
  382. package/bin/runtime-fallback.js +106 -106
  383. package/bin/runtime-launcher.js +116 -116
  384. package/bin/runtime-paths.js +177 -177
  385. package/bin/runtime-registry.js +289 -289
  386. package/bin/runtime-state.js +2541 -2541
  387. package/bin/scan.js +96 -96
  388. package/bin/self-upgrade.js +206 -206
  389. package/bin/skill-spec-validator.js +457 -457
  390. package/bin/spec-command.js +366 -366
  391. package/bin/superpowers.js +384 -384
  392. package/bin/sync-command.js +59 -59
  393. package/bin/sync.js +1904 -1904
  394. package/bin/task-orchestrator-adapter.js +341 -341
  395. package/bin/task-orchestrator-extractor.js +274 -274
  396. package/bin/task-orchestrator-runner.js +1208 -1208
  397. package/bin/telemetry/README.md +66 -66
  398. package/bin/telemetry/aspect.js +153 -153
  399. package/bin/telemetry/collect.js +67 -67
  400. package/bin/telemetry/config.js +114 -114
  401. package/bin/telemetry/defaults.json +5 -5
  402. package/bin/telemetry/healthcheck.js +195 -195
  403. package/bin/telemetry/identity.js +53 -53
  404. package/bin/telemetry/index.js +25 -25
  405. package/bin/telemetry/reporter.js +83 -83
  406. package/bin/telemetry/safe.js +39 -39
  407. package/bin/validate-registry.js +740 -740
  408. package/bin/visual-bridge-config.js +117 -117
  409. package/bin/visual-bridge.js +287 -287
  410. package/bin/visual-command.js +432 -432
  411. package/bin/worktree-command.js +194 -194
  412. package/configs/common/.editorconfig +15 -15
  413. package/configs/common/.husky/commit-msg +4 -4
  414. package/configs/common/.husky/pre-commit +4 -4
  415. package/configs/common/.lintstagedrc +11 -11
  416. package/configs/common/.prettierignore +11 -11
  417. package/configs/common/.prettierrc.json +11 -11
  418. package/configs/common/.stylelintignore +14 -14
  419. package/configs/common/.stylelintrc.json +21 -21
  420. package/configs/common/commitlint.config.js +3 -3
  421. package/configs/profiles/nestjs/.gitkeep +1 -1
  422. package/configs/profiles/node-tooling/.gitkeep +1 -1
  423. package/configs/profiles/react/.eslintignore +6 -6
  424. package/configs/profiles/react/.eslintrc.js +16 -16
  425. package/configs/profiles/react/.stylelintrc.json +18 -18
  426. package/configs/profiles/springboot/.gitkeep +1 -1
  427. package/configs/profiles/vue/.eslintignore +6 -6
  428. package/configs/profiles/vue/.eslintrc.cjs +17 -17
  429. package/contracts/README.md +28 -28
  430. package/contracts/fixtures/asset-package.fixture.json +26 -26
  431. package/contracts/fixtures/asset-usage-feedback.fixture.json +14 -14
  432. package/contracts/fixtures/evidence-report.fixture.json +28 -28
  433. package/contracts/fixtures/manifest.fixture.json +20 -20
  434. package/contracts/fixtures/run-event.fixture.json +15 -15
  435. package/contracts/schemas/asset-package.schema.json +76 -76
  436. package/contracts/schemas/asset-usage-feedback.schema.json +57 -57
  437. package/contracts/schemas/evidence-report.schema.json +60 -60
  438. package/contracts/schemas/manifest.schema.json +63 -63
  439. package/contracts/schemas/run-event.schema.json +72 -72
  440. package/install.ps1 +35 -35
  441. package/install.sh +17 -17
  442. package/internal/ai-protocol-workflow.js +5824 -5600
  443. package/internal/hub-client.js +98 -98
  444. package/internal/hub-sync-selection.js +69 -69
  445. package/internal/visual-hooks/README.md +481 -481
  446. package/internal/visual-hooks/config-loader.js +218 -218
  447. package/internal/visual-hooks/control-puller.js +206 -206
  448. package/internal/visual-hooks/gate-signal.js +150 -150
  449. package/internal/visual-hooks/inbox-consumer.js +469 -469
  450. package/internal/visual-hooks/index.js +197 -197
  451. package/internal/visual-hooks/push-client.js +189 -189
  452. package/internal/visual-hooks/receipt-pusher.js +176 -176
  453. package/internal/visual-hooks/runtime-state-pusher.js +128 -128
  454. package/openspec/config.yaml.template +52 -52
  455. package/openspec/schemas/expert-delivery/schema.yaml +68 -68
  456. package/openspec/schemas/expert-delivery/templates/checklist.md +39 -39
  457. package/openspec/schemas/expert-delivery/templates/design.md +61 -61
  458. package/openspec/schemas/expert-delivery/templates/iterations.md +25 -25
  459. package/openspec/schemas/expert-delivery/templates/proposal.md +45 -45
  460. package/openspec/schemas/expert-delivery/templates/spec.md +29 -29
  461. package/openspec/schemas/expert-delivery/templates/tasks.md +24 -24
  462. package/package.json +1 -1
  463. package/scripts/acceptance-zero-intrusion.sh +168 -168
  464. package/scripts/hub-sync-assets.config.example.json +296 -296
  465. package/scripts/hub-sync-assets.js +2038 -2038
  466. package/scripts/local-verify.sh +280 -280
  467. package/scripts/post-publish-auto-fix-check.js +404 -404
  468. package/scripts/post-publish-verify.sh +175 -175
  469. package/scripts/setup-cursor-manual-test.sh +107 -107
  470. package/scripts/setup-cursor-spec-archive-test.sh +111 -111
  471. package/scripts/setup-visual-integration.sh +225 -225
  472. package/scripts/test-integration.sh +176 -176
  473. package/scripts/update-test-project.sh +93 -93
  474. package/scripts/upload-four-web.sh +57 -57
  475. package/scripts/verify-install-ps1-bom.js +26 -26
  476. package/src/agent/agent-context.js +259 -259
  477. package/src/agent/agent-profile.js +185 -185
  478. package/src/agent/agent-templates.js +161 -161
  479. package/src/agent/agent-types.js +108 -108
  480. package/src/agent/collaboration-protocol.js +333 -333
  481. package/src/agent/conflict-handler.js +364 -364
  482. package/src/agent/file-permission.js +121 -121
  483. package/src/agent/index.js +38 -38
  484. package/src/agent/permission-audit.js +151 -151
  485. package/src/agent/review-repair-loop.js +270 -270
  486. package/src/agent/tool-permission.js +101 -101
  487. package/src/asset/asset-dependency.js +322 -322
  488. package/src/asset/asset-feedback.js +350 -350
  489. package/src/asset/asset-fork.js +300 -300
  490. package/src/asset/asset-install.js +278 -278
  491. package/src/asset/asset-installer.js +497 -497
  492. package/src/asset/asset-lifecycle.js +324 -324
  493. package/src/asset/asset-manager.js +245 -245
  494. package/src/asset/asset-package-manager.js +349 -349
  495. package/src/asset/asset-package.js +186 -186
  496. package/src/asset/asset-quality.js +262 -262
  497. package/src/asset/asset-registry.js +387 -387
  498. package/src/asset/asset-version.js +293 -293
  499. package/src/asset/index.js +86 -86
  500. package/src/cache/agent-profile-cache.js +59 -59
  501. package/src/cache/asset-cache.js +63 -63
  502. package/src/cache/global-cache.js +61 -61
  503. package/src/cache/manifest-cache.js +30 -30
  504. package/src/check/check-service.js +32 -32
  505. package/src/config/config-layer.js +343 -343
  506. package/src/config/config-loader.js +60 -60
  507. package/src/config/defaults.js +49 -49
  508. package/src/connectors/hub/asset-package.js +72 -72
  509. package/src/connectors/hub/asset-usage-feedback.js +46 -46
  510. package/src/connectors/hub/hub-connector.js +44 -44
  511. package/src/connectors/hub/index.js +21 -21
  512. package/src/connectors/visual/evidence-report.js +49 -49
  513. package/src/connectors/visual/index.js +15 -15
  514. package/src/connectors/visual/queue.js +41 -41
  515. package/src/connectors/visual/run-event.js +81 -81
  516. package/src/connectors/visual/visual-connector.js +77 -77
  517. package/src/context/context-budget.js +59 -59
  518. package/src/context/context-builder.js +285 -285
  519. package/src/context/context-loader.js +116 -116
  520. package/src/context/context-planner.js +158 -158
  521. package/src/context/types.js +96 -96
  522. package/src/contracts/index.js +63 -63
  523. package/src/executor/executor-registry.js +78 -78
  524. package/src/executor/executor-result-parser.js +44 -44
  525. package/src/executor/executor-runner.js +141 -141
  526. package/src/executor/executor-selector.js +139 -139
  527. package/src/executor/executor-timeout.js +36 -36
  528. package/src/executor/providers/base-provider-utils.js +189 -189
  529. package/src/executor/providers/claude-code-executor-provider.js +128 -128
  530. package/src/executor/providers/codex-executor-provider.js +126 -126
  531. package/src/executor/providers/cursor-executor-provider.js +99 -99
  532. package/src/executor/types.js +137 -137
  533. package/src/git/branch-manager.js +71 -71
  534. package/src/git/dirty-checker.js +43 -43
  535. package/src/git/dirty-strategy-handler.js +29 -29
  536. package/src/git/git-command.js +37 -37
  537. package/src/git/git-repository-detector.js +45 -45
  538. package/src/git/multi-repo-worktree-planner.js +88 -88
  539. package/src/git/policy.js +19 -19
  540. package/src/git/strategies/block-dirty-strategy.js +34 -34
  541. package/src/git/strategies/ignore-dirty-strategy.js +33 -33
  542. package/src/git/strategies/patch-snapshot-strategy.js +53 -53
  543. package/src/git/strategies/wip-commit-strategy.js +38 -38
  544. package/src/git/types.js +71 -71
  545. package/src/git/worktree-manager.js +85 -85
  546. package/src/governance/asset-review.js +351 -351
  547. package/src/governance/audit-log.js +368 -368
  548. package/src/governance/gray-release.js +312 -312
  549. package/src/governance/index.js +31 -31
  550. package/src/governance/policy-types.js +56 -56
  551. package/src/governance/rbac-types.js +171 -171
  552. package/src/governance/rbac.js +382 -382
  553. package/src/governance/rollback.js +360 -360
  554. package/src/governance/security-policy.js +354 -354
  555. package/src/hook/hook-config-writer.js +125 -125
  556. package/src/hub/hub-client.js +186 -186
  557. package/src/hub/hub-config.js +39 -39
  558. package/src/hub/project-facts.js +31 -31
  559. package/src/hub/runtime-feedback-reporter.js +55 -55
  560. package/src/ide/adapters/adapter-protocol.js +385 -385
  561. package/src/ide/adapters/claude-adapter.js +419 -419
  562. package/src/ide/adapters/codex-adapter.js +60 -60
  563. package/src/ide/adapters/cursor-adapter.js +484 -484
  564. package/src/ide/adapters/index.js +24 -24
  565. package/src/ide/anchors/markdown-anchor-writer.js +152 -152
  566. package/src/ide/ide-service.js +270 -270
  567. package/src/ide/ide-types.js +94 -94
  568. package/src/ide/links/link-mode-resolver.js +160 -160
  569. package/src/ide/registry/ide-registry-builder.js +165 -165
  570. package/src/incident/incident-writer.js +47 -47
  571. package/src/incident/types.js +22 -22
  572. package/src/init/ide-linker.js +126 -126
  573. package/src/init/ide-pointer-injector.js +75 -75
  574. package/src/init/init-applier.js +197 -197
  575. package/src/init/init-plan.js +294 -294
  576. package/src/init/init-service.js +65 -65
  577. package/src/init/manifest-installer.js +302 -302
  578. package/src/init/types.js +26 -26
  579. package/src/project/config-writer.js +83 -83
  580. package/src/project/context-index-writer.js +82 -82
  581. package/src/project/json-utils.js +72 -72
  582. package/src/project/local-state-writer.js +50 -50
  583. package/src/project/lock-file-writer.js +98 -98
  584. package/src/project/manifest-writer.js +126 -126
  585. package/src/project/policy-config-writer.js +91 -91
  586. package/src/project/project-config-writer.js +74 -74
  587. package/src/project/project-files.js +39 -39
  588. package/src/project/registry-index-writer.js +43 -43
  589. package/src/project/workspace-config-writer.js +63 -63
  590. package/src/run/index.js +11 -11
  591. package/src/run/run-id.js +32 -32
  592. package/src/run/run-service.js +269 -269
  593. package/src/run/run-store.js +80 -80
  594. package/src/scanner/aggregator/detection-aggregator.js +23 -23
  595. package/src/scanner/boundary/boundary-resolver.js +229 -229
  596. package/src/scanner/detectors/detector-registry.js +44 -44
  597. package/src/scanner/detectors/fastapi-detector.js +46 -46
  598. package/src/scanner/detectors/go-detector.js +46 -46
  599. package/src/scanner/detectors/nestjs-detector.js +57 -57
  600. package/src/scanner/detectors/nextjs-detector.js +52 -52
  601. package/src/scanner/detectors/react-vite-detector.js +52 -52
  602. package/src/scanner/detectors/react-webpack-detector.js +57 -57
  603. package/src/scanner/detectors/springboot-detector.js +46 -46
  604. package/src/scanner/detectors/springcloud-detector.js +46 -46
  605. package/src/scanner/detectors/springmvc-detector.js +46 -46
  606. package/src/scanner/detectors/vue-vite-detector.js +52 -52
  607. package/src/scanner/engine.js +72 -72
  608. package/src/scanner/facts/fact-extractor.js +211 -211
  609. package/src/scanner/types.js +30 -30
  610. package/src/security/asset-tamper-checker.js +188 -188
  611. package/src/security/checksum.js +40 -40
  612. package/src/spec/spec-writer.js +302 -302
  613. package/src/state-machine/circuit-breaker.js +112 -112
  614. package/src/state-machine/escape-hatch.js +49 -49
  615. package/src/state-machine/stage-runner.js +281 -281
  616. package/src/state-machine/state-machine.js +24 -24
  617. package/src/state-machine/transition-guard.js +36 -36
  618. package/src/state-machine/types.js +37 -37
  619. package/src/sync/sync-service.js +192 -192
  620. package/src/visual/agent-visual.js +142 -142
  621. package/src/visual/event-gateway.js +357 -357
  622. package/src/visual/event-mapper.js +128 -128
  623. package/src/visual/hook-dashboard.js +216 -216
  624. package/src/visual/index.js +27 -27
  625. package/src/visual/metrics.js +287 -287
  626. package/src/visual/privacy-filter.js +100 -100
  627. package/src/visual/risk-board.js +252 -252
  628. package/src/visual/timeline.js +245 -245
  629. package/src/visual/visual-client.js +94 -94
  630. package/src/visual/visual-config.js +40 -40
  631. 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();