@orchid-labs/pluxx 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +7 -1
  2. package/bin/pluxx.js +1 -6
  3. package/dist/agent-translation-registry.d.ts +9 -1
  4. package/dist/agent-translation-registry.d.ts.map +1 -1
  5. package/dist/bundle-check.d.ts +21 -0
  6. package/dist/bundle-check.d.ts.map +1 -0
  7. package/dist/claude-hook-probe.d.ts.map +1 -1
  8. package/dist/cli/agent.d.ts.map +1 -1
  9. package/dist/cli/codex-apply.d.ts +39 -0
  10. package/dist/cli/codex-apply.d.ts.map +1 -0
  11. package/dist/cli/doctor.d.ts.map +1 -1
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +32414 -7589
  14. package/dist/cli/install.d.ts.map +1 -1
  15. package/dist/cli/primitive-summary.d.ts.map +1 -1
  16. package/dist/cli/publish.d.ts.map +1 -1
  17. package/dist/cli/sync-from-mcp.d.ts +1 -1
  18. package/dist/cli/sync-from-mcp.d.ts.map +1 -1
  19. package/dist/cli/verify-install.d.ts.map +1 -1
  20. package/dist/codex-hooks-feature.d.ts +1 -1
  21. package/dist/codex-hooks-feature.d.ts.map +1 -1
  22. package/dist/codex-mcp-probe.d.ts.map +1 -1
  23. package/dist/command-translation-registry.d.ts +11 -3
  24. package/dist/command-translation-registry.d.ts.map +1 -1
  25. package/dist/compatibility/core-four-primitives.d.ts +5 -0
  26. package/dist/compatibility/core-four-primitives.d.ts.map +1 -0
  27. package/dist/compatibility/matrix.d.ts +1 -0
  28. package/dist/compatibility/matrix.d.ts.map +1 -1
  29. package/dist/distribution-lifecycle.d.ts +18 -0
  30. package/dist/distribution-lifecycle.d.ts.map +1 -1
  31. package/dist/generators/index.d.ts.map +1 -1
  32. package/dist/generators/opencode/index.d.ts.map +1 -1
  33. package/dist/hook-command-env.d.ts +1 -0
  34. package/dist/hook-command-env.d.ts.map +1 -1
  35. package/dist/hook-events.d.ts +1 -0
  36. package/dist/hook-events.d.ts.map +1 -1
  37. package/dist/host-detection.d.ts +38 -0
  38. package/dist/host-detection.d.ts.map +1 -0
  39. package/dist/index.d.ts +3 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1142 -208
  42. package/dist/install-reference.d.ts +34 -0
  43. package/dist/install-reference.d.ts.map +1 -0
  44. package/dist/readiness.d.ts.map +1 -1
  45. package/dist/schema.d.ts +36 -36
  46. package/dist/user-config.d.ts +11 -0
  47. package/dist/user-config.d.ts.map +1 -1
  48. package/dist/validation/platform-rules.d.ts +1 -0
  49. package/dist/validation/platform-rules.d.ts.map +1 -1
  50. package/package.json +5 -2
package/dist/index.js CHANGED
@@ -7841,6 +7841,75 @@ function definePlugin(config) {
7841
7841
  return PluginConfigSchema.parse(config);
7842
7842
  }
7843
7843
 
7844
+ // src/agent-translation-registry.ts
7845
+ var CODEX_AGENT_CAPABILITY_NOTE = 'Codex custom agents and subagents are real native surfaces, but they are not packaged the same way as Claude or Cursor plugin agents. Local May 13, 2026 headless probes prove explicit invocation, built-in-name override, project-local precedence, and discovered `.agents/skills` inheritance. The same maintained headless suite also showed two config-depth caveats: a parent `[[skills.config]] enabled = false` entry did not disable a discovered project skill, and an agent-local `[[skills.config]]` entry did not preload an undiscovered `skills/` path. The maintained `bun scripts/probe-codex-mcp-runtime.ts --json` headless probe was rerun on June 24, 2026: default project-scoped and user-scoped root MCP both emit a real `mcp_tool_call` item but fail with `user cancelled MCP tool call` before server-side `tools/call`; explicit `[mcp_servers.<id>.tools.<tool>] approval_mode = "approve"` unlocks project-scoped and user-scoped root MCP; custom agents that inherit those approved root MCP servers also reach `tools/call` and return `MCP_PROOF_MARKER_ALLOWED`. The June 24 run also shows that `mcp_servers = {}` in the child agent does not opt out of approved inherited root MCP for either project or user scope, while agent-local inline MCP and agent-local inline approval did not activate. Current custom-agent MCP success now surfaces a parent `mcp_tool_call` item in the `codex exec --json` stream, but `codex mcp list` still sees user-scoped servers and not project-scoped servers. The maintained `bun scripts/probe-codex-agents-interactive-runtime.ts --json` trusted interactive probe also showed the same `sandbox_mode = "read-only"` child agent still wrote to the workspace there too, so these fields are not yet uniformly trustworthy runtime boundaries.';
7846
+ var AGENT_PRIMITIVE_CAPABILITIES = {
7847
+ "claude-code": {
7848
+ mode: "preserve",
7849
+ nativeSurfaces: ["agents/*.md"],
7850
+ notes: "Claude plugin agents are a first-class native surface with rich frontmatter."
7851
+ },
7852
+ cursor: {
7853
+ mode: "translate",
7854
+ nativeSurfaces: ["agents/", ".cursor/agents/", "~/.cursor/agents/"],
7855
+ notes: "Cursor specialization and tool access often live more naturally in subagents than in skills."
7856
+ },
7857
+ codex: {
7858
+ mode: "translate",
7859
+ nativeSurfaces: [".codex/agents/*.toml", "~/.codex/agents/*.toml", "subagent workflows"],
7860
+ notes: CODEX_AGENT_CAPABILITY_NOTE
7861
+ },
7862
+ opencode: {
7863
+ mode: "preserve",
7864
+ nativeSurfaces: ["agents/*.md", "config agent definitions"],
7865
+ notes: "OpenCode agents are first-class native surfaces. Prefer permission-first agent config for new builds; legacy tools remains compatibility input, not the preferred emitted shape."
7866
+ }
7867
+ };
7868
+ function getAgentPrimitiveCapability(platform) {
7869
+ return AGENT_PRIMITIVE_CAPABILITIES[platform];
7870
+ }
7871
+
7872
+ // src/command-translation-registry.ts
7873
+ var CODEX_COMMAND_GUIDANCE_SURFACES = ["skills/", "AGENTS.md", ".codex/commands.generated.json"];
7874
+ var CODEX_COMMAND_GUIDANCE_NOTE = "Current Codex docs do not document plugin-packaged slash-command parity, so Pluxx keeps canonical command intent through routing guidance plus a generated companion mirror.";
7875
+ var COMMAND_PRIMITIVE_CAPABILITIES = {
7876
+ "claude-code": {
7877
+ mode: "preserve",
7878
+ nativeSurfaces: ["commands/*.md", "skills/<skill>/SKILL.md"],
7879
+ notes: "Claude still supports command files, but the product is increasingly converging command workflows into skills."
7880
+ },
7881
+ cursor: {
7882
+ mode: "preserve",
7883
+ nativeSurfaces: ["commands/*", "slash commands"]
7884
+ },
7885
+ codex: {
7886
+ mode: "degrade",
7887
+ nativeSurfaces: [...CODEX_COMMAND_GUIDANCE_SURFACES],
7888
+ notes: CODEX_COMMAND_GUIDANCE_NOTE
7889
+ },
7890
+ opencode: {
7891
+ mode: "preserve",
7892
+ nativeSurfaces: ["commands/*.md", "config command definitions"]
7893
+ }
7894
+ };
7895
+ function getCommandPrimitiveCapability(platform) {
7896
+ return COMMAND_PRIMITIVE_CAPABILITIES[platform];
7897
+ }
7898
+
7899
+ // src/hook-events.ts
7900
+ var CODEX_SUPPORTED_HOOK_EVENTS = [
7901
+ "SessionStart",
7902
+ "SubagentStart",
7903
+ "PreToolUse",
7904
+ "PermissionRequest",
7905
+ "PostToolUse",
7906
+ "PreCompact",
7907
+ "PostCompact",
7908
+ "UserPromptSubmit",
7909
+ "SubagentStop",
7910
+ "Stop"
7911
+ ];
7912
+
7844
7913
  // src/runtime-readiness-registry.ts
7845
7914
  function getEnabledRuntimeReadinessBindings(capability, plan) {
7846
7915
  return capability.bindings.filter((binding) => {
@@ -7855,7 +7924,7 @@ function getEnabledRuntimeReadinessBindings(capability, plan) {
7855
7924
  });
7856
7925
  }
7857
7926
  var NAMED_PROMPT_TARGET_NOTE = "Named `skills` / `commands` readiness targets currently translate through prompt-entry gating with best-effort matching because the core four do not share one exact per-skill or per-command runtime interception surface.";
7858
- var CODEX_EXTERNAL_NOTE = "Codex readiness now bundles translated hooks in the plugin, and Pluxx still emits `.codex/readiness.generated.json` plus `.codex/hooks.generated.json` as debugging companions because plugin-bundled hook activation still depends on `[features].plugin_hooks = true`. The general `hooks = true` flag covers non-plugin hook config and defaults on, while `codex_hooks` is deprecated and should not be treated as a plugin-bundled hook fallback.";
7927
+ var CODEX_EXTERNAL_NOTE = "Codex readiness now bundles translated hooks in the plugin, and Pluxx still emits `.codex/readiness.generated.json` plus `.codex/hooks.generated.json` as debugging companions because hook activation still depends on `[features].hooks = true`, enabled plugin state, review, trust, and runtime support. `codex_hooks` is deprecated and should not be treated as the current hook feature key.";
7859
7928
  function getRuntimeReadinessNamedPromptTargetNote() {
7860
7929
  return NAMED_PROMPT_TARGET_NOTE;
7861
7930
  }
@@ -7975,6 +8044,7 @@ function getRuntimeReadinessCapability(platform, pluginRootVar = "PLUGIN_ROOT")
7975
8044
  }
7976
8045
 
7977
8046
  // src/validation/platform-rules.ts
8047
+ var CORE_FOUR_PLATFORMS = ["claude-code", "cursor", "codex", "opencode"];
7978
8048
  var STANDARD_SKILL_FRONTMATTER = [
7979
8049
  "name",
7980
8050
  "description",
@@ -8103,7 +8173,7 @@ var PLATFORM_LIMIT_POLICIES = {
8103
8173
  },
8104
8174
  hooksFeatureFlag: {
8105
8175
  kind: "hard",
8106
- notes: "Hook support depends on the Codex hooks feature flag/runtime support."
8176
+ notes: "Codex hook support depends on the canonical hooks feature flag plus enabled-plugin, trust/review, and runtime support."
8107
8177
  }
8108
8178
  },
8109
8179
  "cursor": {
@@ -8303,8 +8373,8 @@ var PLATFORM_VALIDATION_RULES = {
8303
8373
  hooks: {
8304
8374
  supported: true,
8305
8375
  files: ["hooks/hooks.json", ".codex/hooks.json", "~/.codex/hooks.json"],
8306
- eventNames: ["SessionStart", "PreToolUse", "PermissionRequest", "PostToolUse", "UserPromptSubmit", "Stop"],
8307
- notes: "Codex documents both project or user hook config and plugin-bundled hooks. Plugin-bundled hooks live at `hooks/hooks.json` in the plugin and require `[features].plugin_hooks = true`; the general `[features].hooks = true` flag covers non-plugin hook config and defaults on. `codex_hooks` is deprecated and should not be treated as a plugin-bundled hook fallback."
8376
+ eventNames: [...CODEX_SUPPORTED_HOOK_EVENTS],
8377
+ notes: "Codex documents both project/user hook config and plugin-bundled hooks. Plugin-bundled hooks live at `hooks/hooks.json` in the plugin and require the canonical `[features].hooks = true` flag, enabled plugin state, user review, trust, and runtime support. `codex_hooks` is deprecated and should not be treated as the current hook feature key."
8308
8378
  },
8309
8379
  instructions: {
8310
8380
  files: ["AGENTS.md", "AGENTS.override.md"],
@@ -8648,14 +8718,10 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
8648
8718
  nativeSurfaces: ["skills/<skill>/SKILL.md"]
8649
8719
  },
8650
8720
  commands: {
8651
- mode: "preserve",
8652
- nativeSurfaces: ["commands/*.md", "skills/<skill>/SKILL.md"],
8653
- notes: "Claude still supports command files, but the product is increasingly converging command workflows into skills."
8721
+ ...getCommandPrimitiveCapability("claude-code")
8654
8722
  },
8655
8723
  agents: {
8656
- mode: "preserve",
8657
- nativeSurfaces: ["agents/*.md"],
8658
- notes: "Claude plugin agents are a first-class native surface with rich frontmatter."
8724
+ ...getAgentPrimitiveCapability("claude-code")
8659
8725
  },
8660
8726
  hooks: {
8661
8727
  mode: "preserve",
@@ -8691,13 +8757,10 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
8691
8757
  notes: "Cursor skills preserve workflow meaning but document a narrower frontmatter set than Claude."
8692
8758
  },
8693
8759
  commands: {
8694
- mode: "preserve",
8695
- nativeSurfaces: ["commands/*", "slash commands"]
8760
+ ...getCommandPrimitiveCapability("cursor")
8696
8761
  },
8697
8762
  agents: {
8698
- mode: "translate",
8699
- nativeSurfaces: ["agents/", ".cursor/agents/", "~/.cursor/agents/"],
8700
- notes: "Cursor specialization and tool access often live more naturally in subagents than in skills."
8763
+ ...getAgentPrimitiveCapability("cursor")
8701
8764
  },
8702
8765
  hooks: {
8703
8766
  mode: "preserve",
@@ -8731,19 +8794,15 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
8731
8794
  nativeSurfaces: ["skills/<skill>/SKILL.md"]
8732
8795
  },
8733
8796
  commands: {
8734
- mode: "degrade",
8735
- nativeSurfaces: ["skills/", "AGENTS.md", ".codex/commands.generated.json"],
8736
- notes: "Current Codex docs do not document plugin-packaged slash-command parity, so Pluxx keeps canonical command intent through routing guidance plus a generated companion mirror."
8797
+ ...getCommandPrimitiveCapability("codex")
8737
8798
  },
8738
8799
  agents: {
8739
- mode: "translate",
8740
- nativeSurfaces: [".codex/agents/*.toml", "~/.codex/agents/*.toml", "subagent workflows"],
8741
- notes: 'Codex custom agents and subagents are real native surfaces, but they are not packaged the same way as Claude or Cursor plugin agents. Local May 13, 2026 headless probes now prove explicit invocation, built-in-name override, project-local precedence, and discovered `.agents/skills` inheritance. The same maintained headless suite also showed two config-depth caveats: a parent `[[skills.config]] enabled = false` entry did not disable a discovered project skill, and an agent-local `[[skills.config]]` entry did not preload an undiscovered `skills/` path. The maintained `bun scripts/probe-codex-mcp-runtime.ts --json` headless probe now also shows a more precise MCP approval split: default project-scoped and user-scoped root MCP both emit a real `mcp_tool_call` item but fail it with `user cancelled MCP tool call` before any server-side `tools/call`, while the default inline-agent path reaches startup plus `tools/list` and then falls back to `MCP_PROOF_MARKER_MISSING`. The same maintained suite now also proves five approved allow-paths: explicit `[mcp_servers.<id>.tools.<tool>] approval_mode = "approve"` works for project-scoped root MCP, user-scoped root MCP, agent-local inline `mcp_servers`, and custom agents that inherit an approved project-scoped or user-scoped root MCP server. All three approved custom-agent MCP paths still avoid a root `mcp_tool_call` item in the parent `codex exec --json` stream and instead surface child `agents_states` moving through `pending_init` to `completed`; project-scoped servers still do not appear in `codex mcp list`, and user-scoped servers do appear there. The maintained `bun scripts/probe-codex-agents-interactive-runtime.ts --json` trusted interactive probe also showed the same `sandbox_mode = "read-only"` child agent still wrote to the workspace there too, so these fields are not yet uniformly trustworthy runtime boundaries.'
8800
+ ...getAgentPrimitiveCapability("codex")
8742
8801
  },
8743
8802
  hooks: {
8744
8803
  mode: "translate",
8745
8804
  nativeSurfaces: ["hooks/hooks.json", ".codex/hooks.json", "~/.codex/hooks.json"],
8746
- notes: "Hooks are native. Pluxx bundles translated Codex hooks in the plugin with the documented `[features].plugin_hooks = true` plugin gate, and still tracks the broader project/user hook config paths where `[features].hooks` is the general flag and `codex_hooks` is deprecated."
8805
+ notes: "Hooks are native. Pluxx bundles translated Codex hooks in the plugin with the documented `[features].hooks = true` feature key, and still tracks broader project/user hook config paths where `codex_hooks` is deprecated. Plugin-bundled execution still also depends on enabled plugin state, review, trust, and runtime support."
8747
8806
  },
8748
8807
  permissions: {
8749
8808
  mode: "translate",
@@ -8753,7 +8812,7 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
8753
8812
  runtime: {
8754
8813
  mode: "preserve",
8755
8814
  nativeSurfaces: [".mcp.json", ".app.json", ".codex/config.toml", "scripts/", "assets/"],
8756
- notes: `Bundle-local MCP config exists, but active MCP state also lives in config.toml. Local May 13, 2026 headless Codex MCP probes reached startup plus \`tools/list\` across project-scoped, user-scoped, and inline-agent config; default root MCP emitted a real \`mcp_tool_call\` item but failed it with \`user cancelled MCP tool call\` before any server-side \`tools/call\`, while the default inline-agent path fell back to \`MCP_PROOF_MARKER_MISSING\` after startup plus \`tools/list\`. The same maintained suite now proves five concrete approval paths: project-scoped root MCP, user-scoped root MCP, agent-local inline \`mcp_servers\`, a custom agent inheriting an approved project-scoped root MCP server, and a custom agent inheriting an approved user-scoped root MCP server all reach real server-side \`tools/call\` once explicit \`[mcp_servers.<id>.tools.<tool>] approval_mode = "approve"\` is present in the relevant layer. All three approved custom-agent MCP paths still avoid a root \`mcp_tool_call\` item in the parent \`codex exec --json\` stream and instead surface child \`agents_states\` moving through \`pending_init\` to \`completed\`; project-scoped servers still do not appear in \`codex mcp list\`, and user-scoped servers do appear there. ${getRuntimeReadinessExternalConfigNote()}`
8815
+ notes: `Bundle-local MCP config exists, but active MCP state also lives in config.toml. Local June 24, 2026 headless Codex MCP probes reached startup plus \`tools/list\` for project-scoped and user-scoped root config. Default root MCP emitted a real \`mcp_tool_call\` item but failed with \`user cancelled MCP tool call\` before server-side \`tools/call\`. Explicit per-tool \`approval_mode = "approve"\` now proves four concrete root/inheritance paths: project-scoped root MCP, user-scoped root MCP, a custom agent inheriting an approved project-scoped root MCP server, and a custom agent inheriting an approved user-scoped root MCP server. The same run also proves that an explicit child-agent \`mcp_servers = {}\` does not opt out of those approved inherited root MCP servers. Agent-local inline MCP and agent-local inline approval did not activate in the June 24 run, so least-privilege delegated MCP still belongs in the gap column. Project-scoped servers still do not appear in \`codex mcp list\`, while user-scoped servers do. ${getRuntimeReadinessExternalConfigNote()}`
8757
8816
  },
8758
8817
  distribution: {
8759
8818
  mode: "preserve",
@@ -8775,13 +8834,10 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
8775
8834
  nativeSurfaces: ["skills/<skill>/SKILL.md"]
8776
8835
  },
8777
8836
  commands: {
8778
- mode: "preserve",
8779
- nativeSurfaces: ["commands/*.md", "config command definitions"]
8837
+ ...getCommandPrimitiveCapability("opencode")
8780
8838
  },
8781
8839
  agents: {
8782
- mode: "preserve",
8783
- nativeSurfaces: ["agents/*.md", "config agent definitions"],
8784
- notes: "OpenCode agents are first-class native surfaces. Prefer permission-first agent config for new builds; legacy tools remains compatibility input, not the preferred emitted shape."
8840
+ ...getAgentPrimitiveCapability("opencode")
8785
8841
  },
8786
8842
  hooks: {
8787
8843
  mode: "translate",
@@ -9123,20 +9179,149 @@ function collectUserConfigEntries(config, platforms = config.targets) {
9123
9179
  }
9124
9180
 
9125
9181
  // src/distribution-lifecycle.ts
9182
+ var CORE_FOUR_INSTALL_DISCOVERY_CAPABILITIES = [
9183
+ {
9184
+ platform: "claude-code",
9185
+ label: "Claude Code",
9186
+ installMethod: "Native Claude plugin install from a generated local marketplace, or the generated release installer.",
9187
+ localInstallPath: "~/.claude/plugins/cache/pluxx-local-<plugin>/<plugin>/<version> after native install; legacy direct installs may still appear at ~/.claude/plugins/<plugin>.",
9188
+ reloadBehavior: "Run /reload-plugins in the active Claude window.",
9189
+ cacheSemantics: "Claude copies the selected marketplace plugin into a versioned plugin cache. Pluxx verifies that cache path rather than assuming the source bundle is live.",
9190
+ discoverySurface: "Claude plugin marketplace/listing commands and the active Claude plugin list after reload.",
9191
+ brandListingSupport: "No shared manifest-backed brand fields from Pluxx brand today; instructions, skills, commands, and assets still ship inside the bundle.",
9192
+ installFollowupNote: "Claude Code note: if Claude is already open, run /reload-plugins to pick up the new install.",
9193
+ publishReloadInstruction: "If Claude is already open, run /reload-plugins."
9194
+ },
9195
+ {
9196
+ platform: "cursor",
9197
+ label: "Cursor",
9198
+ installMethod: "Local plugin install or generated release installer that replaces the local bundle.",
9199
+ localInstallPath: "~/.cursor/plugins/local/<plugin>",
9200
+ reloadBehavior: "Use Developer: Reload Window or restart Cursor.",
9201
+ cacheSemantics: "Pluxx installs the local bundle path directly; no separate Pluxx-managed active cache is modeled.",
9202
+ discoverySurface: "Cursor local plugin directory and host plugin UI after reload or restart.",
9203
+ brandListingSupport: "Narrow shared-brand translation: homepage and logo can be emitted; richer listing copy is not a shared Cursor surface today.",
9204
+ installFollowupNote: "Cursor note: if Cursor is already open, use Developer: Reload Window or restart Cursor to pick up the new install.",
9205
+ publishReloadInstruction: "If Cursor is already open, use Developer: Reload Window or restart Cursor so the plugin is picked up."
9206
+ },
9207
+ {
9208
+ platform: "codex",
9209
+ label: "Codex",
9210
+ installMethod: "Local plugin install plus a local marketplace catalog entry, or the generated release installer.",
9211
+ localInstallPath: "~/.codex/plugins/<plugin> plus a local marketplace catalog entry.",
9212
+ reloadBehavior: "Use Plugins > Refresh when that UI action is available, otherwise restart Codex.",
9213
+ cacheSemantics: "Codex may load a separate active cache under ~/.codex/plugins/cache/local-plugins/<plugin>/<version>. Pluxx clears local cache on install/uninstall and verify-install detects stale cache contents.",
9214
+ discoverySurface: "Codex Plugins view and plugin detail page. Plugin-bundled MCP servers may appear on the plugin detail page without appearing in the global MCP servers settings page.",
9215
+ brandListingSupport: "Richest current shared-brand target: display name, descriptions, category, color, icon/logo, screenshots, default prompt, website, and policy links can be emitted into .codex-plugin/plugin.json interface metadata.",
9216
+ installFollowupNote: "Codex note: if Codex is already open, use Plugins > Refresh if that action is available in your current UI, or restart Codex to pick up the new install. Plugin-bundled MCP servers may appear on the plugin detail page without appearing in the global MCP servers settings page.",
9217
+ publishReloadInstruction: "If Codex is already open, use Plugins > Refresh if that action is available in your current UI, or restart Codex so the plugin is picked up.",
9218
+ verifyStaleAction: "in Codex, use Plugins > Refresh if available, or restart Codex so the plugin cache reloads"
9219
+ },
9220
+ {
9221
+ platform: "opencode",
9222
+ label: "OpenCode",
9223
+ installMethod: "Local plugin directory plus generated entry wrapper, generated release installer, or npm-backed wrapper package path.",
9224
+ localInstallPath: "~/.config/opencode/plugins/<plugin> plus ~/.config/opencode/plugins/<plugin>.ts and synced skills under ~/.config/opencode/skills/<plugin>-<skill>.",
9225
+ reloadBehavior: "Restart or reload OpenCode.",
9226
+ cacheSemantics: "Pluxx writes the local plugin wrapper and synced skill files directly; no separate Pluxx-managed active cache is modeled.",
9227
+ discoverySurface: "OpenCode plugin loader, plugin wrapper file, and synced skill namespace.",
9228
+ brandListingSupport: "No shared manifest-backed brand fields from Pluxx brand today; OpenCode receives functional plugin module, instructions, skills, and package metadata.",
9229
+ installFollowupNote: "OpenCode note: if OpenCode is already open, restart or reload it so the plugin is picked up.",
9230
+ publishReloadInstruction: "If OpenCode is already open, restart or reload it so the plugin is picked up."
9231
+ }
9232
+ ];
9233
+ var CORE_FOUR_CAPABILITIES_BY_PLATFORM = Object.fromEntries(
9234
+ CORE_FOUR_INSTALL_DISCOVERY_CAPABILITIES.map((capability) => [capability.platform, capability])
9235
+ );
9236
+ var CORE_FOUR_PLATFORMS2 = new Set(
9237
+ CORE_FOUR_INSTALL_DISCOVERY_CAPABILITIES.map((capability) => capability.platform)
9238
+ );
9126
9239
  var VERIFY_STALE_ACTIONS = {
9127
- codex: "in Codex, use Plugins > Refresh if available, or restart Codex so the plugin cache reloads"
9128
- };
9129
- var PUBLISH_RELOAD_INSTRUCTIONS = {
9130
- "claude-code": "If Claude is already open, run /reload-plugins in the active session.",
9131
- cursor: "If Cursor is already open, use Developer: Reload Window or restart Cursor so the plugin is picked up.",
9132
- codex: "If Codex is already open, use Plugins > Refresh if that action is available in your current UI, or restart Codex so the plugin is picked up.",
9133
- opencode: "If OpenCode is already open, restart or reload it so the plugin is picked up."
9240
+ codex: CORE_FOUR_CAPABILITIES_BY_PLATFORM.codex.verifyStaleAction
9134
9241
  };
9242
+ function isCoreFourTarget(target) {
9243
+ return CORE_FOUR_PLATFORMS2.has(target);
9244
+ }
9245
+ function getHostInstallDiscoveryCapability(target) {
9246
+ return CORE_FOUR_CAPABILITIES_BY_PLATFORM[target];
9247
+ }
9135
9248
  function getVerifyInstallStaleAction(target) {
9136
9249
  return VERIFY_STALE_ACTIONS[target];
9137
9250
  }
9138
9251
  function getPublishReloadInstruction(target) {
9139
- return PUBLISH_RELOAD_INSTRUCTIONS[target];
9252
+ return isCoreFourTarget(target) ? getHostInstallDiscoveryCapability(target).publishReloadInstruction : void 0;
9253
+ }
9254
+
9255
+ // src/mcp-native-overrides.ts
9256
+ function asRecord(value) {
9257
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
9258
+ return value;
9259
+ }
9260
+ function firstString(...values) {
9261
+ for (const value of values) {
9262
+ if (typeof value === "string" && value.trim()) return value.trim();
9263
+ }
9264
+ return void 0;
9265
+ }
9266
+ function readStringRecord(value) {
9267
+ const record = asRecord(value);
9268
+ if (!record) return void 0;
9269
+ const stringRecord = Object.fromEntries(
9270
+ Object.entries(record).filter(([, entryValue]) => typeof entryValue === "string")
9271
+ );
9272
+ return Object.keys(stringRecord).length > 0 ? stringRecord : void 0;
9273
+ }
9274
+ function extractEnvVar(value) {
9275
+ const match = value.match(/\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/);
9276
+ return match?.[1] ?? match?.[2];
9277
+ }
9278
+ function envVarToKey(envVar) {
9279
+ return envVar.toLowerCase().replace(/_/g, "-");
9280
+ }
9281
+ function collectNativeMcpAuthUserConfigEntries(config, platforms, existingFields) {
9282
+ const next = [];
9283
+ const seenKeys = new Set(existingFields.map((field) => field.key));
9284
+ const seenEnvVars = new Set(
9285
+ existingFields.map((field) => field.envVar ?? defaultUserConfigEnvVar(field.key))
9286
+ );
9287
+ const pushDerived = (envVar, platform, serverName) => {
9288
+ if (!envVar || seenEnvVars.has(envVar)) return;
9289
+ const key = envVarToKey(envVar);
9290
+ if (seenKeys.has(key)) return;
9291
+ seenEnvVars.add(envVar);
9292
+ seenKeys.add(key);
9293
+ next.push({
9294
+ key,
9295
+ title: envVar,
9296
+ description: `Derived from native ${platform} MCP auth for ${serverName}.`,
9297
+ type: "secret",
9298
+ required: true,
9299
+ envVar,
9300
+ targets: [platform]
9301
+ });
9302
+ };
9303
+ for (const platform of platforms) {
9304
+ const platformConfig = asRecord(config.platforms?.[platform]);
9305
+ const mcpServers = asRecord(platformConfig?.mcpServers);
9306
+ if (!mcpServers) continue;
9307
+ for (const [serverName, rawOverride] of Object.entries(mcpServers)) {
9308
+ const override = asRecord(rawOverride);
9309
+ if (!override) continue;
9310
+ const bearerTokenEnv = firstString(override.bearer_token_env_var, override.bearerTokenEnvVar);
9311
+ if (bearerTokenEnv) pushDerived(bearerTokenEnv, platform, serverName);
9312
+ for (const record of [
9313
+ readStringRecord(override.env_http_headers ?? override.envHttpHeaders),
9314
+ readStringRecord(override.headers),
9315
+ readStringRecord(override.http_headers ?? override.httpHeaders)
9316
+ ]) {
9317
+ for (const value of Object.values(record ?? {})) {
9318
+ const envVar = extractEnvVar(value) ?? value;
9319
+ pushDerived(envVar, platform, serverName);
9320
+ }
9321
+ }
9322
+ }
9323
+ }
9324
+ return next;
9140
9325
  }
9141
9326
 
9142
9327
  // src/cli/publish.ts
@@ -9438,8 +9623,15 @@ echo
9438
9623
  echo "Installed __DISPLAY_NAME__ across ${installerTargets.join(", ")}."
9439
9624
  `.replaceAll("__REPO__", "REPO_PLACEHOLDER").replaceAll("__DISPLAY_NAME__", "DISPLAY_PLACEHOLDER");
9440
9625
  }
9626
+ function collectInstallerUserConfigEntries(config, platforms) {
9627
+ const baseEntries = collectUserConfigEntries(config, platforms);
9628
+ return [
9629
+ ...baseEntries,
9630
+ ...collectNativeMcpAuthUserConfigEntries(config, platforms, baseEntries)
9631
+ ];
9632
+ }
9441
9633
  function renderInstallerUserConfigSnippet(config, platform, installDirVariable) {
9442
- const entries = collectUserConfigEntries(config, [platform]).map((entry) => ({
9634
+ const entries = collectInstallerUserConfigEntries(config, [platform]).map((entry) => ({
9443
9635
  key: entry.key,
9444
9636
  title: entry.title,
9445
9637
  type: entry.type ?? "string",
@@ -9447,6 +9639,7 @@ function renderInstallerUserConfigSnippet(config, platform, installDirVariable)
9447
9639
  envVar: entry.envVar ?? defaultUserConfigEnvVar(entry.key)
9448
9640
  }));
9449
9641
  if (entries.length === 0) return "";
9642
+ const preserveSecretReferences = platform === "codex";
9450
9643
  const promptLines = entries.map((entry) => {
9451
9644
  const functionName = entry.type === "secret" ? "pluxx_prompt_secret_config" : "pluxx_prompt_text_config";
9452
9645
  return `${functionName} ${JSON.stringify(entry.key)} ${JSON.stringify(entry.envVar)} ${JSON.stringify(entry.title)} ${entry.required ? "1" : "0"}`;
@@ -9457,6 +9650,8 @@ ${JSON.stringify(entries)}
9457
9650
  PLUXX_USER_CONFIG_JSON
9458
9651
  )"
9459
9652
  PLUXX_REUSED_USER_CONFIG=0
9653
+ PLUXX_PRESERVE_SECRET_REFS="${preserveSecretReferences ? "1" : "0"}"
9654
+ export PLUXX_PRESERVE_SECRET_REFS
9460
9655
 
9461
9656
  pluxx_is_placeholder_secret() {
9462
9657
  case "$1" in
@@ -9483,12 +9678,14 @@ const fs = require('fs')
9483
9678
  const filepath = process.env.PLUXX_SAVED_USER_CONFIG_PATH
9484
9679
  const key = process.env.PLUXX_SAVED_CONFIG_KEY
9485
9680
  const envVar = process.env.PLUXX_SAVED_CONFIG_ENV_VAR
9681
+ const preserveSecretRefs = process.env.PLUXX_PRESERVE_SECRET_REFS === '1'
9486
9682
 
9487
9683
  try {
9488
9684
  const payload = JSON.parse(fs.readFileSync(filepath, 'utf8'))
9489
9685
  const candidates = [
9490
9686
  payload && payload.env && envVar ? payload.env[envVar] : undefined,
9491
9687
  payload && payload.values && key ? payload.values[key] : undefined,
9688
+ preserveSecretRefs && payload && payload.envRefs && envVar && payload.envRefs[envVar] === envVar ? envVar : undefined,
9492
9689
  ]
9493
9690
 
9494
9691
  for (const candidate of candidates) {
@@ -9505,17 +9702,23 @@ process.exit(1)
9505
9702
  NODE
9506
9703
  }
9507
9704
 
9705
+ pluxx_can_prompt_config() {
9706
+ [[ -r /dev/tty && -w /dev/tty ]] && { [[ -t 0 ]] || [[ -t 1 ]] || [[ -t 2 ]]; }
9707
+ }
9708
+
9508
9709
  pluxx_prompt_secret_config() {
9509
9710
  local key="$1"
9510
9711
  local env_var="$2"
9511
9712
  local label="$3"
9512
9713
  local required="$4"
9513
9714
  local current_value="\${!env_var:-}"
9715
+ local saved_value_invalid=0
9514
9716
 
9515
9717
  if [[ -z "$current_value" && "\${PLUXX_RECONFIGURE:-0}" != "1" ]]; then
9516
9718
  local saved_value=""
9517
9719
  if saved_value="$(pluxx_saved_config_value "$key" "$env_var")"; then
9518
9720
  if pluxx_is_placeholder_secret "$saved_value"; then
9721
+ saved_value_invalid=1
9519
9722
  echo "Ignoring placeholder-looking saved config for $env_var." >&2
9520
9723
  else
9521
9724
  current_value="$saved_value"
@@ -9525,11 +9728,15 @@ pluxx_prompt_secret_config() {
9525
9728
  fi
9526
9729
 
9527
9730
  if [[ -z "$current_value" && "$required" == "1" ]]; then
9528
- if [[ -t 0 || -r /dev/tty ]]; then
9731
+ if pluxx_can_prompt_config; then
9529
9732
  read -r -s -p "$label [$env_var]: " current_value </dev/tty
9530
9733
  echo >/dev/tty
9531
9734
  else
9532
- echo "Missing required config: export $env_var before running this installer." >&2
9735
+ if [[ "$saved_value_invalid" == "1" ]]; then
9736
+ echo "Refusing placeholder-looking saved config for $env_var. Set a real value and rerun the installer." >&2
9737
+ else
9738
+ echo "Missing required config: export $env_var before running this installer." >&2
9739
+ fi
9533
9740
  exit 1
9534
9741
  fi
9535
9742
  fi
@@ -9558,7 +9765,7 @@ pluxx_prompt_text_config() {
9558
9765
  fi
9559
9766
 
9560
9767
  if [[ -z "$current_value" && "$required" == "1" ]]; then
9561
- if [[ -t 0 || -r /dev/tty ]]; then
9768
+ if pluxx_can_prompt_config; then
9562
9769
  read -r -p "$label [$env_var]: " current_value </dev/tty
9563
9770
  else
9564
9771
  echo "Missing required config: export $env_var before running this installer." >&2
@@ -9572,7 +9779,7 @@ pluxx_prompt_text_config() {
9572
9779
  ${promptLines.join("\n")}
9573
9780
 
9574
9781
  if [[ "$PLUXX_REUSED_USER_CONFIG" == "1" ]]; then
9575
- echo "Found existing plugin config; reusing saved install values."
9782
+ echo "Found existing $PLUGIN_NAME config; reusing saved install values."
9576
9783
  fi
9577
9784
 
9578
9785
  export PLUXX_USER_CONFIG_SPEC
@@ -9584,21 +9791,53 @@ const path = require('path')
9584
9791
 
9585
9792
  const installDir = process.env.PLUXX_INSTALL_DIR
9586
9793
  const spec = JSON.parse(process.env.PLUXX_USER_CONFIG_SPEC || '[]')
9794
+ const preserveSecretReferences = ${preserveSecretReferences ? "true" : "false"}
9587
9795
 
9588
9796
  if (installDir && spec.length > 0) {
9589
9797
  const env = {}
9590
9798
  const values = {}
9799
+ const envRefs = {}
9800
+ const secretKeys = []
9801
+ const secretEnv = []
9802
+ let hasSecret = false
9803
+ const secretEnvVars = new Set(
9804
+ spec
9805
+ .filter((entry) => entry && entry.type === 'secret' && typeof entry.envVar === 'string' && entry.envVar !== '')
9806
+ .map((entry) => entry.envVar),
9807
+ )
9591
9808
 
9592
9809
  for (const entry of spec) {
9810
+ if (entry.type === 'secret') {
9811
+ hasSecret = true
9812
+ if (preserveSecretReferences) {
9813
+ if (entry.key) secretKeys.push(entry.key)
9814
+ if (entry.envVar) secretEnv.push(entry.envVar)
9815
+ }
9816
+ }
9593
9817
  const value = process.env[entry.envVar]
9594
9818
  if (value === undefined || value === '') continue
9819
+ if (preserveSecretReferences && entry.type === 'secret') {
9820
+ envRefs[entry.envVar] = entry.envVar
9821
+ continue
9822
+ }
9595
9823
  values[entry.key] = value
9596
9824
  env[entry.envVar] = value
9597
9825
  }
9598
9826
 
9599
9827
  fs.writeFileSync(
9600
9828
  path.join(installDir, '.pluxx-user.json'),
9601
- JSON.stringify({ values, env }, null, 2) + '\\n',
9829
+ JSON.stringify(
9830
+ {
9831
+ ...(Object.keys(values).length > 0 ? { values } : {}),
9832
+ ...(Object.keys(env).length > 0 ? { env } : {}),
9833
+ ...(Object.keys(envRefs).length > 0 ? { envRefs } : {}),
9834
+ ...(hasSecret ? { secretStorage: preserveSecretReferences ? 'env-ref' : 'materialized' } : {}),
9835
+ ...(preserveSecretReferences && secretKeys.length > 0 ? { secretKeys: [...new Set(secretKeys)].sort() } : {}),
9836
+ ...(preserveSecretReferences && secretEnv.length > 0 ? { secretEnv: [...new Set(secretEnv)].sort() } : {}),
9837
+ },
9838
+ null,
9839
+ 2,
9840
+ ) + '\\n',
9602
9841
  )
9603
9842
 
9604
9843
  const envScriptPath = path.join(installDir, 'scripts/check-env.sh')
@@ -9611,7 +9850,10 @@ if (installDir && spec.length > 0) {
9611
9850
 
9612
9851
  const materialize = (value) =>
9613
9852
  typeof value === 'string'
9614
- ? value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_match, name) => env[name] || '${" + name + "}')
9853
+ ? value.replace(
9854
+ /\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g,
9855
+ (_match, name) => (preserveSecretReferences && secretEnvVars.has(name) ? '${" + name + "}' : (env[name] || '${" + name + "}')),
9856
+ )
9615
9857
  : value
9616
9858
 
9617
9859
  const materializeRecord = (record) => {
@@ -9635,7 +9877,7 @@ if (installDir && spec.length > 0) {
9635
9877
  server.env = materializeRecord(server.env)
9636
9878
  }
9637
9879
 
9638
- if (server.bearer_token_env_var && env[server.bearer_token_env_var]) {
9880
+ if (!preserveSecretReferences && server.bearer_token_env_var && env[server.bearer_token_env_var]) {
9639
9881
  server.http_headers = {
9640
9882
  ...(server.http_headers || {}),
9641
9883
  Authorization: 'Bearer ' + env[server.bearer_token_env_var],
@@ -9643,7 +9885,7 @@ if (installDir && spec.length > 0) {
9643
9885
  delete server.bearer_token_env_var
9644
9886
  }
9645
9887
 
9646
- if (server.env_http_headers && typeof server.env_http_headers === 'object') {
9888
+ if (!preserveSecretReferences && server.env_http_headers && typeof server.env_http_headers === 'object') {
9647
9889
  server.http_headers = {
9648
9890
  ...(server.http_headers || {}),
9649
9891
  }
@@ -9668,7 +9910,7 @@ NODE
9668
9910
  `;
9669
9911
  }
9670
9912
  function hasInstallerUserConfig(config, platform) {
9671
- return collectUserConfigEntries(config, [platform]).length > 0;
9913
+ return collectInstallerUserConfigEntries(config, [platform]).length > 0;
9672
9914
  }
9673
9915
  function renderInstallerSavedUserConfigCaptureSnippet(config, platform, installDirVariable) {
9674
9916
  if (!hasInstallerUserConfig(config, platform)) return "";
@@ -9834,13 +10076,13 @@ for (const line of lines) {
9834
10076
  continue
9835
10077
  }
9836
10078
  if (tableName === '') {
9837
- const dottedMatch = trimmed.match(/^features\\.plugin_hooks\\s*=\\s*(.+)$/)
10079
+ const dottedMatch = trimmed.match(/^features\\.hooks\\s*=\\s*(.+)$/)
9838
10080
  if (dottedMatch && isTomlTrue(dottedMatch[1])) process.exit(0)
9839
10081
  const inlineMatch = trimmed.match(/^features\\s*=\\s*(.+)$/)
9840
- if (inlineMatch && /\\bplugin_hooks\\s*=\\s*true\\b/i.test(inlineMatch[1])) process.exit(0)
10082
+ if (inlineMatch && /\\bhooks\\s*=\\s*true\\b/i.test(inlineMatch[1])) process.exit(0)
9841
10083
  }
9842
10084
  if (tableName !== 'features') continue
9843
- const match = trimmed.match(/^plugin_hooks\\s*=\\s*(.+)$/)
10085
+ const match = trimmed.match(/^hooks\\s*=\\s*(.+)$/)
9844
10086
  if (match && isTomlTrue(match[1])) process.exit(0)
9845
10087
  }
9846
10088
  process.exit(1)
@@ -9859,7 +10101,7 @@ NODE
9859
10101
  *)
9860
10102
  if [[ -r /dev/tty ]]; then
9861
10103
  echo "This Codex plugin bundle includes startup hooks." >/dev/tty
9862
- echo "Codex requires [features].plugin_hooks = true before plugin-bundled hooks can run." >/dev/tty
10104
+ echo "Codex requires [features].hooks = true before plugin-bundled hooks can run." >/dev/tty
9863
10105
  read -r -p "Enable Codex plugin-bundled hooks in $CODEX_CONFIG_PATH now? [Y/n] " PLUXX_CODEX_HOOKS_REPLY </dev/tty
9864
10106
  case "$PLUXX_CODEX_HOOKS_REPLY" in
9865
10107
  n|N|no|NO)
@@ -9931,7 +10173,7 @@ for (let index = 0; index < lines.length; index += 1) {
9931
10173
  if (/^features\\.[A-Za-z0-9_-]+\\s*=/.test(trimmed) && firstTopLevelFeaturesDotted < 0) {
9932
10174
  firstTopLevelFeaturesDotted = index
9933
10175
  }
9934
- if (/^features\\.plugin_hooks\\s*=/.test(trimmed)) {
10176
+ if (/^features\\.hooks\\s*=/.test(trimmed)) {
9935
10177
  topLevelPluginHooksDotted = index
9936
10178
  }
9937
10179
  if (/^features\\s*=\\s*\\{/.test(trimmed)) {
@@ -9950,28 +10192,28 @@ if (start >= 0) {
9950
10192
 
9951
10193
  let updated = false
9952
10194
  for (let index = start + 1; index < end; index += 1) {
9953
- if (/^plugin_hooks\\s*=/.test(stripTomlComment(lines[index]).trim())) {
9954
- lines[index] = 'plugin_hooks = true'
10195
+ if (/^hooks\\s*=/.test(stripTomlComment(lines[index]).trim())) {
10196
+ lines[index] = 'hooks = true'
9955
10197
  updated = true
9956
10198
  }
9957
10199
  }
9958
- if (!updated) lines.splice(start + 1, 0, 'plugin_hooks = true')
10200
+ if (!updated) lines.splice(start + 1, 0, 'hooks = true')
9959
10201
  } else if (topLevelPluginHooksDotted >= 0) {
9960
- lines[topLevelPluginHooksDotted] = 'features.plugin_hooks = true'
10202
+ lines[topLevelPluginHooksDotted] = 'features.hooks = true'
9961
10203
  } else if (firstTopLevelFeaturesDotted >= 0) {
9962
- lines.splice(firstTopLevelFeaturesDotted + 1, 0, 'features.plugin_hooks = true')
10204
+ lines.splice(firstTopLevelFeaturesDotted + 1, 0, 'features.hooks = true')
9963
10205
  } else if (topLevelInlineFeatures >= 0 && lines[topLevelInlineFeatures].includes('}')) {
9964
- if (/\\bplugin_hooks\\s*=/.test(lines[topLevelInlineFeatures])) {
10206
+ if (/\\bhooks\\s*=/.test(lines[topLevelInlineFeatures])) {
9965
10207
  lines[topLevelInlineFeatures] = lines[topLevelInlineFeatures].replace(
9966
- /\\bplugin_hooks\\s*=\\s*(true|false)\\b/i,
9967
- 'plugin_hooks = true',
10208
+ /\\bhooks\\s*=\\s*(true|false)\\b/i,
10209
+ 'hooks = true',
9968
10210
  )
9969
10211
  } else {
9970
- lines[topLevelInlineFeatures] = lines[topLevelInlineFeatures].replace(/}/, ', plugin_hooks = true }')
10212
+ lines[topLevelInlineFeatures] = lines[topLevelInlineFeatures].replace(/}/, ', hooks = true }')
9971
10213
  }
9972
10214
  } else {
9973
10215
  if (lines.length > 0 && lines[lines.length - 1] !== '') lines.push('')
9974
- lines.push('[features]', 'plugin_hooks = true')
10216
+ lines.push('[features]', 'hooks = true')
9975
10217
  }
9976
10218
 
9977
10219
  fs.mkdirSync(path.dirname(filepath), { recursive: true })
@@ -9982,7 +10224,7 @@ NODE
9982
10224
  else
9983
10225
  echo "Codex plugin-bundled hooks are not enabled. Startup hooks from this plugin will not run until you add this to $CODEX_CONFIG_PATH:" >&2
9984
10226
  echo "[features]" >&2
9985
- echo "plugin_hooks = true" >&2
10227
+ echo "hooks = true" >&2
9986
10228
  echo "Then restart or refresh Codex before relying on plugin startup hooks." >&2
9987
10229
  echo "Set PLUXX_CODEX_ENABLE_PLUGIN_HOOKS=1 before running this installer to enable it noninteractively." >&2
9988
10230
  fi
@@ -10613,15 +10855,536 @@ function runPublish(config, options = {}) {
10613
10855
  };
10614
10856
  }
10615
10857
 
10858
+ // src/bundle-check.ts
10859
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
10860
+ import { join, relative, resolve as resolve3 } from "path";
10861
+
10862
+ // src/readiness.ts
10863
+ function getRuntimeReadinessPlan(readiness) {
10864
+ if (!readiness || readiness.dependencies.length === 0 && readiness.gates.length === 0) {
10865
+ return {
10866
+ hasReadiness: false,
10867
+ dependencyIds: [],
10868
+ needsSessionStart: false,
10869
+ needsMcpGate: false,
10870
+ needsPromptGate: false,
10871
+ hasNamedMcpTools: false,
10872
+ hasNamedPromptTargets: false
10873
+ };
10874
+ }
10875
+ return {
10876
+ hasReadiness: true,
10877
+ dependencyIds: readiness.dependencies.map((dependency) => dependency.id),
10878
+ needsSessionStart: readiness.dependencies.length > 0,
10879
+ needsMcpGate: readiness.gates.some((gate) => gate.applyTo.includes("mcp-tools")),
10880
+ needsPromptGate: readiness.gates.some((gate) => gate.applyTo.includes("skills") || gate.applyTo.includes("commands")),
10881
+ hasNamedMcpTools: readiness.gates.some((gate) => (gate.tools?.length ?? 0) > 0),
10882
+ hasNamedPromptTargets: readiness.gates.some((gate) => (gate.skills?.length ?? 0) > 0 || (gate.commands?.length ?? 0) > 0)
10883
+ };
10884
+ }
10885
+
10886
+ // src/bundle-check.ts
10887
+ var MAKER_FIELD = "author";
10888
+ var CLAUDE_FAMILY_FIELDS = ["name", "version", "description", MAKER_FIELD, "license", "repository", "keywords"];
10889
+ var MANIFEST_DESCRIPTORS = {
10890
+ "claude-code": {
10891
+ path: ".claude-plugin/plugin.json",
10892
+ expectedFields: (config) => commonExpectedFields(config, CLAUDE_FAMILY_FIELDS),
10893
+ referenceFields: ["commands", "agents", "skills", "hooks", "mcpServers"]
10894
+ },
10895
+ cursor: {
10896
+ path: ".cursor-plugin/plugin.json",
10897
+ expectedFields: (config) => [
10898
+ ...commonExpectedFields(config, ["name", "version", "description", MAKER_FIELD, "repository", "license", "keywords"]),
10899
+ ...config.brand?.websiteURL ? [{ field: "homepage", expected: config.brand.websiteURL }] : [],
10900
+ ...expectedStandardHooksManifestField(config)
10901
+ ],
10902
+ expectedReferences: expectedStandardHookReferences,
10903
+ referenceFields: ["skills", "commands", "agents", "rules", "hooks", "mcpServers", "logo"]
10904
+ },
10905
+ codex: {
10906
+ path: ".codex-plugin/plugin.json",
10907
+ expectedFields: (config) => commonExpectedFields(config, ["name", "version", "description", MAKER_FIELD, "repository", "license", "keywords"]),
10908
+ referenceFields: ["skills", "mcpServers", "apps", "hooks"],
10909
+ collectExtraReferences: (manifest) => collectCodexInterfaceReferences(manifest)
10910
+ },
10911
+ opencode: {
10912
+ path: "package.json",
10913
+ expectedFields: (config) => [
10914
+ { field: "name", expected: config.platforms?.opencode?.npmPackage ?? `opencode-${config.name}` },
10915
+ { field: "version", expected: config.version },
10916
+ { field: "description", expected: `${config.description} (OpenCode plugin)` },
10917
+ { field: "main", expected: "index.ts" },
10918
+ { field: "type", expected: "module" },
10919
+ { field: MAKER_FIELD, expected: getMakerName(config) },
10920
+ { field: "license", expected: config.license }
10921
+ ]
10922
+ },
10923
+ "github-copilot": {
10924
+ path: ".claude-plugin/plugin.json",
10925
+ expectedFields: (config) => [
10926
+ ...commonExpectedFields(config, CLAUDE_FAMILY_FIELDS),
10927
+ ...expectedStandardHooksManifestField(config)
10928
+ ],
10929
+ expectedReferences: expectedStandardHookReferences,
10930
+ referenceFields: ["commands", "agents", "skills", "hooks", "mcpServers"]
10931
+ },
10932
+ openhands: {
10933
+ path: ".plugin/plugin.json",
10934
+ expectedFields: (config) => [
10935
+ ...commonExpectedFields(config, CLAUDE_FAMILY_FIELDS),
10936
+ ...expectedStandardHooksManifestField(config)
10937
+ ],
10938
+ expectedReferences: expectedStandardHookReferences,
10939
+ referenceFields: ["commands", "agents", "skills", "hooks", "mcpServers"]
10940
+ },
10941
+ "gemini-cli": {
10942
+ path: "gemini-extension.json",
10943
+ expectedFields: (config) => commonExpectedFields(config, ["name", "version", "description", MAKER_FIELD]),
10944
+ referenceFields: ["skills"]
10945
+ }
10946
+ };
10947
+ function checkGeneratedBundles(config, rootDir, targets = config.targets) {
10948
+ const reports = targets.map((target) => {
10949
+ const targetRoot = resolve3(rootDir, config.outDir, target);
10950
+ const descriptor = MANIFEST_DESCRIPTORS[target];
10951
+ const files = collectFileInventory(targetRoot);
10952
+ const issues = [];
10953
+ if (!descriptor) {
10954
+ return { target, files, issues };
10955
+ }
10956
+ const manifestFile = resolve3(targetRoot, descriptor.path);
10957
+ if (!existsSync3(manifestFile)) {
10958
+ issues.push({
10959
+ code: "manifest-missing",
10960
+ target,
10961
+ path: descriptor.path,
10962
+ detail: `Generated ${target} bundle is missing manifest ${descriptor.path}.`
10963
+ });
10964
+ return { target, manifestPath: descriptor.path, files, issues };
10965
+ }
10966
+ let manifest;
10967
+ try {
10968
+ manifest = JSON.parse(readFileSync3(manifestFile, "utf-8"));
10969
+ } catch (error) {
10970
+ issues.push({
10971
+ code: "manifest-invalid",
10972
+ target,
10973
+ path: descriptor.path,
10974
+ detail: `Generated ${target} manifest is not parseable: ${error instanceof Error ? error.message : String(error)}`
10975
+ });
10976
+ return { target, manifestPath: descriptor.path, files, issues };
10977
+ }
10978
+ for (const { field, expected } of descriptor.expectedFields(config)) {
10979
+ if (expected === void 0) continue;
10980
+ const actual = getField(manifest, field);
10981
+ if (!jsonEqual(actual, expected)) {
10982
+ issues.push({
10983
+ code: "manifest-field-drift",
10984
+ target,
10985
+ path: `${descriptor.path}#${field}`,
10986
+ detail: `Generated ${target} manifest field "${field}" drifted from source config.`,
10987
+ expected,
10988
+ actual
10989
+ });
10990
+ }
10991
+ }
10992
+ const references = Array.from(/* @__PURE__ */ new Set([
10993
+ ...collectManifestReferences(manifest, descriptor.referenceFields ?? []),
10994
+ ...descriptor.expectedReferences?.(config) ?? [],
10995
+ ...descriptor.collectExtraReferences?.(manifest) ?? []
10996
+ ]));
10997
+ for (const reference of references) {
10998
+ const resolvedReference = resolveBundleReference(targetRoot, reference);
10999
+ if (!resolvedReference || !existsSync3(resolvedReference)) {
11000
+ issues.push({
11001
+ code: "manifest-reference-missing",
11002
+ target,
11003
+ path: descriptor.path,
11004
+ detail: `Generated ${target} manifest references missing bundle path: ${reference}.`
11005
+ });
11006
+ }
11007
+ }
11008
+ return { target, manifestPath: descriptor.path, files, issues };
11009
+ });
11010
+ return {
11011
+ targets: reports,
11012
+ issues: reports.flatMap((report) => report.issues)
11013
+ };
11014
+ }
11015
+ function assertGeneratedBundlesCurrent(config, rootDir, targets = config.targets) {
11016
+ const report = checkGeneratedBundles(config, rootDir, targets);
11017
+ if (report.issues.length === 0) return report;
11018
+ const details = report.issues.map((issue) => {
11019
+ const expected = issue.expected !== void 0 ? ` expected=${JSON.stringify(issue.expected)}` : "";
11020
+ const actual = issue.actual !== void 0 ? ` actual=${JSON.stringify(issue.actual)}` : "";
11021
+ return `- [${issue.target}] ${issue.detail} (${issue.path})${expected}${actual}`;
11022
+ }).join("\n");
11023
+ throw new Error(`Generated bundle check failed:
11024
+ ${details}`);
11025
+ }
11026
+ function getField(input, field) {
11027
+ return field.split(".").reduce((value, key) => {
11028
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
11029
+ return value[key];
11030
+ }, input);
11031
+ }
11032
+ function jsonEqual(a, b) {
11033
+ return JSON.stringify(a) === JSON.stringify(b);
11034
+ }
11035
+ function commonExpectedFields(config, fields) {
11036
+ return fields.map((field) => ({ field, expected: getField(config, field) }));
11037
+ }
11038
+ function getMakerName(config) {
11039
+ return config[MAKER_FIELD]?.name;
11040
+ }
11041
+ function expectsStandardHooksManifest(config) {
11042
+ return Boolean(config.hooks || config.permissions || getRuntimeReadinessPlan(config.readiness).hasReadiness);
11043
+ }
11044
+ function expectedStandardHooksManifestField(config) {
11045
+ return expectsStandardHooksManifest(config) ? [{ field: "hooks", expected: "./hooks/hooks.json" }] : [];
11046
+ }
11047
+ function expectedStandardHookReferences(config) {
11048
+ return expectsStandardHooksManifest(config) ? ["./hooks/hooks.json"] : [];
11049
+ }
11050
+ function collectManifestReferences(manifest, fields) {
11051
+ const references = [];
11052
+ for (const field of fields) {
11053
+ const value = getField(manifest, field);
11054
+ if (typeof value === "string") {
11055
+ references.push(value);
11056
+ } else if (Array.isArray(value)) {
11057
+ for (const item of value) {
11058
+ if (typeof item === "string") references.push(item);
11059
+ }
11060
+ }
11061
+ }
11062
+ return references;
11063
+ }
11064
+ function collectCodexInterfaceReferences(manifest) {
11065
+ const iface = manifest.interface;
11066
+ if (!iface || typeof iface !== "object" || Array.isArray(iface)) return [];
11067
+ return collectManifestReferences(iface, ["composerIcon", "logo", "screenshots"]);
11068
+ }
11069
+ function resolveBundleReference(rootDir, reference) {
11070
+ if (reference.trim() === "") return void 0;
11071
+ const stripped = reference.startsWith("./") ? reference.slice(2) : reference;
11072
+ const resolvedPath = resolve3(rootDir, stripped);
11073
+ const rel = relative(rootDir, resolvedPath);
11074
+ if (rel.startsWith("..")) return void 0;
11075
+ return resolvedPath;
11076
+ }
11077
+ function collectFileInventory(rootDir) {
11078
+ if (!existsSync3(rootDir)) return [];
11079
+ const files = [];
11080
+ const visit = (dir) => {
11081
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
11082
+ const fullPath = join(dir, entry.name);
11083
+ if (entry.isDirectory()) {
11084
+ visit(fullPath);
11085
+ } else if (entry.isFile()) {
11086
+ files.push(relative(rootDir, fullPath).replace(/\\/g, "/"));
11087
+ } else if (entry.isSymbolicLink()) {
11088
+ const stats = statSync(fullPath);
11089
+ if (stats.isFile()) files.push(relative(rootDir, fullPath).replace(/\\/g, "/"));
11090
+ }
11091
+ }
11092
+ };
11093
+ visit(rootDir);
11094
+ return files.sort();
11095
+ }
11096
+
11097
+ // src/install-reference.ts
11098
+ var INSTALL_REFERENCE_SCHEMES = ["local", "github", "npm", "team"];
11099
+ var INSTALL_REFERENCE_HELP = "Install references must use <scheme>:<locator>[@<version>][#<target>] with scheme local, github, npm, or team.";
11100
+ var OWNER_PAIR_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
11101
+ var NPM_PACKAGE_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
11102
+ var VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+:/-]*$/;
11103
+ function parseInstallReference(rawReference) {
11104
+ const raw = rawReference.trim();
11105
+ if (!raw) {
11106
+ throw installReferenceError("Provide an install reference.");
11107
+ }
11108
+ const schemeSeparator = raw.indexOf(":");
11109
+ if (schemeSeparator <= 0) {
11110
+ throw installReferenceError("Install reference is missing a scheme.");
11111
+ }
11112
+ const rawScheme = raw.slice(0, schemeSeparator);
11113
+ if (!isInstallReferenceScheme(rawScheme)) {
11114
+ throw installReferenceError(`Unsupported install reference scheme "${rawScheme}".`);
11115
+ }
11116
+ const rest = raw.slice(schemeSeparator + 1);
11117
+ const { body, target } = splitInstallReferenceTarget(rest);
11118
+ if (!body) {
11119
+ throw installReferenceError(`Install reference "${rawScheme}:" is missing a locator.`);
11120
+ }
11121
+ if (rawScheme === "local") {
11122
+ return buildLocalInstallReference(raw, body, target);
11123
+ }
11124
+ const { locator, version } = splitInstallReferenceVersion(body);
11125
+ if (!locator) {
11126
+ throw installReferenceError(`Install reference "${rawScheme}:" is missing a locator before the version.`);
11127
+ }
11128
+ validateInstallReferenceVersion(version);
11129
+ switch (rawScheme) {
11130
+ case "github":
11131
+ return buildGitHubInstallReference(raw, locator, version, target);
11132
+ case "npm":
11133
+ return buildNpmInstallReference(raw, locator, version, target);
11134
+ case "team":
11135
+ return buildTeamInstallReference(raw, locator, version, target);
11136
+ }
11137
+ }
11138
+ function formatInstallReference(reference) {
11139
+ return reference.normalized;
11140
+ }
11141
+ function buildLocalInstallReference(raw, locator, target) {
11142
+ return {
11143
+ raw,
11144
+ scheme: "local",
11145
+ locator,
11146
+ path: locator,
11147
+ ...target ? { target } : {},
11148
+ normalized: normalizeInstallReference("local", locator, void 0, target)
11149
+ };
11150
+ }
11151
+ function buildGitHubInstallReference(raw, locator, version, target) {
11152
+ if (!OWNER_PAIR_PATTERN.test(locator)) {
11153
+ throw installReferenceError("GitHub install references must look like github:<owner>/<repo>[@<version>][#<target>].");
11154
+ }
11155
+ const [owner, repo] = locator.split("/");
11156
+ return {
11157
+ raw,
11158
+ scheme: "github",
11159
+ locator,
11160
+ owner,
11161
+ repo,
11162
+ ...version ? { version } : {},
11163
+ ...target ? { target } : {},
11164
+ normalized: normalizeInstallReference("github", locator, version, target)
11165
+ };
11166
+ }
11167
+ function buildNpmInstallReference(raw, locator, version, target) {
11168
+ if (!NPM_PACKAGE_PATTERN.test(locator)) {
11169
+ throw installReferenceError("npm install references must look like npm:<package>[@<version>][#<target>].");
11170
+ }
11171
+ return {
11172
+ raw,
11173
+ scheme: "npm",
11174
+ locator,
11175
+ packageName: locator,
11176
+ ...version ? { version } : {},
11177
+ ...target ? { target } : {},
11178
+ normalized: normalizeInstallReference("npm", locator, version, target)
11179
+ };
11180
+ }
11181
+ function buildTeamInstallReference(raw, locator, version, target) {
11182
+ if (!OWNER_PAIR_PATTERN.test(locator)) {
11183
+ throw installReferenceError("Team install references must look like team:<team>/<plugin>[@<version>][#<target>].");
11184
+ }
11185
+ const [team, plugin] = locator.split("/");
11186
+ return {
11187
+ raw,
11188
+ scheme: "team",
11189
+ locator,
11190
+ team,
11191
+ plugin,
11192
+ ...version ? { version } : {},
11193
+ ...target ? { target } : {},
11194
+ normalized: normalizeInstallReference("team", locator, version, target)
11195
+ };
11196
+ }
11197
+ function splitInstallReferenceTarget(input) {
11198
+ const targetSeparator = input.lastIndexOf("#");
11199
+ if (targetSeparator === -1) return { body: input };
11200
+ const body = input.slice(0, targetSeparator).trim();
11201
+ const rawTarget = input.slice(targetSeparator + 1).trim();
11202
+ if (!rawTarget) {
11203
+ throw installReferenceError('Install reference target cannot be empty after "#".');
11204
+ }
11205
+ const parsedTarget = TargetPlatform.safeParse(rawTarget);
11206
+ if (!parsedTarget.success) {
11207
+ throw installReferenceError(`Unknown install reference target "${rawTarget}".`);
11208
+ }
11209
+ return { body, target: parsedTarget.data };
11210
+ }
11211
+ function splitInstallReferenceVersion(input) {
11212
+ const versionSeparator = input.lastIndexOf("@");
11213
+ if (versionSeparator <= 0) {
11214
+ return { locator: input.trim() };
11215
+ }
11216
+ const locator = input.slice(0, versionSeparator).trim();
11217
+ const version = input.slice(versionSeparator + 1).trim();
11218
+ if (!version) {
11219
+ throw installReferenceError('Install reference version cannot be empty after "@".');
11220
+ }
11221
+ return { locator, version };
11222
+ }
11223
+ function validateInstallReferenceVersion(version) {
11224
+ if (!version) return;
11225
+ if (!VERSION_PATTERN.test(version)) {
11226
+ throw installReferenceError(`Install reference version "${version}" is not valid.`);
11227
+ }
11228
+ }
11229
+ function normalizeInstallReference(scheme, locator, version, target) {
11230
+ return `${scheme}:${locator}${version ? `@${version}` : ""}${target ? `#${target}` : ""}`;
11231
+ }
11232
+ function isInstallReferenceScheme(value) {
11233
+ return INSTALL_REFERENCE_SCHEMES.includes(value);
11234
+ }
11235
+ function installReferenceError(message) {
11236
+ return new Error(`${message} ${INSTALL_REFERENCE_HELP}`);
11237
+ }
11238
+
11239
+ // src/host-detection.ts
11240
+ import { accessSync, constants, existsSync as existsSync4, statSync as statSync2 } from "fs";
11241
+ import { delimiter, resolve as resolve4 } from "path";
11242
+ import { homedir } from "os";
11243
+ var CORE_HOST_FAMILIES = CORE_FOUR_PLATFORMS;
11244
+ var HOST_EVIDENCE = {
11245
+ "claude-code": [
11246
+ { type: "cli", label: "Claude Code CLI", command: "claude" },
11247
+ { type: "user-config", label: "Claude user config", path: ({ homeDir }) => resolve4(homeDir, ".claude.json") },
11248
+ { type: "user-config", label: "Claude settings directory", path: ({ homeDir }) => resolve4(homeDir, ".claude/settings.json") },
11249
+ { type: "project-config", label: "Claude project MCP config", path: ({ rootDir }) => resolve4(rootDir, ".mcp.json") },
11250
+ { type: "project-config", label: "Claude project settings", path: ({ rootDir }) => resolve4(rootDir, ".claude/settings.json") },
11251
+ { type: "installed-plugin", label: "Claude plugin cache", path: ({ homeDir }) => resolve4(homeDir, ".claude/plugins/cache") },
11252
+ { type: "installed-plugin", label: "Legacy Claude plugin directory", path: ({ homeDir }) => resolve4(homeDir, ".claude/plugins") }
11253
+ ],
11254
+ cursor: [
11255
+ { type: "cli", label: "Cursor CLI", command: "cursor" },
11256
+ { type: "cli", label: "Cursor agent CLI", command: "cursor-agent" },
11257
+ { type: "app", label: "Cursor app bundle", path: ({ appDirs }) => resolveFirstAppPath(appDirs, "Cursor.app") },
11258
+ { type: "user-config", label: "Cursor user MCP config", path: ({ homeDir }) => resolve4(homeDir, ".cursor/mcp.json") },
11259
+ { type: "user-config", label: "Cursor user settings", path: ({ homeDir }) => resolve4(homeDir, ".cursor/settings.json") },
11260
+ { type: "project-config", label: "Cursor project MCP config", path: ({ rootDir }) => resolve4(rootDir, ".cursor/mcp.json") },
11261
+ { type: "project-config", label: "Cursor project rules", path: ({ rootDir }) => resolve4(rootDir, ".cursor/rules") },
11262
+ { type: "project-config", label: "Cursor root MCP config", path: ({ rootDir }) => resolve4(rootDir, "mcp.json") },
11263
+ { type: "installed-plugin", label: "Cursor local plugins", path: ({ homeDir }) => resolve4(homeDir, ".cursor/plugins/local") }
11264
+ ],
11265
+ codex: [
11266
+ { type: "cli", label: "Codex CLI", command: "codex" },
11267
+ { type: "app", label: "Codex app bundle", path: ({ appDirs }) => resolveFirstAppPath(appDirs, "Codex.app") },
11268
+ { type: "user-config", label: "Codex user config", path: ({ homeDir }) => resolve4(homeDir, ".codex/config.toml") },
11269
+ { type: "project-config", label: "Codex project config", path: ({ rootDir }) => resolve4(rootDir, ".codex/config.toml") },
11270
+ { type: "project-config", label: "Codex project agents", path: ({ rootDir }) => resolve4(rootDir, ".codex/agents") },
11271
+ { type: "installed-plugin", label: "Codex local plugins", path: ({ homeDir }) => resolve4(homeDir, ".codex/plugins") },
11272
+ { type: "installed-plugin", label: "Codex local marketplace catalog", path: ({ homeDir }) => resolve4(homeDir, ".agents/plugins/marketplace.json") }
11273
+ ],
11274
+ opencode: [
11275
+ { type: "cli", label: "OpenCode CLI", command: "opencode" },
11276
+ { type: "user-config", label: "OpenCode user config", path: ({ homeDir }) => resolve4(homeDir, ".config/opencode/opencode.json") },
11277
+ { type: "project-config", label: "OpenCode project config", path: ({ rootDir }) => resolve4(rootDir, "opencode.json") },
11278
+ { type: "project-config", label: "OpenCode alternate project config", path: ({ rootDir }) => resolve4(rootDir, ".opencode.json") },
11279
+ { type: "project-config", label: "OpenCode project directory", path: ({ rootDir }) => resolve4(rootDir, ".opencode") },
11280
+ { type: "installed-plugin", label: "OpenCode local plugins", path: ({ homeDir }) => resolve4(homeDir, ".config/opencode/plugins") },
11281
+ { type: "installed-plugin", label: "OpenCode synced skills", path: ({ homeDir }) => resolve4(homeDir, ".config/opencode/skills") }
11282
+ ]
11283
+ };
11284
+ function detectHostFamilies(options = {}) {
11285
+ const context = {
11286
+ rootDir: options.rootDir ?? process.cwd(),
11287
+ homeDir: options.homeDir ?? homedir(),
11288
+ pathEnv: options.pathEnv ?? process.env.PATH ?? "",
11289
+ appDirs: options.appDirs ?? defaultAppDirs(options.homeDir ?? homedir()),
11290
+ platform: options.platform ?? process.platform
11291
+ };
11292
+ const hostSet = new Set(options.hosts ?? CORE_HOST_FAMILIES);
11293
+ const hosts = CORE_HOST_FAMILIES.filter((host) => hostSet.has(host)).map((host) => detectHostFamily(host, context));
11294
+ return {
11295
+ hosts,
11296
+ detectedHosts: hosts.filter((host) => host.detected).map((host) => host.host)
11297
+ };
11298
+ }
11299
+ function buildHostTargetSelection(configTargets, explicitTargets, report) {
11300
+ const selectedTargets = explicitTargets ?? configTargets;
11301
+ const configCoreTargets = configTargets.filter(isCoreHostFamily);
11302
+ const suggestedTargets = report.detectedHosts.filter((host) => configCoreTargets.includes(host));
11303
+ const explicitOverride = explicitTargets !== void 0;
11304
+ return {
11305
+ source: explicitOverride ? "explicit-targets" : "config-targets",
11306
+ selectedTargets,
11307
+ detectedHosts: report.detectedHosts,
11308
+ suggestedTargets,
11309
+ explicitOverride,
11310
+ note: explicitOverride ? "Explicit --target selection is authoritative; host detection did not change selected targets." : "Host detection is informational; install targets still come from pluxx.config targets."
11311
+ };
11312
+ }
11313
+ function detectHostFamily(host, context) {
11314
+ const evidence = HOST_EVIDENCE[host].map((candidate) => resolveEvidence(candidate, context)).filter((item) => item !== void 0);
11315
+ return {
11316
+ host,
11317
+ detected: evidence.some(isMachineLevelEvidence),
11318
+ evidence
11319
+ };
11320
+ }
11321
+ function isMachineLevelEvidence(evidence) {
11322
+ return evidence.type !== "project-config";
11323
+ }
11324
+ function resolveEvidence(candidate, context) {
11325
+ if (candidate.command) {
11326
+ const commandPath = findExecutable(candidate.command, context.pathEnv, context.platform);
11327
+ if (!commandPath) return void 0;
11328
+ return {
11329
+ type: candidate.type,
11330
+ label: candidate.label,
11331
+ command: candidate.command,
11332
+ path: commandPath
11333
+ };
11334
+ }
11335
+ if (!candidate.path) return void 0;
11336
+ const evidencePath = candidate.path(context);
11337
+ if (!evidencePath || !existsSync4(evidencePath)) return void 0;
11338
+ return {
11339
+ type: candidate.type,
11340
+ label: candidate.label,
11341
+ path: evidencePath
11342
+ };
11343
+ }
11344
+ function findExecutable(command, pathEnv, platform) {
11345
+ if (!pathEnv.trim()) return void 0;
11346
+ const names = platform === "win32" ? [command, `${command}.exe`, `${command}.cmd`, `${command}.bat`] : [command];
11347
+ for (const dir of pathEnv.split(delimiter).filter(Boolean)) {
11348
+ for (const name of names) {
11349
+ const candidate = resolve4(dir, name);
11350
+ if (isExecutableFile(candidate, platform)) return candidate;
11351
+ }
11352
+ }
11353
+ return void 0;
11354
+ }
11355
+ function isExecutableFile(path, platform) {
11356
+ try {
11357
+ if (!statSync2(path).isFile()) return false;
11358
+ if (platform === "win32") return true;
11359
+ accessSync(path, constants.X_OK);
11360
+ return true;
11361
+ } catch {
11362
+ return false;
11363
+ }
11364
+ }
11365
+ function defaultAppDirs(homeDir) {
11366
+ return [
11367
+ "/Applications",
11368
+ resolve4(homeDir, "Applications")
11369
+ ];
11370
+ }
11371
+ function resolveFirstAppPath(appDirs, appName) {
11372
+ return appDirs.map((dir) => resolve4(dir, appName)).find((path) => existsSync4(path)) ?? resolve4(appDirs[0] ?? "/", appName);
11373
+ }
11374
+ function isCoreHostFamily(target) {
11375
+ return CORE_HOST_FAMILIES.includes(target);
11376
+ }
11377
+
10616
11378
  // src/cli/verify-install.ts
10617
- import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync2, statSync } from "fs";
10618
- import { resolve as resolve5 } from "path";
11379
+ import { createHash as createHash2 } from "crypto";
11380
+ import { existsSync as existsSync7, lstatSync as lstatSync2, readdirSync as readdirSync4, readFileSync as readFileSync6, readlinkSync, realpathSync as realpathSync2, statSync as statSync3 } from "fs";
11381
+ import { resolve as resolve7 } from "path";
10619
11382
 
10620
11383
  // src/cli/doctor.ts
10621
11384
  import { spawn, spawnSync as spawnSync2 } from "child_process";
10622
- import { accessSync, constants, existsSync as existsSync4, lstatSync, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "fs";
10623
- import { homedir } from "os";
10624
- import { basename as basename2, dirname as dirname2, resolve as resolve4 } from "path";
11385
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync6, lstatSync, readFileSync as readFileSync5, readdirSync as readdirSync3, realpathSync } from "fs";
11386
+ import { homedir as homedir2 } from "os";
11387
+ import { basename as basename2, dirname as dirname2, relative as relative3, resolve as resolve6 } from "path";
10625
11388
 
10626
11389
  // node_modules/jiti/lib/jiti.mjs
10627
11390
  var import_jiti = __toESM(require_jiti(), 1);
@@ -10634,8 +11397,8 @@ var CONFIG_FILES = [
10634
11397
  ];
10635
11398
 
10636
11399
  // src/cli/install.ts
10637
- import { resolve as resolve3, dirname, basename, relative } from "path";
10638
- import { existsSync as existsSync3, symlinkSync, mkdirSync as mkdirSync2, rmSync as rmSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, cpSync, readdirSync } from "fs";
11400
+ import { resolve as resolve5, dirname, basename, relative as relative2 } from "path";
11401
+ import { existsSync as existsSync5, symlinkSync, mkdirSync as mkdirSync2, rmSync as rmSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2, cpSync, readdirSync as readdirSync2 } from "fs";
10639
11402
 
10640
11403
  // src/mcp-stdio-paths.ts
10641
11404
  function findHostPluginRootVars(value) {
@@ -10664,57 +11427,57 @@ function getInstallTargets(pluginName) {
10664
11427
  return [
10665
11428
  {
10666
11429
  platform: "claude-code",
10667
- pluginDir: resolve3(home, ".claude/plugins", pluginName),
11430
+ pluginDir: resolve5(home, ".claude/plugins", pluginName),
10668
11431
  description: `claude plugin install ${pluginName}@${getClaudeMarketplaceName(pluginName)}`
10669
11432
  },
10670
11433
  {
10671
11434
  platform: "cursor",
10672
- pluginDir: resolve3(home, ".cursor/plugins/local", pluginName),
11435
+ pluginDir: resolve5(home, ".cursor/plugins/local", pluginName),
10673
11436
  description: `~/.cursor/plugins/local/${pluginName}`
10674
11437
  },
10675
11438
  {
10676
11439
  platform: "codex",
10677
- pluginDir: resolve3(home, ".codex/plugins", pluginName),
11440
+ pluginDir: resolve5(home, ".codex/plugins", pluginName),
10678
11441
  description: `~/.codex/plugins/${pluginName} (via ~/.agents/plugins/marketplace.json)`
10679
11442
  },
10680
11443
  {
10681
11444
  platform: "opencode",
10682
- pluginDir: resolve3(home, ".config/opencode/plugins", pluginName),
11445
+ pluginDir: resolve5(home, ".config/opencode/plugins", pluginName),
10683
11446
  description: `~/.config/opencode/plugins/${pluginName}.ts + ~/.config/opencode/plugins/${pluginName}/`
10684
11447
  },
10685
11448
  {
10686
11449
  platform: "github-copilot",
10687
- pluginDir: resolve3(home, ".github-copilot/plugins", pluginName),
11450
+ pluginDir: resolve5(home, ".github-copilot/plugins", pluginName),
10688
11451
  description: `~/.github-copilot/plugins/${pluginName}`
10689
11452
  },
10690
11453
  {
10691
11454
  platform: "openhands",
10692
- pluginDir: resolve3(home, ".openhands/plugins", pluginName),
11455
+ pluginDir: resolve5(home, ".openhands/plugins", pluginName),
10693
11456
  description: `~/.openhands/plugins/${pluginName}`
10694
11457
  },
10695
11458
  {
10696
11459
  platform: "warp",
10697
- pluginDir: resolve3(home, ".warp/plugins", pluginName),
11460
+ pluginDir: resolve5(home, ".warp/plugins", pluginName),
10698
11461
  description: `~/.warp/plugins/${pluginName}`
10699
11462
  },
10700
11463
  {
10701
11464
  platform: "gemini-cli",
10702
- pluginDir: resolve3(home, ".gemini/extensions", pluginName),
11465
+ pluginDir: resolve5(home, ".gemini/extensions", pluginName),
10703
11466
  description: `~/.gemini/extensions/${pluginName}`
10704
11467
  },
10705
11468
  {
10706
11469
  platform: "roo-code",
10707
- pluginDir: resolve3(home, ".roo/plugins", pluginName),
11470
+ pluginDir: resolve5(home, ".roo/plugins", pluginName),
10708
11471
  description: `~/.roo/plugins/${pluginName}`
10709
11472
  },
10710
11473
  {
10711
11474
  platform: "cline",
10712
- pluginDir: resolve3(home, ".cline/plugins", pluginName),
11475
+ pluginDir: resolve5(home, ".cline/plugins", pluginName),
10713
11476
  description: `~/.cline/plugins/${pluginName}`
10714
11477
  },
10715
11478
  {
10716
11479
  platform: "amp",
10717
- pluginDir: resolve3(home, ".amp/plugins", pluginName),
11480
+ pluginDir: resolve5(home, ".amp/plugins", pluginName),
10718
11481
  description: `~/.amp/plugins/${pluginName}`
10719
11482
  }
10720
11483
  ];
@@ -10725,10 +11488,10 @@ function getClaudeMarketplaceName(pluginName) {
10725
11488
  function readBundleManifestVersion(rootDir, platform) {
10726
11489
  const manifestPath = manifestPathForPlatform(platform);
10727
11490
  if (!manifestPath) return void 0;
10728
- const filepath = resolve3(rootDir, manifestPath);
10729
- if (!existsSync3(filepath)) return void 0;
11491
+ const filepath = resolve5(rootDir, manifestPath);
11492
+ if (!existsSync5(filepath)) return void 0;
10730
11493
  try {
10731
- const manifest = JSON.parse(readFileSync3(filepath, "utf-8"));
11494
+ const manifest = JSON.parse(readFileSync4(filepath, "utf-8"));
10732
11495
  return typeof manifest.version === "string" ? manifest.version : void 0;
10733
11496
  } catch {
10734
11497
  return void 0;
@@ -10737,7 +11500,7 @@ function readBundleManifestVersion(rootDir, platform) {
10737
11500
  function resolveClaudeInstalledCachePath(pluginName, version) {
10738
11501
  if (!version) return void 0;
10739
11502
  const home = process.env.HOME ?? "~";
10740
- return resolve3(home, ".claude/plugins/cache", getClaudeMarketplaceName(pluginName), pluginName, version);
11503
+ return resolve5(home, ".claude/plugins/cache", getClaudeMarketplaceName(pluginName), pluginName, version);
10741
11504
  }
10742
11505
  function resolveExpectedInstalledConsumerPath(target, pluginName) {
10743
11506
  if (target.platform === "claude-code" && pluginName !== "") {
@@ -10752,8 +11515,8 @@ function resolveExpectedInstalledConsumerPath(target, pluginName) {
10752
11515
  function resolveInstalledConsumerPath(target, pluginName) {
10753
11516
  if (target.platform === "claude-code" && pluginName !== "") {
10754
11517
  const expectedPath = resolveExpectedInstalledConsumerPath(target, pluginName);
10755
- if (existsSync3(expectedPath)) return expectedPath;
10756
- if (existsSync3(target.pluginDir)) return target.pluginDir;
11518
+ if (existsSync5(expectedPath)) return expectedPath;
11519
+ if (existsSync5(target.pluginDir)) return target.pluginDir;
10757
11520
  return expectedPath;
10758
11521
  }
10759
11522
  return target.pluginDir;
@@ -10775,13 +11538,13 @@ function manifestPathForPlatform(platform) {
10775
11538
  function isRelativeBundlePath(value) {
10776
11539
  return value.startsWith("./") || value.startsWith("../") || value.startsWith(".\\") || value.startsWith("..\\");
10777
11540
  }
10778
- function resolveBundleReference(rootDir, value) {
11541
+ function resolveBundleReference2(rootDir, value) {
10779
11542
  if (isRelativeBundlePath(value)) {
10780
- return resolve3(rootDir, value);
11543
+ return resolve5(rootDir, value);
10781
11544
  }
10782
11545
  const pluginRootMatch = value.match(/^\$\{(?:CLAUDE_PLUGIN_ROOT|CURSOR_PLUGIN_ROOT|CODEX_PLUGIN_ROOT|OPENCODE_PLUGIN_ROOT|PLUGIN_ROOT|PLUXX_PLUGIN_ROOT)\}[\\/](.+)$/);
10783
11546
  if (pluginRootMatch) {
10784
- return resolve3(rootDir, pluginRootMatch[1]);
11547
+ return resolve5(rootDir, pluginRootMatch[1]);
10785
11548
  }
10786
11549
  return void 0;
10787
11550
  }
@@ -10832,15 +11595,26 @@ function commandChangesToKnownPluginRoot(command) {
10832
11595
  return /\bcd\s+["']?\$\{?(?:CLAUDE_PLUGIN_ROOT|CURSOR_PLUGIN_ROOT|CODEX_PLUGIN_ROOT|OPENCODE_PLUGIN_ROOT|PLUGIN_ROOT|PLUXX_PLUGIN_ROOT)\}?/.test(command);
10833
11596
  }
10834
11597
  function formatBundleRelativePath(rootDir, filePath) {
10835
- const relativePath = relative(rootDir, filePath);
11598
+ const relativePath = relative2(rootDir, filePath);
10836
11599
  return relativePath && !relativePath.startsWith("..") ? relativePath : filePath;
10837
11600
  }
10838
11601
  function findHookWrapperPaths(rootDir, command) {
10839
- return extractBundleCommandTargets(command).map((target) => resolveBundleReference(rootDir, target)).filter((target) => {
11602
+ return extractBundleCommandTargets(command).map((target) => resolveBundleReference2(rootDir, target)).filter((target) => {
10840
11603
  if (!target) return false;
10841
- return existsSync3(target) && basename(target).startsWith("pluxx-hook-command-") && target.endsWith(".sh");
11604
+ return existsSync5(target) && basename(target).startsWith("pluxx-hook-command-") && (target.endsWith(".sh") || target.endsWith(".mjs"));
10842
11605
  });
10843
11606
  }
11607
+ function extractGeneratedHookWrapperCommand(wrapper) {
11608
+ const shellAssignment = wrapper.match(/^PLUXX_HOOK_COMMAND=(.*)$/m)?.[1];
11609
+ if (shellAssignment) return shellAssignment;
11610
+ const nodeAssignment = wrapper.match(/^const COMMAND = (.*)$/m)?.[1];
11611
+ if (!nodeAssignment) return "";
11612
+ try {
11613
+ return JSON.parse(nodeAssignment);
11614
+ } catch {
11615
+ return nodeAssignment;
11616
+ }
11617
+ }
10844
11618
  function findCodexCwdUnsafeHookCommands(rootDir, commands) {
10845
11619
  const issues = /* @__PURE__ */ new Set();
10846
11620
  for (const command of commands) {
@@ -10849,8 +11623,8 @@ function findCodexCwdUnsafeHookCommands(rootDir, commands) {
10849
11623
  issues.add(`${command} uses cwd-relative bundle target(s): ${relativeTargets.join(", ")}`);
10850
11624
  }
10851
11625
  for (const wrapperPath of findHookWrapperPaths(rootDir, command)) {
10852
- const wrapper = readFileSync3(wrapperPath, "utf-8");
10853
- const hookCommand = wrapper.match(/^PLUXX_HOOK_COMMAND=(.*)$/m)?.[1] ?? "";
11626
+ const wrapper = readFileSync4(wrapperPath, "utf-8");
11627
+ const hookCommand = extractGeneratedHookWrapperCommand(wrapper);
10854
11628
  const wrapperRelativeTargets = extractRelativeBundleCommandTargets(hookCommand);
10855
11629
  if (wrapperRelativeTargets.length > 0 && !commandChangesToKnownPluginRoot(wrapper)) {
10856
11630
  issues.add(`${formatBundleRelativePath(rootDir, wrapperPath)} evaluates cwd-relative bundle target(s): ${wrapperRelativeTargets.join(", ")}`);
@@ -10864,13 +11638,13 @@ function resolveInstalledHooksReference(rootDir, platform, manifest) {
10864
11638
  if (manifestReference) {
10865
11639
  return {
10866
11640
  reference: manifestReference,
10867
- path: resolveBundleReference(rootDir, manifestReference)
11641
+ path: resolveBundleReference2(rootDir, manifestReference)
10868
11642
  };
10869
11643
  }
10870
11644
  if (platform === "claude-code") {
10871
11645
  const fallbackReference = "./hooks/hooks.json";
10872
- const fallbackPath = resolve3(rootDir, "hooks/hooks.json");
10873
- if (existsSync3(fallbackPath)) {
11646
+ const fallbackPath = resolve5(rootDir, "hooks/hooks.json");
11647
+ if (existsSync5(fallbackPath)) {
10874
11648
  return {
10875
11649
  reference: fallbackReference,
10876
11650
  path: fallbackPath
@@ -10883,9 +11657,9 @@ function getClaudeStandardHooksManifestIssue(rootDir, platform, manifest) {
10883
11657
  if (platform !== "claude-code") return void 0;
10884
11658
  const manifestReference = typeof manifest.hooks === "string" ? manifest.hooks : void 0;
10885
11659
  if (!manifestReference) return void 0;
10886
- const manifestHooksPath = resolveBundleReference(rootDir, manifestReference);
10887
- const standardHooksPath = resolve3(rootDir, "hooks/hooks.json");
10888
- if (!manifestHooksPath || manifestHooksPath !== standardHooksPath || !existsSync3(standardHooksPath)) {
11660
+ const manifestHooksPath = resolveBundleReference2(rootDir, manifestReference);
11661
+ const standardHooksPath = resolve5(rootDir, "hooks/hooks.json");
11662
+ if (!manifestHooksPath || manifestHooksPath !== standardHooksPath || !existsSync5(standardHooksPath)) {
10889
11663
  return void 0;
10890
11664
  }
10891
11665
  return "Claude auto-loads hooks/hooks.json. Current Claude CLI releases report a duplicate hooks file load error when manifest.hooks also points at ./hooks/hooks.json, so manifest.hooks should only reference additional hook files.";
@@ -10900,8 +11674,8 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
10900
11674
  invalidRuntimeScripts: []
10901
11675
  };
10902
11676
  }
10903
- const manifestFile = resolve3(rootDir, manifestPath);
10904
- if (!existsSync3(manifestFile)) {
11677
+ const manifestFile = resolve5(rootDir, manifestPath);
11678
+ if (!existsSync5(manifestFile)) {
10905
11679
  return {
10906
11680
  manifestIssue: `missing plugin manifest at ${manifestPath}`,
10907
11681
  missingManifestPaths: [],
@@ -10912,7 +11686,7 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
10912
11686
  }
10913
11687
  let manifest;
10914
11688
  try {
10915
- manifest = JSON.parse(readFileSync3(manifestFile, "utf-8"));
11689
+ manifest = JSON.parse(readFileSync4(manifestFile, "utf-8"));
10916
11690
  } catch (error) {
10917
11691
  return {
10918
11692
  manifestIssue: `plugin manifest at ${manifestPath} is not parseable: ${error instanceof Error ? error.message : String(error)}`,
@@ -10923,8 +11697,8 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
10923
11697
  };
10924
11698
  }
10925
11699
  const missingManifestPaths = readBundleManifestReferences(manifest).filter((value) => {
10926
- const resolved = resolveBundleReference(rootDir, value);
10927
- return resolved !== void 0 && !existsSync3(resolved);
11700
+ const resolved = resolveBundleReference2(rootDir, value);
11701
+ return resolved !== void 0 && !existsSync5(resolved);
10928
11702
  }).sort();
10929
11703
  const manifestIssue = getClaudeStandardHooksManifestIssue(rootDir, platform, manifest);
10930
11704
  const { reference: hooksReference, path: hooksPath } = resolveInstalledHooksReference(rootDir, platform, manifest);
@@ -10937,7 +11711,7 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
10937
11711
  invalidRuntimeScripts: findInstalledRuntimeScriptIssues(rootDir, manifest)
10938
11712
  };
10939
11713
  }
10940
- if (!hooksPath || !existsSync3(hooksPath)) {
11714
+ if (!hooksPath || !existsSync5(hooksPath)) {
10941
11715
  return {
10942
11716
  ...manifestIssue ? { manifestIssue } : {},
10943
11717
  missingManifestPaths,
@@ -10947,13 +11721,13 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
10947
11721
  };
10948
11722
  }
10949
11723
  try {
10950
- const hooks = JSON.parse(readFileSync3(hooksPath, "utf-8"));
11724
+ const hooks = JSON.parse(readFileSync4(hooksPath, "utf-8"));
10951
11725
  const commands = [];
10952
11726
  collectHookCommandStrings(hooks, commands);
10953
11727
  const missingHookTargets = [...new Set(
10954
11728
  commands.flatMap(extractBundleCommandTargets).filter((value) => {
10955
- const resolved = resolveBundleReference(rootDir, value);
10956
- return resolved !== void 0 && !existsSync3(resolved);
11729
+ const resolved = resolveBundleReference2(rootDir, value);
11730
+ return resolved !== void 0 && !existsSync5(resolved);
10957
11731
  })
10958
11732
  )].sort();
10959
11733
  const cwdUnsafeHookCommands = platform === "codex" ? findCodexCwdUnsafeHookCommands(rootDir, commands) : [];
@@ -10978,10 +11752,10 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
10978
11752
  function findInstalledRuntimeScriptIssues(rootDir, manifest) {
10979
11753
  const mcpReference = typeof manifest.mcpServers === "string" ? manifest.mcpServers : void 0;
10980
11754
  if (!mcpReference) return [];
10981
- const mcpPath = resolveBundleReference(rootDir, mcpReference);
10982
- if (!mcpPath || !existsSync3(mcpPath)) return [];
11755
+ const mcpPath = resolveBundleReference2(rootDir, mcpReference);
11756
+ if (!mcpPath || !existsSync5(mcpPath)) return [];
10983
11757
  try {
10984
- const parsed = JSON.parse(readFileSync3(mcpPath, "utf-8"));
11758
+ const parsed = JSON.parse(readFileSync4(mcpPath, "utf-8"));
10985
11759
  const issues = /* @__PURE__ */ new Set();
10986
11760
  for (const [serverName, server] of Object.entries(parsed.mcpServers ?? {})) {
10987
11761
  if (!server || typeof server !== "object") continue;
@@ -10992,9 +11766,9 @@ function findInstalledRuntimeScriptIssues(rootDir, manifest) {
10992
11766
  ...args
10993
11767
  ].flatMap(extractBundleCommandTargets);
10994
11768
  for (const target of commandTargets) {
10995
- const resolved = resolveBundleReference(rootDir, target);
10996
- if (!resolved || !existsSync3(resolved) || !resolved.endsWith(".sh")) continue;
10997
- const content = readFileSync3(resolved, "utf-8");
11769
+ const resolved = resolveBundleReference2(rootDir, target);
11770
+ if (!resolved || !existsSync5(resolved) || !resolved.endsWith(".sh")) continue;
11771
+ const content = readFileSync4(resolved, "utf-8");
10998
11772
  if (!content.includes("check-env.sh")) continue;
10999
11773
  const relativePath = resolved.startsWith(`${rootDir}/`) ? resolved.slice(rootDir.length + 1) : resolved;
11000
11774
  issues.add(`runtime script ${relativePath} for MCP server "${serverName}" still references installer-owned scripts/check-env.sh`);
@@ -11009,12 +11783,12 @@ function planInstallPlugin(distDir, pluginName, platforms) {
11009
11783
  const targets = getInstallTargets(pluginName);
11010
11784
  const filtered = platforms ? targets.filter((t) => platforms.includes(t.platform)) : targets;
11011
11785
  return filtered.map((target) => {
11012
- const sourceDir = resolve3(distDir, target.platform);
11786
+ const sourceDir = resolve5(distDir, target.platform);
11013
11787
  return {
11014
11788
  ...target,
11015
11789
  sourceDir,
11016
- built: existsSync3(sourceDir),
11017
- existing: existsSync3(target.pluginDir)
11790
+ built: existsSync5(sourceDir),
11791
+ existing: existsSync5(target.pluginDir)
11018
11792
  };
11019
11793
  });
11020
11794
  }
@@ -11156,7 +11930,7 @@ var MUTATING_PREFIX_PATTERN = new RegExp(`^(${MUTATING_PREFIXES.join("|")})\\b`,
11156
11930
  // src/codex-hooks-feature.ts
11157
11931
  var RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG = "hooks";
11158
11932
  var ALTERNATE_CODEX_HOOKS_FEATURE_FLAG = "codex_hooks";
11159
- var PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG = "plugin_hooks";
11933
+ var PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG = "hooks";
11160
11934
  function getCodexHooksFeatureState(features) {
11161
11935
  if (!features) {
11162
11936
  return {
@@ -11334,12 +12108,119 @@ function splitTomlDottedPath(value) {
11334
12108
  var MATERIALIZED_ENV_MARKER = "materialized required config";
11335
12109
  var MIN_NODE_MAJOR = 18;
11336
12110
  var STDIO_LAUNCH_SMOKE_TIMEOUT_MS = 1200;
12111
+ var MAX_SECRET_SCAN_FILE_BYTES = 512 * 1024;
12112
+ var LEGACY_SECRET_IDENTIFIER_PATTERN = /(^|[-_])(api[-_]?key|secret|token|password|credential)([-_]|$)/i;
12113
+ var TEST_SECRET_SENTINELS = [
12114
+ { pattern: /\bshh-secret\b/i, label: "test secret sentinel" },
12115
+ { pattern: /\bsecret-key\b/i, label: "test secret sentinel" },
12116
+ { pattern: /\bliteral-secret-value-that-should-not-copy\b/i, label: "test secret sentinel" }
12117
+ ];
11337
12118
  function renderInstalledPluginRoot(value, rootDir) {
11338
12119
  return value.replaceAll("${PLUGIN_ROOT}", rootDir).replaceAll("${CLAUDE_PLUGIN_ROOT}", rootDir).replaceAll("${CURSOR_PLUGIN_ROOT}", rootDir).replaceAll("${CODEX_PLUGIN_ROOT}", rootDir).replaceAll("${OPENCODE_PLUGIN_ROOT}", rootDir);
11339
12120
  }
11340
12121
  function addCheck(checks, check) {
11341
12122
  checks.push(check);
11342
12123
  }
12124
+ function looksLegacySecretIdentifier(value) {
12125
+ return LEGACY_SECRET_IDENTIFIER_PATTERN.test(
12126
+ value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[._/\s]+/g, "-")
12127
+ );
12128
+ }
12129
+ function walkInstalledBundleFiles(rootDir, currentDir = rootDir) {
12130
+ const files = [];
12131
+ for (const entry of readdirSync3(currentDir, { withFileTypes: true })) {
12132
+ const absolutePath = resolve6(currentDir, entry.name);
12133
+ if (entry.isDirectory()) {
12134
+ files.push(...walkInstalledBundleFiles(rootDir, absolutePath));
12135
+ continue;
12136
+ }
12137
+ if (!entry.isFile()) continue;
12138
+ files.push(relative3(rootDir, absolutePath).replace(/\\/g, "/"));
12139
+ }
12140
+ return files.sort();
12141
+ }
12142
+ function readInstalledTextFile(rootDir, relativePath) {
12143
+ const absolutePath = resolve6(rootDir, relativePath);
12144
+ const content = readFileSync5(absolutePath);
12145
+ if (content.length > MAX_SECRET_SCAN_FILE_BYTES) return null;
12146
+ if (content.includes(0)) return null;
12147
+ return content.toString("utf-8");
12148
+ }
12149
+ function collectInstalledPlaintextSecretCandidates(rootDir) {
12150
+ const userConfigPath = resolve6(rootDir, ".pluxx-user.json");
12151
+ if (!existsSync6(userConfigPath)) return [];
12152
+ try {
12153
+ const payload = JSON.parse(readFileSync5(userConfigPath, "utf-8"));
12154
+ if (payload.secretStorage === "materialized") return [];
12155
+ const secretKeys = new Set(Array.isArray(payload.secretKeys) ? payload.secretKeys.filter((value) => typeof value === "string") : []);
12156
+ const secretEnv = new Set(Array.isArray(payload.secretEnv) ? payload.secretEnv.filter((value) => typeof value === "string") : []);
12157
+ const hasSecretMetadata = secretKeys.size > 0 || secretEnv.size > 0;
12158
+ const candidateLabels = /* @__PURE__ */ new Map();
12159
+ const recordCandidate = (rawValue, label) => {
12160
+ if (typeof rawValue !== "string") return;
12161
+ const value = rawValue.trim();
12162
+ if (!value) return;
12163
+ if (extractEnvReference(value)) return;
12164
+ if (isPlaceholderSecretValue(value)) return;
12165
+ const labels = candidateLabels.get(value) ?? /* @__PURE__ */ new Set();
12166
+ labels.add(label);
12167
+ candidateLabels.set(value, labels);
12168
+ };
12169
+ for (const [key, value] of Object.entries(payload.values ?? {})) {
12170
+ if (hasSecretMetadata ? !secretKeys.has(key) : !looksLegacySecretIdentifier(key)) continue;
12171
+ recordCandidate(value, `userConfig "${key}"`);
12172
+ }
12173
+ for (const [key, value] of Object.entries(payload.env ?? {})) {
12174
+ if (hasSecretMetadata ? !secretEnv.has(key) : !looksLegacySecretIdentifier(key)) continue;
12175
+ recordCandidate(value, `env "${key}"`);
12176
+ }
12177
+ return [...candidateLabels.entries()].map(([value, labels]) => ({
12178
+ value,
12179
+ labels: [...labels].sort()
12180
+ }));
12181
+ } catch {
12182
+ return [];
12183
+ }
12184
+ }
12185
+ function checkInstalledPlaintextSecrets(checks, rootDir) {
12186
+ const literalCandidates = collectInstalledPlaintextSecretCandidates(rootDir);
12187
+ const findings = /* @__PURE__ */ new Map();
12188
+ for (const relativePath of walkInstalledBundleFiles(rootDir)) {
12189
+ const content = readInstalledTextFile(rootDir, relativePath);
12190
+ if (!content) continue;
12191
+ const labels = /* @__PURE__ */ new Set();
12192
+ for (const candidate of literalCandidates) {
12193
+ if (!content.includes(candidate.value)) continue;
12194
+ for (const label of candidate.labels) labels.add(label);
12195
+ }
12196
+ for (const sentinel of TEST_SECRET_SENTINELS) {
12197
+ if (sentinel.pattern.test(content)) {
12198
+ labels.add(sentinel.label);
12199
+ }
12200
+ }
12201
+ if (labels.size > 0) {
12202
+ findings.set(relativePath, labels);
12203
+ }
12204
+ }
12205
+ if (findings.size === 0) {
12206
+ addCheck(checks, {
12207
+ level: "success",
12208
+ code: "consumer-plaintext-secret-absent",
12209
+ title: "Installed bundle does not expose known plaintext secret material",
12210
+ detail: "No known prompted secret values or maintained test-secret sentinels were detected in scanned installed text files.",
12211
+ fix: "No action needed."
12212
+ });
12213
+ return;
12214
+ }
12215
+ const detail = [...findings.entries()].map(([path, labels]) => `${path} (${[...labels].sort().join(", ")})`).join("; ");
12216
+ addCheck(checks, {
12217
+ level: "error",
12218
+ code: "consumer-plaintext-secret-leak",
12219
+ title: "Installed bundle contains plaintext secret material",
12220
+ detail: `Detected plaintext secret material in installed files: ${detail}.`,
12221
+ fix: "Rotate the affected credential, remove the leaking install, reinstall after the Pluxx secret-reference fix, and rerun pluxx doctor --consumer."
12222
+ });
12223
+ }
11343
12224
  function summarizeChecks(checks) {
11344
12225
  const visibleChecks = checks.filter((check) => !check.code.startsWith("primitive-"));
11345
12226
  const errors = visibleChecks.filter((check) => check.level === "error").length;
@@ -11381,7 +12262,7 @@ function addRuntimeChecks(checks, mode) {
11381
12262
  }
11382
12263
  }
11383
12264
  function detectConsumerLayout(rootDir) {
11384
- if (existsSync4(resolve4(rootDir, ".claude-plugin/plugin.json"))) {
12265
+ if (existsSync6(resolve6(rootDir, ".claude-plugin/plugin.json"))) {
11385
12266
  return {
11386
12267
  kind: "installed-platform",
11387
12268
  platform: "claude-code",
@@ -11389,7 +12270,7 @@ function detectConsumerLayout(rootDir) {
11389
12270
  mcpConfigPath: ".mcp.json"
11390
12271
  };
11391
12272
  }
11392
- if (existsSync4(resolve4(rootDir, ".cursor-plugin/plugin.json"))) {
12273
+ if (existsSync6(resolve6(rootDir, ".cursor-plugin/plugin.json"))) {
11393
12274
  return {
11394
12275
  kind: "installed-platform",
11395
12276
  platform: "cursor",
@@ -11397,7 +12278,7 @@ function detectConsumerLayout(rootDir) {
11397
12278
  mcpConfigPath: "mcp.json"
11398
12279
  };
11399
12280
  }
11400
- if (existsSync4(resolve4(rootDir, ".codex-plugin/plugin.json"))) {
12281
+ if (existsSync6(resolve6(rootDir, ".codex-plugin/plugin.json"))) {
11401
12282
  return {
11402
12283
  kind: "installed-platform",
11403
12284
  platform: "codex",
@@ -11405,11 +12286,11 @@ function detectConsumerLayout(rootDir) {
11405
12286
  mcpConfigPath: ".mcp.json"
11406
12287
  };
11407
12288
  }
11408
- const packagePath = resolve4(rootDir, "package.json");
11409
- const indexPath = resolve4(rootDir, "index.ts");
11410
- if (existsSync4(packagePath) && existsSync4(indexPath)) {
12289
+ const packagePath = resolve6(rootDir, "package.json");
12290
+ const indexPath = resolve6(rootDir, "index.ts");
12291
+ if (existsSync6(packagePath) && existsSync6(indexPath)) {
11411
12292
  try {
11412
- const pkg = JSON.parse(readFileSync4(packagePath, "utf-8"));
12293
+ const pkg = JSON.parse(readFileSync5(packagePath, "utf-8"));
11413
12294
  if (pkg.peerDependencies?.["@opencode-ai/plugin"] || pkg.keywords?.includes("opencode-plugin")) {
11414
12295
  return {
11415
12296
  kind: "installed-platform",
@@ -11425,22 +12306,22 @@ function detectConsumerLayout(rootDir) {
11425
12306
  };
11426
12307
  }
11427
12308
  }
11428
- if (CONFIG_FILES.some((filename) => existsSync4(resolve4(rootDir, filename)))) {
12309
+ if (CONFIG_FILES.some((filename) => existsSync6(resolve6(rootDir, filename)))) {
11429
12310
  return { kind: "source-project" };
11430
12311
  }
11431
- if (["claude-code", "cursor", "codex", "opencode"].some((dir) => existsSync4(resolve4(rootDir, dir)))) {
12312
+ if (["claude-code", "cursor", "codex", "opencode"].some((dir) => existsSync6(resolve6(rootDir, dir)))) {
11432
12313
  return { kind: "multi-target-dist" };
11433
12314
  }
11434
12315
  return { kind: "unknown" };
11435
12316
  }
11436
12317
  function readJsonFile(rootDir, relativePath) {
11437
- return JSON.parse(readFileSync4(resolve4(rootDir, relativePath), "utf-8"));
12318
+ return JSON.parse(readFileSync5(resolve6(rootDir, relativePath), "utf-8"));
11438
12319
  }
11439
12320
  function listCodexConfigCandidates(projectRoot) {
11440
- const projectCandidate = resolve4(projectRoot ?? process.cwd(), ".codex/config.toml");
11441
- const homeDir = process.env.HOME?.trim() || homedir();
11442
- const codexHome = process.env.CODEX_HOME?.trim() || resolve4(homeDir, ".codex");
11443
- const userCandidate = resolve4(codexHome, "config.toml");
12321
+ const projectCandidate = resolve6(projectRoot ?? process.cwd(), ".codex/config.toml");
12322
+ const homeDir = process.env.HOME?.trim() || homedir2();
12323
+ const codexHome = process.env.CODEX_HOME?.trim() || resolve6(homeDir, ".codex");
12324
+ const userCandidate = resolve6(codexHome, "config.toml");
11444
12325
  const seen = /* @__PURE__ */ new Set();
11445
12326
  const candidates = [];
11446
12327
  for (const candidate of [
@@ -11455,7 +12336,7 @@ function listCodexConfigCandidates(projectRoot) {
11455
12336
  }
11456
12337
  function readCodexHooksFeatureFlag(filePath) {
11457
12338
  let inFeaturesTable = false;
11458
- const lines = readFileSync4(filePath, "utf-8").split(/\r?\n/);
12339
+ const lines = readFileSync5(filePath, "utf-8").split(/\r?\n/);
11459
12340
  let pluginBundled;
11460
12341
  let recommended;
11461
12342
  let alternate;
@@ -11474,7 +12355,7 @@ function readCodexHooksFeatureFlag(filePath) {
11474
12355
  inFeaturesTable = sectionMatch[1].trim() === "features";
11475
12356
  continue;
11476
12357
  }
11477
- const dottedFeatureMatch = line.match(/^features\.(plugin_hooks|hooks|codex_hooks)\s*=\s*(.+)$/);
12358
+ const dottedFeatureMatch = line.match(/^features\.(hooks|codex_hooks)\s*=\s*(.+)$/);
11478
12359
  if (dottedFeatureMatch) {
11479
12360
  assignFeatureFlag(dottedFeatureMatch[1], dottedFeatureMatch[2]);
11480
12361
  continue;
@@ -11491,7 +12372,7 @@ function readCodexHooksFeatureFlag(filePath) {
11491
12372
  continue;
11492
12373
  }
11493
12374
  if (!inFeaturesTable) continue;
11494
- const featureMatch = line.match(/^(plugin_hooks|hooks|codex_hooks)\s*=\s*(.+)$/);
12375
+ const featureMatch = line.match(/^(hooks|codex_hooks)\s*=\s*(.+)$/);
11495
12376
  if (!featureMatch) continue;
11496
12377
  assignFeatureFlag(featureMatch[1], featureMatch[2]);
11497
12378
  }
@@ -11504,7 +12385,7 @@ function readCodexHooksFeatureFlag(filePath) {
11504
12385
  }
11505
12386
  function probeCodexHooksFeatureFlags(projectRoot) {
11506
12387
  return listCodexConfigCandidates(projectRoot).map((candidate) => {
11507
- if (!existsSync4(candidate.path)) {
12388
+ if (!existsSync6(candidate.path)) {
11508
12389
  return {
11509
12390
  ...candidate,
11510
12391
  exists: false,
@@ -11539,7 +12420,7 @@ function probeCodexHooksFeatureFlags(projectRoot) {
11539
12420
  }
11540
12421
  function probeCodexMcpApprovalEntries(projectRoot) {
11541
12422
  return listCodexConfigCandidates(projectRoot).map((candidate) => {
11542
- if (!existsSync4(candidate.path)) {
12423
+ if (!existsSync6(candidate.path)) {
11543
12424
  return {
11544
12425
  ...candidate,
11545
12426
  exists: false,
@@ -11550,7 +12431,7 @@ function probeCodexMcpApprovalEntries(projectRoot) {
11550
12431
  return {
11551
12432
  ...candidate,
11552
12433
  exists: true,
11553
- approvals: parseCodexApprovedMcpToolsFromToml(readFileSync4(candidate.path, "utf-8"))
12434
+ approvals: parseCodexApprovedMcpToolsFromToml(readFileSync5(candidate.path, "utf-8"))
11554
12435
  };
11555
12436
  } catch (error) {
11556
12437
  return {
@@ -11565,7 +12446,7 @@ function probeCodexMcpApprovalEntries(projectRoot) {
11565
12446
  function getCodexProjectPathCandidates(projectRoot) {
11566
12447
  if (!projectRoot) return [];
11567
12448
  const candidates = /* @__PURE__ */ new Set();
11568
- const resolvedProjectRoot = resolve4(projectRoot);
12449
+ const resolvedProjectRoot = resolve6(projectRoot);
11569
12450
  candidates.add(resolvedProjectRoot);
11570
12451
  try {
11571
12452
  candidates.add(realpathSync(resolvedProjectRoot));
@@ -11585,7 +12466,7 @@ function parseCodexProjectKeySegment(segment) {
11585
12466
  function readCodexProjectTrust(filePath, projectPaths) {
11586
12467
  const matchedPaths = /* @__PURE__ */ new Set();
11587
12468
  let currentProjectKey = null;
11588
- const lines = readFileSync4(filePath, "utf-8").split(/\r?\n/);
12469
+ const lines = readFileSync5(filePath, "utf-8").split(/\r?\n/);
11589
12470
  for (const rawLine of lines) {
11590
12471
  const line = stripTomlComment(rawLine).trim();
11591
12472
  if (!line) continue;
@@ -11622,7 +12503,7 @@ function probeCodexProjectTrust(projectRoot) {
11622
12503
  if (projectPaths.length === 0) return void 0;
11623
12504
  const userConfig = listCodexConfigCandidates(projectRoot).find((candidate) => candidate.scope === "user");
11624
12505
  if (!userConfig) return void 0;
11625
- if (!existsSync4(userConfig.path)) {
12506
+ if (!existsSync6(userConfig.path)) {
11626
12507
  return {
11627
12508
  path: userConfig.path,
11628
12509
  exists: false,
@@ -11661,16 +12542,16 @@ function getClaudeManagedSettingsPath() {
11661
12542
  }
11662
12543
  }
11663
12544
  function listClaudeSettingsCandidates(projectRoot) {
11664
- const homeDir = process.env.HOME?.trim() || homedir();
12545
+ const homeDir = process.env.HOME?.trim() || homedir2();
11665
12546
  const candidates = [];
11666
12547
  const managedPath = getClaudeManagedSettingsPath();
11667
12548
  if (managedPath) {
11668
12549
  candidates.push({ path: managedPath, scope: "managed" });
11669
12550
  }
11670
- candidates.push({ path: resolve4(homeDir, ".claude/settings.json"), scope: "user" });
12551
+ candidates.push({ path: resolve6(homeDir, ".claude/settings.json"), scope: "user" });
11671
12552
  if (projectRoot) {
11672
- candidates.push({ path: resolve4(projectRoot, ".claude/settings.json"), scope: "project" });
11673
- candidates.push({ path: resolve4(projectRoot, ".claude/settings.local.json"), scope: "local" });
12553
+ candidates.push({ path: resolve6(projectRoot, ".claude/settings.json"), scope: "project" });
12554
+ candidates.push({ path: resolve6(projectRoot, ".claude/settings.local.json"), scope: "local" });
11674
12555
  }
11675
12556
  const seen = /* @__PURE__ */ new Set();
11676
12557
  return candidates.filter((candidate) => {
@@ -11685,7 +12566,7 @@ function readClaudeDisableAllHooks(filePath) {
11685
12566
  }
11686
12567
  function probeClaudeDisableAllHooks(projectRoot) {
11687
12568
  return listClaudeSettingsCandidates(projectRoot).map((candidate) => {
11688
- if (!existsSync4(candidate.path)) {
12569
+ if (!existsSync6(candidate.path)) {
11689
12570
  return {
11690
12571
  ...candidate,
11691
12572
  exists: false,
@@ -11709,7 +12590,7 @@ function probeClaudeDisableAllHooks(projectRoot) {
11709
12590
  });
11710
12591
  }
11711
12592
  function getInstalledClaudeHooksReference(rootDir, manifest) {
11712
- if (existsSync4(resolve4(rootDir, "hooks/hooks.json"))) {
12593
+ if (existsSync6(resolve6(rootDir, "hooks/hooks.json"))) {
11713
12594
  return "./hooks/hooks.json";
11714
12595
  }
11715
12596
  const manifestReference = typeof manifest.hooks === "string" ? manifest.hooks : void 0;
@@ -11717,7 +12598,7 @@ function getInstalledClaudeHooksReference(rootDir, manifest) {
11717
12598
  }
11718
12599
  function checkConsumerBundlePath(checks, rootDir) {
11719
12600
  try {
11720
- accessSync(rootDir, constants.R_OK);
12601
+ accessSync2(rootDir, constants2.R_OK);
11721
12602
  const details = lstatSync(rootDir);
11722
12603
  addCheck(checks, {
11723
12604
  level: "success",
@@ -11764,8 +12645,8 @@ function checkConsumerManifest(checks, rootDir, layout) {
11764
12645
  }
11765
12646
  function checkInstalledUserConfig(checks, rootDir) {
11766
12647
  const userConfigPath = ".pluxx-user.json";
11767
- const resolvedPath = resolve4(rootDir, userConfigPath);
11768
- if (!existsSync4(resolvedPath)) {
12648
+ const resolvedPath = resolve6(rootDir, userConfigPath);
12649
+ if (!existsSync6(resolvedPath)) {
11769
12650
  addCheck(checks, {
11770
12651
  level: "info",
11771
12652
  code: "consumer-user-config-missing",
@@ -11777,7 +12658,7 @@ function checkInstalledUserConfig(checks, rootDir) {
11777
12658
  return;
11778
12659
  }
11779
12660
  try {
11780
- const payload = JSON.parse(readFileSync4(resolvedPath, "utf-8"));
12661
+ const payload = JSON.parse(readFileSync5(resolvedPath, "utf-8"));
11781
12662
  const valueCount = Object.keys(payload.values ?? {}).length;
11782
12663
  const envCount = Object.keys(payload.env ?? {}).length;
11783
12664
  const placeholderKeys = [
@@ -11815,8 +12696,8 @@ function checkInstalledUserConfig(checks, rootDir) {
11815
12696
  }
11816
12697
  function checkInstalledEnvValidation(checks, rootDir) {
11817
12698
  const envScriptPath = INSTALLER_OWNED_CHECK_ENV_PATH;
11818
- const resolvedPath = resolve4(rootDir, envScriptPath);
11819
- if (!existsSync4(resolvedPath)) {
12699
+ const resolvedPath = resolve6(rootDir, envScriptPath);
12700
+ if (!existsSync6(resolvedPath)) {
11820
12701
  addCheck(checks, {
11821
12702
  level: "info",
11822
12703
  code: "consumer-env-script-missing",
@@ -11827,7 +12708,7 @@ function checkInstalledEnvValidation(checks, rootDir) {
11827
12708
  });
11828
12709
  return;
11829
12710
  }
11830
- const content = readFileSync4(resolvedPath, "utf-8");
12711
+ const content = readFileSync5(resolvedPath, "utf-8");
11831
12712
  if (content.includes(MATERIALIZED_ENV_MARKER)) {
11832
12713
  addCheck(checks, {
11833
12714
  level: "success",
@@ -11855,7 +12736,7 @@ function checkInstalledRuntimeScriptRoles(checks, rootDir) {
11855
12736
  "scripts/bootstrap-runtime.sh",
11856
12737
  "scripts/start-mcp.sh"
11857
12738
  ];
11858
- const presentRoles = roleFiles.filter((relativePath) => existsSync4(resolve4(rootDir, relativePath))).map((relativePath) => getRuntimeScriptRoleForPath(relativePath)).filter((role) => role !== null);
12739
+ const presentRoles = roleFiles.filter((relativePath) => existsSync6(resolve6(rootDir, relativePath))).map((relativePath) => getRuntimeScriptRoleForPath(relativePath)).filter((role) => role !== null);
11859
12740
  if (presentRoles.length === 0) return;
11860
12741
  addCheck(checks, {
11861
12742
  level: "info",
@@ -11878,8 +12759,8 @@ async function checkInstalledMcpConfig(checks, rootDir, layout) {
11878
12759
  });
11879
12760
  return;
11880
12761
  }
11881
- const resolvedPath = resolve4(rootDir, layout.mcpConfigPath);
11882
- if (!existsSync4(resolvedPath)) {
12762
+ const resolvedPath = resolve6(rootDir, layout.mcpConfigPath);
12763
+ if (!existsSync6(resolvedPath)) {
11883
12764
  addCheck(checks, {
11884
12765
  level: "info",
11885
12766
  code: "consumer-mcp-config-missing",
@@ -12110,20 +12991,20 @@ function checkInstalledCodexHooksFeatureFlag(checks, rootDir, layout, options) {
12110
12991
  const enabledProbes = probes.filter((probe) => probe.enabled);
12111
12992
  const checkedPaths = probes.map((probe) => `${probe.scope} ${probe.exists ? "config" : "path"}: ${probe.path}${probe.exists ? "" : " (missing)"}`).join("; ");
12112
12993
  const describeEnabledFlags = (probe) => {
12113
- const enabledFlags = [];
12114
- if (probe.pluginBundledEnabled) enabledFlags.push(PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG);
12115
- if (probe.recommendedEnabled) enabledFlags.push(RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG);
12116
- if (probe.alternateEnabled) enabledFlags.push(ALTERNATE_CODEX_HOOKS_FEATURE_FLAG);
12117
- return enabledFlags.join(" + ");
12994
+ const enabledFlags = /* @__PURE__ */ new Set();
12995
+ if (probe.pluginBundledEnabled) enabledFlags.add(PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG);
12996
+ if (probe.recommendedEnabled) enabledFlags.add(RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG);
12997
+ if (probe.alternateEnabled) enabledFlags.add(ALTERNATE_CODEX_HOOKS_FEATURE_FLAG);
12998
+ return [...enabledFlags].join(" + ");
12118
12999
  };
12119
13000
  const generalOnlyProbes = probes.filter((probe) => !probe.pluginBundledEnabled && (probe.recommendedEnabled || probe.alternateEnabled));
12120
13001
  if (generalOnlyProbes.length > 0) {
12121
13002
  addCheck(checks, {
12122
13003
  level: "warning",
12123
13004
  code: "consumer-codex-plugin-hooks-feature-flag-general-only",
12124
- title: "Codex config enables only general hook flags, not the plugin-bundled hook gate",
12125
- detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, but ${generalOnlyProbes.map((probe) => `${probe.scope} config ${probe.path}`).join(" and ")} enables only general hook flags (${generalOnlyProbes.map((probe) => describeEnabledFlags(probe)).join(" and ")}). Those general flags do not activate plugin-bundled hooks by themselves.`,
12126
- fix: `Enable \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` in the active Codex config, reload Codex, and rerun pluxx verify-install.`,
13005
+ title: "Codex config enables only deprecated hook flags",
13006
+ detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, but ${generalOnlyProbes.map((probe) => `${probe.scope} config ${probe.path}`).join(" and ")} enables only deprecated hook flags (${generalOnlyProbes.map((probe) => describeEnabledFlags(probe)).join(" and ")}). Current Codex docs use \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG}\` as the canonical feature key.`,
13007
+ fix: `Enable \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` in the active Codex config, reload Codex, and rerun pluxx verify-install.`,
12127
13008
  path: generalOnlyProbes[0].path
12128
13009
  });
12129
13010
  }
@@ -12133,8 +13014,8 @@ function checkInstalledCodexHooksFeatureFlag(checks, rootDir, layout, options) {
12133
13014
  level: "warning",
12134
13015
  code: "consumer-codex-hooks-feature-flag-legacy-only",
12135
13016
  title: "Codex config still uses the deprecated general hook compatibility flag",
12136
- detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, and the checked Codex config enables only the deprecated general hook flag \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG} = true\` in ${legacyOnlyProbes.map((probe) => `${probe.scope} config ${probe.path}`).join(" and ")}. That flag is not the plugin-bundled hook gate.`,
12137
- fix: `Prefer \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` only for non-plugin hook config if needed, and use \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` for plugin-bundled hooks. Reload Codex and rerun pluxx verify-install after updating the active config.`,
13017
+ detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, and the checked Codex config enables only the deprecated hook flag \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG} = true\` in ${legacyOnlyProbes.map((probe) => `${probe.scope} config ${probe.path}`).join(" and ")}. Current Codex docs use \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG}\` as the canonical feature key.`,
13018
+ fix: `Use \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\`, reload Codex, and rerun pluxx verify-install after updating the active config.`,
12138
13019
  path: legacyOnlyProbes[0].path
12139
13020
  });
12140
13021
  }
@@ -12142,8 +13023,8 @@ function checkInstalledCodexHooksFeatureFlag(checks, rootDir, layout, options) {
12142
13023
  addCheck(checks, {
12143
13024
  level: "success",
12144
13025
  code: "consumer-codex-hooks-feature-flag-enabled",
12145
- title: "Codex plugin-bundled hook feature flag found for this install",
12146
- detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, and the plugin hook gate was found in ${enabledProbes.map((probe) => `${probe.scope} config ${probe.path} (${describeEnabledFlags(probe)})`).join(" and ")}. Treat that as a prerequisite, not proof of live hook execution.`,
13026
+ title: "Codex hook feature flag found for this install",
13027
+ detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, and the canonical hook feature key was found in ${enabledProbes.map((probe) => `${probe.scope} config ${probe.path} (${describeEnabledFlags(probe)})`).join(" and ")}. Treat that as a prerequisite, not proof of live hook execution.`,
12147
13028
  fix: "No action needed.",
12148
13029
  path: enabledProbes[0].path
12149
13030
  });
@@ -12154,8 +13035,8 @@ function checkInstalledCodexHooksFeatureFlag(checks, rootDir, layout, options) {
12154
13035
  level: "warning",
12155
13036
  code: "consumer-codex-hooks-feature-flag-missing",
12156
13037
  title: "Codex plugin-bundled hook activation is missing its known feature-gate prerequisite",
12157
- detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, but \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` was not found in the checked Codex config layers. The general \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG}\` flag covers non-plugin hook config and defaults on, while \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` is deprecated and should not be treated as a plugin-bundled hook fallback. Checked ${checkedPaths}.${parseErrors.length > 0 ? ` Unparseable config: ${parseErrors.join("; ")}.` : ""}`,
12158
- fix: `Enable \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` in the relevant project or user Codex config, reload Codex, and rerun pluxx verify-install.`,
13038
+ detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, but \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` was not found in the checked Codex config layers. \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` is deprecated and should not be treated as the current hook feature key. Checked ${checkedPaths}.${parseErrors.length > 0 ? ` Unparseable config: ${parseErrors.join("; ")}.` : ""}`,
13039
+ fix: `Enable \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` in the relevant project or user Codex config, reload Codex, and rerun pluxx verify-install.`,
12159
13040
  path: probes.find((probe) => probe.exists)?.path ?? hooksReference
12160
13041
  });
12161
13042
  }
@@ -12199,12 +13080,12 @@ function checkInstalledCodexProjectTrust(checks, rootDir, layout, options) {
12199
13080
  function checkInstalledCodexPermissionCompanion(checks, rootDir, layout, options) {
12200
13081
  if (layout.platform !== "codex") return;
12201
13082
  if (checks.some((check) => check.code === "consumer-bundle-integrity-invalid")) return;
12202
- const companionPath = resolve4(rootDir, ".codex/config.generated.toml");
13083
+ const companionPath = resolve6(rootDir, ".codex/config.generated.toml");
12203
13084
  const companionReference = ".codex/config.generated.toml";
12204
- if (!existsSync4(companionPath)) return;
13085
+ if (!existsSync6(companionPath)) return;
12205
13086
  let expectedEntries;
12206
13087
  try {
12207
- expectedEntries = parseCodexApprovedMcpToolsFromToml(readFileSync4(companionPath, "utf-8"));
13088
+ expectedEntries = parseCodexApprovedMcpToolsFromToml(readFileSync5(companionPath, "utf-8"));
12208
13089
  } catch (error) {
12209
13090
  addCheck(checks, {
12210
13091
  level: "error",
@@ -12333,11 +13214,11 @@ function smokeCheckInstalledPermissionHook(rootDir, layout) {
12333
13214
  if (layout.platform !== "claude-code" && layout.platform !== "cursor") {
12334
13215
  return null;
12335
13216
  }
12336
- const scriptPath = resolve4(rootDir, "hooks/pluxx-permissions.mjs");
12337
- if (!existsSync4(scriptPath)) {
13217
+ const scriptPath = resolve6(rootDir, "hooks/pluxx-permissions.mjs");
13218
+ if (!existsSync6(scriptPath)) {
12338
13219
  return null;
12339
13220
  }
12340
- const rules = parseInstalledPermissionRules(readFileSync4(scriptPath, "utf-8"));
13221
+ const rules = parseInstalledPermissionRules(readFileSync5(scriptPath, "utf-8"));
12341
13222
  const selected = /* @__PURE__ */ new Map();
12342
13223
  for (const rule of rules) {
12343
13224
  if (selected.has(rule.action)) continue;
@@ -12423,8 +13304,8 @@ function findMissingInstalledStdioRuntimePaths(rootDir, stdioEntries) {
12423
13304
  const args = Array.isArray(server.args) ? server.args.filter((value) => typeof value === "string") : [];
12424
13305
  for (const candidate of [command, ...args]) {
12425
13306
  if (!candidate || !isLikelyLocalRuntimePath(candidate)) continue;
12426
- const resolvedPath = resolve4(rootDir, candidate);
12427
- if (!existsSync4(resolvedPath)) {
13307
+ const resolvedPath = resolve6(rootDir, candidate);
13308
+ if (!existsSync6(resolvedPath)) {
12428
13309
  missing.add(candidate);
12429
13310
  }
12430
13311
  }
@@ -12535,7 +13416,7 @@ function checkInstalledOpenCodeHostBridge(checks, rootDir) {
12535
13416
  const pluginName = basename2(rootDir);
12536
13417
  const entryPath = `${rootDir}.ts`;
12537
13418
  const entryRelativePath = `${pluginName}.ts`;
12538
- if (!existsSync4(entryPath)) {
13419
+ if (!existsSync6(entryPath)) {
12539
13420
  addCheck(checks, {
12540
13421
  level: "error",
12541
13422
  code: "consumer-opencode-entry-missing",
@@ -12546,7 +13427,7 @@ function checkInstalledOpenCodeHostBridge(checks, rootDir) {
12546
13427
  });
12547
13428
  return;
12548
13429
  }
12549
- const entryContent = readFileSync4(entryPath, "utf-8");
13430
+ const entryContent = readFileSync5(entryPath, "utf-8");
12550
13431
  const expectedImport = `import * as PluginModule from "./${pluginName}/index.ts"`;
12551
13432
  const expectedBridge = `directory: join(context.directory, "${pluginName}")`;
12552
13433
  if (!entryContent.includes(expectedImport) || !entryContent.includes(expectedBridge)) {
@@ -12574,8 +13455,8 @@ function checkInstalledOpenCodeSkills(checks, rootDir) {
12574
13455
  return;
12575
13456
  }
12576
13457
  const pluginName = basename2(rootDir);
12577
- const sourceSkillsDir = resolve4(rootDir, "skills");
12578
- if (!existsSync4(sourceSkillsDir)) {
13458
+ const sourceSkillsDir = resolve6(rootDir, "skills");
13459
+ if (!existsSync6(sourceSkillsDir)) {
12579
13460
  addCheck(checks, {
12580
13461
  level: "info",
12581
13462
  code: "consumer-opencode-skill-sync-not-applicable",
@@ -12586,21 +13467,21 @@ function checkInstalledOpenCodeSkills(checks, rootDir) {
12586
13467
  });
12587
13468
  return;
12588
13469
  }
12589
- const skillRoot = resolve4(dirname2(dirname2(rootDir)), "skills");
13470
+ const skillRoot = resolve6(dirname2(dirname2(rootDir)), "skills");
12590
13471
  const missingSkills = [];
12591
13472
  const malformedSkills = [];
12592
13473
  let expectedSkillCount = 0;
12593
- for (const entry of readdirSync2(sourceSkillsDir, { withFileTypes: true })) {
13474
+ for (const entry of readdirSync3(sourceSkillsDir, { withFileTypes: true })) {
12594
13475
  if (!entry.isDirectory()) continue;
12595
- const sourceSkillPath = resolve4(sourceSkillsDir, entry.name, "SKILL.md");
12596
- if (!existsSync4(sourceSkillPath)) continue;
13476
+ const sourceSkillPath = resolve6(sourceSkillsDir, entry.name, "SKILL.md");
13477
+ if (!existsSync6(sourceSkillPath)) continue;
12597
13478
  expectedSkillCount++;
12598
- const installedSkillPath = resolve4(skillRoot, `${pluginName}-${entry.name}`, "SKILL.md");
12599
- if (!existsSync4(installedSkillPath)) {
13479
+ const installedSkillPath = resolve6(skillRoot, `${pluginName}-${entry.name}`, "SKILL.md");
13480
+ if (!existsSync6(installedSkillPath)) {
12600
13481
  missingSkills.push(`${pluginName}-${entry.name}`);
12601
13482
  continue;
12602
13483
  }
12603
- const installedContent = readFileSync4(installedSkillPath, "utf-8");
13484
+ const installedContent = readFileSync5(installedSkillPath, "utf-8");
12604
13485
  if (!installedContent.includes(`${pluginName}/`)) {
12605
13486
  malformedSkills.push(`${pluginName}-${entry.name}`);
12606
13487
  }
@@ -12689,6 +13570,7 @@ async function doctorConsumer(rootDir = process.cwd(), options = {}) {
12689
13570
  checkInstalledCodexPermissionCompanion(checks, rootDir, layout, options);
12690
13571
  checkInstalledPermissionHook(checks, rootDir, layout);
12691
13572
  checkInstalledUserConfig(checks, rootDir);
13573
+ checkInstalledPlaintextSecrets(checks, rootDir);
12692
13574
  checkInstalledEnvValidation(checks, rootDir);
12693
13575
  checkInstalledRuntimeScriptRoles(checks, rootDir);
12694
13576
  await checkInstalledMcpConfig(checks, rootDir, layout);
@@ -12702,7 +13584,7 @@ async function doctorConsumer(rootDir = process.cwd(), options = {}) {
12702
13584
  // src/cli/verify-install.ts
12703
13585
  function buildCheckFromReport(target, pluginName, report) {
12704
13586
  const consumerPath = resolveInstalledConsumerPath(target, pluginName);
12705
- const staleReason = target.built && existsSync5(consumerPath) ? detectStaleInstall(target, pluginName, consumerPath) : void 0;
13587
+ const staleReason = target.built && existsSync7(consumerPath) ? detectStaleInstall(target, pluginName, consumerPath) : void 0;
12706
13588
  const stale = staleReason !== void 0;
12707
13589
  const issues = listVerifyInstallIssues(report.checks);
12708
13590
  return {
@@ -12710,7 +13592,7 @@ function buildCheckFromReport(target, pluginName, report) {
12710
13592
  installPath: consumerPath,
12711
13593
  consumerPath,
12712
13594
  built: target.built,
12713
- installed: existsSync5(consumerPath),
13595
+ installed: existsSync7(consumerPath),
12714
13596
  stale,
12715
13597
  ...staleReason ? { staleReason } : {},
12716
13598
  ok: report.errors === 0 && !stale,
@@ -12749,10 +13631,10 @@ function manifestPathForPlatform2(platform) {
12749
13631
  function readInstalledManifestVersion(rootDir, platform) {
12750
13632
  const manifestPath = manifestPathForPlatform2(platform);
12751
13633
  if (!manifestPath) return void 0;
12752
- const filepath = resolve5(rootDir, manifestPath);
12753
- if (!existsSync5(filepath)) return void 0;
13634
+ const filepath = resolve7(rootDir, manifestPath);
13635
+ if (!existsSync7(filepath)) return void 0;
12754
13636
  try {
12755
- const manifest = JSON.parse(readFileSync5(filepath, "utf-8"));
13637
+ const manifest = JSON.parse(readFileSync6(filepath, "utf-8"));
12756
13638
  return typeof manifest.version === "string" ? manifest.version : void 0;
12757
13639
  } catch {
12758
13640
  return void 0;
@@ -12760,16 +13642,16 @@ function readInstalledManifestVersion(rootDir, platform) {
12760
13642
  }
12761
13643
  function findCodexCacheCandidates(pluginName) {
12762
13644
  const home = process.env.HOME ?? "~";
12763
- const cacheRoot = resolve5(home, ".codex/plugins/cache");
12764
- if (!existsSync5(cacheRoot)) return [];
13645
+ const cacheRoot = resolve7(home, ".codex/plugins/cache");
13646
+ if (!existsSync7(cacheRoot)) return [];
12765
13647
  const candidates = [];
12766
- for (const marketplace of readdirSync3(cacheRoot)) {
12767
- const pluginRoot = resolve5(cacheRoot, marketplace, pluginName);
12768
- if (!existsSync5(pluginRoot)) continue;
12769
- for (const versionDir of readdirSync3(pluginRoot)) {
12770
- const candidatePath = resolve5(pluginRoot, versionDir);
13648
+ for (const marketplace of readdirSync4(cacheRoot)) {
13649
+ const pluginRoot = resolve7(cacheRoot, marketplace, pluginName);
13650
+ if (!existsSync7(pluginRoot)) continue;
13651
+ for (const versionDir of readdirSync4(pluginRoot)) {
13652
+ const candidatePath = resolve7(pluginRoot, versionDir);
12771
13653
  try {
12772
- const stats = statSync(candidatePath);
13654
+ const stats = statSync3(candidatePath);
12773
13655
  if (!stats.isDirectory()) continue;
12774
13656
  candidates.push({
12775
13657
  path: candidatePath,
@@ -12782,11 +13664,55 @@ function findCodexCacheCandidates(pluginName) {
12782
13664
  }
12783
13665
  return candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
12784
13666
  }
12785
- function detectCodexCacheStaleness(pluginName, builtVersion) {
13667
+ function hashInstalledBundle(rootDir) {
13668
+ const hash = createHash2("sha256");
13669
+ const visit = (currentDir, relativePrefix = "") => {
13670
+ for (const entry of readdirSync4(currentDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
13671
+ const absolutePath = resolve7(currentDir, entry.name);
13672
+ const relativePath = relativePrefix ? `${relativePrefix}/${entry.name}` : entry.name;
13673
+ if (entry.isDirectory()) {
13674
+ visit(absolutePath, relativePath);
13675
+ continue;
13676
+ }
13677
+ const stats = lstatSync2(absolutePath);
13678
+ if (stats.isSymbolicLink()) {
13679
+ hash.update(relativePath);
13680
+ hash.update("\0symlink\0");
13681
+ hash.update(readlinkSync(absolutePath));
13682
+ hash.update("\0");
13683
+ continue;
13684
+ }
13685
+ if (!entry.isFile()) continue;
13686
+ hash.update(relativePath);
13687
+ hash.update("\0file\0");
13688
+ hash.update(readFileSync6(absolutePath));
13689
+ hash.update("\0");
13690
+ }
13691
+ };
13692
+ visit(rootDir);
13693
+ return hash.digest("hex");
13694
+ }
13695
+ function detectCodexCacheStaleness(pluginName, builtVersion, consumerPath) {
12786
13696
  if (!builtVersion) return void 0;
12787
13697
  const candidates = findCodexCacheCandidates(pluginName);
12788
13698
  if (candidates.length === 0) return void 0;
12789
- if (candidates.some((candidate) => candidate.version === builtVersion)) return void 0;
13699
+ const matchingVersionCandidates = candidates.filter((candidate) => candidate.version === builtVersion);
13700
+ if (matchingVersionCandidates.length > 0) {
13701
+ let installedSignature;
13702
+ try {
13703
+ installedSignature = hashInstalledBundle(consumerPath);
13704
+ } catch {
13705
+ return void 0;
13706
+ }
13707
+ for (const candidate of matchingVersionCandidates) {
13708
+ try {
13709
+ if (hashInstalledBundle(candidate.path) === installedSignature) return void 0;
13710
+ } catch {
13711
+ }
13712
+ }
13713
+ const newestMatchingVersion = matchingVersionCandidates[0];
13714
+ return `Codex active cache appears stale at ${newestMatchingVersion.path}; cached version ${newestMatchingVersion.version ?? "unknown"} matches the built version but the cached bundle contents do not match the active local install at ${consumerPath}. Use Plugins > Refresh if available, or restart/reinstall Codex to load the current plugin bundle.`;
13715
+ }
12790
13716
  const newest = candidates[0];
12791
13717
  return `Codex active cache appears stale at ${newest.path}; cached version ${newest.version ?? "unknown"} does not match built version ${builtVersion}. Use Plugins > Refresh if available, or restart/reinstall Codex to load the current plugin bundle.`;
12792
13718
  }
@@ -12809,13 +13735,13 @@ function detectStaleInstall(target, pluginName, consumerPath) {
12809
13735
  return `installed version ${installedVersion} does not match built version ${builtVersion}`;
12810
13736
  }
12811
13737
  if (target.platform === "codex") {
12812
- return detectCodexCacheStaleness(pluginName, builtVersion);
13738
+ return detectCodexCacheStaleness(pluginName, builtVersion, consumerPath);
12813
13739
  }
12814
13740
  return void 0;
12815
13741
  }
12816
13742
  async function verifyInstall(config, options = {}) {
12817
13743
  const rootDir = options.rootDir ?? process.cwd();
12818
- const distDir = resolve5(rootDir, config.outDir);
13744
+ const distDir = resolve7(rootDir, config.outDir);
12819
13745
  const targets = options.targets ?? config.targets;
12820
13746
  const installPlan = planInstallPlugin(distDir, config.name, targets);
12821
13747
  const filteredPlan = options.builtOnly ? installPlan.filter((target) => target.built) : installPlan;
@@ -12878,7 +13804,9 @@ function getVerifyInstallRecoveryActions(check) {
12878
13804
  }
12879
13805
  export {
12880
13806
  CORE_FOUR_PRIMITIVE_CAPABILITIES,
13807
+ CORE_HOST_FAMILIES,
12881
13808
  INSTALLER_OWNED_CHECK_ENV_PATH,
13809
+ INSTALL_REFERENCE_SCHEMES,
12882
13810
  PLATFORM_LIMITS,
12883
13811
  PLATFORM_LIMIT_POLICIES,
12884
13812
  PLATFORM_VALIDATION_RULES,
@@ -12886,10 +13814,15 @@ export {
12886
13814
  PLUXX_COMPILER_INTENT_PATH,
12887
13815
  PORTABLE_RUNTIME_SCRIPT_ROLES,
12888
13816
  PluginConfigSchema,
13817
+ assertGeneratedBundlesCurrent,
12889
13818
  buildGeneratedPermissionHookScript,
13819
+ buildHostTargetSelection,
12890
13820
  buildOpenCodePermissionMap,
13821
+ checkGeneratedBundles,
12891
13822
  collectPermissionRules,
12892
13823
  definePlugin,
13824
+ detectHostFamilies,
13825
+ formatInstallReference,
12893
13826
  formatPublishPlan,
12894
13827
  getConfiguredCompilerBuckets,
12895
13828
  getConsumerEnvScriptActiveDetail,
@@ -12905,6 +13838,7 @@ export {
12905
13838
  getRuntimeReadinessCapability,
12906
13839
  getRuntimeReadinessExternalConfigNote,
12907
13840
  getRuntimeReadinessNamedPromptTargetNote,
13841
+ parseInstallReference,
12908
13842
  parsePermissionRule,
12909
13843
  permissionRulesNeedToolLevelDowngrade,
12910
13844
  planPublish,