@lesliechan721/aw 0.1.0-beta.3
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/dist/authoring/assets/aw-config.d.mts +2 -0
- package/dist/authoring/assets/aw-config.mjs +556 -0
- package/dist/authoring/assets/aw-config.mjs.map +1 -0
- package/dist/authoring/scripts/add-skill.d.mts +2 -0
- package/dist/authoring/scripts/add-skill.mjs +355 -0
- package/dist/authoring/scripts/add-skill.mjs.map +1 -0
- package/dist/authoring/scripts/check-plugin-versions.d.mts +2 -0
- package/dist/authoring/scripts/check-plugin-versions.mjs +5 -0
- package/dist/authoring/scripts/check-plugin-versions.mjs.map +1 -0
- package/dist/authoring/scripts/init-plugin.d.mts +2 -0
- package/dist/authoring/scripts/init-plugin.mjs +512 -0
- package/dist/authoring/scripts/init-plugin.mjs.map +1 -0
- package/dist/authoring/scripts/lib/javascript-import-scanner.d.mts +4 -0
- package/dist/authoring/scripts/lib/javascript-import-scanner.mjs +261 -0
- package/dist/authoring/scripts/lib/javascript-import-scanner.mjs.map +1 -0
- package/dist/authoring/scripts/lib/plugin-root.d.mts +26 -0
- package/dist/authoring/scripts/lib/plugin-root.mjs +125 -0
- package/dist/authoring/scripts/lib/plugin-root.mjs.map +1 -0
- package/dist/authoring/scripts/lib/plugin-version-check.d.mts +55 -0
- package/dist/authoring/scripts/lib/plugin-version-check.mjs +777 -0
- package/dist/authoring/scripts/lib/plugin-version-check.mjs.map +1 -0
- package/dist/authoring/scripts/lib/semver.d.mts +9 -0
- package/dist/authoring/scripts/lib/semver.mjs +63 -0
- package/dist/authoring/scripts/lib/semver.mjs.map +1 -0
- package/dist/authoring/scripts/lib/skill-scaffold.d.mts +124 -0
- package/dist/authoring/scripts/lib/skill-scaffold.mjs +374 -0
- package/dist/authoring/scripts/lib/skill-scaffold.mjs.map +1 -0
- package/dist/authoring/scripts/plugin-status.d.mts +2 -0
- package/dist/authoring/scripts/plugin-status.mjs +96 -0
- package/dist/authoring/scripts/plugin-status.mjs.map +1 -0
- package/dist/authoring/scripts/validate-plugin.d.mts +2 -0
- package/dist/authoring/scripts/validate-plugin.mjs +4442 -0
- package/dist/authoring/scripts/validate-plugin.mjs.map +1 -0
- package/dist/authoring/scripts/validate-skill.d.mts +2 -0
- package/dist/authoring/scripts/validate-skill.mjs +126 -0
- package/dist/authoring/scripts/validate-skill.mjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/scripts/aw-skill-eval-engine.d.ts +13 -0
- package/dist/scripts/aw-skill-eval-engine.js +31 -0
- package/dist/scripts/aw-skill-eval-engine.js.map +1 -0
- package/dist/scripts/bundle-plugins.d.ts +18 -0
- package/dist/scripts/bundle-plugins.js +123 -0
- package/dist/scripts/bundle-plugins.js.map +1 -0
- package/dist/scripts/codex-eval-app-server.d.ts +89 -0
- package/dist/scripts/codex-eval-app-server.js +1228 -0
- package/dist/scripts/codex-eval-app-server.js.map +1 -0
- package/dist/scripts/codex-eval-isolation.d.ts +61 -0
- package/dist/scripts/codex-eval-isolation.js +318 -0
- package/dist/scripts/codex-eval-isolation.js.map +1 -0
- package/dist/scripts/codex-eval-process-management.d.ts +8 -0
- package/dist/scripts/codex-eval-process-management.js +69 -0
- package/dist/scripts/codex-eval-process-management.js.map +1 -0
- package/dist/scripts/codex-eval-process-tree.d.ts +38 -0
- package/dist/scripts/codex-eval-process-tree.js +803 -0
- package/dist/scripts/codex-eval-process-tree.js.map +1 -0
- package/dist/scripts/codex-eval-runtime-config.d.ts +62 -0
- package/dist/scripts/codex-eval-runtime-config.js +843 -0
- package/dist/scripts/codex-eval-runtime-config.js.map +1 -0
- package/dist/scripts/codex-trigger-eval.d.ts +84 -0
- package/dist/scripts/codex-trigger-eval.js +968 -0
- package/dist/scripts/codex-trigger-eval.js.map +1 -0
- package/dist/scripts/eval-plugin-skills-audited.d.ts +6 -0
- package/dist/scripts/eval-plugin-skills-audited.js +261 -0
- package/dist/scripts/eval-plugin-skills-audited.js.map +1 -0
- package/dist/scripts/eval-plugin-skills.d.ts +45 -0
- package/dist/scripts/eval-plugin-skills.js +1586 -0
- package/dist/scripts/eval-plugin-skills.js.map +1 -0
- package/dist/scripts/eval-run-layout.d.ts +17 -0
- package/dist/scripts/eval-run-layout.js +26 -0
- package/dist/scripts/eval-run-layout.js.map +1 -0
- package/dist/scripts/plugin-eval-contract.d.ts +185 -0
- package/dist/scripts/plugin-eval-contract.js +695 -0
- package/dist/scripts/plugin-eval-contract.js.map +1 -0
- package/dist/scripts/plugin-eval-materializer.d.ts +13 -0
- package/dist/scripts/plugin-eval-materializer.js +421 -0
- package/dist/scripts/plugin-eval-materializer.js.map +1 -0
- package/dist/scripts/plugin-staging.d.ts +43 -0
- package/dist/scripts/plugin-staging.js +196 -0
- package/dist/scripts/plugin-staging.js.map +1 -0
- package/dist/scripts/skill-reporting.d.ts +212 -0
- package/dist/scripts/skill-reporting.js +922 -0
- package/dist/scripts/skill-reporting.js.map +1 -0
- package/dist/scripts/test-plugin-scripts.d.ts +7 -0
- package/dist/scripts/test-plugin-scripts.js +674 -0
- package/dist/scripts/test-plugin-scripts.js.map +1 -0
- package/dist/src/adapters/helpers.d.ts +44 -0
- package/dist/src/adapters/helpers.js +95 -0
- package/dist/src/adapters/helpers.js.map +1 -0
- package/dist/src/authoring/workspace-commands.d.ts +46 -0
- package/dist/src/authoring/workspace-commands.js +133 -0
- package/dist/src/authoring/workspace-commands.js.map +1 -0
- package/dist/src/commands/catalog-publish.d.ts +33 -0
- package/dist/src/commands/catalog-publish.js +374 -0
- package/dist/src/commands/catalog-publish.js.map +1 -0
- package/dist/src/commands/catalog-source.d.ts +111 -0
- package/dist/src/commands/catalog-source.js +266 -0
- package/dist/src/commands/catalog-source.js.map +1 -0
- package/dist/src/commands/console.d.ts +3 -0
- package/dist/src/commands/console.js +44 -0
- package/dist/src/commands/console.js.map +1 -0
- package/dist/src/commands/dependency-operation-plan.d.ts +28 -0
- package/dist/src/commands/dependency-operation-plan.js +15 -0
- package/dist/src/commands/dependency-operation-plan.js.map +1 -0
- package/dist/src/commands/init.d.ts +31 -0
- package/dist/src/commands/init.js +63 -0
- package/dist/src/commands/init.js.map +1 -0
- package/dist/src/commands/options.d.ts +68 -0
- package/dist/src/commands/options.js +146 -0
- package/dist/src/commands/options.js.map +1 -0
- package/dist/src/commands/rebuild-command.d.ts +77 -0
- package/dist/src/commands/rebuild-command.js +1681 -0
- package/dist/src/commands/rebuild-command.js.map +1 -0
- package/dist/src/commands/rebuild-integration.d.ts +94 -0
- package/dist/src/commands/rebuild-integration.js +3289 -0
- package/dist/src/commands/rebuild-integration.js.map +1 -0
- package/dist/src/commands/rebuild-operations.d.ts +110 -0
- package/dist/src/commands/rebuild-operations.js +791 -0
- package/dist/src/commands/rebuild-operations.js.map +1 -0
- package/dist/src/commands/review-publication.d.ts +99 -0
- package/dist/src/commands/review-publication.js +904 -0
- package/dist/src/commands/review-publication.js.map +1 -0
- package/dist/src/commands/runtime.d.ts +176 -0
- package/dist/src/commands/runtime.js +746 -0
- package/dist/src/commands/runtime.js.map +1 -0
- package/dist/src/commands/status-v4.d.ts +102 -0
- package/dist/src/commands/status-v4.js +844 -0
- package/dist/src/commands/status-v4.js.map +1 -0
- package/dist/src/commands/status.d.ts +93 -0
- package/dist/src/commands/status.js +442 -0
- package/dist/src/commands/status.js.map +1 -0
- package/dist/src/commands/wizard.d.ts +57 -0
- package/dist/src/commands/wizard.js +58 -0
- package/dist/src/commands/wizard.js.map +1 -0
- package/dist/src/commands/write-commands.d.ts +7 -0
- package/dist/src/commands/write-commands.js +167 -0
- package/dist/src/commands/write-commands.js.map +1 -0
- package/dist/src/constants.d.ts +25 -0
- package/dist/src/constants.js +26 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/generators/agents.d.ts +3 -0
- package/dist/src/generators/agents.js +46 -0
- package/dist/src/generators/agents.js.map +1 -0
- package/dist/src/generators/hooks.d.ts +15 -0
- package/dist/src/generators/hooks.js +76 -0
- package/dist/src/generators/hooks.js.map +1 -0
- package/dist/src/generators/instructions.d.ts +13 -0
- package/dist/src/generators/instructions.js +66 -0
- package/dist/src/generators/instructions.js.map +1 -0
- package/dist/src/generators/local-plugin.d.ts +53 -0
- package/dist/src/generators/local-plugin.js +1268 -0
- package/dist/src/generators/local-plugin.js.map +1 -0
- package/dist/src/generators/permissions.d.ts +10 -0
- package/dist/src/generators/permissions.js +37 -0
- package/dist/src/generators/permissions.js.map +1 -0
- package/dist/src/generators/shared.d.ts +2 -0
- package/dist/src/generators/shared.js +15 -0
- package/dist/src/generators/shared.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.js +2344 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/output/command-receipt.d.ts +90 -0
- package/dist/src/output/command-receipt.js +129 -0
- package/dist/src/output/command-receipt.js.map +1 -0
- package/dist/src/output/debug-log.d.ts +4 -0
- package/dist/src/output/debug-log.js +97 -0
- package/dist/src/output/debug-log.js.map +1 -0
- package/dist/src/output/equivalent-command.d.ts +9 -0
- package/dist/src/output/equivalent-command.js +39 -0
- package/dist/src/output/equivalent-command.js.map +1 -0
- package/dist/src/output/model.d.ts +117 -0
- package/dist/src/output/model.js +145 -0
- package/dist/src/output/model.js.map +1 -0
- package/dist/src/output/rebuild-progress.d.ts +21 -0
- package/dist/src/output/rebuild-progress.js +138 -0
- package/dist/src/output/rebuild-progress.js.map +1 -0
- package/dist/src/output/redact.d.ts +3 -0
- package/dist/src/output/redact.js +39 -0
- package/dist/src/output/redact.js.map +1 -0
- package/dist/src/output/reporter.d.ts +9 -0
- package/dist/src/output/reporter.js +133 -0
- package/dist/src/output/reporter.js.map +1 -0
- package/dist/src/portable/commands.d.ts +120 -0
- package/dist/src/portable/commands.js +506 -0
- package/dist/src/portable/commands.js.map +1 -0
- package/dist/src/portable/materializer.d.ts +50 -0
- package/dist/src/portable/materializer.js +408 -0
- package/dist/src/portable/materializer.js.map +1 -0
- package/dist/src/portable/skills-cli.d.ts +43 -0
- package/dist/src/portable/skills-cli.js +148 -0
- package/dist/src/portable/skills-cli.js.map +1 -0
- package/dist/src/tui/components.d.ts +32 -0
- package/dist/src/tui/components.js +42 -0
- package/dist/src/tui/components.js.map +1 -0
- package/dist/src/tui/console-app-v4.d.ts +20 -0
- package/dist/src/tui/console-app-v4.js +2613 -0
- package/dist/src/tui/console-app-v4.js.map +1 -0
- package/dist/src/tui/controller-runtime.d.ts +11 -0
- package/dist/src/tui/controller-runtime.js +902 -0
- package/dist/src/tui/controller-runtime.js.map +1 -0
- package/dist/src/tui/controller.d.ts +367 -0
- package/dist/src/tui/controller.js +1724 -0
- package/dist/src/tui/controller.js.map +1 -0
- package/dist/src/tui/index.d.ts +13 -0
- package/dist/src/tui/index.js +14 -0
- package/dist/src/tui/index.js.map +1 -0
- package/dist/src/tui/keys.d.ts +12 -0
- package/dist/src/tui/keys.js +22 -0
- package/dist/src/tui/keys.js.map +1 -0
- package/dist/src/tui/layout.d.ts +12 -0
- package/dist/src/tui/layout.js +19 -0
- package/dist/src/tui/layout.js.map +1 -0
- package/dist/src/tui/model.d.ts +279 -0
- package/dist/src/tui/model.js +652 -0
- package/dist/src/tui/model.js.map +1 -0
- package/dist/src/tui/prompt.d.ts +8 -0
- package/dist/src/tui/prompt.js +242 -0
- package/dist/src/tui/prompt.js.map +1 -0
- package/dist/src/tui/router.d.ts +153 -0
- package/dist/src/tui/router.js +102 -0
- package/dist/src/tui/router.js.map +1 -0
- package/dist/src/tui/runner.d.ts +11 -0
- package/dist/src/tui/runner.js +71 -0
- package/dist/src/tui/runner.js.map +1 -0
- package/dist/src/tui/theme.d.ts +18 -0
- package/dist/src/tui/theme.js +20 -0
- package/dist/src/tui/theme.js.map +1 -0
- package/dist/src/tui/views.d.ts +57 -0
- package/dist/src/tui/views.js +173 -0
- package/dist/src/tui/views.js.map +1 -0
- package/dist/src/tui/wizard.d.ts +11 -0
- package/dist/src/tui/wizard.js +321 -0
- package/dist/src/tui/wizard.js.map +1 -0
- package/dist/src/types.d.ts +313 -0
- package/dist/src/types.js +33 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/active-marketplace-runtime-lock.d.ts +12 -0
- package/dist/src/utils/active-marketplace-runtime-lock.js +133 -0
- package/dist/src/utils/active-marketplace-runtime-lock.js.map +1 -0
- package/dist/src/utils/agent-version.d.ts +13 -0
- package/dist/src/utils/agent-version.js +172 -0
- package/dist/src/utils/agent-version.js.map +1 -0
- package/dist/src/utils/artifact-plan.d.ts +26 -0
- package/dist/src/utils/artifact-plan.js +444 -0
- package/dist/src/utils/artifact-plan.js.map +1 -0
- package/dist/src/utils/artifact-renderer.d.ts +124 -0
- package/dist/src/utils/artifact-renderer.js +442 -0
- package/dist/src/utils/artifact-renderer.js.map +1 -0
- package/dist/src/utils/artifact-sources.d.ts +8 -0
- package/dist/src/utils/artifact-sources.js +10 -0
- package/dist/src/utils/artifact-sources.js.map +1 -0
- package/dist/src/utils/aw-home-mutation-lock.d.ts +10 -0
- package/dist/src/utils/aw-home-mutation-lock.js +55 -0
- package/dist/src/utils/aw-home-mutation-lock.js.map +1 -0
- package/dist/src/utils/bundle-integrity.d.ts +3 -0
- package/dist/src/utils/bundle-integrity.js +88 -0
- package/dist/src/utils/bundle-integrity.js.map +1 -0
- package/dist/src/utils/bundle-layout.d.ts +4 -0
- package/dist/src/utils/bundle-layout.js +546 -0
- package/dist/src/utils/bundle-layout.js.map +1 -0
- package/dist/src/utils/catalog-digests.d.ts +8 -0
- package/dist/src/utils/catalog-digests.js +108 -0
- package/dist/src/utils/catalog-digests.js.map +1 -0
- package/dist/src/utils/catalog-manifest-v3.d.ts +2 -0
- package/dist/src/utils/catalog-manifest-v3.js +3 -0
- package/dist/src/utils/catalog-manifest-v3.js.map +1 -0
- package/dist/src/utils/catalog-manifest-v4.d.ts +5 -0
- package/dist/src/utils/catalog-manifest-v4.js +106 -0
- package/dist/src/utils/catalog-manifest-v4.js.map +1 -0
- package/dist/src/utils/catalog-public-error.d.ts +15 -0
- package/dist/src/utils/catalog-public-error.js +84 -0
- package/dist/src/utils/catalog-public-error.js.map +1 -0
- package/dist/src/utils/catalog-runtime-projection.d.ts +36 -0
- package/dist/src/utils/catalog-runtime-projection.js +172 -0
- package/dist/src/utils/catalog-runtime-projection.js.map +1 -0
- package/dist/src/utils/catalog-snapshot.d.ts +8 -0
- package/dist/src/utils/catalog-snapshot.js +12 -0
- package/dist/src/utils/catalog-snapshot.js.map +1 -0
- package/dist/src/utils/catalog-source-cache.d.ts +61 -0
- package/dist/src/utils/catalog-source-cache.js +276 -0
- package/dist/src/utils/catalog-source-cache.js.map +1 -0
- package/dist/src/utils/catalog-source-identity.d.ts +4 -0
- package/dist/src/utils/catalog-source-identity.js +118 -0
- package/dist/src/utils/catalog-source-identity.js.map +1 -0
- package/dist/src/utils/catalog-source-presentation.d.ts +1 -0
- package/dist/src/utils/catalog-source-presentation.js +32 -0
- package/dist/src/utils/catalog-source-presentation.js.map +1 -0
- package/dist/src/utils/catalog-source-references.d.ts +65 -0
- package/dist/src/utils/catalog-source-references.js +1418 -0
- package/dist/src/utils/catalog-source-references.js.map +1 -0
- package/dist/src/utils/catalog-sources.d.ts +39 -0
- package/dist/src/utils/catalog-sources.js +293 -0
- package/dist/src/utils/catalog-sources.js.map +1 -0
- package/dist/src/utils/codex-config.d.ts +124 -0
- package/dist/src/utils/codex-config.js +1011 -0
- package/dist/src/utils/codex-config.js.map +1 -0
- package/dist/src/utils/codex-project-surface.d.ts +3 -0
- package/dist/src/utils/codex-project-surface.js +49 -0
- package/dist/src/utils/codex-project-surface.js.map +1 -0
- package/dist/src/utils/codex-roles.d.ts +71 -0
- package/dist/src/utils/codex-roles.js +257 -0
- package/dist/src/utils/codex-roles.js.map +1 -0
- package/dist/src/utils/codex-skill-staging.d.ts +9 -0
- package/dist/src/utils/codex-skill-staging.js +8 -0
- package/dist/src/utils/codex-skill-staging.js.map +1 -0
- package/dist/src/utils/config-value.d.ts +3 -0
- package/dist/src/utils/config-value.js +23 -0
- package/dist/src/utils/config-value.js.map +1 -0
- package/dist/src/utils/dynamic-skill-tree.d.ts +32 -0
- package/dist/src/utils/dynamic-skill-tree.js +436 -0
- package/dist/src/utils/dynamic-skill-tree.js.map +1 -0
- package/dist/src/utils/file-rules.d.ts +1 -0
- package/dist/src/utils/file-rules.js +2 -0
- package/dist/src/utils/file-rules.js.map +1 -0
- package/dist/src/utils/git-catalog-provider.d.ts +7 -0
- package/dist/src/utils/git-catalog-provider.js +510 -0
- package/dist/src/utils/git-catalog-provider.js.map +1 -0
- package/dist/src/utils/hooks-contract.d.ts +1 -0
- package/dist/src/utils/hooks-contract.js +2 -0
- package/dist/src/utils/hooks-contract.js.map +1 -0
- package/dist/src/utils/json.d.ts +4 -0
- package/dist/src/utils/json.js +29 -0
- package/dist/src/utils/json.js.map +1 -0
- package/dist/src/utils/lifecycle-active-store.d.ts +175 -0
- package/dist/src/utils/lifecycle-active-store.js +1276 -0
- package/dist/src/utils/lifecycle-active-store.js.map +1 -0
- package/dist/src/utils/local-marketplace.d.ts +41 -0
- package/dist/src/utils/local-marketplace.js +317 -0
- package/dist/src/utils/local-marketplace.js.map +1 -0
- package/dist/src/utils/manifest.d.ts +14 -0
- package/dist/src/utils/manifest.js +36 -0
- package/dist/src/utils/manifest.js.map +1 -0
- package/dist/src/utils/marketplace.d.ts +3 -0
- package/dist/src/utils/marketplace.js +76 -0
- package/dist/src/utils/marketplace.js.map +1 -0
- package/dist/src/utils/multi-source-catalog.d.ts +72 -0
- package/dist/src/utils/multi-source-catalog.js +402 -0
- package/dist/src/utils/multi-source-catalog.js.map +1 -0
- package/dist/src/utils/native-cli.d.ts +75 -0
- package/dist/src/utils/native-cli.js +435 -0
- package/dist/src/utils/native-cli.js.map +1 -0
- package/dist/src/utils/orphan-marketplace-cleanup.d.ts +18 -0
- package/dist/src/utils/orphan-marketplace-cleanup.js +202 -0
- package/dist/src/utils/orphan-marketplace-cleanup.js.map +1 -0
- package/dist/src/utils/ownership.d.ts +130 -0
- package/dist/src/utils/ownership.js +766 -0
- package/dist/src/utils/ownership.js.map +1 -0
- package/dist/src/utils/path-catalog-provider.d.ts +7 -0
- package/dist/src/utils/path-catalog-provider.js +87 -0
- package/dist/src/utils/path-catalog-provider.js.map +1 -0
- package/dist/src/utils/paths.d.ts +41 -0
- package/dist/src/utils/paths.js +138 -0
- package/dist/src/utils/paths.js.map +1 -0
- package/dist/src/utils/platform-entries.d.ts +1 -0
- package/dist/src/utils/platform-entries.js +2 -0
- package/dist/src/utils/platform-entries.js.map +1 -0
- package/dist/src/utils/platform-support.d.ts +1 -0
- package/dist/src/utils/platform-support.js +2 -0
- package/dist/src/utils/platform-support.js.map +1 -0
- package/dist/src/utils/plugin-authoring-capabilities.d.ts +23 -0
- package/dist/src/utils/plugin-authoring-capabilities.js +37 -0
- package/dist/src/utils/plugin-authoring-capabilities.js.map +1 -0
- package/dist/src/utils/plugin-authoring-capabilities.json +92 -0
- package/dist/src/utils/plugin-config-write.d.ts +107 -0
- package/dist/src/utils/plugin-config-write.js +565 -0
- package/dist/src/utils/plugin-config-write.js.map +1 -0
- package/dist/src/utils/plugin-config.d.ts +396 -0
- package/dist/src/utils/plugin-config.js +1898 -0
- package/dist/src/utils/plugin-config.js.map +1 -0
- package/dist/src/utils/plugin-dependencies.d.ts +12 -0
- package/dist/src/utils/plugin-dependencies.js +162 -0
- package/dist/src/utils/plugin-dependencies.js.map +1 -0
- package/dist/src/utils/plugin-release.d.ts +2 -0
- package/dist/src/utils/plugin-release.js +2 -0
- package/dist/src/utils/plugin-release.js.map +1 -0
- package/dist/src/utils/plugin-script-contract.d.ts +1 -0
- package/dist/src/utils/plugin-script-contract.js +2 -0
- package/dist/src/utils/plugin-script-contract.js.map +1 -0
- package/dist/src/utils/plugin-scripts.d.ts +172 -0
- package/dist/src/utils/plugin-scripts.js +585 -0
- package/dist/src/utils/plugin-scripts.js.map +1 -0
- package/dist/src/utils/process-lock.d.ts +47 -0
- package/dist/src/utils/process-lock.js +532 -0
- package/dist/src/utils/process-lock.js.map +1 -0
- package/dist/src/utils/process-tree.d.ts +2 -0
- package/dist/src/utils/process-tree.js +2 -0
- package/dist/src/utils/process-tree.js.map +1 -0
- package/dist/src/utils/project-boundary.d.ts +21 -0
- package/dist/src/utils/project-boundary.js +95 -0
- package/dist/src/utils/project-boundary.js.map +1 -0
- package/dist/src/utils/rendered-contract.d.ts +8 -0
- package/dist/src/utils/rendered-contract.js +134 -0
- package/dist/src/utils/rendered-contract.js.map +1 -0
- package/dist/src/utils/runtime-skill-closure.d.ts +13 -0
- package/dist/src/utils/runtime-skill-closure.js +863 -0
- package/dist/src/utils/runtime-skill-closure.js.map +1 -0
- package/dist/src/utils/skill-authoring.d.ts +1 -0
- package/dist/src/utils/skill-authoring.js +2 -0
- package/dist/src/utils/skill-authoring.js.map +1 -0
- package/dist/src/utils/skill-frontmatter-capabilities.d.ts +201 -0
- package/dist/src/utils/skill-frontmatter-capabilities.js +5 -0
- package/dist/src/utils/skill-frontmatter-capabilities.js.map +1 -0
- package/dist/src/utils/skill-frontmatter-capabilities.json +200 -0
- package/dist/src/utils/skill-validation.d.ts +37 -0
- package/dist/src/utils/skill-validation.js +326 -0
- package/dist/src/utils/skill-validation.js.map +1 -0
- package/dist/src/utils/slash-commands.d.ts +1 -0
- package/dist/src/utils/slash-commands.js +2 -0
- package/dist/src/utils/slash-commands.js.map +1 -0
- package/dist/src/utils/state-paths.d.ts +13 -0
- package/dist/src/utils/state-paths.js +129 -0
- package/dist/src/utils/state-paths.js.map +1 -0
- package/dist/src/utils/target-eligibility.d.ts +7 -0
- package/dist/src/utils/target-eligibility.js +26 -0
- package/dist/src/utils/target-eligibility.js.map +1 -0
- package/dist/src/utils/target-order.d.ts +7 -0
- package/dist/src/utils/target-order.js +10 -0
- package/dist/src/utils/target-order.js.map +1 -0
- package/dist/src/utils/template-engine.d.ts +1 -0
- package/dist/src/utils/template-engine.js +2 -0
- package/dist/src/utils/template-engine.js.map +1 -0
- package/dist/src/utils/test-path-fence.d.ts +12 -0
- package/dist/src/utils/test-path-fence.js +31 -0
- package/dist/src/utils/test-path-fence.js.map +1 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +16 -0
- package/dist/vitest.config.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,3289 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { generateAgentArtifacts } from '../generators/agents.js';
|
|
5
|
+
import { extractCodexHookGroups } from '../generators/hooks.js';
|
|
6
|
+
import { generateInstructionArtifacts, renderCodexManagedInstructionBlock, } from '../generators/instructions.js';
|
|
7
|
+
import { generateCodexPermissionArtifacts, mergeClaudePermissions, } from '../generators/permissions.js';
|
|
8
|
+
import { buildGeneratedName } from '../generators/shared.js';
|
|
9
|
+
import { applyConditionalCodexConfigTransactionFile, applyCodexConfigTransactionFile, composeManagedDeveloperInstructionsBlock, readCodexConfigFile, readRenderedCodexMcpServersFile, } from '../utils/codex-config.js';
|
|
10
|
+
import { createCodexSkillExpectedTree, createCodexSkillStagingTreeContentDigest, } from '../utils/codex-skill-staging.js';
|
|
11
|
+
import { createDynamicSkillExpectedTree, createDynamicSkillTreeContentDigest, skillDirectoryMatchesExpectedTree, validateSkillDirectory, writeDynamicSkillTree, } from '../utils/dynamic-skill-tree.js';
|
|
12
|
+
import { parseCodexSharedHooksFile, renderTrackedHookGroupIdentity, resolvePlatformHookDefinitions, serializeTrackedHookGroupIdentity, } from '../utils/hooks-contract.js';
|
|
13
|
+
import { buildCodexMarketplaceAddInvocation, buildCodexMarketplaceListInvocation, buildCodexMarketplaceRemoveInvocation, buildCodexPluginAddInvocation, buildCodexPluginListInvocation, buildCodexPluginRemoveInvocation, runNativeCliChecked, } from '../utils/native-cli.js';
|
|
14
|
+
import { OWNED_TEXT_HEADER_PREFIX, createSharedOwnerToken, createSkillOwnerMarkerContent, parseSharedOwnerToken, parseOwnedTextHeader, readSkillOwnerMarker, inspectOwnedComponents, writeOwnedTextFile, } from '../utils/ownership.js';
|
|
15
|
+
import { resolveArtifactRenderConfig, runArtifactRenderer } from '../utils/artifact-renderer.js';
|
|
16
|
+
import { validateArtifactPlan } from '../utils/artifact-plan.js';
|
|
17
|
+
import { readPluginManifest } from '../utils/manifest.js';
|
|
18
|
+
import { resolvePlatformEntries } from '../utils/platform-entries.js';
|
|
19
|
+
import { resolveAwHome, resolveClaudePaths, resolveCodexPaths, resolveRepoRoot, getCodexHome, } from '../utils/paths.js';
|
|
20
|
+
import { runLifecycleScript } from '../utils/plugin-scripts.js';
|
|
21
|
+
import { atomicRemoveConfigFile, atomicReplaceConfigFile, withConfigFileLock, } from '../utils/plugin-config.js';
|
|
22
|
+
import { withActiveMarketplaceLifecycleLease, withActiveMarketplaceRuntimeLease, } from '../utils/active-marketplace-runtime-lock.js';
|
|
23
|
+
import { readCatalogSourcesConfigSnapshot } from '../utils/catalog-sources.js';
|
|
24
|
+
import { getNamespacedMarketplaceName, projectMarketplaceManifestForPlatform, replayNamespacedCodexSkillOverlays, } from '../utils/local-marketplace.js';
|
|
25
|
+
import { cleanupOrphanNamespacedMarketplaceRoot, commitMarketplaceGenerationBackup, } from '../utils/orphan-marketplace-cleanup.js';
|
|
26
|
+
import { readClaudeSettings, writeClaudeSettings } from '../adapters/helpers.js';
|
|
27
|
+
import { requiresCodexNativePlugin } from '../utils/codex-project-surface.js';
|
|
28
|
+
import { assertCanonicalProjectManagedRoots } from '../utils/project-boundary.js';
|
|
29
|
+
import { findCodexRoleNameConflict, resolveCodexRoleNamespace, } from '../utils/codex-roles.js';
|
|
30
|
+
const MARKETPLACE_NAME = 'aw';
|
|
31
|
+
const MCP_OWNER_ENV = 'AW_AGENT_WORKFLOWS_OWNER';
|
|
32
|
+
function ownerIdentity(plugin, artifactKey, platform, scope) {
|
|
33
|
+
return { owner: 'agent-workflows', plugin, artifactKey, platform, scope };
|
|
34
|
+
}
|
|
35
|
+
function pluginOwnerIdentity(plugin, artifactKey, platform, scope) {
|
|
36
|
+
return {
|
|
37
|
+
...ownerIdentity(plugin.manifest.name, artifactKey, platform, scope),
|
|
38
|
+
...(plugin.sourceId ? {
|
|
39
|
+
sourceId: plugin.sourceId,
|
|
40
|
+
sourceIdentity: plugin.sourceIdentity,
|
|
41
|
+
pluginId: plugin.pluginId,
|
|
42
|
+
} : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function bindNamespacedOwnerIdentity(components, source) {
|
|
46
|
+
if (!source.sourceId)
|
|
47
|
+
return;
|
|
48
|
+
for (const component of components) {
|
|
49
|
+
component.identity.sourceId = source.sourceId;
|
|
50
|
+
component.identity.sourceIdentity = source.sourceIdentity;
|
|
51
|
+
component.identity.pluginId = source.pluginId;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function internalArtifactKey(kind, value) {
|
|
55
|
+
return `${kind}-${crypto.createHash('sha256').update(value).digest('hex').slice(0, 20)}`;
|
|
56
|
+
}
|
|
57
|
+
function pathComponent(options) {
|
|
58
|
+
if (options.kind !== 'skill-directory')
|
|
59
|
+
return options;
|
|
60
|
+
return {
|
|
61
|
+
...options,
|
|
62
|
+
validateHealth: async () => {
|
|
63
|
+
await validateSkillDirectory(options.path, options.identity.platform === 'codex' ? 'codex.skill' : 'claude.skill', { ownerMarkerPolicy: 'ignore' });
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function generatedTextWrite(options) {
|
|
68
|
+
const artifactPath = path.join(options.configDir, options.relativePath);
|
|
69
|
+
const identity = ownerIdentity(options.plugin.manifest.name, options.artifactKey, options.platform, options.scope);
|
|
70
|
+
const component = pathComponent({
|
|
71
|
+
kind: 'owned-text-file',
|
|
72
|
+
path: artifactPath,
|
|
73
|
+
allowedRoot: options.configDir,
|
|
74
|
+
identity,
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
artifact: {
|
|
78
|
+
path: artifactPath,
|
|
79
|
+
kind: options.kind,
|
|
80
|
+
platform: options.platform,
|
|
81
|
+
artifactKey: options.artifactKey,
|
|
82
|
+
},
|
|
83
|
+
component,
|
|
84
|
+
write: async () => {
|
|
85
|
+
await fs.ensureDir(options.configDir);
|
|
86
|
+
await writeOwnedTextFile({
|
|
87
|
+
path: artifactPath,
|
|
88
|
+
allowedRoot: options.configDir,
|
|
89
|
+
identity,
|
|
90
|
+
content: options.content,
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function mergePermissionStates(values) {
|
|
96
|
+
return {
|
|
97
|
+
allow: [...new Set(values.flatMap((value) => value?.allow ?? []))],
|
|
98
|
+
ask: [...new Set(values.flatMap((value) => value?.ask ?? []))],
|
|
99
|
+
deny: [...new Set(values.flatMap((value) => value?.deny ?? []))],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function subtractPermissionState(value, generated) {
|
|
103
|
+
const remove = {
|
|
104
|
+
allow: new Set(generated.allow),
|
|
105
|
+
ask: new Set(generated.ask),
|
|
106
|
+
deny: new Set(generated.deny),
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
allow: (value?.allow ?? []).filter((entry) => !remove.allow.has(entry)),
|
|
110
|
+
ask: (value?.ask ?? []).filter((entry) => !remove.ask.has(entry)),
|
|
111
|
+
deny: (value?.deny ?? []).filter((entry) => !remove.deny.has(entry)),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function intersectPermissionState(value, generated) {
|
|
115
|
+
const keep = {
|
|
116
|
+
allow: new Set(generated.allow),
|
|
117
|
+
ask: new Set(generated.ask),
|
|
118
|
+
deny: new Set(generated.deny),
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
allow: (value?.allow ?? []).filter((entry) => keep.allow.has(entry)),
|
|
122
|
+
ask: (value?.ask ?? []).filter((entry) => keep.ask.has(entry)),
|
|
123
|
+
deny: (value?.deny ?? []).filter((entry) => keep.deny.has(entry)),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function rebuildClaudePermissions(settings, ownerKey, desired) {
|
|
127
|
+
const previousGenerated = mergePermissionStates(Object.values(settings.awGenerated?.permissions ?? {}));
|
|
128
|
+
const currentPermissions = mergePermissionStates([settings.permissions]);
|
|
129
|
+
const manual = mergePermissionStates([
|
|
130
|
+
subtractPermissionState(currentPermissions, previousGenerated),
|
|
131
|
+
intersectPermissionState(intersectPermissionState(settings.awGenerated?.manualPermissions, previousGenerated), currentPermissions),
|
|
132
|
+
]);
|
|
133
|
+
settings.awGenerated ??= {};
|
|
134
|
+
settings.awGenerated.permissions ??= {};
|
|
135
|
+
if (desired)
|
|
136
|
+
settings.awGenerated.permissions[ownerKey] = desired;
|
|
137
|
+
else
|
|
138
|
+
delete settings.awGenerated.permissions[ownerKey];
|
|
139
|
+
if (Object.keys(settings.awGenerated.permissions).length === 0) {
|
|
140
|
+
delete settings.awGenerated.permissions;
|
|
141
|
+
}
|
|
142
|
+
if (manual.allow.length || manual.ask.length || manual.deny.length) {
|
|
143
|
+
settings.awGenerated.manualPermissions = manual;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
delete settings.awGenerated.manualPermissions;
|
|
147
|
+
}
|
|
148
|
+
settings.permissions = mergePermissionStates([
|
|
149
|
+
manual,
|
|
150
|
+
...Object.values(settings.awGenerated.permissions ?? {}),
|
|
151
|
+
]);
|
|
152
|
+
if (settings.awGenerated.permissions === undefined
|
|
153
|
+
&& settings.awGenerated.manualPermissions === undefined
|
|
154
|
+
&& settings.awGenerated.hooks === undefined
|
|
155
|
+
&& settings.awGenerated.manualHooks === undefined) {
|
|
156
|
+
delete settings.awGenerated;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function createClaudePermissionsComponent(options) {
|
|
160
|
+
const ownerKey = createSharedOwnerToken(options.identity);
|
|
161
|
+
return {
|
|
162
|
+
kind: 'shared-config',
|
|
163
|
+
identity: options.identity,
|
|
164
|
+
label: `${options.settingsPath}#awGenerated.permissions.${ownerKey}`,
|
|
165
|
+
inspect: async () => {
|
|
166
|
+
const settings = await readClaudeSettings(options.settingsPath);
|
|
167
|
+
return settings.awGenerated?.permissions?.[ownerKey] ? 'present' : 'absent';
|
|
168
|
+
},
|
|
169
|
+
remove: async () => {
|
|
170
|
+
const settings = await readClaudeSettings(options.settingsPath);
|
|
171
|
+
rebuildClaudePermissions(settings, ownerKey);
|
|
172
|
+
await writeClaudeSettings(options.settingsPath, settings);
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function createCodexInstructionComponent(options) {
|
|
177
|
+
const ownerKey = createSharedOwnerToken(options.identity);
|
|
178
|
+
return {
|
|
179
|
+
kind: 'managed-block',
|
|
180
|
+
identity: options.identity,
|
|
181
|
+
label: `${options.configPath}#developer_instructions:${ownerKey}`,
|
|
182
|
+
inspect: async () => {
|
|
183
|
+
const config = await readCodexConfigFile(options.configPath);
|
|
184
|
+
return config.snapshot.developerInstructions.managedBlocks[ownerKey]
|
|
185
|
+
? 'present'
|
|
186
|
+
: 'absent';
|
|
187
|
+
},
|
|
188
|
+
remove: async () => {
|
|
189
|
+
await applyCodexConfigTransactionFile(options.configPath, {
|
|
190
|
+
removeManagedInstructionPluginName: ownerKey,
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function codexMcpOwner(actual) {
|
|
196
|
+
const envValue = actual?.value?.env;
|
|
197
|
+
return envValue
|
|
198
|
+
&& typeof envValue === 'object'
|
|
199
|
+
&& !Array.isArray(envValue)
|
|
200
|
+
&& typeof envValue[MCP_OWNER_ENV] === 'string'
|
|
201
|
+
? envValue[MCP_OWNER_ENV]
|
|
202
|
+
: null;
|
|
203
|
+
}
|
|
204
|
+
function createCodexMcpComponent(options) {
|
|
205
|
+
const ownerKey = createSharedOwnerToken(options.identity);
|
|
206
|
+
return {
|
|
207
|
+
kind: 'shared-config',
|
|
208
|
+
identity: options.identity,
|
|
209
|
+
label: `${options.configPath}#mcp_servers.${options.serverKey}`,
|
|
210
|
+
inspect: async () => {
|
|
211
|
+
const config = await readCodexConfigFile(options.configPath);
|
|
212
|
+
const actual = config.snapshot.mcpServers[options.serverKey];
|
|
213
|
+
if (!actual)
|
|
214
|
+
return 'absent';
|
|
215
|
+
return codexMcpOwner(actual) === ownerKey ? 'present' : 'foreign';
|
|
216
|
+
},
|
|
217
|
+
remove: async () => {
|
|
218
|
+
await applyConditionalCodexConfigTransactionFile(options.configPath, (snapshot) => {
|
|
219
|
+
const actual = snapshot.mcpServers[options.serverKey];
|
|
220
|
+
return codexMcpOwner(actual) === ownerKey
|
|
221
|
+
? { removeMcpServers: [options.serverKey] }
|
|
222
|
+
: null;
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function codexRoleRegistrationOwned(options) {
|
|
228
|
+
if (!options.registration?.configFile)
|
|
229
|
+
return false;
|
|
230
|
+
const expected = path.normalize(path.relative(path.dirname(options.configPath), options.rolePath));
|
|
231
|
+
const actual = path.normalize(path.relative(path.dirname(options.configPath), path.resolve(path.dirname(options.configPath), options.registration.configFile)));
|
|
232
|
+
if (actual !== expected)
|
|
233
|
+
return false;
|
|
234
|
+
const raw = await fs.readFile(options.rolePath, 'utf8').catch(() => '');
|
|
235
|
+
try {
|
|
236
|
+
return Boolean(parseOwnedTextHeader(raw, options.fileIdentity));
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function createCodexRoleRegistrationComponent(options) {
|
|
243
|
+
const inspect = async () => {
|
|
244
|
+
const config = await readCodexConfigFile(options.configPath);
|
|
245
|
+
const registration = config.snapshot.agents[options.roleKey];
|
|
246
|
+
if (!registration)
|
|
247
|
+
return 'absent';
|
|
248
|
+
return await codexRoleRegistrationOwned({ ...options, registration })
|
|
249
|
+
? 'present'
|
|
250
|
+
: 'foreign';
|
|
251
|
+
};
|
|
252
|
+
return {
|
|
253
|
+
kind: 'shared-config',
|
|
254
|
+
identity: options.identity,
|
|
255
|
+
label: `${options.configPath}#agents.${options.roleKey}`,
|
|
256
|
+
inspect,
|
|
257
|
+
remove: async () => {
|
|
258
|
+
await applyConditionalCodexConfigTransactionFile(options.configPath, async (snapshot) => {
|
|
259
|
+
const registration = snapshot.agents[options.roleKey];
|
|
260
|
+
return await codexRoleRegistrationOwned({ ...options, registration })
|
|
261
|
+
? { removeAgentRegistrations: [options.roleKey] }
|
|
262
|
+
: null;
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async function readCodexHooks(hooksPath) {
|
|
268
|
+
if (!await fs.pathExists(hooksPath))
|
|
269
|
+
return parseCodexSharedHooksFile({ hooks: {} });
|
|
270
|
+
return parseCodexSharedHooksFile(await fs.readJson(hooksPath));
|
|
271
|
+
}
|
|
272
|
+
function renderCodexHookGroups(groups) {
|
|
273
|
+
const hooks = {};
|
|
274
|
+
for (const group of groups) {
|
|
275
|
+
hooks[group.event] ??= [];
|
|
276
|
+
hooks[group.event].push(group.rawGroup);
|
|
277
|
+
}
|
|
278
|
+
return `${JSON.stringify({ hooks }, null, 2)}\n`;
|
|
279
|
+
}
|
|
280
|
+
export async function applyCodexHookGroupsTransactionFile(hooksPath, update, testHooks) {
|
|
281
|
+
return withConfigFileLock({
|
|
282
|
+
filePath: hooksPath,
|
|
283
|
+
allowedRoot: path.dirname(hooksPath),
|
|
284
|
+
action: async () => {
|
|
285
|
+
const parsed = await readCodexHooks(hooksPath);
|
|
286
|
+
const groups = update(parsed);
|
|
287
|
+
if (!groups)
|
|
288
|
+
return false;
|
|
289
|
+
await testHooks?.beforeCommit?.();
|
|
290
|
+
if (groups.length === 0) {
|
|
291
|
+
await atomicRemoveConfigFile({
|
|
292
|
+
filePath: hooksPath,
|
|
293
|
+
allowedRoot: path.dirname(hooksPath),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
await atomicReplaceConfigFile({
|
|
298
|
+
filePath: hooksPath,
|
|
299
|
+
allowedRoot: path.dirname(hooksPath),
|
|
300
|
+
content: renderCodexHookGroups(groups),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function renderCodexSharedHookGroup(identity) {
|
|
308
|
+
const rendered = renderTrackedHookGroupIdentity(identity);
|
|
309
|
+
const rawGroup = {
|
|
310
|
+
...(rendered.matcher === undefined ? {} : { matcher: rendered.matcher }),
|
|
311
|
+
hooks: rendered.hooks,
|
|
312
|
+
};
|
|
313
|
+
return {
|
|
314
|
+
event: rendered.event,
|
|
315
|
+
matcher: rendered.matcher,
|
|
316
|
+
hooks: rendered.hooks,
|
|
317
|
+
rawGroup,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function createCodexHookComponent(options) {
|
|
321
|
+
const serialized = serializeTrackedHookGroupIdentity(options.identity);
|
|
322
|
+
return {
|
|
323
|
+
kind: 'shared-config',
|
|
324
|
+
identity: ownerIdentity(options.pluginName, internalArtifactKey('hook', serialized), 'codex', options.scope),
|
|
325
|
+
label: `${options.hooksPath}#${serialized}`,
|
|
326
|
+
inspect: async () => {
|
|
327
|
+
const parsed = await readCodexHooks(options.hooksPath);
|
|
328
|
+
return parsed.managedIdentities.some((entry) => serializeTrackedHookGroupIdentity(entry) === serialized)
|
|
329
|
+
? 'present'
|
|
330
|
+
: 'absent';
|
|
331
|
+
},
|
|
332
|
+
remove: async () => {
|
|
333
|
+
await applyCodexHookGroupsTransactionFile(options.hooksPath, (parsed) => {
|
|
334
|
+
let removed = false;
|
|
335
|
+
const remaining = parsed.groups.flatMap((entry) => {
|
|
336
|
+
if (!removed
|
|
337
|
+
&& entry.managedIdentity
|
|
338
|
+
&& serializeTrackedHookGroupIdentity(entry.managedIdentity) === serialized) {
|
|
339
|
+
removed = true;
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
return [entry.rawGroup];
|
|
343
|
+
});
|
|
344
|
+
return removed ? remaining : null;
|
|
345
|
+
});
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function ownedCodexHookIdentity(options) {
|
|
350
|
+
const artifactKey = internalArtifactKey('hook', serializeTrackedHookGroupIdentity(options.identity));
|
|
351
|
+
const identity = {
|
|
352
|
+
...ownerIdentity(options.pluginName, artifactKey, 'codex', options.scope),
|
|
353
|
+
...(options.sourceId ? {
|
|
354
|
+
sourceId: options.sourceId,
|
|
355
|
+
sourceIdentity: options.sourceIdentity,
|
|
356
|
+
pluginId: options.pluginId,
|
|
357
|
+
} : {}),
|
|
358
|
+
};
|
|
359
|
+
return {
|
|
360
|
+
...options.identity,
|
|
361
|
+
command: `${OWNED_TEXT_HEADER_PREFIX}${JSON.stringify(JSON.parse(createSkillOwnerMarkerContent(identity)))}\n${options.identity.command}`,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function sameOwnerIdentity(left, right) {
|
|
365
|
+
return left.owner === right.owner
|
|
366
|
+
&& left.sourceId === right.sourceId
|
|
367
|
+
&& left.sourceIdentity === right.sourceIdentity
|
|
368
|
+
&& left.pluginId === right.pluginId
|
|
369
|
+
&& left.plugin === right.plugin
|
|
370
|
+
&& left.artifactKey === right.artifactKey
|
|
371
|
+
&& left.platform === right.platform
|
|
372
|
+
&& left.scope === right.scope;
|
|
373
|
+
}
|
|
374
|
+
function readOwnedHookIdentity(identity, scope) {
|
|
375
|
+
try {
|
|
376
|
+
const parsed = parseOwnedTextHeader(identity.command);
|
|
377
|
+
return parsed?.identity.platform === 'codex' && parsed.identity.scope === scope
|
|
378
|
+
? parsed.identity
|
|
379
|
+
: null;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function sharedOwnerPluginName(value) {
|
|
386
|
+
return parseSharedOwnerToken(value)?.plugin ?? value;
|
|
387
|
+
}
|
|
388
|
+
async function scanCodexSharedComponents(options) {
|
|
389
|
+
const output = [];
|
|
390
|
+
const expectedIdentity = (artifactKey) => ({
|
|
391
|
+
...ownerIdentity(options.pluginName, artifactKey, 'codex', options.scope),
|
|
392
|
+
...(options.sourceId ? {
|
|
393
|
+
sourceId: options.sourceId,
|
|
394
|
+
sourceIdentity: options.sourceIdentity,
|
|
395
|
+
pluginId: options.pluginId,
|
|
396
|
+
} : {}),
|
|
397
|
+
});
|
|
398
|
+
const corruptedComponent = (filePath, error) => ({
|
|
399
|
+
kind: 'shared-config',
|
|
400
|
+
identity: ownerIdentity(options.pluginName, internalArtifactKey('corrupted-shared-config', filePath), 'codex', options.scope),
|
|
401
|
+
label: filePath,
|
|
402
|
+
inspect: async () => {
|
|
403
|
+
throw new Error(`包含 ${options.pluginName} 的 AW ownership 证据,但共享配置无法解析:${error instanceof Error ? error.message : String(error)}`);
|
|
404
|
+
},
|
|
405
|
+
remove: async () => undefined,
|
|
406
|
+
});
|
|
407
|
+
const containsOwnedEvidence = (raw) => raw.includes(options.pluginName) && (raw.includes('[[aw-managed:start plugin=')
|
|
408
|
+
|| raw.includes(MCP_OWNER_ENV)
|
|
409
|
+
|| raw.includes(OWNED_TEXT_HEADER_PREFIX)
|
|
410
|
+
|| raw.includes('agent-workflows'));
|
|
411
|
+
if (options.scanConfig) {
|
|
412
|
+
const config = await readCodexConfigFile(options.paths.configTomlPath).catch(async (error) => {
|
|
413
|
+
if (!options.ignoreReadErrors)
|
|
414
|
+
throw error;
|
|
415
|
+
const raw = await fs.readFile(options.paths.configTomlPath, 'utf8').catch(() => '');
|
|
416
|
+
if (containsOwnedEvidence(raw)) {
|
|
417
|
+
output.push(corruptedComponent(options.paths.configTomlPath, error));
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
});
|
|
421
|
+
const instructionIdentity = expectedIdentity('developer-instructions');
|
|
422
|
+
const instructionOwnerKey = createSharedOwnerToken(instructionIdentity);
|
|
423
|
+
if (config?.snapshot.developerInstructions.managedBlocks[instructionOwnerKey]) {
|
|
424
|
+
output.push(createCodexInstructionComponent({
|
|
425
|
+
identity: instructionIdentity,
|
|
426
|
+
scope: options.scope,
|
|
427
|
+
configPath: options.paths.configTomlPath,
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
for (const [serverKey, server] of Object.entries(config?.snapshot.mcpServers ?? {})) {
|
|
431
|
+
const envValue = server.value?.env;
|
|
432
|
+
if (envValue
|
|
433
|
+
&& typeof envValue === 'object'
|
|
434
|
+
&& !Array.isArray(envValue)
|
|
435
|
+
&& parseSharedOwnerToken(String(envValue[MCP_OWNER_ENV]))?.pluginId === options.pluginId) {
|
|
436
|
+
const identity = expectedIdentity(internalArtifactKey('mcp', serverKey));
|
|
437
|
+
if (createSharedOwnerToken(identity) !== envValue[MCP_OWNER_ENV])
|
|
438
|
+
continue;
|
|
439
|
+
output.push(createCodexMcpComponent({
|
|
440
|
+
identity,
|
|
441
|
+
scope: options.scope,
|
|
442
|
+
configPath: options.paths.configTomlPath,
|
|
443
|
+
serverKey,
|
|
444
|
+
owned: true,
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
for (const [roleKey, registration] of Object.entries(config?.snapshot.agents ?? {})) {
|
|
449
|
+
if (!registration.configFile)
|
|
450
|
+
continue;
|
|
451
|
+
const rolePath = path.resolve(path.dirname(options.paths.configTomlPath), registration.configFile);
|
|
452
|
+
const raw = await fs.readFile(rolePath, 'utf8').catch(() => '');
|
|
453
|
+
const parsed = (() => {
|
|
454
|
+
try {
|
|
455
|
+
return parseOwnedTextHeader(raw);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
})();
|
|
461
|
+
if (!parsed || !sameOwnerIdentity(parsed.identity, expectedIdentity(roleKey)))
|
|
462
|
+
continue;
|
|
463
|
+
output.push(createCodexRoleRegistrationComponent({
|
|
464
|
+
identity: expectedIdentity(internalArtifactKey('role', roleKey)),
|
|
465
|
+
fileIdentity: expectedIdentity(roleKey),
|
|
466
|
+
configPath: options.paths.configTomlPath,
|
|
467
|
+
roleKey,
|
|
468
|
+
rolePath,
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (options.scope === 'project' && options.scanHooks) {
|
|
473
|
+
const hooks = await readCodexHooks(options.paths.hooksPath).catch(async (error) => {
|
|
474
|
+
if (!options.ignoreReadErrors)
|
|
475
|
+
throw error;
|
|
476
|
+
const raw = await fs.readFile(options.paths.hooksPath, 'utf8').catch(() => '');
|
|
477
|
+
if (containsOwnedEvidence(raw)) {
|
|
478
|
+
output.push(corruptedComponent(options.paths.hooksPath, error));
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
});
|
|
482
|
+
for (const identity of hooks?.managedIdentities ?? []) {
|
|
483
|
+
const parsedIdentity = readOwnedHookIdentity(identity, options.scope);
|
|
484
|
+
if (!parsedIdentity || !sameOwnerIdentity(parsedIdentity, expectedIdentity(parsedIdentity.artifactKey)))
|
|
485
|
+
continue;
|
|
486
|
+
output.push(createCodexHookComponent({
|
|
487
|
+
pluginName: options.pluginName,
|
|
488
|
+
scope: options.scope,
|
|
489
|
+
hooksPath: options.paths.hooksPath,
|
|
490
|
+
identity,
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return output;
|
|
495
|
+
}
|
|
496
|
+
async function scanClaudeSharedComponents(options) {
|
|
497
|
+
const settings = await readClaudeSettings(options.settingsPath);
|
|
498
|
+
const identity = {
|
|
499
|
+
...ownerIdentity(options.pluginName, 'permissions', 'claude', options.scope),
|
|
500
|
+
...(options.sourceId ? {
|
|
501
|
+
sourceId: options.sourceId,
|
|
502
|
+
sourceIdentity: options.sourceIdentity,
|
|
503
|
+
pluginId: options.pluginId,
|
|
504
|
+
} : {}),
|
|
505
|
+
};
|
|
506
|
+
return settings.awGenerated?.permissions?.[createSharedOwnerToken(identity)]
|
|
507
|
+
? [createClaudePermissionsComponent({ ...options, identity })]
|
|
508
|
+
: [];
|
|
509
|
+
}
|
|
510
|
+
async function scanOwnedTextFiles(options) {
|
|
511
|
+
if (!await fs.pathExists(options.root))
|
|
512
|
+
return [];
|
|
513
|
+
const output = [];
|
|
514
|
+
const pending = [options.root];
|
|
515
|
+
while (pending.length > 0) {
|
|
516
|
+
const current = pending.pop();
|
|
517
|
+
for (const entry of await fs.readdir(current, { withFileTypes: true })) {
|
|
518
|
+
const entryPath = path.join(current, entry.name);
|
|
519
|
+
if (entry.isSymbolicLink())
|
|
520
|
+
continue;
|
|
521
|
+
if (entry.isDirectory()) {
|
|
522
|
+
pending.push(entryPath);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (!entry.isFile())
|
|
526
|
+
continue;
|
|
527
|
+
const raw = await fs.readFile(entryPath, 'utf8').catch(() => '');
|
|
528
|
+
if (!raw.includes(OWNED_TEXT_HEADER_PREFIX))
|
|
529
|
+
continue;
|
|
530
|
+
let parsed;
|
|
531
|
+
try {
|
|
532
|
+
parsed = parseOwnedTextHeader(raw);
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (parsed?.identity.plugin !== options.pluginName
|
|
538
|
+
|| (options.sourceId !== undefined && parsed.identity.sourceId !== options.sourceId)
|
|
539
|
+
|| parsed.identity.platform !== options.platform
|
|
540
|
+
|| parsed.identity.scope !== options.scope)
|
|
541
|
+
continue;
|
|
542
|
+
output.push(pathComponent({
|
|
543
|
+
kind: 'owned-text-file',
|
|
544
|
+
path: entryPath,
|
|
545
|
+
allowedRoot: options.root,
|
|
546
|
+
identity: parsed.identity,
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return output;
|
|
551
|
+
}
|
|
552
|
+
async function scanOwnedSkills(options) {
|
|
553
|
+
if (!await fs.pathExists(options.root))
|
|
554
|
+
return [];
|
|
555
|
+
const output = [];
|
|
556
|
+
const pending = [options.root];
|
|
557
|
+
while (pending.length > 0) {
|
|
558
|
+
const current = pending.pop();
|
|
559
|
+
for (const entry of await fs.readdir(current, { withFileTypes: true })) {
|
|
560
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
561
|
+
continue;
|
|
562
|
+
const skillRoot = path.join(current, entry.name);
|
|
563
|
+
const marker = await readSkillOwnerMarker({
|
|
564
|
+
skillRoot,
|
|
565
|
+
allowedRoot: current,
|
|
566
|
+
}).catch(() => null);
|
|
567
|
+
if (marker?.plugin === options.pluginName
|
|
568
|
+
&& (options.sourceId === undefined || marker.sourceId === options.sourceId)
|
|
569
|
+
&& marker.platform === options.platform
|
|
570
|
+
&& marker.scope === options.scope) {
|
|
571
|
+
output.push(pathComponent({
|
|
572
|
+
kind: 'skill-directory',
|
|
573
|
+
path: skillRoot,
|
|
574
|
+
allowedRoot: current,
|
|
575
|
+
identity: marker,
|
|
576
|
+
}));
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
pending.push(skillRoot);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return output;
|
|
583
|
+
}
|
|
584
|
+
function componentIdentityKey(component) {
|
|
585
|
+
return [
|
|
586
|
+
component.kind,
|
|
587
|
+
component.identity.artifactKey,
|
|
588
|
+
'path' in component ? path.resolve(component.path) : 'label' in component ? component.label : component.pluginId,
|
|
589
|
+
].join('\0');
|
|
590
|
+
}
|
|
591
|
+
function dedupeComponents(components) {
|
|
592
|
+
const seen = new Set();
|
|
593
|
+
return components.filter((component) => {
|
|
594
|
+
const key = componentIdentityKey(component);
|
|
595
|
+
if (seen.has(key))
|
|
596
|
+
return false;
|
|
597
|
+
seen.add(key);
|
|
598
|
+
return true;
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
async function preflightCodexWriteSafety(options) {
|
|
602
|
+
const pluginName = options.plugin.manifest.name;
|
|
603
|
+
const userConfigPath = resolveCodexPaths(options.cwd, 'user', options.env).configTomlPath;
|
|
604
|
+
const projectConfigPath = resolveCodexPaths(options.cwd, 'project', options.env).configTomlPath;
|
|
605
|
+
let userConfig;
|
|
606
|
+
try {
|
|
607
|
+
userConfig = await readCodexConfigFile(userConfigPath);
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
throw new Error(`codexProjectUserConfigUnreadable: 无法解析用户级 Codex 配置 ${userConfigPath}:${error instanceof Error ? error.message : String(error)}`);
|
|
611
|
+
}
|
|
612
|
+
if (options.scope === 'project') {
|
|
613
|
+
const expectedPluginId = `${pluginName}@${options.plugin.marketplaceName ?? MARKETPLACE_NAME}`;
|
|
614
|
+
for (const [key, plugin] of Object.entries(userConfig.snapshot.plugins)) {
|
|
615
|
+
if (key !== expectedPluginId
|
|
616
|
+
&& (key === pluginName || key.startsWith(`${pluginName}@`) || plugin.name === pluginName)) {
|
|
617
|
+
throw new Error(`externalOwnershipPresent: 用户级 Codex 已存在同名外部插件 ${key}。`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const roleKeys = new Set((options.plugin.manifest.artifacts?.targets ?? [])
|
|
622
|
+
.filter((target) => target.platform === 'codex'
|
|
623
|
+
&& target.scopes.includes(options.scope)
|
|
624
|
+
&& target.target === 'codex.agentRole'
|
|
625
|
+
&& target.roleKey)
|
|
626
|
+
.map((target) => target.roleKey));
|
|
627
|
+
const needsProjectConfig = options.scope === 'project' && Boolean(options.plugin.manifest.mcpServers
|
|
628
|
+
|| resolvePlatformEntries(options.plugin.manifest.extensions?.instructions, 'codex').length > 0
|
|
629
|
+
|| (options.plugin.manifest.artifacts?.targets ?? []).some((target) => target.platform === 'codex'
|
|
630
|
+
&& target.scopes.includes('project')
|
|
631
|
+
&& (target.target === 'codex.agentRole'
|
|
632
|
+
|| target.target === 'codex.developerInstructionsBlock')));
|
|
633
|
+
if (needsProjectConfig) {
|
|
634
|
+
try {
|
|
635
|
+
await readCodexConfigFile(projectConfigPath);
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
throw new Error(`projectConfigUnreadable: 无法解析项目级 Codex 配置 ${projectConfigPath}:${error instanceof Error ? error.message : String(error)}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (roleKeys.size === 0)
|
|
642
|
+
return;
|
|
643
|
+
for (const configPath of options.scope === 'project'
|
|
644
|
+
? [userConfigPath, projectConfigPath]
|
|
645
|
+
: [userConfigPath]) {
|
|
646
|
+
const configScope = configPath === userConfigPath ? 'user' : 'project';
|
|
647
|
+
const ownedRolePaths = [];
|
|
648
|
+
const ownedRoleKeys = [];
|
|
649
|
+
for (const roleKey of roleKeys) {
|
|
650
|
+
const rolePath = path.join(path.dirname(configPath), 'agents', `${buildGeneratedName(pluginName, roleKey)}.toml`);
|
|
651
|
+
const raw = await fs.readFile(rolePath, 'utf8').catch(() => '');
|
|
652
|
+
try {
|
|
653
|
+
if (parseOwnedTextHeader(raw, pluginOwnerIdentity(options.plugin, roleKey, 'codex', configScope))) {
|
|
654
|
+
ownedRolePaths.push(rolePath);
|
|
655
|
+
ownedRoleKeys.push(roleKey);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
// A foreign file remains external below.
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
let namespace;
|
|
663
|
+
try {
|
|
664
|
+
namespace = await resolveCodexRoleNamespace(configPath, {
|
|
665
|
+
strict: true,
|
|
666
|
+
awManagedTableKeys: ownedRoleKeys,
|
|
667
|
+
awManagedRoleLayerPaths: ownedRolePaths,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
throw new Error(`projectConfigUnreadable: 无法读取 Codex role namespace ${configPath}:${error instanceof Error ? error.message : String(error)}`);
|
|
672
|
+
}
|
|
673
|
+
for (const roleKey of roleKeys) {
|
|
674
|
+
const tableCollision = namespace.roles.find((role) => role.source === 'registration' && role.tableKey === roleKey && !role.awManaged);
|
|
675
|
+
const nameConflict = findCodexRoleNameConflict(namespace, roleKey);
|
|
676
|
+
if (tableCollision || nameConflict?.conflict) {
|
|
677
|
+
throw new Error(`roleNameConflict: Codex role "${roleKey}" 已由外部配置占用(${configPath})。`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function renderCodexRole(options) {
|
|
683
|
+
const stringValue = (value) => JSON.stringify(value.trim());
|
|
684
|
+
return [
|
|
685
|
+
`name = ${stringValue(options.roleKey)}`,
|
|
686
|
+
`description = ${stringValue(options.description)}`,
|
|
687
|
+
`developer_instructions = ${stringValue(options.developerInstructions)}`,
|
|
688
|
+
...(options.model ? [`model = ${stringValue(options.model)}`] : []),
|
|
689
|
+
...(options.modelReasoningEffort
|
|
690
|
+
? [`model_reasoning_effort = ${stringValue(options.modelReasoningEffort)}`]
|
|
691
|
+
: []),
|
|
692
|
+
'',
|
|
693
|
+
].join('\n');
|
|
694
|
+
}
|
|
695
|
+
async function resolveCodexMcpServers(plugin, scope) {
|
|
696
|
+
if (!plugin.manifest.mcpServers)
|
|
697
|
+
return {};
|
|
698
|
+
const relative = plugin.manifest.mcpServers.replace(/^\.\//u, '');
|
|
699
|
+
const direct = path.resolve(plugin.rootDir, relative);
|
|
700
|
+
const mcpPath = await fs.pathExists(direct)
|
|
701
|
+
? direct
|
|
702
|
+
: path.resolve(plugin.rootDir, 'codex', relative);
|
|
703
|
+
const servers = await readRenderedCodexMcpServersFile(mcpPath);
|
|
704
|
+
return Object.fromEntries(Object.entries(servers).map(([key, server]) => {
|
|
705
|
+
const existingEnv = server.env;
|
|
706
|
+
if (existingEnv !== undefined && (typeof existingEnv !== 'object' || Array.isArray(existingEnv))) {
|
|
707
|
+
throw new Error(`MCP server ${key} 的 env 必须是 object。`);
|
|
708
|
+
}
|
|
709
|
+
const env = (existingEnv ?? {});
|
|
710
|
+
const ownerKey = createSharedOwnerToken(pluginOwnerIdentity(plugin, internalArtifactKey('mcp', key), 'codex', scope));
|
|
711
|
+
if (env[MCP_OWNER_ENV] !== undefined
|
|
712
|
+
&& env[MCP_OWNER_ENV] !== plugin.manifest.name
|
|
713
|
+
&& env[MCP_OWNER_ENV] !== ownerKey) {
|
|
714
|
+
throw new Error(`MCP server ${key} 不能覆盖保留的 ${MCP_OWNER_ENV} owner marker。`);
|
|
715
|
+
}
|
|
716
|
+
return [key, {
|
|
717
|
+
...server,
|
|
718
|
+
env: { ...env, [MCP_OWNER_ENV]: ownerKey },
|
|
719
|
+
}];
|
|
720
|
+
}));
|
|
721
|
+
}
|
|
722
|
+
async function resolveDynamicArtifacts(options) {
|
|
723
|
+
const declarations = (options.plugin.manifest.artifacts?.targets ?? []).filter((target) => target.platform === options.platform && target.scopes.includes(options.scope));
|
|
724
|
+
if (declarations.length === 0)
|
|
725
|
+
return { artifacts: [], sourceDirs: () => [] };
|
|
726
|
+
const render = await runArtifactRenderer({
|
|
727
|
+
event: options.event,
|
|
728
|
+
plugin: options.plugin,
|
|
729
|
+
platform: options.platform,
|
|
730
|
+
scope: options.scope,
|
|
731
|
+
cwd: options.cwd,
|
|
732
|
+
commandEnv: options.env,
|
|
733
|
+
previousArtifacts: [...options.previousArtifacts],
|
|
734
|
+
scriptTimeoutMs: options.scriptTimeoutMs,
|
|
735
|
+
});
|
|
736
|
+
const config = await resolveArtifactRenderConfig({
|
|
737
|
+
sourceId: options.plugin.sourceId,
|
|
738
|
+
sourceIdentity: options.plugin.sourceIdentity,
|
|
739
|
+
pluginName: options.plugin.manifest.name,
|
|
740
|
+
manifest: options.plugin.manifest,
|
|
741
|
+
scope: options.scope,
|
|
742
|
+
cwd: options.cwd,
|
|
743
|
+
env: options.env,
|
|
744
|
+
});
|
|
745
|
+
const artifacts = validateArtifactPlan({
|
|
746
|
+
manifest: options.plugin.manifest,
|
|
747
|
+
plan: render.plan,
|
|
748
|
+
platform: options.platform,
|
|
749
|
+
scope: options.scope,
|
|
750
|
+
renderConfig: config,
|
|
751
|
+
rendererDigest: render.target.rendererDigest,
|
|
752
|
+
manifestDigest: render.target.manifestDigest,
|
|
753
|
+
}).artifacts;
|
|
754
|
+
return {
|
|
755
|
+
artifacts,
|
|
756
|
+
sourceDirs: (artifact) => render.context.artifactSources.find((entry) => entry.id === artifact.id && entry.target === artifact.target)?.sourceDirs ?? [],
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function dynamicSkillWrite(options) {
|
|
760
|
+
const skillKey = 'skillKey' in options.artifact ? options.artifact.skillKey : '';
|
|
761
|
+
const identity = ownerIdentity(options.plugin.manifest.name, skillKey, options.platform, options.scope);
|
|
762
|
+
const component = pathComponent({
|
|
763
|
+
kind: 'skill-directory',
|
|
764
|
+
path: options.skillPath,
|
|
765
|
+
allowedRoot: options.allowedRoot,
|
|
766
|
+
identity,
|
|
767
|
+
...(options.platform === 'codex' && options.scope === 'user'
|
|
768
|
+
? {
|
|
769
|
+
unownedIsPristine: (actualPath) => skillDirectoryMatchesExpectedTree(actualPath, options.tree),
|
|
770
|
+
}
|
|
771
|
+
: {}),
|
|
772
|
+
});
|
|
773
|
+
return {
|
|
774
|
+
artifact: {
|
|
775
|
+
path: options.skillPath,
|
|
776
|
+
paths: Object.keys(options.tree.files).map((relativePath) => path.join(options.skillPath, relativePath.split('/').join(path.sep))),
|
|
777
|
+
kind: 'skill',
|
|
778
|
+
platform: options.platform,
|
|
779
|
+
artifactKey: skillKey,
|
|
780
|
+
id: options.artifact.id,
|
|
781
|
+
target: options.artifact.target,
|
|
782
|
+
state: options.artifact.state,
|
|
783
|
+
...(options.artifact.disabledReason
|
|
784
|
+
? { disabledReason: options.artifact.disabledReason }
|
|
785
|
+
: {}),
|
|
786
|
+
refreshKeys: [...options.artifact.refreshKeys],
|
|
787
|
+
configDigest: options.artifact.configDigest,
|
|
788
|
+
contentDigest: options.platform === 'codex'
|
|
789
|
+
? createCodexSkillStagingTreeContentDigest(options.tree)
|
|
790
|
+
: createDynamicSkillTreeContentDigest(options.tree),
|
|
791
|
+
},
|
|
792
|
+
...(options.artifact.state === 'present' ? { component } : {}),
|
|
793
|
+
write: async () => {
|
|
794
|
+
if (options.artifact.state === 'disabled') {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
await writeDynamicSkillTree(options.skillPath, options.tree, {
|
|
798
|
+
allowedRoot: options.allowedRoot,
|
|
799
|
+
identity,
|
|
800
|
+
allowPristineUnowned: options.platform === 'codex' && options.scope === 'user',
|
|
801
|
+
});
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
async function resolveCodexSkillsRelativeRoot(plugin, marketplaceFallbackRoot) {
|
|
806
|
+
const manifestPaths = [
|
|
807
|
+
path.join(plugin.rootDir, '.codex-plugin', 'plugin.json'),
|
|
808
|
+
...(marketplaceFallbackRoot
|
|
809
|
+
? [path.join(marketplaceFallbackRoot, '.codex-plugin', 'plugin.json')]
|
|
810
|
+
: []),
|
|
811
|
+
];
|
|
812
|
+
let manifest = null;
|
|
813
|
+
for (const manifestPath of manifestPaths) {
|
|
814
|
+
manifest = await fs.readJson(manifestPath).catch(() => null);
|
|
815
|
+
if (manifest)
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
const raw = typeof manifest?.skills === 'string' && manifest.skills.trim() !== ''
|
|
819
|
+
? manifest.skills
|
|
820
|
+
: plugin.manifest.skills ?? './codex/skills';
|
|
821
|
+
const normalized = path.posix.normalize(raw.replace(/^\.\//u, ''));
|
|
822
|
+
if (normalized === '..' || normalized.startsWith('../') || path.isAbsolute(normalized)) {
|
|
823
|
+
throw new Error(`Codex plugin skills root 越界:${raw}`);
|
|
824
|
+
}
|
|
825
|
+
return normalized;
|
|
826
|
+
}
|
|
827
|
+
async function buildBundledCodexSkillWrites(options) {
|
|
828
|
+
const relativeRoot = await resolveCodexSkillsRelativeRoot(options.plugin);
|
|
829
|
+
const sourceRoot = path.join(options.plugin.rootDir, ...relativeRoot.split('/'));
|
|
830
|
+
if (!await fs.pathExists(sourceRoot))
|
|
831
|
+
return [];
|
|
832
|
+
const dynamicKeys = new Set((options.plugin.manifest.artifacts?.targets ?? [])
|
|
833
|
+
.filter((target) => target.target === 'codex.skill' && target.skillKey)
|
|
834
|
+
.map((target) => target.skillKey));
|
|
835
|
+
const targetRoot = options.scope === 'project'
|
|
836
|
+
? path.join(options.repoRoot, '.agents', 'skills')
|
|
837
|
+
: path.join(options.marketplaceRoot ?? resolveAwHome(options.env), 'plugins', options.plugin.manifest.name, ...relativeRoot.split('/'));
|
|
838
|
+
const writes = [];
|
|
839
|
+
for (const entry of await fs.readdir(sourceRoot, { withFileTypes: true })) {
|
|
840
|
+
if (entry.isSymbolicLink()) {
|
|
841
|
+
throw new Error(`Codex skills source 不能包含符号链接:${entry.name}`);
|
|
842
|
+
}
|
|
843
|
+
if (!entry.isDirectory() || dynamicKeys.has(entry.name))
|
|
844
|
+
continue;
|
|
845
|
+
const skillSource = path.join(sourceRoot, entry.name);
|
|
846
|
+
if (!await fs.pathExists(path.join(skillSource, 'SKILL.md')))
|
|
847
|
+
continue;
|
|
848
|
+
const tree = await createCodexSkillExpectedTree({
|
|
849
|
+
artifact: {
|
|
850
|
+
target: 'codex.skill',
|
|
851
|
+
skillKey: entry.name,
|
|
852
|
+
state: 'present',
|
|
853
|
+
content: { files: {} },
|
|
854
|
+
},
|
|
855
|
+
sourceDirs: [skillSource],
|
|
856
|
+
});
|
|
857
|
+
const skillPath = path.join(targetRoot, entry.name);
|
|
858
|
+
const canRecoverPristine = options.scope === 'user'
|
|
859
|
+
&& path.resolve(skillSource) !== path.resolve(skillPath);
|
|
860
|
+
const identity = ownerIdentity(options.plugin.manifest.name, entry.name, 'codex', options.scope);
|
|
861
|
+
const component = pathComponent({
|
|
862
|
+
kind: 'skill-directory',
|
|
863
|
+
path: skillPath,
|
|
864
|
+
allowedRoot: targetRoot,
|
|
865
|
+
identity,
|
|
866
|
+
...(canRecoverPristine
|
|
867
|
+
? {
|
|
868
|
+
unownedIsPristine: (actualPath) => skillDirectoryMatchesExpectedTree(actualPath, tree),
|
|
869
|
+
}
|
|
870
|
+
: {}),
|
|
871
|
+
});
|
|
872
|
+
writes.push({
|
|
873
|
+
artifact: {
|
|
874
|
+
path: skillPath,
|
|
875
|
+
paths: Object.keys(tree.files).map((relativePath) => path.join(skillPath, relativePath)),
|
|
876
|
+
kind: 'skill',
|
|
877
|
+
platform: 'codex',
|
|
878
|
+
artifactKey: entry.name,
|
|
879
|
+
},
|
|
880
|
+
component,
|
|
881
|
+
write: async () => {
|
|
882
|
+
await writeDynamicSkillTree(skillPath, tree, {
|
|
883
|
+
allowedRoot: targetRoot,
|
|
884
|
+
identity,
|
|
885
|
+
allowPristineUnowned: canRecoverPristine,
|
|
886
|
+
});
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
return writes;
|
|
891
|
+
}
|
|
892
|
+
async function buildClaudeWrites(options) {
|
|
893
|
+
const paths = resolveClaudePaths(options.cwd, options.scope, options.env);
|
|
894
|
+
const writes = [];
|
|
895
|
+
const dynamic = await resolveDynamicArtifacts({
|
|
896
|
+
...options,
|
|
897
|
+
platform: 'claude',
|
|
898
|
+
previousArtifacts: options.historicalArtifacts,
|
|
899
|
+
});
|
|
900
|
+
for (const artifact of dynamic.artifacts.filter((entry) => entry.target === 'claude.skill')) {
|
|
901
|
+
const skillKey = artifact.skillKey;
|
|
902
|
+
const tree = await createDynamicSkillExpectedTree({
|
|
903
|
+
artifact,
|
|
904
|
+
sourceDirs: dynamic.sourceDirs(artifact),
|
|
905
|
+
requireReadme: true,
|
|
906
|
+
});
|
|
907
|
+
writes.push(dynamicSkillWrite({
|
|
908
|
+
...options,
|
|
909
|
+
platform: 'claude',
|
|
910
|
+
artifact,
|
|
911
|
+
skillPath: path.join(paths.configDir, 'skills', skillKey),
|
|
912
|
+
allowedRoot: path.join(paths.configDir, 'skills'),
|
|
913
|
+
tree,
|
|
914
|
+
}));
|
|
915
|
+
}
|
|
916
|
+
for (const artifact of generateInstructionArtifacts(options.plugin.manifest.name, resolvePlatformEntries(options.plugin.manifest.extensions?.instructions, 'claude'), 'claude')) {
|
|
917
|
+
writes.push(generatedTextWrite({
|
|
918
|
+
...options,
|
|
919
|
+
platform: 'claude',
|
|
920
|
+
configDir: paths.configDir,
|
|
921
|
+
relativePath: artifact.relativePath,
|
|
922
|
+
artifactKey: artifact.artifactKey,
|
|
923
|
+
content: artifact.content,
|
|
924
|
+
kind: 'instruction',
|
|
925
|
+
}));
|
|
926
|
+
}
|
|
927
|
+
const permissions = resolvePlatformEntries(options.plugin.manifest.extensions?.permissions, 'claude');
|
|
928
|
+
if (permissions.length > 0) {
|
|
929
|
+
const component = createClaudePermissionsComponent({
|
|
930
|
+
identity: pluginOwnerIdentity(options.plugin, 'permissions', 'claude', options.scope),
|
|
931
|
+
scope: options.scope,
|
|
932
|
+
settingsPath: paths.settingsPath,
|
|
933
|
+
});
|
|
934
|
+
writes.push({
|
|
935
|
+
artifact: { path: paths.settingsPath, kind: 'permission', platform: 'claude' },
|
|
936
|
+
component,
|
|
937
|
+
write: async () => {
|
|
938
|
+
const settings = await readClaudeSettings(paths.settingsPath);
|
|
939
|
+
rebuildClaudePermissions(settings, createSharedOwnerToken(component.identity), mergeClaudePermissions(permissions));
|
|
940
|
+
await writeClaudeSettings(paths.settingsPath, settings);
|
|
941
|
+
},
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
return writes;
|
|
945
|
+
}
|
|
946
|
+
async function buildCodexWrites(options) {
|
|
947
|
+
const paths = resolveCodexPaths(options.cwd, options.scope, options.env);
|
|
948
|
+
const writes = [];
|
|
949
|
+
const dynamic = await resolveDynamicArtifacts({
|
|
950
|
+
...options,
|
|
951
|
+
platform: 'codex',
|
|
952
|
+
previousArtifacts: options.historicalArtifacts,
|
|
953
|
+
});
|
|
954
|
+
const dynamicInstructions = dynamic.artifacts.filter((entry) => entry.target === 'codex.developerInstructionsBlock');
|
|
955
|
+
const dynamicRoles = dynamic.artifacts.filter((entry) => entry.target === 'codex.agentRole');
|
|
956
|
+
const dynamicSkills = dynamic.artifacts.filter((entry) => entry.target === 'codex.skill');
|
|
957
|
+
const repoRoot = await resolveRepoRoot(options.cwd);
|
|
958
|
+
writes.push(...await buildBundledCodexSkillWrites({
|
|
959
|
+
plugin: options.plugin,
|
|
960
|
+
scope: options.scope,
|
|
961
|
+
repoRoot,
|
|
962
|
+
env: options.env,
|
|
963
|
+
marketplaceRoot: options.marketplaceRoot,
|
|
964
|
+
}));
|
|
965
|
+
const relativeSkillsRoot = await resolveCodexSkillsRelativeRoot(options.plugin);
|
|
966
|
+
const skillRoot = options.scope === 'user'
|
|
967
|
+
? path.join(options.marketplaceRoot ?? resolveAwHome(options.env), 'plugins', options.plugin.manifest.name, ...relativeSkillsRoot.split('/'))
|
|
968
|
+
: path.join(repoRoot, '.agents', 'skills');
|
|
969
|
+
for (const artifact of dynamicSkills) {
|
|
970
|
+
const tree = await createCodexSkillExpectedTree({
|
|
971
|
+
artifact,
|
|
972
|
+
sourceDirs: dynamic.sourceDirs(artifact),
|
|
973
|
+
requireReadme: true,
|
|
974
|
+
});
|
|
975
|
+
writes.push(dynamicSkillWrite({
|
|
976
|
+
...options,
|
|
977
|
+
platform: 'codex',
|
|
978
|
+
artifact,
|
|
979
|
+
skillPath: path.join(skillRoot, artifact.skillKey),
|
|
980
|
+
allowedRoot: skillRoot,
|
|
981
|
+
tree,
|
|
982
|
+
}));
|
|
983
|
+
}
|
|
984
|
+
const staticInstructions = dynamicInstructions.length > 0
|
|
985
|
+
? []
|
|
986
|
+
: resolvePlatformEntries(options.plugin.manifest.extensions?.instructions, 'codex');
|
|
987
|
+
const mcpServers = await resolveCodexMcpServers(options.plugin, options.scope);
|
|
988
|
+
const hasInstructionOutput = dynamicInstructions.length > 0 || staticInstructions.length > 0;
|
|
989
|
+
if (hasInstructionOutput) {
|
|
990
|
+
const component = createCodexInstructionComponent({
|
|
991
|
+
identity: pluginOwnerIdentity(options.plugin, 'developer-instructions', 'codex', options.scope),
|
|
992
|
+
scope: options.scope,
|
|
993
|
+
configPath: paths.configTomlPath,
|
|
994
|
+
});
|
|
995
|
+
const dynamicBlock = dynamicInstructions.length > 0
|
|
996
|
+
? composeManagedDeveloperInstructionsBlock(options.plugin.manifest.name, dynamicInstructions
|
|
997
|
+
.filter((entry) => entry.state === 'present')
|
|
998
|
+
.map((entry) => ({ artifactId: entry.id, value: entry.content })))
|
|
999
|
+
: undefined;
|
|
1000
|
+
const block = dynamicBlock?.value
|
|
1001
|
+
? { ...dynamicBlock, pluginName: createSharedOwnerToken(component.identity) }
|
|
1002
|
+
: staticInstructions.length > 0
|
|
1003
|
+
? {
|
|
1004
|
+
pluginName: createSharedOwnerToken(component.identity),
|
|
1005
|
+
value: renderCodexManagedInstructionBlock(options.plugin.manifest.name, staticInstructions),
|
|
1006
|
+
}
|
|
1007
|
+
: undefined;
|
|
1008
|
+
for (const artifact of dynamicInstructions.length > 0 ? dynamicInstructions : [{
|
|
1009
|
+
id: undefined,
|
|
1010
|
+
target: undefined,
|
|
1011
|
+
state: 'present',
|
|
1012
|
+
refreshKeys: [],
|
|
1013
|
+
configDigest: undefined,
|
|
1014
|
+
disabledReason: undefined,
|
|
1015
|
+
}]) {
|
|
1016
|
+
writes.push({
|
|
1017
|
+
artifact: {
|
|
1018
|
+
path: paths.configTomlPath,
|
|
1019
|
+
kind: 'instruction',
|
|
1020
|
+
platform: 'codex',
|
|
1021
|
+
...(artifact.id ? { id: artifact.id } : {}),
|
|
1022
|
+
...(artifact.target ? { target: artifact.target } : {}),
|
|
1023
|
+
state: artifact.state,
|
|
1024
|
+
refreshKeys: [...artifact.refreshKeys],
|
|
1025
|
+
...(artifact.configDigest ? { configDigest: artifact.configDigest } : {}),
|
|
1026
|
+
...(artifact.disabledReason ? { disabledReason: artifact.disabledReason } : {}),
|
|
1027
|
+
},
|
|
1028
|
+
...(block ? { component } : {}),
|
|
1029
|
+
write: async () => {
|
|
1030
|
+
await applyCodexConfigTransactionFile(paths.configTomlPath, block
|
|
1031
|
+
? { upsertManagedInstructionBlock: block }
|
|
1032
|
+
: { removeManagedInstructionPluginName: createSharedOwnerToken(component.identity) });
|
|
1033
|
+
},
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
for (const [serverKey, server] of Object.entries(mcpServers)) {
|
|
1038
|
+
const component = createCodexMcpComponent({
|
|
1039
|
+
identity: pluginOwnerIdentity(options.plugin, internalArtifactKey('mcp', serverKey), 'codex', options.scope),
|
|
1040
|
+
scope: options.scope,
|
|
1041
|
+
configPath: paths.configTomlPath,
|
|
1042
|
+
serverKey,
|
|
1043
|
+
owned: false,
|
|
1044
|
+
});
|
|
1045
|
+
writes.push({
|
|
1046
|
+
artifact: {
|
|
1047
|
+
path: paths.configTomlPath,
|
|
1048
|
+
kind: 'mcpServer',
|
|
1049
|
+
platform: 'codex',
|
|
1050
|
+
artifactKey: serverKey,
|
|
1051
|
+
},
|
|
1052
|
+
component,
|
|
1053
|
+
write: async () => {
|
|
1054
|
+
await applyConditionalCodexConfigTransactionFile(paths.configTomlPath, (snapshot) => {
|
|
1055
|
+
const actual = snapshot.mcpServers[serverKey];
|
|
1056
|
+
const actualOwner = codexMcpOwner(actual);
|
|
1057
|
+
if (actual && actualOwner !== createSharedOwnerToken(component.identity)) {
|
|
1058
|
+
throw new Error(`Codex MCP server "${serverKey}" is foreign and cannot be overwritten by plugin "${options.plugin.manifest.name}".`);
|
|
1059
|
+
}
|
|
1060
|
+
return { upsertMcpServers: { [serverKey]: server } };
|
|
1061
|
+
});
|
|
1062
|
+
},
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
for (const artifact of dynamicRoles) {
|
|
1066
|
+
const roleKey = artifact.roleKey;
|
|
1067
|
+
const rolePath = path.join(paths.configDir, 'agents', `${buildGeneratedName(options.plugin.manifest.name, roleKey)}.toml`);
|
|
1068
|
+
const roleComponent = pathComponent({
|
|
1069
|
+
kind: 'owned-text-file',
|
|
1070
|
+
path: rolePath,
|
|
1071
|
+
allowedRoot: paths.configDir,
|
|
1072
|
+
identity: pluginOwnerIdentity(options.plugin, roleKey, 'codex', options.scope),
|
|
1073
|
+
});
|
|
1074
|
+
const registrationComponent = createCodexRoleRegistrationComponent({
|
|
1075
|
+
identity: pluginOwnerIdentity(options.plugin, internalArtifactKey('role', roleKey), 'codex', options.scope),
|
|
1076
|
+
fileIdentity: roleComponent.identity,
|
|
1077
|
+
configPath: paths.configTomlPath,
|
|
1078
|
+
roleKey,
|
|
1079
|
+
rolePath,
|
|
1080
|
+
});
|
|
1081
|
+
const roleFile = artifact.content?.roleFile;
|
|
1082
|
+
const description = artifact.content?.registration?.description
|
|
1083
|
+
?? roleFile?.description;
|
|
1084
|
+
const present = artifact.state === 'present';
|
|
1085
|
+
if (present && (!description || !roleFile?.developer_instructions)) {
|
|
1086
|
+
throw new Error(`Artifact "${artifact.id}" 的 codex.agentRole 缺少 description 或 developer_instructions。`);
|
|
1087
|
+
}
|
|
1088
|
+
writes.push({
|
|
1089
|
+
artifact: {
|
|
1090
|
+
path: rolePath,
|
|
1091
|
+
paths: [paths.configTomlPath, rolePath],
|
|
1092
|
+
kind: 'agent',
|
|
1093
|
+
platform: 'codex',
|
|
1094
|
+
artifactKey: roleKey,
|
|
1095
|
+
roleKey,
|
|
1096
|
+
id: artifact.id,
|
|
1097
|
+
target: artifact.target,
|
|
1098
|
+
state: artifact.state,
|
|
1099
|
+
refreshKeys: [...artifact.refreshKeys],
|
|
1100
|
+
configDigest: artifact.configDigest,
|
|
1101
|
+
...(artifact.disabledReason ? { disabledReason: artifact.disabledReason } : {}),
|
|
1102
|
+
},
|
|
1103
|
+
...(present ? { component: roleComponent } : {}),
|
|
1104
|
+
...(present ? { additionalComponents: [registrationComponent] } : {}),
|
|
1105
|
+
write: async () => {
|
|
1106
|
+
if (!present) {
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
await fs.ensureDir(paths.configDir);
|
|
1110
|
+
await writeOwnedTextFile({
|
|
1111
|
+
path: rolePath,
|
|
1112
|
+
allowedRoot: paths.configDir,
|
|
1113
|
+
identity: roleComponent.identity,
|
|
1114
|
+
content: renderCodexRole({
|
|
1115
|
+
roleKey,
|
|
1116
|
+
description: description,
|
|
1117
|
+
developerInstructions: roleFile.developer_instructions,
|
|
1118
|
+
model: roleFile.model,
|
|
1119
|
+
modelReasoningEffort: roleFile.model_reasoning_effort,
|
|
1120
|
+
}),
|
|
1121
|
+
});
|
|
1122
|
+
const registration = {
|
|
1123
|
+
description,
|
|
1124
|
+
configFile: path.relative(path.dirname(paths.configTomlPath), rolePath).split(path.sep).join('/'),
|
|
1125
|
+
};
|
|
1126
|
+
await applyConditionalCodexConfigTransactionFile(paths.configTomlPath, async (snapshot) => {
|
|
1127
|
+
const actual = snapshot.agents[roleKey];
|
|
1128
|
+
if (actual && !await codexRoleRegistrationOwned({
|
|
1129
|
+
fileIdentity: roleComponent.identity,
|
|
1130
|
+
configPath: paths.configTomlPath,
|
|
1131
|
+
roleKey,
|
|
1132
|
+
rolePath,
|
|
1133
|
+
registration: actual,
|
|
1134
|
+
})) {
|
|
1135
|
+
throw new Error(`Codex agent registration "${roleKey}" is foreign and cannot be overwritten by plugin "${options.plugin.manifest.name}".`);
|
|
1136
|
+
}
|
|
1137
|
+
return { upsertAgentRegistrations: { [roleKey]: registration } };
|
|
1138
|
+
});
|
|
1139
|
+
},
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
for (const artifact of generateCodexPermissionArtifacts(options.plugin.manifest.name, resolvePlatformEntries(options.plugin.manifest.extensions?.permissions, 'codex'))) {
|
|
1143
|
+
writes.push(generatedTextWrite({
|
|
1144
|
+
...options,
|
|
1145
|
+
platform: 'codex',
|
|
1146
|
+
configDir: paths.configDir,
|
|
1147
|
+
relativePath: artifact.relativePath,
|
|
1148
|
+
artifactKey: artifact.artifactKey,
|
|
1149
|
+
content: artifact.content,
|
|
1150
|
+
kind: 'permission',
|
|
1151
|
+
}));
|
|
1152
|
+
}
|
|
1153
|
+
for (const artifact of generateAgentArtifacts(options.plugin.manifest.name, resolvePlatformEntries(options.plugin.manifest.extensions?.agents, 'codex'), 'codex')) {
|
|
1154
|
+
writes.push(generatedTextWrite({
|
|
1155
|
+
...options,
|
|
1156
|
+
platform: 'codex',
|
|
1157
|
+
configDir: paths.configDir,
|
|
1158
|
+
relativePath: artifact.relativePath,
|
|
1159
|
+
artifactKey: artifact.artifactKey,
|
|
1160
|
+
content: artifact.content,
|
|
1161
|
+
kind: 'agent',
|
|
1162
|
+
}));
|
|
1163
|
+
}
|
|
1164
|
+
if (options.scope === 'project') {
|
|
1165
|
+
const hooks = resolvePlatformHookDefinitions(options.plugin.manifest, 'codex');
|
|
1166
|
+
for (const rawIdentity of extractCodexHookGroups(hooks)) {
|
|
1167
|
+
const identity = ownedCodexHookIdentity({
|
|
1168
|
+
pluginName: options.plugin.manifest.name,
|
|
1169
|
+
sourceId: options.plugin.sourceId,
|
|
1170
|
+
sourceIdentity: options.plugin.sourceIdentity,
|
|
1171
|
+
pluginId: options.plugin.pluginId,
|
|
1172
|
+
scope: options.scope,
|
|
1173
|
+
identity: rawIdentity,
|
|
1174
|
+
});
|
|
1175
|
+
const component = createCodexHookComponent({
|
|
1176
|
+
pluginName: options.plugin.manifest.name,
|
|
1177
|
+
scope: options.scope,
|
|
1178
|
+
hooksPath: paths.hooksPath,
|
|
1179
|
+
identity,
|
|
1180
|
+
});
|
|
1181
|
+
writes.push({
|
|
1182
|
+
artifact: {
|
|
1183
|
+
path: paths.hooksPath,
|
|
1184
|
+
kind: 'hook',
|
|
1185
|
+
platform: 'codex',
|
|
1186
|
+
hookGroups: [identity],
|
|
1187
|
+
},
|
|
1188
|
+
component,
|
|
1189
|
+
write: async () => {
|
|
1190
|
+
await applyCodexHookGroupsTransactionFile(paths.hooksPath, (parsed) => {
|
|
1191
|
+
const serialized = serializeTrackedHookGroupIdentity(identity);
|
|
1192
|
+
const exists = parsed.managedIdentities.some((entry) => serializeTrackedHookGroupIdentity(entry) === serialized);
|
|
1193
|
+
return exists
|
|
1194
|
+
? null
|
|
1195
|
+
: [...parsed.rawGroups, renderCodexSharedHookGroup(identity)];
|
|
1196
|
+
});
|
|
1197
|
+
},
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return writes;
|
|
1202
|
+
}
|
|
1203
|
+
async function buildInspectionWrites(options) {
|
|
1204
|
+
const writes = [];
|
|
1205
|
+
if (options.platform === 'claude') {
|
|
1206
|
+
const paths = resolveClaudePaths(options.cwd, options.scope, options.env);
|
|
1207
|
+
for (const artifact of generateInstructionArtifacts(options.plugin.manifest.name, resolvePlatformEntries(options.plugin.manifest.extensions?.instructions, 'claude'), 'claude')) {
|
|
1208
|
+
writes.push(generatedTextWrite({
|
|
1209
|
+
...options,
|
|
1210
|
+
configDir: paths.configDir,
|
|
1211
|
+
relativePath: artifact.relativePath,
|
|
1212
|
+
artifactKey: artifact.artifactKey,
|
|
1213
|
+
content: artifact.content,
|
|
1214
|
+
kind: 'instruction',
|
|
1215
|
+
}));
|
|
1216
|
+
}
|
|
1217
|
+
if (resolvePlatformEntries(options.plugin.manifest.extensions?.permissions, 'claude').length > 0) {
|
|
1218
|
+
writes.push({
|
|
1219
|
+
artifact: { path: paths.settingsPath, kind: 'permission', platform: 'claude' },
|
|
1220
|
+
component: createClaudePermissionsComponent({
|
|
1221
|
+
identity: pluginOwnerIdentity(options.plugin, 'permissions', 'claude', options.scope),
|
|
1222
|
+
scope: options.scope,
|
|
1223
|
+
settingsPath: paths.settingsPath,
|
|
1224
|
+
}),
|
|
1225
|
+
write: async () => undefined,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
for (const target of options.plugin.manifest.artifacts?.targets ?? []) {
|
|
1229
|
+
if (target.platform !== 'claude'
|
|
1230
|
+
|| !target.scopes.includes(options.scope)
|
|
1231
|
+
|| target.target !== 'claude.skill'
|
|
1232
|
+
|| !target.skillKey)
|
|
1233
|
+
continue;
|
|
1234
|
+
const state = target.states.includes('present') ? 'present' : 'disabled';
|
|
1235
|
+
const skillPath = path.join(paths.configDir, 'skills', target.skillKey);
|
|
1236
|
+
const component = pathComponent({
|
|
1237
|
+
kind: 'skill-directory',
|
|
1238
|
+
path: skillPath,
|
|
1239
|
+
allowedRoot: path.dirname(skillPath),
|
|
1240
|
+
identity: ownerIdentity(options.plugin.manifest.name, target.skillKey, 'claude', options.scope),
|
|
1241
|
+
});
|
|
1242
|
+
writes.push({
|
|
1243
|
+
artifact: {
|
|
1244
|
+
path: skillPath,
|
|
1245
|
+
kind: 'skill',
|
|
1246
|
+
platform: 'claude',
|
|
1247
|
+
artifactKey: target.skillKey,
|
|
1248
|
+
id: target.id,
|
|
1249
|
+
target: target.target,
|
|
1250
|
+
state,
|
|
1251
|
+
},
|
|
1252
|
+
...(state === 'present' ? { component } : {}),
|
|
1253
|
+
...(target.states.includes('present') && target.states.includes('disabled')
|
|
1254
|
+
? { optional: true }
|
|
1255
|
+
: {}),
|
|
1256
|
+
write: async () => undefined,
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
return writes;
|
|
1260
|
+
}
|
|
1261
|
+
const paths = resolveCodexPaths(options.cwd, options.scope, options.env);
|
|
1262
|
+
const repoRoot = await resolveRepoRoot(options.cwd);
|
|
1263
|
+
writes.push(...await buildBundledCodexSkillWrites({
|
|
1264
|
+
plugin: options.plugin,
|
|
1265
|
+
scope: options.scope,
|
|
1266
|
+
repoRoot,
|
|
1267
|
+
env: options.env,
|
|
1268
|
+
marketplaceRoot: options.marketplaceRoot,
|
|
1269
|
+
}));
|
|
1270
|
+
const instructions = resolvePlatformEntries(options.plugin.manifest.extensions?.instructions, 'codex');
|
|
1271
|
+
const targets = (options.plugin.manifest.artifacts?.targets ?? []).filter((target) => target.platform === 'codex' && target.scopes.includes(options.scope));
|
|
1272
|
+
const instructionTargets = targets.filter((target) => target.target === 'codex.developerInstructionsBlock');
|
|
1273
|
+
const presentCapableInstructionTargets = instructionTargets.filter((target) => target.states.includes('present'));
|
|
1274
|
+
const instructionDesired = presentCapableInstructionTargets.length > 0;
|
|
1275
|
+
const instructionOptional = presentCapableInstructionTargets.length > 0
|
|
1276
|
+
&& presentCapableInstructionTargets.every((target) => target.states.includes('disabled'));
|
|
1277
|
+
if (instructions.length > 0 || instructionDesired) {
|
|
1278
|
+
writes.push({
|
|
1279
|
+
artifact: { path: paths.configTomlPath, kind: 'instruction', platform: 'codex' },
|
|
1280
|
+
component: createCodexInstructionComponent({
|
|
1281
|
+
identity: pluginOwnerIdentity(options.plugin, 'developer-instructions', 'codex', options.scope),
|
|
1282
|
+
scope: options.scope,
|
|
1283
|
+
configPath: paths.configTomlPath,
|
|
1284
|
+
}),
|
|
1285
|
+
...(instructions.length === 0 && instructionOptional ? { optional: true } : {}),
|
|
1286
|
+
write: async () => undefined,
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
const mcpServers = await resolveCodexMcpServers(options.plugin, options.scope);
|
|
1290
|
+
for (const serverKey of Object.keys(mcpServers)) {
|
|
1291
|
+
writes.push({
|
|
1292
|
+
artifact: {
|
|
1293
|
+
path: paths.configTomlPath,
|
|
1294
|
+
kind: 'mcpServer',
|
|
1295
|
+
platform: 'codex',
|
|
1296
|
+
artifactKey: serverKey,
|
|
1297
|
+
},
|
|
1298
|
+
component: createCodexMcpComponent({
|
|
1299
|
+
identity: pluginOwnerIdentity(options.plugin, internalArtifactKey('mcp', serverKey), 'codex', options.scope),
|
|
1300
|
+
scope: options.scope,
|
|
1301
|
+
configPath: paths.configTomlPath,
|
|
1302
|
+
serverKey,
|
|
1303
|
+
owned: false,
|
|
1304
|
+
}),
|
|
1305
|
+
write: async () => undefined,
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
const relativeSkillsRoot = await resolveCodexSkillsRelativeRoot(options.plugin);
|
|
1309
|
+
const dynamicSkillRoot = options.scope === 'user'
|
|
1310
|
+
? path.join(options.marketplaceRoot ?? resolveAwHome(options.env), 'plugins', options.plugin.manifest.name, ...relativeSkillsRoot.split('/'))
|
|
1311
|
+
: path.join(repoRoot, '.agents', 'skills');
|
|
1312
|
+
for (const target of targets) {
|
|
1313
|
+
const state = target.states.includes('present') ? 'present' : 'disabled';
|
|
1314
|
+
if (target.target === 'codex.skill' && target.skillKey) {
|
|
1315
|
+
const skillPath = path.join(dynamicSkillRoot, target.skillKey);
|
|
1316
|
+
const component = pathComponent({
|
|
1317
|
+
kind: 'skill-directory',
|
|
1318
|
+
path: skillPath,
|
|
1319
|
+
allowedRoot: dynamicSkillRoot,
|
|
1320
|
+
identity: ownerIdentity(options.plugin.manifest.name, target.skillKey, 'codex', options.scope),
|
|
1321
|
+
});
|
|
1322
|
+
writes.push({
|
|
1323
|
+
artifact: {
|
|
1324
|
+
path: skillPath,
|
|
1325
|
+
kind: 'skill',
|
|
1326
|
+
platform: 'codex',
|
|
1327
|
+
artifactKey: target.skillKey,
|
|
1328
|
+
id: target.id,
|
|
1329
|
+
target: target.target,
|
|
1330
|
+
state,
|
|
1331
|
+
},
|
|
1332
|
+
...(state === 'present' ? { component } : {}),
|
|
1333
|
+
...(target.states.includes('present') && target.states.includes('disabled')
|
|
1334
|
+
? { optional: true }
|
|
1335
|
+
: {}),
|
|
1336
|
+
write: async () => undefined,
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
if (target.target === 'codex.agentRole' && target.roleKey) {
|
|
1340
|
+
const rolePath = path.join(paths.configDir, 'agents', `${buildGeneratedName(options.plugin.manifest.name, target.roleKey)}.toml`);
|
|
1341
|
+
const roleComponent = pathComponent({
|
|
1342
|
+
kind: 'owned-text-file',
|
|
1343
|
+
path: rolePath,
|
|
1344
|
+
allowedRoot: paths.configDir,
|
|
1345
|
+
identity: pluginOwnerIdentity(options.plugin, target.roleKey, 'codex', options.scope),
|
|
1346
|
+
});
|
|
1347
|
+
const registrationComponent = createCodexRoleRegistrationComponent({
|
|
1348
|
+
identity: pluginOwnerIdentity(options.plugin, internalArtifactKey('role', target.roleKey), 'codex', options.scope),
|
|
1349
|
+
fileIdentity: pluginOwnerIdentity(options.plugin, target.roleKey, 'codex', options.scope),
|
|
1350
|
+
configPath: paths.configTomlPath,
|
|
1351
|
+
roleKey: target.roleKey,
|
|
1352
|
+
rolePath,
|
|
1353
|
+
});
|
|
1354
|
+
writes.push({
|
|
1355
|
+
artifact: {
|
|
1356
|
+
path: rolePath,
|
|
1357
|
+
kind: 'agent',
|
|
1358
|
+
platform: 'codex',
|
|
1359
|
+
artifactKey: target.roleKey,
|
|
1360
|
+
roleKey: target.roleKey,
|
|
1361
|
+
id: target.id,
|
|
1362
|
+
target: target.target,
|
|
1363
|
+
state,
|
|
1364
|
+
},
|
|
1365
|
+
...(state === 'present'
|
|
1366
|
+
? { component: roleComponent, additionalComponents: [registrationComponent] }
|
|
1367
|
+
: {}),
|
|
1368
|
+
...(target.states.includes('present') && target.states.includes('disabled')
|
|
1369
|
+
? { optional: true }
|
|
1370
|
+
: {}),
|
|
1371
|
+
write: async () => undefined,
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
for (const artifact of generateCodexPermissionArtifacts(options.plugin.manifest.name, resolvePlatformEntries(options.plugin.manifest.extensions?.permissions, 'codex'))) {
|
|
1376
|
+
writes.push(generatedTextWrite({
|
|
1377
|
+
...options,
|
|
1378
|
+
configDir: paths.configDir,
|
|
1379
|
+
relativePath: artifact.relativePath,
|
|
1380
|
+
artifactKey: artifact.artifactKey,
|
|
1381
|
+
content: artifact.content,
|
|
1382
|
+
kind: 'permission',
|
|
1383
|
+
}));
|
|
1384
|
+
}
|
|
1385
|
+
for (const artifact of generateAgentArtifacts(options.plugin.manifest.name, resolvePlatformEntries(options.plugin.manifest.extensions?.agents, 'codex'), 'codex')) {
|
|
1386
|
+
writes.push(generatedTextWrite({
|
|
1387
|
+
...options,
|
|
1388
|
+
configDir: paths.configDir,
|
|
1389
|
+
relativePath: artifact.relativePath,
|
|
1390
|
+
artifactKey: artifact.artifactKey,
|
|
1391
|
+
content: artifact.content,
|
|
1392
|
+
kind: 'agent',
|
|
1393
|
+
}));
|
|
1394
|
+
}
|
|
1395
|
+
if (options.scope === 'project') {
|
|
1396
|
+
for (const rawIdentity of extractCodexHookGroups(resolvePlatformHookDefinitions(options.plugin.manifest, 'codex'))) {
|
|
1397
|
+
const identity = ownedCodexHookIdentity({
|
|
1398
|
+
pluginName: options.plugin.manifest.name,
|
|
1399
|
+
sourceId: options.plugin.sourceId,
|
|
1400
|
+
sourceIdentity: options.plugin.sourceIdentity,
|
|
1401
|
+
pluginId: options.plugin.pluginId,
|
|
1402
|
+
scope: options.scope,
|
|
1403
|
+
identity: rawIdentity,
|
|
1404
|
+
});
|
|
1405
|
+
writes.push({
|
|
1406
|
+
artifact: {
|
|
1407
|
+
path: paths.hooksPath,
|
|
1408
|
+
kind: 'hook',
|
|
1409
|
+
platform: 'codex',
|
|
1410
|
+
hookGroups: [identity],
|
|
1411
|
+
},
|
|
1412
|
+
component: createCodexHookComponent({
|
|
1413
|
+
pluginName: options.plugin.manifest.name,
|
|
1414
|
+
scope: options.scope,
|
|
1415
|
+
hooksPath: paths.hooksPath,
|
|
1416
|
+
identity,
|
|
1417
|
+
}),
|
|
1418
|
+
write: async () => undefined,
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return writes;
|
|
1423
|
+
}
|
|
1424
|
+
async function buildHistoricalComponents(options) {
|
|
1425
|
+
const components = [];
|
|
1426
|
+
const paths = options.platform === 'codex'
|
|
1427
|
+
? resolveCodexPaths(options.cwd, options.scope, options.env)
|
|
1428
|
+
: resolveClaudePaths(options.cwd, options.scope, options.env);
|
|
1429
|
+
const historicalIdentity = (artifactKey) => ({
|
|
1430
|
+
...ownerIdentity(options.pluginName, artifactKey, options.platform, options.scope),
|
|
1431
|
+
...(options.sourceId ? {
|
|
1432
|
+
sourceId: options.sourceId,
|
|
1433
|
+
sourceIdentity: options.sourceIdentity,
|
|
1434
|
+
pluginId: options.pluginId,
|
|
1435
|
+
} : {}),
|
|
1436
|
+
});
|
|
1437
|
+
for (const artifact of options.artifacts) {
|
|
1438
|
+
if (artifact.platform !== options.platform)
|
|
1439
|
+
continue;
|
|
1440
|
+
if (artifact.state === 'disabled')
|
|
1441
|
+
continue;
|
|
1442
|
+
if (artifact.kind === 'hook' && options.platform === 'codex') {
|
|
1443
|
+
for (const identity of artifact.hookGroups ?? []) {
|
|
1444
|
+
components.push(createCodexHookComponent({
|
|
1445
|
+
pluginName: options.pluginName,
|
|
1446
|
+
scope: options.scope,
|
|
1447
|
+
hooksPath: resolveCodexPaths(options.cwd, options.scope, options.env).hooksPath,
|
|
1448
|
+
identity,
|
|
1449
|
+
}));
|
|
1450
|
+
}
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
if (artifact.kind === 'mcpServer' && options.platform === 'codex' && artifact.artifactKey) {
|
|
1454
|
+
components.push(createCodexMcpComponent({
|
|
1455
|
+
identity: historicalIdentity(internalArtifactKey('mcp', artifact.artifactKey)),
|
|
1456
|
+
scope: options.scope,
|
|
1457
|
+
configPath: resolveCodexPaths(options.cwd, options.scope, options.env).configTomlPath,
|
|
1458
|
+
serverKey: artifact.artifactKey,
|
|
1459
|
+
owned: false,
|
|
1460
|
+
}));
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
if (artifact.kind === 'instruction' && options.platform === 'codex' && !artifact.artifactKey) {
|
|
1464
|
+
components.push(createCodexInstructionComponent({
|
|
1465
|
+
identity: historicalIdentity('developer-instructions'),
|
|
1466
|
+
scope: options.scope,
|
|
1467
|
+
configPath: resolveCodexPaths(options.cwd, options.scope, options.env).configTomlPath,
|
|
1468
|
+
}));
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
if (artifact.kind === 'permission' && options.platform === 'claude') {
|
|
1472
|
+
components.push(createClaudePermissionsComponent({
|
|
1473
|
+
identity: historicalIdentity('permissions'),
|
|
1474
|
+
scope: options.scope,
|
|
1475
|
+
settingsPath: resolveClaudePaths(options.cwd, options.scope, options.env).settingsPath,
|
|
1476
|
+
}));
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
if (artifact.target === 'codex.agentRole'
|
|
1480
|
+
&& options.platform === 'codex'
|
|
1481
|
+
&& (artifact.roleKey || artifact.artifactKey)) {
|
|
1482
|
+
const roleKey = artifact.roleKey ?? artifact.artifactKey;
|
|
1483
|
+
components.push(createCodexRoleRegistrationComponent({
|
|
1484
|
+
identity: historicalIdentity(internalArtifactKey('role', roleKey)),
|
|
1485
|
+
fileIdentity: historicalIdentity(roleKey),
|
|
1486
|
+
configPath: resolveCodexPaths(options.cwd, options.scope, options.env).configTomlPath,
|
|
1487
|
+
roleKey,
|
|
1488
|
+
rolePath: artifact.path,
|
|
1489
|
+
}));
|
|
1490
|
+
}
|
|
1491
|
+
if (!artifact.artifactKey)
|
|
1492
|
+
continue;
|
|
1493
|
+
const identity = ownerIdentity(options.pluginName, artifact.artifactKey, options.platform, options.scope);
|
|
1494
|
+
components.push(pathComponent({
|
|
1495
|
+
kind: artifact.kind === 'skill' ? 'skill-directory' : 'owned-text-file',
|
|
1496
|
+
path: artifact.path,
|
|
1497
|
+
allowedRoot: artifact.kind === 'skill' ? path.dirname(artifact.path) : paths.configDir,
|
|
1498
|
+
identity,
|
|
1499
|
+
}));
|
|
1500
|
+
}
|
|
1501
|
+
return components;
|
|
1502
|
+
}
|
|
1503
|
+
function isAbsentCodexResiduePath(error) {
|
|
1504
|
+
const code = error.code;
|
|
1505
|
+
return code === 'ENOENT';
|
|
1506
|
+
}
|
|
1507
|
+
async function lstatCodexResidueIfPresent(filePath) {
|
|
1508
|
+
return fs.lstat(filePath).catch((error) => {
|
|
1509
|
+
if (isAbsentCodexResiduePath(error))
|
|
1510
|
+
return null;
|
|
1511
|
+
throw error;
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
async function readdirCodexResidueIfPresent(directory) {
|
|
1515
|
+
const stats = await lstatCodexResidueIfPresent(directory);
|
|
1516
|
+
if (!stats)
|
|
1517
|
+
return [];
|
|
1518
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
1519
|
+
throw new Error(`Codex residue root is not a canonical directory: ${directory}`);
|
|
1520
|
+
}
|
|
1521
|
+
return fs.readdir(directory, { withFileTypes: true });
|
|
1522
|
+
}
|
|
1523
|
+
async function inspectCodexNativeResidue(options) {
|
|
1524
|
+
const marketplaceName = options.marketplaceName ?? MARKETPLACE_NAME;
|
|
1525
|
+
const marketplaceRoot = options.marketplaceRoot ?? resolveAwHome(options.env);
|
|
1526
|
+
const pluginId = `${options.pluginName}@${marketplaceName}`;
|
|
1527
|
+
const configPath = resolveCodexPaths(options.cwd, 'user', options.env).configTomlPath;
|
|
1528
|
+
const config = await readCodexConfigFile(configPath);
|
|
1529
|
+
const marketplace = config.snapshot.marketplaces[marketplaceName];
|
|
1530
|
+
const canonicalMarketplace = marketplace?.sourceType === 'local'
|
|
1531
|
+
&& !marketplace.sourcePointsToManifest
|
|
1532
|
+
&& path.normalize(marketplace.source ?? '') === path.normalize(marketplaceRoot);
|
|
1533
|
+
const cacheRoot = path.join(getCodexHome(options.env), 'plugins', 'cache', marketplaceName, options.pluginName);
|
|
1534
|
+
const installedRoot = path.join(getCodexHome(options.env), 'plugins', pluginId);
|
|
1535
|
+
const installedManifest = path.join(installedRoot, 'manifest.json');
|
|
1536
|
+
const labels = [];
|
|
1537
|
+
const foreignLabels = [];
|
|
1538
|
+
const versions = new Set();
|
|
1539
|
+
const configEntry = config.snapshot.plugins[pluginId];
|
|
1540
|
+
if (configEntry) {
|
|
1541
|
+
(canonicalMarketplace ? labels : foreignLabels).push(`${configPath}#plugins.${pluginId}`);
|
|
1542
|
+
}
|
|
1543
|
+
const inspectManifest = async (manifestPath, directoryVersion) => {
|
|
1544
|
+
const stats = await lstatCodexResidueIfPresent(manifestPath);
|
|
1545
|
+
if (!stats)
|
|
1546
|
+
return;
|
|
1547
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
1548
|
+
foreignLabels.push(manifestPath);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
const manifest = await fs.readJson(manifestPath);
|
|
1552
|
+
const identity = manifest?.id ?? manifest?.name;
|
|
1553
|
+
const version = typeof manifest?.version === 'string' ? manifest.version : null;
|
|
1554
|
+
if (!canonicalMarketplace
|
|
1555
|
+
|| (identity !== options.pluginName && identity !== pluginId)
|
|
1556
|
+
|| !version
|
|
1557
|
+
|| (directoryVersion !== undefined && version !== directoryVersion)) {
|
|
1558
|
+
foreignLabels.push(manifestPath);
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
labels.push(manifestPath);
|
|
1562
|
+
versions.add(version);
|
|
1563
|
+
};
|
|
1564
|
+
for (const entry of await readdirCodexResidueIfPresent(cacheRoot)) {
|
|
1565
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
1566
|
+
foreignLabels.push(path.join(cacheRoot, entry.name));
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
const versionRoot = path.join(cacheRoot, entry.name);
|
|
1570
|
+
const candidates = [
|
|
1571
|
+
path.join(versionRoot, 'plugin.json'),
|
|
1572
|
+
path.join(versionRoot, '.codex-plugin', 'plugin.json'),
|
|
1573
|
+
];
|
|
1574
|
+
const existing = [];
|
|
1575
|
+
for (const candidate of candidates) {
|
|
1576
|
+
if (await lstatCodexResidueIfPresent(candidate))
|
|
1577
|
+
existing.push(candidate);
|
|
1578
|
+
}
|
|
1579
|
+
if (existing.length === 0)
|
|
1580
|
+
foreignLabels.push(versionRoot);
|
|
1581
|
+
for (const candidate of existing)
|
|
1582
|
+
await inspectManifest(candidate, entry.name);
|
|
1583
|
+
}
|
|
1584
|
+
const installedStats = await lstatCodexResidueIfPresent(installedRoot);
|
|
1585
|
+
if (installedStats && (!installedStats.isDirectory() || installedStats.isSymbolicLink())) {
|
|
1586
|
+
foreignLabels.push(installedRoot);
|
|
1587
|
+
}
|
|
1588
|
+
else if (installedStats) {
|
|
1589
|
+
await inspectManifest(installedManifest);
|
|
1590
|
+
}
|
|
1591
|
+
return foreignLabels.length > 0
|
|
1592
|
+
? {
|
|
1593
|
+
state: 'foreign', labels: [...labels, ...foreignLabels], versions: [...versions],
|
|
1594
|
+
enabled: configEntry?.enabled === true,
|
|
1595
|
+
}
|
|
1596
|
+
: labels.length > 0
|
|
1597
|
+
? { state: 'present', labels, versions: [...versions], enabled: configEntry?.enabled === true }
|
|
1598
|
+
: { state: 'absent', labels: [], versions: [], enabled: false };
|
|
1599
|
+
}
|
|
1600
|
+
export async function discoverCodexNativeResidueNames(options) {
|
|
1601
|
+
const marketplaceName = options.marketplaceName ?? MARKETPLACE_NAME;
|
|
1602
|
+
const marketplaceRoot = options.marketplaceRoot ?? resolveAwHome(options.env);
|
|
1603
|
+
const names = new Set();
|
|
1604
|
+
const config = await readCodexConfigFile(resolveCodexPaths(options.cwd, 'user', options.env).configTomlPath);
|
|
1605
|
+
const marketplace = config.snapshot.marketplaces[marketplaceName];
|
|
1606
|
+
const canonicalMarketplace = marketplace?.sourceType === 'local'
|
|
1607
|
+
&& !marketplace.sourcePointsToManifest
|
|
1608
|
+
&& path.normalize(marketplace.source ?? '') === path.normalize(marketplaceRoot);
|
|
1609
|
+
const probeFailure = (location, cause, pluginName) => {
|
|
1610
|
+
if (cause instanceof CodexNativeResidueProbeError)
|
|
1611
|
+
throw cause;
|
|
1612
|
+
throw new CodexNativeResidueProbeError(location, cause, pluginName);
|
|
1613
|
+
};
|
|
1614
|
+
for (const pluginId of canonicalMarketplace ? Object.keys(config.snapshot.plugins) : []) {
|
|
1615
|
+
const suffix = `@${marketplaceName}`;
|
|
1616
|
+
const match = pluginId.endsWith(suffix)
|
|
1617
|
+
? /^([a-z0-9]+(?:-[a-z0-9]+)*)$/u.exec(pluginId.slice(0, -suffix.length))
|
|
1618
|
+
: null;
|
|
1619
|
+
if (match)
|
|
1620
|
+
names.add(match[1]);
|
|
1621
|
+
}
|
|
1622
|
+
const cacheRoot = path.join(getCodexHome(options.env), 'plugins', 'cache', marketplaceName);
|
|
1623
|
+
const cacheEntries = await readdirCodexResidueIfPresent(cacheRoot).catch((error) => probeFailure(cacheRoot, error));
|
|
1624
|
+
for (const entry of cacheEntries) {
|
|
1625
|
+
const entryPath = path.join(cacheRoot, entry.name);
|
|
1626
|
+
const pluginName = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(entry.name)
|
|
1627
|
+
? entry.name
|
|
1628
|
+
: probeFailure(entryPath, new Error('Codex cache residue has an invalid plugin identity.'));
|
|
1629
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
1630
|
+
probeFailure(entryPath, new Error('Codex cache residue is not a canonical plugin directory.'), pluginName);
|
|
1631
|
+
}
|
|
1632
|
+
let versionEntries;
|
|
1633
|
+
try {
|
|
1634
|
+
versionEntries = await fs.readdir(entryPath, { withFileTypes: true });
|
|
1635
|
+
}
|
|
1636
|
+
catch (error) {
|
|
1637
|
+
throw new CodexNativeResidueProbeError(entryPath, error, pluginName);
|
|
1638
|
+
}
|
|
1639
|
+
if (versionEntries.length === 0) {
|
|
1640
|
+
if (names.has(pluginName)) {
|
|
1641
|
+
probeFailure(entryPath, new Error('Codex cache residue manifest is missing.'), pluginName);
|
|
1642
|
+
}
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
let residue;
|
|
1646
|
+
try {
|
|
1647
|
+
residue = await inspectCodexNativeResidue({ ...options, pluginName: entry.name });
|
|
1648
|
+
}
|
|
1649
|
+
catch (error) {
|
|
1650
|
+
throw new CodexNativeResidueProbeError(entryPath, error, pluginName);
|
|
1651
|
+
}
|
|
1652
|
+
if (residue.state !== 'present') {
|
|
1653
|
+
probeFailure(entryPath, new Error('Codex cache residue has foreign or incomplete manifest evidence.'), pluginName);
|
|
1654
|
+
}
|
|
1655
|
+
names.add(pluginName);
|
|
1656
|
+
}
|
|
1657
|
+
const pluginsRoot = path.join(getCodexHome(options.env), 'plugins');
|
|
1658
|
+
const pluginEntries = await readdirCodexResidueIfPresent(pluginsRoot).catch((error) => probeFailure(pluginsRoot, error));
|
|
1659
|
+
for (const entry of pluginEntries) {
|
|
1660
|
+
const suffix = `@${marketplaceName}`;
|
|
1661
|
+
const match = entry.name.endsWith(suffix)
|
|
1662
|
+
? /^([a-z0-9]+(?:-[a-z0-9]+)*)$/u.exec(entry.name.slice(0, -suffix.length))
|
|
1663
|
+
: null;
|
|
1664
|
+
if (!match)
|
|
1665
|
+
continue;
|
|
1666
|
+
const installedRoot = path.join(pluginsRoot, entry.name);
|
|
1667
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
1668
|
+
probeFailure(installedRoot, new Error('Codex installed residue is not a canonical plugin directory.'), match[1]);
|
|
1669
|
+
}
|
|
1670
|
+
const installedStats = await lstatCodexResidueIfPresent(installedRoot);
|
|
1671
|
+
if (!installedStats || !installedStats.isDirectory() || installedStats.isSymbolicLink()) {
|
|
1672
|
+
probeFailure(installedRoot, new Error('Codex installed residue is not a canonical plugin directory.'), match[1]);
|
|
1673
|
+
}
|
|
1674
|
+
const manifestPath = path.join(pluginsRoot, entry.name, 'manifest.json');
|
|
1675
|
+
const stats = await lstatCodexResidueIfPresent(manifestPath);
|
|
1676
|
+
if (!stats?.isFile() || stats.isSymbolicLink()) {
|
|
1677
|
+
probeFailure(manifestPath, new Error('Codex installed residue manifest is missing or nonregular.'), match[1]);
|
|
1678
|
+
}
|
|
1679
|
+
let rawManifest;
|
|
1680
|
+
try {
|
|
1681
|
+
rawManifest = await fs.readJson(manifestPath);
|
|
1682
|
+
}
|
|
1683
|
+
catch (error) {
|
|
1684
|
+
throw new CodexNativeResidueProbeError(manifestPath, error, match[1]);
|
|
1685
|
+
}
|
|
1686
|
+
if (!rawManifest || typeof rawManifest !== 'object' || Array.isArray(rawManifest)) {
|
|
1687
|
+
probeFailure(manifestPath, new Error('Codex installed residue manifest must be an object.'), match[1]);
|
|
1688
|
+
}
|
|
1689
|
+
const manifest = rawManifest;
|
|
1690
|
+
const identity = manifest.id ?? manifest.name;
|
|
1691
|
+
if (!canonicalMarketplace
|
|
1692
|
+
|| (identity !== match[1] && identity !== entry.name)
|
|
1693
|
+
|| typeof manifest.version !== 'string'
|
|
1694
|
+
|| manifest.version.trim() === '') {
|
|
1695
|
+
probeFailure(manifestPath, new Error('Codex installed residue manifest has foreign identity or version.'), match[1]);
|
|
1696
|
+
}
|
|
1697
|
+
names.add(match[1]);
|
|
1698
|
+
}
|
|
1699
|
+
return [...names];
|
|
1700
|
+
}
|
|
1701
|
+
export class CodexNativeResidueProbeError extends Error {
|
|
1702
|
+
code = 'codexNativeResidueProbeFailed';
|
|
1703
|
+
location;
|
|
1704
|
+
pluginName;
|
|
1705
|
+
constructor(location, cause, pluginName) {
|
|
1706
|
+
super(`codexNativeResidueProbeFailed: cannot classify Codex native residue at ${location}.`, {
|
|
1707
|
+
cause,
|
|
1708
|
+
});
|
|
1709
|
+
this.name = 'CodexNativeResidueProbeError';
|
|
1710
|
+
this.location = location;
|
|
1711
|
+
this.pluginName = pluginName;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
async function ensureCodexActiveSkillsManifest(options) {
|
|
1715
|
+
const relativeSkillsRoot = await resolveCodexSkillsRelativeRoot(options.plugin);
|
|
1716
|
+
const skillsRoot = path.join(options.marketplaceRoot, 'plugins', options.plugin.manifest.name, ...relativeSkillsRoot.split('/'));
|
|
1717
|
+
if (options.hasRequiredSkills && !await fs.pathExists(skillsRoot)) {
|
|
1718
|
+
throw new Error(`Codex required Skills root 不存在:${skillsRoot}`);
|
|
1719
|
+
}
|
|
1720
|
+
const manifestPath = path.join(options.marketplaceRoot, 'plugins', options.plugin.manifest.name, '.codex-plugin', 'plugin.json');
|
|
1721
|
+
const manifest = await fs.readJson(manifestPath).catch((error) => {
|
|
1722
|
+
if (error.code === 'ENOENT')
|
|
1723
|
+
return {};
|
|
1724
|
+
throw error;
|
|
1725
|
+
});
|
|
1726
|
+
const updated = {
|
|
1727
|
+
...manifest,
|
|
1728
|
+
name: options.plugin.manifest.name,
|
|
1729
|
+
version: options.plugin.manifest.version,
|
|
1730
|
+
};
|
|
1731
|
+
if (options.hasRequiredSkills)
|
|
1732
|
+
updated.skills = `./${relativeSkillsRoot}`;
|
|
1733
|
+
else
|
|
1734
|
+
delete updated.skills;
|
|
1735
|
+
await fs.outputJson(manifestPath, updated, { spaces: 2 });
|
|
1736
|
+
return options.hasRequiredSkills ? relativeSkillsRoot : null;
|
|
1737
|
+
}
|
|
1738
|
+
function createCodexMarketplaceComponent(options) {
|
|
1739
|
+
const configPath = resolveCodexPaths(options.cwd, 'user', options.env).configTomlPath;
|
|
1740
|
+
const inspectCanonicalMarketplace = async () => {
|
|
1741
|
+
const config = await readCodexConfigFile(configPath);
|
|
1742
|
+
const marketplace = config.snapshot.marketplaces[options.marketplaceName];
|
|
1743
|
+
if (!marketplace)
|
|
1744
|
+
return 'absent';
|
|
1745
|
+
if (marketplace.sourceType !== 'local'
|
|
1746
|
+
|| marketplace.sourcePointsToManifest
|
|
1747
|
+
|| path.normalize(marketplace.source ?? '') !== path.normalize(options.marketplaceRoot))
|
|
1748
|
+
return 'foreign';
|
|
1749
|
+
return 'present';
|
|
1750
|
+
};
|
|
1751
|
+
const hasRemainingPlugins = async () => {
|
|
1752
|
+
return (await discoverCodexNativeResidueNames({
|
|
1753
|
+
cwd: options.cwd,
|
|
1754
|
+
env: options.env,
|
|
1755
|
+
marketplaceName: options.marketplaceName,
|
|
1756
|
+
marketplaceRoot: options.marketplaceRoot,
|
|
1757
|
+
})).some((name) => name !== options.pluginName);
|
|
1758
|
+
};
|
|
1759
|
+
const inspect = async () => {
|
|
1760
|
+
const canonical = await inspectCanonicalMarketplace();
|
|
1761
|
+
const plugin = await inspectCodexNativeResidue({
|
|
1762
|
+
pluginName: options.pluginName,
|
|
1763
|
+
cwd: options.cwd,
|
|
1764
|
+
env: options.env,
|
|
1765
|
+
marketplaceName: options.marketplaceName,
|
|
1766
|
+
marketplaceRoot: options.marketplaceRoot,
|
|
1767
|
+
});
|
|
1768
|
+
if (plugin.state === 'foreign')
|
|
1769
|
+
return 'foreign';
|
|
1770
|
+
if (options.desired) {
|
|
1771
|
+
return canonical === 'present' && plugin.state === 'present' && plugin.enabled
|
|
1772
|
+
? 'present'
|
|
1773
|
+
: 'absent';
|
|
1774
|
+
}
|
|
1775
|
+
if (canonical !== 'present')
|
|
1776
|
+
return canonical;
|
|
1777
|
+
return await hasRemainingPlugins() ? 'absent' : 'present';
|
|
1778
|
+
};
|
|
1779
|
+
return {
|
|
1780
|
+
kind: 'shared-config',
|
|
1781
|
+
identity: ownerIdentity(options.pluginName, `marketplace-${options.marketplaceName}`, 'codex', 'user'),
|
|
1782
|
+
label: `${configPath}#marketplaces.${options.marketplaceName}`,
|
|
1783
|
+
inspect,
|
|
1784
|
+
remove: async () => {
|
|
1785
|
+
const currentResidue = await inspectCodexNativeResidue({
|
|
1786
|
+
pluginName: options.pluginName,
|
|
1787
|
+
cwd: options.cwd,
|
|
1788
|
+
env: options.env,
|
|
1789
|
+
marketplaceName: options.marketplaceName,
|
|
1790
|
+
marketplaceRoot: options.marketplaceRoot,
|
|
1791
|
+
});
|
|
1792
|
+
if (currentResidue.state === 'foreign') {
|
|
1793
|
+
throw new Error(`Codex native owner 为 foreign,拒绝移除 marketplace:${currentResidue.labels.join(',')}`);
|
|
1794
|
+
}
|
|
1795
|
+
if (await inspectCanonicalMarketplace() !== 'present'
|
|
1796
|
+
|| currentResidue.state === 'present'
|
|
1797
|
+
|| await hasRemainingPlugins())
|
|
1798
|
+
return;
|
|
1799
|
+
await runNativeCliChecked(buildCodexMarketplaceRemoveInvocation(options.marketplaceName, options.cwd, options.env), options.runner);
|
|
1800
|
+
await runNativeCliChecked(buildCodexMarketplaceListInvocation(options.cwd, options.env), options.runner);
|
|
1801
|
+
},
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
async function assertCodexNativeInstallState(options) {
|
|
1805
|
+
const marketplaceName = options.marketplaceName ?? MARKETPLACE_NAME;
|
|
1806
|
+
const pluginId = `${options.pluginName}@${marketplaceName}`;
|
|
1807
|
+
const configPath = resolveCodexPaths(options.cwd, 'user', options.env).configTomlPath;
|
|
1808
|
+
const config = await readCodexConfigFile(configPath);
|
|
1809
|
+
const marketplace = config.snapshot.marketplaces[marketplaceName];
|
|
1810
|
+
if (marketplace?.sourceType !== 'local'
|
|
1811
|
+
|| marketplace.sourcePointsToManifest
|
|
1812
|
+
|| path.normalize(marketplace.source ?? '') !== path.normalize(options.marketplaceRoot)) {
|
|
1813
|
+
throw new Error(`Codex marketplace aw 未指向当前 AW_HOME:${options.marketplaceRoot}`);
|
|
1814
|
+
}
|
|
1815
|
+
if (config.snapshot.plugins[pluginId]?.enabled !== true) {
|
|
1816
|
+
throw new Error(`Codex enabled config 缺少 ${pluginId}。`);
|
|
1817
|
+
}
|
|
1818
|
+
const cacheRoot = path.join(getCodexHome(options.env), 'plugins', 'cache', marketplaceName, options.pluginName, options.pluginVersion);
|
|
1819
|
+
const candidates = [
|
|
1820
|
+
path.join(cacheRoot, 'plugin.json'),
|
|
1821
|
+
path.join(cacheRoot, '.codex-plugin', 'plugin.json'),
|
|
1822
|
+
];
|
|
1823
|
+
const expectedRoot = path.join(options.marketplaceRoot, 'plugins', options.pluginName);
|
|
1824
|
+
const assertExpectedPayload = async () => {
|
|
1825
|
+
const collectFiles = async (root) => {
|
|
1826
|
+
const output = [];
|
|
1827
|
+
const pending = [''];
|
|
1828
|
+
while (pending.length > 0) {
|
|
1829
|
+
const relativeRoot = pending.pop();
|
|
1830
|
+
for (const entry of await fs.readdir(path.join(root, relativeRoot), { withFileTypes: true })) {
|
|
1831
|
+
const relativePath = path.join(relativeRoot, entry.name);
|
|
1832
|
+
if (entry.isSymbolicLink()) {
|
|
1833
|
+
throw new Error(`Codex native payload 不能包含符号链接:${path.join(root, relativePath)}`);
|
|
1834
|
+
}
|
|
1835
|
+
if (entry.isDirectory())
|
|
1836
|
+
pending.push(relativePath);
|
|
1837
|
+
else if (entry.isFile())
|
|
1838
|
+
output.push(relativePath);
|
|
1839
|
+
else
|
|
1840
|
+
throw new Error(`Codex native payload 包含不支持的节点:${path.join(root, relativePath)}`);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
return output;
|
|
1844
|
+
};
|
|
1845
|
+
const manifestPaths = new Set(['plugin.json', path.join('.codex-plugin', 'plugin.json')]);
|
|
1846
|
+
const allowedMetadata = new Set(['metadata.json', '.metadata.json']);
|
|
1847
|
+
const [expectedFiles, actualFiles] = await Promise.all([
|
|
1848
|
+
collectFiles(expectedRoot),
|
|
1849
|
+
collectFiles(cacheRoot),
|
|
1850
|
+
]);
|
|
1851
|
+
const expectedSet = new Set(expectedFiles);
|
|
1852
|
+
for (const relativePath of expectedFiles) {
|
|
1853
|
+
if (manifestPaths.has(relativePath))
|
|
1854
|
+
continue;
|
|
1855
|
+
const expectedPath = path.join(expectedRoot, relativePath);
|
|
1856
|
+
const actualPath = path.join(cacheRoot, relativePath);
|
|
1857
|
+
const actualStats = await fs.lstat(actualPath).catch(() => null);
|
|
1858
|
+
if (!actualStats?.isFile() || actualStats.isSymbolicLink()) {
|
|
1859
|
+
throw new Error(`Codex native cache 缺少 expected payload:${actualPath}`);
|
|
1860
|
+
}
|
|
1861
|
+
const [expectedContent, actualContent] = await Promise.all([
|
|
1862
|
+
fs.readFile(expectedPath),
|
|
1863
|
+
fs.readFile(actualPath),
|
|
1864
|
+
]);
|
|
1865
|
+
if (!expectedContent.equals(actualContent)) {
|
|
1866
|
+
throw new Error(`Codex native cache expected payload digest 不匹配:${actualPath}`);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
for (const relativePath of actualFiles) {
|
|
1870
|
+
if (expectedSet.has(relativePath)
|
|
1871
|
+
|| manifestPaths.has(relativePath)
|
|
1872
|
+
|| allowedMetadata.has(relativePath))
|
|
1873
|
+
continue;
|
|
1874
|
+
throw new Error(`Codex native cache 包含 unexpected loadable payload:${path.join(cacheRoot, relativePath)}`);
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
let matchedManifest = false;
|
|
1878
|
+
for (const candidate of candidates) {
|
|
1879
|
+
if (!await fs.pathExists(candidate))
|
|
1880
|
+
continue;
|
|
1881
|
+
const manifest = await fs.readJson(candidate).catch(() => null);
|
|
1882
|
+
if (!manifest) {
|
|
1883
|
+
throw new Error(`Codex native cache manifest 无法解析:${candidate}`);
|
|
1884
|
+
}
|
|
1885
|
+
const identity = manifest.id ?? manifest.name;
|
|
1886
|
+
const matches = ((identity === options.pluginName || identity === pluginId)
|
|
1887
|
+
&& manifest.version === options.pluginVersion
|
|
1888
|
+
&& (options.skillsRelativeRoot === null
|
|
1889
|
+
? typeof manifest.skills !== 'string'
|
|
1890
|
+
: typeof manifest.skills === 'string'
|
|
1891
|
+
&& path.posix.normalize(manifest.skills.replace(/^\.\//u, '')) === options.skillsRelativeRoot));
|
|
1892
|
+
if (!matches) {
|
|
1893
|
+
throw new Error(`Codex native cache manifest 不匹配:${candidate}`);
|
|
1894
|
+
}
|
|
1895
|
+
matchedManifest = true;
|
|
1896
|
+
}
|
|
1897
|
+
if (!matchedManifest) {
|
|
1898
|
+
throw new Error(`Codex native cache 缺少匹配 ${pluginId}@${options.pluginVersion} 的 manifest:${cacheRoot}`);
|
|
1899
|
+
}
|
|
1900
|
+
for (const skillKey of options.requiredSkillKeys) {
|
|
1901
|
+
const skillPath = path.join(cacheRoot, ...options.skillsRelativeRoot.split('/'), skillKey, 'SKILL.md');
|
|
1902
|
+
if (!await fs.pathExists(skillPath)) {
|
|
1903
|
+
throw new Error(`Codex native cache 缺少 required Skill:${skillPath}`);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
await assertExpectedPayload();
|
|
1907
|
+
}
|
|
1908
|
+
async function readJsonObjectIfExists(filePath, label) {
|
|
1909
|
+
let value;
|
|
1910
|
+
try {
|
|
1911
|
+
value = await fs.readJson(filePath);
|
|
1912
|
+
}
|
|
1913
|
+
catch (error) {
|
|
1914
|
+
if (error.code === 'ENOENT')
|
|
1915
|
+
return {};
|
|
1916
|
+
throw new Error(`${label} 无法读取:${error instanceof Error ? error.message : String(error)}`);
|
|
1917
|
+
}
|
|
1918
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1919
|
+
throw new Error(`${label} 必须是 JSON object。`);
|
|
1920
|
+
}
|
|
1921
|
+
return value;
|
|
1922
|
+
}
|
|
1923
|
+
function nonEmptyString(value) {
|
|
1924
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
1925
|
+
}
|
|
1926
|
+
function readClaudeMarketplaceSourcePath(value, label) {
|
|
1927
|
+
if (value === undefined)
|
|
1928
|
+
return undefined;
|
|
1929
|
+
if (typeof value === 'string') {
|
|
1930
|
+
const source = nonEmptyString(value);
|
|
1931
|
+
if (!source)
|
|
1932
|
+
throw new Error(`${label} 必须是非空 string。`);
|
|
1933
|
+
return source;
|
|
1934
|
+
}
|
|
1935
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1936
|
+
throw new Error(`${label} 必须是 string 或 object。`);
|
|
1937
|
+
}
|
|
1938
|
+
const record = value;
|
|
1939
|
+
if (record.source && typeof record.source === 'object' && !Array.isArray(record.source)) {
|
|
1940
|
+
return readClaudeMarketplaceSourcePath(record.source, `${label}.source`);
|
|
1941
|
+
}
|
|
1942
|
+
const source = nonEmptyString(record.source);
|
|
1943
|
+
if (!source)
|
|
1944
|
+
throw new Error(`${label}.source 必须是非空 string 或 object。`);
|
|
1945
|
+
if (record.path === undefined)
|
|
1946
|
+
return source;
|
|
1947
|
+
const sourcePath = nonEmptyString(record.path);
|
|
1948
|
+
if (!sourcePath)
|
|
1949
|
+
throw new Error(`${label}.path 必须是非空 string。`);
|
|
1950
|
+
return source === 'directory' ? sourcePath : null;
|
|
1951
|
+
}
|
|
1952
|
+
function classifyClaudeMarketplaceSources(sources, awHome) {
|
|
1953
|
+
if (sources.some((source) => source === null || (source !== undefined && path.normalize(source) !== awHome))) {
|
|
1954
|
+
return 'foreign';
|
|
1955
|
+
}
|
|
1956
|
+
return sources.some((source) => source !== undefined) ? 'canonical' : 'absent';
|
|
1957
|
+
}
|
|
1958
|
+
function readClaudeKnownMarketplaceSources(value, label) {
|
|
1959
|
+
if (value === undefined)
|
|
1960
|
+
return [];
|
|
1961
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1962
|
+
throw new Error(`${label} 记录无效。`);
|
|
1963
|
+
}
|
|
1964
|
+
const record = value;
|
|
1965
|
+
const sources = [];
|
|
1966
|
+
if (record.installLocation !== undefined) {
|
|
1967
|
+
const installLocation = nonEmptyString(record.installLocation);
|
|
1968
|
+
if (!installLocation)
|
|
1969
|
+
throw new Error(`${label}.installLocation 必须是非空 string。`);
|
|
1970
|
+
sources.push(installLocation);
|
|
1971
|
+
}
|
|
1972
|
+
sources.push(readClaudeMarketplaceSourcePath(record.source, `${label}.source`));
|
|
1973
|
+
return sources;
|
|
1974
|
+
}
|
|
1975
|
+
async function readClaudeNativeState(options) {
|
|
1976
|
+
const paths = resolveClaudePaths(options.cwd, options.scope, options.env);
|
|
1977
|
+
const marketplaceName = options.marketplaceName ?? MARKETPLACE_NAME;
|
|
1978
|
+
const awHome = path.normalize(options.marketplaceRoot ?? resolveAwHome(options.env));
|
|
1979
|
+
const settings = await readJsonObjectIfExists(paths.settingsPath, 'Claude settings');
|
|
1980
|
+
const known = await readJsonObjectIfExists(paths.knownMarketplacesPath, 'Claude known_marketplaces');
|
|
1981
|
+
const installed = await readJsonObjectIfExists(paths.installedPluginsPath, 'Claude installed_plugins');
|
|
1982
|
+
if (settings.extraKnownMarketplaces !== undefined && (!settings.extraKnownMarketplaces
|
|
1983
|
+
|| typeof settings.extraKnownMarketplaces !== 'object'
|
|
1984
|
+
|| Array.isArray(settings.extraKnownMarketplaces)))
|
|
1985
|
+
throw new Error('Claude settings.extraKnownMarketplaces 必须是 object。');
|
|
1986
|
+
if (settings.plugins !== undefined && (!settings.plugins || typeof settings.plugins !== 'object' || Array.isArray(settings.plugins)))
|
|
1987
|
+
throw new Error('Claude settings.plugins 必须是 object。');
|
|
1988
|
+
if (settings.enabledPlugins !== undefined && (!settings.enabledPlugins
|
|
1989
|
+
|| typeof settings.enabledPlugins !== 'object'
|
|
1990
|
+
|| Array.isArray(settings.enabledPlugins)))
|
|
1991
|
+
throw new Error('Claude settings.enabledPlugins 必须是 object。');
|
|
1992
|
+
const settingsPlugins = settings.plugins;
|
|
1993
|
+
if (settingsPlugins?.installed !== undefined && (!settingsPlugins.installed
|
|
1994
|
+
|| typeof settingsPlugins.installed !== 'object'
|
|
1995
|
+
|| Array.isArray(settingsPlugins.installed)))
|
|
1996
|
+
throw new Error('Claude settings.plugins.installed 必须是 object。');
|
|
1997
|
+
if (known.marketplaces !== undefined && (!known.marketplaces || typeof known.marketplaces !== 'object' || Array.isArray(known.marketplaces)))
|
|
1998
|
+
throw new Error('Claude known_marketplaces.marketplaces 必须是 object。');
|
|
1999
|
+
if (installed.plugins !== undefined && (!installed.plugins || typeof installed.plugins !== 'object' || Array.isArray(installed.plugins)))
|
|
2000
|
+
throw new Error('Claude installed_plugins.plugins 必须是 object。');
|
|
2001
|
+
const extraKnown = settings.extraKnownMarketplaces;
|
|
2002
|
+
const settingsSourceValue = extraKnown?.[marketplaceName];
|
|
2003
|
+
const settingsSource = readClaudeMarketplaceSourcePath(settingsSourceValue, 'Claude settings marketplace aw');
|
|
2004
|
+
const legacyKnown = known.marketplaces;
|
|
2005
|
+
const marketplaceDeclaredState = classifyClaudeMarketplaceSources([settingsSource], awHome);
|
|
2006
|
+
const marketplaceMaterializedState = classifyClaudeMarketplaceSources(readClaudeKnownMarketplaceSources(known[marketplaceName], 'Claude known marketplace aw'), awHome);
|
|
2007
|
+
const legacyMarketplaceState = classifyClaudeMarketplaceSources(readClaudeKnownMarketplaceSources(legacyKnown?.[marketplaceName], 'Claude known marketplace aw legacy'), awHome);
|
|
2008
|
+
const marketplaceStates = [
|
|
2009
|
+
marketplaceDeclaredState,
|
|
2010
|
+
marketplaceMaterializedState,
|
|
2011
|
+
legacyMarketplaceState,
|
|
2012
|
+
];
|
|
2013
|
+
const marketplaceState = marketplaceStates.includes('foreign')
|
|
2014
|
+
? 'foreign'
|
|
2015
|
+
: marketplaceStates.includes('canonical')
|
|
2016
|
+
? 'canonical'
|
|
2017
|
+
: 'absent';
|
|
2018
|
+
const pluginId = `${options.pluginName}@${marketplaceName}`;
|
|
2019
|
+
const enabledPlugins = settings.enabledPlugins;
|
|
2020
|
+
const rawEnabled = enabledPlugins?.[pluginId];
|
|
2021
|
+
if (rawEnabled !== undefined
|
|
2022
|
+
&& typeof rawEnabled !== 'boolean'
|
|
2023
|
+
&& (!Array.isArray(rawEnabled) || !rawEnabled.every((entry) => typeof entry === 'string'))) {
|
|
2024
|
+
throw new Error(`Claude settings.enabledPlugins 记录 ${pluginId} 必须是 boolean 或 string array。`);
|
|
2025
|
+
}
|
|
2026
|
+
const pluginEnabled = rawEnabled === true || Array.isArray(rawEnabled);
|
|
2027
|
+
const enabledPluginEvidence = rawEnabled !== undefined;
|
|
2028
|
+
const records = [];
|
|
2029
|
+
const unboundPluginNames = new Set();
|
|
2030
|
+
for (const [pluginKey, raw] of Object.entries(installed.plugins ?? {})) {
|
|
2031
|
+
for (const entry of Array.isArray(raw) ? raw : [raw]) {
|
|
2032
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
2033
|
+
throw new Error(`Claude installed_plugins 记录 ${pluginKey} 必须是 object。`);
|
|
2034
|
+
}
|
|
2035
|
+
const record = entry;
|
|
2036
|
+
const version = nonEmptyString(record.version);
|
|
2037
|
+
if (!version)
|
|
2038
|
+
throw new Error(`Claude installed_plugins 记录 ${pluginKey} 缺少 version。`);
|
|
2039
|
+
if (record.scope !== undefined
|
|
2040
|
+
&& record.scope !== 'user'
|
|
2041
|
+
&& record.scope !== 'project'
|
|
2042
|
+
&& record.scope !== 'local') {
|
|
2043
|
+
throw new Error(`Claude installed_plugins 记录 ${pluginKey} 的 scope 无效。`);
|
|
2044
|
+
}
|
|
2045
|
+
if (record.projectPath !== undefined && typeof record.projectPath !== 'string') {
|
|
2046
|
+
throw new Error(`Claude installed_plugins 记录 ${pluginKey} 的 projectPath 无效。`);
|
|
2047
|
+
}
|
|
2048
|
+
const marketplaceSeparator = pluginKey.lastIndexOf('@');
|
|
2049
|
+
const keyedMarketplace = marketplaceSeparator > 0 && marketplaceSeparator < pluginKey.length - 1
|
|
2050
|
+
? pluginKey.slice(marketplaceSeparator + 1)
|
|
2051
|
+
: undefined;
|
|
2052
|
+
if (!pluginKey.includes('@') && record.marketplace === undefined) {
|
|
2053
|
+
unboundPluginNames.add(pluginKey);
|
|
2054
|
+
}
|
|
2055
|
+
records.push({
|
|
2056
|
+
name: typeof record.name === 'string'
|
|
2057
|
+
? record.name
|
|
2058
|
+
: keyedMarketplace ? pluginKey.slice(0, marketplaceSeparator) : pluginKey,
|
|
2059
|
+
marketplace: typeof record.marketplace === 'string'
|
|
2060
|
+
? record.marketplace
|
|
2061
|
+
: keyedMarketplace,
|
|
2062
|
+
scope: record.scope === 'project' || record.scope === 'local' ? record.scope : 'user',
|
|
2063
|
+
...(typeof record.projectPath === 'string' ? { projectPath: record.projectPath } : {}),
|
|
2064
|
+
version,
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
const projectPath = options.scope === 'project' ? path.normalize(await resolveRepoRoot(options.cwd)) : undefined;
|
|
2069
|
+
const matchingRecords = records.filter((record) => record.name === options.pluginName
|
|
2070
|
+
&& record.marketplace === marketplaceName
|
|
2071
|
+
&& record.scope === options.scope
|
|
2072
|
+
&& (options.scope === 'user' || path.normalize(record.projectPath ?? '') === projectPath));
|
|
2073
|
+
const settingsInstalled = settingsPlugins?.installed;
|
|
2074
|
+
const rawSettingsRecord = settingsInstalled?.[options.pluginName];
|
|
2075
|
+
if (rawSettingsRecord !== undefined && (!rawSettingsRecord || typeof rawSettingsRecord !== 'object' || Array.isArray(rawSettingsRecord)))
|
|
2076
|
+
throw new Error(`Claude settings plugin 记录 ${options.pluginName} 必须是 object。`);
|
|
2077
|
+
const settingsRecord = rawSettingsRecord;
|
|
2078
|
+
if (settingsRecord && !nonEmptyString(settingsRecord.version)) {
|
|
2079
|
+
throw new Error(`Claude settings plugin 记录 ${options.pluginName} 缺少 version。`);
|
|
2080
|
+
}
|
|
2081
|
+
if (settingsRecord && !nonEmptyString(settingsRecord.marketplace)) {
|
|
2082
|
+
throw new Error(`Claude settings plugin 记录 ${options.pluginName} 缺少 marketplace。`);
|
|
2083
|
+
}
|
|
2084
|
+
if (settingsRecord?.scope !== undefined
|
|
2085
|
+
&& settingsRecord.scope !== 'user'
|
|
2086
|
+
&& settingsRecord.scope !== 'project'
|
|
2087
|
+
&& settingsRecord.scope !== 'local') {
|
|
2088
|
+
throw new Error(`Claude settings plugin 记录 ${options.pluginName} 的 scope 无效。`);
|
|
2089
|
+
}
|
|
2090
|
+
if (settingsRecord?.projectPath !== undefined && typeof settingsRecord.projectPath !== 'string') {
|
|
2091
|
+
throw new Error(`Claude settings plugin 记录 ${options.pluginName} 的 projectPath 无效。`);
|
|
2092
|
+
}
|
|
2093
|
+
const settingsMatchesScope = settingsRecord
|
|
2094
|
+
&& settingsRecord.marketplace === marketplaceName
|
|
2095
|
+
&& (settingsRecord.scope ?? 'user') === options.scope
|
|
2096
|
+
&& (options.scope === 'user'
|
|
2097
|
+
|| path.normalize(settingsRecord.projectPath ?? '') === projectPath);
|
|
2098
|
+
const canonicalEvidence = matchingRecords.length > 0
|
|
2099
|
+
|| Boolean(settingsMatchesScope)
|
|
2100
|
+
|| enabledPluginEvidence;
|
|
2101
|
+
const foreign = canonicalEvidence && marketplaceState === 'foreign';
|
|
2102
|
+
const canonicalRecords = matchingRecords;
|
|
2103
|
+
const canonicalSettingsRecord = settingsMatchesScope;
|
|
2104
|
+
const marketplaceConsumerSuffix = `@${marketplaceName}`;
|
|
2105
|
+
const hasMarketplaceConsumerKey = (pluginKey) => pluginKey.length > marketplaceConsumerSuffix.length
|
|
2106
|
+
&& pluginKey.endsWith(marketplaceConsumerSuffix);
|
|
2107
|
+
const marketplaceConsumerEvidence = records.some((record) => record.marketplace === marketplaceName)
|
|
2108
|
+
|| Object.keys(enabledPlugins ?? {}).some(hasMarketplaceConsumerKey)
|
|
2109
|
+
|| Object.entries(settingsInstalled ?? {}).some(([pluginKey, raw]) => hasMarketplaceConsumerKey(pluginKey)
|
|
2110
|
+
|| (raw !== null
|
|
2111
|
+
&& typeof raw === 'object'
|
|
2112
|
+
&& !Array.isArray(raw)
|
|
2113
|
+
&& raw.marketplace === marketplaceName));
|
|
2114
|
+
const marketplaceConsumers = records
|
|
2115
|
+
.filter((record) => record.marketplace === marketplaceName)
|
|
2116
|
+
.map((record) => ({
|
|
2117
|
+
name: record.name,
|
|
2118
|
+
scope: record.scope,
|
|
2119
|
+
...(record.projectPath ? { projectPath: record.projectPath } : {}),
|
|
2120
|
+
}));
|
|
2121
|
+
for (const pluginKey of Object.keys(enabledPlugins ?? {})) {
|
|
2122
|
+
const suffix = `@${marketplaceName}`;
|
|
2123
|
+
if (!pluginKey.endsWith(suffix))
|
|
2124
|
+
continue;
|
|
2125
|
+
marketplaceConsumers.push({
|
|
2126
|
+
name: pluginKey.slice(0, -suffix.length),
|
|
2127
|
+
scope: options.scope,
|
|
2128
|
+
...(projectPath ? { projectPath } : {}),
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
for (const [pluginKey, rawRecord] of Object.entries(settingsInstalled ?? {})) {
|
|
2132
|
+
if (!rawRecord || typeof rawRecord !== 'object' || Array.isArray(rawRecord))
|
|
2133
|
+
continue;
|
|
2134
|
+
const record = rawRecord;
|
|
2135
|
+
if (record.marketplace !== marketplaceName)
|
|
2136
|
+
continue;
|
|
2137
|
+
const name = nonEmptyString(record.name)
|
|
2138
|
+
?? (pluginKey.endsWith(`@${marketplaceName}`)
|
|
2139
|
+
? pluginKey.slice(0, -`@${marketplaceName}`.length)
|
|
2140
|
+
: pluginKey);
|
|
2141
|
+
const scope = record.scope === 'project' || record.scope === 'local' ? record.scope : 'user';
|
|
2142
|
+
marketplaceConsumers.push({
|
|
2143
|
+
name,
|
|
2144
|
+
scope,
|
|
2145
|
+
...(typeof record.projectPath === 'string' ? { projectPath: record.projectPath } : {}),
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
const pluginState = foreign
|
|
2149
|
+
? 'foreign'
|
|
2150
|
+
: canonicalEvidence
|
|
2151
|
+
? 'present'
|
|
2152
|
+
: 'absent';
|
|
2153
|
+
const pluginVersions = [
|
|
2154
|
+
...canonicalRecords.map((record) => record.version),
|
|
2155
|
+
...(canonicalSettingsRecord ? [nonEmptyString(settingsRecord.version)] : []),
|
|
2156
|
+
];
|
|
2157
|
+
return {
|
|
2158
|
+
paths,
|
|
2159
|
+
marketplaceState,
|
|
2160
|
+
marketplaceDeclaredState,
|
|
2161
|
+
marketplaceMaterializedState,
|
|
2162
|
+
legacyMarketplaceState,
|
|
2163
|
+
pluginState,
|
|
2164
|
+
pluginEnabled,
|
|
2165
|
+
records,
|
|
2166
|
+
marketplaceConsumers,
|
|
2167
|
+
marketplaceConsumerEvidence,
|
|
2168
|
+
unboundPluginNames: [...unboundPluginNames],
|
|
2169
|
+
pluginVersions,
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
export async function assertNoClaudeUnboundNativeNameCollision(options) {
|
|
2173
|
+
const state = await readClaudeNativeState({
|
|
2174
|
+
pluginName: options.pluginName,
|
|
2175
|
+
scope: 'user',
|
|
2176
|
+
cwd: options.cwd,
|
|
2177
|
+
env: options.env,
|
|
2178
|
+
});
|
|
2179
|
+
if (state.unboundPluginNames.includes(options.pluginName)) {
|
|
2180
|
+
throw new Error(`externalOwnershipPresent: Claude has an unbound installed plugin named ${options.pluginName}; native install would overwrite it.`);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
export async function inspectNativeMarketplaceRegistrations(options) {
|
|
2184
|
+
const codexConfig = await readCodexConfigFile(resolveCodexPaths(options.cwd, 'user', options.env).configTomlPath);
|
|
2185
|
+
const codexMarketplace = codexConfig.snapshot.marketplaces[options.marketplaceName];
|
|
2186
|
+
const codexRegistration = codexMarketplace === undefined
|
|
2187
|
+
? 'absent'
|
|
2188
|
+
: codexMarketplace.sourceType === 'local'
|
|
2189
|
+
&& !codexMarketplace.sourcePointsToManifest
|
|
2190
|
+
&& path.normalize(codexMarketplace.source ?? '') === path.normalize(options.marketplaceRoot)
|
|
2191
|
+
? 'canonical'
|
|
2192
|
+
: 'foreign';
|
|
2193
|
+
const codexConsumerSuffix = `@${options.marketplaceName}`;
|
|
2194
|
+
const codex = codexRegistration === 'absent'
|
|
2195
|
+
&& Object.keys(codexConfig.snapshot.plugins).some((pluginId) => pluginId.length > codexConsumerSuffix.length && pluginId.endsWith(codexConsumerSuffix))
|
|
2196
|
+
? 'foreign'
|
|
2197
|
+
: codexRegistration;
|
|
2198
|
+
const claudeState = await readClaudeNativeState({
|
|
2199
|
+
pluginName: '__marketplace_preflight__',
|
|
2200
|
+
scope: 'user',
|
|
2201
|
+
cwd: options.cwd,
|
|
2202
|
+
env: options.env,
|
|
2203
|
+
marketplaceName: options.marketplaceName,
|
|
2204
|
+
marketplaceRoot: options.marketplaceRoot,
|
|
2205
|
+
});
|
|
2206
|
+
const claude = claudeState.marketplaceState === 'absent'
|
|
2207
|
+
&& claudeState.marketplaceConsumerEvidence
|
|
2208
|
+
? 'foreign'
|
|
2209
|
+
: claudeState.marketplaceState;
|
|
2210
|
+
return { codex, claude };
|
|
2211
|
+
}
|
|
2212
|
+
function createClaudeMarketplaceComponent(options) {
|
|
2213
|
+
const inspect = async () => {
|
|
2214
|
+
const state = await readClaudeNativeState(options);
|
|
2215
|
+
if (state.pluginState === 'foreign')
|
|
2216
|
+
return 'foreign';
|
|
2217
|
+
if (options.desired) {
|
|
2218
|
+
return state.marketplaceDeclaredState === 'canonical'
|
|
2219
|
+
&& state.marketplaceMaterializedState === 'canonical'
|
|
2220
|
+
&& state.pluginState === 'present'
|
|
2221
|
+
&& state.pluginEnabled
|
|
2222
|
+
? 'present'
|
|
2223
|
+
: 'absent';
|
|
2224
|
+
}
|
|
2225
|
+
if (state.marketplaceState !== 'canonical')
|
|
2226
|
+
return state.marketplaceState;
|
|
2227
|
+
const projectPath = options.scope === 'project'
|
|
2228
|
+
? path.normalize(await resolveRepoRoot(options.cwd))
|
|
2229
|
+
: undefined;
|
|
2230
|
+
return state.marketplaceConsumers.some((consumer) => !(consumer.name === options.pluginName
|
|
2231
|
+
&& consumer.scope === options.scope
|
|
2232
|
+
&& (options.scope === 'user' || path.normalize(consumer.projectPath ?? '') === projectPath)))
|
|
2233
|
+
? 'absent'
|
|
2234
|
+
: 'present';
|
|
2235
|
+
};
|
|
2236
|
+
return {
|
|
2237
|
+
kind: 'shared-config',
|
|
2238
|
+
identity: ownerIdentity(options.pluginName, `marketplace-${options.marketplaceName}`, 'claude', options.scope),
|
|
2239
|
+
label: `${resolveClaudePaths(options.cwd, options.scope, options.env).settingsPath}#marketplaces.${options.marketplaceName}`,
|
|
2240
|
+
inspect,
|
|
2241
|
+
remove: async () => {
|
|
2242
|
+
if (await inspect() !== 'present')
|
|
2243
|
+
return;
|
|
2244
|
+
const before = await readClaudeNativeState(options);
|
|
2245
|
+
if (before.pluginState !== 'absent')
|
|
2246
|
+
return;
|
|
2247
|
+
if (before.marketplaceConsumers.length > 0)
|
|
2248
|
+
return;
|
|
2249
|
+
await runNativeCliChecked({
|
|
2250
|
+
command: 'claude',
|
|
2251
|
+
args: ['plugin', 'marketplace', 'remove', options.marketplaceName],
|
|
2252
|
+
cwd: options.cwd,
|
|
2253
|
+
env: options.env,
|
|
2254
|
+
}, options.runner);
|
|
2255
|
+
const after = await readClaudeNativeState(options);
|
|
2256
|
+
if (after.marketplaceState !== 'absent') {
|
|
2257
|
+
throw new Error('Claude marketplace remove 后 canonical aw source 仍存在。');
|
|
2258
|
+
}
|
|
2259
|
+
},
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
function createNativeVersionComponent(options) {
|
|
2263
|
+
return {
|
|
2264
|
+
kind: 'shared-config',
|
|
2265
|
+
identity: ownerIdentity(options.pluginName, internalArtifactKey('native-version', options.pluginVersion), options.platform, options.scope),
|
|
2266
|
+
label: `${options.pluginName}@${options.marketplaceName ?? MARKETPLACE_NAME}#version=${options.pluginVersion}`,
|
|
2267
|
+
inspect: async () => {
|
|
2268
|
+
let versions;
|
|
2269
|
+
if (options.platform === 'codex') {
|
|
2270
|
+
const residue = await inspectCodexNativeResidue({
|
|
2271
|
+
pluginName: options.pluginName,
|
|
2272
|
+
cwd: options.cwd,
|
|
2273
|
+
env: options.env,
|
|
2274
|
+
marketplaceName: options.marketplaceName,
|
|
2275
|
+
marketplaceRoot: options.marketplaceRoot,
|
|
2276
|
+
});
|
|
2277
|
+
versions = residue.enabled && residue.state === 'present' ? residue.versions : [];
|
|
2278
|
+
}
|
|
2279
|
+
else {
|
|
2280
|
+
const state = await readClaudeNativeState({
|
|
2281
|
+
pluginName: options.pluginName,
|
|
2282
|
+
scope: options.scope,
|
|
2283
|
+
cwd: options.cwd,
|
|
2284
|
+
env: options.env,
|
|
2285
|
+
marketplaceName: options.marketplaceName,
|
|
2286
|
+
marketplaceRoot: options.marketplaceRoot,
|
|
2287
|
+
});
|
|
2288
|
+
versions = state.pluginEnabled && state.pluginState === 'present'
|
|
2289
|
+
? state.pluginVersions
|
|
2290
|
+
: [];
|
|
2291
|
+
}
|
|
2292
|
+
return versions.includes(options.pluginVersion) ? 'present' : 'absent';
|
|
2293
|
+
},
|
|
2294
|
+
remove: async () => undefined,
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
async function mirrorCatalog(catalog, env, target) {
|
|
2298
|
+
if (!target?.sourceId || !target.marketplaceRoot) {
|
|
2299
|
+
const rootDir = resolveAwHome(env);
|
|
2300
|
+
await fs.ensureDir(rootDir);
|
|
2301
|
+
await fs.copy(catalog.rootDir, rootDir, { overwrite: true });
|
|
2302
|
+
return { rootDir, commit: async () => undefined, rollback: async () => undefined };
|
|
2303
|
+
}
|
|
2304
|
+
const awHome = path.resolve(resolveAwHome(env));
|
|
2305
|
+
const awHomeStats = await fs.lstat(awHome).catch((error) => {
|
|
2306
|
+
if (error.code === 'ENOENT')
|
|
2307
|
+
return null;
|
|
2308
|
+
throw error;
|
|
2309
|
+
});
|
|
2310
|
+
if (!awHomeStats?.isDirectory() || awHomeStats.isSymbolicLink()) {
|
|
2311
|
+
throw new Error(`marketplaceRootInvalid: AW_HOME must be a regular directory: ${awHome}`);
|
|
2312
|
+
}
|
|
2313
|
+
const canonicalAwHome = await fs.realpath(awHome);
|
|
2314
|
+
const expectedParent = path.join(canonicalAwHome, 'marketplaces');
|
|
2315
|
+
const destination = target.marketplaceRoot;
|
|
2316
|
+
const parent = path.dirname(destination);
|
|
2317
|
+
if (path.resolve(parent) !== path.join(awHome, 'marketplaces')) {
|
|
2318
|
+
throw new Error(`marketplaceRootInvalid: marketplace parent is not under canonical AW_HOME: ${parent}`);
|
|
2319
|
+
}
|
|
2320
|
+
let parentStats = await fs.lstat(parent).catch((error) => {
|
|
2321
|
+
if (error.code === 'ENOENT')
|
|
2322
|
+
return null;
|
|
2323
|
+
throw error;
|
|
2324
|
+
});
|
|
2325
|
+
if (!parentStats) {
|
|
2326
|
+
await fs.mkdir(parent);
|
|
2327
|
+
parentStats = await fs.lstat(parent);
|
|
2328
|
+
}
|
|
2329
|
+
if (!parentStats.isDirectory() || parentStats.isSymbolicLink()) {
|
|
2330
|
+
throw new Error(`marketplaceRootInvalid: marketplace parent must be a regular directory: ${parent}`);
|
|
2331
|
+
}
|
|
2332
|
+
const canonicalParent = await fs.realpath(parent);
|
|
2333
|
+
if (canonicalParent !== expectedParent) {
|
|
2334
|
+
throw new Error(`marketplaceRootInvalid: marketplace parent is outside canonical AW_HOME: ${parent}`);
|
|
2335
|
+
}
|
|
2336
|
+
const staging = await fs.mkdtemp(path.join(parent, `.${target.sourceId}-staging-`));
|
|
2337
|
+
try {
|
|
2338
|
+
for (const entry of ['.agents', '.claude-plugin', 'plugins']) {
|
|
2339
|
+
await fs.copy(path.join(catalog.rootDir, entry), path.join(staging, entry));
|
|
2340
|
+
}
|
|
2341
|
+
const marketplaceName = target.marketplaceName ?? getNamespacedMarketplaceName(target.sourceId);
|
|
2342
|
+
for (const [platform, manifestPath] of [
|
|
2343
|
+
['codex', path.join(staging, '.agents', 'plugins', 'marketplace.json')],
|
|
2344
|
+
['claude', path.join(staging, '.claude-plugin', 'marketplace.json')],
|
|
2345
|
+
]) {
|
|
2346
|
+
const manifest = await fs.readJson(manifestPath);
|
|
2347
|
+
await fs.writeJson(manifestPath, {
|
|
2348
|
+
...projectMarketplaceManifestForPlatform(manifest, platform),
|
|
2349
|
+
name: marketplaceName,
|
|
2350
|
+
}, { spaces: 2 });
|
|
2351
|
+
}
|
|
2352
|
+
await fs.writeJson(path.join(staging, 'config.json'), {
|
|
2353
|
+
schemaVersion: 2,
|
|
2354
|
+
sourceId: target.sourceId,
|
|
2355
|
+
sourceIdentity: target.sourceIdentity,
|
|
2356
|
+
marketplaceName,
|
|
2357
|
+
}, { spaces: 2 });
|
|
2358
|
+
const destinationExists = await fs.lstat(destination).then(() => true, (error) => {
|
|
2359
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR')
|
|
2360
|
+
return false;
|
|
2361
|
+
throw error;
|
|
2362
|
+
});
|
|
2363
|
+
if (destinationExists) {
|
|
2364
|
+
await replayNamespacedCodexSkillOverlays({
|
|
2365
|
+
sourceRoot: destination,
|
|
2366
|
+
stagingRoot: staging,
|
|
2367
|
+
sourceId: target.sourceId,
|
|
2368
|
+
sourceIdentity: target.sourceIdentity,
|
|
2369
|
+
marketplaceName,
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
const backup = `${destination}.backup-${crypto.randomUUID()}`;
|
|
2373
|
+
if (destinationExists)
|
|
2374
|
+
await fs.move(destination, backup);
|
|
2375
|
+
try {
|
|
2376
|
+
await fs.move(staging, destination);
|
|
2377
|
+
}
|
|
2378
|
+
catch (error) {
|
|
2379
|
+
const backupStats = await fs.lstat(backup).catch((inspectionError) => {
|
|
2380
|
+
if (inspectionError.code === 'ENOENT')
|
|
2381
|
+
return null;
|
|
2382
|
+
throw inspectionError;
|
|
2383
|
+
});
|
|
2384
|
+
const destinationStats = await fs.lstat(destination).catch((inspectionError) => {
|
|
2385
|
+
if (inspectionError.code === 'ENOENT')
|
|
2386
|
+
return null;
|
|
2387
|
+
throw inspectionError;
|
|
2388
|
+
});
|
|
2389
|
+
if (backupStats && !destinationStats) {
|
|
2390
|
+
await fs.move(backup, destination);
|
|
2391
|
+
}
|
|
2392
|
+
throw error;
|
|
2393
|
+
}
|
|
2394
|
+
let state = 'active';
|
|
2395
|
+
return {
|
|
2396
|
+
rootDir: destination,
|
|
2397
|
+
commit: async () => {
|
|
2398
|
+
if (state !== 'active')
|
|
2399
|
+
return;
|
|
2400
|
+
try {
|
|
2401
|
+
await commitMarketplaceGenerationBackup({
|
|
2402
|
+
backupRoot: backup,
|
|
2403
|
+
sourceId: target.sourceId,
|
|
2404
|
+
sourceIdentity: target.sourceIdentity,
|
|
2405
|
+
...(target.marketplaceName ? { marketplaceName: target.marketplaceName } : {}),
|
|
2406
|
+
env,
|
|
2407
|
+
});
|
|
2408
|
+
state = 'committed';
|
|
2409
|
+
}
|
|
2410
|
+
catch (error) {
|
|
2411
|
+
if (error.durableMaterializeCommit === true) {
|
|
2412
|
+
state = 'committed';
|
|
2413
|
+
}
|
|
2414
|
+
throw error;
|
|
2415
|
+
}
|
|
2416
|
+
},
|
|
2417
|
+
rollback: async () => {
|
|
2418
|
+
if (state !== 'active')
|
|
2419
|
+
return;
|
|
2420
|
+
const backupStats = await fs.lstat(backup).catch((error) => {
|
|
2421
|
+
if (error.code === 'ENOENT')
|
|
2422
|
+
return null;
|
|
2423
|
+
throw error;
|
|
2424
|
+
});
|
|
2425
|
+
await fs.remove(destination);
|
|
2426
|
+
if (backupStats)
|
|
2427
|
+
await fs.move(backup, destination);
|
|
2428
|
+
state = 'rolledBack';
|
|
2429
|
+
},
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
catch (error) {
|
|
2433
|
+
await fs.remove(staging);
|
|
2434
|
+
throw error;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
function createNativeDriver(options) {
|
|
2438
|
+
const runner = options.context.nativeCliRunner;
|
|
2439
|
+
const materializeLeaseHeld = new WeakSet();
|
|
2440
|
+
const mirrorTransactions = new WeakMap();
|
|
2441
|
+
const finalizeNamespacedCleanup = async (target) => {
|
|
2442
|
+
const { sourceId, sourceIdentity, marketplaceRoot, marketplaceName } = target.identity;
|
|
2443
|
+
if (!sourceId || !sourceIdentity || !marketplaceRoot)
|
|
2444
|
+
return;
|
|
2445
|
+
await cleanupOrphanNamespacedMarketplaceRoot({
|
|
2446
|
+
sourceId,
|
|
2447
|
+
sourceIdentity,
|
|
2448
|
+
...(marketplaceName ? { marketplaceName } : {}),
|
|
2449
|
+
cwd: target.identity.cwd,
|
|
2450
|
+
env: options.context.env,
|
|
2451
|
+
});
|
|
2452
|
+
};
|
|
2453
|
+
return {
|
|
2454
|
+
withOwnershipLease: (target, operation) => (!options.context.runtimeLeaseHeld
|
|
2455
|
+
&& target.identity.platform === 'codex'
|
|
2456
|
+
&& target.identity.scope === 'user'
|
|
2457
|
+
&& !materializeLeaseHeld.has(target)
|
|
2458
|
+
? withActiveMarketplaceRuntimeLease(options.context.env, operation)
|
|
2459
|
+
: operation()),
|
|
2460
|
+
withMaterializeLease: (target, operation) => {
|
|
2461
|
+
if (!target.identity.sourceId)
|
|
2462
|
+
return operation();
|
|
2463
|
+
if (options.context.runtimeLeaseHeld && !options.context.lifecycleLeaseHeld) {
|
|
2464
|
+
throw new Error('lockOrderViolation: namespaced materialization cannot acquire lifecycle after runtime.');
|
|
2465
|
+
}
|
|
2466
|
+
const runOperation = async () => {
|
|
2467
|
+
const previousRuntimeHeld = options.context.runtimeLeaseHeld;
|
|
2468
|
+
options.context.runtimeLeaseHeld = true;
|
|
2469
|
+
materializeLeaseHeld.add(target);
|
|
2470
|
+
try {
|
|
2471
|
+
if (options.context.expectedCatalogConfigDigest) {
|
|
2472
|
+
const current = await readCatalogSourcesConfigSnapshot(options.context.env);
|
|
2473
|
+
if (current.digest !== options.context.expectedCatalogConfigDigest) {
|
|
2474
|
+
throw new Error('catalogSourceConflict: catalog source config changed before owner publication.');
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
return await operation();
|
|
2478
|
+
}
|
|
2479
|
+
finally {
|
|
2480
|
+
materializeLeaseHeld.delete(target);
|
|
2481
|
+
options.context.runtimeLeaseHeld = previousRuntimeHeld;
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
const withRuntime = () => options.context.runtimeLeaseHeld
|
|
2485
|
+
? runOperation()
|
|
2486
|
+
: withActiveMarketplaceRuntimeLease(options.context.env, runOperation);
|
|
2487
|
+
if (options.context.lifecycleLeaseHeld)
|
|
2488
|
+
return withRuntime();
|
|
2489
|
+
return withActiveMarketplaceLifecycleLease(options.context.env, async () => {
|
|
2490
|
+
const previousLifecycleHeld = options.context.lifecycleLeaseHeld;
|
|
2491
|
+
options.context.lifecycleLeaseHeld = true;
|
|
2492
|
+
try {
|
|
2493
|
+
return await withRuntime();
|
|
2494
|
+
}
|
|
2495
|
+
finally {
|
|
2496
|
+
options.context.lifecycleLeaseHeld = previousLifecycleHeld;
|
|
2497
|
+
}
|
|
2498
|
+
});
|
|
2499
|
+
},
|
|
2500
|
+
preflightMaterialize: async (target) => {
|
|
2501
|
+
if (target.identity.platform === 'claude') {
|
|
2502
|
+
await assertNoClaudeUnboundNativeNameCollision({
|
|
2503
|
+
pluginName: target.identity.pluginName,
|
|
2504
|
+
cwd: target.identity.cwd,
|
|
2505
|
+
env: options.context.env,
|
|
2506
|
+
});
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
const marketplaceName = target.identity.marketplaceName ?? MARKETPLACE_NAME;
|
|
2510
|
+
const marketplaceRoot = target.identity.marketplaceRoot ?? resolveAwHome(options.context.env);
|
|
2511
|
+
const config = await readCodexConfigFile(resolveCodexPaths(target.identity.cwd, 'user', options.context.env).configTomlPath);
|
|
2512
|
+
const marketplace = config.snapshot.marketplaces[marketplaceName];
|
|
2513
|
+
const canonical = marketplace?.sourceType === 'local'
|
|
2514
|
+
&& !marketplace.sourcePointsToManifest
|
|
2515
|
+
&& path.resolve(marketplace.source ?? '') === path.resolve(marketplaceRoot);
|
|
2516
|
+
const expectedPluginId = `${target.identity.pluginName}@${marketplaceName}`;
|
|
2517
|
+
if ((marketplace !== undefined && !canonical)
|
|
2518
|
+
|| (Object.hasOwn(config.snapshot.plugins, expectedPluginId) && !canonical)) {
|
|
2519
|
+
throw new Error(`externalOwnershipPresent: Codex marketplace ${marketplaceName} has a foreign source instead of its canonical namespaced root.`);
|
|
2520
|
+
}
|
|
2521
|
+
},
|
|
2522
|
+
commitMaterialize: async (target) => {
|
|
2523
|
+
const transaction = mirrorTransactions.get(target);
|
|
2524
|
+
if (!transaction)
|
|
2525
|
+
return;
|
|
2526
|
+
await transaction.commit();
|
|
2527
|
+
mirrorTransactions.delete(target);
|
|
2528
|
+
},
|
|
2529
|
+
rollbackMaterialize: async (target) => {
|
|
2530
|
+
const transaction = mirrorTransactions.get(target);
|
|
2531
|
+
if (!transaction)
|
|
2532
|
+
return;
|
|
2533
|
+
await transaction.rollback();
|
|
2534
|
+
mirrorTransactions.delete(target);
|
|
2535
|
+
},
|
|
2536
|
+
finalizeCleanup: finalizeNamespacedCleanup,
|
|
2537
|
+
probeNative: async (component) => {
|
|
2538
|
+
const target = [...plansTargetEntries(options.plans)].find((entry) => entry.target.identity.pluginName === component.identity.plugin
|
|
2539
|
+
&& entry.target.identity.sourceId === component.identity.sourceId
|
|
2540
|
+
&& entry.target.identity.platform === component.identity.platform
|
|
2541
|
+
&& entry.target.identity.scope === component.identity.scope)?.target;
|
|
2542
|
+
if (!target)
|
|
2543
|
+
throw new Error('Native ownership probe cannot resolve its desired target.');
|
|
2544
|
+
if (target.identity.platform === 'claude') {
|
|
2545
|
+
const state = await readClaudeNativeState({
|
|
2546
|
+
pluginName: target.identity.pluginName,
|
|
2547
|
+
scope: target.identity.scope,
|
|
2548
|
+
cwd: target.identity.cwd,
|
|
2549
|
+
env: options.context.env,
|
|
2550
|
+
marketplaceName: target.identity.marketplaceName,
|
|
2551
|
+
marketplaceRoot: target.identity.marketplaceRoot,
|
|
2552
|
+
});
|
|
2553
|
+
return state.pluginState;
|
|
2554
|
+
}
|
|
2555
|
+
if (target.identity.platform === 'codex' && target.identity.scope === 'user') {
|
|
2556
|
+
const residue = await inspectCodexNativeResidue({
|
|
2557
|
+
pluginName: target.identity.pluginName,
|
|
2558
|
+
cwd: target.identity.cwd,
|
|
2559
|
+
env: options.context.env,
|
|
2560
|
+
marketplaceName: target.identity.marketplaceName,
|
|
2561
|
+
marketplaceRoot: target.identity.marketplaceRoot,
|
|
2562
|
+
});
|
|
2563
|
+
return residue.state;
|
|
2564
|
+
}
|
|
2565
|
+
return 'absent';
|
|
2566
|
+
},
|
|
2567
|
+
probeNativeHealth: async (component) => {
|
|
2568
|
+
const target = [...plansTargetEntries(options.plans)].find((entry) => entry.target.identity.pluginName === component.identity.plugin
|
|
2569
|
+
&& entry.target.identity.sourceId === component.identity.sourceId
|
|
2570
|
+
&& entry.target.identity.platform === component.identity.platform
|
|
2571
|
+
&& entry.target.identity.scope === component.identity.scope)?.target;
|
|
2572
|
+
if (!target)
|
|
2573
|
+
throw new Error('Native health probe cannot resolve its desired target.');
|
|
2574
|
+
if (target.identity.platform === 'claude') {
|
|
2575
|
+
const state = await readClaudeNativeState({
|
|
2576
|
+
pluginName: target.identity.pluginName,
|
|
2577
|
+
scope: target.identity.scope,
|
|
2578
|
+
cwd: target.identity.cwd,
|
|
2579
|
+
env: options.context.env,
|
|
2580
|
+
marketplaceName: target.identity.marketplaceName,
|
|
2581
|
+
marketplaceRoot: target.identity.marketplaceRoot,
|
|
2582
|
+
});
|
|
2583
|
+
if (state.pluginState !== 'present')
|
|
2584
|
+
return state.pluginState;
|
|
2585
|
+
return state.pluginEnabled ? 'present' : 'partial';
|
|
2586
|
+
}
|
|
2587
|
+
if (target.identity.platform === 'codex' && target.identity.scope === 'user') {
|
|
2588
|
+
const residue = await inspectCodexNativeResidue({
|
|
2589
|
+
pluginName: target.identity.pluginName,
|
|
2590
|
+
cwd: target.identity.cwd,
|
|
2591
|
+
env: options.context.env,
|
|
2592
|
+
marketplaceName: target.identity.marketplaceName,
|
|
2593
|
+
marketplaceRoot: target.identity.marketplaceRoot,
|
|
2594
|
+
});
|
|
2595
|
+
if (residue.state !== 'present')
|
|
2596
|
+
return residue.state;
|
|
2597
|
+
return residue.enabled ? 'present' : 'partial';
|
|
2598
|
+
}
|
|
2599
|
+
return 'absent';
|
|
2600
|
+
},
|
|
2601
|
+
removeNative: async (component) => {
|
|
2602
|
+
const target = [...plansTargetEntries(options.plans)].find((entry) => entry.target.identity.pluginName === component.identity.plugin
|
|
2603
|
+
&& entry.target.identity.sourceId === component.identity.sourceId
|
|
2604
|
+
&& entry.target.identity.platform === component.identity.platform
|
|
2605
|
+
&& entry.target.identity.scope === component.identity.scope)?.target;
|
|
2606
|
+
if (!target)
|
|
2607
|
+
throw new Error('Native ownership removal cannot resolve its desired target.');
|
|
2608
|
+
if (target.identity.platform === 'codex') {
|
|
2609
|
+
const before = await inspectCodexNativeResidue({
|
|
2610
|
+
pluginName: target.identity.pluginName,
|
|
2611
|
+
cwd: target.identity.cwd,
|
|
2612
|
+
env: options.context.env,
|
|
2613
|
+
marketplaceName: target.identity.marketplaceName,
|
|
2614
|
+
marketplaceRoot: target.identity.marketplaceRoot,
|
|
2615
|
+
});
|
|
2616
|
+
if (before.state === 'foreign') {
|
|
2617
|
+
throw new Error(`Codex native owner 为 foreign,拒绝调用 plugin remove:${before.labels.join(',')}`);
|
|
2618
|
+
}
|
|
2619
|
+
if (before.state === 'absent')
|
|
2620
|
+
return;
|
|
2621
|
+
await runNativeCliChecked(buildCodexPluginRemoveInvocation(component.pluginId, target.identity.cwd, options.context.env), runner);
|
|
2622
|
+
const residue = await inspectCodexNativeResidue({
|
|
2623
|
+
pluginName: target.identity.pluginName,
|
|
2624
|
+
cwd: target.identity.cwd,
|
|
2625
|
+
env: options.context.env,
|
|
2626
|
+
marketplaceName: target.identity.marketplaceName,
|
|
2627
|
+
marketplaceRoot: target.identity.marketplaceRoot,
|
|
2628
|
+
});
|
|
2629
|
+
if (residue.state !== 'absent') {
|
|
2630
|
+
throw new Error(`Codex native remove 后仍有残留:${residue.labels.join(',')}`);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
else {
|
|
2634
|
+
await runNativeCliChecked({
|
|
2635
|
+
command: 'claude',
|
|
2636
|
+
args: ['plugin', 'uninstall', component.pluginId, '--scope', target.identity.scope],
|
|
2637
|
+
cwd: target.identity.cwd,
|
|
2638
|
+
env: options.context.env,
|
|
2639
|
+
}, runner);
|
|
2640
|
+
await runNativeCliChecked({
|
|
2641
|
+
command: 'claude',
|
|
2642
|
+
args: ['plugin', 'list'],
|
|
2643
|
+
cwd: target.identity.cwd,
|
|
2644
|
+
env: options.context.env,
|
|
2645
|
+
}, runner);
|
|
2646
|
+
const after = await readClaudeNativeState({
|
|
2647
|
+
pluginName: target.identity.pluginName,
|
|
2648
|
+
scope: target.identity.scope,
|
|
2649
|
+
cwd: target.identity.cwd,
|
|
2650
|
+
env: options.context.env,
|
|
2651
|
+
marketplaceName: target.identity.marketplaceName,
|
|
2652
|
+
marketplaceRoot: target.identity.marketplaceRoot,
|
|
2653
|
+
});
|
|
2654
|
+
if (after.pluginState !== 'absent') {
|
|
2655
|
+
throw new Error(`Claude native uninstall 后仍有 ${after.pluginState} plugin evidence。`);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
},
|
|
2659
|
+
writeNative: async (target) => {
|
|
2660
|
+
const plan = options.plans.get(target);
|
|
2661
|
+
if (!plan)
|
|
2662
|
+
throw new Error('Missing rebuild integration plan.');
|
|
2663
|
+
const marketplaceName = target.identity.marketplaceName ?? MARKETPLACE_NAME;
|
|
2664
|
+
const expectedMarketplaceRoot = target.identity.marketplaceRoot ?? resolveAwHome(options.context.env);
|
|
2665
|
+
const writeGeneration = async () => {
|
|
2666
|
+
if (mirrorTransactions.has(target)) {
|
|
2667
|
+
throw new Error('Marketplace mirror transaction is already active for this target.');
|
|
2668
|
+
}
|
|
2669
|
+
{
|
|
2670
|
+
if (target.identity.platform === 'codex') {
|
|
2671
|
+
const config = await readCodexConfigFile(resolveCodexPaths(target.identity.cwd, 'user', options.context.env).configTomlPath);
|
|
2672
|
+
const marketplace = config.snapshot.marketplaces[marketplaceName];
|
|
2673
|
+
if (marketplace && (marketplace.sourceType !== 'local'
|
|
2674
|
+
|| marketplace.sourcePointsToManifest
|
|
2675
|
+
|| path.normalize(marketplace.source ?? '') !== path.normalize(expectedMarketplaceRoot))) {
|
|
2676
|
+
throw new Error(`externalOwnershipPresent: Codex marketplace ${marketplaceName} 已由 foreign source 占用,拒绝覆盖。`);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
else {
|
|
2680
|
+
const state = await readClaudeNativeState({
|
|
2681
|
+
pluginName: target.identity.pluginName,
|
|
2682
|
+
scope: target.identity.scope,
|
|
2683
|
+
cwd: target.identity.cwd,
|
|
2684
|
+
env: options.context.env,
|
|
2685
|
+
marketplaceName,
|
|
2686
|
+
marketplaceRoot: expectedMarketplaceRoot,
|
|
2687
|
+
});
|
|
2688
|
+
if (state.marketplaceState === 'foreign') {
|
|
2689
|
+
throw new Error(`Claude marketplace ${marketplaceName} 已由 foreign source 占用,拒绝覆盖。`);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
const mirrored = await mirrorCatalog(plan.catalog, options.context.env, target.identity);
|
|
2693
|
+
mirrorTransactions.set(target, mirrored);
|
|
2694
|
+
const marketplaceRoot = mirrored.rootDir;
|
|
2695
|
+
const pluginId = `${target.identity.pluginName}@${marketplaceName}`;
|
|
2696
|
+
if (target.identity.platform === 'codex') {
|
|
2697
|
+
const requiredSkillWrites = plan.writes.filter((entry) => !entry.optional
|
|
2698
|
+
&& entry.component?.kind === 'skill-directory'
|
|
2699
|
+
&& entry.artifact.kind === 'skill'
|
|
2700
|
+
&& entry.artifact.state !== 'disabled'
|
|
2701
|
+
&& target.identity.scope === 'user');
|
|
2702
|
+
for (const write of requiredSkillWrites) {
|
|
2703
|
+
await write.write();
|
|
2704
|
+
}
|
|
2705
|
+
const skillsRelativeRoot = await ensureCodexActiveSkillsManifest({
|
|
2706
|
+
plugin: target.plugin,
|
|
2707
|
+
marketplaceRoot,
|
|
2708
|
+
hasRequiredSkills: requiredSkillWrites.length > 0,
|
|
2709
|
+
});
|
|
2710
|
+
const config = await readCodexConfigFile(resolveCodexPaths(target.identity.cwd, 'user', options.context.env).configTomlPath);
|
|
2711
|
+
const marketplace = config.snapshot.marketplaces[marketplaceName];
|
|
2712
|
+
if (path.normalize(marketplace?.source ?? '') !== path.normalize(marketplaceRoot)) {
|
|
2713
|
+
await runNativeCliChecked(buildCodexMarketplaceAddInvocation(marketplaceRoot, target.identity.cwd, options.context.env), runner);
|
|
2714
|
+
}
|
|
2715
|
+
await runNativeCliChecked(buildCodexMarketplaceListInvocation(target.identity.cwd, options.context.env), runner);
|
|
2716
|
+
await runNativeCliChecked(buildCodexPluginAddInvocation(pluginId, target.identity.cwd, options.context.env), runner);
|
|
2717
|
+
await runNativeCliChecked(buildCodexPluginListInvocation(target.identity.cwd, options.context.env), runner);
|
|
2718
|
+
await assertCodexNativeInstallState({
|
|
2719
|
+
pluginName: target.identity.pluginName,
|
|
2720
|
+
pluginVersion: target.identity.pluginVersion,
|
|
2721
|
+
cwd: target.identity.cwd,
|
|
2722
|
+
env: options.context.env,
|
|
2723
|
+
marketplaceRoot,
|
|
2724
|
+
marketplaceName,
|
|
2725
|
+
skillsRelativeRoot,
|
|
2726
|
+
requiredSkillKeys: requiredSkillWrites.flatMap((write) => write.artifact.artifactKey ? [write.artifact.artifactKey] : []),
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
else {
|
|
2730
|
+
await runNativeCliChecked({
|
|
2731
|
+
command: 'claude',
|
|
2732
|
+
args: ['plugin', 'marketplace', 'add', marketplaceRoot, '--scope', target.identity.scope],
|
|
2733
|
+
cwd: target.identity.cwd,
|
|
2734
|
+
env: options.context.env,
|
|
2735
|
+
}, runner);
|
|
2736
|
+
await runNativeCliChecked({
|
|
2737
|
+
command: 'claude',
|
|
2738
|
+
args: ['plugin', 'install', pluginId, '--scope', target.identity.scope],
|
|
2739
|
+
cwd: target.identity.cwd,
|
|
2740
|
+
env: options.context.env,
|
|
2741
|
+
}, runner);
|
|
2742
|
+
await runNativeCliChecked({
|
|
2743
|
+
command: 'claude',
|
|
2744
|
+
args: ['plugin', 'list'],
|
|
2745
|
+
cwd: target.identity.cwd,
|
|
2746
|
+
env: options.context.env,
|
|
2747
|
+
}, runner);
|
|
2748
|
+
const after = await readClaudeNativeState({
|
|
2749
|
+
pluginName: target.identity.pluginName,
|
|
2750
|
+
scope: target.identity.scope,
|
|
2751
|
+
cwd: target.identity.cwd,
|
|
2752
|
+
env: options.context.env,
|
|
2753
|
+
marketplaceName,
|
|
2754
|
+
marketplaceRoot,
|
|
2755
|
+
});
|
|
2756
|
+
if (after.marketplaceDeclaredState !== 'canonical'
|
|
2757
|
+
|| after.marketplaceMaterializedState !== 'canonical'
|
|
2758
|
+
|| after.pluginState !== 'present'
|
|
2759
|
+
|| !after.pluginEnabled
|
|
2760
|
+
|| !after.pluginVersions.includes(target.identity.pluginVersion)) {
|
|
2761
|
+
throw new Error(`Claude native install postcheck 失败:declaredMarketplace=${after.marketplaceDeclaredState},materializedMarketplace=${after.marketplaceMaterializedState},plugin=${after.pluginState},enabled=${after.pluginEnabled},versions=${after.pluginVersions.join(',')}。`);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
};
|
|
2766
|
+
await (target.identity.sourceId
|
|
2767
|
+
&& !options.context.runtimeLeaseHeld
|
|
2768
|
+
&& !materializeLeaseHeld.has(target)
|
|
2769
|
+
? withActiveMarketplaceRuntimeLease(options.context.env, writeGeneration)
|
|
2770
|
+
: writeGeneration());
|
|
2771
|
+
},
|
|
2772
|
+
writeArtifacts: async (target) => {
|
|
2773
|
+
const plan = options.plans.get(target);
|
|
2774
|
+
if (!plan)
|
|
2775
|
+
throw new Error('Missing rebuild integration plan.');
|
|
2776
|
+
const writeAll = async () => {
|
|
2777
|
+
for (const write of plan.writes)
|
|
2778
|
+
await write.write();
|
|
2779
|
+
return plan.writes.map((write) => write.artifact);
|
|
2780
|
+
};
|
|
2781
|
+
return !options.context.runtimeLeaseHeld
|
|
2782
|
+
&& !materializeLeaseHeld.has(target)
|
|
2783
|
+
&& target.identity.platform === 'codex' && target.identity.scope === 'user'
|
|
2784
|
+
? withActiveMarketplaceRuntimeLease(options.context.env, writeAll)
|
|
2785
|
+
: writeAll();
|
|
2786
|
+
},
|
|
2787
|
+
runLifecycle: async (invocation) => {
|
|
2788
|
+
await runLifecycleScript({
|
|
2789
|
+
event: invocation.event,
|
|
2790
|
+
pluginName: invocation.plugin.manifest.name,
|
|
2791
|
+
platform: invocation.target.identity.platform,
|
|
2792
|
+
scope: invocation.target.identity.scope,
|
|
2793
|
+
cwd: invocation.target.identity.cwd,
|
|
2794
|
+
context: options.context,
|
|
2795
|
+
currentPlugin: invocation.plugin,
|
|
2796
|
+
artifacts: [...invocation.artifacts],
|
|
2797
|
+
scriptTimeoutMs: options.scriptTimeoutMs,
|
|
2798
|
+
});
|
|
2799
|
+
},
|
|
2800
|
+
};
|
|
2801
|
+
}
|
|
2802
|
+
const planTargets = new WeakMap();
|
|
2803
|
+
function plansTargetEntries(plans) {
|
|
2804
|
+
return (planTargets.get(plans) ?? []).flatMap((target) => {
|
|
2805
|
+
const plan = plans.get(target);
|
|
2806
|
+
return plan ? [{ target, plan }] : [];
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
export function createRebuildIntegration(options) {
|
|
2810
|
+
const plans = new WeakMap();
|
|
2811
|
+
planTargets.set(plans, []);
|
|
2812
|
+
const driver = createNativeDriver({ plans, ...options });
|
|
2813
|
+
return {
|
|
2814
|
+
driver,
|
|
2815
|
+
buildTarget: async (input) => {
|
|
2816
|
+
const materializing = input.command !== 'cleanup'
|
|
2817
|
+
&& input.command !== 'uninstall'
|
|
2818
|
+
&& input.readOnlyInspectionPlan !== true;
|
|
2819
|
+
if (input.scope === 'project') {
|
|
2820
|
+
const canonicalRepoRoot = await assertCanonicalProjectManagedRoots({
|
|
2821
|
+
cwd: input.cwd,
|
|
2822
|
+
rootNames: input.platform === 'codex' ? ['.agents', '.codex'] : ['.claude'],
|
|
2823
|
+
managedPaths: input.platform === 'codex'
|
|
2824
|
+
? ['.codex/config.toml', '.codex/hooks.json']
|
|
2825
|
+
: ['.claude/settings.json'],
|
|
2826
|
+
});
|
|
2827
|
+
input = { ...input, cwd: canonicalRepoRoot };
|
|
2828
|
+
if (materializing && input.platform === 'codex' && input.plugin.manifest.apps) {
|
|
2829
|
+
throw new Error(`Codex project scope 不支持 apps:${input.plugin.manifest.name}`);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
if (materializing && input.platform === 'codex') {
|
|
2833
|
+
await preflightCodexWriteSafety({
|
|
2834
|
+
plugin: input.plugin,
|
|
2835
|
+
scope: input.scope,
|
|
2836
|
+
cwd: input.cwd,
|
|
2837
|
+
env: options.context.env,
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
const event = input.command === 'upgrade' ? 'upgrade' : 'install';
|
|
2841
|
+
let historicalArtifacts = [];
|
|
2842
|
+
if (input.includeHistoricalLifecycleArtifacts) {
|
|
2843
|
+
const loadPlugin = (bundleRoot) => readPluginManifest(bundleRoot, { mode: 'bundle' });
|
|
2844
|
+
const { readLifecycleActiveSnapshot } = await import('../utils/lifecycle-active-store.js');
|
|
2845
|
+
const active = await readLifecycleActiveSnapshot({
|
|
2846
|
+
...(input.sourceId ? {
|
|
2847
|
+
sourceId: input.sourceId,
|
|
2848
|
+
sourceIdentity: input.sourceIdentity,
|
|
2849
|
+
} : {}),
|
|
2850
|
+
pluginName: input.plugin.manifest.name,
|
|
2851
|
+
platform: input.platform,
|
|
2852
|
+
scope: input.scope,
|
|
2853
|
+
cwd: input.cwd,
|
|
2854
|
+
env: options.context.env,
|
|
2855
|
+
loadPlugin,
|
|
2856
|
+
});
|
|
2857
|
+
historicalArtifacts = active?.artifacts ?? [];
|
|
2858
|
+
}
|
|
2859
|
+
let writes = [];
|
|
2860
|
+
if (input.command !== 'cleanup' && input.command !== 'uninstall') {
|
|
2861
|
+
if (input.readOnlyInspectionPlan) {
|
|
2862
|
+
writes = await buildInspectionWrites({
|
|
2863
|
+
plugin: input.plugin,
|
|
2864
|
+
platform: input.platform,
|
|
2865
|
+
scope: input.scope,
|
|
2866
|
+
cwd: input.cwd,
|
|
2867
|
+
env: options.context.env,
|
|
2868
|
+
marketplaceRoot: input.marketplaceRoot,
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
else if (input.platform === 'codex') {
|
|
2872
|
+
writes = await buildCodexWrites({
|
|
2873
|
+
...input,
|
|
2874
|
+
env: options.context.env,
|
|
2875
|
+
event,
|
|
2876
|
+
historicalArtifacts,
|
|
2877
|
+
scriptTimeoutMs: options.scriptTimeoutMs,
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
else {
|
|
2881
|
+
writes = await buildClaudeWrites({
|
|
2882
|
+
...input,
|
|
2883
|
+
env: options.context.env,
|
|
2884
|
+
event,
|
|
2885
|
+
historicalArtifacts,
|
|
2886
|
+
scriptTimeoutMs: options.scriptTimeoutMs,
|
|
2887
|
+
});
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
const desiredComponents = writes.filter((write) => !write.optional).flatMap((write) => [
|
|
2891
|
+
...(write.additionalComponents ?? []),
|
|
2892
|
+
...(write.component ? [write.component] : []),
|
|
2893
|
+
]);
|
|
2894
|
+
const optionalComponents = writes
|
|
2895
|
+
.filter((write) => write.optional)
|
|
2896
|
+
.flatMap((write) => [
|
|
2897
|
+
...(write.additionalComponents ?? []),
|
|
2898
|
+
...(write.component ? [write.component] : []),
|
|
2899
|
+
]);
|
|
2900
|
+
const optionalComponentKeys = new Set(optionalComponents.map(componentIdentityKey));
|
|
2901
|
+
const configDir = input.platform === 'codex'
|
|
2902
|
+
? resolveCodexPaths(input.cwd, input.scope, options.context.env).configDir
|
|
2903
|
+
: resolveClaudePaths(input.cwd, input.scope, options.context.env).configDir;
|
|
2904
|
+
const textRoots = input.platform === 'codex'
|
|
2905
|
+
? [path.join(configDir, 'rules'), path.join(configDir, 'agents')]
|
|
2906
|
+
: [path.join(configDir, 'rules'), path.join(configDir, 'agents')];
|
|
2907
|
+
const scannedText = (await Promise.all(textRoots.map((root) => scanOwnedTextFiles({
|
|
2908
|
+
sourceId: input.sourceId,
|
|
2909
|
+
sourceIdentity: input.sourceIdentity,
|
|
2910
|
+
root,
|
|
2911
|
+
pluginName: input.plugin.manifest.name,
|
|
2912
|
+
platform: input.platform,
|
|
2913
|
+
scope: input.scope,
|
|
2914
|
+
})))).flat();
|
|
2915
|
+
const skillRoots = input.platform === 'codex'
|
|
2916
|
+
? input.scope === 'project'
|
|
2917
|
+
? [path.join(await resolveRepoRoot(input.cwd), '.agents', 'skills')]
|
|
2918
|
+
: [path.join(input.marketplaceRoot ?? resolveAwHome(options.context.env), 'plugins', input.plugin.manifest.name)]
|
|
2919
|
+
: [path.join(configDir, 'skills')];
|
|
2920
|
+
const scanSkills = () => Promise.all(skillRoots.map((root) => scanOwnedSkills({
|
|
2921
|
+
sourceId: input.sourceId,
|
|
2922
|
+
sourceIdentity: input.sourceIdentity,
|
|
2923
|
+
root,
|
|
2924
|
+
pluginName: input.plugin.manifest.name,
|
|
2925
|
+
platform: input.platform,
|
|
2926
|
+
scope: input.scope,
|
|
2927
|
+
})));
|
|
2928
|
+
const scannedSkills = (await (input.platform === 'codex'
|
|
2929
|
+
&& input.scope === 'user'
|
|
2930
|
+
&& input.readOnlyInspectionPlan !== true
|
|
2931
|
+
? withActiveMarketplaceRuntimeLease(options.context.env, scanSkills)
|
|
2932
|
+
: scanSkills())).flat();
|
|
2933
|
+
const historicalComponents = await buildHistoricalComponents({
|
|
2934
|
+
sourceId: input.sourceId,
|
|
2935
|
+
sourceIdentity: input.sourceIdentity,
|
|
2936
|
+
pluginId: input.pluginId,
|
|
2937
|
+
pluginName: input.plugin.manifest.name,
|
|
2938
|
+
platform: input.platform,
|
|
2939
|
+
scope: input.scope,
|
|
2940
|
+
cwd: input.cwd,
|
|
2941
|
+
env: options.context.env,
|
|
2942
|
+
artifacts: historicalArtifacts,
|
|
2943
|
+
});
|
|
2944
|
+
const sharedComponents = input.platform === 'codex'
|
|
2945
|
+
? await scanCodexSharedComponents({
|
|
2946
|
+
sourceId: input.sourceId,
|
|
2947
|
+
sourceIdentity: input.sourceIdentity,
|
|
2948
|
+
pluginId: input.pluginId,
|
|
2949
|
+
pluginName: input.plugin.manifest.name,
|
|
2950
|
+
scope: input.scope,
|
|
2951
|
+
paths: resolveCodexPaths(input.cwd, input.scope, options.context.env),
|
|
2952
|
+
scanConfig: true,
|
|
2953
|
+
scanHooks: true,
|
|
2954
|
+
ignoreReadErrors: true,
|
|
2955
|
+
})
|
|
2956
|
+
: await scanClaudeSharedComponents({
|
|
2957
|
+
sourceId: input.sourceId,
|
|
2958
|
+
sourceIdentity: input.sourceIdentity,
|
|
2959
|
+
pluginId: input.pluginId,
|
|
2960
|
+
pluginName: input.plugin.manifest.name,
|
|
2961
|
+
scope: input.scope,
|
|
2962
|
+
settingsPath: resolveClaudePaths(input.cwd, input.scope, options.context.env).settingsPath,
|
|
2963
|
+
});
|
|
2964
|
+
const nativeRequired = input.platform === 'claude'
|
|
2965
|
+
|| (input.scope === 'user' && requiresCodexNativePlugin(input.plugin, input.scope));
|
|
2966
|
+
const nativeCleanupCandidate = input.platform === 'claude' || input.scope === 'user';
|
|
2967
|
+
const nativePlugin = (nativeRequired || nativeCleanupCandidate)
|
|
2968
|
+
? [{
|
|
2969
|
+
kind: 'native-plugin',
|
|
2970
|
+
identity: ownerIdentity(input.plugin.manifest.name, 'native', input.platform, input.scope),
|
|
2971
|
+
pluginId: `${input.plugin.manifest.name}@${input.marketplaceName ?? MARKETPLACE_NAME}`,
|
|
2972
|
+
}]
|
|
2973
|
+
: [];
|
|
2974
|
+
const marketplaceDesired = nativeRequired
|
|
2975
|
+
&& input.command !== 'cleanup'
|
|
2976
|
+
&& input.command !== 'uninstall';
|
|
2977
|
+
const marketplace = input.platform === 'codex' && input.scope === 'user'
|
|
2978
|
+
? [createCodexMarketplaceComponent({
|
|
2979
|
+
pluginName: input.plugin.manifest.name,
|
|
2980
|
+
cwd: input.cwd,
|
|
2981
|
+
env: options.context.env,
|
|
2982
|
+
runner: options.context.nativeCliRunner,
|
|
2983
|
+
marketplaceName: input.marketplaceName ?? MARKETPLACE_NAME,
|
|
2984
|
+
marketplaceRoot: input.marketplaceRoot ?? resolveAwHome(options.context.env),
|
|
2985
|
+
desired: marketplaceDesired,
|
|
2986
|
+
})]
|
|
2987
|
+
: input.platform === 'claude'
|
|
2988
|
+
? [createClaudeMarketplaceComponent({
|
|
2989
|
+
pluginName: input.plugin.manifest.name,
|
|
2990
|
+
scope: input.scope,
|
|
2991
|
+
cwd: input.cwd,
|
|
2992
|
+
env: options.context.env,
|
|
2993
|
+
runner: options.context.nativeCliRunner,
|
|
2994
|
+
marketplaceName: input.marketplaceName ?? MARKETPLACE_NAME,
|
|
2995
|
+
marketplaceRoot: input.marketplaceRoot ?? resolveAwHome(options.context.env),
|
|
2996
|
+
desired: marketplaceDesired,
|
|
2997
|
+
})]
|
|
2998
|
+
: [];
|
|
2999
|
+
const nativeVersion = nativeRequired
|
|
3000
|
+
&& input.command !== 'cleanup'
|
|
3001
|
+
&& input.command !== 'uninstall'
|
|
3002
|
+
? [createNativeVersionComponent({
|
|
3003
|
+
pluginName: input.plugin.manifest.name,
|
|
3004
|
+
pluginVersion: input.plugin.manifest.version,
|
|
3005
|
+
platform: input.platform,
|
|
3006
|
+
scope: input.scope,
|
|
3007
|
+
cwd: input.cwd,
|
|
3008
|
+
env: options.context.env,
|
|
3009
|
+
marketplaceName: input.marketplaceName,
|
|
3010
|
+
marketplaceRoot: input.marketplaceRoot,
|
|
3011
|
+
})]
|
|
3012
|
+
: [];
|
|
3013
|
+
if (nativeRequired
|
|
3014
|
+
&& input.command !== 'cleanup'
|
|
3015
|
+
&& input.command !== 'uninstall')
|
|
3016
|
+
desiredComponents.push(...nativePlugin, ...marketplace, ...nativeVersion);
|
|
3017
|
+
const allComponents = dedupeComponents([
|
|
3018
|
+
...desiredComponents,
|
|
3019
|
+
...optionalComponents,
|
|
3020
|
+
...(input.platform === 'codex' ? sharedComponents : []),
|
|
3021
|
+
...scannedText,
|
|
3022
|
+
...scannedSkills,
|
|
3023
|
+
...historicalComponents,
|
|
3024
|
+
...(input.platform === 'claude' ? sharedComponents : []),
|
|
3025
|
+
...nativePlugin,
|
|
3026
|
+
...marketplace,
|
|
3027
|
+
...nativeVersion,
|
|
3028
|
+
]);
|
|
3029
|
+
bindNamespacedOwnerIdentity(allComponents, input);
|
|
3030
|
+
const desiredKeys = new Set(desiredComponents.map(componentIdentityKey));
|
|
3031
|
+
const target = {
|
|
3032
|
+
plugin: input.plugin,
|
|
3033
|
+
identity: {
|
|
3034
|
+
...(input.sourceId ? {
|
|
3035
|
+
sourceId: input.sourceId,
|
|
3036
|
+
sourceIdentity: input.sourceIdentity,
|
|
3037
|
+
pluginId: input.pluginId,
|
|
3038
|
+
marketplaceName: input.marketplaceName,
|
|
3039
|
+
marketplaceRoot: input.marketplaceRoot,
|
|
3040
|
+
} : {}),
|
|
3041
|
+
pluginName: input.plugin.manifest.name,
|
|
3042
|
+
pluginVersion: input.plugin.manifest.version,
|
|
3043
|
+
platform: input.platform,
|
|
3044
|
+
scope: input.scope,
|
|
3045
|
+
cwd: input.cwd,
|
|
3046
|
+
},
|
|
3047
|
+
ownedComponents: allComponents,
|
|
3048
|
+
desiredOwnedComponents: allComponents.filter((component) => desiredKeys.has(componentIdentityKey(component))),
|
|
3049
|
+
optionalOwnedComponents: allComponents.filter((component) => optionalComponentKeys.has(componentIdentityKey(component))),
|
|
3050
|
+
requiresNative: nativeRequired
|
|
3051
|
+
&& input.command !== 'cleanup'
|
|
3052
|
+
&& input.command !== 'uninstall',
|
|
3053
|
+
artifacts: writes.map((write) => write.artifact),
|
|
3054
|
+
lifecycleSource: input.plugin,
|
|
3055
|
+
};
|
|
3056
|
+
plans.set(target, {
|
|
3057
|
+
catalog: input.catalog,
|
|
3058
|
+
event,
|
|
3059
|
+
writes,
|
|
3060
|
+
});
|
|
3061
|
+
planTargets.get(plans).push(target);
|
|
3062
|
+
return target;
|
|
3063
|
+
},
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
export async function inspectRebuildTarget(options) {
|
|
3067
|
+
const integration = createRebuildIntegration({ context: options.context });
|
|
3068
|
+
const target = await integration.buildTarget({
|
|
3069
|
+
...(options.sourceId ? {
|
|
3070
|
+
sourceId: options.sourceId,
|
|
3071
|
+
sourceIdentity: options.sourceIdentity,
|
|
3072
|
+
pluginId: options.pluginId,
|
|
3073
|
+
marketplaceName: options.marketplaceName,
|
|
3074
|
+
marketplaceRoot: options.marketplaceRoot,
|
|
3075
|
+
} : {}),
|
|
3076
|
+
plugin: options.plugin,
|
|
3077
|
+
platform: options.platform,
|
|
3078
|
+
scope: options.scope,
|
|
3079
|
+
cwd: options.cwd,
|
|
3080
|
+
command: 'install',
|
|
3081
|
+
catalog: options.catalog,
|
|
3082
|
+
includeHistoricalLifecycleArtifacts: false,
|
|
3083
|
+
readOnlyInspectionPlan: true,
|
|
3084
|
+
});
|
|
3085
|
+
const inspection = await inspectOwnedComponents({
|
|
3086
|
+
components: target.ownedComponents,
|
|
3087
|
+
nativeProbe: integration.driver.probeNativeHealth ?? integration.driver.probeNative,
|
|
3088
|
+
verifyHealth: true,
|
|
3089
|
+
});
|
|
3090
|
+
return { target, inspection };
|
|
3091
|
+
}
|
|
3092
|
+
async function discoverOwnerNamesInTextRoot(root, platform, scope) {
|
|
3093
|
+
if (!await fs.pathExists(root))
|
|
3094
|
+
return [];
|
|
3095
|
+
const names = new Set();
|
|
3096
|
+
const pending = [root];
|
|
3097
|
+
while (pending.length > 0) {
|
|
3098
|
+
const current = pending.pop();
|
|
3099
|
+
for (const entry of await fs.readdir(current, { withFileTypes: true })) {
|
|
3100
|
+
const entryPath = path.join(current, entry.name);
|
|
3101
|
+
if (entry.isSymbolicLink())
|
|
3102
|
+
continue;
|
|
3103
|
+
if (entry.isDirectory()) {
|
|
3104
|
+
pending.push(entryPath);
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
if (!entry.isFile())
|
|
3108
|
+
continue;
|
|
3109
|
+
const raw = await fs.readFile(entryPath, 'utf8').catch(() => '');
|
|
3110
|
+
if (!raw.includes(OWNED_TEXT_HEADER_PREFIX))
|
|
3111
|
+
continue;
|
|
3112
|
+
const parsed = (() => {
|
|
3113
|
+
try {
|
|
3114
|
+
return parseOwnedTextHeader(raw);
|
|
3115
|
+
}
|
|
3116
|
+
catch {
|
|
3117
|
+
return null;
|
|
3118
|
+
}
|
|
3119
|
+
})();
|
|
3120
|
+
if (parsed?.identity.platform === platform && parsed.identity.scope === scope) {
|
|
3121
|
+
names.add(parsed.identity.plugin);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
return [...names];
|
|
3126
|
+
}
|
|
3127
|
+
async function discoverOwnerNamesInSkillRoot(root, platform, scope) {
|
|
3128
|
+
if (!await fs.pathExists(root))
|
|
3129
|
+
return [];
|
|
3130
|
+
const names = new Set();
|
|
3131
|
+
for (const entry of await fs.readdir(root, { withFileTypes: true })) {
|
|
3132
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
3133
|
+
continue;
|
|
3134
|
+
const marker = await readSkillOwnerMarker({
|
|
3135
|
+
skillRoot: path.join(root, entry.name),
|
|
3136
|
+
allowedRoot: root,
|
|
3137
|
+
}).catch(() => null);
|
|
3138
|
+
if (marker?.platform === platform && marker.scope === scope)
|
|
3139
|
+
names.add(marker.plugin);
|
|
3140
|
+
}
|
|
3141
|
+
return [...names];
|
|
3142
|
+
}
|
|
3143
|
+
export async function discoverRebuildTargetNames(options) {
|
|
3144
|
+
if (options.scope === 'project') {
|
|
3145
|
+
const canonicalRepoRoot = await assertCanonicalProjectManagedRoots({
|
|
3146
|
+
cwd: options.cwd,
|
|
3147
|
+
rootNames: options.platform === 'codex' ? ['.agents', '.codex'] : ['.claude'],
|
|
3148
|
+
managedPaths: options.platform === 'codex'
|
|
3149
|
+
? ['.codex/config.toml', '.codex/hooks.json']
|
|
3150
|
+
: ['.claude/settings.json'],
|
|
3151
|
+
});
|
|
3152
|
+
options = { ...options, cwd: canonicalRepoRoot };
|
|
3153
|
+
}
|
|
3154
|
+
const names = new Set();
|
|
3155
|
+
const paths = options.platform === 'codex'
|
|
3156
|
+
? resolveCodexPaths(options.cwd, options.scope, options.env)
|
|
3157
|
+
: resolveClaudePaths(options.cwd, options.scope, options.env);
|
|
3158
|
+
const textRoots = [path.join(paths.configDir, 'rules'), path.join(paths.configDir, 'agents')];
|
|
3159
|
+
for (const name of (await Promise.all(textRoots.map((root) => discoverOwnerNamesInTextRoot(root, options.platform, options.scope)))).flat())
|
|
3160
|
+
names.add(name);
|
|
3161
|
+
const skillRoots = options.platform === 'codex'
|
|
3162
|
+
? options.scope === 'project'
|
|
3163
|
+
? [path.join(await resolveRepoRoot(options.cwd), '.agents', 'skills')]
|
|
3164
|
+
: [path.join(resolveAwHome(options.env), 'plugins')]
|
|
3165
|
+
: [path.join(paths.configDir, 'skills')];
|
|
3166
|
+
if (options.platform === 'codex' && options.scope === 'user') {
|
|
3167
|
+
const pluginsRoot = skillRoots[0];
|
|
3168
|
+
if (await fs.pathExists(pluginsRoot)) {
|
|
3169
|
+
for (const pluginEntry of await fs.readdir(pluginsRoot, { withFileTypes: true })) {
|
|
3170
|
+
if (!pluginEntry.isDirectory())
|
|
3171
|
+
continue;
|
|
3172
|
+
const pluginRoot = path.join(pluginsRoot, pluginEntry.name);
|
|
3173
|
+
const platformManifest = await fs.readJson(path.join(pluginRoot, '.codex-plugin', 'plugin.json')).catch(() => null);
|
|
3174
|
+
const relativeSkillsRoot = typeof platformManifest?.skills === 'string'
|
|
3175
|
+
? path.posix.normalize(platformManifest.skills.replace(/^\.\//u, ''))
|
|
3176
|
+
: 'codex/skills';
|
|
3177
|
+
if (relativeSkillsRoot === '..'
|
|
3178
|
+
|| relativeSkillsRoot.startsWith('../')
|
|
3179
|
+
|| path.isAbsolute(relativeSkillsRoot))
|
|
3180
|
+
continue;
|
|
3181
|
+
for (const name of await discoverOwnerNamesInSkillRoot(path.join(pluginRoot, ...relativeSkillsRoot.split('/')), options.platform, options.scope))
|
|
3182
|
+
names.add(name);
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
else {
|
|
3187
|
+
for (const root of skillRoots) {
|
|
3188
|
+
for (const name of await discoverOwnerNamesInSkillRoot(root, options.platform, options.scope)) {
|
|
3189
|
+
names.add(name);
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
if (options.platform === 'codex') {
|
|
3194
|
+
const config = await readCodexConfigFile(resolveCodexPaths(options.cwd, options.scope, options.env).configTomlPath);
|
|
3195
|
+
for (const name of Object.keys(config.snapshot.developerInstructions.managedBlocks)) {
|
|
3196
|
+
names.add(sharedOwnerPluginName(name));
|
|
3197
|
+
}
|
|
3198
|
+
for (const server of Object.values(config.snapshot.mcpServers)) {
|
|
3199
|
+
const envValue = server.value?.env;
|
|
3200
|
+
const owner = envValue
|
|
3201
|
+
&& typeof envValue === 'object'
|
|
3202
|
+
&& !Array.isArray(envValue)
|
|
3203
|
+
&& typeof envValue[MCP_OWNER_ENV] === 'string'
|
|
3204
|
+
? envValue[MCP_OWNER_ENV]
|
|
3205
|
+
: null;
|
|
3206
|
+
if (owner)
|
|
3207
|
+
names.add(sharedOwnerPluginName(owner));
|
|
3208
|
+
}
|
|
3209
|
+
if (options.scope === 'project') {
|
|
3210
|
+
const hooksPath = resolveCodexPaths(options.cwd, options.scope, options.env).hooksPath;
|
|
3211
|
+
const hooks = await readCodexHooks(hooksPath).catch(() => null);
|
|
3212
|
+
for (const identity of hooks?.managedIdentities ?? []) {
|
|
3213
|
+
const owner = readOwnedHookIdentity(identity, options.scope);
|
|
3214
|
+
if (owner)
|
|
3215
|
+
names.add(owner.plugin);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
else {
|
|
3219
|
+
for (const name of await discoverCodexNativeResidueNames({
|
|
3220
|
+
cwd: options.cwd,
|
|
3221
|
+
env: options.env,
|
|
3222
|
+
marketplaceName: options.marketplaceName,
|
|
3223
|
+
marketplaceRoot: options.marketplaceRoot,
|
|
3224
|
+
}))
|
|
3225
|
+
names.add(name);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
else {
|
|
3229
|
+
const settings = await readClaudeSettings(resolveClaudePaths(options.cwd, options.scope, options.env).settingsPath);
|
|
3230
|
+
for (const name of Object.keys(settings.awGenerated?.permissions ?? {})) {
|
|
3231
|
+
names.add(sharedOwnerPluginName(name));
|
|
3232
|
+
}
|
|
3233
|
+
const installed = await readJsonObjectIfExists(resolveClaudePaths(options.cwd, options.scope, options.env).installedPluginsPath, 'Claude installed_plugins');
|
|
3234
|
+
const installedPlugins = installed.plugins;
|
|
3235
|
+
if (installedPlugins !== undefined && (!installedPlugins || typeof installedPlugins !== 'object' || Array.isArray(installedPlugins)))
|
|
3236
|
+
throw new Error('Claude installed_plugins.plugins 必须是 object。');
|
|
3237
|
+
const candidates = new Set();
|
|
3238
|
+
for (const [pluginKey, raw] of Object.entries(installedPlugins ?? {})) {
|
|
3239
|
+
for (const entry of Array.isArray(raw) ? raw : [raw]) {
|
|
3240
|
+
const record = entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
3241
|
+
? entry
|
|
3242
|
+
: null;
|
|
3243
|
+
const name = typeof record?.name === 'string'
|
|
3244
|
+
? record.name
|
|
3245
|
+
: pluginKey.includes('@') ? pluginKey.slice(0, pluginKey.indexOf('@')) : pluginKey;
|
|
3246
|
+
if (/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(name))
|
|
3247
|
+
candidates.add(name);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
const rawSettings = await readJsonObjectIfExists(resolveClaudePaths(options.cwd, options.scope, options.env).settingsPath, 'Claude settings');
|
|
3251
|
+
const settingsPlugins = rawSettings.plugins;
|
|
3252
|
+
const settingsInstalled = settingsPlugins
|
|
3253
|
+
&& typeof settingsPlugins === 'object'
|
|
3254
|
+
&& !Array.isArray(settingsPlugins)
|
|
3255
|
+
? settingsPlugins.installed
|
|
3256
|
+
: undefined;
|
|
3257
|
+
if (settingsInstalled !== undefined && (!settingsInstalled || typeof settingsInstalled !== 'object' || Array.isArray(settingsInstalled)))
|
|
3258
|
+
throw new Error('Claude settings.plugins.installed 必须是 object。');
|
|
3259
|
+
for (const name of Object.keys(settingsInstalled ?? {})) {
|
|
3260
|
+
if (/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(name))
|
|
3261
|
+
candidates.add(name);
|
|
3262
|
+
}
|
|
3263
|
+
const enabledPlugins = rawSettings.enabledPlugins;
|
|
3264
|
+
if (enabledPlugins !== undefined && (!enabledPlugins || typeof enabledPlugins !== 'object' || Array.isArray(enabledPlugins)))
|
|
3265
|
+
throw new Error('Claude settings.enabledPlugins 必须是 object。');
|
|
3266
|
+
for (const pluginId of Object.keys(enabledPlugins ?? {})) {
|
|
3267
|
+
const suffix = `@${options.marketplaceName ?? MARKETPLACE_NAME}`;
|
|
3268
|
+
if (!pluginId.endsWith(suffix))
|
|
3269
|
+
continue;
|
|
3270
|
+
const name = pluginId.slice(0, -suffix.length);
|
|
3271
|
+
if (/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(name))
|
|
3272
|
+
candidates.add(name);
|
|
3273
|
+
}
|
|
3274
|
+
for (const name of candidates) {
|
|
3275
|
+
const state = await readClaudeNativeState({
|
|
3276
|
+
pluginName: name,
|
|
3277
|
+
scope: options.scope,
|
|
3278
|
+
cwd: options.cwd,
|
|
3279
|
+
env: options.env,
|
|
3280
|
+
marketplaceName: options.marketplaceName,
|
|
3281
|
+
marketplaceRoot: options.marketplaceRoot,
|
|
3282
|
+
});
|
|
3283
|
+
if (state.pluginState === 'present')
|
|
3284
|
+
names.add(name);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
return [...names].sort();
|
|
3288
|
+
}
|
|
3289
|
+
//# sourceMappingURL=rebuild-integration.js.map
|