@orchid-labs/pluxx 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -5170,12 +5170,39 @@ function buildUserConfigEnvMap(entries) {
5170
5170
  }
5171
5171
  return env;
5172
5172
  }
5173
- function buildUserConfigValueMap(entries) {
5173
+ function buildInstalledUserConfigPayload(entries, options = {}) {
5174
+ const preserveSecretReferences = options.preserveSecretReferences === true;
5174
5175
  const values = {};
5176
+ const env = {};
5177
+ const envRefs = {};
5178
+ const secretKeys = /* @__PURE__ */ new Set();
5179
+ const secretEnv = /* @__PURE__ */ new Set();
5180
+ let hasSecret = false;
5175
5181
  for (const entry of entries) {
5182
+ const envVar = entry.envVar;
5183
+ const isSecret = entry.field.type === "secret";
5184
+ if (isSecret) {
5185
+ hasSecret = true;
5186
+ if (preserveSecretReferences) {
5187
+ secretKeys.add(entry.field.key);
5188
+ if (envVar) secretEnv.add(envVar);
5189
+ }
5190
+ }
5191
+ if (preserveSecretReferences && isSecret) {
5192
+ if (envVar) envRefs[envVar] = envVar;
5193
+ continue;
5194
+ }
5176
5195
  values[entry.field.key] = entry.value;
5196
+ if (envVar) env[envVar] = String(entry.value);
5177
5197
  }
5178
- return values;
5198
+ return {
5199
+ ...Object.keys(values).length > 0 ? { values } : {},
5200
+ ...Object.keys(env).length > 0 ? { env } : {},
5201
+ ...Object.keys(envRefs).length > 0 ? { envRefs } : {},
5202
+ ...hasSecret ? { secretStorage: preserveSecretReferences ? "env-ref" : "materialized" } : {},
5203
+ ...preserveSecretReferences && secretKeys.size > 0 ? { secretKeys: [...secretKeys].sort() } : {},
5204
+ ...preserveSecretReferences && secretEnv.size > 0 ? { secretEnv: [...secretEnv].sort() } : {}
5205
+ };
5179
5206
  }
5180
5207
 
5181
5208
  // src/mcp-native-overrides.ts
@@ -5494,6 +5521,18 @@ var CURSOR_SUPPORTED_HOOK_EVENTS = [
5494
5521
  "beforeTabFileRead",
5495
5522
  "afterTabFileEdit"
5496
5523
  ];
5524
+ var CODEX_SUPPORTED_HOOK_EVENTS = [
5525
+ "SessionStart",
5526
+ "SubagentStart",
5527
+ "PreToolUse",
5528
+ "PermissionRequest",
5529
+ "PostToolUse",
5530
+ "PreCompact",
5531
+ "PostCompact",
5532
+ "UserPromptSubmit",
5533
+ "SubagentStop",
5534
+ "Stop"
5535
+ ];
5497
5536
  var PASCAL_CASE_HOOK_ALIASES = {
5498
5537
  beforeSubmitPrompt: "UserPromptSubmit"
5499
5538
  };
@@ -5540,8 +5579,8 @@ var HOOK_PLATFORM_REGISTRY = {
5540
5579
  },
5541
5580
  codex: {
5542
5581
  supportedTypes: ["command"],
5543
- supportedEvents: ["SessionStart", "PreToolUse", "PermissionRequest", "PostToolUse", "UserPromptSubmit", "Stop"],
5544
- unsupportedEventReason: "Codex currently documents only SessionStart, PreToolUse, PermissionRequest, PostToolUse, UserPromptSubmit, and Stop for hook configuration.",
5582
+ supportedEvents: CODEX_SUPPORTED_HOOK_EVENTS,
5583
+ unsupportedEventReason: `Codex currently documents only ${CODEX_SUPPORTED_HOOK_EVENTS.join(", ")} for hook configuration.`,
5545
5584
  fields: {
5546
5585
  prompt: { mode: "drop" },
5547
5586
  matcher: {
@@ -5838,8 +5877,16 @@ function loadUserEnv(pluginRoot) {
5838
5877
  const env = parsed && typeof parsed === "object" && parsed.env && typeof parsed.env === "object"
5839
5878
  ? parsed.env
5840
5879
  : {}
5880
+ const envRefs = parsed && typeof parsed === "object" && parsed.envRefs && typeof parsed.envRefs === "object"
5881
+ ? parsed.envRefs
5882
+ : {}
5841
5883
  return Object.fromEntries(
5842
- Object.entries(env).filter(([key]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)),
5884
+ [
5885
+ ...Object.entries(env),
5886
+ ...Object.entries(envRefs)
5887
+ .filter(([, value]) => typeof value === "string" && value in process.env)
5888
+ .map(([key, value]) => [key, process.env[value]]),
5889
+ ].filter(([key, value]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) && typeof value === "string"),
5843
5890
  )
5844
5891
  } catch {
5845
5892
  return {}
@@ -6077,7 +6124,7 @@ function getEnabledRuntimeReadinessBindings(capability, plan) {
6077
6124
  });
6078
6125
  }
6079
6126
  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.";
6080
- var CODEX_EXTERNAL_NOTE = "Codex readiness now bundles translated hooks in the plugin, and Pluxx still emits `.codex/readiness.generated.json` plus `.codex/hooks.generated.json` as debugging companions because plugin-bundled hook activation still depends on `[features].plugin_hooks = true`. The general `hooks = true` flag covers non-plugin hook config and defaults on, while `codex_hooks` is deprecated and should not be treated as a plugin-bundled hook fallback.";
6127
+ 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.";
6081
6128
  function getRuntimeReadinessNamedPromptTargetNote() {
6082
6129
  return NAMED_PROMPT_TARGET_NOTE;
6083
6130
  }
@@ -6393,10 +6440,19 @@ function buildHookCommandWrapperScript(command2, pluginRootVar, envFileVar) {
6393
6440
  'const env = payload && typeof payload === "object" && payload.env && typeof payload.env === "object"',
6394
6441
  " ? payload.env",
6395
6442
  " : {}",
6443
+ 'const envRefs = payload && typeof payload === "object" && payload.envRefs && typeof payload.envRefs === "object"',
6444
+ " ? payload.envRefs",
6445
+ " : {}",
6396
6446
  "",
6397
6447
  "for (const [key, value] of Object.entries(env)) {",
6398
6448
  " if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue",
6399
6449
  " process.stdout.write(`export ${key}=${shellSingleQuote(value)}\\0`)",
6450
+ "}",
6451
+ "for (const [key, envVar] of Object.entries(envRefs)) {",
6452
+ " if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue",
6453
+ ' if (typeof envVar !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(envVar)) continue',
6454
+ " if (!(envVar in process.env)) continue",
6455
+ " process.stdout.write(`export ${key}=${shellSingleQuote(process.env[envVar])}\\0`)",
6400
6456
  "}"
6401
6457
  ].join("\n");
6402
6458
  const maybeAppendEnvFile = envFileVar ? [
@@ -9875,7 +9931,7 @@ function toQuotedTomlKey(value) {
9875
9931
  // src/codex-hooks-feature.ts
9876
9932
  var RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG = "hooks";
9877
9933
  var ALTERNATE_CODEX_HOOKS_FEATURE_FLAG = "codex_hooks";
9878
- var PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG = "plugin_hooks";
9934
+ var PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG = "hooks";
9879
9935
  function getCodexHooksFeatureState(features) {
9880
9936
  if (!features) {
9881
9937
  return {
@@ -10225,7 +10281,7 @@ var CodexGenerator = class extends Generator {
10225
10281
  pluginBundleFeatureFlag: PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG,
10226
10282
  generalFeatureFlag: RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG,
10227
10283
  deprecatedGeneralFeatureFlag: ALTERNATE_CODEX_HOOKS_FEATURE_FLAG,
10228
- note: "Codex plugin-bundled hook configuration is bundled at hooks/hooks.json in the plugin and currently requires `[features].plugin_hooks = true`. The general `[features].hooks` flag covers non-plugin hook config and defaults on, while `codex_hooks` is deprecated and should not be treated as a plugin-bundled hook fallback. This companion mirror preserves the translated native event names, matcher groups, and command handlers while highlighting any dropped events or fields.",
10284
+ note: "Codex plugin-bundled hook configuration is bundled at hooks/hooks.json in the plugin and currently requires `[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. This companion mirror preserves the translated native event names, matcher groups, and command handlers while highlighting any dropped events or fields.",
10229
10285
  hooks,
10230
10286
  ...unsupported.length > 0 ? { unsupported } : {}
10231
10287
  });
@@ -10434,7 +10490,7 @@ var OpenCodeGenerator = class extends Generator {
10434
10490
  `const isMcpTool = (tool: string): boolean =>`,
10435
10491
  ` tool === "mcp" || tool.startsWith("mcp.") || tool.startsWith("mcp_")`,
10436
10492
  "",
10437
- `const loadUserConfig = (directory: string): { values?: Record<string, string | number | boolean>; env?: Record<string, string> } => {`,
10493
+ `const loadUserConfig = (directory: string): { values?: Record<string, string | number | boolean>; env?: Record<string, string>; envRefs?: Record<string, string> } => {`,
10438
10494
  ` const filepath = resolve(directory, ".pluxx-user.json")`,
10439
10495
  ` if (!existsSync(filepath)) return {}`,
10440
10496
  ` try {`,
@@ -10444,28 +10500,30 @@ var OpenCodeGenerator = class extends Generator {
10444
10500
  ` }`,
10445
10501
  `}`,
10446
10502
  "",
10447
- `const resolveRuntimeValue = (name: string, userEnv: Record<string, string>): string | undefined =>`,
10448
- ` userEnv[name] ?? process.env[name]`,
10503
+ `const resolveRuntimeValue = (name: string, userEnv: Record<string, string>, userEnvRefs: Record<string, string>): string | undefined =>`,
10504
+ ` userEnv[name] ?? (userEnvRefs[name] ? process.env[userEnvRefs[name]] : undefined) ?? process.env[name]`,
10449
10505
  "",
10450
- `const materializeEnv = (input: Record<string, string> | undefined, userEnv: Record<string, string>): Record<string, string> | undefined => {`,
10506
+ `const materializeEnv = (input: Record<string, string> | undefined, userEnv: Record<string, string>, userEnvRefs: Record<string, string>): Record<string, string> | undefined => {`,
10451
10507
  ` if (!input) return undefined`,
10452
10508
  ` const output: Record<string, string> = {}`,
10453
10509
  ` for (const [key, value] of Object.entries(input)) {`,
10454
- ` output[key] = value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_match, name) => resolveRuntimeValue(name, userEnv) ?? \`\\\${\${name}}\`)`,
10510
+ ` output[key] = value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_match, name) => resolveRuntimeValue(name, userEnv, userEnvRefs) ?? \`\\\${\${name}}\`)`,
10455
10511
  ` }`,
10456
10512
  ` return output`,
10457
10513
  `}`,
10458
10514
  "",
10459
10515
  `const buildMcpConfig = (directory: string): NonNullable<Config["mcp"]> => {`,
10460
10516
  ` const config: NonNullable<Config["mcp"]> = {}`,
10461
- ` const userEnv = loadUserConfig(directory).env ?? {}`,
10517
+ ` const userConfig = loadUserConfig(directory)`,
10518
+ ` const userEnv = userConfig.env ?? {}`,
10519
+ ` const userEnvRefs = userConfig.envRefs ?? {}`,
10462
10520
  "",
10463
10521
  ` for (const [name, definition] of Object.entries(MCP_DEFINITIONS)) {`,
10464
10522
  ` if (definition.transport === "stdio" && definition.command) {`,
10465
10523
  ` config[name] = {`,
10466
10524
  ` type: "local",`,
10467
10525
  ` command: [definition.command, ...(definition.args ?? [])],`,
10468
- ` ...(definition.env ? { environment: materializeEnv(definition.env, userEnv) } : {}),`,
10526
+ ` ...(definition.env ? { environment: materializeEnv(definition.env, userEnv, userEnvRefs) } : {}),`,
10469
10527
  ` }`,
10470
10528
  ` continue`,
10471
10529
  ` }`,
@@ -10482,12 +10540,12 @@ var OpenCodeGenerator = class extends Generator {
10482
10540
  ` }`,
10483
10541
  "",
10484
10542
  ` if (definition.auth?.type === "bearer" && definition.auth.envVar) {`,
10485
- ` const token = resolveRuntimeValue(definition.auth.envVar, userEnv)`,
10543
+ ` const token = resolveRuntimeValue(definition.auth.envVar, userEnv, userEnvRefs)`,
10486
10544
  ` if (token) remote.headers = { Authorization: \`Bearer \${token}\` }`,
10487
10545
  ` }`,
10488
10546
  "",
10489
10547
  ` if (definition.auth?.type === "header" && definition.auth.envVar && definition.auth.headerName && definition.auth.headerTemplate) {`,
10490
- ` const value = resolveRuntimeValue(definition.auth.envVar, userEnv)`,
10548
+ ` const value = resolveRuntimeValue(definition.auth.envVar, userEnv, userEnvRefs)`,
10491
10549
  ` if (value) {`,
10492
10550
  ` remote.headers = {`,
10493
10551
  ` ...(remote.headers ?? {}),`,
@@ -10517,10 +10575,15 @@ var OpenCodeGenerator = class extends Generator {
10517
10575
  ` const shellSingleQuote = (input: string): string => \`'\${String(input ?? "").replace(/'/g, \`'"'"'\`)}'\``,
10518
10576
  "",
10519
10577
  ` const buildHookShellCommand = (rawCommand: string): string => {`,
10520
- ` const userEnv = loadUserConfig(directory).env ?? {}`,
10578
+ ` const userConfig = loadUserConfig(directory)`,
10579
+ ` const userEnv = userConfig.env ?? {}`,
10580
+ ` const userEnvRefs = userConfig.envRefs ?? {}`,
10521
10581
  ` const exports = Object.entries(userEnv)`,
10522
10582
  ` .filter(([key]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key))`,
10523
10583
  ` .map(([key, value]) => \`export \${key}=\${shellSingleQuote(String(value))}\`)`,
10584
+ ` .concat(Object.entries(userEnvRefs)`,
10585
+ ` .filter(([key, envVar]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) && typeof envVar === "string" && envVar in process.env)`,
10586
+ ` .map(([key, envVar]) => \`export \${key}=\${shellSingleQuote(String(process.env[envVar]))}\`))`,
10524
10587
  ` .join("; ")`,
10525
10588
  ` const command = rawCommand.replaceAll("\${PLUGIN_ROOT}", directory)`,
10526
10589
  ` return exports ? \`\${exports}; \${command}\` : command`,
@@ -11318,6 +11381,7 @@ import { existsSync as existsSync18, lstatSync as lstatSync2, readdirSync as rea
11318
11381
  import { resolve as resolve14, relative as relative8, basename as basename5, dirname as dirname4 } from "path";
11319
11382
 
11320
11383
  // src/validation/platform-rules.ts
11384
+ var CORE_FOUR_PLATFORMS = ["claude-code", "cursor", "codex", "opencode"];
11321
11385
  var STANDARD_SKILL_FRONTMATTER = [
11322
11386
  "name",
11323
11387
  "description",
@@ -11446,7 +11510,7 @@ var PLATFORM_LIMIT_POLICIES = {
11446
11510
  },
11447
11511
  hooksFeatureFlag: {
11448
11512
  kind: "hard",
11449
- notes: "Hook support depends on the Codex hooks feature flag/runtime support."
11513
+ notes: "Codex hook support depends on the canonical hooks feature flag plus enabled-plugin, trust/review, and runtime support."
11450
11514
  }
11451
11515
  },
11452
11516
  "cursor": {
@@ -11646,8 +11710,8 @@ var PLATFORM_VALIDATION_RULES = {
11646
11710
  hooks: {
11647
11711
  supported: true,
11648
11712
  files: ["hooks/hooks.json", ".codex/hooks.json", "~/.codex/hooks.json"],
11649
- eventNames: ["SessionStart", "PreToolUse", "PermissionRequest", "PostToolUse", "UserPromptSubmit", "Stop"],
11650
- notes: "Codex documents both project or user hook config and plugin-bundled hooks. Plugin-bundled hooks live at `hooks/hooks.json` in the plugin and require `[features].plugin_hooks = true`; the general `[features].hooks = true` flag covers non-plugin hook config and defaults on. `codex_hooks` is deprecated and should not be treated as a plugin-bundled hook fallback."
11713
+ eventNames: [...CODEX_SUPPORTED_HOOK_EVENTS],
11714
+ 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."
11651
11715
  },
11652
11716
  instructions: {
11653
11717
  files: ["AGENTS.md", "AGENTS.override.md"],
@@ -12086,7 +12150,7 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
12086
12150
  hooks: {
12087
12151
  mode: "translate",
12088
12152
  nativeSurfaces: ["hooks/hooks.json", ".codex/hooks.json", "~/.codex/hooks.json"],
12089
- notes: "Hooks are native. Pluxx bundles translated Codex hooks in the plugin with the documented `[features].plugin_hooks = true` plugin gate, and still tracks the broader project/user hook config paths where `[features].hooks` is the general flag and `codex_hooks` is deprecated."
12153
+ 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."
12090
12154
  },
12091
12155
  permissions: {
12092
12156
  mode: "translate",
@@ -12157,7 +12221,6 @@ function getCoreFourPrimitiveCapabilities(platform) {
12157
12221
  }
12158
12222
 
12159
12223
  // src/cli/primitive-summary.ts
12160
- var CORE_FOUR_ORDER = ["claude-code", "cursor", "codex", "opencode"];
12161
12224
  var TARGET_LABELS = {
12162
12225
  "claude-code": "claude",
12163
12226
  cursor: "cursor",
@@ -12171,7 +12234,7 @@ var MODE_LABELS = {
12171
12234
  drop: "drop"
12172
12235
  };
12173
12236
  function buildPrimitiveTranslationSummary(config, targets = config.targets) {
12174
- const selectedTargets = CORE_FOUR_ORDER.filter((target) => targets.includes(target));
12237
+ const selectedTargets = CORE_FOUR_PLATFORMS.filter((target) => targets.includes(target));
12175
12238
  if (selectedTargets.length === 0) return void 0;
12176
12239
  const configuredBuckets = getConfiguredCompilerBuckets({
12177
12240
  ...config,
@@ -13361,7 +13424,7 @@ function lintCodexHooksExternalConfig(config, issues) {
13361
13424
  pushIssue(issues, {
13362
13425
  level: "warning",
13363
13426
  code: "codex-hooks-external-config",
13364
- message: `Pluxx now bundles Codex hooks at \`hooks/hooks.json\`, and Codex plugin-bundled hook loading requires \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\`. The general \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG}\` flag covers non-plugin hook config and defaults on; \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` is deprecated and should not be treated as a plugin-bundled hook fallback. If bundled hooks do not activate, enable \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\`, reload Codex, and retest in a trusted interactive session.`,
13427
+ message: `Pluxx now bundles Codex hooks at \`hooks/hooks.json\`, and current Codex docs use \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` as the canonical hook feature key. \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` is deprecated and should not be treated as the current hook feature key. If bundled hooks do not activate, enable \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\`, reload Codex, and retest in a trusted interactive session.`,
13365
13428
  file: "pluxx.config.ts",
13366
13429
  platform: "Codex"
13367
13430
  });
@@ -13370,7 +13433,7 @@ function lintCodexHooksExternalConfig(config, issues) {
13370
13433
  pushIssue(issues, {
13371
13434
  level: "warning",
13372
13435
  code: "codex-hooks-legacy-feature-flag",
13373
- message: `\`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` is deprecated for general Codex hook config and should not be treated as a plugin-bundled hook fallback. Keep it only as a compatibility fallback for non-plugin hook wiring, prefer \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` there, and use \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` for plugin-bundled hooks.`,
13436
+ message: `\`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` is deprecated for Codex hook config. Prefer \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\`.`,
13374
13437
  file: "pluxx.config.ts",
13375
13438
  platform: "Codex"
13376
13439
  });
@@ -13379,7 +13442,7 @@ function lintCodexHooksExternalConfig(config, issues) {
13379
13442
  pushIssue(issues, {
13380
13443
  level: "warning",
13381
13444
  code: "codex-hooks-general-feature-flag-only",
13382
- message: `This Codex config enables only general hook flags. Plugin-bundled hooks at \`hooks/hooks.json\` still need \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\`; \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG}\` and \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` alone are not sufficient.`,
13445
+ message: `This Codex config enables only deprecated hook flags. Plugin-bundled hooks at \`hooks/hooks.json\` should use \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\`.`,
13383
13446
  file: "pluxx.config.ts",
13384
13447
  platform: "Codex"
13385
13448
  });
@@ -17992,7 +18055,7 @@ ${additions.map((block) => `- ${block.replace(/\n/g, "\n ")}`).join("\n")}
17992
18055
  import { spawn as spawn3, spawnSync as spawnSync2 } from "child_process";
17993
18056
  import { accessSync, constants, existsSync as existsSync24, lstatSync as lstatSync3, readFileSync as readFileSync14, readdirSync as readdirSync10, realpathSync } from "fs";
17994
18057
  import { homedir as homedir2 } from "os";
17995
- import { basename as basename7, dirname as dirname7, resolve as resolve20 } from "path";
18058
+ import { basename as basename7, dirname as dirname7, relative as relative12, resolve as resolve20 } from "path";
17996
18059
 
17997
18060
  // src/cli/install.ts
17998
18061
  import { resolve as resolve19, dirname as dirname6, basename as basename6, relative as relative11 } from "path";
@@ -18479,19 +18542,25 @@ function createOpenCodeCopiedInstall(target, pluginName) {
18479
18542
  writeOpenCodeEntryFile(target.pluginDir, pluginName);
18480
18543
  syncOpenCodeSkills(target.pluginDir, pluginName);
18481
18544
  }
18482
- function materializeTemplateValue(value, env) {
18483
- return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => env[name] ?? `\${${name}}`);
18545
+ function materializeTemplateValue(value, env, secretEnvVars = /* @__PURE__ */ new Set()) {
18546
+ return value.replace(
18547
+ /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
18548
+ (_match, name) => secretEnvVars.has(name) ? `\${${name}}` : env[name] ?? `\${${name}}`
18549
+ );
18484
18550
  }
18485
- function materializeEnvRecord(input, env) {
18551
+ function materializeEnvRecord(input, env, secretEnvVars = /* @__PURE__ */ new Set()) {
18486
18552
  const output = {};
18487
18553
  for (const [key, value] of Object.entries(input ?? {})) {
18488
- output[key] = materializeTemplateValue(value, env);
18554
+ output[key] = materializeTemplateValue(value, env, secretEnvVars);
18489
18555
  }
18490
18556
  return output;
18491
18557
  }
18492
18558
  function patchInstalledMcpConfig(pluginDir, platform, config, entries) {
18493
18559
  if (!config.mcp) return;
18494
18560
  const env = buildUserConfigEnvMap(entries);
18561
+ const secretEnvVars = new Set(
18562
+ entries.filter((entry) => entry.field.type === "secret" && entry.envVar).map((entry) => entry.envVar)
18563
+ );
18495
18564
  if (platform === "claude-code" || platform === "cursor") {
18496
18565
  const filepath = resolve19(pluginDir, platform === "claude-code" ? ".mcp.json" : "mcp.json");
18497
18566
  if (!existsSync23(filepath)) return;
@@ -18536,7 +18605,7 @@ function patchInstalledMcpConfig(pluginDir, platform, config, entries) {
18536
18605
  mcpServers[name] = {
18537
18606
  command: materializeInstalledPluginOwnedStdioPathForPlatform(server.command, platform, pluginDir),
18538
18607
  args: (server.args ?? []).map((value) => materializeInstalledPluginOwnedStdioPathForPlatform(value, platform, pluginDir)),
18539
- env: materializeEnvRecord(server.env, env)
18608
+ env: materializeEnvRecord(server.env, env, secretEnvVars)
18540
18609
  };
18541
18610
  continue;
18542
18611
  }
@@ -18546,49 +18615,46 @@ function patchInstalledMcpConfig(pluginDir, platform, config, entries) {
18546
18615
  const nativeOverride = getNativeCodexMcpEntryOverride(config, name);
18547
18616
  if (nativeOverride) {
18548
18617
  if (typeof nativeOverride.bearer_token_env_var === "string") {
18549
- const envVar = nativeOverride.bearer_token_env_var;
18550
- if (env[envVar]) {
18551
- entry.bearer_token_env_var = envVar;
18552
- }
18618
+ entry.bearer_token_env_var = nativeOverride.bearer_token_env_var;
18553
18619
  }
18554
18620
  if (nativeOverride.auth) {
18555
18621
  entry.auth = nativeOverride.auth;
18556
18622
  }
18557
18623
  if (nativeOverride.env_http_headers && typeof nativeOverride.env_http_headers === "object") {
18558
- entry.http_headers = Object.fromEntries(
18559
- Object.entries(nativeOverride.env_http_headers).filter(([, value]) => typeof value === "string").map(([headerName, envVar]) => [
18560
- headerName,
18561
- env[envVar] ?? `\${${envVar}}`
18562
- ])
18624
+ entry.env_http_headers = Object.fromEntries(
18625
+ Object.entries(nativeOverride.env_http_headers).filter(([, value]) => typeof value === "string").map(([headerName, envVar]) => [headerName, envVar])
18563
18626
  );
18564
18627
  }
18565
18628
  if (nativeOverride.http_headers && typeof nativeOverride.http_headers === "object") {
18566
18629
  entry.http_headers = materializeEnvRecord(
18567
18630
  nativeOverride.http_headers,
18568
- env
18631
+ env,
18632
+ secretEnvVars
18569
18633
  );
18570
18634
  }
18571
- } else if (server.auth?.type === "bearer" && server.auth.envVar && env[server.auth.envVar]) {
18572
- entry.http_headers = {
18573
- Authorization: `Bearer ${env[server.auth.envVar]}`
18574
- };
18575
- } else if (server.auth?.type === "header" && server.auth.envVar && env[server.auth.envVar]) {
18576
- entry.http_headers = {
18577
- [server.auth.headerName]: server.auth.headerTemplate.replace("${value}", env[server.auth.envVar])
18578
- };
18635
+ } else if (server.auth?.type === "bearer" && server.auth.envVar) {
18636
+ entry.bearer_token_env_var = server.auth.envVar;
18637
+ } else if (server.auth?.type === "header" && server.auth.envVar) {
18638
+ const isBearerAuthorizationHeader = server.auth.headerName === "Authorization" && server.auth.headerTemplate === "Bearer ${value}";
18639
+ if (isBearerAuthorizationHeader) {
18640
+ entry.bearer_token_env_var = server.auth.envVar;
18641
+ } else if (server.auth.headerTemplate === "${value}") {
18642
+ entry.env_http_headers = {
18643
+ [server.auth.headerName]: server.auth.envVar
18644
+ };
18645
+ }
18579
18646
  }
18580
18647
  mcpServers[name] = entry;
18581
18648
  }
18582
18649
  writeFileSync4(filepath, JSON.stringify({ mcpServers }, null, 2) + "\n");
18583
18650
  }
18584
18651
  }
18585
- function writeInstalledUserConfig(pluginDir, entries) {
18652
+ function writeInstalledUserConfig(pluginDir, entries, platform) {
18586
18653
  if (entries.length === 0) return;
18587
18654
  const filepath = resolve19(pluginDir, ".pluxx-user.json");
18588
- const payload = {
18589
- values: buildUserConfigValueMap(entries),
18590
- env: buildUserConfigEnvMap(entries)
18591
- };
18655
+ const payload = buildInstalledUserConfigPayload(entries, {
18656
+ preserveSecretReferences: platform === "codex"
18657
+ });
18592
18658
  writeFileSync4(filepath, JSON.stringify(payload, null, 2) + "\n");
18593
18659
  }
18594
18660
  function disableInstalledEnvValidation(pluginDir, entries) {
@@ -18602,7 +18668,7 @@ function disableInstalledEnvValidation(pluginDir, entries) {
18602
18668
  }
18603
18669
  function materializeInstalledPlugin(pluginDir, platform, config, entries) {
18604
18670
  if (entries.length > 0) {
18605
- writeInstalledUserConfig(pluginDir, entries);
18671
+ writeInstalledUserConfig(pluginDir, entries, platform);
18606
18672
  disableInstalledEnvValidation(pluginDir, entries);
18607
18673
  }
18608
18674
  patchInstalledMcpConfig(pluginDir, platform, config, entries);
@@ -19169,6 +19235,13 @@ var LOW_INFO_DESCRIPTION_PATTERNS = [
19169
19235
  var MATERIALIZED_ENV_MARKER = "materialized required config";
19170
19236
  var MIN_NODE_MAJOR = 18;
19171
19237
  var STDIO_LAUNCH_SMOKE_TIMEOUT_MS = 1200;
19238
+ var MAX_SECRET_SCAN_FILE_BYTES = 512 * 1024;
19239
+ var LEGACY_SECRET_IDENTIFIER_PATTERN = /(^|[-_])(api[-_]?key|secret|token|password|credential)([-_]|$)/i;
19240
+ var TEST_SECRET_SENTINELS = [
19241
+ { pattern: /\bshh-secret\b/i, label: "test secret sentinel" },
19242
+ { pattern: /\bsecret-key\b/i, label: "test secret sentinel" },
19243
+ { pattern: /\bliteral-secret-value-that-should-not-copy\b/i, label: "test secret sentinel" }
19244
+ ];
19172
19245
  var PRIMITIVE_MODE_LEVEL = {
19173
19246
  preserve: "success",
19174
19247
  translate: "info",
@@ -19181,6 +19254,106 @@ function renderInstalledPluginRoot(value, rootDir) {
19181
19254
  function addCheck2(checks, check) {
19182
19255
  checks.push(check);
19183
19256
  }
19257
+ function looksLegacySecretIdentifier(value) {
19258
+ return LEGACY_SECRET_IDENTIFIER_PATTERN.test(
19259
+ value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[._/\s]+/g, "-")
19260
+ );
19261
+ }
19262
+ function walkInstalledBundleFiles(rootDir, currentDir = rootDir) {
19263
+ const files = [];
19264
+ for (const entry of readdirSync10(currentDir, { withFileTypes: true })) {
19265
+ const absolutePath = resolve20(currentDir, entry.name);
19266
+ if (entry.isDirectory()) {
19267
+ files.push(...walkInstalledBundleFiles(rootDir, absolutePath));
19268
+ continue;
19269
+ }
19270
+ if (!entry.isFile()) continue;
19271
+ files.push(relative12(rootDir, absolutePath).replace(/\\/g, "/"));
19272
+ }
19273
+ return files.sort();
19274
+ }
19275
+ function readInstalledTextFile(rootDir, relativePath) {
19276
+ const absolutePath = resolve20(rootDir, relativePath);
19277
+ const content = readFileSync14(absolutePath);
19278
+ if (content.length > MAX_SECRET_SCAN_FILE_BYTES) return null;
19279
+ if (content.includes(0)) return null;
19280
+ return content.toString("utf-8");
19281
+ }
19282
+ function collectInstalledPlaintextSecretCandidates(rootDir) {
19283
+ const userConfigPath = resolve20(rootDir, ".pluxx-user.json");
19284
+ if (!existsSync24(userConfigPath)) return [];
19285
+ try {
19286
+ const payload = JSON.parse(readFileSync14(userConfigPath, "utf-8"));
19287
+ if (payload.secretStorage === "materialized") return [];
19288
+ const secretKeys = new Set(Array.isArray(payload.secretKeys) ? payload.secretKeys.filter((value) => typeof value === "string") : []);
19289
+ const secretEnv = new Set(Array.isArray(payload.secretEnv) ? payload.secretEnv.filter((value) => typeof value === "string") : []);
19290
+ const hasSecretMetadata = secretKeys.size > 0 || secretEnv.size > 0;
19291
+ const candidateLabels = /* @__PURE__ */ new Map();
19292
+ const recordCandidate = (rawValue, label) => {
19293
+ if (typeof rawValue !== "string") return;
19294
+ const value = rawValue.trim();
19295
+ if (!value) return;
19296
+ if (extractEnvReference(value)) return;
19297
+ if (isPlaceholderSecretValue(value)) return;
19298
+ const labels = candidateLabels.get(value) ?? /* @__PURE__ */ new Set();
19299
+ labels.add(label);
19300
+ candidateLabels.set(value, labels);
19301
+ };
19302
+ for (const [key, value] of Object.entries(payload.values ?? {})) {
19303
+ if (hasSecretMetadata ? !secretKeys.has(key) : !looksLegacySecretIdentifier(key)) continue;
19304
+ recordCandidate(value, `userConfig "${key}"`);
19305
+ }
19306
+ for (const [key, value] of Object.entries(payload.env ?? {})) {
19307
+ if (hasSecretMetadata ? !secretEnv.has(key) : !looksLegacySecretIdentifier(key)) continue;
19308
+ recordCandidate(value, `env "${key}"`);
19309
+ }
19310
+ return [...candidateLabels.entries()].map(([value, labels]) => ({
19311
+ value,
19312
+ labels: [...labels].sort()
19313
+ }));
19314
+ } catch {
19315
+ return [];
19316
+ }
19317
+ }
19318
+ function checkInstalledPlaintextSecrets(checks, rootDir) {
19319
+ const literalCandidates = collectInstalledPlaintextSecretCandidates(rootDir);
19320
+ const findings = /* @__PURE__ */ new Map();
19321
+ for (const relativePath of walkInstalledBundleFiles(rootDir)) {
19322
+ const content = readInstalledTextFile(rootDir, relativePath);
19323
+ if (!content) continue;
19324
+ const labels = /* @__PURE__ */ new Set();
19325
+ for (const candidate of literalCandidates) {
19326
+ if (!content.includes(candidate.value)) continue;
19327
+ for (const label of candidate.labels) labels.add(label);
19328
+ }
19329
+ for (const sentinel of TEST_SECRET_SENTINELS) {
19330
+ if (sentinel.pattern.test(content)) {
19331
+ labels.add(sentinel.label);
19332
+ }
19333
+ }
19334
+ if (labels.size > 0) {
19335
+ findings.set(relativePath, labels);
19336
+ }
19337
+ }
19338
+ if (findings.size === 0) {
19339
+ addCheck2(checks, {
19340
+ level: "success",
19341
+ code: "consumer-plaintext-secret-absent",
19342
+ title: "Installed bundle does not expose known plaintext secret material",
19343
+ detail: "No known prompted secret values or maintained test-secret sentinels were detected in scanned installed text files.",
19344
+ fix: "No action needed."
19345
+ });
19346
+ return;
19347
+ }
19348
+ const detail = [...findings.entries()].map(([path, labels]) => `${path} (${[...labels].sort().join(", ")})`).join("; ");
19349
+ addCheck2(checks, {
19350
+ level: "error",
19351
+ code: "consumer-plaintext-secret-leak",
19352
+ title: "Installed bundle contains plaintext secret material",
19353
+ detail: `Detected plaintext secret material in installed files: ${detail}.`,
19354
+ fix: "Rotate the affected credential, remove the leaking install, reinstall after the Pluxx secret-reference fix, and rerun pluxx doctor --consumer."
19355
+ });
19356
+ }
19184
19357
  function summarizeChecks2(checks) {
19185
19358
  const visibleChecks = checks.filter((check) => !check.code.startsWith("primitive-"));
19186
19359
  const errors = visibleChecks.filter((check) => check.level === "error").length;
@@ -19805,7 +19978,7 @@ function readCodexHooksFeatureFlag(filePath) {
19805
19978
  inFeaturesTable = sectionMatch[1].trim() === "features";
19806
19979
  continue;
19807
19980
  }
19808
- const dottedFeatureMatch = line.match(/^features\.(plugin_hooks|hooks|codex_hooks)\s*=\s*(.+)$/);
19981
+ const dottedFeatureMatch = line.match(/^features\.(hooks|codex_hooks)\s*=\s*(.+)$/);
19809
19982
  if (dottedFeatureMatch) {
19810
19983
  assignFeatureFlag(dottedFeatureMatch[1], dottedFeatureMatch[2]);
19811
19984
  continue;
@@ -19822,7 +19995,7 @@ function readCodexHooksFeatureFlag(filePath) {
19822
19995
  continue;
19823
19996
  }
19824
19997
  if (!inFeaturesTable) continue;
19825
- const featureMatch = line.match(/^(plugin_hooks|hooks|codex_hooks)\s*=\s*(.+)$/);
19998
+ const featureMatch = line.match(/^(hooks|codex_hooks)\s*=\s*(.+)$/);
19826
19999
  if (!featureMatch) continue;
19827
20000
  assignFeatureFlag(featureMatch[1], featureMatch[2]);
19828
20001
  }
@@ -20441,20 +20614,20 @@ function checkInstalledCodexHooksFeatureFlag(checks, rootDir, layout, options) {
20441
20614
  const enabledProbes = probes.filter((probe) => probe.enabled);
20442
20615
  const checkedPaths = probes.map((probe) => `${probe.scope} ${probe.exists ? "config" : "path"}: ${probe.path}${probe.exists ? "" : " (missing)"}`).join("; ");
20443
20616
  const describeEnabledFlags = (probe) => {
20444
- const enabledFlags = [];
20445
- if (probe.pluginBundledEnabled) enabledFlags.push(PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG);
20446
- if (probe.recommendedEnabled) enabledFlags.push(RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG);
20447
- if (probe.alternateEnabled) enabledFlags.push(ALTERNATE_CODEX_HOOKS_FEATURE_FLAG);
20448
- return enabledFlags.join(" + ");
20617
+ const enabledFlags = /* @__PURE__ */ new Set();
20618
+ if (probe.pluginBundledEnabled) enabledFlags.add(PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG);
20619
+ if (probe.recommendedEnabled) enabledFlags.add(RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG);
20620
+ if (probe.alternateEnabled) enabledFlags.add(ALTERNATE_CODEX_HOOKS_FEATURE_FLAG);
20621
+ return [...enabledFlags].join(" + ");
20449
20622
  };
20450
20623
  const generalOnlyProbes = probes.filter((probe) => !probe.pluginBundledEnabled && (probe.recommendedEnabled || probe.alternateEnabled));
20451
20624
  if (generalOnlyProbes.length > 0) {
20452
20625
  addCheck2(checks, {
20453
20626
  level: "warning",
20454
20627
  code: "consumer-codex-plugin-hooks-feature-flag-general-only",
20455
- title: "Codex config enables only general hook flags, not the plugin-bundled hook gate",
20456
- detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, but ${generalOnlyProbes.map((probe) => `${probe.scope} config ${probe.path}`).join(" and ")} enables only general hook flags (${generalOnlyProbes.map((probe) => describeEnabledFlags(probe)).join(" and ")}). Those general flags do not activate plugin-bundled hooks by themselves.`,
20457
- fix: `Enable \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` in the active Codex config, reload Codex, and rerun pluxx verify-install.`,
20628
+ title: "Codex config enables only deprecated hook flags",
20629
+ 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.`,
20630
+ fix: `Enable \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` in the active Codex config, reload Codex, and rerun pluxx verify-install.`,
20458
20631
  path: generalOnlyProbes[0].path
20459
20632
  });
20460
20633
  }
@@ -20464,8 +20637,8 @@ function checkInstalledCodexHooksFeatureFlag(checks, rootDir, layout, options) {
20464
20637
  level: "warning",
20465
20638
  code: "consumer-codex-hooks-feature-flag-legacy-only",
20466
20639
  title: "Codex config still uses the deprecated general hook compatibility flag",
20467
- detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, and the checked Codex config enables only the deprecated general hook flag \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG} = true\` in ${legacyOnlyProbes.map((probe) => `${probe.scope} config ${probe.path}`).join(" and ")}. That flag is not the plugin-bundled hook gate.`,
20468
- fix: `Prefer \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` only for non-plugin hook config if needed, and use \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` for plugin-bundled hooks. Reload Codex and rerun pluxx verify-install after updating the active config.`,
20640
+ 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.`,
20641
+ fix: `Use \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\`, reload Codex, and rerun pluxx verify-install after updating the active config.`,
20469
20642
  path: legacyOnlyProbes[0].path
20470
20643
  });
20471
20644
  }
@@ -20473,8 +20646,8 @@ function checkInstalledCodexHooksFeatureFlag(checks, rootDir, layout, options) {
20473
20646
  addCheck2(checks, {
20474
20647
  level: "success",
20475
20648
  code: "consumer-codex-hooks-feature-flag-enabled",
20476
- title: "Codex plugin-bundled hook feature flag found for this install",
20477
- detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, and the plugin hook gate was found in ${enabledProbes.map((probe) => `${probe.scope} config ${probe.path} (${describeEnabledFlags(probe)})`).join(" and ")}. Treat that as a prerequisite, not proof of live hook execution.`,
20649
+ title: "Codex hook feature flag found for this install",
20650
+ 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.`,
20478
20651
  fix: "No action needed.",
20479
20652
  path: enabledProbes[0].path
20480
20653
  });
@@ -20485,8 +20658,8 @@ function checkInstalledCodexHooksFeatureFlag(checks, rootDir, layout, options) {
20485
20658
  level: "warning",
20486
20659
  code: "consumer-codex-hooks-feature-flag-missing",
20487
20660
  title: "Codex plugin-bundled hook activation is missing its known feature-gate prerequisite",
20488
- detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, but \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` was not found in the checked Codex config layers. The general \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG}\` flag covers non-plugin hook config and defaults on, while \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` is deprecated and should not be treated as a plugin-bundled hook fallback. Checked ${checkedPaths}.${parseErrors.length > 0 ? ` Unparseable config: ${parseErrors.join("; ")}.` : ""}`,
20489
- fix: `Enable \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` in the relevant project or user Codex config, reload Codex, and rerun pluxx verify-install.`,
20661
+ 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("; ")}.` : ""}`,
20662
+ 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.`,
20490
20663
  path: probes.find((probe) => probe.exists)?.path ?? hooksReference
20491
20664
  });
20492
20665
  }
@@ -21020,6 +21193,7 @@ async function doctorConsumer(rootDir = process.cwd(), options = {}) {
21020
21193
  checkInstalledCodexPermissionCompanion(checks, rootDir, layout, options);
21021
21194
  checkInstalledPermissionHook(checks, rootDir, layout);
21022
21195
  checkInstalledUserConfig(checks, rootDir);
21196
+ checkInstalledPlaintextSecrets(checks, rootDir);
21023
21197
  checkInstalledEnvValidation(checks, rootDir);
21024
21198
  checkInstalledRuntimeScriptRoles(checks, rootDir);
21025
21199
  await checkInstalledMcpConfig(checks, rootDir, layout);
@@ -21132,7 +21306,7 @@ function printDoctorReport(report) {
21132
21306
 
21133
21307
  // src/cli/dev.ts
21134
21308
  import { watch } from "fs";
21135
- import { relative as relative12, resolve as resolve21 } from "path";
21309
+ import { relative as relative13, resolve as resolve21 } from "path";
21136
21310
  var WATCH_PATTERNS = [
21137
21311
  /^pluxx\.config\.(ts|js|json)$/,
21138
21312
  /^skills\//,
@@ -21158,7 +21332,7 @@ async function runDev(args2) {
21158
21332
  let pendingFile = null;
21159
21333
  const watcher = watch(rootDir, { recursive: true }, (_event, filename) => {
21160
21334
  if (!filename) return;
21161
- const rel = relative12(rootDir, resolve21(rootDir, filename));
21335
+ const rel = relative13(rootDir, resolve21(rootDir, filename));
21162
21336
  if (rel.startsWith("dist/") || rel.startsWith(".") || rel.includes("node_modules")) {
21163
21337
  return;
21164
21338
  }
@@ -21213,7 +21387,7 @@ async function runBuild(rootDir, targets) {
21213
21387
  }
21214
21388
 
21215
21389
  // src/cli/migrate.ts
21216
- import { basename as basename8, dirname as dirname8, relative as relative13, resolve as resolve22 } from "path";
21390
+ import { basename as basename8, dirname as dirname8, relative as relative14, resolve as resolve22 } from "path";
21217
21391
  import { existsSync as existsSync25, readdirSync as readdirSync11, mkdirSync as mkdirSync5, cpSync as cpSync4, readFileSync as readFileSync15, writeFileSync as writeFileSync5 } from "fs";
21218
21392
  function detectPlatform(pluginDir) {
21219
21393
  const checks = [
@@ -21586,7 +21760,7 @@ function parseCursorRuleFile(pluginDir, filePath) {
21586
21760
  ...globs !== void 0 ? { globs } : {},
21587
21761
  ...alwaysApply !== void 0 ? { alwaysApply } : {},
21588
21762
  ...body.trim() ? { content: body.trim() } : {},
21589
- path: `./${relative13(pluginDir, filePath).replace(/\\/g, "/")}`
21763
+ path: `./${relative14(pluginDir, filePath).replace(/\\/g, "/")}`
21590
21764
  };
21591
21765
  }
21592
21766
  function collectFiles(dir, predicate) {
@@ -21631,7 +21805,7 @@ function findInstructionSources(pluginDir, detection) {
21631
21805
  const extraPaths = /* @__PURE__ */ new Set();
21632
21806
  const platformOverrides = {};
21633
21807
  if (detection.platform === "cursor") {
21634
- const nestedAgents = collectFiles(pluginDir, (entry) => entry === "AGENTS.md").map((path) => `./${relative13(pluginDir, path).replace(/\\/g, "/")}`).filter((path) => path !== primary);
21808
+ const nestedAgents = collectFiles(pluginDir, (entry) => entry === "AGENTS.md").map((path) => `./${relative14(pluginDir, path).replace(/\\/g, "/")}`).filter((path) => path !== primary);
21635
21809
  if (nestedAgents.length > 0) {
21636
21810
  platformOverrides.cursor = {
21637
21811
  ...platformOverrides.cursor ?? {},
@@ -22350,7 +22524,7 @@ function normalizeMigratedOpenCodeAgents(destDir) {
22350
22524
  const normalized = [];
22351
22525
  for (const filePath of walkMarkdownFiles3(destDir)) {
22352
22526
  if (normalizeMigratedOpenCodeAgentFile(filePath)) {
22353
- normalized.push(relative13(destDir, filePath).replace(/\\/g, "/"));
22527
+ normalized.push(relative14(destDir, filePath).replace(/\\/g, "/"));
22354
22528
  }
22355
22529
  }
22356
22530
  return normalized.sort();
@@ -24497,8 +24671,15 @@ echo
24497
24671
  echo "Installed __DISPLAY_NAME__ across ${installerTargets.join(", ")}."
24498
24672
  `.replaceAll("__REPO__", "REPO_PLACEHOLDER").replaceAll("__DISPLAY_NAME__", "DISPLAY_PLACEHOLDER");
24499
24673
  }
24674
+ function collectInstallerUserConfigEntries(config, platforms) {
24675
+ const baseEntries = collectUserConfigEntries(config, platforms);
24676
+ return [
24677
+ ...baseEntries,
24678
+ ...collectNativeMcpAuthUserConfigEntries(config, platforms, baseEntries)
24679
+ ];
24680
+ }
24500
24681
  function renderInstallerUserConfigSnippet(config, platform, installDirVariable) {
24501
- const entries = collectUserConfigEntries(config, [platform]).map((entry) => ({
24682
+ const entries = collectInstallerUserConfigEntries(config, [platform]).map((entry) => ({
24502
24683
  key: entry.key,
24503
24684
  title: entry.title,
24504
24685
  type: entry.type ?? "string",
@@ -24506,6 +24687,7 @@ function renderInstallerUserConfigSnippet(config, platform, installDirVariable)
24506
24687
  envVar: entry.envVar ?? defaultUserConfigEnvVar(entry.key)
24507
24688
  }));
24508
24689
  if (entries.length === 0) return "";
24690
+ const preserveSecretReferences = platform === "codex";
24509
24691
  const promptLines = entries.map((entry) => {
24510
24692
  const functionName = entry.type === "secret" ? "pluxx_prompt_secret_config" : "pluxx_prompt_text_config";
24511
24693
  return `${functionName} ${JSON.stringify(entry.key)} ${JSON.stringify(entry.envVar)} ${JSON.stringify(entry.title)} ${entry.required ? "1" : "0"}`;
@@ -24516,6 +24698,8 @@ ${JSON.stringify(entries)}
24516
24698
  PLUXX_USER_CONFIG_JSON
24517
24699
  )"
24518
24700
  PLUXX_REUSED_USER_CONFIG=0
24701
+ PLUXX_PRESERVE_SECRET_REFS="${preserveSecretReferences ? "1" : "0"}"
24702
+ export PLUXX_PRESERVE_SECRET_REFS
24519
24703
 
24520
24704
  pluxx_is_placeholder_secret() {
24521
24705
  case "$1" in
@@ -24542,12 +24726,14 @@ const fs = require('fs')
24542
24726
  const filepath = process.env.PLUXX_SAVED_USER_CONFIG_PATH
24543
24727
  const key = process.env.PLUXX_SAVED_CONFIG_KEY
24544
24728
  const envVar = process.env.PLUXX_SAVED_CONFIG_ENV_VAR
24729
+ const preserveSecretRefs = process.env.PLUXX_PRESERVE_SECRET_REFS === '1'
24545
24730
 
24546
24731
  try {
24547
24732
  const payload = JSON.parse(fs.readFileSync(filepath, 'utf8'))
24548
24733
  const candidates = [
24549
24734
  payload && payload.env && envVar ? payload.env[envVar] : undefined,
24550
24735
  payload && payload.values && key ? payload.values[key] : undefined,
24736
+ preserveSecretRefs && payload && payload.envRefs && envVar && payload.envRefs[envVar] === envVar ? envVar : undefined,
24551
24737
  ]
24552
24738
 
24553
24739
  for (const candidate of candidates) {
@@ -24643,21 +24829,53 @@ const path = require('path')
24643
24829
 
24644
24830
  const installDir = process.env.PLUXX_INSTALL_DIR
24645
24831
  const spec = JSON.parse(process.env.PLUXX_USER_CONFIG_SPEC || '[]')
24832
+ const preserveSecretReferences = ${preserveSecretReferences ? "true" : "false"}
24646
24833
 
24647
24834
  if (installDir && spec.length > 0) {
24648
24835
  const env = {}
24649
24836
  const values = {}
24837
+ const envRefs = {}
24838
+ const secretKeys = []
24839
+ const secretEnv = []
24840
+ let hasSecret = false
24841
+ const secretEnvVars = new Set(
24842
+ spec
24843
+ .filter((entry) => entry && entry.type === 'secret' && typeof entry.envVar === 'string' && entry.envVar !== '')
24844
+ .map((entry) => entry.envVar),
24845
+ )
24650
24846
 
24651
24847
  for (const entry of spec) {
24848
+ if (entry.type === 'secret') {
24849
+ hasSecret = true
24850
+ if (preserveSecretReferences) {
24851
+ if (entry.key) secretKeys.push(entry.key)
24852
+ if (entry.envVar) secretEnv.push(entry.envVar)
24853
+ }
24854
+ }
24652
24855
  const value = process.env[entry.envVar]
24653
24856
  if (value === undefined || value === '') continue
24857
+ if (preserveSecretReferences && entry.type === 'secret') {
24858
+ envRefs[entry.envVar] = entry.envVar
24859
+ continue
24860
+ }
24654
24861
  values[entry.key] = value
24655
24862
  env[entry.envVar] = value
24656
24863
  }
24657
24864
 
24658
24865
  fs.writeFileSync(
24659
24866
  path.join(installDir, '.pluxx-user.json'),
24660
- JSON.stringify({ values, env }, null, 2) + '\\n',
24867
+ JSON.stringify(
24868
+ {
24869
+ ...(Object.keys(values).length > 0 ? { values } : {}),
24870
+ ...(Object.keys(env).length > 0 ? { env } : {}),
24871
+ ...(Object.keys(envRefs).length > 0 ? { envRefs } : {}),
24872
+ ...(hasSecret ? { secretStorage: preserveSecretReferences ? 'env-ref' : 'materialized' } : {}),
24873
+ ...(preserveSecretReferences && secretKeys.length > 0 ? { secretKeys: [...new Set(secretKeys)].sort() } : {}),
24874
+ ...(preserveSecretReferences && secretEnv.length > 0 ? { secretEnv: [...new Set(secretEnv)].sort() } : {}),
24875
+ },
24876
+ null,
24877
+ 2,
24878
+ ) + '\\n',
24661
24879
  )
24662
24880
 
24663
24881
  const envScriptPath = path.join(installDir, 'scripts/check-env.sh')
@@ -24670,7 +24888,10 @@ if (installDir && spec.length > 0) {
24670
24888
 
24671
24889
  const materialize = (value) =>
24672
24890
  typeof value === 'string'
24673
- ? value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_match, name) => env[name] || '${" + name + "}')
24891
+ ? value.replace(
24892
+ /\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g,
24893
+ (_match, name) => (preserveSecretReferences && secretEnvVars.has(name) ? '${" + name + "}' : (env[name] || '${" + name + "}')),
24894
+ )
24674
24895
  : value
24675
24896
 
24676
24897
  const materializeRecord = (record) => {
@@ -24694,7 +24915,7 @@ if (installDir && spec.length > 0) {
24694
24915
  server.env = materializeRecord(server.env)
24695
24916
  }
24696
24917
 
24697
- if (server.bearer_token_env_var && env[server.bearer_token_env_var]) {
24918
+ if (!preserveSecretReferences && server.bearer_token_env_var && env[server.bearer_token_env_var]) {
24698
24919
  server.http_headers = {
24699
24920
  ...(server.http_headers || {}),
24700
24921
  Authorization: 'Bearer ' + env[server.bearer_token_env_var],
@@ -24702,7 +24923,7 @@ if (installDir && spec.length > 0) {
24702
24923
  delete server.bearer_token_env_var
24703
24924
  }
24704
24925
 
24705
- if (server.env_http_headers && typeof server.env_http_headers === 'object') {
24926
+ if (!preserveSecretReferences && server.env_http_headers && typeof server.env_http_headers === 'object') {
24706
24927
  server.http_headers = {
24707
24928
  ...(server.http_headers || {}),
24708
24929
  }
@@ -24727,7 +24948,7 @@ NODE
24727
24948
  `;
24728
24949
  }
24729
24950
  function hasInstallerUserConfig(config, platform) {
24730
- return collectUserConfigEntries(config, [platform]).length > 0;
24951
+ return collectInstallerUserConfigEntries(config, [platform]).length > 0;
24731
24952
  }
24732
24953
  function renderInstallerSavedUserConfigCaptureSnippet(config, platform, installDirVariable) {
24733
24954
  if (!hasInstallerUserConfig(config, platform)) return "";
@@ -24893,13 +25114,13 @@ for (const line of lines) {
24893
25114
  continue
24894
25115
  }
24895
25116
  if (tableName === '') {
24896
- const dottedMatch = trimmed.match(/^features\\.plugin_hooks\\s*=\\s*(.+)$/)
25117
+ const dottedMatch = trimmed.match(/^features\\.hooks\\s*=\\s*(.+)$/)
24897
25118
  if (dottedMatch && isTomlTrue(dottedMatch[1])) process.exit(0)
24898
25119
  const inlineMatch = trimmed.match(/^features\\s*=\\s*(.+)$/)
24899
- if (inlineMatch && /\\bplugin_hooks\\s*=\\s*true\\b/i.test(inlineMatch[1])) process.exit(0)
25120
+ if (inlineMatch && /\\bhooks\\s*=\\s*true\\b/i.test(inlineMatch[1])) process.exit(0)
24900
25121
  }
24901
25122
  if (tableName !== 'features') continue
24902
- const match = trimmed.match(/^plugin_hooks\\s*=\\s*(.+)$/)
25123
+ const match = trimmed.match(/^hooks\\s*=\\s*(.+)$/)
24903
25124
  if (match && isTomlTrue(match[1])) process.exit(0)
24904
25125
  }
24905
25126
  process.exit(1)
@@ -24918,7 +25139,7 @@ NODE
24918
25139
  *)
24919
25140
  if [[ -r /dev/tty ]]; then
24920
25141
  echo "This Codex plugin bundle includes startup hooks." >/dev/tty
24921
- echo "Codex requires [features].plugin_hooks = true before plugin-bundled hooks can run." >/dev/tty
25142
+ echo "Codex requires [features].hooks = true before plugin-bundled hooks can run." >/dev/tty
24922
25143
  read -r -p "Enable Codex plugin-bundled hooks in $CODEX_CONFIG_PATH now? [Y/n] " PLUXX_CODEX_HOOKS_REPLY </dev/tty
24923
25144
  case "$PLUXX_CODEX_HOOKS_REPLY" in
24924
25145
  n|N|no|NO)
@@ -24990,7 +25211,7 @@ for (let index = 0; index < lines.length; index += 1) {
24990
25211
  if (/^features\\.[A-Za-z0-9_-]+\\s*=/.test(trimmed) && firstTopLevelFeaturesDotted < 0) {
24991
25212
  firstTopLevelFeaturesDotted = index
24992
25213
  }
24993
- if (/^features\\.plugin_hooks\\s*=/.test(trimmed)) {
25214
+ if (/^features\\.hooks\\s*=/.test(trimmed)) {
24994
25215
  topLevelPluginHooksDotted = index
24995
25216
  }
24996
25217
  if (/^features\\s*=\\s*\\{/.test(trimmed)) {
@@ -25009,28 +25230,28 @@ if (start >= 0) {
25009
25230
 
25010
25231
  let updated = false
25011
25232
  for (let index = start + 1; index < end; index += 1) {
25012
- if (/^plugin_hooks\\s*=/.test(stripTomlComment(lines[index]).trim())) {
25013
- lines[index] = 'plugin_hooks = true'
25233
+ if (/^hooks\\s*=/.test(stripTomlComment(lines[index]).trim())) {
25234
+ lines[index] = 'hooks = true'
25014
25235
  updated = true
25015
25236
  }
25016
25237
  }
25017
- if (!updated) lines.splice(start + 1, 0, 'plugin_hooks = true')
25238
+ if (!updated) lines.splice(start + 1, 0, 'hooks = true')
25018
25239
  } else if (topLevelPluginHooksDotted >= 0) {
25019
- lines[topLevelPluginHooksDotted] = 'features.plugin_hooks = true'
25240
+ lines[topLevelPluginHooksDotted] = 'features.hooks = true'
25020
25241
  } else if (firstTopLevelFeaturesDotted >= 0) {
25021
- lines.splice(firstTopLevelFeaturesDotted + 1, 0, 'features.plugin_hooks = true')
25242
+ lines.splice(firstTopLevelFeaturesDotted + 1, 0, 'features.hooks = true')
25022
25243
  } else if (topLevelInlineFeatures >= 0 && lines[topLevelInlineFeatures].includes('}')) {
25023
- if (/\\bplugin_hooks\\s*=/.test(lines[topLevelInlineFeatures])) {
25244
+ if (/\\bhooks\\s*=/.test(lines[topLevelInlineFeatures])) {
25024
25245
  lines[topLevelInlineFeatures] = lines[topLevelInlineFeatures].replace(
25025
- /\\bplugin_hooks\\s*=\\s*(true|false)\\b/i,
25026
- 'plugin_hooks = true',
25246
+ /\\bhooks\\s*=\\s*(true|false)\\b/i,
25247
+ 'hooks = true',
25027
25248
  )
25028
25249
  } else {
25029
- lines[topLevelInlineFeatures] = lines[topLevelInlineFeatures].replace(/}/, ', plugin_hooks = true }')
25250
+ lines[topLevelInlineFeatures] = lines[topLevelInlineFeatures].replace(/}/, ', hooks = true }')
25030
25251
  }
25031
25252
  } else {
25032
25253
  if (lines.length > 0 && lines[lines.length - 1] !== '') lines.push('')
25033
- lines.push('[features]', 'plugin_hooks = true')
25254
+ lines.push('[features]', 'hooks = true')
25034
25255
  }
25035
25256
 
25036
25257
  fs.mkdirSync(path.dirname(filepath), { recursive: true })
@@ -25041,7 +25262,7 @@ NODE
25041
25262
  else
25042
25263
  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
25043
25264
  echo "[features]" >&2
25044
- echo "plugin_hooks = true" >&2
25265
+ echo "hooks = true" >&2
25045
25266
  echo "Then restart or refresh Codex before relying on plugin startup hooks." >&2
25046
25267
  echo "Set PLUXX_CODEX_ENABLE_PLUGIN_HOOKS=1 before running this installer to enable it noninteractively." >&2
25047
25268
  fi
@@ -26235,6 +26456,7 @@ async function executeBehavioralCommand(platform, command2, cwd, timeoutMs) {
26235
26456
  };
26236
26457
  }
26237
26458
  return await new Promise((resolvePromise, reject) => {
26459
+ const startedAt = Date.now();
26238
26460
  const child = spawn5(command2[0], command2.slice(1), {
26239
26461
  cwd,
26240
26462
  detached: process.platform !== "win32",
@@ -26243,15 +26465,30 @@ async function executeBehavioralCommand(platform, command2, cwd, timeoutMs) {
26243
26465
  });
26244
26466
  const stdoutChunks = [];
26245
26467
  const stderrChunks = [];
26468
+ let settled = false;
26469
+ let timedOut = false;
26470
+ const timeout = setTimeout(() => {
26471
+ timedOut = true;
26472
+ signalBehavioralProcess(child, "SIGKILL");
26473
+ }, timeoutMs);
26246
26474
  child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk)));
26247
26475
  child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
26248
- child.on("error", reject);
26476
+ child.on("error", (error) => {
26477
+ if (settled) return;
26478
+ settled = true;
26479
+ clearTimeout(timeout);
26480
+ reject(error);
26481
+ });
26249
26482
  child.on("close", (code) => {
26483
+ if (settled) return;
26484
+ settled = true;
26485
+ clearTimeout(timeout);
26486
+ const exceededDeadline = timedOut || Date.now() - startedAt > timeoutMs;
26250
26487
  const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
26251
26488
  const stderr = Buffer.concat(stderrChunks).toString("utf-8");
26252
26489
  resolvePromise({
26253
- exitCode: code ?? 1,
26254
- response: stdout.trim() || stderr.trim()
26490
+ exitCode: exceededDeadline ? 124 : code ?? 1,
26491
+ response: exceededDeadline ? `behavioral runner timed out after ${timeoutMs}ms` : stdout.trim() || stderr.trim()
26255
26492
  });
26256
26493
  });
26257
26494
  });
@@ -26293,6 +26530,22 @@ async function commandSucceeds2(command2) {
26293
26530
  child.on("error", () => resolvePromise(false));
26294
26531
  });
26295
26532
  }
26533
+ function signalBehavioralProcess(child, signal) {
26534
+ if (child.exitCode != null || child.signalCode != null) {
26535
+ return;
26536
+ }
26537
+ if (process.platform !== "win32" && typeof child.pid === "number") {
26538
+ try {
26539
+ process.kill(-child.pid, signal);
26540
+ return;
26541
+ } catch {
26542
+ }
26543
+ }
26544
+ try {
26545
+ child.kill(signal);
26546
+ } catch {
26547
+ }
26548
+ }
26296
26549
  function truncate2(value, length) {
26297
26550
  if (value.length <= length) return value;
26298
26551
  return `${value.slice(0, Math.max(0, length - 3))}...`;
@@ -26314,7 +26567,7 @@ function shellQuote2(value) {
26314
26567
  // src/cli/discover-installed-mcp.ts
26315
26568
  import { existsSync as existsSync30, readFileSync as readFileSync21, realpathSync as realpathSync3 } from "fs";
26316
26569
  import { homedir as homedir3 } from "os";
26317
- import { isAbsolute as isAbsolute2, relative as relative14, resolve as resolve28 } from "path";
26570
+ import { isAbsolute as isAbsolute2, relative as relative15, resolve as resolve28 } from "path";
26318
26571
  var INSTALLED_MCP_HOSTS = ["claude-code", "cursor", "codex", "opencode"];
26319
26572
  function discoverInstalledMcpServers(options = {}) {
26320
26573
  const rootDir = options.rootDir ?? process.cwd();
@@ -26453,13 +26706,13 @@ function buildClaudeProjectSourceScope(projectPath, rootDir, homeDir) {
26453
26706
  return `projects/absolute/${normalizeScopePath(projectPath)}`;
26454
26707
  }
26455
26708
  function buildRelativeScopePath(baseDir, targetPath) {
26456
- const relativePath = relative14(baseDir, targetPath);
26709
+ const relativePath = relative15(baseDir, targetPath);
26457
26710
  if (!relativePath.startsWith("..") && !isAbsolute2(relativePath)) {
26458
26711
  return normalizeScopePath(relativePath);
26459
26712
  }
26460
26713
  if (!existsSync30(baseDir) || !existsSync30(targetPath)) return void 0;
26461
26714
  try {
26462
- const realRelativePath = relative14(realpathSync3(baseDir), realpathSync3(targetPath));
26715
+ const realRelativePath = relative15(realpathSync3(baseDir), realpathSync3(targetPath));
26463
26716
  if (realRelativePath.startsWith("..") || isAbsolute2(realRelativePath)) return void 0;
26464
26717
  return normalizeScopePath(realRelativePath);
26465
26718
  } catch {
@@ -26659,7 +26912,7 @@ function buildInstalledMcpSourceLabel(discovered, rootDir, homeDir) {
26659
26912
  return discovered.sourceScope ? `${base}:${discovered.sourceScope}` : base;
26660
26913
  }
26661
26914
  function buildRelativeSelectorLabel(prefix, baseDir, sourcePath) {
26662
- const relativePath = relative14(baseDir, sourcePath);
26915
+ const relativePath = relative15(baseDir, sourcePath);
26663
26916
  if (relativePath === "") return `${prefix}:.`;
26664
26917
  if (relativePath.startsWith("..") || isAbsolute2(relativePath)) return void 0;
26665
26918
  return `${prefix}:${relativePath.replace(/\\/g, "/")}`;