@open-mercato/cli 0.6.7-develop.6862.1.c11a64ce0a → 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.
- package/.turbo/turbo-build.log +4 -4
- package/build.mjs +7 -24
- package/dist/agentic/claude-code/hooks/entity-migration-check.ts +88 -32
- package/dist/agentic/codex/enforcement-rules.md +16 -2
- package/dist/agentic/cursor/hooks/entity-migration-check.mjs +50 -19
- package/dist/agentic/cursor/rules/entity-guard.mdc +21 -9
- package/dist/agentic/cursor/rules/generated-guard.mdc +5 -3
- package/dist/agentic/cursor/rules/open-mercato.mdc +29 -3
- package/dist/agentic/guides/cache.md +45 -0
- package/dist/agentic/guides/core.md +453 -0
- package/dist/agentic/guides/events.md +117 -0
- package/dist/agentic/guides/module-facts.json +3833 -115419
- package/dist/agentic/guides/module-system.md +127 -12
- package/dist/agentic/guides/modules/ai_assistant.md +28 -227
- package/dist/agentic/guides/modules/api_docs.md +2 -58
- package/dist/agentic/guides/modules/api_keys.md +7 -92
- package/dist/agentic/guides/modules/attachments.md +14 -119
- package/dist/agentic/guides/modules/audit_logs.md +10 -131
- package/dist/agentic/guides/modules/auth.md +33 -276
- package/dist/agentic/guides/modules/business_rules.md +14 -149
- package/dist/agentic/guides/modules/catalog.md +43 -475
- package/dist/agentic/guides/modules/channel_gmail.md +3 -82
- package/dist/agentic/guides/modules/channel_imap.md +3 -82
- package/dist/agentic/guides/modules/checkout.md +27 -299
- package/dist/agentic/guides/modules/communication_channels.md +33 -315
- package/dist/agentic/guides/modules/configs.md +8 -104
- package/dist/agentic/guides/modules/content.md +2 -57
- package/dist/agentic/guides/modules/currencies.md +17 -152
- package/dist/agentic/guides/modules/customer_accounts.md +45 -387
- package/dist/agentic/guides/modules/customers.md +85 -746
- package/dist/agentic/guides/modules/dashboards.md +10 -155
- package/dist/agentic/guides/modules/data_sync.md +16 -149
- package/dist/agentic/guides/modules/dictionaries.md +12 -107
- package/dist/agentic/guides/modules/directory.md +15 -148
- package/dist/agentic/guides/modules/entities.md +13 -145
- package/dist/agentic/guides/modules/events.md +2 -59
- package/dist/agentic/guides/modules/feature_toggles.md +9 -119
- package/dist/agentic/guides/modules/gateway_stripe.md +4 -92
- package/dist/agentic/guides/modules/generators.md +2 -45
- package/dist/agentic/guides/modules/inbox_ops.md +27 -207
- package/dist/agentic/guides/modules/integrations.md +16 -146
- package/dist/agentic/guides/modules/messages.md +24 -223
- package/dist/agentic/guides/modules/notifications.md +8 -96
- package/dist/agentic/guides/modules/onboarding.md +6 -77
- package/dist/agentic/guides/modules/payment_gateways.md +21 -164
- package/dist/agentic/guides/modules/perspectives.md +7 -77
- package/dist/agentic/guides/modules/planner.md +18 -136
- package/dist/agentic/guides/modules/portal.md +2 -93
- package/dist/agentic/guides/modules/progress.md +14 -104
- package/dist/agentic/guides/modules/query_index.md +15 -154
- package/dist/agentic/guides/modules/record_locks.md +21 -206
- package/dist/agentic/guides/modules/resources.md +30 -197
- package/dist/agentic/guides/modules/sales.md +84 -678
- package/dist/agentic/guides/modules/scheduler.md +14 -133
- package/dist/agentic/guides/modules/search.md +6 -170
- package/dist/agentic/guides/modules/security.md +35 -294
- package/dist/agentic/guides/modules/shipping_carriers.md +18 -134
- package/dist/agentic/guides/modules/sso.md +26 -151
- package/dist/agentic/guides/modules/staff.md +57 -369
- package/dist/agentic/guides/modules/storage_s3.md +5 -95
- package/dist/agentic/guides/modules/sync_akeneo.md +3 -85
- package/dist/agentic/guides/modules/sync_excel.md +6 -75
- package/dist/agentic/guides/modules/system_status_overlays.md +2 -60
- package/dist/agentic/guides/modules/translations.md +10 -104
- package/dist/agentic/guides/modules/webhooks.md +24 -203
- package/dist/agentic/guides/modules/wms.md +46 -441
- package/dist/agentic/guides/modules/workflows.md +43 -302
- package/dist/agentic/guides/queue.md +53 -0
- package/dist/agentic/guides/search.md +115 -0
- package/dist/agentic/guides/shared.md +108 -0
- package/dist/agentic/guides/ui.md +347 -0
- package/dist/agentic/shared/AGENTS.md.template +311 -124
- package/dist/agentic/shared/ai/agentic.config.json +0 -2
- package/dist/agentic/shared/ai/lessons.md +3 -26
- package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +27 -0
- package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +27 -0
- package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +0 -4
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +241 -20
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +260 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +601 -16
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +146 -0
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +310 -14
- package/dist/agentic/shared/ai/skills/om-help/SKILL.md +97 -15
- package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +84 -0
- package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +193 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +152 -33
- package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +708 -24
- package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +34 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +762 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +820 -20
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +50 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +97 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +843 -19
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +271 -0
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +63 -15
- package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +467 -16
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +101 -0
- package/dist/agentic/shared/ai/skills/tiers.json +21 -143
- package/dist/agentic/shared/ai/skills/tiers.schema.json +40 -52
- package/dist/agentic/shared/ai/specs/README.md +3 -4
- package/dist/agentic/shared/ai/specs/SPEC-000-template.md +10 -226
- package/dist/agentic/shared/ai/trackers/github.md +4 -108
- package/dist/agentic/shared/scripts/install-skills.sh +694 -4
- package/dist/bin.js +1 -13
- package/dist/bin.js.map +2 -2
- package/dist/lib/__integration__/TC-INT-008.spec.js +8 -88
- package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
- package/dist/lib/agentic-init.js +3 -13
- package/dist/lib/agentic-init.js.map +2 -2
- package/dist/lib/agentic-setup.js +186 -418
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/generators/extensions/injection-widgets.js +0 -20
- package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
- package/dist/lib/generators/module-facts-discovery.js +7 -41
- package/dist/lib/generators/module-facts-discovery.js.map +2 -2
- package/dist/lib/generators/module-facts.js +45 -1387
- package/dist/lib/generators/module-facts.js.map +3 -3
- package/dist/lib/generators/module-registry.js +5 -11
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/resolver.js +0 -2
- package/dist/lib/resolver.js.map +2 -2
- package/dist/lib/testing/integration.js +0 -5
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +1 -23
- package/dist/mercato.js.map +2 -2
- package/package.json +11 -13
- package/src/bin.ts +1 -33
- package/src/lib/__integration__/TC-INT-008.spec.ts +9 -107
- package/src/lib/__tests__/agentic-init.test.ts +5 -41
- package/src/lib/agentic-init.ts +3 -15
- package/src/lib/agentic-setup.ts +224 -530
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +2 -88
- package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +0 -1
- package/src/lib/generators/__tests__/module-facts.discovery.test.ts +10 -54
- package/src/lib/generators/__tests__/module-subset.test.ts +0 -18
- package/src/lib/generators/extensions/injection-widgets.ts +0 -51
- package/src/lib/generators/module-facts-discovery.ts +7 -58
- package/src/lib/generators/module-facts.ts +46 -1851
- package/src/lib/generators/module-registry.ts +6 -16
- package/src/lib/resolver.ts +0 -3
- package/src/lib/testing/__tests__/integration.test.ts +0 -4
- package/src/lib/testing/integration.ts +0 -5
- package/src/mercato.ts +1 -26
- package/dist/agentic/guides/ai-workflows.md +0 -76
- package/dist/agentic/guides/architecture.md +0 -107
- package/dist/agentic/guides/backend-ui.md +0 -80
- package/dist/agentic/guides/contracts.md +0 -92
- package/dist/agentic/guides/extensions.md +0 -121
- package/dist/agentic/guides/framework-extension-points.md +0 -36
- package/dist/agentic/guides/integrations.md +0 -81
- package/dist/agentic/guides/modules/design_system.md +0 -102
- package/dist/agentic/guides/spec-delivery.md +0 -22
- package/dist/agentic/guides/testing-debugging.md +0 -80
- package/dist/agentic/guides/upstream/AGENTS.md +0 -298
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +0 -316
- package/dist/agentic/guides/upstream/manifest.json +0 -8
- package/dist/agentic/shared/ai/harness/README.md +0 -50
- package/dist/agentic/shared/ai/harness/RELEASE.md +0 -57
- package/dist/agentic/shared/ai/harness/cases.json +0 -13680
- package/dist/agentic/shared/ai/harness/cases.schema.json +0 -133
- package/dist/agentic/shared/ai/harness/fixtures/index.json +0 -49
- package/dist/agentic/shared/ai/harness/fixtures/seeds.json +0 -162
- package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +0 -21
- package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +0 -96
- package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +0 -172
- package/dist/agentic/shared/ai/harness/release-matrix.json +0 -158
- package/dist/agentic/shared/ai/harness/release-result.schema.json +0 -219
- package/dist/agentic/shared/ai/harness/result.schema.json +0 -76
- package/dist/agentic/shared/ai/harness/routing-response.schema.json +0 -12
- package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +0 -73
- package/dist/agentic/shared/ai/harness/validators.json +0 -78
- package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +0 -2121
- package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +0 -1096
- package/dist/agentic/shared/ai/lessons/_template.md +0 -16
- package/dist/agentic/shared/ai/review-checklist.md +0 -36
- package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +0 -21
- package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +0 -98
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +0 -16
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +0 -53
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +0 -13
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +0 -11
- package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +0 -30
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +0 -12
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +0 -13
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +0 -14
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +0 -28
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +0 -12
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +0 -22
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +0 -12
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +0 -19
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +0 -19
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +0 -13
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +0 -14
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +0 -10
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +0 -17
- package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +0 -29
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +0 -100
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +0 -16
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +0 -17
- package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +0 -25
- package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +0 -11
- package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +0 -13
- package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +0 -18
- package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +0 -20
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +0 -33
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +0 -40
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +0 -27
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +0 -24
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +0 -16
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +0 -14
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +0 -15
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +0 -12
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +0 -34
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +0 -4
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +0 -11
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +0 -31
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +0 -39
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +0 -43
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +0 -11
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +0 -19
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +0 -172
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +0 -12
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +0 -83
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +0 -21
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +0 -11
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +0 -22
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +0 -13
- package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +0 -41
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +0 -19
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +0 -44
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +0 -39
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +0 -20
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +0 -57
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +0 -11
- package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +0 -486
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +0 -18
- package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +0 -27
- package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +0 -13
- package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +0 -41
- package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +0 -11
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +0 -15
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +0 -16
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +0 -14
- package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +0 -187
- package/dist/agentic/shared/scripts/check-lessons.mjs +0 -178
- package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +0 -2958
- package/dist/agentic/shared/scripts/execution-sandbox.mjs +0 -329
- package/dist/agentic/shared/scripts/framework-context.mjs +0 -888
- package/dist/agentic/shared/scripts/install-skills.mjs +0 -1029
- package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +0 -222
- package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +0 -1774
- package/dist/lib/generators/module-extension-facts.js +0 -1840
- package/dist/lib/generators/module-extension-facts.js.map +0 -7
- package/dist/lib/generators/module-fact-sources.js +0 -70
- package/dist/lib/generators/module-fact-sources.js.map +0 -7
- package/dist/lib/generators/module-override-targets.js +0 -688
- package/dist/lib/generators/module-override-targets.js.map +0 -7
- package/dist/lib/load-env.js +0 -24
- package/dist/lib/load-env.js.map +0 -7
- package/dist/lib/telemetry-init.js +0 -358
- package/dist/lib/telemetry-init.js.map +0 -7
- package/src/lib/__tests__/agentic-setup.ownership.test.ts +0 -386
- package/src/lib/__tests__/injection-widget-id-validation.test.ts +0 -184
- package/src/lib/__tests__/load-env.test.ts +0 -65
- package/src/lib/__tests__/telemetry-init.test.ts +0 -298
- package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +0 -92
- package/src/lib/generators/__tests__/module-extension-activations.test.ts +0 -762
- package/src/lib/generators/__tests__/module-extension-facts.test.ts +0 -404
- package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +0 -108
- package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +0 -561
- package/src/lib/generators/__tests__/module-facts.source-links.test.ts +0 -119
- package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +0 -149
- package/src/lib/generators/__tests__/module-override-targets.test.ts +0 -304
- package/src/lib/generators/module-extension-facts.ts +0 -2365
- package/src/lib/generators/module-fact-sources.ts +0 -108
- package/src/lib/generators/module-override-targets.ts +0 -863
- package/src/lib/load-env.ts +0 -30
- 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
|
-
}
|