@open-mercato/cli 0.6.7-develop.6863.1.61001cf9a8 → 0.6.7

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 (278) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/build.mjs +7 -24
  3. package/dist/agentic/claude-code/hooks/entity-migration-check.ts +88 -32
  4. package/dist/agentic/codex/enforcement-rules.md +16 -2
  5. package/dist/agentic/cursor/hooks/entity-migration-check.mjs +50 -19
  6. package/dist/agentic/cursor/rules/entity-guard.mdc +21 -9
  7. package/dist/agentic/cursor/rules/generated-guard.mdc +5 -3
  8. package/dist/agentic/cursor/rules/open-mercato.mdc +29 -3
  9. package/dist/agentic/guides/cache.md +45 -0
  10. package/dist/agentic/guides/core.md +453 -0
  11. package/dist/agentic/guides/events.md +117 -0
  12. package/dist/agentic/guides/module-facts.json +3833 -115419
  13. package/dist/agentic/guides/module-system.md +127 -12
  14. package/dist/agentic/guides/modules/ai_assistant.md +28 -227
  15. package/dist/agentic/guides/modules/api_docs.md +2 -58
  16. package/dist/agentic/guides/modules/api_keys.md +7 -92
  17. package/dist/agentic/guides/modules/attachments.md +14 -119
  18. package/dist/agentic/guides/modules/audit_logs.md +10 -131
  19. package/dist/agentic/guides/modules/auth.md +33 -276
  20. package/dist/agentic/guides/modules/business_rules.md +14 -149
  21. package/dist/agentic/guides/modules/catalog.md +43 -475
  22. package/dist/agentic/guides/modules/channel_gmail.md +3 -82
  23. package/dist/agentic/guides/modules/channel_imap.md +3 -82
  24. package/dist/agentic/guides/modules/checkout.md +27 -299
  25. package/dist/agentic/guides/modules/communication_channels.md +33 -315
  26. package/dist/agentic/guides/modules/configs.md +8 -104
  27. package/dist/agentic/guides/modules/content.md +2 -57
  28. package/dist/agentic/guides/modules/currencies.md +17 -152
  29. package/dist/agentic/guides/modules/customer_accounts.md +45 -387
  30. package/dist/agentic/guides/modules/customers.md +85 -746
  31. package/dist/agentic/guides/modules/dashboards.md +10 -155
  32. package/dist/agentic/guides/modules/data_sync.md +16 -149
  33. package/dist/agentic/guides/modules/dictionaries.md +12 -107
  34. package/dist/agentic/guides/modules/directory.md +15 -148
  35. package/dist/agentic/guides/modules/entities.md +13 -145
  36. package/dist/agentic/guides/modules/events.md +2 -59
  37. package/dist/agentic/guides/modules/feature_toggles.md +9 -119
  38. package/dist/agentic/guides/modules/gateway_stripe.md +4 -92
  39. package/dist/agentic/guides/modules/generators.md +2 -45
  40. package/dist/agentic/guides/modules/inbox_ops.md +27 -207
  41. package/dist/agentic/guides/modules/integrations.md +16 -146
  42. package/dist/agentic/guides/modules/messages.md +24 -223
  43. package/dist/agentic/guides/modules/notifications.md +8 -96
  44. package/dist/agentic/guides/modules/onboarding.md +6 -77
  45. package/dist/agentic/guides/modules/payment_gateways.md +21 -164
  46. package/dist/agentic/guides/modules/perspectives.md +7 -77
  47. package/dist/agentic/guides/modules/planner.md +18 -136
  48. package/dist/agentic/guides/modules/portal.md +2 -93
  49. package/dist/agentic/guides/modules/progress.md +14 -104
  50. package/dist/agentic/guides/modules/query_index.md +15 -154
  51. package/dist/agentic/guides/modules/record_locks.md +21 -206
  52. package/dist/agentic/guides/modules/resources.md +30 -197
  53. package/dist/agentic/guides/modules/sales.md +84 -678
  54. package/dist/agentic/guides/modules/scheduler.md +14 -133
  55. package/dist/agentic/guides/modules/search.md +6 -170
  56. package/dist/agentic/guides/modules/security.md +35 -294
  57. package/dist/agentic/guides/modules/shipping_carriers.md +18 -134
  58. package/dist/agentic/guides/modules/sso.md +26 -151
  59. package/dist/agentic/guides/modules/staff.md +57 -369
  60. package/dist/agentic/guides/modules/storage_s3.md +5 -95
  61. package/dist/agentic/guides/modules/sync_akeneo.md +3 -85
  62. package/dist/agentic/guides/modules/sync_excel.md +6 -75
  63. package/dist/agentic/guides/modules/system_status_overlays.md +2 -60
  64. package/dist/agentic/guides/modules/translations.md +10 -104
  65. package/dist/agentic/guides/modules/webhooks.md +24 -203
  66. package/dist/agentic/guides/modules/wms.md +46 -441
  67. package/dist/agentic/guides/modules/workflows.md +43 -302
  68. package/dist/agentic/guides/queue.md +53 -0
  69. package/dist/agentic/guides/search.md +115 -0
  70. package/dist/agentic/guides/shared.md +108 -0
  71. package/dist/agentic/guides/ui.md +347 -0
  72. package/dist/agentic/shared/AGENTS.md.template +311 -124
  73. package/dist/agentic/shared/ai/agentic.config.json +0 -2
  74. package/dist/agentic/shared/ai/lessons.md +3 -26
  75. package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +27 -0
  76. package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +27 -0
  77. package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +0 -4
  78. package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +241 -20
  79. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +260 -0
  80. package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +601 -16
  81. package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +146 -0
  82. package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +310 -14
  83. package/dist/agentic/shared/ai/skills/om-help/SKILL.md +97 -15
  84. package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +84 -0
  85. package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +193 -0
  86. package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +152 -33
  87. package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +708 -24
  88. package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +34 -0
  89. package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +762 -0
  90. package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +820 -20
  91. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +50 -0
  92. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +97 -0
  93. package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +843 -19
  94. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +271 -0
  95. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +63 -15
  96. package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +467 -16
  97. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +101 -0
  98. package/dist/agentic/shared/ai/skills/tiers.json +21 -143
  99. package/dist/agentic/shared/ai/skills/tiers.schema.json +40 -52
  100. package/dist/agentic/shared/ai/specs/README.md +3 -4
  101. package/dist/agentic/shared/ai/specs/SPEC-000-template.md +10 -226
  102. package/dist/agentic/shared/ai/trackers/github.md +4 -108
  103. package/dist/agentic/shared/scripts/install-skills.sh +694 -4
  104. package/dist/bin.js +1 -13
  105. package/dist/bin.js.map +2 -2
  106. package/dist/lib/__integration__/TC-INT-008.spec.js +8 -88
  107. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  108. package/dist/lib/agentic-init.js +3 -13
  109. package/dist/lib/agentic-init.js.map +2 -2
  110. package/dist/lib/agentic-setup.js +186 -418
  111. package/dist/lib/agentic-setup.js.map +2 -2
  112. package/dist/lib/generators/extensions/injection-widgets.js +0 -20
  113. package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
  114. package/dist/lib/generators/module-facts-discovery.js +7 -41
  115. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  116. package/dist/lib/generators/module-facts.js +45 -1387
  117. package/dist/lib/generators/module-facts.js.map +3 -3
  118. package/dist/lib/generators/module-registry.js +5 -11
  119. package/dist/lib/generators/module-registry.js.map +2 -2
  120. package/dist/lib/resolver.js +0 -2
  121. package/dist/lib/resolver.js.map +2 -2
  122. package/dist/lib/testing/integration.js +0 -5
  123. package/dist/lib/testing/integration.js.map +2 -2
  124. package/dist/mercato.js +1 -23
  125. package/dist/mercato.js.map +2 -2
  126. package/package.json +11 -13
  127. package/src/bin.ts +1 -33
  128. package/src/lib/__integration__/TC-INT-008.spec.ts +9 -107
  129. package/src/lib/__tests__/agentic-init.test.ts +5 -41
  130. package/src/lib/agentic-init.ts +3 -15
  131. package/src/lib/agentic-setup.ts +224 -530
  132. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +2 -88
  133. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +0 -1
  134. package/src/lib/generators/__tests__/module-facts.discovery.test.ts +10 -54
  135. package/src/lib/generators/__tests__/module-subset.test.ts +0 -18
  136. package/src/lib/generators/extensions/injection-widgets.ts +0 -51
  137. package/src/lib/generators/module-facts-discovery.ts +7 -58
  138. package/src/lib/generators/module-facts.ts +46 -1851
  139. package/src/lib/generators/module-registry.ts +6 -16
  140. package/src/lib/resolver.ts +0 -3
  141. package/src/lib/testing/__tests__/integration.test.ts +0 -4
  142. package/src/lib/testing/integration.ts +0 -5
  143. package/src/mercato.ts +1 -26
  144. package/dist/agentic/guides/ai-workflows.md +0 -76
  145. package/dist/agentic/guides/architecture.md +0 -107
  146. package/dist/agentic/guides/backend-ui.md +0 -80
  147. package/dist/agentic/guides/contracts.md +0 -92
  148. package/dist/agentic/guides/extensions.md +0 -121
  149. package/dist/agentic/guides/framework-extension-points.md +0 -36
  150. package/dist/agentic/guides/integrations.md +0 -81
  151. package/dist/agentic/guides/modules/design_system.md +0 -102
  152. package/dist/agentic/guides/spec-delivery.md +0 -22
  153. package/dist/agentic/guides/testing-debugging.md +0 -80
  154. package/dist/agentic/guides/upstream/AGENTS.md +0 -298
  155. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +0 -316
  156. package/dist/agentic/guides/upstream/manifest.json +0 -8
  157. package/dist/agentic/shared/ai/harness/README.md +0 -50
  158. package/dist/agentic/shared/ai/harness/RELEASE.md +0 -57
  159. package/dist/agentic/shared/ai/harness/cases.json +0 -13680
  160. package/dist/agentic/shared/ai/harness/cases.schema.json +0 -133
  161. package/dist/agentic/shared/ai/harness/fixtures/index.json +0 -49
  162. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +0 -162
  163. package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +0 -21
  164. package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +0 -96
  165. package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +0 -172
  166. package/dist/agentic/shared/ai/harness/release-matrix.json +0 -158
  167. package/dist/agentic/shared/ai/harness/release-result.schema.json +0 -219
  168. package/dist/agentic/shared/ai/harness/result.schema.json +0 -76
  169. package/dist/agentic/shared/ai/harness/routing-response.schema.json +0 -12
  170. package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +0 -73
  171. package/dist/agentic/shared/ai/harness/validators.json +0 -78
  172. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +0 -2121
  173. package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +0 -1096
  174. package/dist/agentic/shared/ai/lessons/_template.md +0 -16
  175. package/dist/agentic/shared/ai/review-checklist.md +0 -36
  176. package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +0 -21
  177. package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +0 -98
  178. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +0 -16
  179. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +0 -53
  180. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +0 -13
  181. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +0 -11
  182. package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +0 -30
  183. package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +0 -12
  184. package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +0 -13
  185. package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +0 -14
  186. package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +0 -28
  187. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +0 -12
  188. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +0 -22
  189. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +0 -12
  190. package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +0 -19
  191. package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +0 -19
  192. package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +0 -13
  193. package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +0 -14
  194. package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +0 -10
  195. package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +0 -17
  196. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +0 -29
  197. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +0 -100
  198. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +0 -16
  199. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +0 -17
  200. package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +0 -25
  201. package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +0 -11
  202. package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +0 -13
  203. package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +0 -18
  204. package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +0 -20
  205. package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +0 -33
  206. package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +0 -40
  207. package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +0 -27
  208. package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +0 -24
  209. package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +0 -16
  210. package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +0 -14
  211. package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +0 -15
  212. package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +0 -12
  213. package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +0 -34
  214. package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +0 -4
  215. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +0 -11
  216. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +0 -31
  217. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +0 -39
  218. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +0 -43
  219. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +0 -11
  220. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +0 -19
  221. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +0 -172
  222. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +0 -12
  223. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +0 -83
  224. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +0 -21
  225. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +0 -11
  226. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +0 -22
  227. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +0 -13
  228. package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +0 -41
  229. package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +0 -19
  230. package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +0 -44
  231. package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +0 -39
  232. package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +0 -20
  233. package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +0 -57
  234. package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +0 -11
  235. package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +0 -486
  236. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +0 -18
  237. package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +0 -27
  238. package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +0 -13
  239. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +0 -41
  240. package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +0 -11
  241. package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +0 -15
  242. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +0 -16
  243. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +0 -14
  244. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +0 -187
  245. package/dist/agentic/shared/scripts/check-lessons.mjs +0 -178
  246. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +0 -2958
  247. package/dist/agentic/shared/scripts/execution-sandbox.mjs +0 -329
  248. package/dist/agentic/shared/scripts/framework-context.mjs +0 -888
  249. package/dist/agentic/shared/scripts/install-skills.mjs +0 -1029
  250. package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +0 -222
  251. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +0 -1774
  252. package/dist/lib/generators/module-extension-facts.js +0 -1840
  253. package/dist/lib/generators/module-extension-facts.js.map +0 -7
  254. package/dist/lib/generators/module-fact-sources.js +0 -70
  255. package/dist/lib/generators/module-fact-sources.js.map +0 -7
  256. package/dist/lib/generators/module-override-targets.js +0 -688
  257. package/dist/lib/generators/module-override-targets.js.map +0 -7
  258. package/dist/lib/load-env.js +0 -24
  259. package/dist/lib/load-env.js.map +0 -7
  260. package/dist/lib/telemetry-init.js +0 -358
  261. package/dist/lib/telemetry-init.js.map +0 -7
  262. package/src/lib/__tests__/agentic-setup.ownership.test.ts +0 -386
  263. package/src/lib/__tests__/injection-widget-id-validation.test.ts +0 -184
  264. package/src/lib/__tests__/load-env.test.ts +0 -65
  265. package/src/lib/__tests__/telemetry-init.test.ts +0 -298
  266. package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +0 -92
  267. package/src/lib/generators/__tests__/module-extension-activations.test.ts +0 -762
  268. package/src/lib/generators/__tests__/module-extension-facts.test.ts +0 -404
  269. package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +0 -108
  270. package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +0 -561
  271. package/src/lib/generators/__tests__/module-facts.source-links.test.ts +0 -119
  272. package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +0 -149
  273. package/src/lib/generators/__tests__/module-override-targets.test.ts +0 -304
  274. package/src/lib/generators/module-extension-facts.ts +0 -2365
  275. package/src/lib/generators/module-fact-sources.ts +0 -108
  276. package/src/lib/generators/module-override-targets.ts +0 -863
  277. package/src/lib/load-env.ts +0 -30
  278. package/src/lib/telemetry-init.ts +0 -461
@@ -1,1029 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { createHash, randomUUID } from 'node:crypto'
4
- import {
5
- cpSync,
6
- lstatSync,
7
- mkdirSync,
8
- mkdtempSync,
9
- readFileSync,
10
- readdirSync,
11
- readlinkSync,
12
- realpathSync,
13
- renameSync,
14
- rmdirSync,
15
- rmSync,
16
- symlinkSync,
17
- unlinkSync,
18
- writeFileSync,
19
- } from 'node:fs'
20
- import { tmpdir } from 'node:os'
21
- import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
22
- import { fileURLToPath } from 'node:url'
23
- import { gunzipSync } from 'node:zlib'
24
-
25
- const KNOWN_AGENTS = ['claude-code', 'codex', 'cursor']
26
- const LEGACY_AGENTS = ['claude-code', 'codex']
27
- const AGENT_DIRECTORIES = {
28
- 'claude-code': ['.claude', 'skills'],
29
- codex: ['.codex', 'skills'],
30
- cursor: ['.cursor', 'skills'],
31
- }
32
- const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/
33
- const COMMIT_PATTERN = /^[a-f0-9]{40}$/
34
- const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/
35
- const ARCHIVE_LIMIT_BYTES = 32 * 1024 * 1024
36
- const EXTRACTED_LIMIT_BYTES = 256 * 1024 * 1024
37
- const EXTERNAL_OWNERSHIP_FILE = '.om-external-ownership.json'
38
-
39
- const USAGE = `Usage: install-skills.mjs [options]
40
-
41
- Options:
42
- (no options) Install the default local and external tiers.
43
- --with <csv> Install default local/external tiers plus the named tiers.
44
- --tiers <csv> Install exactly the named local/external tiers.
45
- --all Install every local and external tier.
46
- --legacy-links Also expose skills through .claude/skills and .codex/skills.
47
- --ignore-agents <csv>
48
- Never write the named agent directories.
49
- --no-external Skip the pinned external collection (also OM_SKIP_EXTERNAL_SKILLS=1).
50
- --update Resolve the shared repository's current main commit, pin
51
- its verified skill hashes, then install that exact snapshot.
52
- --list Print the tier and external-skill catalog, then exit.
53
- --clean Remove harness-owned skill links, then exit.
54
- --help, -h Show this message.
55
-
56
- --with, --tiers, and --all are mutually exclusive.`
57
-
58
- function fail(message) {
59
- throw new Error(`install-skills: ${message}`)
60
- }
61
-
62
- function unique(values) {
63
- return [...new Set(values)]
64
- }
65
-
66
- function csv(value) {
67
- return unique(value.split(',').map((entry) => entry.trim()).filter(Boolean))
68
- }
69
-
70
- function isWithin(candidate, root) {
71
- const pathFromRoot = relative(resolve(root), resolve(candidate))
72
- return pathFromRoot === '' || (!pathFromRoot.startsWith(`..${sep}`) && pathFromRoot !== '..' && !isAbsolute(pathFromRoot))
73
- }
74
-
75
- function realInstallerRoot(rootDir) {
76
- const resolved = resolve(rootDir)
77
- const entry = lstatSync(resolved, { throwIfNoEntry: false })
78
- if (!entry || entry.isSymbolicLink() || !entry.isDirectory()) fail(`installer root must be a real directory: ${resolved}`)
79
- return realpathSync(resolved)
80
- }
81
-
82
- function relativeComponents(rootDir, targetPath) {
83
- if (!isWithin(targetPath, rootDir)) fail(`installer-owned path escapes the app root: ${targetPath}`)
84
- const pathFromRoot = relative(resolve(rootDir), resolve(targetPath))
85
- return pathFromRoot ? pathFromRoot.split(sep).filter(Boolean) : []
86
- }
87
-
88
- function assertRealDirectoryComponents(rootDir, targetPath, { allowedLeafLinks = [], requireLeaf = false } = {}) {
89
- const components = relativeComponents(rootDir, targetPath)
90
- let current = resolve(rootDir)
91
- for (let index = 0; index < components.length; index += 1) {
92
- current = join(current, components[index])
93
- const entry = lstatSync(current, { throwIfNoEntry: false })
94
- if (!entry) {
95
- if (requireLeaf) fail(`installer-owned directory is missing: ${current}`)
96
- return
97
- }
98
- if (entry.isSymbolicLink()) {
99
- const isLeaf = index === components.length - 1
100
- const target = symlinkTarget(current)
101
- const allowed = isLeaf && allowedLeafLinks.some((candidate) => resolve(candidate) === resolve(target))
102
- if (allowed) return
103
- fail(`installer-owned path contains a symbolic-link component: ${current}`)
104
- }
105
- if (!entry.isDirectory()) fail(`installer-owned path component is not a directory: ${current}`)
106
- }
107
- }
108
-
109
- function ensureRealDirectory(rootDir, targetPath) {
110
- const components = relativeComponents(rootDir, targetPath)
111
- let current = resolve(rootDir)
112
- for (const component of components) {
113
- assertRealDirectoryComponents(rootDir, current, { requireLeaf: true })
114
- current = join(current, component)
115
- let entry = lstatSync(current, { throwIfNoEntry: false })
116
- if (!entry) {
117
- mkdirSync(current)
118
- entry = lstatSync(current, { throwIfNoEntry: false })
119
- }
120
- if (!entry || entry.isSymbolicLink() || !entry.isDirectory()) {
121
- fail(`installer-owned directory is not a real directory: ${current}`)
122
- }
123
- }
124
- }
125
-
126
- function assertRegularOwnedFile(rootDir, filePath, { optional = true } = {}) {
127
- assertRealDirectoryComponents(rootDir, dirname(filePath), { requireLeaf: true })
128
- const entry = lstatSync(filePath, { throwIfNoEntry: false })
129
- if (!entry) {
130
- if (!optional) fail(`installer-owned file is missing: ${filePath}`)
131
- return
132
- }
133
- if (entry.isSymbolicLink() || !entry.isFile()) fail(`installer-owned file must be a regular file: ${filePath}`)
134
- }
135
-
136
- function assertOwnedPathAbsent(rootDir, targetPath) {
137
- assertRealDirectoryComponents(rootDir, dirname(targetPath), { requireLeaf: true })
138
- if (lstatSync(targetPath, { throwIfNoEntry: false })) fail(`installer temporary path already exists: ${targetPath}`)
139
- }
140
-
141
- function removeInstallerTransactionPath(rootDir, targetPath) {
142
- assertRealDirectoryComponents(rootDir, dirname(targetPath), { requireLeaf: true })
143
- const entry = lstatSync(targetPath, { throwIfNoEntry: false })
144
- if (!entry) return
145
- if (entry.isDirectory() && !entry.isSymbolicLink()) rmSync(targetPath, { recursive: true, force: true })
146
- else unlinkSync(targetPath)
147
- }
148
-
149
- function assertInstallerPathPreflight(rootDir) {
150
- const aiSkillsDir = join(rootDir, '.ai', 'skills')
151
- const canonicalDir = join(rootDir, '.agents', 'skills')
152
- assertRealDirectoryComponents(rootDir, aiSkillsDir, { requireLeaf: true })
153
- assertRealDirectoryComponents(rootDir, join(rootDir, '.agents'))
154
- assertRealDirectoryComponents(rootDir, canonicalDir, { allowedLeafLinks: [aiSkillsDir] })
155
- assertRealDirectoryComponents(rootDir, join(rootDir, '.agents', 'skills-quarantine'))
156
- for (const agent of KNOWN_AGENTS) {
157
- const agentRoot = join(rootDir, AGENT_DIRECTORIES[agent][0])
158
- const agentSkills = join(rootDir, ...AGENT_DIRECTORIES[agent])
159
- assertRealDirectoryComponents(rootDir, agentRoot)
160
- assertRealDirectoryComponents(rootDir, agentSkills, { allowedLeafLinks: [aiSkillsDir, canonicalDir] })
161
- }
162
- }
163
-
164
- function symlinkTarget(linkPath) {
165
- const target = readlinkSync(linkPath)
166
- return resolve(dirname(linkPath), target)
167
- }
168
-
169
- function isHarnessOwnedLink(linkPath, aiSkillsDir, canonicalDir) {
170
- const entry = lstatSync(linkPath, { throwIfNoEntry: false })
171
- if (!entry?.isSymbolicLink()) return false
172
- const target = symlinkTarget(linkPath)
173
- return isWithin(target, aiSkillsDir) || isWithin(target, canonicalDir)
174
- }
175
-
176
- function removeEmptyDirectory(rootDir, path) {
177
- assertRealDirectoryComponents(rootDir, dirname(path), { requireLeaf: true })
178
- const entry = lstatSync(path, { throwIfNoEntry: false })
179
- if (entry?.isDirectory() && !entry.isSymbolicLink() && readdirSync(path).length === 0) {
180
- assertRealDirectoryComponents(rootDir, dirname(path), { requireLeaf: true })
181
- rmdirSync(path)
182
- }
183
- }
184
-
185
- function prepareLinkDirectory(rootDir, path, aiSkillsDir, canonicalDir) {
186
- ensureRealDirectory(rootDir, dirname(path))
187
- assertRealDirectoryComponents(rootDir, dirname(path), { requireLeaf: true })
188
- const entry = lstatSync(path, { throwIfNoEntry: false })
189
- if (entry?.isSymbolicLink()) {
190
- const target = symlinkTarget(path)
191
- if (resolve(target) !== resolve(aiSkillsDir) && resolve(target) !== resolve(canonicalDir)) {
192
- fail(`refusing to replace user-owned link ${path}`)
193
- }
194
- assertRealDirectoryComponents(rootDir, dirname(path), { requireLeaf: true })
195
- unlinkSync(path)
196
- } else if (entry && !entry.isDirectory()) {
197
- fail(`refusing to replace user-owned path ${path}`)
198
- }
199
- ensureRealDirectory(rootDir, path)
200
- }
201
-
202
- function replaceManagedLink(rootDir, linkPath, targetPath, relativeTarget, platform, aiSkillsDir, canonicalDir) {
203
- assertRealDirectoryComponents(rootDir, dirname(linkPath), { requireLeaf: true })
204
- const entry = lstatSync(linkPath, { throwIfNoEntry: false })
205
- if (entry) {
206
- if (!entry.isSymbolicLink() || !isHarnessOwnedLink(linkPath, aiSkillsDir, canonicalDir)) {
207
- fail(`refusing to replace user-owned path ${linkPath}`)
208
- }
209
- if (resolve(symlinkTarget(linkPath)) === resolve(targetPath)) return
210
- assertRealDirectoryComponents(rootDir, dirname(linkPath), { requireLeaf: true })
211
- unlinkSync(linkPath)
212
- }
213
- assertRealDirectoryComponents(rootDir, dirname(linkPath), { requireLeaf: true })
214
- if (platform === 'win32') {
215
- symlinkSync(resolve(targetPath), linkPath, 'junction')
216
- } else {
217
- symlinkSync(relativeTarget, linkPath, 'dir')
218
- }
219
- }
220
-
221
- function cleanManagedLinks(rootDir, directory, aiSkillsDir, canonicalDir, keep = new Set()) {
222
- assertRealDirectoryComponents(rootDir, dirname(directory))
223
- const entry = lstatSync(directory, { throwIfNoEntry: false })
224
- if (!entry) return
225
- if (entry.isSymbolicLink()) {
226
- if (isHarnessOwnedLink(directory, aiSkillsDir, canonicalDir)) {
227
- assertRealDirectoryComponents(rootDir, dirname(directory), { requireLeaf: true })
228
- unlinkSync(directory)
229
- }
230
- return
231
- }
232
- if (!entry.isDirectory()) return
233
- for (const name of readdirSync(directory)) {
234
- const candidate = join(directory, name)
235
- if (!keep.has(name) && isHarnessOwnedLink(candidate, aiSkillsDir, canonicalDir)) {
236
- assertRealDirectoryComponents(rootDir, directory, { requireLeaf: true })
237
- unlinkSync(candidate)
238
- }
239
- }
240
- removeEmptyDirectory(rootDir, directory)
241
- }
242
-
243
- function parseArgs(args, env) {
244
- const options = {
245
- mode: 'default',
246
- tierValues: [],
247
- list: false,
248
- clean: false,
249
- legacyLinks: false,
250
- ignoreAgents: undefined,
251
- update: false,
252
- noExternal: Boolean(env.OM_SKIP_EXTERNAL_SKILLS && env.OM_SKIP_EXTERNAL_SKILLS !== '0'),
253
- }
254
- let selectionFlag
255
- for (let index = 0; index < args.length; index += 1) {
256
- const arg = args[index]
257
- if (arg === '--help' || arg === '-h') return { ...options, help: true }
258
- if (arg === '--list') options.list = true
259
- else if (arg === '--clean') options.clean = true
260
- else if (arg === '--legacy-links') options.legacyLinks = true
261
- else if (arg === '--no-external') options.noExternal = true
262
- else if (arg === '--update') options.update = true
263
- else if (arg === '--all') {
264
- if (selectionFlag && selectionFlag !== '--all') fail('--with, --tiers, and --all are mutually exclusive')
265
- selectionFlag = '--all'
266
- options.mode = 'all'
267
- } else if (arg === '--with' || arg === '--tiers' || arg === '--ignore-agents') {
268
- const value = args[index + 1]
269
- if (!value || value.startsWith('--')) fail(`${arg} requires a comma-separated value`)
270
- index += 1
271
- if (arg === '--ignore-agents') options.ignoreAgents = csv(value)
272
- else {
273
- if (selectionFlag && selectionFlag !== arg) fail('--with, --tiers, and --all are mutually exclusive')
274
- selectionFlag = arg
275
- options.mode = arg === '--with' ? 'with' : 'tiers'
276
- options.tierValues = csv(value)
277
- if (options.tierValues.length === 0) fail(`${arg} requires at least one tier name`)
278
- }
279
- } else if (arg.startsWith('--with=') || arg.startsWith('--tiers=') || arg.startsWith('--ignore-agents=')) {
280
- const [flag, value = ''] = arg.split(/=(.*)/s, 2)
281
- if (flag === '--ignore-agents') options.ignoreAgents = csv(value)
282
- else {
283
- if (selectionFlag && selectionFlag !== flag) fail('--with, --tiers, and --all are mutually exclusive')
284
- selectionFlag = flag
285
- options.mode = flag === '--with' ? 'with' : 'tiers'
286
- options.tierValues = csv(value)
287
- if (options.tierValues.length === 0) fail(`${flag} requires at least one tier name`)
288
- }
289
- } else fail(`unknown option '${arg}'`)
290
- }
291
- if (options.update && options.noExternal) fail('--update and --no-external are mutually exclusive')
292
- return options
293
- }
294
-
295
- function readManifest(rootDir) {
296
- const manifestPath = join(rootDir, '.ai', 'skills', 'tiers.json')
297
- assertRegularOwnedFile(rootDir, manifestPath, { optional: false })
298
- let manifest
299
- try {
300
- manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
301
- } catch (error) {
302
- fail(`cannot parse ${manifestPath}: ${error.message}`)
303
- }
304
- if (!Array.isArray(manifest.default) || manifest.default.length === 0 || !manifest.tiers || typeof manifest.tiers !== 'object') {
305
- fail('manifest requires a non-empty default list and a tiers object')
306
- }
307
- for (const tier of manifest.default) if (!manifest.tiers[tier]) fail(`default tier '${tier}' is not defined`)
308
- const assigned = new Set()
309
- for (const [tierName, tier] of Object.entries(manifest.tiers)) {
310
- if (!tier || typeof tier.description !== 'string' || !Array.isArray(tier.skills) || tier.skills.length === 0) {
311
- fail(`tier '${tierName}' requires a description and non-empty skills list`)
312
- }
313
- for (const skill of tier.skills) {
314
- if (typeof skill !== 'string' || !SKILL_NAME_PATTERN.test(skill)) fail(`local skill name '${skill}' is invalid`)
315
- if (assigned.has(skill)) fail(`local skill '${skill}' belongs to more than one tier`)
316
- assigned.add(skill)
317
- }
318
- }
319
- const external = manifest.external
320
- if (!external || typeof external.source !== 'string' || !COMMIT_PATTERN.test(external.ref ?? '')) {
321
- fail('external.source and an exact 40-character external.ref are required')
322
- }
323
- if (!Array.isArray(external.skills) || external.skills.length === 0) fail('external.skills must be non-empty')
324
- for (const skill of external.skills) {
325
- if (typeof skill !== 'string' || !SKILL_NAME_PATTERN.test(skill)) fail(`external skill name '${skill}' is invalid`)
326
- }
327
- const externalNames = new Set(external.skills)
328
- if (externalNames.size !== external.skills.length) fail('external.skills contains duplicates')
329
- for (const skill of externalNames) if (assigned.has(skill)) fail(`skill '${skill}' is both local and external`)
330
- if (!external.tiers || typeof external.tiers !== 'object' || Object.keys(external.tiers).length === 0) {
331
- fail('external.tiers must define at least one explicit tier')
332
- }
333
- const tierAssigned = new Set()
334
- for (const [tierName, tier] of Object.entries(external.tiers)) {
335
- if (!manifest.tiers[tierName]) fail(`external tier '${tierName}' has no matching local tier selector`)
336
- if (!tier || typeof tier.description !== 'string' || !Array.isArray(tier.skills) || tier.skills.length === 0) {
337
- fail(`external tier '${tierName}' requires a description and non-empty skills list`)
338
- }
339
- for (const skill of tier.skills) {
340
- if (!externalNames.has(skill)) fail(`external tier '${tierName}' names unknown skill '${skill}'`)
341
- if (tierAssigned.has(skill)) fail(`external skill '${skill}' belongs to more than one tier`)
342
- tierAssigned.add(skill)
343
- }
344
- }
345
- for (const skill of externalNames) {
346
- if (!tierAssigned.has(skill)) fail(`external skill '${skill}' is not assigned to an external tier`)
347
- }
348
- if (!external.dependencies || typeof external.dependencies !== 'object') fail('external.dependencies is required')
349
- if (!external.contentHashes || typeof external.contentHashes !== 'object') fail('external.contentHashes is required')
350
- for (const skill of external.skills) {
351
- const dependencies = external.dependencies[skill]
352
- if (!Array.isArray(dependencies)) fail(`external dependency graph has no entry for '${skill}'`)
353
- for (const dependency of dependencies) {
354
- if (!externalNames.has(dependency)) fail(`external skill '${skill}' requires missing '${dependency}'`)
355
- }
356
- if (!SHA256_PATTERN.test(external.contentHashes[skill] ?? '')) fail(`external skill '${skill}' has no pinned SHA-256 hash`)
357
- }
358
- for (const skill of Object.keys(external.dependencies)) if (!externalNames.has(skill)) fail(`dependency graph names unknown skill '${skill}'`)
359
- for (const skill of Object.keys(external.contentHashes)) if (!externalNames.has(skill)) fail(`contentHashes names unknown skill '${skill}'`)
360
- for (const agent of manifest.agents?.ignore ?? []) {
361
- if (!KNOWN_AGENTS.includes(agent)) fail(`unknown agent '${agent}'; valid agents: ${KNOWN_AGENTS.join(', ')}`)
362
- }
363
- return manifest
364
- }
365
-
366
- function selectedTiers(manifest, options) {
367
- const allTiers = Object.keys(manifest.tiers).sort()
368
- let selected
369
- if (options.mode === 'all') selected = allTiers
370
- else if (options.mode === 'tiers') selected = options.tierValues
371
- else selected = [...manifest.default, ...(options.mode === 'with' ? options.tierValues : [])]
372
- selected = unique(selected)
373
- for (const tier of selected) if (!manifest.tiers[tier]) fail(`unknown tier '${tier}'; valid tiers: ${allTiers.join(', ')}`)
374
- return selected
375
- }
376
-
377
- function selectedLocalSkills(manifest, tiers) {
378
- return unique(tiers.flatMap((tier) => manifest.tiers[tier].skills))
379
- }
380
-
381
- function selectedExternalSkills(manifest, tiers) {
382
- const selected = new Set(tiers.flatMap((tier) => manifest.external.tiers[tier]?.skills ?? []))
383
- const visited = new Set()
384
- const visit = (skill) => {
385
- if (visited.has(skill)) return
386
- visited.add(skill)
387
- for (const dependency of manifest.external.dependencies[skill]) {
388
- selected.add(dependency)
389
- visit(dependency)
390
- }
391
- }
392
- for (const skill of [...selected]) visit(skill)
393
- return manifest.external.skills.filter((skill) => selected.has(skill))
394
- }
395
-
396
- function selectedExternalConfig(manifest, tiers) {
397
- return { ...manifest.external, skills: selectedExternalSkills(manifest, tiers) }
398
- }
399
-
400
- function printCatalog(manifest) {
401
- for (const [name, tier] of Object.entries(manifest.tiers).sort(([left], [right]) => left.localeCompare(right))) {
402
- const label = manifest.default.includes(name) ? 'default' : 'opt-in'
403
- console.log(`${name.padEnd(12)} (${tier.skills.length} skills, ${label}):`)
404
- console.log(` ${tier.skills.join(', ')}`)
405
- }
406
- console.log(`\nexternal (${manifest.external.skills.length} skills available, pinned):`)
407
- console.log(` source: ${manifest.external.source}@${manifest.external.ref}`)
408
- for (const [name, tier] of Object.entries(manifest.external.tiers).sort(([left], [right]) => left.localeCompare(right))) {
409
- const label = manifest.default.includes(name) ? 'default' : 'opt-in'
410
- console.log(` ${name.padEnd(10)} (${tier.skills.length} entry skills, ${label}; dependencies added): ${tier.skills.join(', ')}`)
411
- }
412
- }
413
-
414
- function readTarString(buffer, start, length) {
415
- const end = buffer.indexOf(0, start)
416
- return buffer.subarray(start, end === -1 || end > start + length ? start + length : end).toString('utf8')
417
- }
418
-
419
- function extractGitHubArchive(compressed, destination) {
420
- const archive = gunzipSync(compressed, { maxOutputLength: EXTRACTED_LIMIT_BYTES })
421
- let offset = 0
422
- let rootName
423
- while (offset + 512 <= archive.length) {
424
- const header = archive.subarray(offset, offset + 512)
425
- if (header.every((byte) => byte === 0)) break
426
- const name = readTarString(header, 0, 100)
427
- const prefix = readTarString(header, 345, 155)
428
- const archivePath = prefix ? `${prefix}/${name}` : name
429
- const sizeText = readTarString(header, 124, 12).trim().replace(/\0/g, '')
430
- const size = sizeText ? Number.parseInt(sizeText, 8) : 0
431
- if (!Number.isSafeInteger(size) || size < 0) fail('external archive contains an invalid entry size')
432
- const type = String.fromCharCode(header[156] || 48)
433
- if (type === 'x' || type === 'g') {
434
- offset += 512 + Math.ceil(size / 512) * 512
435
- continue
436
- }
437
- const segments = archivePath.split('/').filter(Boolean)
438
- if (segments.length === 0 || segments.some((segment) => segment === '..' || segment.includes('\\'))) {
439
- fail(`external archive contains unsafe path '${archivePath}'`)
440
- }
441
- rootName ??= segments[0]
442
- if (segments[0] !== rootName) fail('external archive contains multiple roots')
443
- const outputPath = join(destination, ...segments)
444
- if (!isWithin(outputPath, destination)) fail(`external archive escapes extraction root: '${archivePath}'`)
445
- if (type === '5') mkdirSync(outputPath, { recursive: true })
446
- else if (type === '0' || type === '\0') {
447
- mkdirSync(dirname(outputPath), { recursive: true })
448
- writeFileSync(outputPath, archive.subarray(offset + 512, offset + 512 + size))
449
- } else {
450
- fail(`external archive contains unsupported entry type '${type}'`)
451
- }
452
- offset += 512 + Math.ceil(size / 512) * 512
453
- }
454
- if (!rootName) fail('external archive is empty')
455
- return join(destination, rootName)
456
- }
457
-
458
- async function downloadPinnedSource(external, fetchImpl) {
459
- const match = external.source.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/)
460
- if (!match) fail(`unsupported external source '${external.source}'; expected owner/repository`)
461
- const [, owner, repository] = match
462
- const archiveUrl = `https://codeload.github.com/${owner}/${repository}/tar.gz/${external.ref}`
463
- const response = await fetchImpl(archiveUrl, { signal: AbortSignal.timeout(30_000) })
464
- if (!response.ok) fail(`external archive download failed (${response.status})`)
465
- const declaredLength = Number(response.headers.get('content-length') ?? 0)
466
- if (declaredLength > ARCHIVE_LIMIT_BYTES) fail('external archive exceeds the download limit')
467
- const compressed = Buffer.from(await response.arrayBuffer())
468
- if (compressed.length > ARCHIVE_LIMIT_BYTES) fail('external archive exceeds the download limit')
469
- const tempRoot = mkdtempSync(join(tmpdir(), 'om-skills-'))
470
- try {
471
- return { tempRoot, sourceDir: extractGitHubArchive(compressed, tempRoot) }
472
- } catch (error) {
473
- rmSync(tempRoot, { recursive: true, force: true })
474
- throw error
475
- }
476
- }
477
-
478
- async function resolveLatestRef(external, fetchImpl) {
479
- const match = external.source.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/)
480
- if (!match) fail(`unsupported external source '${external.source}'; expected owner/repository`)
481
- const [, owner, repository] = match
482
- const response = await fetchImpl(`https://api.github.com/repos/${owner}/${repository}/commits/main`, {
483
- headers: { accept: 'application/vnd.github+json' },
484
- signal: AbortSignal.timeout(30_000),
485
- })
486
- if (!response.ok) fail(`external current-ref lookup failed (${response.status})`)
487
- const payload = await response.json()
488
- if (!COMMIT_PATTERN.test(payload?.sha ?? '')) fail('external current-ref lookup returned an invalid commit')
489
- return payload.sha
490
- }
491
-
492
- function inspectDownloadedSource(downloaded) {
493
- const lexicalDownloadRoot = resolve(downloaded.tempRoot)
494
- if (!isWithin(downloaded.sourceDir, lexicalDownloadRoot)) fail('downloaded external source escapes its temporary root')
495
- const downloadRoot = realInstallerRoot(downloaded.tempRoot)
496
- const sourceEntry = lstatSync(downloaded.sourceDir, { throwIfNoEntry: false })
497
- if (!sourceEntry || sourceEntry.isSymbolicLink() || !sourceEntry.isDirectory()) {
498
- fail('downloaded external source must be a real directory')
499
- }
500
- const sourceDir = realpathSync(downloaded.sourceDir)
501
- if (!isWithin(sourceDir, downloadRoot)) fail('downloaded external source escapes its temporary root')
502
- assertRealDirectoryComponents(downloadRoot, sourceDir, { requireLeaf: true })
503
- assertRegularSkillTree(sourceDir)
504
- const sourceSkillsDir = join(sourceDir, 'skills')
505
- assertRealDirectoryComponents(downloadRoot, sourceSkillsDir, { requireLeaf: true })
506
- return { downloadRoot, sourceDir, sourceSkillsDir }
507
- }
508
-
509
- function cleanupDownloadedSource(downloadRoot) {
510
- if (!downloadRoot) return
511
- const entry = lstatSync(downloadRoot, { throwIfNoEntry: false })
512
- if (entry?.isDirectory() && !entry.isSymbolicLink()) rmSync(downloadRoot, { recursive: true, force: true })
513
- else if (entry) unlinkSync(downloadRoot)
514
- }
515
-
516
- function writeManifest(rootDir, manifestOrSource) {
517
- const manifestPath = join(rootDir, '.ai', 'skills', 'tiers.json')
518
- assertRegularOwnedFile(rootDir, manifestPath, { optional: false })
519
- const temporary = `${manifestPath}.tmp-${randomUUID()}`
520
- const source = typeof manifestOrSource === 'string'
521
- ? manifestOrSource
522
- : `${JSON.stringify(manifestOrSource, null, 2)}\n`
523
- try {
524
- assertOwnedPathAbsent(rootDir, temporary)
525
- writeFileSync(temporary, source, { mode: 0o600, flag: 'wx' })
526
- assertRegularOwnedFile(rootDir, temporary, { optional: false })
527
- assertRegularOwnedFile(rootDir, manifestPath, { optional: false })
528
- renameSync(temporary, manifestPath)
529
- } finally {
530
- const entry = lstatSync(temporary, { throwIfNoEntry: false })
531
- if (entry) unlinkSync(temporary)
532
- }
533
- }
534
-
535
- async function refreshExternalManifest(rootDir, manifest, fetchImpl, downloadSource, latestRefResolver) {
536
- const ref = await latestRefResolver(manifest.external, fetchImpl)
537
- const candidate = { ...manifest.external, ref }
538
- let downloadRoot
539
- try {
540
- const downloaded = await downloadSource(candidate, fetchImpl)
541
- downloadRoot = realInstallerRoot(downloaded.tempRoot)
542
- const inspected = inspectDownloadedSource(downloaded)
543
- downloadRoot = inspected.downloadRoot
544
- const contentHashes = {}
545
- for (const skill of candidate.skills) {
546
- const skillDir = join(inspected.sourceSkillsDir, skill)
547
- const entry = lstatSync(skillDir, { throwIfNoEntry: false })
548
- if (!entry || entry.isSymbolicLink() || !entry.isDirectory()) fail(`current external skill is missing: ${skill}`)
549
- contentHashes[skill] = hashSkillDirectory(skillDir)
550
- }
551
- const refreshed = {
552
- ...manifest,
553
- external: { ...candidate, contentHashes },
554
- }
555
- writeManifest(rootDir, refreshed)
556
- return { manifest: refreshed, downloaded }
557
- } catch (error) {
558
- cleanupDownloadedSource(downloadRoot)
559
- throw error
560
- }
561
- }
562
-
563
- function filesystemEntryKind(entry) {
564
- if (entry.isSymbolicLink()) return 'symbolic link'
565
- if (entry.isFIFO()) return 'FIFO'
566
- if (entry.isSocket()) return 'socket'
567
- if (entry.isBlockDevice()) return 'block device'
568
- if (entry.isCharacterDevice()) return 'character device'
569
- return 'unsupported filesystem node'
570
- }
571
-
572
- function regularFilesRecursively(root, current = '') {
573
- const directory = join(root, current)
574
- const directoryEntry = lstatSync(directory, { throwIfNoEntry: false })
575
- const displayDirectory = current ? current.split(sep).join('/') : '.'
576
- if (!directoryEntry) fail(`skill tree entry disappeared during validation: '${displayDirectory}'`)
577
- if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
578
- fail(`skill tree requires a real directory at '${displayDirectory}', found ${filesystemEntryKind(directoryEntry)}`)
579
- }
580
-
581
- const result = []
582
- for (const name of readdirSync(directory)) {
583
- const child = join(current, name)
584
- const childPath = join(root, child)
585
- const entry = lstatSync(childPath, { throwIfNoEntry: false })
586
- const displayChild = child.split(sep).join('/')
587
- if (!entry) fail(`skill tree entry disappeared during validation: '${displayChild}'`)
588
- if (entry.isSymbolicLink()) fail(`skill tree contains a symbolic link at '${displayChild}'`)
589
- if (entry.isDirectory()) result.push(...regularFilesRecursively(root, child))
590
- else if (entry.isFile()) result.push(child)
591
- else fail(`skill tree contains an unsupported ${filesystemEntryKind(entry)} at '${displayChild}'`)
592
- }
593
- return result
594
- }
595
-
596
- export function assertRegularSkillTree(root) {
597
- regularFilesRecursively(root)
598
- }
599
-
600
- export function hashSkillDirectory(root) {
601
- const hash = createHash('sha256')
602
- for (const file of regularFilesRecursively(root).sort()) {
603
- hash.update(file.split(sep).join('/'))
604
- hash.update('\0')
605
- hash.update(readFileSync(join(root, file)))
606
- hash.update('\0')
607
- }
608
- return `sha256:${hash.digest('hex')}`
609
- }
610
-
611
- function externalOwnershipPath(rootDir) {
612
- return join(rootDir, '.agents', 'skills', EXTERNAL_OWNERSHIP_FILE)
613
- }
614
-
615
- function readExternalOwnership(rootDir) {
616
- const ledgerPath = externalOwnershipPath(rootDir)
617
- assertRealDirectoryComponents(rootDir, dirname(ledgerPath))
618
- const ledgerEntry = lstatSync(ledgerPath, { throwIfNoEntry: false })
619
- if (!ledgerEntry) return null
620
- if (ledgerEntry.isSymbolicLink() || !ledgerEntry.isFile()) fail('external skill ownership ledger must be a regular file')
621
- let ledger
622
- try {
623
- ledger = JSON.parse(readFileSync(ledgerPath, 'utf8'))
624
- } catch (error) {
625
- fail(`cannot parse external skill ownership ledger: ${error.message}`)
626
- }
627
- if (ledger?.version !== 1 || typeof ledger.source !== 'string' || !COMMIT_PATTERN.test(ledger.ref ?? '') || !ledger.skills || typeof ledger.skills !== 'object') {
628
- fail('external skill ownership ledger is invalid')
629
- }
630
- for (const [skill, hash] of Object.entries(ledger.skills)) {
631
- if (!/^[a-z0-9][a-z0-9._-]*$/.test(skill) || !SHA256_PATTERN.test(hash)) fail('external skill ownership ledger is invalid')
632
- }
633
- return ledger
634
- }
635
-
636
- function writeExternalOwnership(rootDir, external, skills = undefined) {
637
- const ledgerPath = externalOwnershipPath(rootDir)
638
- ensureRealDirectory(rootDir, dirname(ledgerPath))
639
- assertRegularOwnedFile(rootDir, ledgerPath)
640
- const temporary = `${ledgerPath}.tmp-${randomUUID()}`
641
- const ledger = {
642
- version: 1,
643
- source: external.source,
644
- ref: external.ref,
645
- skills: skills ?? Object.fromEntries(external.skills.map((skill) => [skill, external.contentHashes[skill]])),
646
- }
647
- try {
648
- assertOwnedPathAbsent(rootDir, temporary)
649
- writeFileSync(temporary, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600, flag: 'wx' })
650
- assertRegularOwnedFile(rootDir, temporary, { optional: false })
651
- assertRegularOwnedFile(rootDir, ledgerPath)
652
- assertRealDirectoryComponents(rootDir, dirname(ledgerPath), { requireLeaf: true })
653
- renameSync(temporary, ledgerPath)
654
- } finally {
655
- assertRealDirectoryComponents(rootDir, dirname(temporary), { requireLeaf: true })
656
- const temporaryEntry = lstatSync(temporary, { throwIfNoEntry: false })
657
- if (temporaryEntry) {
658
- if (temporaryEntry.isDirectory() && !temporaryEntry.isSymbolicLink()) rmSync(temporary, { recursive: true, force: true })
659
- else unlinkSync(temporary)
660
- }
661
- }
662
- }
663
-
664
- function uniqueQuarantinePath(rootDir, skill) {
665
- const quarantineRoot = join(rootDir, '.agents', 'skills-quarantine')
666
- ensureRealDirectory(rootDir, quarantineRoot)
667
- assertRealDirectoryComponents(rootDir, quarantineRoot, { requireLeaf: true })
668
- for (let suffix = 1; ; suffix += 1) {
669
- const candidate = join(quarantineRoot, suffix === 1 ? skill : `${skill}.${suffix}`)
670
- if (!lstatSync(candidate, { throwIfNoEntry: false })) return candidate
671
- }
672
- }
673
-
674
- export function reconcileExternalSkillVisibility(rootDir, external) {
675
- assertRealDirectoryComponents(rootDir, join(rootDir, '.agents', 'skills'))
676
- assertRealDirectoryComponents(rootDir, join(rootDir, '.agents', 'skills-quarantine'))
677
- const ledger = readExternalOwnership(rootDir)
678
- const canonicalDir = join(rootDir, '.agents', 'skills')
679
- const quarantined = []
680
- const active = {}
681
- const names = unique([...Object.keys(ledger?.skills ?? {}), ...external.skills])
682
- for (const skill of names) {
683
- const skillDir = join(canonicalDir, skill)
684
- const entry = lstatSync(skillDir, { throwIfNoEntry: false })
685
- if (!entry) continue
686
- let actual = 'non-directory'
687
- if (entry.isDirectory() && !entry.isSymbolicLink()) {
688
- try {
689
- actual = hashSkillDirectory(skillDir)
690
- } catch (error) {
691
- actual = `unsafe (${error.message})`
692
- }
693
- }
694
- const expected = external.skills.includes(skill) ? external.contentHashes[skill] : undefined
695
- if (actual === expected) {
696
- active[skill] = expected
697
- continue
698
- }
699
- const previouslyOwned = Object.hasOwn(ledger?.skills ?? {}, skill)
700
- if (!previouslyOwned) continue
701
- const quarantinePath = uniqueQuarantinePath(rootDir, skill)
702
- assertRealDirectoryComponents(rootDir, canonicalDir, { requireLeaf: true })
703
- assertRealDirectoryComponents(rootDir, dirname(quarantinePath), { requireLeaf: true })
704
- renameSync(skillDir, quarantinePath)
705
- quarantined.push({ skill, path: quarantinePath, actual, expected: expected ?? 'removed from manifest' })
706
- }
707
- if (ledger || Object.keys(active).length > 0) writeExternalOwnership(rootDir, external, active)
708
- return quarantined
709
- }
710
-
711
- export function assertExternalDestinationsReplaceable(rootDir, external) {
712
- const ledger = readExternalOwnership(rootDir)
713
- const aiSkillsDir = join(rootDir, '.ai', 'skills')
714
- const canonicalDir = join(rootDir, '.agents', 'skills')
715
- assertRealDirectoryComponents(rootDir, aiSkillsDir, { requireLeaf: true })
716
- assertRealDirectoryComponents(rootDir, canonicalDir, { requireLeaf: true })
717
- for (const skill of external.skills) {
718
- const destination = join(canonicalDir, skill)
719
- const existing = lstatSync(destination, { throwIfNoEntry: false })
720
- if (!existing) continue
721
- if (existing.isSymbolicLink()) {
722
- if (!isHarnessOwnedLink(destination, aiSkillsDir, canonicalDir)) fail(`refusing to replace user-owned link ${destination}`)
723
- continue
724
- }
725
- if (!existing.isDirectory()) fail(`refusing to replace user-owned path ${destination}`)
726
- const actual = hashSkillDirectory(destination)
727
- const previouslyOwned = ledger?.skills?.[skill] === actual
728
- const alreadyPinned = external.contentHashes[skill] === actual
729
- if (!previouslyOwned && !alreadyPinned) fail(`refusing to replace unowned external skill directory ${destination}`)
730
- }
731
- }
732
-
733
- async function installExternal(rootDir, external, fetchImpl, downloadSource, activationObserver) {
734
- let downloaded
735
- let downloadRoot
736
- try {
737
- downloaded = await downloadSource(external, fetchImpl)
738
- downloadRoot = realInstallerRoot(downloaded.tempRoot)
739
- const inspected = inspectDownloadedSource(downloaded)
740
- downloadRoot = inspected.downloadRoot
741
- const { sourceSkillsDir } = inspected
742
- const mismatches = []
743
- for (const skill of external.skills) {
744
- const skillDir = join(sourceSkillsDir, skill)
745
- const entry = lstatSync(skillDir, { throwIfNoEntry: false })
746
- const actual = entry?.isDirectory() && !entry.isSymbolicLink() ? hashSkillDirectory(skillDir) : 'missing'
747
- if (actual !== external.contentHashes[skill]) mismatches.push(`${skill} (${actual})`)
748
- }
749
- if (mismatches.length > 0) fail(`external skill integrity check failed: ${mismatches.join(', ')}`)
750
-
751
- const aiSkillsDir = join(rootDir, '.ai', 'skills')
752
- const canonicalDir = join(rootDir, '.agents', 'skills')
753
- prepareLinkDirectory(rootDir, canonicalDir, aiSkillsDir, canonicalDir)
754
- assertExternalDestinationsReplaceable(rootDir, external)
755
- const nonce = randomUUID()
756
- const transactions = external.skills.map((skill) => ({
757
- skill,
758
- destination: join(canonicalDir, skill),
759
- stagedDestination: join(canonicalDir, `.om-install-${skill}-${nonce}`),
760
- backupDestination: join(canonicalDir, `.om-backup-${skill}-${nonce}`),
761
- existed: Boolean(lstatSync(join(canonicalDir, skill), { throwIfNoEntry: false })),
762
- staged: false,
763
- backedUp: false,
764
- activated: false,
765
- }))
766
- let committed = false
767
- try {
768
- // Stage and attest the complete set before changing any discoverable path.
769
- for (const transaction of transactions) {
770
- const { skill, stagedDestination, backupDestination } = transaction
771
- assertRealDirectoryComponents(downloadRoot, sourceSkillsDir, { requireLeaf: true })
772
- assertOwnedPathAbsent(rootDir, stagedDestination)
773
- assertOwnedPathAbsent(rootDir, backupDestination)
774
- cpSync(join(sourceSkillsDir, skill), stagedDestination, { recursive: true, errorOnExist: true })
775
- transaction.staged = true
776
- const copiedHash = hashSkillDirectory(stagedDestination)
777
- if (copiedHash !== external.contentHashes[skill]) {
778
- fail(`external skill changed while copying '${skill}' (${copiedHash})`)
779
- }
780
- }
781
-
782
- // Keep every previous destination as a backup until every new skill and
783
- // the ownership ledger have committed. Any failure restores the full old
784
- // set, so the non-fatal caller can safely continue with local skills.
785
- for (const transaction of transactions) {
786
- const { skill, destination, stagedDestination, backupDestination } = transaction
787
- if (transaction.existed) {
788
- assertRealDirectoryComponents(rootDir, canonicalDir, { requireLeaf: true })
789
- renameSync(destination, backupDestination)
790
- transaction.backedUp = true
791
- }
792
- assertRealDirectoryComponents(rootDir, canonicalDir, { requireLeaf: true })
793
- renameSync(stagedDestination, destination)
794
- transaction.staged = false
795
- transaction.activated = true
796
- const installedHash = hashSkillDirectory(destination)
797
- if (installedHash !== external.contentHashes[skill]) {
798
- fail(`external skill changed while activating '${skill}' (${installedHash})`)
799
- }
800
- activationObserver?.(skill)
801
- }
802
-
803
- const verified = {}
804
- for (const skill of external.skills) {
805
- const actual = hashSkillDirectory(join(canonicalDir, skill))
806
- if (actual !== external.contentHashes[skill]) fail(`installed external skill integrity check failed: ${skill} (${actual})`)
807
- verified[skill] = actual
808
- }
809
- writeExternalOwnership(rootDir, external, verified)
810
- committed = true
811
- } catch (error) {
812
- const rollbackErrors = []
813
- for (const transaction of [...transactions].reverse()) {
814
- try {
815
- if (transaction.activated) removeInstallerTransactionPath(rootDir, transaction.destination)
816
- if (transaction.backedUp && lstatSync(transaction.backupDestination, { throwIfNoEntry: false })) {
817
- assertRealDirectoryComponents(rootDir, canonicalDir, { requireLeaf: true })
818
- renameSync(transaction.backupDestination, transaction.destination)
819
- transaction.backedUp = false
820
- }
821
- } catch (rollbackError) {
822
- rollbackErrors.push(`${transaction.skill}: ${rollbackError.message}`)
823
- }
824
- }
825
- if (rollbackErrors.length > 0) {
826
- fail(`${error.message}; external skill rollback failed (${rollbackErrors.join('; ')})`)
827
- }
828
- throw error
829
- } finally {
830
- for (const transaction of transactions) {
831
- if (transaction.staged) removeInstallerTransactionPath(rootDir, transaction.stagedDestination)
832
- if (committed && transaction.backedUp) {
833
- try {
834
- removeInstallerTransactionPath(rootDir, transaction.backupDestination)
835
- transaction.backedUp = false
836
- } catch (error) {
837
- console.warn(`install-skills: warning: committed '${transaction.skill}' but could not remove its hidden backup (${error.message})`)
838
- }
839
- }
840
- }
841
- }
842
- return `installed ${external.source}@${external.ref}`
843
- } finally {
844
- cleanupDownloadedSource(downloadRoot)
845
- }
846
- }
847
-
848
- function relativeLink(fromDirectory, target) {
849
- const value = relative(fromDirectory, target) || '.'
850
- return value.split(sep).join('/')
851
- }
852
-
853
- function installLocalLinks(rootDir, localSkills, platform) {
854
- const aiSkillsDir = join(rootDir, '.ai', 'skills')
855
- const canonicalDir = join(rootDir, '.agents', 'skills')
856
- prepareLinkDirectory(rootDir, canonicalDir, aiSkillsDir, canonicalDir)
857
- for (const skill of localSkills) {
858
- const target = join(aiSkillsDir, skill)
859
- assertRealDirectoryComponents(rootDir, target, { requireLeaf: true })
860
- assertRegularSkillTree(target)
861
- replaceManagedLink(
862
- rootDir,
863
- join(canonicalDir, skill),
864
- target,
865
- relativeLink(canonicalDir, target),
866
- platform,
867
- aiSkillsDir,
868
- canonicalDir,
869
- )
870
- }
871
- cleanManagedLinks(rootDir, canonicalDir, aiSkillsDir, canonicalDir, new Set(localSkills))
872
- }
873
-
874
- function installedExternalSkills(rootDir, external) {
875
- const canonicalDir = join(rootDir, '.agents', 'skills')
876
- return external.skills.filter((skill) => {
877
- const skillDir = join(canonicalDir, skill)
878
- const entry = lstatSync(skillDir, { throwIfNoEntry: false })
879
- if (!entry?.isDirectory() || entry.isSymbolicLink()) return false
880
- try {
881
- return hashSkillDirectory(skillDir) === external.contentHashes[skill]
882
- } catch {
883
- return false
884
- }
885
- })
886
- }
887
-
888
- function installAgentLinks(rootDir, agent, names, localSkills, legacyLinks, platform) {
889
- const aiSkillsDir = join(rootDir, '.ai', 'skills')
890
- const canonicalDir = join(rootDir, '.agents', 'skills')
891
- const harnessDir = join(rootDir, ...AGENT_DIRECTORIES[agent])
892
- prepareLinkDirectory(rootDir, harnessDir, aiSkillsDir, canonicalDir)
893
- for (const skill of names) {
894
- const localLegacyTarget = legacyLinks && localSkills.includes(skill)
895
- const target = localLegacyTarget ? join(aiSkillsDir, skill) : join(canonicalDir, skill)
896
- replaceManagedLink(
897
- rootDir,
898
- join(harnessDir, skill),
899
- target,
900
- relativeLink(harnessDir, target),
901
- platform,
902
- aiSkillsDir,
903
- canonicalDir,
904
- )
905
- }
906
- cleanManagedLinks(rootDir, harnessDir, aiSkillsDir, canonicalDir, new Set(names))
907
- }
908
-
909
- function cleanAllLinks(rootDir) {
910
- const aiSkillsDir = join(rootDir, '.ai', 'skills')
911
- const canonicalDir = join(rootDir, '.agents', 'skills')
912
- for (const agent of KNOWN_AGENTS) cleanManagedLinks(rootDir, join(rootDir, ...AGENT_DIRECTORIES[agent]), aiSkillsDir, canonicalDir)
913
- cleanManagedLinks(rootDir, canonicalDir, aiSkillsDir, canonicalDir)
914
- }
915
-
916
- export async function runInstaller({
917
- rootDir,
918
- args = [],
919
- env = process.env,
920
- platform = process.platform,
921
- fetchImpl = globalThis.fetch,
922
- downloadSource = downloadPinnedSource,
923
- resolveLatestRef: latestRefResolver = resolveLatestRef,
924
- activationObserver = undefined,
925
- } = {}) {
926
- const options = parseArgs(args, env)
927
- if (options.help) {
928
- console.log(USAGE)
929
- return 0
930
- }
931
- rootDir = realInstallerRoot(rootDir)
932
- assertInstallerPathPreflight(rootDir)
933
- let manifest = readManifest(rootDir)
934
- if (options.list) {
935
- printCatalog(manifest)
936
- return 0
937
- }
938
- if (options.clean) {
939
- cleanAllLinks(rootDir)
940
- console.log('Removed harness-owned skill links; user-owned paths and installed external directories were preserved.')
941
- return 0
942
- }
943
- const ignoredAgents = options.ignoreAgents ?? manifest.agents?.ignore ?? []
944
- for (const agent of ignoredAgents) if (!KNOWN_AGENTS.includes(agent)) fail(`unknown agent '${agent}'; valid agents: ${KNOWN_AGENTS.join(', ')}`)
945
- const tiers = selectedTiers(manifest, options)
946
- const localSkills = selectedLocalSkills(manifest, tiers)
947
- let external = selectedExternalConfig(manifest, tiers)
948
- for (const skill of localSkills) {
949
- const localSkillDir = join(rootDir, '.ai', 'skills', skill)
950
- assertRealDirectoryComponents(rootDir, localSkillDir, { requireLeaf: true })
951
- assertRegularSkillTree(localSkillDir)
952
- }
953
- prepareLinkDirectory(rootDir, join(rootDir, '.agents', 'skills'), join(rootDir, '.ai', 'skills'), join(rootDir, '.agents', 'skills'))
954
- const quarantined = reconcileExternalSkillVisibility(rootDir, external)
955
- for (const item of quarantined) {
956
- console.warn(`install-skills: quarantined stale or modified managed skill '${item.skill}' outside agent discovery: ${item.path}`)
957
- }
958
- let externalStatus = 'skipped (--no-external)'
959
- let refreshDownload
960
- const originalManifestSource = options.update
961
- ? readFileSync(join(rootDir, '.ai', 'skills', 'tiers.json'), 'utf8')
962
- : undefined
963
- if (options.update) {
964
- const refreshed = await refreshExternalManifest(rootDir, manifest, fetchImpl, downloadSource, latestRefResolver)
965
- manifest = refreshed.manifest
966
- refreshDownload = refreshed.downloaded
967
- external = selectedExternalConfig(manifest, tiers)
968
- }
969
- try {
970
- if (external.skills.length === 0) {
971
- externalStatus = 'skipped (no external skills selected)'
972
- } else if (!options.noExternal) {
973
- try {
974
- const installDownloadSource = refreshDownload
975
- ? async () => {
976
- const downloaded = refreshDownload
977
- refreshDownload = undefined
978
- return downloaded
979
- }
980
- : downloadSource
981
- externalStatus = await installExternal(rootDir, external, fetchImpl, installDownloadSource, activationObserver)
982
- } catch (error) {
983
- if (options.update) {
984
- writeManifest(rootDir, originalManifestSource)
985
- throw error
986
- }
987
- externalStatus = `unavailable (${error.message})`
988
- console.warn(`install-skills: warning: ${error.message}`)
989
- console.warn(' Local skills will still be installed. Retry with `yarn install-skills` when online.')
990
- }
991
- }
992
- } finally {
993
- if (refreshDownload) cleanupDownloadedSource(realInstallerRoot(refreshDownload.tempRoot))
994
- }
995
- installLocalLinks(rootDir, localSkills, platform)
996
- const externalInstalled = installedExternalSkills(rootDir, external)
997
- const allInstalled = unique([...localSkills, ...externalInstalled])
998
- const linkAgents = options.legacyLinks ? LEGACY_AGENTS : ['claude-code']
999
- for (const agent of KNOWN_AGENTS) {
1000
- const harnessDir = join(rootDir, ...AGENT_DIRECTORIES[agent])
1001
- if (ignoredAgents.includes(agent)) {
1002
- cleanManagedLinks(rootDir, harnessDir, join(rootDir, '.ai', 'skills'), join(rootDir, '.agents', 'skills'))
1003
- } else if (linkAgents.includes(agent)) {
1004
- installAgentLinks(rootDir, agent, allInstalled, localSkills, options.legacyLinks, platform)
1005
- } else {
1006
- cleanManagedLinks(rootDir, harnessDir, join(rootDir, '.ai', 'skills'), join(rootDir, '.agents', 'skills'))
1007
- }
1008
- }
1009
- console.log(`Installed ${localSkills.length} local skills across ${tiers.length} tier(s): ${tiers.join(', ')}.`)
1010
- console.log(`External skills: ${externalStatus}.`)
1011
- if (options.update) console.log(`Pinned current shared skills at ${external.ref}.`)
1012
- const links = linkAgents.filter((agent) => !ignoredAgents.includes(agent))
1013
- console.log(`Layout: .agents/skills/ (canonical); per-agent links: ${links.join(', ') || 'none'}.`)
1014
- if (options.mode === 'default') console.log('Tip: inspect opt-in tiers with `yarn install-skills --list`.')
1015
- return 0
1016
- }
1017
-
1018
- // Node canonicalizes import.meta.url through real filesystem paths, while argv
1019
- // may retain a lexical alias (for example macOS /tmp -> /private/tmp). Compare
1020
- // canonical paths so scaffolding through a symlinked parent still runs the
1021
- // installer instead of returning success without installing anything.
1022
- const isEntryPoint = process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
1023
- if (isEntryPoint) {
1024
- const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '..')
1025
- runInstaller({ rootDir, args: process.argv.slice(2) }).catch((error) => {
1026
- console.error(error.message)
1027
- process.exitCode = 1
1028
- })
1029
- }