@orchid-labs/pluxx 0.1.6 → 0.1.8

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
@@ -85,7 +85,7 @@ var require_src = __commonJS({
85
85
  });
86
86
 
87
87
  // src/cli/index.ts
88
- import { readFileSync as readFileSync16 } from "fs";
88
+ import { readFileSync as readFileSync18 } from "fs";
89
89
 
90
90
  // src/config/load.ts
91
91
  import { resolve, extname, dirname } from "path";
@@ -5192,6 +5192,51 @@ async function generateClaudeFamilyOutputs(args2) {
5192
5192
  writeInstructions(config, rootDir, options, writeFile3)
5193
5193
  ]);
5194
5194
  }
5195
+ function shellSingleQuote(value) {
5196
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
5197
+ }
5198
+ function buildClaudeHookCommandWrapperScript(command2) {
5199
+ const serializedCommand = shellSingleQuote(command2);
5200
+ const exportLoader = [
5201
+ 'import { readFileSync } from "node:fs"',
5202
+ "",
5203
+ "const filepath = process.argv[1]",
5204
+ "if (!filepath) process.exit(0)",
5205
+ 'const payload = JSON.parse(readFileSync(filepath, "utf8"))',
5206
+ 'const env = payload && typeof payload === "object" && payload.env && typeof payload.env === "object"',
5207
+ " ? payload.env",
5208
+ " : {}",
5209
+ "",
5210
+ "for (const [key, value] of Object.entries(env)) {",
5211
+ " if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue",
5212
+ ' console.log(`export ${key}=${JSON.stringify(String(value ?? ""))}`)',
5213
+ "}"
5214
+ ].join("\n");
5215
+ return [
5216
+ "#!/usr/bin/env bash",
5217
+ "set -euo pipefail",
5218
+ "",
5219
+ 'PLUXX_PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"',
5220
+ 'PLUXX_USER_CONFIG_PATH="$PLUXX_PLUGIN_ROOT/.pluxx-user.json"',
5221
+ "",
5222
+ 'if [ -f "$PLUXX_USER_CONFIG_PATH" ]; then',
5223
+ " while IFS= read -r pluxx_export; do",
5224
+ ' if [ -n "$pluxx_export" ]; then',
5225
+ ' eval "$pluxx_export"',
5226
+ ' if [ -n "${CLAUDE_ENV_FILE:-}" ]; then',
5227
+ ` printf '%s\\n' "$pluxx_export" >> "$CLAUDE_ENV_FILE"`,
5228
+ " fi",
5229
+ " fi",
5230
+ " done < <(",
5231
+ ` node --input-type=module -e ${shellSingleQuote(exportLoader)} "$PLUXX_USER_CONFIG_PATH"`,
5232
+ " )",
5233
+ "fi",
5234
+ "",
5235
+ `PLUXX_HOOK_COMMAND=${serializedCommand}`,
5236
+ 'eval "$PLUXX_HOOK_COMMAND"',
5237
+ ""
5238
+ ].join("\n");
5239
+ }
5195
5240
  async function writeManifest(config, rootDir, options, writeJson) {
5196
5241
  const manifest = {
5197
5242
  name: config.name,
@@ -5269,7 +5314,9 @@ async function writeHooks(config, platform, options, writeJson, writeFile3) {
5269
5314
  const hooks = {};
5270
5315
  const mapEventName = options.mapEventName ?? defaultMapEventName;
5271
5316
  const usesPlatformManagedAuth = platform === "claude-code" && config.platforms?.["claude-code"]?.mcpAuth === "platform";
5317
+ const shouldWrapClaudeHookCommands = platform === "claude-code";
5272
5318
  const permissionScript = buildGeneratedPermissionHookScript(config.permissions);
5319
+ let generatedClaudeHookCommandCount = 0;
5273
5320
  if (permissionScript) {
5274
5321
  await writeFile3("hooks/pluxx-permissions.mjs", permissionScript);
5275
5322
  hooks.PreToolUse = [{
@@ -5299,12 +5346,21 @@ async function writeHooks(config, platform, options, writeJson, writeFile3) {
5299
5346
  if (commandEntries.length === 0) continue;
5300
5347
  hooks[mappedEvent] = [
5301
5348
  ...hooks[mappedEvent] ?? [],
5302
- ...commandEntries.map((entry) => ({
5303
- ...entry.matcher !== void 0 ? { matcher: entry.matcher } : {},
5304
- hooks: [{
5305
- type: "command",
5306
- command: entry.command.replace("${PLUGIN_ROOT}", `\${${options.pluginRootVar}}`)
5307
- }]
5349
+ ...await Promise.all(commandEntries.map(async (entry) => {
5350
+ const command2 = entry.command.replace("${PLUGIN_ROOT}", `\${${options.pluginRootVar}}`);
5351
+ const finalCommand = shouldWrapClaudeHookCommands ? await (async () => {
5352
+ generatedClaudeHookCommandCount += 1;
5353
+ const relativePath = `hooks/pluxx-hook-command-${generatedClaudeHookCommandCount}.sh`;
5354
+ await writeFile3(relativePath, buildClaudeHookCommandWrapperScript(command2));
5355
+ return `bash "\${${options.pluginRootVar}}/${relativePath}"`;
5356
+ })() : command2;
5357
+ return {
5358
+ ...entry.matcher !== void 0 ? { matcher: entry.matcher } : {},
5359
+ hooks: [{
5360
+ type: "command",
5361
+ command: finalCommand
5362
+ }]
5363
+ };
5308
5364
  }))
5309
5365
  ];
5310
5366
  }
@@ -7084,14 +7140,14 @@ async function build(config, rootDir, options = {}) {
7084
7140
  }
7085
7141
 
7086
7142
  // src/cli/agent.ts
7087
- import { existsSync as existsSync21, readdirSync as readdirSync7, readFileSync as readFileSync9, statSync as statSync4 } from "fs";
7143
+ import { existsSync as existsSync22, readdirSync as readdirSync7, readFileSync as readFileSync9, statSync as statSync4 } from "fs";
7088
7144
  import { chmod, copyFile, mkdir as mkdir3, mkdtemp, readFile as readFile3, rm as rm2 } from "fs/promises";
7089
7145
  import { homedir, tmpdir as tmpdir2 } from "os";
7090
- import { relative as relative8, resolve as resolve14 } from "path";
7146
+ import { relative as relative9, resolve as resolve14 } from "path";
7091
7147
  import { spawn as spawn2 } from "child_process";
7092
7148
 
7093
7149
  // src/cli/lint.ts
7094
- import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync6 } from "fs";
7150
+ import { existsSync as existsSync17, lstatSync, readdirSync as readdirSync5, readFileSync as readFileSync6 } from "fs";
7095
7151
  import { resolve as resolve9, relative as relative6, basename as basename4, dirname as dirname3 } from "path";
7096
7152
 
7097
7153
  // src/validation/platform-rules.ts
@@ -8452,10 +8508,11 @@ function lintMcpUrls(config, issues) {
8452
8508
  }
8453
8509
  }
8454
8510
  }
8455
- function lintMcpRuntimeState(config, issues) {
8511
+ function lintMcpRuntimeState(rootDir, config, issues) {
8456
8512
  if (!config.mcp) return;
8457
8513
  const claudeUsesPlatformAuth = config.targets.includes("claude-code") && config.platforms?.["claude-code"]?.mcpAuth === "platform";
8458
8514
  const cursorUsesPlatformAuth = config.targets.includes("cursor") && config.platforms?.cursor?.mcpAuth === "platform";
8515
+ const passthroughDirs = (config.passthrough ?? []).map((entry) => resolveBundledPassthroughDir(rootDir, entry)).filter((entry) => Boolean(entry));
8459
8516
  for (const [serverName, server] of Object.entries(config.mcp)) {
8460
8517
  if (server.transport === "stdio") {
8461
8518
  pushIssue(issues, {
@@ -8465,6 +8522,17 @@ function lintMcpRuntimeState(config, issues) {
8465
8522
  file: "pluxx.config.ts",
8466
8523
  platform: "MCP"
8467
8524
  });
8525
+ for (const runtimePath of findLocalStdioRuntimePaths(rootDir, server)) {
8526
+ if (passthroughDirs.some((dir) => runtimePath === dir || runtimePath.startsWith(`${dir}/`))) continue;
8527
+ const relativeDir = `./${relative6(rootDir, runtimePath).replace(/\\/g, "/")}/`;
8528
+ pushIssue(issues, {
8529
+ level: "warning",
8530
+ code: "mcp-stdio-runtime-unbundled",
8531
+ message: `MCP server "${serverName}" references a project-local stdio runtime under ${relativeDir}, but that directory is not included in passthrough. Installed bundles may ship the MCP config without the executable payload.`,
8532
+ file: "pluxx.config.ts",
8533
+ platform: "MCP"
8534
+ });
8535
+ }
8468
8536
  }
8469
8537
  const runtimeAuthTargets = [];
8470
8538
  if (server.auth?.type === "platform") {
@@ -8488,6 +8556,37 @@ function lintMcpRuntimeState(config, issues) {
8488
8556
  }
8489
8557
  }
8490
8558
  }
8559
+ function resolveBundledPassthroughDir(rootDir, entry) {
8560
+ const resolvedPath = resolve9(rootDir, entry);
8561
+ if (!existsSync17(resolvedPath)) return null;
8562
+ try {
8563
+ const stats = lstatSync(resolvedPath);
8564
+ if (!stats.isDirectory()) return null;
8565
+ return resolvedPath.replace(/\/+$/, "");
8566
+ } catch {
8567
+ return null;
8568
+ }
8569
+ }
8570
+ function findLocalStdioRuntimePaths(rootDir, server) {
8571
+ if (server.transport !== "stdio") return [];
8572
+ const runtimeDirs = /* @__PURE__ */ new Set();
8573
+ const candidates = [server.command, ...server.args ?? []];
8574
+ for (const candidate of candidates) {
8575
+ if (!isLikelyLocalRuntimePath(candidate)) continue;
8576
+ const resolvedPath = resolve9(rootDir, candidate);
8577
+ if (!existsSync17(resolvedPath)) continue;
8578
+ try {
8579
+ const stats = lstatSync(resolvedPath);
8580
+ const runtimeDir = stats.isDirectory() ? resolvedPath : dirname3(resolvedPath);
8581
+ runtimeDirs.add(runtimeDir.replace(/\/+$/, ""));
8582
+ } catch {
8583
+ }
8584
+ }
8585
+ return [...runtimeDirs].sort();
8586
+ }
8587
+ function isLikelyLocalRuntimePath(value) {
8588
+ return value.startsWith("./") || value.startsWith("../") || value.startsWith(".\\") || value.startsWith("..\\");
8589
+ }
8491
8590
  function lintCodexHookCompatibility(config, issues) {
8492
8591
  if (!isCodexTargetEnabled(config) || !config.hooks) return;
8493
8592
  for (const hookEvent of Object.keys(config.hooks)) {
@@ -8721,15 +8820,19 @@ function lintOpenCodeAgentFrontmatter(dir, config, issues) {
8721
8820
  const agents = readCanonicalAgentFiles(resolve9(dir, config.agents));
8722
8821
  for (const agent of agents) {
8723
8822
  if (!("tools" in agent.frontmatter)) continue;
8823
+ if (hasCanonicalAgentPermission(agent.frontmatter.permission)) continue;
8724
8824
  pushIssue(issues, {
8725
8825
  level: "warning",
8726
8826
  code: "opencode-agent-tools-deprecated",
8727
- message: "OpenCode agent `tools` is deprecated. Pluxx will translate legacy agent tools into permission-first OpenCode output where possible, but canonical agents should prefer `permission`.",
8827
+ message: "OpenCode agent `tools` is deprecated. Add canonical `permission` frontmatter so Pluxx can keep the emitted OpenCode agent permission-first even when shared cross-host authoring still carries legacy tool hints.",
8728
8828
  file: relative6(dir, agent.filePath).replace(/\\/g, "/"),
8729
8829
  platform: "OpenCode"
8730
8830
  });
8731
8831
  }
8732
8832
  }
8833
+ function hasCanonicalAgentPermission(value) {
8834
+ return !!value && typeof value === "object" && !Array.isArray(value);
8835
+ }
8733
8836
  function lintAbsolutePaths(config, issues) {
8734
8837
  const absolutePathPattern = /^\/[a-zA-Z]|^[A-Z]:\\/;
8735
8838
  if (config.hooks) {
@@ -9172,7 +9275,7 @@ async function lintProject(dir = process.cwd(), options = {}) {
9172
9275
  lintAgentIsolation(agentFiles, issues, frontmatterCache);
9173
9276
  lintOpenCodeAgentFrontmatter(dir, { ...lintConfig, agents: lintConfig.agents ?? "./agents/" }, issues);
9174
9277
  lintMcpUrls(lintConfig, issues);
9175
- lintMcpRuntimeState(lintConfig, issues);
9278
+ lintMcpRuntimeState(dir, lintConfig, issues);
9176
9279
  lintBrandMetadata(lintConfig, issues);
9177
9280
  lintCodexOverrides(lintConfig, issues);
9178
9281
  lintCodexHookCompatibility(lintConfig, issues);
@@ -9256,19 +9359,30 @@ async function runLint(dir = process.cwd()) {
9256
9359
  }
9257
9360
 
9258
9361
  // src/cli/test.ts
9259
- import { existsSync as existsSync19 } from "fs";
9362
+ import { existsSync as existsSync20 } from "fs";
9260
9363
  import { resolve as resolve12 } from "path";
9261
9364
 
9262
9365
  // src/cli/eval.ts
9263
- import { existsSync as existsSync18, readFileSync as readFileSync7 } from "fs";
9366
+ import { existsSync as existsSync19, readFileSync as readFileSync7 } from "fs";
9264
9367
  import { resolve as resolve11 } from "path";
9265
9368
 
9266
9369
  // src/cli/init-from-mcp.ts
9370
+ import { existsSync as existsSync18, lstatSync as lstatSync2 } from "fs";
9267
9371
  import { mkdir as mkdir2 } from "fs/promises";
9268
- import { basename as basename5, resolve as resolve10 } from "path";
9372
+ import { basename as basename5, dirname as dirname4, relative as relative7, resolve as resolve10 } from "path";
9269
9373
 
9270
9374
  // src/user-config.ts
9271
9375
  var ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
9376
+ var PLACEHOLDER_SECRET_PATTERNS = [
9377
+ /\bdummy\b/i,
9378
+ /\bplaceholder\b/i,
9379
+ /\bexample\b/i,
9380
+ /\bchangeme\b/i,
9381
+ /\breplace[_ -]?me\b/i,
9382
+ /\byour[_ -]?(api[_ -]?)?key\b/i,
9383
+ /\bapi[_ -]?key[_ -]?here\b/i,
9384
+ /\btoken[_ -]?here\b/i
9385
+ ];
9272
9386
  function normalizeUserConfigKey(value) {
9273
9387
  return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/[._/\s]+/g, "-").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
9274
9388
  }
@@ -9283,6 +9397,12 @@ function extractEnvReference(value) {
9283
9397
  const match = value.match(/^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/);
9284
9398
  return match?.[1];
9285
9399
  }
9400
+ function isPlaceholderSecretValue(value) {
9401
+ if (typeof value !== "string") return false;
9402
+ const normalized = value.trim();
9403
+ if (normalized === "") return false;
9404
+ return PLACEHOLDER_SECRET_PATTERNS.some((pattern) => pattern.test(normalized));
9405
+ }
9286
9406
  function isRuntimePlatformManaged(config, target, server) {
9287
9407
  if (server.transport === "stdio") return false;
9288
9408
  if (server.auth?.type === "platform") {
@@ -9459,6 +9579,42 @@ var WORKFLOW_SKILL_DEFINITIONS = [
9459
9579
  title: "General Research",
9460
9580
  description: "Handle broad search and query workflows when there is not a more specific product surface match.",
9461
9581
  match: ["search", "query", "lookup", "look up", "discover", "find"]
9582
+ },
9583
+ {
9584
+ key: "web-research",
9585
+ title: "Web Research",
9586
+ description: "Search the web, fetch pages, and synthesize public source-backed research.",
9587
+ match: ["web", "website", "url", "page", "fetch", "crawl", "scrape", "search", "result", "source"]
9588
+ },
9589
+ {
9590
+ key: "source-review",
9591
+ title: "Source Review",
9592
+ description: "Audit sources, citations, evidence quality, and duplicate or weak research results.",
9593
+ match: ["source", "citation", "evidence", "audit", "review", "quality", "rank", "dedupe", "duplicate"]
9594
+ },
9595
+ {
9596
+ key: "content-extraction",
9597
+ title: "Content Extraction",
9598
+ description: "Extract structured content, entities, and summaries from pages, documents, or search results.",
9599
+ match: ["extract", "parse", "content", "summary", "summarize", "document", "html", "markdown"]
9600
+ },
9601
+ {
9602
+ key: "knowledge-search",
9603
+ title: "Knowledge Search",
9604
+ description: "Search docs, knowledge bases, papers, repositories, and internal reference material.",
9605
+ match: ["docs", "documentation", "knowledge", "paper", "papers", "repo", "repository", "api", "reference"]
9606
+ },
9607
+ {
9608
+ key: "news-monitoring",
9609
+ title: "News Monitoring",
9610
+ description: "Find recent news, launches, announcements, and time-bounded developments.",
9611
+ match: ["news", "recent", "latest", "launch", "announcement", "announced", "date", "published"]
9612
+ },
9613
+ {
9614
+ key: "code-research",
9615
+ title: "Code Research",
9616
+ description: "Find implementation docs, code examples, API usage, migration notes, and troubleshooting context.",
9617
+ match: ["code", "sdk", "api", "github", "example", "migration", "error", "troubleshoot", "implementation"]
9462
9618
  }
9463
9619
  ];
9464
9620
  var WORKFLOW_MATCH_MIN_SCORE = 2;
@@ -9541,6 +9697,8 @@ async function planMcpScaffold(options) {
9541
9697
  options.persistedSkills,
9542
9698
  options.toolRenames
9543
9699
  );
9700
+ const skillGrouping = options.skillGrouping ?? "workflow";
9701
+ const plannedCommands = planCommandScaffolds(plannedSkills, skillGrouping);
9544
9702
  const description = options.description ?? deriveScaffoldDescription({
9545
9703
  displayName,
9546
9704
  introspection: options.introspection,
@@ -9548,6 +9706,7 @@ async function planMcpScaffold(options) {
9548
9706
  });
9549
9707
  const serverName = options.serverName ?? toKebabCase(options.introspection.serverInfo.name) ?? pluginName;
9550
9708
  const permissions = options.permissions ?? (options.approveMcpTools ? { allow: [`MCP(${serverName}.*)`] } : void 0);
9709
+ const passthroughPaths = inferLocalRuntimePassthroughPaths(options.rootDir, options.source);
9551
9710
  const runtimeAuthMode = options.runtimeAuthMode ?? (options.source.transport !== "stdio" && options.source.auth?.type === "platform" ? "platform" : "inline");
9552
9711
  const instructionsPath = resolve10(options.rootDir, "INSTRUCTIONS.md");
9553
9712
  const skillRoot = resolve10(options.rootDir, "skills");
@@ -9589,9 +9748,10 @@ async function planMcpScaffold(options) {
9589
9748
  userConfig,
9590
9749
  hooks: generatedHooks.hookEntries,
9591
9750
  scriptsPath: generatedHooks.scriptsPath,
9751
+ passthroughPaths,
9592
9752
  runtimeAuthMode,
9593
9753
  permissions,
9594
- commandsPath: "./commands/"
9754
+ commandsPath: plannedCommands.length > 0 ? "./commands/" : void 0
9595
9755
  })
9596
9756
  );
9597
9757
  await addPlannedFile(
@@ -9621,8 +9781,6 @@ async function planMcpScaffold(options) {
9621
9781
  for (const skill of plannedSkills) {
9622
9782
  const relativeSkillPath = `skills/${skill.dirName}`;
9623
9783
  const skillPath = resolve10(skillRoot, skill.dirName, "SKILL.md");
9624
- const relativeCommandPath = `commands/${skill.dirName}.md`;
9625
- const commandPath = resolve10(commandsRoot, `${skill.dirName}.md`);
9626
9784
  await addPlannedFile(
9627
9785
  `${relativeSkillPath}/SKILL.md`,
9628
9786
  wrapManagedMarkdown(
@@ -9636,6 +9794,10 @@ async function planMcpScaffold(options) {
9636
9794
  );
9637
9795
  skillDirectories.push(relativeSkillPath);
9638
9796
  generatedFiles.push(`${relativeSkillPath}/SKILL.md`);
9797
+ }
9798
+ for (const skill of plannedCommands) {
9799
+ const relativeCommandPath = `commands/${skill.dirName}.md`;
9800
+ const commandPath = resolve10(commandsRoot, `${skill.dirName}.md`);
9639
9801
  await addPlannedFile(
9640
9802
  relativeCommandPath,
9641
9803
  buildCommandContent(
@@ -9655,7 +9817,7 @@ async function planMcpScaffold(options) {
9655
9817
  introspection: options.introspection,
9656
9818
  pluginName,
9657
9819
  displayName,
9658
- skillGrouping: options.skillGrouping ?? "workflow",
9820
+ skillGrouping,
9659
9821
  requestedHookMode: options.hookMode ?? "none",
9660
9822
  generatedHookMode: generatedHooks.mode,
9661
9823
  generatedHookEvents: Object.keys(generatedHooks.hookEntries ?? {}),
@@ -9882,6 +10044,19 @@ function buildCommandContent(skill, existingContent) {
9882
10044
  }
9883
10045
  );
9884
10046
  }
10047
+ function hasStrongCommandShape(skill, grouping) {
10048
+ if (grouping === "tool") return true;
10049
+ if (skill.tools.length > 1) return true;
10050
+ if (skill.prompts.length > 0) return true;
10051
+ if (skill.resources.length > 0 || skill.resourceTemplates.length > 0) return true;
10052
+ const requiredFields = skill.tools.flatMap((tool) => getTopLevelSchemaFields(tool.inputSchema).filter((field) => field.required));
10053
+ return requiredFields.length >= 2;
10054
+ }
10055
+ function planCommandScaffolds(plannedSkills, grouping) {
10056
+ const commands = plannedSkills.filter((skill) => hasStrongCommandShape(skill, grouping));
10057
+ if (commands.length > 0) return commands;
10058
+ return plannedSkills.slice(0, 1);
10059
+ }
9885
10060
  function formatArgumentHintFrontmatter(value) {
9886
10061
  const trimmed = value.trim();
9887
10062
  if (!trimmed) return '""';
@@ -9986,6 +10161,8 @@ function buildConfigTemplate(input) {
9986
10161
  userConfig: ${serializeUserConfig(input.userConfig)},
9987
10162
  ` : "";
9988
10163
  const scriptsBlock = input.scriptsPath ? ` scripts: ${JSON.stringify(input.scriptsPath)},
10164
+ ` : "";
10165
+ const passthroughBlock = input.passthroughPaths && input.passthroughPaths.length > 0 ? ` passthrough: ${JSON.stringify(input.passthroughPaths)},
9989
10166
  ` : "";
9990
10167
  const commandsBlock = input.commandsPath ? ` commands: ${JSON.stringify(input.commandsPath)},
9991
10168
  ` : "";
@@ -10021,6 +10198,7 @@ ${commandsBlock}
10021
10198
  instructions: './INSTRUCTIONS.md',
10022
10199
  ${userConfigBlock}
10023
10200
  ${scriptsBlock}
10201
+ ${passthroughBlock}
10024
10202
 
10025
10203
  mcp: {
10026
10204
  ${mcpBlock}
@@ -10037,6 +10215,29 @@ ${platformsBlock}
10037
10215
  })
10038
10216
  `;
10039
10217
  }
10218
+ function inferLocalRuntimePassthroughPaths(rootDir, source) {
10219
+ if (source.transport !== "stdio") return [];
10220
+ const passthrough = /* @__PURE__ */ new Set();
10221
+ const candidates = [source.command, ...source.args ?? []];
10222
+ for (const candidate of candidates) {
10223
+ if (!isLikelyLocalRuntimePath2(candidate)) continue;
10224
+ const resolvedPath = resolve10(rootDir, candidate);
10225
+ if (!existsSync18(resolvedPath)) continue;
10226
+ const stats = lstatSync2(resolvedPath);
10227
+ const runtimeDir = stats.isDirectory() ? resolvedPath : dirname4(resolvedPath);
10228
+ const normalized = normalizePassthroughDir(rootDir, runtimeDir);
10229
+ if (normalized) passthrough.add(normalized);
10230
+ }
10231
+ return [...passthrough].sort();
10232
+ }
10233
+ function isLikelyLocalRuntimePath2(value) {
10234
+ return value.startsWith("./") || value.startsWith("../") || value.startsWith(".\\") || value.startsWith("..\\");
10235
+ }
10236
+ function normalizePassthroughDir(rootDir, dirPath) {
10237
+ const relativePath = relative7(rootDir, dirPath).replace(/\\/g, "/").replace(/\/+$/, "");
10238
+ if (!relativePath || relativePath === ".") return null;
10239
+ return `./${relativePath}/`;
10240
+ }
10040
10241
  function buildMcpBlock(serverName, source) {
10041
10242
  if (source.transport === "stdio") {
10042
10243
  const argsLine = source.args && source.args.length > 0 ? `,
@@ -11076,7 +11277,7 @@ function summarizeChecks(checks) {
11076
11277
  }
11077
11278
  async function loadMcpScaffoldMetadata(rootDir) {
11078
11279
  const metadataPath = resolve11(rootDir, MCP_SCAFFOLD_METADATA_PATH);
11079
- if (!existsSync18(metadataPath)) {
11280
+ if (!existsSync19(metadataPath)) {
11080
11281
  return null;
11081
11282
  }
11082
11283
  const parsed = JSON.parse(readFileSync7(metadataPath, "utf-8"));
@@ -11107,7 +11308,7 @@ function collectSkillPromptLabels(skill) {
11107
11308
  function evaluateInstructions(rootDir, metadata, checks) {
11108
11309
  const relativePath = "INSTRUCTIONS.md";
11109
11310
  const filePath = resolve11(rootDir, relativePath);
11110
- if (!existsSync18(filePath)) {
11311
+ if (!existsSync19(filePath)) {
11111
11312
  addCheck(checks, {
11112
11313
  level: "error",
11113
11314
  code: "instructions-missing",
@@ -11153,7 +11354,7 @@ function evaluateSkills(rootDir, metadata, checks) {
11153
11354
  for (const skill of metadata.skills) {
11154
11355
  const relativePath = `skills/${skill.dirName}/SKILL.md`;
11155
11356
  const filePath = resolve11(rootDir, relativePath);
11156
- if (!existsSync18(filePath)) {
11357
+ if (!existsSync19(filePath)) {
11157
11358
  failures.push({ path: relativePath, missing: ["skill file"] });
11158
11359
  continue;
11159
11360
  }
@@ -11196,6 +11397,11 @@ function evaluateSkills(rootDir, metadata, checks) {
11196
11397
  function hasManagedCommands(metadata) {
11197
11398
  return metadata.managedFiles.some((file) => file.startsWith("commands/"));
11198
11399
  }
11400
+ function managedCommandSkillNames(metadata) {
11401
+ return new Set(
11402
+ metadata.managedFiles.filter((file) => file.startsWith("commands/") && file.endsWith(".md")).map((file) => file.replace(/^commands\//, "").replace(/\.md$/, ""))
11403
+ );
11404
+ }
11199
11405
  function evaluateCommands(rootDir, metadata, checks) {
11200
11406
  if (!hasManagedCommands(metadata)) {
11201
11407
  addCheck(checks, {
@@ -11208,10 +11414,11 @@ function evaluateCommands(rootDir, metadata, checks) {
11208
11414
  return;
11209
11415
  }
11210
11416
  const failures = [];
11211
- for (const skill of metadata.skills) {
11417
+ const commandSkillNames = managedCommandSkillNames(metadata);
11418
+ for (const skill of metadata.skills.filter((entry) => commandSkillNames.has(entry.dirName))) {
11212
11419
  const relativePath = `commands/${skill.dirName}.md`;
11213
11420
  const filePath = resolve11(rootDir, relativePath);
11214
- if (!existsSync18(filePath)) {
11421
+ if (!existsSync19(filePath)) {
11215
11422
  failures.push({ path: relativePath, missing: ["command file"] });
11216
11423
  continue;
11217
11424
  }
@@ -11256,10 +11463,36 @@ function evaluateCommands(rootDir, metadata, checks) {
11256
11463
  level: "success",
11257
11464
  code: "command-quality-contract",
11258
11465
  title: "Command scaffolds expose expected routing guidance and related surfaces",
11259
- detail: `Checked ${metadata.skills.length} generated command file(s) for argument hints, tool routing, and related discovery surfaces.`,
11466
+ detail: `Checked ${commandSkillNames.size} generated command file(s) for argument hints, tool routing, and related discovery surfaces.`,
11260
11467
  fix: "No action needed."
11261
11468
  });
11262
11469
  }
11470
+ function evaluateScaffoldArchitecture(metadata, checks) {
11471
+ const commandSkillNames = managedCommandSkillNames(metadata);
11472
+ if (metadata.tools.length === 0 || commandSkillNames.size === 0) return;
11473
+ const singletonSkills = metadata.skills.filter((skill) => (skill.toolNames?.length ?? 0) === 1);
11474
+ const commandBackedSingletons = singletonSkills.filter((skill) => commandSkillNames.has(skill.dirName));
11475
+ const commandPerTool = commandSkillNames.size >= metadata.tools.length && commandBackedSingletons.length >= Math.ceil(commandSkillNames.size * 0.8);
11476
+ if (commandPerTool) {
11477
+ addCheck(checks, {
11478
+ level: "warning",
11479
+ code: "scaffold-command-per-tool-shape",
11480
+ title: "Scaffold looks like command-per-tool output",
11481
+ detail: `This scaffold exposes ${commandSkillNames.size} command(s) for ${metadata.tools.length} MCP tool(s), with most commands wrapping a single tool.`,
11482
+ fix: "Prefer workflow-level commands and keep singleton raw tool wrappers as skills only unless the tool has a strong user-facing workflow or required arguments."
11483
+ });
11484
+ }
11485
+ const singletonRatio = singletonSkills.length / Math.max(metadata.skills.length, 1);
11486
+ if (metadata.skills.length >= 6 && singletonRatio >= 0.75) {
11487
+ addCheck(checks, {
11488
+ level: "warning",
11489
+ code: "scaffold-singleton-heavy-taxonomy",
11490
+ title: "Scaffold taxonomy is singleton-heavy",
11491
+ detail: `${singletonSkills.length} of ${metadata.skills.length} skill(s) wrap a single MCP tool, which usually means the plugin is still shaped like an API surface rather than user workflows.`,
11492
+ fix: "Run `pluxx agent run taxonomy` or `pluxx autopilot --mode standard` to merge raw tools into product workflows before shipping."
11493
+ });
11494
+ }
11495
+ }
11263
11496
  function evaluateAgentContext(contextContent, metadata, checks) {
11264
11497
  const missing = [];
11265
11498
  if (((metadata.resources?.length ?? 0) > 0 || (metadata.resourceTemplates?.length ?? 0) > 0 || (metadata.prompts?.length ?? 0) > 0) && !contextContent.includes("## MCP Discovery Surfaces")) {
@@ -11399,6 +11632,7 @@ async function runEvalSuite(options = {}) {
11399
11632
  evaluateInstructions(rootDir, metadata, checks);
11400
11633
  evaluateSkills(rootDir, metadata, checks);
11401
11634
  evaluateCommands(rootDir, metadata, checks);
11635
+ evaluateScaffoldArchitecture(metadata, checks);
11402
11636
  }
11403
11637
  evaluateAgentContext(contextContent, metadata, checks);
11404
11638
  for (const kind of AGENT_PROMPT_KINDS) {
@@ -11463,7 +11697,7 @@ async function runTestSuite(options = {}) {
11463
11697
  const evalReport = await runEvalSuite({ rootDir });
11464
11698
  const checks = targets.map((platform) => {
11465
11699
  const requiredPath = SMOKE_PATHS[platform];
11466
- const ok = existsSync19(resolve12(rootDir, config.outDir, platform, requiredPath));
11700
+ const ok = existsSync20(resolve12(rootDir, config.outDir, platform, requiredPath));
11467
11701
  return { platform, requiredPath, ok };
11468
11702
  });
11469
11703
  return {
@@ -11520,8 +11754,8 @@ function printTestResult(result) {
11520
11754
  }
11521
11755
 
11522
11756
  // src/cli/sync-from-mcp.ts
11523
- import { cpSync as cpSync2, existsSync as existsSync20, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2, readdirSync as readdirSync6, rmdirSync, writeFileSync as writeFileSync3 } from "fs";
11524
- import { dirname as dirname4, isAbsolute, relative as relative7, resolve as resolve13 } from "path";
11757
+ import { cpSync as cpSync2, existsSync as existsSync21, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2, readdirSync as readdirSync6, rmdirSync, writeFileSync as writeFileSync3 } from "fs";
11758
+ import { dirname as dirname5, isAbsolute, relative as relative8, resolve as resolve13 } from "path";
11525
11759
  import { tmpdir } from "os";
11526
11760
 
11527
11761
  // src/mcp/introspect.ts
@@ -11803,10 +12037,10 @@ async function createSseClient(server) {
11803
12037
  let resolveEndpoint;
11804
12038
  let rejectEndpoint;
11805
12039
  let endpointSettled = false;
11806
- const endpointReady = new Promise((resolve24, reject) => {
12040
+ const endpointReady = new Promise((resolve25, reject) => {
11807
12041
  resolveEndpoint = (value) => {
11808
12042
  endpointSettled = true;
11809
- resolve24(value);
12043
+ resolve25(value);
11810
12044
  };
11811
12045
  rejectEndpoint = (error) => {
11812
12046
  endpointSettled = true;
@@ -11943,7 +12177,7 @@ async function createSseClient(server) {
11943
12177
  async request(method, params) {
11944
12178
  const requestId = nextRequestId();
11945
12179
  const endpoint = endpointUrl ?? await endpointReady;
11946
- const resultPromise = new Promise((resolve24, reject) => {
12180
+ const resultPromise = new Promise((resolve25, reject) => {
11947
12181
  const timeout = setTimeout(() => {
11948
12182
  pending.delete(requestId);
11949
12183
  reject(new McpIntrospectionError(`Timed out waiting for MCP SSE response to ${method}.`));
@@ -11951,7 +12185,7 @@ async function createSseClient(server) {
11951
12185
  pending.set(requestId, {
11952
12186
  resolve: (value) => {
11953
12187
  clearTimeout(timeout);
11954
- resolve24(value);
12188
+ resolve25(value);
11955
12189
  },
11956
12190
  reject: (error) => {
11957
12191
  clearTimeout(timeout);
@@ -12104,7 +12338,7 @@ async function createStdioClient(server) {
12104
12338
  method,
12105
12339
  ...params ? { params } : {}
12106
12340
  });
12107
- return new Promise((resolve24, reject) => {
12341
+ return new Promise((resolve25, reject) => {
12108
12342
  const timeout = setTimeout(() => {
12109
12343
  pending.delete(id);
12110
12344
  reject(new McpIntrospectionError(`Timed out waiting for MCP stdio response to ${method}.`));
@@ -12112,7 +12346,7 @@ async function createStdioClient(server) {
12112
12346
  pending.set(id, {
12113
12347
  resolve: (value) => {
12114
12348
  clearTimeout(timeout);
12115
- resolve24(value);
12349
+ resolve25(value);
12116
12350
  },
12117
12351
  reject: (error) => {
12118
12352
  clearTimeout(timeout);
@@ -12290,7 +12524,7 @@ function nextRequestId() {
12290
12524
  // src/cli/sync-from-mcp.ts
12291
12525
  async function readMcpScaffoldMetadata(rootDir) {
12292
12526
  const filepath = resolveWithinRoot(rootDir, MCP_SCAFFOLD_METADATA_PATH);
12293
- if (!existsSync20(filepath)) {
12527
+ if (!existsSync21(filepath)) {
12294
12528
  throw new Error(
12295
12529
  `No MCP scaffold metadata found at ${MCP_SCAFFOLD_METADATA_PATH}. Run "pluxx init --from-mcp" first.`
12296
12530
  );
@@ -12325,14 +12559,14 @@ async function syncFromMcp(options) {
12325
12559
  const skillRenames = detectSkillRenames(metadata.skills, newMetadata.skills, toolRenames);
12326
12560
  for (const [oldSkillDir, newSkillDir] of skillRenames) {
12327
12561
  const oldSkillPath = resolveWithinRoot(options.rootDir, `skills/${oldSkillDir}/SKILL.md`);
12328
- if (!existsSync20(oldSkillPath)) continue;
12562
+ if (!existsSync21(oldSkillPath)) continue;
12329
12563
  const oldContent = readFileSync8(oldSkillPath, "utf-8");
12330
12564
  const extracted = extractMixedMarkdownContent(oldContent, "");
12331
12565
  if (!hasMeaningfulCustomContent(oldContent)) continue;
12332
12566
  const newSkill = newMetadata.skills.find((s) => s.dirName === newSkillDir);
12333
12567
  if (!newSkill) continue;
12334
12568
  const newSkillPath = resolveWithinRoot(options.rootDir, `skills/${newSkill.dirName}/SKILL.md`);
12335
- if (!existsSync20(newSkillPath)) continue;
12569
+ if (!existsSync21(newSkillPath)) continue;
12336
12570
  const currentContent = readFileSync8(newSkillPath, "utf-8");
12337
12571
  const updatedContent = injectCustomContent(currentContent, extracted.customContent);
12338
12572
  writeFileSync3(newSkillPath, updatedContent, "utf-8");
@@ -12375,7 +12609,7 @@ async function syncFromMcp(options) {
12375
12609
  if (!beforeManaged.has(file)) return false;
12376
12610
  const before = beforeContents.get(file);
12377
12611
  const currentPath = resolveWithinRoot(options.rootDir, file);
12378
- if (!existsSync20(currentPath)) return false;
12612
+ if (!existsSync21(currentPath)) return false;
12379
12613
  const after = readFileSync8(currentPath, "utf-8");
12380
12614
  return before !== after;
12381
12615
  });
@@ -12464,7 +12698,7 @@ async function planSyncFromMcp(options) {
12464
12698
  }
12465
12699
  function readPersistedSkills(rootDir, metadata) {
12466
12700
  const taxonomyPath = resolveWithinRoot(rootDir, MCP_TAXONOMY_PATH);
12467
- if (existsSync20(taxonomyPath)) {
12701
+ if (existsSync21(taxonomyPath)) {
12468
12702
  return JSON.parse(readFileSync8(taxonomyPath, "utf-8"));
12469
12703
  }
12470
12704
  return metadata.skills.map((skill) => ({
@@ -12477,12 +12711,12 @@ function readPersistedSkills(rootDir, metadata) {
12477
12711
  function preserveCustomContentForRenames(rootDir, renames, pathForName) {
12478
12712
  for (const [oldName, newName] of renames) {
12479
12713
  const oldPath = resolveWithinRoot(rootDir, pathForName(oldName));
12480
- if (!existsSync20(oldPath)) continue;
12714
+ if (!existsSync21(oldPath)) continue;
12481
12715
  const oldContent = readFileSync8(oldPath, "utf-8");
12482
12716
  const extracted = extractMixedMarkdownContent(oldContent, "");
12483
12717
  if (!hasMeaningfulCustomContent(oldContent)) continue;
12484
12718
  const newPath = resolveWithinRoot(rootDir, pathForName(newName));
12485
- if (!existsSync20(newPath)) continue;
12719
+ if (!existsSync21(newPath)) continue;
12486
12720
  const currentContent = readFileSync8(newPath, "utf-8");
12487
12721
  const updatedContent = injectCustomContent(currentContent, extracted.customContent);
12488
12722
  writeFileSync3(newPath, updatedContent, "utf-8");
@@ -12492,21 +12726,21 @@ function snapshotManagedFiles(rootDir, files) {
12492
12726
  const contents = /* @__PURE__ */ new Map();
12493
12727
  for (const file of files) {
12494
12728
  const filepath = resolveWithinRoot(rootDir, file);
12495
- if (!existsSync20(filepath)) continue;
12729
+ if (!existsSync21(filepath)) continue;
12496
12730
  contents.set(file, readFileSync8(filepath, "utf-8"));
12497
12731
  }
12498
12732
  return contents;
12499
12733
  }
12500
12734
  function removeManagedFile(rootDir, relativePath) {
12501
12735
  const filepath = resolveWithinRoot(rootDir, relativePath);
12502
- if (!existsSync20(filepath)) return;
12736
+ if (!existsSync21(filepath)) return;
12503
12737
  rmSync2(filepath, { force: true });
12504
- pruneEmptyDirectories(rootDir, dirname4(filepath));
12738
+ pruneEmptyDirectories(rootDir, dirname5(filepath));
12505
12739
  }
12506
12740
  function shouldPreserveManagedFile(rootDir, relativePath) {
12507
12741
  if (!relativePath.endsWith(".md")) return false;
12508
12742
  const filepath = resolveWithinRoot(rootDir, relativePath);
12509
- if (!existsSync20(filepath)) return false;
12743
+ if (!existsSync21(filepath)) return false;
12510
12744
  return hasMeaningfulCustomContent(readFileSync8(filepath, "utf-8"));
12511
12745
  }
12512
12746
  function pruneEmptyDirectories(rootDir, startDir) {
@@ -12516,7 +12750,7 @@ function pruneEmptyDirectories(rootDir, startDir) {
12516
12750
  const entries = readdirSync6(current);
12517
12751
  if (entries.length > 0) return;
12518
12752
  rmdirSync(current);
12519
- current = dirname4(current);
12753
+ current = dirname5(current);
12520
12754
  }
12521
12755
  }
12522
12756
  var AGENT_PACK_FILES = [
@@ -12680,7 +12914,7 @@ function computeSkillRenameScore(oldSkill, newSkill, toolRenames) {
12680
12914
  function resolveWithinRoot(rootDir, relativePath) {
12681
12915
  const rootPath = resolve13(rootDir);
12682
12916
  const filepath = resolve13(rootPath, relativePath);
12683
- const relativePathFromRoot = relative7(rootPath, filepath);
12917
+ const relativePathFromRoot = relative8(rootPath, filepath);
12684
12918
  if (relativePathFromRoot === "" || !relativePathFromRoot.startsWith("..") && !isAbsolute(relativePathFromRoot)) {
12685
12919
  return filepath;
12686
12920
  }
@@ -12696,13 +12930,13 @@ function formatSyncSummary(result, rootDir) {
12696
12930
  result.renamedFiles.forEach((rename) => lines.push(` \u2192 ${rename.from} \u2192 ${rename.to}`));
12697
12931
  }
12698
12932
  lines.push(`Added: ${result.addedFiles.length}`);
12699
- result.addedFiles.forEach((file) => lines.push(` + ${relative7(rootDir, resolve13(rootDir, file))}`));
12933
+ result.addedFiles.forEach((file) => lines.push(` + ${relative8(rootDir, resolve13(rootDir, file))}`));
12700
12934
  lines.push(`Updated: ${result.updatedFiles.length}`);
12701
- result.updatedFiles.forEach((file) => lines.push(` ~ ${relative7(rootDir, resolve13(rootDir, file))}`));
12935
+ result.updatedFiles.forEach((file) => lines.push(` ~ ${relative8(rootDir, resolve13(rootDir, file))}`));
12702
12936
  lines.push(`Removed: ${result.removedFiles.length}`);
12703
- result.removedFiles.forEach((file) => lines.push(` - ${relative7(rootDir, resolve13(rootDir, file))}`));
12937
+ result.removedFiles.forEach((file) => lines.push(` - ${relative8(rootDir, resolve13(rootDir, file))}`));
12704
12938
  lines.push(`Preserved: ${result.preservedFiles.length}`);
12705
- result.preservedFiles.forEach((file) => lines.push(` ! ${relative7(rootDir, resolve13(rootDir, file))}`));
12939
+ result.preservedFiles.forEach((file) => lines.push(` ! ${relative8(rootDir, resolve13(rootDir, file))}`));
12706
12940
  return lines;
12707
12941
  }
12708
12942
 
@@ -12791,7 +13025,7 @@ async function planAgentPrompt(rootDir, kind, options = {}) {
12791
13025
  const project = await loadAgentProjectModel(rootDir, config);
12792
13026
  const overrides = await loadAgentOverrides(rootDir);
12793
13027
  const contextPath = resolve14(rootDir, AGENT_CONTEXT_PATH);
12794
- if (!options.allowMissingContext && !existsSync21(contextPath)) {
13028
+ if (!options.allowMissingContext && !existsSync22(contextPath)) {
12795
13029
  throw new Error(`No agent context found at ${AGENT_CONTEXT_PATH}. Run "pluxx agent prepare" first.`);
12796
13030
  }
12797
13031
  if (project.sourceKind !== "mcp-derived" && kind !== "review") {
@@ -12803,7 +13037,7 @@ async function planAgentPrompt(rootDir, kind, options = {}) {
12803
13037
  displayName: project.displayName,
12804
13038
  skillPaths: project.skills.map((skill) => skill.path),
12805
13039
  commandPaths: project.commands.map((command2) => command2.path),
12806
- extraContextPaths: [AGENT_SOURCES_PATH, AGENT_DOCS_CONTEXT_PATH].filter((path) => existsSync21(resolve14(rootDir, path))),
13040
+ extraContextPaths: [AGENT_SOURCES_PATH, AGENT_DOCS_CONTEXT_PATH].filter((path) => existsSync22(resolve14(rootDir, path))),
12807
13041
  sourceKind: project.sourceKind,
12808
13042
  taxonomyPath: project.taxonomyPath,
12809
13043
  overrides
@@ -12971,7 +13205,7 @@ function buildProtectedFiles() {
12971
13205
  }
12972
13206
  async function loadMcpScaffoldMetadata2(rootDir) {
12973
13207
  const metadataPath = resolve14(rootDir, MCP_SCAFFOLD_METADATA_PATH);
12974
- if (!existsSync21(metadataPath)) {
13208
+ if (!existsSync22(metadataPath)) {
12975
13209
  throw new Error(`No MCP scaffold metadata found at ${MCP_SCAFFOLD_METADATA_PATH}. Run "pluxx init --from-mcp" first.`);
12976
13210
  }
12977
13211
  try {
@@ -12985,7 +13219,7 @@ async function loadMcpScaffoldMetadata2(rootDir) {
12985
13219
  }
12986
13220
  async function loadAgentProjectModel(rootDir, config) {
12987
13221
  const metadataPath = resolve14(rootDir, MCP_SCAFFOLD_METADATA_PATH);
12988
- if (existsSync21(metadataPath)) {
13222
+ if (existsSync22(metadataPath)) {
12989
13223
  const metadata = await loadMcpScaffoldMetadata2(rootDir);
12990
13224
  const serverEntry = Object.entries(config.mcp ?? {})[0];
12991
13225
  const [serverName, server] = serverEntry ?? ["unknown", metadata.source];
@@ -13023,7 +13257,7 @@ function loadManualAgentProjectModel(rootDir, config) {
13023
13257
  const commandsDir = config.commands ? resolve14(rootDir, config.commands) : void 0;
13024
13258
  const skills = readCanonicalSkillFiles(rootDir, skillsDir);
13025
13259
  const commands = readCanonicalCommandFiles(commandsDir).map((command2) => ({
13026
- path: normalizeRelativePath(relative8(rootDir, command2.filePath)),
13260
+ path: normalizeRelativePath(relative9(rootDir, command2.filePath)),
13027
13261
  title: command2.title,
13028
13262
  description: command2.description
13029
13263
  }));
@@ -13354,7 +13588,7 @@ async function collectAgentContextPackInternal(rootDir, options, overrides) {
13354
13588
  if (seenFilePaths.has(relativePath)) continue;
13355
13589
  seenFilePaths.add(relativePath);
13356
13590
  const filePath = resolve14(rootDir, relativePath);
13357
- if (!existsSync21(filePath)) {
13591
+ if (!existsSync22(filePath)) {
13358
13592
  const source2 = {
13359
13593
  label: relativePath,
13360
13594
  kind: "file",
@@ -14524,18 +14758,18 @@ function normalizeRelativePath(path) {
14524
14758
  return path.replace(/\\/g, "/").replace(/^\.\//, "");
14525
14759
  }
14526
14760
  function readCanonicalSkillFiles(rootDir, skillsDir) {
14527
- if (!skillsDir || !existsSync21(skillsDir)) return [];
14761
+ if (!skillsDir || !existsSync22(skillsDir)) return [];
14528
14762
  return walkSkillMarkdownFiles(skillsDir).sort((a, b) => a.localeCompare(b)).map((filePath) => {
14529
14763
  const content = readFileSync9(filePath, "utf-8");
14530
14764
  const { frontmatterLines, body } = splitSkillMarkdownFrontmatter(content);
14531
- const dirName = normalizeRelativePath(relative8(skillsDir, filePath).replace(/\/SKILL\.md$/i, ""));
14765
+ const dirName = normalizeRelativePath(relative9(skillsDir, filePath).replace(/\/SKILL\.md$/i, ""));
14532
14766
  const title = firstMarkdownHeading(body) ?? dirName;
14533
14767
  return {
14534
14768
  dirName,
14535
14769
  title,
14536
14770
  description: parseYamlDescription(frontmatterLines),
14537
14771
  toolNames: [],
14538
- path: normalizeRelativePath(relative8(rootDir, filePath))
14772
+ path: normalizeRelativePath(relative9(rootDir, filePath))
14539
14773
  };
14540
14774
  });
14541
14775
  }
@@ -14845,7 +15079,7 @@ async function executeCommand(command2, cwd, options = {}) {
14845
15079
  let claudeTurnCompleted = false;
14846
15080
  let claudeTurnFailed = false;
14847
15081
  const sentinelInterval = codexLastMessagePath || isClaudeStreamJson ? setInterval(() => {
14848
- const sawCompletionSignal = codexTurnCompleted || codexTurnFailed || claudeTurnCompleted || claudeTurnFailed || (codexLastMessagePath ? existsSync21(codexLastMessagePath) : false);
15082
+ const sawCompletionSignal = codexTurnCompleted || codexTurnFailed || claudeTurnCompleted || claudeTurnFailed || (codexLastMessagePath ? existsSync22(codexLastMessagePath) : false);
14849
15083
  if (!sawCompletionSignal) return;
14850
15084
  if (sawFinalMessageAt == null) {
14851
15085
  sawFinalMessageAt = Date.now();
@@ -14971,15 +15205,15 @@ exec ${shellQuote(cursorBinary)} "$@"
14971
15205
  await mkdir3(resolve14(isolatedCodexHome, "memories"), { recursive: true });
14972
15206
  for (const relativePath of ["auth.json", "config.toml", "hooks.json", "installation_id"]) {
14973
15207
  const sourcePath = resolve14(currentCodexHome, relativePath);
14974
- if (!existsSync21(sourcePath)) continue;
15208
+ if (!existsSync22(sourcePath)) continue;
14975
15209
  await copyFile(sourcePath, resolve14(isolatedCodexHome, relativePath));
14976
15210
  }
14977
15211
  const rulesSourceDir = resolve14(currentCodexHome, "rules");
14978
- if (existsSync21(rulesSourceDir)) {
15212
+ if (existsSync22(rulesSourceDir)) {
14979
15213
  const rulesTargetDir = resolve14(isolatedCodexHome, "rules");
14980
15214
  await mkdir3(rulesTargetDir, { recursive: true });
14981
15215
  const defaultRulesPath = resolve14(rulesSourceDir, "default.rules");
14982
- if (existsSync21(defaultRulesPath)) {
15216
+ if (existsSync22(defaultRulesPath)) {
14983
15217
  await copyFile(defaultRulesPath, resolve14(rulesTargetDir, "default.rules"));
14984
15218
  }
14985
15219
  }
@@ -15017,7 +15251,7 @@ function titleCase(value) {
15017
15251
  }
15018
15252
  async function loadAgentOverrides(rootDir) {
15019
15253
  const overridesPath = resolve14(rootDir, AGENT_OVERRIDES_PATH);
15020
- if (!existsSync21(overridesPath)) {
15254
+ if (!existsSync22(overridesPath)) {
15021
15255
  return null;
15022
15256
  }
15023
15257
  const content = await readTextFile(overridesPath);
@@ -15119,12 +15353,12 @@ ${additions.map((block) => `- ${block.replace(/\n/g, "\n ")}`).join("\n")}
15119
15353
  }
15120
15354
 
15121
15355
  // src/cli/doctor.ts
15122
- import { accessSync, constants, existsSync as existsSync23, lstatSync, readFileSync as readFileSync11, readdirSync as readdirSync9 } from "fs";
15123
- import { basename as basename6, dirname as dirname6, resolve as resolve16 } from "path";
15356
+ import { accessSync, constants, existsSync as existsSync24, lstatSync as lstatSync3, readFileSync as readFileSync11, readdirSync as readdirSync9 } from "fs";
15357
+ import { basename as basename6, dirname as dirname7, resolve as resolve16 } from "path";
15124
15358
 
15125
15359
  // src/cli/install.ts
15126
- import { resolve as resolve15, dirname as dirname5 } from "path";
15127
- import { existsSync as existsSync22, symlinkSync, mkdirSync as mkdirSync4, rmSync as rmSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync4, cpSync as cpSync3, readdirSync as readdirSync8 } from "fs";
15360
+ import { resolve as resolve15, dirname as dirname6 } from "path";
15361
+ import { existsSync as existsSync23, symlinkSync, mkdirSync as mkdirSync4, rmSync as rmSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync4, cpSync as cpSync3, readdirSync as readdirSync8 } from "fs";
15128
15362
  import { spawnSync } from "child_process";
15129
15363
  import * as readline2 from "readline";
15130
15364
  function listHookCommands(hooks) {
@@ -15173,6 +15407,10 @@ async function resolveInstallUserConfig(config, platforms = config.targets, opti
15173
15407
  const isTTY = options.isTTY ?? process.stdin.isTTY === true;
15174
15408
  for (const entry of planned) {
15175
15409
  if (entry.value !== void 0) {
15410
+ if (entry.field.type === "secret" && isPlaceholderSecretValue(entry.value)) {
15411
+ const hint = entry.envVar ? ` Export a real value first: export ${entry.envVar}='your_real_key'. Then rerun pluxx install.` : " Provide a real secret value before installing.";
15412
+ throw new Error(`Refusing to install placeholder secret for userConfig "${entry.field.key}". Placeholder values like "dummy" are not usable by installed MCP servers.${hint}`);
15413
+ }
15176
15414
  resolved.push({
15177
15415
  field: entry.field,
15178
15416
  value: entry.value,
@@ -15184,8 +15422,8 @@ async function resolveInstallUserConfig(config, platforms = config.targets, opti
15184
15422
  continue;
15185
15423
  }
15186
15424
  if (!isTTY) {
15187
- const hint = entry.envVar ? ` Export ${entry.envVar} or install interactively.` : " Re-run interactively to provide it.";
15188
- throw new Error(`Missing required userConfig "${entry.field.key}".${hint}`);
15425
+ const hint = entry.envVar ? ` Export it before installing: export ${entry.envVar}='your_real_key'. Then rerun pluxx install.` : " Re-run interactively to provide it.";
15426
+ throw new Error(`Missing required userConfig "${entry.field.key}". Installed plugins cannot prompt for this value later in every host UI.${hint}`);
15189
15427
  }
15190
15428
  const promptLabel = entry.field.title || entry.field.key;
15191
15429
  const envHint = entry.envVar ? ` [env: ${entry.envVar}]` : "";
@@ -15260,7 +15498,7 @@ async function ensureHookTrust(options) {
15260
15498
  const isTTY = options.isTTY ?? process.stdin.isTTY === true;
15261
15499
  if (!isTTY) {
15262
15500
  throw new Error(
15263
- `Refusing to install plugin with hooks in non-interactive mode. Re-run with --trust to continue.`
15501
+ `Refusing to install plugin with hooks in non-interactive mode. Review the hook commands above. Re-run with --trust if you trust this plugin author.`
15264
15502
  );
15265
15503
  }
15266
15504
  const confirm = options.confirmPrompt ?? promptTrustConfirmation;
@@ -15393,12 +15631,12 @@ ${content.slice(frontmatterMatch[0].length)}`;
15393
15631
  }
15394
15632
  function syncOpenCodeSkills(pluginDir, pluginName) {
15395
15633
  const sourceSkillsDir = resolve15(pluginDir, "skills");
15396
- if (!existsSync22(sourceSkillsDir)) return;
15634
+ if (!existsSync23(sourceSkillsDir)) return;
15397
15635
  mkdirSync4(getOpenCodeSkillRoot(), { recursive: true });
15398
15636
  for (const entry of readdirSync8(sourceSkillsDir, { withFileTypes: true })) {
15399
15637
  if (!entry.isDirectory()) continue;
15400
15638
  const skillSourceDir = resolve15(sourceSkillsDir, entry.name);
15401
- if (!existsSync22(resolve15(skillSourceDir, "SKILL.md"))) continue;
15639
+ if (!existsSync23(resolve15(skillSourceDir, "SKILL.md"))) continue;
15402
15640
  const installedSkillDir = getOpenCodeInstalledSkillDir(pluginName, entry.name);
15403
15641
  rmSync3(installedSkillDir, { recursive: true, force: true });
15404
15642
  cpSync3(skillSourceDir, installedSkillDir, { recursive: true });
@@ -15411,7 +15649,7 @@ function syncOpenCodeSkills(pluginDir, pluginName) {
15411
15649
  }
15412
15650
  function verifyOpenCodeInstall(pluginDir, pluginName) {
15413
15651
  const entryPath = getOpenCodeEntryPath(pluginDir);
15414
- if (!existsSync22(entryPath)) {
15652
+ if (!existsSync23(entryPath)) {
15415
15653
  throw new Error(`OpenCode install is incomplete: missing host entry file at ${entryPath}`);
15416
15654
  }
15417
15655
  const entryContent = readFileSync10(entryPath, "utf-8");
@@ -15424,13 +15662,13 @@ function verifyOpenCodeInstall(pluginDir, pluginName) {
15424
15662
  throw new Error(`OpenCode install is incomplete: ${entryPath} does not preserve the plugin root bridge`);
15425
15663
  }
15426
15664
  const sourceSkillsDir = resolve15(pluginDir, "skills");
15427
- if (!existsSync22(sourceSkillsDir)) return;
15665
+ if (!existsSync23(sourceSkillsDir)) return;
15428
15666
  for (const entry of readdirSync8(sourceSkillsDir, { withFileTypes: true })) {
15429
15667
  if (!entry.isDirectory()) continue;
15430
15668
  const sourceSkillPath = resolve15(sourceSkillsDir, entry.name, "SKILL.md");
15431
- if (!existsSync22(sourceSkillPath)) continue;
15669
+ if (!existsSync23(sourceSkillPath)) continue;
15432
15670
  const installedSkillPath = resolve15(getOpenCodeInstalledSkillDir(pluginName, entry.name), "SKILL.md");
15433
- if (!existsSync22(installedSkillPath)) {
15671
+ if (!existsSync23(installedSkillPath)) {
15434
15672
  throw new Error(`OpenCode install is incomplete: missing synced skill at ${installedSkillPath}`);
15435
15673
  }
15436
15674
  const installedSkillContent = readFileSync10(installedSkillPath, "utf-8");
@@ -15441,7 +15679,7 @@ function verifyOpenCodeInstall(pluginDir, pluginName) {
15441
15679
  }
15442
15680
  function removeOpenCodeSkills(pluginName) {
15443
15681
  const root = getOpenCodeSkillRoot();
15444
- if (!existsSync22(root)) return false;
15682
+ if (!existsSync23(root)) return false;
15445
15683
  let removed = false;
15446
15684
  for (const entry of readdirSync8(root, { withFileTypes: true })) {
15447
15685
  if (!entry.name.startsWith(`${pluginName}-`)) continue;
@@ -15459,7 +15697,7 @@ function getInstallFollowupNotes(platforms) {
15459
15697
  notes.push("Cursor note: if Cursor is already open, use Developer: Reload Window or restart Cursor to pick up the new install.");
15460
15698
  }
15461
15699
  if (platforms.includes("codex")) {
15462
- notes.push("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.");
15700
+ notes.push("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.");
15463
15701
  }
15464
15702
  if (platforms.includes("opencode")) {
15465
15703
  notes.push("OpenCode note: if OpenCode is already open, restart or reload it so the plugin is picked up.");
@@ -15477,7 +15715,7 @@ function runCommandDefault(command2, args2) {
15477
15715
  function createSymlinkInstall(target) {
15478
15716
  const parentDir = resolve15(target.pluginDir, "..");
15479
15717
  mkdirSync4(parentDir, { recursive: true });
15480
- if (existsSync22(target.pluginDir)) {
15718
+ if (existsSync23(target.pluginDir)) {
15481
15719
  rmSync3(target.pluginDir, { recursive: true, force: true });
15482
15720
  }
15483
15721
  symlinkSync(target.sourceDir, target.pluginDir);
@@ -15495,7 +15733,7 @@ function getCodexMarketplacePluginPath(pluginName) {
15495
15733
  return `./.codex/plugins/${pluginName}`;
15496
15734
  }
15497
15735
  function readCodexMarketplace(filepath) {
15498
- if (!existsSync22(filepath)) {
15736
+ if (!existsSync23(filepath)) {
15499
15737
  return {
15500
15738
  name: "pluxx-local",
15501
15739
  interface: {
@@ -15514,7 +15752,7 @@ function readCodexMarketplace(filepath) {
15514
15752
  }
15515
15753
  function ensureCodexMarketplace(pluginName) {
15516
15754
  const filepath = getCodexMarketplacePath();
15517
- mkdirSync4(dirname5(filepath), { recursive: true });
15755
+ mkdirSync4(dirname6(filepath), { recursive: true });
15518
15756
  const marketplace = readCodexMarketplace(filepath);
15519
15757
  const nextPlugins = (marketplace.plugins ?? []).filter((plugin) => plugin.name !== pluginName);
15520
15758
  nextPlugins.push({
@@ -15540,7 +15778,7 @@ function ensureCodexMarketplace(pluginName) {
15540
15778
  }
15541
15779
  function removeCodexMarketplacePlugin(pluginName) {
15542
15780
  const filepath = getCodexMarketplacePath();
15543
- if (!existsSync22(filepath)) return;
15781
+ if (!existsSync23(filepath)) return;
15544
15782
  const marketplace = readCodexMarketplace(filepath);
15545
15783
  const nextPlugins = (marketplace.plugins ?? []).filter((plugin) => plugin.name !== pluginName);
15546
15784
  if (nextPlugins.length === (marketplace.plugins ?? []).length) {
@@ -15562,7 +15800,7 @@ function removeCodexMarketplacePlugin(pluginName) {
15562
15800
  function createCopiedInstall(target) {
15563
15801
  const parentDir = resolve15(target.pluginDir, "..");
15564
15802
  mkdirSync4(parentDir, { recursive: true });
15565
- if (existsSync22(target.pluginDir)) {
15803
+ if (existsSync23(target.pluginDir)) {
15566
15804
  rmSync3(target.pluginDir, { recursive: true, force: true });
15567
15805
  }
15568
15806
  cpSync3(target.sourceDir, target.pluginDir, { recursive: true });
@@ -15587,7 +15825,7 @@ function patchInstalledMcpConfig(pluginDir, platform, config, entries) {
15587
15825
  const env = buildUserConfigEnvMap(entries);
15588
15826
  if (platform === "claude-code" || platform === "cursor") {
15589
15827
  const filepath = resolve15(pluginDir, platform === "claude-code" ? ".mcp.json" : "mcp.json");
15590
- if (!existsSync22(filepath)) return;
15828
+ if (!existsSync23(filepath)) return;
15591
15829
  const mcpServers = {};
15592
15830
  const usesPlatformManagedAuth = platform === "claude-code" ? config.platforms?.["claude-code"]?.mcpAuth === "platform" : config.platforms?.cursor?.mcpAuth === "platform";
15593
15831
  for (const [name, server] of Object.entries(config.mcp)) {
@@ -15619,7 +15857,7 @@ function patchInstalledMcpConfig(pluginDir, platform, config, entries) {
15619
15857
  }
15620
15858
  if (platform === "codex") {
15621
15859
  const filepath = resolve15(pluginDir, ".mcp.json");
15622
- if (!existsSync22(filepath)) return;
15860
+ if (!existsSync23(filepath)) return;
15623
15861
  const mcpServers = {};
15624
15862
  for (const [name, server] of Object.entries(config.mcp)) {
15625
15863
  if (server.transport === "stdio") {
@@ -15659,7 +15897,7 @@ function writeInstalledUserConfig(pluginDir, entries) {
15659
15897
  function disableInstalledEnvValidation(pluginDir, entries) {
15660
15898
  if (entries.length === 0) return;
15661
15899
  const filepath = resolve15(pluginDir, "scripts/check-env.sh");
15662
- if (!existsSync22(filepath)) return;
15900
+ if (!existsSync23(filepath)) return;
15663
15901
  writeFileSync4(
15664
15902
  filepath,
15665
15903
  "#!/usr/bin/env bash\nset -euo pipefail\n# pluxx install materialized required config for this local plugin install.\nexit 0\n"
@@ -15679,11 +15917,159 @@ function getClaudeMarketplaceRoot(pluginName) {
15679
15917
  return resolve15(home, ".claude/plugins/data", getClaudeMarketplaceName(pluginName));
15680
15918
  }
15681
15919
  function resolveInstalledConsumerPath(target, pluginName) {
15682
- if (target.platform === "claude-code") {
15683
- return resolve15(getClaudeMarketplaceRoot(pluginName), "plugins", pluginName);
15920
+ if (target.platform === "claude-code" && pluginName !== "") {
15921
+ return target.pluginDir;
15684
15922
  }
15685
15923
  return target.pluginDir;
15686
15924
  }
15925
+ function manifestPathForPlatform(platform) {
15926
+ switch (platform) {
15927
+ case "claude-code":
15928
+ return ".claude-plugin/plugin.json";
15929
+ case "cursor":
15930
+ return ".cursor-plugin/plugin.json";
15931
+ case "codex":
15932
+ return ".codex-plugin/plugin.json";
15933
+ case "opencode":
15934
+ return "package.json";
15935
+ default:
15936
+ return void 0;
15937
+ }
15938
+ }
15939
+ function isRelativeBundlePath(value) {
15940
+ return value.startsWith("./") || value.startsWith("../") || value.startsWith(".\\") || value.startsWith("..\\");
15941
+ }
15942
+ function resolveBundleReference(rootDir, value) {
15943
+ if (isRelativeBundlePath(value)) {
15944
+ return resolve15(rootDir, value);
15945
+ }
15946
+ const pluginRootMatch = value.match(/^\$\{(?:CLAUDE_PLUGIN_ROOT|CURSOR_PLUGIN_ROOT|PLUGIN_ROOT)\}[\\/](.+)$/);
15947
+ if (pluginRootMatch) {
15948
+ return resolve15(rootDir, pluginRootMatch[1]);
15949
+ }
15950
+ return void 0;
15951
+ }
15952
+ function readBundleManifestReferences(manifest) {
15953
+ const references = [];
15954
+ for (const key of ["commands", "skills", "hooks", "mcpServers"]) {
15955
+ const value = manifest[key];
15956
+ if (typeof value === "string") {
15957
+ references.push(value);
15958
+ }
15959
+ }
15960
+ const agents = manifest.agents;
15961
+ if (typeof agents === "string") {
15962
+ references.push(agents);
15963
+ } else if (Array.isArray(agents)) {
15964
+ for (const entry of agents) {
15965
+ if (typeof entry === "string") {
15966
+ references.push(entry);
15967
+ }
15968
+ }
15969
+ }
15970
+ return references;
15971
+ }
15972
+ function collectHookCommandStrings(value, commands) {
15973
+ if (Array.isArray(value)) {
15974
+ for (const entry of value) {
15975
+ collectHookCommandStrings(entry, commands);
15976
+ }
15977
+ return;
15978
+ }
15979
+ if (!value || typeof value !== "object") return;
15980
+ for (const [key, child] of Object.entries(value)) {
15981
+ if (key === "command" && typeof child === "string") {
15982
+ commands.push(child);
15983
+ continue;
15984
+ }
15985
+ collectHookCommandStrings(child, commands);
15986
+ }
15987
+ }
15988
+ function extractBundleCommandTargets(command2) {
15989
+ const matches = command2.match(/\$\{(?:CLAUDE_PLUGIN_ROOT|CURSOR_PLUGIN_ROOT|PLUGIN_ROOT)\}[\\/][^\s"'`;$|&<>]+|\.\.?[\\/][^\s"'`;$|&<>]+/g);
15990
+ return matches ?? [];
15991
+ }
15992
+ function findInstalledBundleIntegrityIssues(rootDir, platform) {
15993
+ const manifestPath = manifestPathForPlatform(platform);
15994
+ if (!manifestPath) {
15995
+ return {
15996
+ missingManifestPaths: [],
15997
+ missingHookTargets: []
15998
+ };
15999
+ }
16000
+ const manifestFile = resolve15(rootDir, manifestPath);
16001
+ if (!existsSync23(manifestFile)) {
16002
+ return {
16003
+ manifestIssue: `missing plugin manifest at ${manifestPath}`,
16004
+ missingManifestPaths: [],
16005
+ missingHookTargets: []
16006
+ };
16007
+ }
16008
+ let manifest;
16009
+ try {
16010
+ manifest = JSON.parse(readFileSync10(manifestFile, "utf-8"));
16011
+ } catch (error) {
16012
+ return {
16013
+ manifestIssue: `plugin manifest at ${manifestPath} is not parseable: ${error instanceof Error ? error.message : String(error)}`,
16014
+ missingManifestPaths: [],
16015
+ missingHookTargets: []
16016
+ };
16017
+ }
16018
+ const missingManifestPaths = readBundleManifestReferences(manifest).filter((value) => {
16019
+ const resolved = resolveBundleReference(rootDir, value);
16020
+ return resolved !== void 0 && !existsSync23(resolved);
16021
+ }).sort();
16022
+ const hooksReference = typeof manifest.hooks === "string" ? manifest.hooks : void 0;
16023
+ if (!hooksReference) {
16024
+ return {
16025
+ missingManifestPaths,
16026
+ missingHookTargets: []
16027
+ };
16028
+ }
16029
+ const hooksPath = resolveBundleReference(rootDir, hooksReference);
16030
+ if (!hooksPath || !existsSync23(hooksPath)) {
16031
+ return {
16032
+ missingManifestPaths,
16033
+ missingHookTargets: []
16034
+ };
16035
+ }
16036
+ try {
16037
+ const hooks = JSON.parse(readFileSync10(hooksPath, "utf-8"));
16038
+ const commands = [];
16039
+ collectHookCommandStrings(hooks, commands);
16040
+ const missingHookTargets = [...new Set(
16041
+ commands.flatMap(extractBundleCommandTargets).filter((value) => {
16042
+ const resolved = resolveBundleReference(rootDir, value);
16043
+ return resolved !== void 0 && !existsSync23(resolved);
16044
+ })
16045
+ )].sort();
16046
+ return {
16047
+ missingManifestPaths,
16048
+ missingHookTargets
16049
+ };
16050
+ } catch {
16051
+ return {
16052
+ missingManifestPaths,
16053
+ missingHookTargets: []
16054
+ };
16055
+ }
16056
+ }
16057
+ function assertInstalledBundleIntegrity(rootDir, platform, label) {
16058
+ const issues = findInstalledBundleIntegrityIssues(rootDir, platform);
16059
+ const details = [];
16060
+ if (issues.manifestIssue) {
16061
+ details.push(issues.manifestIssue);
16062
+ }
16063
+ if (issues.missingManifestPaths.length > 0) {
16064
+ details.push(`manifest paths missing: ${issues.missingManifestPaths.join(", ")}`);
16065
+ }
16066
+ if (issues.missingHookTargets.length > 0) {
16067
+ details.push(`hook targets missing: ${issues.missingHookTargets.join(", ")}`);
16068
+ }
16069
+ if (details.length > 0) {
16070
+ throw new Error(`${label} is incomplete: ${details.join("; ")}`);
16071
+ }
16072
+ }
15687
16073
  function ensureClaudeMarketplace(pluginName, sourceDir, materialized) {
15688
16074
  const marketplaceName = getClaudeMarketplaceName(pluginName);
15689
16075
  const marketplaceRoot = getClaudeMarketplaceRoot(pluginName);
@@ -15694,12 +16080,11 @@ function ensureClaudeMarketplace(pluginName, sourceDir, materialized) {
15694
16080
  rmSync3(marketplaceRoot, { recursive: true, force: true });
15695
16081
  mkdirSync4(marketplaceManifestDir, { recursive: true });
15696
16082
  mkdirSync4(resolve15(marketplaceRoot, "plugins"), { recursive: true });
16083
+ cpSync3(sourceDir, marketplacePluginDir, { recursive: true });
15697
16084
  if (materialized && materialized.entries.length > 0) {
15698
- cpSync3(sourceDir, marketplacePluginDir, { recursive: true });
15699
16085
  materializeInstalledPlugin(marketplacePluginDir, "claude-code", materialized.config, materialized.entries);
15700
- } else {
15701
- symlinkSync(sourceDir, marketplacePluginDir);
15702
16086
  }
16087
+ assertInstalledBundleIntegrity(marketplacePluginDir, "claude-code", "Claude marketplace bundle");
15703
16088
  writeFileSync4(
15704
16089
  resolve15(marketplaceManifestDir, "marketplace.json"),
15705
16090
  JSON.stringify({
@@ -15741,7 +16126,7 @@ function ensureClaudeMarketplaceRegistered(pluginName, sourceDir, runCommand, ma
15741
16126
  }
15742
16127
  function installClaudePlugin(target, pluginName, runCommand, materialized) {
15743
16128
  const marketplaceName = ensureClaudeMarketplaceRegistered(pluginName, target.sourceDir, runCommand, materialized);
15744
- if (existsSync22(target.pluginDir)) {
16129
+ if (existsSync23(target.pluginDir)) {
15745
16130
  rmSync3(target.pluginDir, { recursive: true, force: true });
15746
16131
  }
15747
16132
  runCommand("claude", ["plugin", "uninstall", `${pluginName}@${marketplaceName}`]);
@@ -15749,6 +16134,7 @@ function installClaudePlugin(target, pluginName, runCommand, materialized) {
15749
16134
  if (install.status !== 0) {
15750
16135
  throw new Error(`Failed to install Claude plugin: ${install.stderr || install.stdout}`);
15751
16136
  }
16137
+ assertInstalledBundleIntegrity(target.pluginDir, "claude-code", "Installed Claude plugin bundle");
15752
16138
  }
15753
16139
  function uninstallClaudePlugin(target, pluginName, runCommand, options = {}) {
15754
16140
  const marketplaceName = getClaudeMarketplaceName(pluginName);
@@ -15760,9 +16146,9 @@ function uninstallClaudePlugin(target, pluginName, runCommand, options = {}) {
15760
16146
  }
15761
16147
  }
15762
16148
  const marketplaceRoot = getClaudeMarketplaceRoot(pluginName);
15763
- const hadMarketplaceRoot = existsSync22(marketplaceRoot);
16149
+ const hadMarketplaceRoot = existsSync23(marketplaceRoot);
15764
16150
  rmSync3(marketplaceRoot, { recursive: true, force: true });
15765
- const hadLegacyPluginDir = existsSync22(target.pluginDir);
16151
+ const hadLegacyPluginDir = existsSync23(target.pluginDir);
15766
16152
  if (hadLegacyPluginDir) {
15767
16153
  rmSync3(target.pluginDir, { recursive: true, force: true });
15768
16154
  }
@@ -15776,8 +16162,8 @@ function planInstallPlugin(distDir, pluginName, platforms) {
15776
16162
  return {
15777
16163
  ...target,
15778
16164
  sourceDir,
15779
- built: existsSync22(sourceDir),
15780
- existing: existsSync22(target.pluginDir)
16165
+ built: existsSync23(sourceDir),
16166
+ existing: existsSync23(target.pluginDir)
15781
16167
  };
15782
16168
  });
15783
16169
  }
@@ -15830,7 +16216,11 @@ async function installPlugin(distDir, pluginName, platforms, options = {}) {
15830
16216
  console.log("Nothing to install. Run `pluxx build` first.");
15831
16217
  } else if (!options.quiet) {
15832
16218
  console.log(`
15833
- Installed ${installed} plugin(s). Reload or restart your tools to pick them up.`);
16219
+ Installed ${installed} plugin(s).`);
16220
+ console.log("Next checks:");
16221
+ console.log(` 1. Run: pluxx verify-install --target ${filtered.map((target) => target.platform).join(",")}`);
16222
+ console.log(" 2. Open the host plugin screen and confirm the plugin appears there.");
16223
+ console.log(" 3. Reload or restart the host if it was already open.");
15834
16224
  for (const note of getInstallFollowupNotes(filtered.map((target) => target.platform))) {
15835
16225
  console.log(note);
15836
16226
  }
@@ -15853,13 +16243,13 @@ async function uninstallPlugin(pluginName, platforms, options = {}) {
15853
16243
  continue;
15854
16244
  }
15855
16245
  let removedTarget = false;
15856
- if (existsSync22(target.pluginDir)) {
16246
+ if (existsSync23(target.pluginDir)) {
15857
16247
  rmSync3(target.pluginDir, { recursive: true, force: true });
15858
16248
  removedTarget = true;
15859
16249
  }
15860
16250
  if (target.platform === "opencode") {
15861
16251
  const entryPath = getOpenCodeEntryPath(target.pluginDir);
15862
- if (existsSync22(entryPath)) {
16252
+ if (existsSync23(entryPath)) {
15863
16253
  rmSync3(entryPath, { force: true });
15864
16254
  removedTarget = true;
15865
16255
  }
@@ -15969,7 +16359,7 @@ function checkReadablePath(checks, rootDir, label, configuredPath, required) {
15969
16359
  return;
15970
16360
  }
15971
16361
  const resolvedPath = resolve16(rootDir, configuredPath);
15972
- if (!existsSync23(resolvedPath)) {
16362
+ if (!existsSync24(resolvedPath)) {
15973
16363
  addCheck2(checks, {
15974
16364
  level: "error",
15975
16365
  code: "path-not-found",
@@ -16335,7 +16725,7 @@ function checkCompilerIntent(checks, rootDir) {
16335
16725
  }
16336
16726
  function checkScaffoldMetadata(checks, rootDir, config) {
16337
16727
  const metadataPath = resolve16(rootDir, MCP_SCAFFOLD_METADATA_PATH);
16338
- if (!existsSync23(metadataPath)) {
16728
+ if (!existsSync24(metadataPath)) {
16339
16729
  addCheck2(checks, {
16340
16730
  level: "info",
16341
16731
  code: "mcp-metadata-missing",
@@ -16400,7 +16790,7 @@ function checkScaffoldMetadata(checks, rootDir, config) {
16400
16790
  }
16401
16791
  }
16402
16792
  function detectConsumerLayout(rootDir) {
16403
- if (existsSync23(resolve16(rootDir, ".claude-plugin/plugin.json"))) {
16793
+ if (existsSync24(resolve16(rootDir, ".claude-plugin/plugin.json"))) {
16404
16794
  return {
16405
16795
  kind: "installed-platform",
16406
16796
  platform: "claude-code",
@@ -16408,7 +16798,7 @@ function detectConsumerLayout(rootDir) {
16408
16798
  mcpConfigPath: ".mcp.json"
16409
16799
  };
16410
16800
  }
16411
- if (existsSync23(resolve16(rootDir, ".cursor-plugin/plugin.json"))) {
16801
+ if (existsSync24(resolve16(rootDir, ".cursor-plugin/plugin.json"))) {
16412
16802
  return {
16413
16803
  kind: "installed-platform",
16414
16804
  platform: "cursor",
@@ -16416,7 +16806,7 @@ function detectConsumerLayout(rootDir) {
16416
16806
  mcpConfigPath: "mcp.json"
16417
16807
  };
16418
16808
  }
16419
- if (existsSync23(resolve16(rootDir, ".codex-plugin/plugin.json"))) {
16809
+ if (existsSync24(resolve16(rootDir, ".codex-plugin/plugin.json"))) {
16420
16810
  return {
16421
16811
  kind: "installed-platform",
16422
16812
  platform: "codex",
@@ -16426,7 +16816,7 @@ function detectConsumerLayout(rootDir) {
16426
16816
  }
16427
16817
  const packagePath = resolve16(rootDir, "package.json");
16428
16818
  const indexPath = resolve16(rootDir, "index.ts");
16429
- if (existsSync23(packagePath) && existsSync23(indexPath)) {
16819
+ if (existsSync24(packagePath) && existsSync24(indexPath)) {
16430
16820
  try {
16431
16821
  const pkg = JSON.parse(readFileSync11(packagePath, "utf-8"));
16432
16822
  if (pkg.peerDependencies?.["@opencode-ai/plugin"] || pkg.keywords?.includes("opencode-plugin")) {
@@ -16444,10 +16834,10 @@ function detectConsumerLayout(rootDir) {
16444
16834
  };
16445
16835
  }
16446
16836
  }
16447
- if (CONFIG_FILES.some((filename) => existsSync23(resolve16(rootDir, filename)))) {
16837
+ if (CONFIG_FILES.some((filename) => existsSync24(resolve16(rootDir, filename)))) {
16448
16838
  return { kind: "source-project" };
16449
16839
  }
16450
- if (["claude-code", "cursor", "codex", "opencode"].some((dir) => existsSync23(resolve16(rootDir, dir)))) {
16840
+ if (["claude-code", "cursor", "codex", "opencode"].some((dir) => existsSync24(resolve16(rootDir, dir)))) {
16451
16841
  return { kind: "multi-target-dist" };
16452
16842
  }
16453
16843
  return { kind: "unknown" };
@@ -16458,7 +16848,7 @@ function readJsonFile2(rootDir, relativePath) {
16458
16848
  function checkConsumerBundlePath(checks, rootDir) {
16459
16849
  try {
16460
16850
  accessSync(rootDir, constants.R_OK);
16461
- const details = lstatSync(rootDir);
16851
+ const details = lstatSync3(rootDir);
16462
16852
  addCheck2(checks, {
16463
16853
  level: "success",
16464
16854
  code: "consumer-path-readable",
@@ -16505,7 +16895,7 @@ function checkConsumerManifest(checks, rootDir, layout) {
16505
16895
  function checkInstalledUserConfig(checks, rootDir) {
16506
16896
  const userConfigPath = ".pluxx-user.json";
16507
16897
  const resolvedPath = resolve16(rootDir, userConfigPath);
16508
- if (!existsSync23(resolvedPath)) {
16898
+ if (!existsSync24(resolvedPath)) {
16509
16899
  addCheck2(checks, {
16510
16900
  level: "info",
16511
16901
  code: "consumer-user-config-missing",
@@ -16520,6 +16910,10 @@ function checkInstalledUserConfig(checks, rootDir) {
16520
16910
  const payload = JSON.parse(readFileSync11(resolvedPath, "utf-8"));
16521
16911
  const valueCount = Object.keys(payload.values ?? {}).length;
16522
16912
  const envCount = Object.keys(payload.env ?? {}).length;
16913
+ const placeholderKeys = [
16914
+ ...Object.entries(payload.values ?? {}).filter(([, value]) => isPlaceholderSecretValue(value)).map(([key]) => key),
16915
+ ...Object.entries(payload.env ?? {}).filter(([, value]) => isPlaceholderSecretValue(value)).map(([key]) => key)
16916
+ ];
16523
16917
  addCheck2(checks, {
16524
16918
  level: "success",
16525
16919
  code: "consumer-user-config-valid",
@@ -16528,6 +16922,16 @@ function checkInstalledUserConfig(checks, rootDir) {
16528
16922
  fix: "No action needed.",
16529
16923
  path: userConfigPath
16530
16924
  });
16925
+ if (placeholderKeys.length > 0) {
16926
+ addCheck2(checks, {
16927
+ level: "warning",
16928
+ code: "consumer-user-config-placeholder-secret",
16929
+ title: "Local install config contains placeholder-looking secret values",
16930
+ detail: `.pluxx-user.json contains placeholder-looking value${placeholderKeys.length === 1 ? "" : "s"} for ${placeholderKeys.join(", ")}.`,
16931
+ fix: "Reinstall the plugin with real secret values, or edit .pluxx-user.json and refresh/restart the host.",
16932
+ path: userConfigPath
16933
+ });
16934
+ }
16531
16935
  } catch (error) {
16532
16936
  addCheck2(checks, {
16533
16937
  level: "error",
@@ -16542,7 +16946,7 @@ function checkInstalledUserConfig(checks, rootDir) {
16542
16946
  function checkInstalledEnvValidation(checks, rootDir) {
16543
16947
  const envScriptPath = "scripts/check-env.sh";
16544
16948
  const resolvedPath = resolve16(rootDir, envScriptPath);
16545
- if (!existsSync23(resolvedPath)) {
16949
+ if (!existsSync24(resolvedPath)) {
16546
16950
  addCheck2(checks, {
16547
16951
  level: "info",
16548
16952
  code: "consumer-env-script-missing",
@@ -16587,7 +16991,7 @@ function checkInstalledMcpConfig(checks, rootDir, layout) {
16587
16991
  return;
16588
16992
  }
16589
16993
  const resolvedPath = resolve16(rootDir, layout.mcpConfigPath);
16590
- if (!existsSync23(resolvedPath)) {
16994
+ if (!existsSync24(resolvedPath)) {
16591
16995
  addCheck2(checks, {
16592
16996
  level: "info",
16593
16997
  code: "consumer-mcp-config-missing",
@@ -16612,6 +17016,16 @@ function checkInstalledMcpConfig(checks, rootDir, layout) {
16612
17016
  if (servers.length === 0) {
16613
17017
  return;
16614
17018
  }
17019
+ if (layout.platform === "codex") {
17020
+ addCheck2(checks, {
17021
+ level: "info",
17022
+ code: "consumer-codex-mcp-bundled-visibility",
17023
+ title: "Codex plugin-bundled MCP visibility clarified",
17024
+ detail: `This Codex plugin bundle includes ${servers.length} MCP server${servers.length === 1 ? "" : "s"} through ${layout.mcpConfigPath}. Codex may show this on the plugin detail page without listing it on the global MCP servers settings page.`,
17025
+ fix: "Use the plugin detail page, tool availability in chat, and `pluxx verify-install --target codex` as the source of truth for plugin-bundled MCP wiring. If the MCP remains unavailable after install, use Plugins > Refresh if present or restart Codex.",
17026
+ path: layout.mcpConfigPath
17027
+ });
17028
+ }
16615
17029
  const remoteEntries = servers.filter((server) => "url" in server);
16616
17030
  const stdioEntries = servers.filter((server) => "command" in server);
16617
17031
  const inlineHeaderEntries = servers.filter((server) => {
@@ -16628,6 +17042,17 @@ function checkInstalledMcpConfig(checks, rootDir, layout) {
16628
17042
  fix: "If tools fail, verify the bundled command or its runtime dependencies on this machine.",
16629
17043
  path: layout.mcpConfigPath
16630
17044
  });
17045
+ const missingRuntimePaths = findMissingInstalledStdioRuntimePaths(rootDir, stdioEntries);
17046
+ if (missingRuntimePaths.length > 0) {
17047
+ addCheck2(checks, {
17048
+ level: "warning",
17049
+ code: "consumer-mcp-stdio-runtime-missing",
17050
+ title: "Bundled stdio MCP runtime files are missing",
17051
+ detail: `This installed MCP config references local runtime path${missingRuntimePaths.length === 1 ? "" : "s"} that do not exist in the bundle: ${missingRuntimePaths.join(", ")}.`,
17052
+ fix: "Rebuild the plugin with the MCP runtime directory included in passthrough, then reinstall the host bundle.",
17053
+ path: layout.mcpConfigPath
17054
+ });
17055
+ }
16631
17056
  }
16632
17057
  if (remoteEntries.length > 0 && inlineHeaderEntries.length > 0) {
16633
17058
  addCheck2(checks, {
@@ -16662,9 +17087,59 @@ function checkInstalledMcpConfig(checks, rootDir, layout) {
16662
17087
  });
16663
17088
  }
16664
17089
  }
17090
+ function checkInstalledBundleIntegrity(checks, rootDir, layout) {
17091
+ const issues = findInstalledBundleIntegrityIssues(rootDir, layout.platform);
17092
+ const details = [];
17093
+ if (issues.manifestIssue) {
17094
+ details.push(issues.manifestIssue);
17095
+ }
17096
+ if (issues.missingManifestPaths.length > 0) {
17097
+ details.push(`manifest references missing path${issues.missingManifestPaths.length === 1 ? "" : "s"}: ${issues.missingManifestPaths.join(", ")}`);
17098
+ }
17099
+ if (issues.missingHookTargets.length > 0) {
17100
+ details.push(`hook commands reference missing bundle target${issues.missingHookTargets.length === 1 ? "" : "s"}: ${issues.missingHookTargets.join(", ")}`);
17101
+ }
17102
+ if (details.length === 0) {
17103
+ addCheck2(checks, {
17104
+ level: "success",
17105
+ code: "consumer-bundle-integrity-valid",
17106
+ title: "Installed bundle references resolve inside the plugin",
17107
+ detail: "Every manifest-declared path and bundle-relative hook target exists in this installed bundle.",
17108
+ fix: "No action needed.",
17109
+ path: layout.manifestPath
17110
+ });
17111
+ return;
17112
+ }
17113
+ addCheck2(checks, {
17114
+ level: "error",
17115
+ code: "consumer-bundle-integrity-invalid",
17116
+ title: "Installed bundle is missing referenced files",
17117
+ detail: details.join("; "),
17118
+ fix: "Reinstall the plugin or rebuild the bundle so every manifest path and hook target exists in the installed plugin.",
17119
+ path: layout.manifestPath
17120
+ });
17121
+ }
17122
+ function findMissingInstalledStdioRuntimePaths(rootDir, stdioEntries) {
17123
+ const missing = /* @__PURE__ */ new Set();
17124
+ for (const server of stdioEntries) {
17125
+ const command2 = typeof server.command === "string" ? server.command : void 0;
17126
+ const args2 = Array.isArray(server.args) ? server.args.filter((value) => typeof value === "string") : [];
17127
+ for (const candidate of [command2, ...args2]) {
17128
+ if (!candidate || !isLikelyLocalRuntimePath3(candidate)) continue;
17129
+ const resolvedPath = resolve16(rootDir, candidate);
17130
+ if (!existsSync24(resolvedPath)) {
17131
+ missing.add(candidate);
17132
+ }
17133
+ }
17134
+ }
17135
+ return [...missing].sort();
17136
+ }
17137
+ function isLikelyLocalRuntimePath3(value) {
17138
+ return value.startsWith("./") || value.startsWith("../") || value.startsWith(".\\") || value.startsWith("..\\");
17139
+ }
16665
17140
  function isLikelyOpenCodeInstallPath(rootDir) {
16666
- const parent = dirname6(rootDir);
16667
- const grandparent = dirname6(parent);
17141
+ const parent = dirname7(rootDir);
17142
+ const grandparent = dirname7(parent);
16668
17143
  return basename6(parent) === "plugins" && basename6(grandparent) === "opencode";
16669
17144
  }
16670
17145
  function checkInstalledOpenCodeHostBridge(checks, rootDir) {
@@ -16682,7 +17157,7 @@ function checkInstalledOpenCodeHostBridge(checks, rootDir) {
16682
17157
  const pluginName = basename6(rootDir);
16683
17158
  const entryPath = `${rootDir}.ts`;
16684
17159
  const entryRelativePath = `${pluginName}.ts`;
16685
- if (!existsSync23(entryPath)) {
17160
+ if (!existsSync24(entryPath)) {
16686
17161
  addCheck2(checks, {
16687
17162
  level: "error",
16688
17163
  code: "consumer-opencode-entry-missing",
@@ -16722,7 +17197,7 @@ function checkInstalledOpenCodeSkills(checks, rootDir) {
16722
17197
  }
16723
17198
  const pluginName = basename6(rootDir);
16724
17199
  const sourceSkillsDir = resolve16(rootDir, "skills");
16725
- if (!existsSync23(sourceSkillsDir)) {
17200
+ if (!existsSync24(sourceSkillsDir)) {
16726
17201
  addCheck2(checks, {
16727
17202
  level: "info",
16728
17203
  code: "consumer-opencode-skill-sync-not-applicable",
@@ -16733,17 +17208,17 @@ function checkInstalledOpenCodeSkills(checks, rootDir) {
16733
17208
  });
16734
17209
  return;
16735
17210
  }
16736
- const skillRoot = resolve16(dirname6(dirname6(rootDir)), "skills");
17211
+ const skillRoot = resolve16(dirname7(dirname7(rootDir)), "skills");
16737
17212
  const missingSkills = [];
16738
17213
  const malformedSkills = [];
16739
17214
  let expectedSkillCount = 0;
16740
17215
  for (const entry of readdirSync9(sourceSkillsDir, { withFileTypes: true })) {
16741
17216
  if (!entry.isDirectory()) continue;
16742
17217
  const sourceSkillPath = resolve16(sourceSkillsDir, entry.name, "SKILL.md");
16743
- if (!existsSync23(sourceSkillPath)) continue;
17218
+ if (!existsSync24(sourceSkillPath)) continue;
16744
17219
  expectedSkillCount++;
16745
17220
  const installedSkillPath = resolve16(skillRoot, `${pluginName}-${entry.name}`, "SKILL.md");
16746
- if (!existsSync23(installedSkillPath)) {
17221
+ if (!existsSync24(installedSkillPath)) {
16747
17222
  missingSkills.push(`${pluginName}-${entry.name}`);
16748
17223
  continue;
16749
17224
  }
@@ -16829,6 +17304,7 @@ async function doctorConsumer(rootDir = process.cwd()) {
16829
17304
  path: layout.manifestPath
16830
17305
  });
16831
17306
  checkConsumerManifest(checks, rootDir, layout);
17307
+ checkInstalledBundleIntegrity(checks, rootDir, layout);
16832
17308
  checkInstalledUserConfig(checks, rootDir);
16833
17309
  checkInstalledEnvValidation(checks, rootDir);
16834
17310
  checkInstalledMcpConfig(checks, rootDir, layout);
@@ -16840,7 +17316,7 @@ async function doctorConsumer(rootDir = process.cwd()) {
16840
17316
  }
16841
17317
  async function doctorProject(rootDir = process.cwd()) {
16842
17318
  const checks = [];
16843
- const configPath = CONFIG_FILES.find((filename) => existsSync23(resolve16(rootDir, filename)));
17319
+ const configPath = CONFIG_FILES.find((filename) => existsSync24(resolve16(rootDir, filename)));
16844
17320
  addRuntimeChecks(checks, "project");
16845
17321
  if (!configPath) {
16846
17322
  addCheck2(checks, {
@@ -16939,7 +17415,7 @@ function printDoctorReport(report) {
16939
17415
 
16940
17416
  // src/cli/dev.ts
16941
17417
  import { watch } from "fs";
16942
- import { relative as relative9, resolve as resolve17 } from "path";
17418
+ import { relative as relative10, resolve as resolve17 } from "path";
16943
17419
  var WATCH_PATTERNS = [
16944
17420
  /^pluxx\.config\.(ts|js|json)$/,
16945
17421
  /^skills\//,
@@ -16965,7 +17441,7 @@ async function runDev(args2) {
16965
17441
  let pendingFile = null;
16966
17442
  const watcher = watch(rootDir, { recursive: true }, (_event, filename) => {
16967
17443
  if (!filename) return;
16968
- const rel = relative9(rootDir, resolve17(rootDir, filename));
17444
+ const rel = relative10(rootDir, resolve17(rootDir, filename));
16969
17445
  if (rel.startsWith("dist/") || rel.startsWith(".") || rel.includes("node_modules")) {
16970
17446
  return;
16971
17447
  }
@@ -17020,8 +17496,8 @@ async function runBuild(rootDir, targets) {
17020
17496
  }
17021
17497
 
17022
17498
  // src/cli/migrate.ts
17023
- import { basename as basename7, relative as relative10, resolve as resolve18 } from "path";
17024
- import { existsSync as existsSync24, readdirSync as readdirSync10, mkdirSync as mkdirSync5, cpSync as cpSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
17499
+ import { basename as basename7, relative as relative11, resolve as resolve18 } from "path";
17500
+ import { existsSync as existsSync25, readdirSync as readdirSync10, mkdirSync as mkdirSync5, cpSync as cpSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
17025
17501
  function detectPlatform(pluginDir) {
17026
17502
  const checks = [
17027
17503
  { dir: ".claude-plugin", platform: "claude-code" },
@@ -17030,12 +17506,12 @@ function detectPlatform(pluginDir) {
17030
17506
  ];
17031
17507
  for (const check of checks) {
17032
17508
  const manifestPath = resolve18(pluginDir, check.dir, "plugin.json");
17033
- if (existsSync24(manifestPath)) {
17509
+ if (existsSync25(manifestPath)) {
17034
17510
  return { platform: check.platform, manifestPath };
17035
17511
  }
17036
17512
  }
17037
17513
  const pkgPath = resolve18(pluginDir, "package.json");
17038
- if (existsSync24(pkgPath)) {
17514
+ if (existsSync25(pkgPath)) {
17039
17515
  try {
17040
17516
  const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
17041
17517
  const deps = {
@@ -17088,7 +17564,7 @@ function parseMcp(pluginDir, detection) {
17088
17564
  } catch {
17089
17565
  }
17090
17566
  for (const mcpPath of mcpPaths) {
17091
- if (!existsSync24(mcpPath)) continue;
17567
+ if (!existsSync25(mcpPath)) continue;
17092
17568
  try {
17093
17569
  const raw = JSON.parse(readFileSync12(mcpPath, "utf-8"));
17094
17570
  const servers = raw.mcpServers ?? raw;
@@ -17182,7 +17658,7 @@ function parseHooks(pluginDir, detection) {
17182
17658
  } catch {
17183
17659
  }
17184
17660
  for (const hooksPath of hooksPaths) {
17185
- if (!existsSync24(hooksPath)) continue;
17661
+ if (!existsSync25(hooksPath)) continue;
17186
17662
  try {
17187
17663
  const raw = JSON.parse(readFileSync12(hooksPath, "utf-8"));
17188
17664
  const hooksObj = raw.hooks ?? raw;
@@ -17231,7 +17707,7 @@ function findInstructions(pluginDir) {
17231
17707
  ];
17232
17708
  for (const candidate of candidates) {
17233
17709
  const filePath = resolve18(pluginDir, candidate);
17234
- if (existsSync24(filePath)) {
17710
+ if (existsSync25(filePath)) {
17235
17711
  return `./${candidate}`;
17236
17712
  }
17237
17713
  }
@@ -17256,7 +17732,7 @@ function detectCanonicalSourcePaths(pluginDir) {
17256
17732
  for (const bucket of Object.keys(CANONICAL_SOURCE_CANDIDATES)) {
17257
17733
  for (const candidate of CANONICAL_SOURCE_CANDIDATES[bucket]) {
17258
17734
  const normalized = stripRelativePrefix(candidate);
17259
- if (!existsSync24(resolve18(pluginDir, normalized))) continue;
17735
+ if (!existsSync25(resolve18(pluginDir, normalized))) continue;
17260
17736
  result[bucket] = normalizeRelativeDir(normalized);
17261
17737
  break;
17262
17738
  }
@@ -17272,7 +17748,7 @@ function detectPassthroughDirs(pluginDir, mcp) {
17272
17748
  if (!match?.[1]) continue;
17273
17749
  const dirName = match[1];
17274
17750
  const dirPath = resolve18(pluginDir, dirName);
17275
- if (existsSync24(dirPath)) {
17751
+ if (existsSync25(dirPath)) {
17276
17752
  passthrough.add(`./${dirName}/`);
17277
17753
  }
17278
17754
  }
@@ -17373,7 +17849,7 @@ function inferPermissionsFromMigratedSkills(pluginDir, sourcePaths) {
17373
17849
  for (const entry of entries) {
17374
17850
  if (!entry.isDirectory()) continue;
17375
17851
  const skillPath = resolve18(skillsDir, entry.name, "SKILL.md");
17376
- if (!existsSync24(skillPath)) continue;
17852
+ if (!existsSync25(skillPath)) continue;
17377
17853
  const content = readFileSync12(skillPath, "utf-8");
17378
17854
  const inferredRules = extractAllowedTools(content).map(normalizeMigratedAllowedTool).filter((rule) => Boolean(rule));
17379
17855
  if (inferredRules.length === 0) continue;
@@ -17418,7 +17894,7 @@ function readMigratedSkills(pluginDir, sourcePaths) {
17418
17894
  const skillPath = resolve18(skillsDir, dirName, "SKILL.md");
17419
17895
  let title = titleCaseFromDirName(dirName);
17420
17896
  let description;
17421
- if (existsSync24(skillPath)) {
17897
+ if (existsSync25(skillPath)) {
17422
17898
  const content = readFileSync12(skillPath, "utf-8");
17423
17899
  title = extractFrontmatterField(content, "name") ?? firstHeading3(content) ?? title;
17424
17900
  description = extractFrontmatterField(content, "description");
@@ -17452,13 +17928,13 @@ var HOST_NATIVE_SKILL_FRONTMATTER_KEYS = /* @__PURE__ */ new Set([
17452
17928
  "allowed-tools"
17453
17929
  ]);
17454
17930
  function sanitizeMigratedSkillFrontmatter(outputDir) {
17455
- if (!existsSync24(resolve18(outputDir, "skills"))) return;
17931
+ if (!existsSync25(resolve18(outputDir, "skills"))) return;
17456
17932
  const skillsDir = resolve18(outputDir, "skills");
17457
17933
  const entries = readdirSync10(skillsDir, { withFileTypes: true });
17458
17934
  for (const entry of entries) {
17459
17935
  if (!entry.isDirectory()) continue;
17460
17936
  const skillPath = resolve18(skillsDir, entry.name, "SKILL.md");
17461
- if (!existsSync24(skillPath)) continue;
17937
+ if (!existsSync25(skillPath)) continue;
17462
17938
  const content = readFileSync12(skillPath, "utf-8");
17463
17939
  const lines = content.split(/\r?\n/);
17464
17940
  if (lines[0]?.trim() !== "---") continue;
@@ -17621,11 +18097,11 @@ function walkMarkdownFiles3(dir) {
17621
18097
  return files;
17622
18098
  }
17623
18099
  function normalizeMigratedOpenCodeAgents(destDir) {
17624
- if (!existsSync24(destDir)) return [];
18100
+ if (!existsSync25(destDir)) return [];
17625
18101
  const normalized = [];
17626
18102
  for (const filePath of walkMarkdownFiles3(destDir)) {
17627
18103
  if (normalizeMigratedOpenCodeAgentFile(filePath)) {
17628
- normalized.push(relative10(destDir, filePath).replace(/\\/g, "/"));
18104
+ normalized.push(relative11(destDir, filePath).replace(/\\/g, "/"));
17629
18105
  }
17630
18106
  }
17631
18107
  return normalized.sort();
@@ -17710,7 +18186,7 @@ function buildMigratedScaffoldMetadata(result, outputDir) {
17710
18186
  if (!result.sourcePaths[dir]) return [];
17711
18187
  const baseDir = dir;
17712
18188
  const dirPath = resolve18(outputDir, baseDir);
17713
- if (!existsSync24(dirPath)) return [];
18189
+ if (!existsSync25(dirPath)) return [];
17714
18190
  const entries = readdirSync10(dirPath, { withFileTypes: true });
17715
18191
  const files = [];
17716
18192
  for (const entry of entries) {
@@ -17777,7 +18253,7 @@ function copyDirectories(pluginDir, outputDir, sourcePaths, passthrough) {
17777
18253
  const normalizedSource = stripRelativePrefix(sourcePath);
17778
18254
  const src = resolve18(pluginDir, normalizedSource);
17779
18255
  const dest = resolve18(outputDir, dir);
17780
- if (existsSync24(dest)) {
18256
+ if (existsSync25(dest)) {
17781
18257
  console.log(` skip ./${dir}/ (already exists)`);
17782
18258
  continue;
17783
18259
  }
@@ -17794,7 +18270,7 @@ function copyDirectories(pluginDir, outputDir, sourcePaths, passthrough) {
17794
18270
  const normalized = entry.replace(/^\.\//, "").replace(/\/$/, "");
17795
18271
  const src = resolve18(pluginDir, normalized);
17796
18272
  const dest = resolve18(outputDir, normalized);
17797
- if (existsSync24(dest)) {
18273
+ if (existsSync25(dest)) {
17798
18274
  console.log(` skip ./${normalized}/ (already exists)`);
17799
18275
  continue;
17800
18276
  }
@@ -17940,7 +18416,7 @@ function quote(s) {
17940
18416
  async function migrate(inputPath) {
17941
18417
  const pluginDir = resolve18(inputPath);
17942
18418
  const outputDir = process.cwd();
17943
- if (!existsSync24(pluginDir)) {
18419
+ if (!existsSync25(pluginDir)) {
17944
18420
  console.error(`Error: Path does not exist: ${pluginDir}`);
17945
18421
  process.exit(1);
17946
18422
  }
@@ -18007,7 +18483,7 @@ async function migrate(inputPath) {
18007
18483
  };
18008
18484
  const configContent = generateConfigTs(result);
18009
18485
  const configPath = resolve18(outputDir, "pluxx.config.ts");
18010
- if (existsSync24(configPath)) {
18486
+ if (existsSync25(configPath)) {
18011
18487
  console.error(`
18012
18488
  Error: pluxx.config.ts already exists in ${outputDir}`);
18013
18489
  console.error("Remove it first or run from a different directory.");
@@ -18024,7 +18500,7 @@ Generated pluxx.config.ts`);
18024
18500
  if (instructions) {
18025
18501
  const srcInstr = resolve18(pluginDir, instructions);
18026
18502
  const destInstr = resolve18(outputDir, instructions);
18027
- if (!existsSync24(destInstr)) {
18503
+ if (!existsSync25(destInstr)) {
18028
18504
  const content = readFileSync12(srcInstr, "utf-8");
18029
18505
  await writeTextFile(destInstr, content);
18030
18506
  console.log(`Copied: ${instructions}`);
@@ -18060,7 +18536,7 @@ Generated pluxx.config.ts`);
18060
18536
 
18061
18537
  // src/cli/mcp-proxy.ts
18062
18538
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync13 } from "fs";
18063
- import { dirname as dirname7, resolve as resolve19 } from "path";
18539
+ import { dirname as dirname8, resolve as resolve19 } from "path";
18064
18540
  import * as readline3 from "readline";
18065
18541
  function usage() {
18066
18542
  return [
@@ -18137,7 +18613,7 @@ async function loadReplayTape(filepath) {
18137
18613
  }
18138
18614
  async function writeTape(filepath, tape) {
18139
18615
  const absolutePath = resolve19(process.cwd(), filepath);
18140
- mkdirSync6(dirname7(absolutePath), { recursive: true });
18616
+ mkdirSync6(dirname8(absolutePath), { recursive: true });
18141
18617
  await writeTextFile(absolutePath, `${JSON.stringify(tape, null, 2)}
18142
18618
  `);
18143
18619
  }
@@ -18308,7 +18784,7 @@ var PromptCancelledError = class extends Error {
18308
18784
  }
18309
18785
  };
18310
18786
  function ask(question) {
18311
- return new Promise((resolve24, reject) => {
18787
+ return new Promise((resolve25, reject) => {
18312
18788
  const rl = readline4.createInterface({
18313
18789
  input: process.stdin,
18314
18790
  output: process.stdout
@@ -18336,7 +18812,7 @@ function ask(question) {
18336
18812
  rl.once("close", onClose);
18337
18813
  rl.question(question, (answer) => {
18338
18814
  settle(() => {
18339
- resolve24(answer);
18815
+ resolve25(answer);
18340
18816
  rl.close();
18341
18817
  });
18342
18818
  });
@@ -19311,13 +19787,13 @@ ${c2}
19311
19787
  } }).prompt();
19312
19788
 
19313
19789
  // src/cli/index.ts
19314
- import { basename as basename8, resolve as resolve23 } from "path";
19790
+ import { basename as basename8, resolve as resolve24 } from "path";
19315
19791
  import { mkdir as mkdir4, mkdtemp as mkdtemp3, rm as rm4 } from "fs/promises";
19316
19792
  import { tmpdir as tmpdir5 } from "os";
19317
19793
  import { spawn as spawn4, spawnSync as spawnSync3 } from "child_process";
19318
19794
 
19319
19795
  // src/cli/publish.ts
19320
- import { chmodSync, existsSync as existsSync25, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
19796
+ import { chmodSync, existsSync as existsSync26, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
19321
19797
  import { createHash } from "crypto";
19322
19798
  import { resolve as resolve20 } from "path";
19323
19799
  import { spawnSync as spawnSync2 } from "child_process";
@@ -19352,7 +19828,7 @@ function resolveRequestedChannels(options) {
19352
19828
  }
19353
19829
  function getBuiltTargets(rootDir, config) {
19354
19830
  return config.targets.filter(
19355
- (platform) => existsSync25(resolve20(rootDir, config.outDir, platform))
19831
+ (platform) => existsSync26(resolve20(rootDir, config.outDir, platform))
19356
19832
  );
19357
19833
  }
19358
19834
  function getArchiveAssetName(pluginName, platform, version, variant) {
@@ -19411,7 +19887,7 @@ function buildReleaseAssets(rootDir, config, version, targets) {
19411
19887
  function readNpmPackageName(rootDir, config) {
19412
19888
  const packageDir = resolve20(rootDir, config.outDir, "opencode");
19413
19889
  const packageJsonPath = resolve20(packageDir, "package.json");
19414
- if (!existsSync25(packageJsonPath)) {
19890
+ if (!existsSync26(packageJsonPath)) {
19415
19891
  return {};
19416
19892
  }
19417
19893
  try {
@@ -19464,7 +19940,7 @@ function collectChecks(args2) {
19464
19940
  if (args2.npmEnabled) {
19465
19941
  checks.push({
19466
19942
  name: "npm-package-ready",
19467
- ok: Boolean(args2.packageDir && existsSync25(resolve20(args2.packageDir, "package.json")) && args2.packageName),
19943
+ ok: Boolean(args2.packageDir && existsSync26(resolve20(args2.packageDir, "package.json")) && args2.packageName),
19468
19944
  code: "npm-package-ready",
19469
19945
  detail: args2.packageDir ? `OpenCode package dir: ${args2.packageDir}` : "No npm-backed target package found."
19470
19946
  });
@@ -19620,6 +20096,175 @@ echo
19620
20096
  echo "Installed __DISPLAY_NAME__ across ${installerTargets.join(", ")}."
19621
20097
  `.replaceAll("__REPO__", "REPO_PLACEHOLDER").replaceAll("__DISPLAY_NAME__", "DISPLAY_PLACEHOLDER");
19622
20098
  }
20099
+ function renderInstallerUserConfigSnippet(config, platform, installDirVariable) {
20100
+ const entries = collectUserConfigEntries(config, [platform]).map((entry) => ({
20101
+ key: entry.key,
20102
+ title: entry.title,
20103
+ type: entry.type ?? "string",
20104
+ required: entry.required !== false,
20105
+ envVar: entry.envVar ?? defaultUserConfigEnvVar(entry.key)
20106
+ }));
20107
+ if (entries.length === 0) return "";
20108
+ const promptLines = entries.map((entry) => {
20109
+ const functionName = entry.type === "secret" ? "pluxx_prompt_secret_config" : "pluxx_prompt_text_config";
20110
+ return `${functionName} ${JSON.stringify(entry.envVar)} ${JSON.stringify(entry.title)} ${entry.required ? "1" : "0"}`;
20111
+ });
20112
+ return `
20113
+ PLUXX_USER_CONFIG_SPEC="$(cat <<'PLUXX_USER_CONFIG_JSON'
20114
+ ${JSON.stringify(entries)}
20115
+ PLUXX_USER_CONFIG_JSON
20116
+ )"
20117
+
20118
+ pluxx_is_placeholder_secret() {
20119
+ case "$1" in
20120
+ *dummy*|*Dummy*|*DUMMY*|*placeholder*|*Placeholder*|*PLACEHOLDER*|*changeme*|*CHANGE_ME*|*replace*me*|*Replace*Me*|*your*key*|*YOUR*KEY*|*api*key*here*|*API*KEY*HERE*|*token*here*|*TOKEN*HERE*)
20121
+ return 0
20122
+ ;;
20123
+ *)
20124
+ return 1
20125
+ ;;
20126
+ esac
20127
+ }
20128
+
20129
+ pluxx_prompt_secret_config() {
20130
+ local env_var="$1"
20131
+ local label="$2"
20132
+ local required="$3"
20133
+ local current_value="\${!env_var:-}"
20134
+
20135
+ if [[ -z "$current_value" && "$required" == "1" ]]; then
20136
+ if [[ -t 0 || -r /dev/tty ]]; then
20137
+ read -r -s -p "$label [$env_var]: " current_value </dev/tty
20138
+ echo >/dev/tty
20139
+ else
20140
+ echo "Missing required config: export $env_var before running this installer." >&2
20141
+ exit 1
20142
+ fi
20143
+ fi
20144
+
20145
+ if [[ -n "$current_value" ]] && pluxx_is_placeholder_secret "$current_value"; then
20146
+ echo "Refusing placeholder-looking secret for $env_var. Set a real value and rerun the installer." >&2
20147
+ exit 1
20148
+ fi
20149
+
20150
+ export "$env_var=$current_value"
20151
+ }
20152
+
20153
+ pluxx_prompt_text_config() {
20154
+ local env_var="$1"
20155
+ local label="$2"
20156
+ local required="$3"
20157
+ local current_value="\${!env_var:-}"
20158
+
20159
+ if [[ -z "$current_value" && "$required" == "1" ]]; then
20160
+ if [[ -t 0 || -r /dev/tty ]]; then
20161
+ read -r -p "$label [$env_var]: " current_value </dev/tty
20162
+ else
20163
+ echo "Missing required config: export $env_var before running this installer." >&2
20164
+ exit 1
20165
+ fi
20166
+ fi
20167
+
20168
+ export "$env_var=$current_value"
20169
+ }
20170
+
20171
+ ${promptLines.join("\n")}
20172
+
20173
+ export PLUXX_USER_CONFIG_SPEC
20174
+ export PLUXX_INSTALL_DIR="${installDirVariable}"
20175
+
20176
+ node <<'NODE'
20177
+ const fs = require('fs')
20178
+ const path = require('path')
20179
+
20180
+ const installDir = process.env.PLUXX_INSTALL_DIR
20181
+ const spec = JSON.parse(process.env.PLUXX_USER_CONFIG_SPEC || '[]')
20182
+
20183
+ if (installDir && spec.length > 0) {
20184
+ const env = {}
20185
+ const values = {}
20186
+
20187
+ for (const entry of spec) {
20188
+ const value = process.env[entry.envVar]
20189
+ if (value === undefined || value === '') continue
20190
+ values[entry.key] = value
20191
+ env[entry.envVar] = value
20192
+ }
20193
+
20194
+ fs.writeFileSync(
20195
+ path.join(installDir, '.pluxx-user.json'),
20196
+ JSON.stringify({ values, env }, null, 2) + '\\n',
20197
+ )
20198
+
20199
+ const envScriptPath = path.join(installDir, 'scripts/check-env.sh')
20200
+ if (fs.existsSync(envScriptPath)) {
20201
+ fs.writeFileSync(
20202
+ envScriptPath,
20203
+ '#!/usr/bin/env bash\\nset -euo pipefail\\n# pluxx install materialized required config for this local plugin install.\\nexit 0\\n',
20204
+ )
20205
+ }
20206
+
20207
+ const materialize = (value) =>
20208
+ typeof value === 'string'
20209
+ ? value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_match, name) => env[name] || '${" + name + "}')
20210
+ : value
20211
+
20212
+ const materializeRecord = (record) => {
20213
+ if (!record || typeof record !== 'object') return record
20214
+ const next = {}
20215
+ for (const [key, value] of Object.entries(record)) {
20216
+ next[key] = materialize(value)
20217
+ }
20218
+ return next
20219
+ }
20220
+
20221
+ for (const relativePath of ['.mcp.json', 'mcp.json']) {
20222
+ const filepath = path.join(installDir, relativePath)
20223
+ if (!fs.existsSync(filepath)) continue
20224
+
20225
+ const payload = JSON.parse(fs.readFileSync(filepath, 'utf8'))
20226
+ for (const server of Object.values(payload.mcpServers || {})) {
20227
+ if (!server || typeof server !== 'object') continue
20228
+
20229
+ if (server.env) {
20230
+ server.env = materializeRecord(server.env)
20231
+ }
20232
+
20233
+ if (server.bearer_token_env_var && env[server.bearer_token_env_var]) {
20234
+ server.http_headers = {
20235
+ ...(server.http_headers || {}),
20236
+ Authorization: 'Bearer ' + env[server.bearer_token_env_var],
20237
+ }
20238
+ delete server.bearer_token_env_var
20239
+ }
20240
+
20241
+ if (server.env_http_headers && typeof server.env_http_headers === 'object') {
20242
+ server.http_headers = {
20243
+ ...(server.http_headers || {}),
20244
+ }
20245
+ for (const [headerName, envVar] of Object.entries(server.env_http_headers)) {
20246
+ if (env[envVar]) server.http_headers[headerName] = env[envVar]
20247
+ }
20248
+ delete server.env_http_headers
20249
+ }
20250
+
20251
+ if (server.headers) {
20252
+ server.headers = materializeRecord(server.headers)
20253
+ }
20254
+ if (server.http_headers) {
20255
+ server.http_headers = materializeRecord(server.http_headers)
20256
+ }
20257
+ }
20258
+
20259
+ fs.writeFileSync(filepath, JSON.stringify(payload, null, 2) + '\\n')
20260
+ }
20261
+ }
20262
+ NODE
20263
+ `;
20264
+ }
20265
+ function hasInstallerUserConfig(config, platform) {
20266
+ return collectUserConfigEntries(config, [platform]).length > 0;
20267
+ }
19623
20268
  function renderInstallClaudeCodeScript(config) {
19624
20269
  return `#!/usr/bin/env bash
19625
20270
  set -euo pipefail
@@ -19646,6 +20291,7 @@ need_cmd tar
19646
20291
  need_cmd mktemp
19647
20292
  need_cmd grep
19648
20293
  need_cmd sed
20294
+ ${hasInstallerUserConfig(config, "claude-code") ? "need_cmd node" : ""}
19649
20295
 
19650
20296
  if [[ "$SKIP_INSTALL" != "1" ]]; then
19651
20297
  need_cmd curl
@@ -19682,6 +20328,7 @@ DESCRIPTION="$(grep -E '"description"' "$PLUGIN_MANIFEST" | head -n1 | sed -E 's
19682
20328
  mkdir -p "$INSTALL_ROOT/.claude-plugin" "$INSTALL_ROOT/plugins"
19683
20329
  rm -rf "$INSTALL_ROOT/plugins/$PLUGIN_NAME"
19684
20330
  cp -R "$BUNDLE_DIR" "$INSTALL_ROOT/plugins/$PLUGIN_NAME"
20331
+ ${renderInstallerUserConfigSnippet(config, "claude-code", "$INSTALL_ROOT/plugins/$PLUGIN_NAME")}
19685
20332
 
19686
20333
  cat > "$INSTALL_ROOT/.claude-plugin/marketplace.json" <<JSON
19687
20334
  {
@@ -19745,6 +20392,7 @@ need_cmd() {
19745
20392
  need_cmd tar
19746
20393
  need_cmd mktemp
19747
20394
  need_cmd curl
20395
+ ${hasInstallerUserConfig(config, "cursor") ? "need_cmd node" : ""}
19748
20396
 
19749
20397
  TMP_DIR="$(mktemp -d)"
19750
20398
  cleanup() {
@@ -19773,6 +20421,7 @@ fi
19773
20421
  mkdir -p "$(dirname "$INSTALL_DIR")"
19774
20422
  rm -rf "$INSTALL_DIR"
19775
20423
  cp -R "$BUNDLE_DIR" "$INSTALL_DIR"
20424
+ ${renderInstallerUserConfigSnippet(config, "cursor", "$INSTALL_DIR")}
19776
20425
 
19777
20426
  echo "Installed $PLUGIN_NAME to $INSTALL_DIR"
19778
20427
  echo "If Cursor is already open, use Developer: Reload Window or restart Cursor so the plugin is picked up."
@@ -19830,6 +20479,7 @@ fi
19830
20479
  mkdir -p "$(dirname "$INSTALL_DIR")"
19831
20480
  rm -rf "$INSTALL_DIR"
19832
20481
  cp -R "$BUNDLE_DIR" "$INSTALL_DIR"
20482
+ ${renderInstallerUserConfigSnippet(config, "codex", "$INSTALL_DIR")}
19833
20483
 
19834
20484
  mkdir -p "$(dirname "$MARKETPLACE_PATH")"
19835
20485
 
@@ -19944,6 +20594,7 @@ fi
19944
20594
  mkdir -p "$(dirname "$INSTALL_DIR")" "$SKILLS_ROOT"
19945
20595
  rm -rf "$INSTALL_DIR"
19946
20596
  cp -R "$BUNDLE_DIR" "$INSTALL_DIR"
20597
+ ${renderInstallerUserConfigSnippet(config, "opencode", "$INSTALL_DIR")}
19947
20598
 
19948
20599
  export ENTRY_PATH
19949
20600
  export PLUGIN_NAME
@@ -20271,22 +20922,107 @@ function printJson(value) {
20271
20922
  }
20272
20923
 
20273
20924
  // src/cli/verify-install.ts
20274
- import { existsSync as existsSync26 } from "fs";
20925
+ import { existsSync as existsSync27, lstatSync as lstatSync4, readdirSync as readdirSync11, readFileSync as readFileSync15, readlinkSync, realpathSync, statSync as statSync5 } from "fs";
20275
20926
  import { resolve as resolve21 } from "path";
20276
20927
  function buildCheckFromReport(target, pluginName, report) {
20277
20928
  const consumerPath = resolveInstalledConsumerPath(target, pluginName);
20929
+ const staleReason = target.built && existsSync27(consumerPath) ? detectStaleInstall(target, pluginName, consumerPath) : void 0;
20930
+ const stale = staleReason !== void 0;
20278
20931
  return {
20279
20932
  platform: target.platform,
20280
20933
  installPath: target.pluginDir,
20281
20934
  consumerPath,
20282
20935
  built: target.built,
20283
- installed: existsSync26(consumerPath),
20284
- ok: report.errors === 0,
20285
- errors: report.errors,
20936
+ installed: existsSync27(consumerPath),
20937
+ stale,
20938
+ ...staleReason ? { staleReason } : {},
20939
+ ok: report.errors === 0 && !stale,
20940
+ errors: report.errors + (stale ? 1 : 0),
20286
20941
  warnings: report.warnings,
20287
20942
  infos: report.infos
20288
20943
  };
20289
20944
  }
20945
+ function manifestPathForPlatform2(platform) {
20946
+ switch (platform) {
20947
+ case "claude-code":
20948
+ return ".claude-plugin/plugin.json";
20949
+ case "cursor":
20950
+ return ".cursor-plugin/plugin.json";
20951
+ case "codex":
20952
+ return ".codex-plugin/plugin.json";
20953
+ case "opencode":
20954
+ return "package.json";
20955
+ default:
20956
+ return void 0;
20957
+ }
20958
+ }
20959
+ function readInstalledManifestVersion(rootDir, platform) {
20960
+ const manifestPath = manifestPathForPlatform2(platform);
20961
+ if (!manifestPath) return void 0;
20962
+ const filepath = resolve21(rootDir, manifestPath);
20963
+ if (!existsSync27(filepath)) return void 0;
20964
+ try {
20965
+ const manifest = JSON.parse(readFileSync15(filepath, "utf-8"));
20966
+ return typeof manifest.version === "string" ? manifest.version : void 0;
20967
+ } catch {
20968
+ return void 0;
20969
+ }
20970
+ }
20971
+ function findCodexCacheCandidates(pluginName) {
20972
+ const home = process.env.HOME ?? "~";
20973
+ const cacheRoot = resolve21(home, ".codex/plugins/cache");
20974
+ if (!existsSync27(cacheRoot)) return [];
20975
+ const candidates = [];
20976
+ for (const marketplace of readdirSync11(cacheRoot)) {
20977
+ const pluginRoot = resolve21(cacheRoot, marketplace, pluginName);
20978
+ if (!existsSync27(pluginRoot)) continue;
20979
+ for (const versionDir of readdirSync11(pluginRoot)) {
20980
+ const candidatePath = resolve21(pluginRoot, versionDir);
20981
+ try {
20982
+ const stats = statSync5(candidatePath);
20983
+ if (!stats.isDirectory()) continue;
20984
+ candidates.push({
20985
+ path: candidatePath,
20986
+ version: readInstalledManifestVersion(candidatePath, "codex") ?? versionDir,
20987
+ mtimeMs: stats.mtimeMs
20988
+ });
20989
+ } catch {
20990
+ }
20991
+ }
20992
+ }
20993
+ return candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
20994
+ }
20995
+ function detectCodexCacheStaleness(pluginName, builtVersion) {
20996
+ if (!builtVersion) return void 0;
20997
+ const candidates = findCodexCacheCandidates(pluginName);
20998
+ if (candidates.length === 0) return void 0;
20999
+ if (candidates.some((candidate) => candidate.version === builtVersion)) return void 0;
21000
+ const newest = candidates[0];
21001
+ 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.`;
21002
+ }
21003
+ function detectStaleInstall(target, pluginName, consumerPath) {
21004
+ try {
21005
+ const details = lstatSync4(consumerPath);
21006
+ if (details.isSymbolicLink()) {
21007
+ const installedRealPath = realpathSync(consumerPath);
21008
+ const builtRealPath = realpathSync(target.sourceDir);
21009
+ if (installedRealPath !== builtRealPath) {
21010
+ return `installed symlink points to ${readlinkSync(consumerPath)}, not the current build at ${target.sourceDir}`;
21011
+ }
21012
+ }
21013
+ } catch {
21014
+ return void 0;
21015
+ }
21016
+ const builtVersion = readInstalledManifestVersion(target.sourceDir, target.platform);
21017
+ const installedVersion = readInstalledManifestVersion(consumerPath, target.platform);
21018
+ if (builtVersion && installedVersion && builtVersion !== installedVersion) {
21019
+ return `installed version ${installedVersion} does not match built version ${builtVersion}`;
21020
+ }
21021
+ if (target.platform === "codex") {
21022
+ return detectCodexCacheStaleness(pluginName, builtVersion);
21023
+ }
21024
+ return void 0;
21025
+ }
20290
21026
  async function verifyInstall(config, options = {}) {
20291
21027
  const rootDir = options.rootDir ?? process.cwd();
20292
21028
  const distDir = resolve21(rootDir, config.outDir);
@@ -20314,12 +21050,39 @@ function printVerifyInstallResult(result) {
20314
21050
  console.log(`${prefix} ${check.platform}: ${check.consumerPath}`);
20315
21051
  console.log(` install path: ${check.installPath}`);
20316
21052
  console.log(` built: ${check.built ? "yes" : "no"}; installed: ${check.installed ? "yes" : "no"}; errors: ${check.errors}; warnings: ${check.warnings}; infos: ${check.infos}`);
21053
+ if (check.stale) {
21054
+ console.log(` stale install: ${check.staleReason}`);
21055
+ }
21056
+ if (!check.ok) {
21057
+ for (const action of getVerifyInstallRecoveryActions(check)) {
21058
+ console.log(` fix: ${action}`);
21059
+ }
21060
+ }
20317
21061
  }
20318
21062
  console.log(result.ok ? "pluxx verify-install passed." : "pluxx verify-install failed.");
20319
21063
  }
21064
+ function getVerifyInstallRecoveryActions(check) {
21065
+ const actions = [];
21066
+ if (!check.built) {
21067
+ actions.push(`run pluxx build --target ${check.platform}`);
21068
+ }
21069
+ if (check.built && !check.installed) {
21070
+ actions.push(`run pluxx install --target ${check.platform}${check.platform === "claude-code" ? " and accept/trust any hook prompt if expected" : ""}`);
21071
+ }
21072
+ if (check.stale) {
21073
+ actions.push(`rerun pluxx install --target ${check.platform} to replace the stale local install`);
21074
+ if (check.platform === "codex") {
21075
+ actions.push("in Codex, use Plugins > Refresh if available, or restart Codex so the plugin cache reloads");
21076
+ }
21077
+ }
21078
+ if (check.errors > 0 && actions.length === 0) {
21079
+ actions.push(`run pluxx doctor --consumer "${check.consumerPath}" for the detailed host-specific failure`);
21080
+ }
21081
+ return actions;
21082
+ }
20320
21083
 
20321
21084
  // src/cli/behavioral.ts
20322
- import { existsSync as existsSync27, readFileSync as readFileSync15 } from "fs";
21085
+ import { existsSync as existsSync28, readFileSync as readFileSync16 } from "fs";
20323
21086
  import { mkdtemp as mkdtemp2, rm as rm3 } from "fs/promises";
20324
21087
  import { tmpdir as tmpdir4 } from "os";
20325
21088
  import { resolve as resolve22 } from "path";
@@ -20356,12 +21119,12 @@ function loadBehavioralCases(rootDir, targets, promptOverride) {
20356
21119
  }];
20357
21120
  }
20358
21121
  const filePath = resolve22(rootDir, BEHAVIORAL_CONFIG_PATH);
20359
- if (!existsSync27(filePath)) {
21122
+ if (!existsSync28(filePath)) {
20360
21123
  throw new Error(
20361
21124
  `No behavioral smoke config found at ${BEHAVIORAL_CONFIG_PATH}. Add that file or pass --behavioral-prompt to define a real example query.`
20362
21125
  );
20363
21126
  }
20364
- const parsed = JSON.parse(readFileSync15(filePath, "utf-8"));
21127
+ const parsed = JSON.parse(readFileSync16(filePath, "utf-8"));
20365
21128
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.cases) || parsed.cases.length === 0) {
20366
21129
  throw new Error(`${BEHAVIORAL_CONFIG_PATH} must contain a non-empty "cases" array.`);
20367
21130
  }
@@ -20471,7 +21234,7 @@ async function executeBehavioralCommand(platform, command2, cwd) {
20471
21234
  child.on("close", (code) => {
20472
21235
  const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
20473
21236
  const stderr = Buffer.concat(stderrChunks).toString("utf-8");
20474
- const codexMessage = codexLastMessagePath && existsSync27(codexLastMessagePath) ? readFileSync15(codexLastMessagePath, "utf-8") : "";
21237
+ const codexMessage = codexLastMessagePath && existsSync28(codexLastMessagePath) ? readFileSync16(codexLastMessagePath, "utf-8") : "";
20475
21238
  resolvePromise({
20476
21239
  exitCode: code ?? 1,
20477
21240
  response: codexMessage.trim() || stdout.trim() || stderr.trim()
@@ -20533,6 +21296,396 @@ function shellQuote2(value) {
20533
21296
  return `'${value.replace(/'/g, `'\\''`)}'`;
20534
21297
  }
20535
21298
 
21299
+ // src/cli/discover-installed-mcp.ts
21300
+ import { existsSync as existsSync29, readFileSync as readFileSync17 } from "fs";
21301
+ import { homedir as homedir2 } from "os";
21302
+ import { resolve as resolve23 } from "path";
21303
+ var INSTALLED_MCP_HOSTS = ["claude-code", "cursor", "codex", "opencode"];
21304
+ function discoverInstalledMcpServers(options = {}) {
21305
+ const rootDir = options.rootDir ?? process.cwd();
21306
+ const homeDir = options.homeDir ?? homedir2();
21307
+ const hostSet = new Set(options.hosts ?? INSTALLED_MCP_HOSTS);
21308
+ const results = [];
21309
+ const seen = /* @__PURE__ */ new Set();
21310
+ for (const candidate of installedMcpFileCandidates(rootDir, homeDir)) {
21311
+ if (!hostSet.has(candidate.host) || !existsSync29(candidate.path)) continue;
21312
+ const parsed = candidate.parser === "json" ? parseJsonMcpFile(candidate.path, candidate.host) : parseCodexTomlMcpFile(candidate.path);
21313
+ for (const discovered of parsed) {
21314
+ const key = `${discovered.host}:${discovered.serverName}:${discovered.sourcePath}`;
21315
+ if (seen.has(key)) continue;
21316
+ seen.add(key);
21317
+ results.push(discovered);
21318
+ }
21319
+ }
21320
+ return results.sort((a, b) => {
21321
+ const hostOrder = INSTALLED_MCP_HOSTS.indexOf(a.host) - INSTALLED_MCP_HOSTS.indexOf(b.host);
21322
+ if (hostOrder !== 0) return hostOrder;
21323
+ return a.serverName.localeCompare(b.serverName);
21324
+ });
21325
+ }
21326
+ function resolveInstalledMcpSelector(selector, candidates) {
21327
+ const trimmed = selector.trim();
21328
+ if (!trimmed) {
21329
+ throw new Error("Provide an installed MCP selector such as `exa` or `codex:exa`.");
21330
+ }
21331
+ const exactId = candidates.filter((candidate) => candidate.id === trimmed);
21332
+ if (exactId.length === 1) return exactId[0];
21333
+ const hostMatch = trimmed.match(/^([^:]+):(.+)$/);
21334
+ if (hostMatch) {
21335
+ const host = normalizeInstalledMcpHost(hostMatch[1]);
21336
+ const serverName = hostMatch[2];
21337
+ const matches = candidates.filter((candidate) => candidate.host === host && candidate.serverName === serverName);
21338
+ if (matches.length === 1) return matches[0];
21339
+ if (matches.length > 1) {
21340
+ throw new Error(`Installed MCP selector "${trimmed}" matched multiple config files. Use one of: ${matches.map((match) => match.id).join(", ")}`);
21341
+ }
21342
+ }
21343
+ const nameMatches = candidates.filter((candidate) => candidate.serverName === trimmed);
21344
+ if (nameMatches.length === 1) return nameMatches[0];
21345
+ if (nameMatches.length > 1) {
21346
+ throw new Error(`Installed MCP selector "${trimmed}" is ambiguous. Use one of: ${nameMatches.map((match) => match.id).join(", ")}`);
21347
+ }
21348
+ const available = candidates.map((candidate) => candidate.id).join(", ") || "none";
21349
+ throw new Error(`No installed MCP named "${trimmed}" was found. Available installed MCPs: ${available}`);
21350
+ }
21351
+ function normalizeInstalledMcpHost(value) {
21352
+ const normalized = value.trim().toLowerCase();
21353
+ const aliases = {
21354
+ claude: "claude-code",
21355
+ "claude-code": "claude-code",
21356
+ cursor: "cursor",
21357
+ codex: "codex",
21358
+ open: "opencode",
21359
+ opencode: "opencode"
21360
+ };
21361
+ const host = aliases[normalized];
21362
+ if (!host) {
21363
+ throw new Error(`Installed MCP host must be one of: ${INSTALLED_MCP_HOSTS.join(", ")}`);
21364
+ }
21365
+ return host;
21366
+ }
21367
+ function formatInstalledMcpSource(discovered) {
21368
+ const transport = discovered.server.transport;
21369
+ const endpoint = transport === "stdio" ? [discovered.server.command, ...discovered.server.args ?? []].join(" ") : discovered.server.url;
21370
+ return `${discovered.id} (${transport}: ${endpoint})`;
21371
+ }
21372
+ function installedMcpFileCandidates(rootDir, homeDir) {
21373
+ return [
21374
+ { host: "claude-code", path: resolve23(rootDir, ".mcp.json"), parser: "json" },
21375
+ { host: "claude-code", path: resolve23(rootDir, ".claude-plugin/plugin.json"), parser: "json" },
21376
+ { host: "claude-code", path: resolve23(rootDir, ".claude/settings.json"), parser: "json" },
21377
+ { host: "claude-code", path: resolve23(rootDir, ".claude/settings.local.json"), parser: "json" },
21378
+ { host: "claude-code", path: resolve23(homeDir, ".claude/settings.json"), parser: "json" },
21379
+ { host: "claude-code", path: resolve23(homeDir, ".claude/settings.local.json"), parser: "json" },
21380
+ { host: "claude-code", path: resolve23(homeDir, ".claude.json"), parser: "json" },
21381
+ { host: "cursor", path: resolve23(rootDir, "mcp.json"), parser: "json" },
21382
+ { host: "cursor", path: resolve23(rootDir, ".cursor/mcp.json"), parser: "json" },
21383
+ { host: "cursor", path: resolve23(homeDir, ".cursor/mcp.json"), parser: "json" },
21384
+ { host: "codex", path: resolve23(rootDir, ".mcp.json"), parser: "json" },
21385
+ { host: "codex", path: resolve23(rootDir, ".codex/config.toml"), parser: "toml" },
21386
+ { host: "codex", path: resolve23(homeDir, ".codex/config.toml"), parser: "toml" },
21387
+ { host: "opencode", path: resolve23(rootDir, "opencode.json"), parser: "json" },
21388
+ { host: "opencode", path: resolve23(rootDir, ".opencode.json"), parser: "json" },
21389
+ { host: "opencode", path: resolve23(homeDir, ".config/opencode/opencode.json"), parser: "json" }
21390
+ ];
21391
+ }
21392
+ function parseJsonMcpFile(path, host) {
21393
+ try {
21394
+ const raw = JSON.parse(readFileSync17(path, "utf-8"));
21395
+ const servers = extractJsonMcpServers(raw, path, host);
21396
+ return Object.entries(servers).flatMap(([serverName, config]) => {
21397
+ const normalized = normalizeCommonMcpServer(config, host);
21398
+ if (!normalized) return [];
21399
+ return [toDiscovered(host, serverName, path, normalized.server, normalized.warnings)];
21400
+ });
21401
+ } catch {
21402
+ return [];
21403
+ }
21404
+ }
21405
+ function extractJsonMcpServers(raw, path, host) {
21406
+ if (host === "opencode" && raw.mcp && typeof raw.mcp === "object") {
21407
+ return raw.mcp;
21408
+ }
21409
+ if (raw.mcpServers && typeof raw.mcpServers === "object") {
21410
+ return raw.mcpServers;
21411
+ }
21412
+ if (host === "claude-code" && raw.projects && typeof raw.projects === "object") {
21413
+ const projectServers = {};
21414
+ for (const projectConfig of Object.values(raw.projects)) {
21415
+ if (!projectConfig || typeof projectConfig !== "object") continue;
21416
+ const mcpServers = projectConfig.mcpServers;
21417
+ if (!mcpServers || typeof mcpServers !== "object") continue;
21418
+ Object.assign(projectServers, mcpServers);
21419
+ }
21420
+ return projectServers;
21421
+ }
21422
+ if (typeof raw.mcpServers === "string") return {};
21423
+ if (path.endsWith("mcp.json") || path.endsWith(".mcp.json")) {
21424
+ return raw;
21425
+ }
21426
+ return {};
21427
+ }
21428
+ function parseCodexTomlMcpFile(path) {
21429
+ const text = readFileSync17(path, "utf-8");
21430
+ const servers = {};
21431
+ let currentServer;
21432
+ let currentSubtable;
21433
+ for (const rawLine of text.split(/\r?\n/)) {
21434
+ const line = stripTomlComment(rawLine).trim();
21435
+ if (!line) continue;
21436
+ const section = line.match(/^\[mcp_servers\.([A-Za-z0-9_.-]+)(?:\.([A-Za-z0-9_.-]+))?\]$/);
21437
+ if (section) {
21438
+ currentServer = section[1];
21439
+ currentSubtable = section[2];
21440
+ servers[currentServer] ??= {};
21441
+ if (currentSubtable) {
21442
+ const existing = servers[currentServer][currentSubtable];
21443
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
21444
+ servers[currentServer][currentSubtable] = {};
21445
+ }
21446
+ }
21447
+ continue;
21448
+ }
21449
+ if (!currentServer) continue;
21450
+ const assignment = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/);
21451
+ if (!assignment) continue;
21452
+ const key = assignment[1];
21453
+ const value = parseTomlValue(assignment[2].trim());
21454
+ if (currentSubtable) {
21455
+ const table = servers[currentServer][currentSubtable];
21456
+ table[key] = value;
21457
+ } else {
21458
+ servers[currentServer][key] = value;
21459
+ }
21460
+ }
21461
+ return Object.entries(servers).flatMap(([serverName, config]) => {
21462
+ const normalized = normalizeCommonMcpServer(config, "codex");
21463
+ if (!normalized) return [];
21464
+ return [toDiscovered("codex", serverName, path, normalized.server, normalized.warnings)];
21465
+ });
21466
+ }
21467
+ function normalizeCommonMcpServer(config, host) {
21468
+ if (!config || typeof config !== "object") return null;
21469
+ const cfg = config;
21470
+ const warnings = [];
21471
+ const auth = inferAuth(cfg, warnings);
21472
+ const remoteUrl = firstString(cfg.url, cfg.endpoint);
21473
+ if (remoteUrl) {
21474
+ const server = cfg.type === "sse" || cfg.transport === "sse" ? {
21475
+ transport: "sse",
21476
+ url: remoteUrl,
21477
+ ...auth ? { auth } : {}
21478
+ } : {
21479
+ transport: "http",
21480
+ url: remoteUrl,
21481
+ ...auth ? { auth } : {}
21482
+ };
21483
+ return { server, warnings };
21484
+ }
21485
+ const command2 = normalizeCommand(cfg.command, host);
21486
+ if (!command2) return null;
21487
+ const args2 = normalizeArgs(cfg.command, cfg.args, host);
21488
+ const env = normalizeEnv(cfg.env, warnings);
21489
+ return {
21490
+ server: {
21491
+ transport: "stdio",
21492
+ command: command2,
21493
+ ...args2.length > 0 ? { args: args2 } : {},
21494
+ ...Object.keys(env).length > 0 ? { env } : {},
21495
+ ...auth ? { auth } : {}
21496
+ },
21497
+ warnings
21498
+ };
21499
+ }
21500
+ function normalizeCommand(value, host) {
21501
+ if (typeof value === "string") return value;
21502
+ if (host === "opencode" && Array.isArray(value) && typeof value[0] === "string") {
21503
+ return value[0];
21504
+ }
21505
+ return void 0;
21506
+ }
21507
+ function normalizeArgs(command2, args2, host) {
21508
+ if (host === "opencode" && Array.isArray(command2)) {
21509
+ return command2.slice(1).map(String);
21510
+ }
21511
+ return normalizeStringArray(args2);
21512
+ }
21513
+ function normalizeStringArray(value) {
21514
+ if (!Array.isArray(value)) return [];
21515
+ return value.map(String);
21516
+ }
21517
+ function normalizeEnv(value, warnings) {
21518
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
21519
+ const env = {};
21520
+ for (const [key, rawValue] of Object.entries(value)) {
21521
+ if (typeof rawValue !== "string") continue;
21522
+ const placeholder = envPlaceholder(rawValue);
21523
+ if (placeholder) {
21524
+ env[key] = placeholder;
21525
+ continue;
21526
+ }
21527
+ if (looksSecretKey(key)) {
21528
+ env[key] = `\${${key}}`;
21529
+ warnings.push(`Literal secret-like env value for ${key} was replaced with \${${key}}.`);
21530
+ continue;
21531
+ }
21532
+ env[key] = rawValue;
21533
+ }
21534
+ return env;
21535
+ }
21536
+ function inferAuth(cfg, warnings) {
21537
+ const bearerTokenEnv = firstString(cfg.bearer_token_env_var, cfg.bearerTokenEnvVar);
21538
+ if (bearerTokenEnv) {
21539
+ return {
21540
+ type: "bearer",
21541
+ envVar: bearerTokenEnv,
21542
+ headerName: "Authorization",
21543
+ headerTemplate: "Bearer ${value}"
21544
+ };
21545
+ }
21546
+ const envHttpHeaders = cfg.env_http_headers ?? cfg.envHttpHeaders;
21547
+ if (envHttpHeaders && typeof envHttpHeaders === "object" && !Array.isArray(envHttpHeaders)) {
21548
+ const [headerName, envVar] = Object.entries(envHttpHeaders).find(([, value]) => typeof value === "string") ?? [];
21549
+ if (typeof headerName === "string" && typeof envVar === "string") {
21550
+ return {
21551
+ type: headerName.toLowerCase() === "authorization" ? "bearer" : "header",
21552
+ envVar,
21553
+ headerName,
21554
+ headerTemplate: headerName.toLowerCase() === "authorization" ? "Bearer ${value}" : "${value}"
21555
+ };
21556
+ }
21557
+ }
21558
+ const headers = cfg.headers ?? cfg.http_headers ?? cfg.httpHeaders;
21559
+ if (!headers || typeof headers !== "object" || Array.isArray(headers)) return void 0;
21560
+ const headerEntries = Object.entries(headers);
21561
+ for (const [headerName, rawValue] of headerEntries) {
21562
+ if (typeof rawValue !== "string") continue;
21563
+ const envVar = extractEnvVar(rawValue);
21564
+ if (envVar) {
21565
+ return {
21566
+ type: headerName.toLowerCase() === "authorization" ? "bearer" : "header",
21567
+ envVar,
21568
+ headerName,
21569
+ headerTemplate: rawValue.replace(envReferencePattern(envVar), "${value}")
21570
+ };
21571
+ }
21572
+ if (looksSecretValue(rawValue)) {
21573
+ warnings.push(`Literal ${headerName} header value was not copied. Re-run init with --auth-env if this server needs auth.`);
21574
+ }
21575
+ }
21576
+ return void 0;
21577
+ }
21578
+ function toDiscovered(host, serverName, sourcePath, server, warnings) {
21579
+ return {
21580
+ id: `${host}:${serverName}`,
21581
+ host,
21582
+ serverName,
21583
+ sourcePath,
21584
+ server,
21585
+ warnings
21586
+ };
21587
+ }
21588
+ function firstString(...values) {
21589
+ for (const value of values) {
21590
+ if (typeof value === "string" && value.trim()) return value.trim();
21591
+ }
21592
+ return void 0;
21593
+ }
21594
+ function envPlaceholder(value) {
21595
+ const envVar = extractEnvVar(value);
21596
+ return envVar ? `\${${envVar}}` : void 0;
21597
+ }
21598
+ function extractEnvVar(value) {
21599
+ const match = value.match(/\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/);
21600
+ return match?.[1] ?? match?.[2];
21601
+ }
21602
+ function envReferencePattern(envVar) {
21603
+ return new RegExp(`\\$\\{(?:env:)?${escapeRegExp2(envVar)}\\}|\\$${escapeRegExp2(envVar)}`);
21604
+ }
21605
+ function escapeRegExp2(value) {
21606
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
21607
+ }
21608
+ function looksSecretKey(value) {
21609
+ return /(api|auth|secret|token|key|password|credential)/i.test(value);
21610
+ }
21611
+ function looksSecretValue(value) {
21612
+ return value.length >= 16 && !extractEnvVar(value);
21613
+ }
21614
+ function stripTomlComment(line) {
21615
+ let inString = false;
21616
+ let quote2 = "";
21617
+ for (let i = 0; i < line.length; i += 1) {
21618
+ const char = line[i];
21619
+ if ((char === '"' || char === "'") && line[i - 1] !== "\\") {
21620
+ if (!inString) {
21621
+ inString = true;
21622
+ quote2 = char;
21623
+ } else if (quote2 === char) {
21624
+ inString = false;
21625
+ quote2 = "";
21626
+ }
21627
+ continue;
21628
+ }
21629
+ if (char === "#" && !inString) return line.slice(0, i);
21630
+ }
21631
+ return line;
21632
+ }
21633
+ function parseTomlValue(value) {
21634
+ if (value.startsWith('"') && value.endsWith('"')) return unquoteTomlString(value);
21635
+ if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
21636
+ if (value.startsWith("[") && value.endsWith("]")) {
21637
+ const inner = value.slice(1, -1).trim();
21638
+ if (!inner) return [];
21639
+ return splitTomlList(inner).map((part) => parseTomlValue(part.trim()));
21640
+ }
21641
+ if (value.startsWith("{") && value.endsWith("}")) {
21642
+ const inner = value.slice(1, -1).trim();
21643
+ const result = {};
21644
+ if (!inner) return result;
21645
+ for (const part of splitTomlList(inner)) {
21646
+ const assignment = part.match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/);
21647
+ if (!assignment) continue;
21648
+ result[assignment[1]] = parseTomlValue(assignment[2].trim());
21649
+ }
21650
+ return result;
21651
+ }
21652
+ if (value === "true") return true;
21653
+ if (value === "false") return false;
21654
+ return value;
21655
+ }
21656
+ function splitTomlList(value) {
21657
+ const parts = [];
21658
+ let current = "";
21659
+ let inString = false;
21660
+ let quote2 = "";
21661
+ let braceDepth = 0;
21662
+ for (let i = 0; i < value.length; i += 1) {
21663
+ const char = value[i];
21664
+ if ((char === '"' || char === "'") && value[i - 1] !== "\\") {
21665
+ if (!inString) {
21666
+ inString = true;
21667
+ quote2 = char;
21668
+ } else if (quote2 === char) {
21669
+ inString = false;
21670
+ quote2 = "";
21671
+ }
21672
+ }
21673
+ if (!inString && char === "{") braceDepth += 1;
21674
+ if (!inString && char === "}") braceDepth -= 1;
21675
+ if (!inString && braceDepth === 0 && char === ",") {
21676
+ parts.push(current);
21677
+ current = "";
21678
+ continue;
21679
+ }
21680
+ current += char;
21681
+ }
21682
+ if (current.trim()) parts.push(current);
21683
+ return parts;
21684
+ }
21685
+ function unquoteTomlString(value) {
21686
+ return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\\\/g, "\\");
21687
+ }
21688
+
20536
21689
  // src/cli/index.ts
20537
21690
  var CLI_PACKAGE_NAME = "@orchid-labs/pluxx";
20538
21691
  var rawArgs = process.argv.slice(2);
@@ -20583,6 +21736,9 @@ async function main() {
20583
21736
  case "mcp":
20584
21737
  await runMcp();
20585
21738
  break;
21739
+ case "discover-mcp":
21740
+ await runDiscoverMcp();
21741
+ break;
20586
21742
  case "autopilot":
20587
21743
  await runAutopilot();
20588
21744
  break;
@@ -20596,6 +21752,10 @@ async function main() {
20596
21752
  await runVerifyInstall();
20597
21753
  break;
20598
21754
  case "publish":
21755
+ if (isHelpRequested(args.slice(1))) {
21756
+ printPublishHelp();
21757
+ break;
21758
+ }
20599
21759
  await runPublishCommand();
20600
21760
  break;
20601
21761
  case "uninstall":
@@ -20634,9 +21794,12 @@ function normalizeTopLevelArgs(input) {
20634
21794
  }
20635
21795
  return input;
20636
21796
  }
21797
+ function isHelpRequested(input) {
21798
+ return input.includes("--help") || input.includes("-h");
21799
+ }
20637
21800
  function getCliPackageVersion() {
20638
21801
  const packageJsonPath = new URL("../../package.json", import.meta.url);
20639
- const raw = JSON.parse(readFileSync16(packageJsonPath, "utf-8"));
21802
+ const raw = JSON.parse(readFileSync18(packageJsonPath, "utf-8"));
20640
21803
  if (typeof raw.version !== "string" || raw.version.trim() === "") {
20641
21804
  throw new Error("Unable to determine the installed pluxx version from package.json.");
20642
21805
  }
@@ -20748,20 +21911,21 @@ function planAutopilotPasses(input) {
20748
21911
  };
20749
21912
  }
20750
21913
  function countAutopilotSteps(input) {
20751
- return 2 + Number(input.taxonomy.enabled) + Number(input.instructions.enabled) + Number(input.review.enabled) + Number(input.verify);
21914
+ return 2 + Number(input.taxonomy.enabled) + Number(input.instructions.enabled) + Number(input.review.enabled) + Number(input.verify) + Number(input.install) + Number(input.behavioral);
20752
21915
  }
20753
21916
  function formatAutopilotPassLine(label, decision) {
20754
21917
  return `${label}: ${decision.enabled ? "run" : "skip"} (${decision.reason})`;
20755
21918
  }
20756
21919
  function summarizeAutopilotWorkload(input) {
20757
21920
  const agentPassCount = Number(input.taxonomy.enabled) + Number(input.instructions.enabled) + Number(input.review.enabled);
21921
+ const suffix = `${input.verify ? " + verification" : ""}${input.install ? " + install" : ""}${input.behavioral ? " + behavioral smoke" : ""}`;
20758
21922
  if (agentPassCount === 0 && !input.verify) {
20759
- return "deterministic scaffold only";
21923
+ return `deterministic scaffold${input.install ? " + install" : ""}${input.behavioral ? " + behavioral smoke" : ""}`;
20760
21924
  }
20761
21925
  if (agentPassCount === 0) {
20762
- return "deterministic scaffold + verification";
21926
+ return `deterministic scaffold${suffix}`;
20763
21927
  }
20764
- return `${agentPassCount} agent pass${agentPassCount === 1 ? "" : "es"}${input.verify ? " + verification" : ""}`;
21928
+ return `${agentPassCount} agent pass${agentPassCount === 1 ? "" : "es"}${suffix}`;
20765
21929
  }
20766
21930
  function formatDuration(durationMs) {
20767
21931
  if (durationMs === void 0) {
@@ -21004,6 +22168,16 @@ function parseTargetFlagValues(rawArgs2) {
21004
22168
  if (!values) return void 0;
21005
22169
  return parseTargetPlatforms(values.join(","));
21006
22170
  }
22171
+ function parseInstallTargetFlag(rawArgs2, targets) {
22172
+ const raw = readOption2(rawArgs2, "--install-target");
22173
+ if (!raw) return [targets[0]];
22174
+ const installTargets = parseTargetPlatforms(raw);
22175
+ const unsupported = installTargets.filter((target) => !targets.includes(target));
22176
+ if (unsupported.length > 0) {
22177
+ throw new Error(`--install-target must be one of the configured autopilot targets: ${targets.join(", ")}`);
22178
+ }
22179
+ return installTargets;
22180
+ }
21007
22181
  function defaultHookMode(source) {
21008
22182
  if (source.auth?.type && source.auth.type !== "none" && source.auth.envVar) {
21009
22183
  return "safe";
@@ -21296,7 +22470,7 @@ async function planInitContextArtifactFiles(rootDir, contextPack) {
21296
22470
  return plannedFiles;
21297
22471
  }
21298
22472
  async function planAuxiliaryFile(rootDir, relativePath, content) {
21299
- const filePath = resolve23(rootDir, relativePath);
22473
+ const filePath = resolve24(rootDir, relativePath);
21300
22474
  const action = await planTextFileAction(filePath, content);
21301
22475
  return {
21302
22476
  relativePath,
@@ -21326,6 +22500,7 @@ function formatMcpDiscoverySummary(introspection) {
21326
22500
  function parseInitFromMcpOptions(rawArgs2, initialName, initialSource) {
21327
22501
  return {
21328
22502
  source: initialSource ?? readOption2(rawArgs2, "--from-mcp"),
22503
+ installedMcp: readOption2(rawArgs2, "--from-installed-mcp"),
21329
22504
  assumeDefaults: rawArgs2.includes("--yes"),
21330
22505
  name: readOption2(rawArgs2, "--name") ?? initialName,
21331
22506
  author: readOption2(rawArgs2, "--author"),
@@ -21354,11 +22529,49 @@ function toKebabCase3(value) {
21354
22529
  function toTsString(value) {
21355
22530
  return JSON.stringify(value);
21356
22531
  }
22532
+ function parseInstalledMcpHosts(rawArgs2) {
22533
+ const hostValues = readMultiValueOption(rawArgs2, "--host") ?? readMultiValueOption(rawArgs2, "--hosts");
22534
+ if (!hostValues) return void 0;
22535
+ return hostValues.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean).map(normalizeInstalledMcpHost);
22536
+ }
22537
+ function resolveInstalledMcpForInit(selector, hosts) {
22538
+ const discovered = discoverInstalledMcpServers({ hosts });
22539
+ return resolveInstalledMcpSelector(selector, discovered);
22540
+ }
22541
+ async function runDiscoverMcp() {
22542
+ const hosts = parseInstalledMcpHosts(args);
22543
+ const discovered = discoverInstalledMcpServers({ hosts });
22544
+ if (runtime.jsonOutput) {
22545
+ printJson({
22546
+ count: discovered.length,
22547
+ servers: discovered
22548
+ });
22549
+ return;
22550
+ }
22551
+ if (discovered.length === 0) {
22552
+ console.log("No installed MCP servers found.");
22553
+ console.log(`Searched hosts: ${(hosts ?? INSTALLED_MCP_HOSTS).join(", ")}`);
22554
+ return;
22555
+ }
22556
+ console.log("Installed MCP servers:");
22557
+ for (const server of discovered) {
22558
+ console.log(` ${server.id}`);
22559
+ console.log(` source: ${server.sourcePath}`);
22560
+ console.log(` server: ${formatInstalledMcpSource(server)}`);
22561
+ for (const warning of server.warnings) {
22562
+ console.log(` warning: ${warning}`);
22563
+ }
22564
+ }
22565
+ console.log("");
22566
+ console.log("Import one:");
22567
+ console.log(` pluxx init --from-installed-mcp ${discovered[0].id} --yes`);
22568
+ }
21357
22569
  async function runInit() {
21358
22570
  const positionalName = args[1] && !args[1].startsWith("-") ? args[1] : void 0;
21359
22571
  const fromMcpFlag = args.indexOf("--from-mcp");
22572
+ const fromInstalledMcpFlag = args.indexOf("--from-installed-mcp");
21360
22573
  const fromMcpInput = fromMcpFlag !== -1 && args[fromMcpFlag + 1] && !args[fromMcpFlag + 1].startsWith("-") ? args[fromMcpFlag + 1] : void 0;
21361
- if (fromMcpFlag !== -1) {
22574
+ if (fromMcpFlag !== -1 || fromInstalledMcpFlag !== -1) {
21362
22575
  await runInitFromMcp(positionalName, fromMcpInput);
21363
22576
  return;
21364
22577
  }
@@ -21442,7 +22655,7 @@ ${mcpBlock}${brandBlock}
21442
22655
  targets: [${targetsList}],
21443
22656
  })
21444
22657
  `;
21445
- await writeTextFile(resolve23(process.cwd(), "pluxx.config.ts"), template);
22658
+ await writeTextFile(resolve24(process.cwd(), "pluxx.config.ts"), template);
21446
22659
  const skillDir = `skills/${skillName}`;
21447
22660
  await mkdir4(skillDir, { recursive: true });
21448
22661
  const skillContent = `---
@@ -21464,7 +22677,7 @@ Describe how agents should use this skill.
21464
22677
  Example prompt or command here
21465
22678
  \`\`\`
21466
22679
  `;
21467
- await writeTextFile(resolve23(process.cwd(), `${skillDir}/SKILL.md`), skillContent);
22680
+ await writeTextFile(resolve24(process.cwd(), `${skillDir}/SKILL.md`), skillContent);
21468
22681
  console.log("");
21469
22682
  console.log(" Created:");
21470
22683
  console.log(" pluxx.config.ts");
@@ -21493,14 +22706,15 @@ async function runInitFromMcp(initialName, initialSource) {
21493
22706
  const contextPaths = options.contextPaths ?? [];
21494
22707
  const ingestProvider = options.ingestProvider ? parseChoiceOption(options.ingestProvider, AGENT_INGEST_PROVIDERS, "Ingestion provider") : void 0;
21495
22708
  if (!options.jsonOutput && !runtime.quiet) {
21496
- mt("pluxx init --from-mcp");
22709
+ mt(options.installedMcp ? "pluxx init --from-installed-mcp" : "pluxx init --from-mcp");
21497
22710
  }
21498
22711
  try {
21499
- const rawSource = options.source ?? (interactive ? await clackText("MCP server URL or local command") : "");
22712
+ const installedMcpSource = options.installedMcp ? resolveInstalledMcpForInit(options.installedMcp, parseInstalledMcpHosts(args)) : void 0;
22713
+ const rawSource = installedMcpSource ? formatInstalledMcpSource(installedMcpSource) : options.source ?? (interactive ? await clackText("MCP server URL or local command") : "");
21500
22714
  if (!rawSource) {
21501
- throw new Error("Provide an MCP server URL or local command. Example: pluxx init --from-mcp https://example.com/mcp");
22715
+ throw new Error("Provide an MCP server URL/local command or an installed MCP selector. Example: pluxx init --from-mcp https://example.com/mcp");
21502
22716
  }
21503
- let source = parseMcpSourceInput(rawSource, options.transport);
22717
+ let source = installedMcpSource?.server ?? parseMcpSourceInput(rawSource, options.transport);
21504
22718
  let introspectionSource = source;
21505
22719
  const configuredRemoteAuth = source.transport === "stdio" ? void 0 : buildRemoteAuthConfig(options);
21506
22720
  if (configuredRemoteAuth && !source.auth) {
@@ -21653,7 +22867,7 @@ ${formatAuthRequiredMessage("init", retryError, source)}`);
21653
22867
  if (!options.jsonOutput && !runtime.quiet) {
21654
22868
  O2.step("Step 2/4 \xB7 Plugin identity");
21655
22869
  }
21656
- const defaultPluginName = options.name ? toKebabCase3(options.name) : derivePluginName(introspection, source);
22870
+ const defaultPluginName = options.name ? toKebabCase3(options.name) : installedMcpSource?.serverName ? toKebabCase3(installedMcpSource.serverName) : derivePluginName(introspection, source);
21657
22871
  const pluginName = toKebabCase3(
21658
22872
  options.name ?? (interactive ? await clackText("Plugin name", defaultPluginName) : defaultPluginName)
21659
22873
  );
@@ -21687,6 +22901,7 @@ ${formatAuthRequiredMessage("init", retryError, source)}`);
21687
22901
  source,
21688
22902
  runtimeAuthMode,
21689
22903
  introspection,
22904
+ serverName: installedMcpSource?.serverName,
21690
22905
  displayName,
21691
22906
  description: sourcedContextPack?.docsContext?.shortDescription,
21692
22907
  websiteUrl,
@@ -21708,7 +22923,7 @@ ${formatAuthRequiredMessage("init", retryError, source)}`);
21708
22923
  if (!runtime.dryRun) {
21709
22924
  await applyMcpScaffoldPlan(process.cwd(), plan);
21710
22925
  for (const file of contextArtifactFiles) {
21711
- await writeTextFile(resolve23(process.cwd(), file.relativePath), file.content);
22926
+ await writeTextFile(resolve24(process.cwd(), file.relativePath), file.content);
21712
22927
  }
21713
22928
  }
21714
22929
  const lintResult = runtime.dryRun ? { errors: 0, warnings: 0, issues: [] } : await lintProject(process.cwd());
@@ -21778,6 +22993,11 @@ ${formatAuthRequiredMessage("init", retryError, source)}`);
21778
22993
  O2.info(n);
21779
22994
  }
21780
22995
  }
22996
+ if (installedMcpSource && installedMcpSource.warnings.length > 0) {
22997
+ for (const warning of installedMcpSource.warnings) {
22998
+ O2.warn(`Installed MCP import: ${warning}`);
22999
+ }
23000
+ }
21781
23001
  if (summary.quality.issues.length > 0) {
21782
23002
  for (const line of formatMcpQualityLines(summary.quality)) {
21783
23003
  O2.info(line);
@@ -21871,7 +23091,7 @@ async function runSync() {
21871
23091
  async function runDoctor() {
21872
23092
  const consumerMode = readFlag(args, "--consumer");
21873
23093
  const doctorPath = args.slice(1).find((value) => !value.startsWith("-"));
21874
- const rootDir = doctorPath ? resolve23(process.cwd(), doctorPath) : process.cwd();
23094
+ const rootDir = doctorPath ? resolve24(process.cwd(), doctorPath) : process.cwd();
21875
23095
  const report = consumerMode ? await doctorConsumer(rootDir) : await doctorProject(rootDir);
21876
23096
  if (runtime.jsonOutput) {
21877
23097
  printJson(report);
@@ -22056,6 +23276,9 @@ async function runAutopilot() {
22056
23276
  const attach = readOption2(args, "--attach");
22057
23277
  const reviewRequested = args.includes("--review");
22058
23278
  const verify = !args.includes("--no-verify");
23279
+ const installRequested = args.includes("--install");
23280
+ const behavioralRequested = args.includes("--behavioral");
23281
+ const behavioralPrompt = readOption2(args, "--behavioral-prompt");
22059
23282
  const verboseRunner = args.includes("--verbose-runner");
22060
23283
  const interactive = !runtime.jsonOutput && runtime.isInteractive && !initOptions.assumeDefaults;
22061
23284
  let authEnv = initOptions.authEnv;
@@ -22064,17 +23287,23 @@ async function runAutopilot() {
22064
23287
  let authTemplate = initOptions.authTemplate;
22065
23288
  let runtimeAuthMode = resolveRuntimeAuthMode(initOptions.runtimeAuth);
22066
23289
  if (!initOptions.source && !interactive) {
22067
- console.error(`Usage: pluxx autopilot --from-mcp <source> --runner <${AGENT_RUNNERS.join("|")}> [--mode <${AUTOPILOT_MODES.join("|")}>] [--name NAME] [--display-name NAME] [--author NAME] [--targets <platforms>] [--grouping workflow|tool] [--hooks none|safe] [--approve-mcp-tools] [--auth-env ENV] [--auth-type bearer|header|platform] [--auth-header NAME] [--auth-template TEMPLATE] [--runtime-auth inline|platform] [--oauth-wrapper] [--website URL] [--docs URL] [--ingest-provider <${AGENT_INGEST_PROVIDERS.join("|")}>] [--context <files...>] [--review] [--no-verify] [--verbose-runner] [--json] [--dry-run] [--quiet]`);
23290
+ console.error(`Usage: pluxx autopilot --from-mcp <source> --runner <${AGENT_RUNNERS.join("|")}> [--mode <${AUTOPILOT_MODES.join("|")}>] [--name NAME] [--display-name NAME] [--author NAME] [--targets <platforms>] [--grouping workflow|tool] [--hooks none|safe] [--approve-mcp-tools] [--auth-env ENV] [--auth-type bearer|header|platform] [--auth-header NAME] [--auth-template TEMPLATE] [--runtime-auth inline|platform] [--oauth-wrapper] [--website URL] [--docs URL] [--ingest-provider <${AGENT_INGEST_PROVIDERS.join("|")}>] [--context <files...>] [--review] [--install] [--install-target <platform>] [--trust] [--behavioral] [--behavioral-prompt TEXT] [--no-verify] [--verbose-runner] [--json] [--dry-run] [--quiet]`);
22068
23291
  process.exit(1);
22069
23292
  }
22070
23293
  if ((!runnerRaw || !AGENT_RUNNERS.includes(runnerRaw)) && !interactive) {
22071
- console.error(`Usage: pluxx autopilot --from-mcp <source> --runner <${AGENT_RUNNERS.join("|")}> [--mode <${AUTOPILOT_MODES.join("|")}>] [--name NAME] [--display-name NAME] [--author NAME] [--targets <platforms>] [--grouping workflow|tool] [--hooks none|safe] [--approve-mcp-tools] [--auth-env ENV] [--auth-type bearer|header|platform] [--auth-header NAME] [--auth-template TEMPLATE] [--runtime-auth inline|platform] [--oauth-wrapper] [--website URL] [--docs URL] [--ingest-provider <${AGENT_INGEST_PROVIDERS.join("|")}>] [--context <files...>] [--review] [--no-verify] [--verbose-runner] [--json] [--dry-run] [--quiet]`);
23294
+ console.error(`Usage: pluxx autopilot --from-mcp <source> --runner <${AGENT_RUNNERS.join("|")}> [--mode <${AUTOPILOT_MODES.join("|")}>] [--name NAME] [--display-name NAME] [--author NAME] [--targets <platforms>] [--grouping workflow|tool] [--hooks none|safe] [--approve-mcp-tools] [--auth-env ENV] [--auth-type bearer|header|platform] [--auth-header NAME] [--auth-template TEMPLATE] [--runtime-auth inline|platform] [--oauth-wrapper] [--website URL] [--docs URL] [--ingest-provider <${AGENT_INGEST_PROVIDERS.join("|")}>] [--context <files...>] [--review] [--install] [--install-target <platform>] [--trust] [--behavioral] [--behavioral-prompt TEXT] [--no-verify] [--verbose-runner] [--json] [--dry-run] [--quiet]`);
22072
23295
  process.exit(1);
22073
23296
  }
22074
23297
  if (modeRaw && !AUTOPILOT_MODES.includes(modeRaw)) {
22075
23298
  console.error(`Autopilot mode must be one of: ${AUTOPILOT_MODES.join(", ")}`);
22076
23299
  process.exit(1);
22077
23300
  }
23301
+ if (installRequested && !verify) {
23302
+ throw new Error("--install requires verification so autopilot can build outputs before installing. Remove --no-verify or omit --install.");
23303
+ }
23304
+ if (behavioralRequested && !installRequested) {
23305
+ throw new Error("--behavioral requires --install so the selected host CLI can see the installed plugin bundle.");
23306
+ }
22078
23307
  let tempDir;
22079
23308
  try {
22080
23309
  if (!runtime.jsonOutput && !runtime.quiet && interactive) {
@@ -22242,6 +23471,7 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22242
23471
  const authorName = initOptions.author ?? (interactive ? await clackText("Author name", defaultAuthorName) : defaultAuthorName);
22243
23472
  const targetsRaw = initOptions.targets ?? (interactive ? await clackText("Platforms (comma-separated)", DEFAULT_INIT_TARGETS.join(",")) : DEFAULT_INIT_TARGETS.join(","));
22244
23473
  const targets = parseTargetPlatforms(targetsRaw);
23474
+ const installTargets = installRequested ? parseInstallTargetFlag(args, targets) : [];
22245
23475
  const grouping = initOptions.grouping ? parseChoiceOption(initOptions.grouping, MCP_SKILL_GROUPINGS, "Skill grouping") : interactive ? await clackSelect("Skill grouping", [
22246
23476
  { value: "workflow", label: "workflow", hint: "Group related tools into workflow skills" },
22247
23477
  { value: "tool", label: "tool", hint: "One skill per tool" }
@@ -22271,7 +23501,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22271
23501
  taxonomy: passDecisions.taxonomy,
22272
23502
  instructions: passDecisions.instructions,
22273
23503
  review: passDecisions.review,
22274
- verify
23504
+ verify,
23505
+ install: installRequested,
23506
+ behavioral: behavioralRequested
22275
23507
  });
22276
23508
  const workspaceRoot = runtime.dryRun ? await mkdtemp3(`${tmpdir5()}/pluxx-autopilot-`) : process.cwd();
22277
23509
  tempDir = runtime.dryRun ? workspaceRoot : void 0;
@@ -22357,6 +23589,12 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22357
23589
  quality,
22358
23590
  review: passDecisions.review.enabled,
22359
23591
  verify,
23592
+ install: installRequested ? {
23593
+ enabled: true,
23594
+ platforms: installTargets,
23595
+ notes: getInstallFollowupNotes(installTargets),
23596
+ installTargets: []
23597
+ } : void 0,
22360
23598
  steps: totalSteps,
22361
23599
  init: {
22362
23600
  createdFiles: initCreatedFiles,
@@ -22404,7 +23642,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22404
23642
  taxonomy: passDecisions.taxonomy,
22405
23643
  instructions: passDecisions.instructions,
22406
23644
  review: passDecisions.review,
22407
- verify
23645
+ verify,
23646
+ install: installRequested,
23647
+ behavioral: behavioralRequested
22408
23648
  })}`);
22409
23649
  console.log(` Quality: ${quality.warnings} warning(s), ${quality.infos} info message(s)`);
22410
23650
  console.log(` Scaffold create/update: ${[...initCreatedFiles, ...initUpdatedFiles].join(", ") || "none"}`);
@@ -22425,6 +23665,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22425
23665
  } else {
22426
23666
  console.log(" Verification: skipped (--no-verify)");
22427
23667
  }
23668
+ if (installRequested) {
23669
+ console.log(` Install: ${installTargets.join(", ")} after verification`);
23670
+ }
22428
23671
  }
22429
23672
  return;
22430
23673
  }
@@ -22504,9 +23747,13 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22504
23747
  });
22505
23748
  return result;
22506
23749
  })() : void 0;
22507
- const ok = (taxonomyResult?.ok ?? true) && (instructionsResult?.ok ?? true) && (reviewResult?.ok ?? true) && (verification?.ok ?? true);
22508
- const failureStage = taxonomyResult && taxonomyResult.runnerExitCode !== 0 ? "runner" : instructionsResult && instructionsResult.runnerExitCode !== 0 ? "runner" : reviewResult && reviewResult.runnerExitCode !== 0 ? "runner" : verification && !verification.ok ? "verification" : void 0;
22509
- const failureMessage = failureStage === "runner" ? "A headless runner command failed. Re-run with --verbose-runner to stream full runner output." : failureStage === "verification" ? "Verification failed after scaffold/refinement. Run `pluxx test` for details." : void 0;
23750
+ const preInstallOk = (taxonomyResult?.ok ?? true) && (instructionsResult?.ok ?? true) && (reviewResult?.ok ?? true) && (verification?.ok ?? true);
23751
+ const installedConfig = installRequested && preInstallOk ? await loadConfig() : void 0;
23752
+ const install = installedConfig ? await maybeInstallBuiltOutputs(installedConfig, installTargets, { verifyConsumers: true }) : void 0;
23753
+ const behavioralResult = install && install.verification?.ok && behavioralRequested ? await runBehavioralSuite(process.cwd(), installedConfig, installTargets, { promptOverride: behavioralPrompt }) : void 0;
23754
+ const ok = preInstallOk && (install?.verification?.ok ?? true) && (behavioralResult?.ok ?? true);
23755
+ const failureStage = taxonomyResult && taxonomyResult.runnerExitCode !== 0 ? "runner" : instructionsResult && instructionsResult.runnerExitCode !== 0 ? "runner" : reviewResult && reviewResult.runnerExitCode !== 0 ? "runner" : verification && !verification.ok ? "verification" : install?.verification && !install.verification.ok ? "verification" : behavioralResult && !behavioralResult.ok ? "verification" : void 0;
23756
+ const failureMessage = failureStage === "runner" ? "A headless runner command failed. Re-run with --verbose-runner to stream full runner output." : failureStage === "verification" ? "Verification, install, or behavioral smoke failed after scaffold/refinement. Run `pluxx test --install` for details." : void 0;
22510
23757
  const summary = {
22511
23758
  ok,
22512
23759
  pluginName,
@@ -22568,6 +23815,8 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22568
23815
  },
22569
23816
  verification,
22570
23817
  verificationDurationMs,
23818
+ install,
23819
+ behavioral: behavioralResult,
22571
23820
  failureStage,
22572
23821
  failureMessage
22573
23822
  };
@@ -22583,7 +23832,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22583
23832
  taxonomy: passDecisions.taxonomy,
22584
23833
  instructions: passDecisions.instructions,
22585
23834
  review: passDecisions.review,
22586
- verify
23835
+ verify,
23836
+ install: installRequested,
23837
+ behavioral: behavioralRequested
22587
23838
  })}`);
22588
23839
  console.log(` Quality: ${quality.warnings} warning(s), ${quality.infos} info message(s)`);
22589
23840
  if (!verboseRunner) {
@@ -22606,6 +23857,23 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22606
23857
  } else {
22607
23858
  console.log(" Verification: skipped (--no-verify)");
22608
23859
  }
23860
+ if (install) {
23861
+ console.log(` Install: ${install.verification?.ok === false ? "failed" : "passed"} for ${install.platforms.join(", ")}`);
23862
+ for (const target of install.installTargets) {
23863
+ console.log(` ${target.platform}: ${target.consumerPath}`);
23864
+ }
23865
+ if (install.verification) {
23866
+ console.log(` Verify-install: ${install.verification.ok ? "passed" : "failed"}`);
23867
+ }
23868
+ for (const note of install.notes) {
23869
+ console.log(` ${note}`);
23870
+ }
23871
+ } else if (installRequested) {
23872
+ console.log(` Install: skipped because verification did not produce a passing build`);
23873
+ }
23874
+ if (behavioralResult) {
23875
+ console.log(` Behavioral: ${behavioralResult.ok ? "passed" : "failed"}`);
23876
+ }
22609
23877
  if (failureStage && failureMessage) {
22610
23878
  console.log(` Failure stage: ${failureStage}`);
22611
23879
  console.log(` Failure detail: ${failureMessage}`);
@@ -22614,6 +23882,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22614
23882
  console.log(" 1. Review INSTRUCTIONS.md and skills/");
22615
23883
  console.log(` 2. Run: pluxx build${mode === "quick" && !passDecisions.taxonomy.enabled && !passDecisions.instructions.enabled && !passDecisions.review.enabled ? " (agent refinement was skipped; only do this if the deterministic scaffold already looks good)" : ""}`);
22616
23884
  console.log(` 3. Run: pluxx install${scaffoldPlan.generatedHookMode === "safe" ? " --trust" : ""} --target ${targets[0]}`);
23885
+ if (!installRequested) {
23886
+ console.log(` Or rerun onboarding: pluxx autopilot --from-mcp "${rawSource}" --runner ${runner} --mode ${mode} --install --install-target ${targets[0]}${scaffoldPlan.generatedHookMode === "safe" ? " --trust" : ""}`);
23887
+ }
22617
23888
  }
22618
23889
  if (!ok) {
22619
23890
  process.exit(1);
@@ -22828,7 +24099,7 @@ async function runVerifyInstall() {
22828
24099
  const targets = parseTargetFlagValues(args);
22829
24100
  const config = await loadConfig();
22830
24101
  if (runtime.dryRun) {
22831
- const distDir = resolve23(process.cwd(), config.outDir);
24102
+ const distDir = resolve24(process.cwd(), config.outDir);
22832
24103
  const plan = planInstallPlugin(distDir, config.name, targets ?? config.targets);
22833
24104
  const summary = {
22834
24105
  dryRun: true,
@@ -22915,9 +24186,10 @@ Usage:
22915
24186
  pluxx agent prepare Generate agent context + boundary files for host agents
22916
24187
  pluxx agent prompt <kind> Generate a prompt pack (taxonomy, instructions, review)
22917
24188
  pluxx agent run <kind> --runner <id> Execute a prompt pack via Claude, Cursor, Codex, or OpenCode headlessly
24189
+ pluxx discover-mcp [--host <hosts...>] List installed MCP servers from local host configs
22918
24190
  pluxx mcp proxy ... Run a local MCP proxy with optional record/replay tapes
22919
24191
  pluxx autopilot --from-mcp ... Run import + agent refinement + verification in one command
22920
- pluxx init [name] [--from-mcp <source>] Create a new pluxx.config.ts
24192
+ pluxx init [name] [--from-mcp <source>|--from-installed-mcp <selector>] Create a new pluxx.config.ts
22921
24193
  pluxx sync [--from-mcp <source>] Refresh MCP-derived scaffold files
22922
24194
  pluxx migrate <path> Import an existing plugin into pluxx
22923
24195
  pluxx test [--target <platforms...>] [--install] [--behavioral] Run config, lint, eval, build, and smoke checks
@@ -22937,6 +24209,10 @@ Common flags:
22937
24209
  --mode quick|standard|thorough Control how much agent refinement autopilot performs
22938
24210
  --approve-mcp-tools Preapprove all tools from the imported MCP in canonical permissions
22939
24211
  --ingest-provider auto|local|firecrawl Choose the docs/website ingestion backend for agent prepare/autopilot
24212
+ --install Install autopilot's verified build into one selected host
24213
+ --install-target <platform> Host to install after autopilot verification; defaults to the first target
24214
+ --trust Trust local hook commands during install/test flows
24215
+ --behavioral Run installed headless example-query smoke checks
22940
24216
 
22941
24217
  Targets:
22942
24218
  claude-code, cursor, codex, opencode, github-copilot, openhands,
@@ -22952,6 +24228,9 @@ Examples:
22952
24228
  pluxx init my-plugin Scaffold a new plugin config
22953
24229
  pluxx init --from-mcp https://example.com/mcp Scaffold from a remote MCP server
22954
24230
  pluxx init --from-mcp "npx -y -p @acme/mcp acme-mcp" Scaffold from a local MCP command
24231
+ pluxx discover-mcp List installed MCP servers in Claude, Cursor, Codex, and OpenCode config
24232
+ pluxx discover-mcp --host codex opencode --json
24233
+ pluxx init --from-installed-mcp codex:acme --yes Scaffold from an already installed MCP config
22955
24234
  pluxx init --from-mcp https://example.com/mcp --yes --name acme --display-name "Acme" --author "Acme" --targets claude-code,codex --grouping workflow --hooks safe --json
22956
24235
  pluxx init --from-mcp https://example.com/mcp --yes --auth-env API_KEY --auth-type header --auth-header X-API-Key --auth-template "\${value}"
22957
24236
  pluxx init --from-mcp https://example.com/mcp --yes --auth-type platform --runtime-auth platform
@@ -22976,6 +24255,8 @@ Examples:
22976
24255
  --attach is only supported for the opencode runner
22977
24256
  pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode quick --yes
22978
24257
  pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode standard --yes --name acme --display-name "Acme"
24258
+ pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode standard --install --install-target codex --trust
24259
+ pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode standard --auth-env API_KEY --auth-type bearer --install --install-target codex --trust
22979
24260
  pluxx autopilot --from-mcp https://example.com/mcp --runner codex --yes --approve-mcp-tools
22980
24261
  pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode thorough --yes --verbose-runner
22981
24262
  pluxx autopilot --from-mcp https://mcp.linear.app/mcp --runner codex --yes --oauth-wrapper
@@ -22992,6 +24273,39 @@ Examples:
22992
24273
  pluxx verify-install --target codex Verify the installed Codex bundle in its native local path
22993
24274
  pluxx install --dry-run Preview local install paths and trust implications
22994
24275
  pluxx install --trust Install without hook trust confirmation
24276
+ pluxx publish --dry-run Preview npm/GitHub release publish checks
24277
+ pluxx publish --github-release --version 1.0.0 Create release installers and a GitHub release
24278
+ pluxx publish --npm --tag next Publish the npm package under a non-latest dist-tag
24279
+ `);
24280
+ }
24281
+ function printPublishHelp() {
24282
+ console.log(`
24283
+ pluxx publish \u2014 package and release a built plugin or CLI
24284
+
24285
+ Usage:
24286
+ pluxx publish [--npm] [--github-release] [--allow-dirty] [--dry-run] [--json] [--tag latest] [--version x.y.z]
24287
+
24288
+ Behavior:
24289
+ - loads the current pluxx.config.* project
24290
+ - plans npm publish and/or GitHub release work
24291
+ - runs clean-working-tree checks unless --allow-dirty is passed
24292
+ - writes release assets such as install scripts and release-manifest.json when applicable
24293
+
24294
+ Flags:
24295
+ --npm Publish to npm using the configured package metadata
24296
+ --github-release Create or update GitHub release assets for the requested version
24297
+ --version x.y.z Override the release version for the publish plan
24298
+ --tag <name> Set the npm dist-tag (default: latest)
24299
+ --allow-dirty Skip the clean-working-tree check
24300
+ --dry-run Show the publish plan without writing or uploading
24301
+ --json Print the publish plan or result as JSON
24302
+ --help, -h Show this command help
24303
+
24304
+ Examples:
24305
+ pluxx publish --dry-run
24306
+ pluxx publish --github-release --version 1.0.0
24307
+ pluxx publish --npm --version 0.1.7
24308
+ pluxx publish --npm --github-release --version 1.0.0
22995
24309
  `);
22996
24310
  }
22997
24311
  if (import.meta.main) {