@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.
- package/README.md +7 -1
- package/bin/pluxx.js +1 -6
- package/dist/agent-translation-registry.d.ts +9 -1
- package/dist/agent-translation-registry.d.ts.map +1 -1
- package/dist/bundle-check.d.ts +21 -0
- package/dist/bundle-check.d.ts.map +1 -0
- package/dist/claude-hook-probe.d.ts.map +1 -1
- package/dist/cli/agent.d.ts.map +1 -1
- package/dist/cli/codex-apply.d.ts +39 -0
- package/dist/cli/codex-apply.d.ts.map +1 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +32414 -7589
- package/dist/cli/install.d.ts.map +1 -1
- package/dist/cli/primitive-summary.d.ts.map +1 -1
- package/dist/cli/publish.d.ts.map +1 -1
- package/dist/cli/sync-from-mcp.d.ts +1 -1
- package/dist/cli/sync-from-mcp.d.ts.map +1 -1
- package/dist/cli/verify-install.d.ts.map +1 -1
- package/dist/codex-hooks-feature.d.ts +1 -1
- package/dist/codex-hooks-feature.d.ts.map +1 -1
- package/dist/codex-mcp-probe.d.ts.map +1 -1
- package/dist/command-translation-registry.d.ts +11 -3
- package/dist/command-translation-registry.d.ts.map +1 -1
- package/dist/compatibility/core-four-primitives.d.ts +5 -0
- package/dist/compatibility/core-four-primitives.d.ts.map +1 -0
- package/dist/compatibility/matrix.d.ts +1 -0
- package/dist/compatibility/matrix.d.ts.map +1 -1
- package/dist/distribution-lifecycle.d.ts +18 -0
- package/dist/distribution-lifecycle.d.ts.map +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/opencode/index.d.ts.map +1 -1
- package/dist/hook-command-env.d.ts +1 -0
- package/dist/hook-command-env.d.ts.map +1 -1
- package/dist/hook-events.d.ts +1 -0
- package/dist/hook-events.d.ts.map +1 -1
- package/dist/host-detection.d.ts +38 -0
- package/dist/host-detection.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1142 -208
- package/dist/install-reference.d.ts +34 -0
- package/dist/install-reference.d.ts.map +1 -0
- package/dist/readiness.d.ts.map +1 -1
- package/dist/schema.d.ts +36 -36
- package/dist/user-config.d.ts +11 -0
- package/dist/user-config.d.ts.map +1 -1
- package/dist/validation/platform-rules.d.ts +1 -0
- package/dist/validation/platform-rules.d.ts.map +1 -1
- 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
|
|
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: "
|
|
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: [
|
|
8307
|
-
notes: "Codex documents both project
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8695
|
-
nativeSurfaces: ["commands/*", "slash commands"]
|
|
8760
|
+
...getCommandPrimitiveCapability("cursor")
|
|
8696
8761
|
},
|
|
8697
8762
|
agents: {
|
|
8698
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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].
|
|
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
|
|
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
|
-
|
|
8779
|
-
nativeSurfaces: ["commands/*.md", "config command definitions"]
|
|
8837
|
+
...getCommandPrimitiveCapability("opencode")
|
|
8780
8838
|
},
|
|
8781
8839
|
agents: {
|
|
8782
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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\\.
|
|
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 && /\\
|
|
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(/^
|
|
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].
|
|
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\\.
|
|
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 (/^
|
|
9954
|
-
lines[index] = '
|
|
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, '
|
|
10200
|
+
if (!updated) lines.splice(start + 1, 0, 'hooks = true')
|
|
9959
10201
|
} else if (topLevelPluginHooksDotted >= 0) {
|
|
9960
|
-
lines[topLevelPluginHooksDotted] = 'features.
|
|
10202
|
+
lines[topLevelPluginHooksDotted] = 'features.hooks = true'
|
|
9961
10203
|
} else if (firstTopLevelFeaturesDotted >= 0) {
|
|
9962
|
-
lines.splice(firstTopLevelFeaturesDotted + 1, 0, 'features.
|
|
10204
|
+
lines.splice(firstTopLevelFeaturesDotted + 1, 0, 'features.hooks = true')
|
|
9963
10205
|
} else if (topLevelInlineFeatures >= 0 && lines[topLevelInlineFeatures].includes('}')) {
|
|
9964
|
-
if (/\\
|
|
10206
|
+
if (/\\bhooks\\s*=/.test(lines[topLevelInlineFeatures])) {
|
|
9965
10207
|
lines[topLevelInlineFeatures] = lines[topLevelInlineFeatures].replace(
|
|
9966
|
-
/\\
|
|
9967
|
-
'
|
|
10208
|
+
/\\bhooks\\s*=\\s*(true|false)\\b/i,
|
|
10209
|
+
'hooks = true',
|
|
9968
10210
|
)
|
|
9969
10211
|
} else {
|
|
9970
|
-
lines[topLevelInlineFeatures] = lines[topLevelInlineFeatures].replace(/}/, ',
|
|
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]', '
|
|
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 "
|
|
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 {
|
|
10618
|
-
import {
|
|
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
|
|
10623
|
-
import { homedir } from "os";
|
|
10624
|
-
import { basename as basename2, dirname as dirname2, resolve as
|
|
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
|
|
10638
|
-
import { existsSync as
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
11455
|
+
pluginDir: resolve5(home, ".openhands/plugins", pluginName),
|
|
10693
11456
|
description: `~/.openhands/plugins/${pluginName}`
|
|
10694
11457
|
},
|
|
10695
11458
|
{
|
|
10696
11459
|
platform: "warp",
|
|
10697
|
-
pluginDir:
|
|
11460
|
+
pluginDir: resolve5(home, ".warp/plugins", pluginName),
|
|
10698
11461
|
description: `~/.warp/plugins/${pluginName}`
|
|
10699
11462
|
},
|
|
10700
11463
|
{
|
|
10701
11464
|
platform: "gemini-cli",
|
|
10702
|
-
pluginDir:
|
|
11465
|
+
pluginDir: resolve5(home, ".gemini/extensions", pluginName),
|
|
10703
11466
|
description: `~/.gemini/extensions/${pluginName}`
|
|
10704
11467
|
},
|
|
10705
11468
|
{
|
|
10706
11469
|
platform: "roo-code",
|
|
10707
|
-
pluginDir:
|
|
11470
|
+
pluginDir: resolve5(home, ".roo/plugins", pluginName),
|
|
10708
11471
|
description: `~/.roo/plugins/${pluginName}`
|
|
10709
11472
|
},
|
|
10710
11473
|
{
|
|
10711
11474
|
platform: "cline",
|
|
10712
|
-
pluginDir:
|
|
11475
|
+
pluginDir: resolve5(home, ".cline/plugins", pluginName),
|
|
10713
11476
|
description: `~/.cline/plugins/${pluginName}`
|
|
10714
11477
|
},
|
|
10715
11478
|
{
|
|
10716
11479
|
platform: "amp",
|
|
10717
|
-
pluginDir:
|
|
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 =
|
|
10729
|
-
if (!
|
|
11491
|
+
const filepath = resolve5(rootDir, manifestPath);
|
|
11492
|
+
if (!existsSync5(filepath)) return void 0;
|
|
10730
11493
|
try {
|
|
10731
|
-
const manifest = JSON.parse(
|
|
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
|
|
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 (
|
|
10756
|
-
if (
|
|
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
|
|
11541
|
+
function resolveBundleReference2(rootDir, value) {
|
|
10779
11542
|
if (isRelativeBundlePath(value)) {
|
|
10780
|
-
return
|
|
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
|
|
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 =
|
|
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) =>
|
|
11602
|
+
return extractBundleCommandTargets(command).map((target) => resolveBundleReference2(rootDir, target)).filter((target) => {
|
|
10840
11603
|
if (!target) return false;
|
|
10841
|
-
return
|
|
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 =
|
|
10853
|
-
const hookCommand = wrapper
|
|
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:
|
|
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 =
|
|
10873
|
-
if (
|
|
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 =
|
|
10887
|
-
const standardHooksPath =
|
|
10888
|
-
if (!manifestHooksPath || manifestHooksPath !== 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 =
|
|
10904
|
-
if (!
|
|
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(
|
|
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 =
|
|
10927
|
-
return resolved !== void 0 && !
|
|
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 || !
|
|
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(
|
|
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 =
|
|
10956
|
-
return resolved !== void 0 && !
|
|
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 =
|
|
10982
|
-
if (!mcpPath || !
|
|
11755
|
+
const mcpPath = resolveBundleReference2(rootDir, mcpReference);
|
|
11756
|
+
if (!mcpPath || !existsSync5(mcpPath)) return [];
|
|
10983
11757
|
try {
|
|
10984
|
-
const parsed = JSON.parse(
|
|
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 =
|
|
10996
|
-
if (!resolved || !
|
|
10997
|
-
const content =
|
|
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 =
|
|
11786
|
+
const sourceDir = resolve5(distDir, target.platform);
|
|
11013
11787
|
return {
|
|
11014
11788
|
...target,
|
|
11015
11789
|
sourceDir,
|
|
11016
|
-
built:
|
|
11017
|
-
existing:
|
|
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 = "
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 =
|
|
11409
|
-
const indexPath =
|
|
11410
|
-
if (
|
|
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(
|
|
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) =>
|
|
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) =>
|
|
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(
|
|
12318
|
+
return JSON.parse(readFileSync5(resolve6(rootDir, relativePath), "utf-8"));
|
|
11438
12319
|
}
|
|
11439
12320
|
function listCodexConfigCandidates(projectRoot) {
|
|
11440
|
-
const projectCandidate =
|
|
11441
|
-
const homeDir = process.env.HOME?.trim() ||
|
|
11442
|
-
const codexHome = process.env.CODEX_HOME?.trim() ||
|
|
11443
|
-
const userCandidate =
|
|
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 =
|
|
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\.(
|
|
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(/^(
|
|
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 (!
|
|
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 (!
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
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() ||
|
|
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:
|
|
12551
|
+
candidates.push({ path: resolve6(homeDir, ".claude/settings.json"), scope: "user" });
|
|
11671
12552
|
if (projectRoot) {
|
|
11672
|
-
candidates.push({ path:
|
|
11673
|
-
candidates.push({ path:
|
|
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 (!
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
11768
|
-
if (!
|
|
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(
|
|
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 =
|
|
11819
|
-
if (!
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
11882
|
-
if (!
|
|
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.
|
|
12115
|
-
if (probe.recommendedEnabled) enabledFlags.
|
|
12116
|
-
if (probe.alternateEnabled) enabledFlags.
|
|
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
|
|
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
|
|
12126
|
-
fix: `Enable \`${
|
|
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
|
|
12137
|
-
fix: `
|
|
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
|
|
12146
|
-
detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, and the
|
|
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 \`${
|
|
12158
|
-
fix: `Enable \`${
|
|
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 =
|
|
13083
|
+
const companionPath = resolve6(rootDir, ".codex/config.generated.toml");
|
|
12203
13084
|
const companionReference = ".codex/config.generated.toml";
|
|
12204
|
-
if (!
|
|
13085
|
+
if (!existsSync6(companionPath)) return;
|
|
12205
13086
|
let expectedEntries;
|
|
12206
13087
|
try {
|
|
12207
|
-
expectedEntries = parseCodexApprovedMcpToolsFromToml(
|
|
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 =
|
|
12337
|
-
if (!
|
|
13217
|
+
const scriptPath = resolve6(rootDir, "hooks/pluxx-permissions.mjs");
|
|
13218
|
+
if (!existsSync6(scriptPath)) {
|
|
12338
13219
|
return null;
|
|
12339
13220
|
}
|
|
12340
|
-
const rules = parseInstalledPermissionRules(
|
|
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 =
|
|
12427
|
-
if (!
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
12578
|
-
if (!
|
|
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 =
|
|
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
|
|
13474
|
+
for (const entry of readdirSync3(sourceSkillsDir, { withFileTypes: true })) {
|
|
12594
13475
|
if (!entry.isDirectory()) continue;
|
|
12595
|
-
const sourceSkillPath =
|
|
12596
|
-
if (!
|
|
13476
|
+
const sourceSkillPath = resolve6(sourceSkillsDir, entry.name, "SKILL.md");
|
|
13477
|
+
if (!existsSync6(sourceSkillPath)) continue;
|
|
12597
13478
|
expectedSkillCount++;
|
|
12598
|
-
const installedSkillPath =
|
|
12599
|
-
if (!
|
|
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 =
|
|
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 &&
|
|
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:
|
|
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 =
|
|
12753
|
-
if (!
|
|
13634
|
+
const filepath = resolve7(rootDir, manifestPath);
|
|
13635
|
+
if (!existsSync7(filepath)) return void 0;
|
|
12754
13636
|
try {
|
|
12755
|
-
const manifest = JSON.parse(
|
|
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 =
|
|
12764
|
-
if (!
|
|
13645
|
+
const cacheRoot = resolve7(home, ".codex/plugins/cache");
|
|
13646
|
+
if (!existsSync7(cacheRoot)) return [];
|
|
12765
13647
|
const candidates = [];
|
|
12766
|
-
for (const marketplace of
|
|
12767
|
-
const pluginRoot =
|
|
12768
|
-
if (!
|
|
12769
|
-
for (const versionDir of
|
|
12770
|
-
const candidatePath =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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,
|