@orchid-labs/pluxx 0.1.7 → 0.1.9

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 readFileSync17 } 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,53 @@ 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 shellSingleQuote = (input) => `'${String(input ?? \"\").replace(/'/g, `'\"'\"'`)}'`",
5204
+ "",
5205
+ "const filepath = process.argv[1]",
5206
+ "if (!filepath) process.exit(0)",
5207
+ 'const payload = JSON.parse(readFileSync(filepath, "utf8"))',
5208
+ 'const env = payload && typeof payload === "object" && payload.env && typeof payload.env === "object"',
5209
+ " ? payload.env",
5210
+ " : {}",
5211
+ "",
5212
+ "for (const [key, value] of Object.entries(env)) {",
5213
+ " if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue",
5214
+ " process.stdout.write(`export ${key}=${shellSingleQuote(value)}\\0`)",
5215
+ "}"
5216
+ ].join("\n");
5217
+ return [
5218
+ "#!/usr/bin/env bash",
5219
+ "set -euo pipefail",
5220
+ "",
5221
+ 'PLUXX_PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"',
5222
+ 'PLUXX_USER_CONFIG_PATH="$PLUXX_PLUGIN_ROOT/.pluxx-user.json"',
5223
+ "",
5224
+ 'if [ -f "$PLUXX_USER_CONFIG_PATH" ]; then',
5225
+ " while IFS= read -r -d '' pluxx_export; do",
5226
+ ' if [ -n "$pluxx_export" ]; then',
5227
+ ' eval "$pluxx_export"',
5228
+ ' if [ -n "${CLAUDE_ENV_FILE:-}" ]; then',
5229
+ ` printf '%s\\n' "$pluxx_export" >> "$CLAUDE_ENV_FILE"`,
5230
+ " fi",
5231
+ " fi",
5232
+ " done < <(",
5233
+ ` node --input-type=module -e ${shellSingleQuote(exportLoader)} "$PLUXX_USER_CONFIG_PATH"`,
5234
+ " )",
5235
+ "fi",
5236
+ "",
5237
+ `PLUXX_HOOK_COMMAND=${serializedCommand}`,
5238
+ 'eval "$PLUXX_HOOK_COMMAND"',
5239
+ ""
5240
+ ].join("\n");
5241
+ }
5195
5242
  async function writeManifest(config, rootDir, options, writeJson) {
5196
5243
  const manifest = {
5197
5244
  name: config.name,
@@ -5269,7 +5316,9 @@ async function writeHooks(config, platform, options, writeJson, writeFile3) {
5269
5316
  const hooks = {};
5270
5317
  const mapEventName = options.mapEventName ?? defaultMapEventName;
5271
5318
  const usesPlatformManagedAuth = platform === "claude-code" && config.platforms?.["claude-code"]?.mcpAuth === "platform";
5319
+ const shouldWrapClaudeHookCommands = platform === "claude-code";
5272
5320
  const permissionScript = buildGeneratedPermissionHookScript(config.permissions);
5321
+ let generatedClaudeHookCommandCount = 0;
5273
5322
  if (permissionScript) {
5274
5323
  await writeFile3("hooks/pluxx-permissions.mjs", permissionScript);
5275
5324
  hooks.PreToolUse = [{
@@ -5299,12 +5348,21 @@ async function writeHooks(config, platform, options, writeJson, writeFile3) {
5299
5348
  if (commandEntries.length === 0) continue;
5300
5349
  hooks[mappedEvent] = [
5301
5350
  ...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
- }]
5351
+ ...await Promise.all(commandEntries.map(async (entry) => {
5352
+ const command2 = entry.command.replace("${PLUGIN_ROOT}", `\${${options.pluginRootVar}}`);
5353
+ const finalCommand = shouldWrapClaudeHookCommands ? await (async () => {
5354
+ generatedClaudeHookCommandCount += 1;
5355
+ const relativePath = `hooks/pluxx-hook-command-${generatedClaudeHookCommandCount}.sh`;
5356
+ await writeFile3(relativePath, buildClaudeHookCommandWrapperScript(command2));
5357
+ return `bash "\${${options.pluginRootVar}}/${relativePath}"`;
5358
+ })() : command2;
5359
+ return {
5360
+ ...entry.matcher !== void 0 ? { matcher: entry.matcher } : {},
5361
+ hooks: [{
5362
+ type: "command",
5363
+ command: finalCommand
5364
+ }]
5365
+ };
5308
5366
  }))
5309
5367
  ];
5310
5368
  }
@@ -9317,6 +9375,16 @@ import { basename as basename5, dirname as dirname4, relative as relative7, reso
9317
9375
 
9318
9376
  // src/user-config.ts
9319
9377
  var ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
9378
+ var PLACEHOLDER_SECRET_PATTERNS = [
9379
+ /\bdummy\b/i,
9380
+ /\bplaceholder\b/i,
9381
+ /\bexample\b/i,
9382
+ /\bchangeme\b/i,
9383
+ /\breplace[_ -]?me\b/i,
9384
+ /\byour[_ -]?(api[_ -]?)?key\b/i,
9385
+ /\bapi[_ -]?key[_ -]?here\b/i,
9386
+ /\btoken[_ -]?here\b/i
9387
+ ];
9320
9388
  function normalizeUserConfigKey(value) {
9321
9389
  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, "");
9322
9390
  }
@@ -9331,6 +9399,12 @@ function extractEnvReference(value) {
9331
9399
  const match = value.match(/^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/);
9332
9400
  return match?.[1];
9333
9401
  }
9402
+ function isPlaceholderSecretValue(value) {
9403
+ if (typeof value !== "string") return false;
9404
+ const normalized = value.trim();
9405
+ if (normalized === "") return false;
9406
+ return PLACEHOLDER_SECRET_PATTERNS.some((pattern) => pattern.test(normalized));
9407
+ }
9334
9408
  function isRuntimePlatformManaged(config, target, server) {
9335
9409
  if (server.transport === "stdio") return false;
9336
9410
  if (server.auth?.type === "platform") {
@@ -9507,6 +9581,42 @@ var WORKFLOW_SKILL_DEFINITIONS = [
9507
9581
  title: "General Research",
9508
9582
  description: "Handle broad search and query workflows when there is not a more specific product surface match.",
9509
9583
  match: ["search", "query", "lookup", "look up", "discover", "find"]
9584
+ },
9585
+ {
9586
+ key: "web-research",
9587
+ title: "Web Research",
9588
+ description: "Search the web, fetch pages, and synthesize public source-backed research.",
9589
+ match: ["web", "website", "url", "page", "fetch", "crawl", "scrape", "search", "result", "source"]
9590
+ },
9591
+ {
9592
+ key: "source-review",
9593
+ title: "Source Review",
9594
+ description: "Audit sources, citations, evidence quality, and duplicate or weak research results.",
9595
+ match: ["source", "citation", "evidence", "audit", "review", "quality", "rank", "dedupe", "duplicate"]
9596
+ },
9597
+ {
9598
+ key: "content-extraction",
9599
+ title: "Content Extraction",
9600
+ description: "Extract structured content, entities, and summaries from pages, documents, or search results.",
9601
+ match: ["extract", "parse", "content", "summary", "summarize", "document", "html", "markdown"]
9602
+ },
9603
+ {
9604
+ key: "knowledge-search",
9605
+ title: "Knowledge Search",
9606
+ description: "Search docs, knowledge bases, papers, repositories, and internal reference material.",
9607
+ match: ["docs", "documentation", "knowledge", "paper", "papers", "repo", "repository", "api", "reference"]
9608
+ },
9609
+ {
9610
+ key: "news-monitoring",
9611
+ title: "News Monitoring",
9612
+ description: "Find recent news, launches, announcements, and time-bounded developments.",
9613
+ match: ["news", "recent", "latest", "launch", "announcement", "announced", "date", "published"]
9614
+ },
9615
+ {
9616
+ key: "code-research",
9617
+ title: "Code Research",
9618
+ description: "Find implementation docs, code examples, API usage, migration notes, and troubleshooting context.",
9619
+ match: ["code", "sdk", "api", "github", "example", "migration", "error", "troubleshoot", "implementation"]
9510
9620
  }
9511
9621
  ];
9512
9622
  var WORKFLOW_MATCH_MIN_SCORE = 2;
@@ -9589,6 +9699,8 @@ async function planMcpScaffold(options) {
9589
9699
  options.persistedSkills,
9590
9700
  options.toolRenames
9591
9701
  );
9702
+ const skillGrouping = options.skillGrouping ?? "workflow";
9703
+ const plannedCommands = planCommandScaffolds(plannedSkills, skillGrouping);
9592
9704
  const description = options.description ?? deriveScaffoldDescription({
9593
9705
  displayName,
9594
9706
  introspection: options.introspection,
@@ -9641,7 +9753,7 @@ async function planMcpScaffold(options) {
9641
9753
  passthroughPaths,
9642
9754
  runtimeAuthMode,
9643
9755
  permissions,
9644
- commandsPath: "./commands/"
9756
+ commandsPath: plannedCommands.length > 0 ? "./commands/" : void 0
9645
9757
  })
9646
9758
  );
9647
9759
  await addPlannedFile(
@@ -9671,8 +9783,6 @@ async function planMcpScaffold(options) {
9671
9783
  for (const skill of plannedSkills) {
9672
9784
  const relativeSkillPath = `skills/${skill.dirName}`;
9673
9785
  const skillPath = resolve10(skillRoot, skill.dirName, "SKILL.md");
9674
- const relativeCommandPath = `commands/${skill.dirName}.md`;
9675
- const commandPath = resolve10(commandsRoot, `${skill.dirName}.md`);
9676
9786
  await addPlannedFile(
9677
9787
  `${relativeSkillPath}/SKILL.md`,
9678
9788
  wrapManagedMarkdown(
@@ -9686,6 +9796,10 @@ async function planMcpScaffold(options) {
9686
9796
  );
9687
9797
  skillDirectories.push(relativeSkillPath);
9688
9798
  generatedFiles.push(`${relativeSkillPath}/SKILL.md`);
9799
+ }
9800
+ for (const skill of plannedCommands) {
9801
+ const relativeCommandPath = `commands/${skill.dirName}.md`;
9802
+ const commandPath = resolve10(commandsRoot, `${skill.dirName}.md`);
9689
9803
  await addPlannedFile(
9690
9804
  relativeCommandPath,
9691
9805
  buildCommandContent(
@@ -9705,7 +9819,7 @@ async function planMcpScaffold(options) {
9705
9819
  introspection: options.introspection,
9706
9820
  pluginName,
9707
9821
  displayName,
9708
- skillGrouping: options.skillGrouping ?? "workflow",
9822
+ skillGrouping,
9709
9823
  requestedHookMode: options.hookMode ?? "none",
9710
9824
  generatedHookMode: generatedHooks.mode,
9711
9825
  generatedHookEvents: Object.keys(generatedHooks.hookEntries ?? {}),
@@ -9932,6 +10046,19 @@ function buildCommandContent(skill, existingContent) {
9932
10046
  }
9933
10047
  );
9934
10048
  }
10049
+ function hasStrongCommandShape(skill, grouping) {
10050
+ if (grouping === "tool") return true;
10051
+ if (skill.tools.length > 1) return true;
10052
+ if (skill.prompts.length > 0) return true;
10053
+ if (skill.resources.length > 0 || skill.resourceTemplates.length > 0) return true;
10054
+ const requiredFields = skill.tools.flatMap((tool) => getTopLevelSchemaFields(tool.inputSchema).filter((field) => field.required));
10055
+ return requiredFields.length >= 2;
10056
+ }
10057
+ function planCommandScaffolds(plannedSkills, grouping) {
10058
+ const commands = plannedSkills.filter((skill) => hasStrongCommandShape(skill, grouping));
10059
+ if (commands.length > 0) return commands;
10060
+ return plannedSkills.slice(0, 1);
10061
+ }
9935
10062
  function formatArgumentHintFrontmatter(value) {
9936
10063
  const trimmed = value.trim();
9937
10064
  if (!trimmed) return '""';
@@ -11272,6 +11399,11 @@ function evaluateSkills(rootDir, metadata, checks) {
11272
11399
  function hasManagedCommands(metadata) {
11273
11400
  return metadata.managedFiles.some((file) => file.startsWith("commands/"));
11274
11401
  }
11402
+ function managedCommandSkillNames(metadata) {
11403
+ return new Set(
11404
+ metadata.managedFiles.filter((file) => file.startsWith("commands/") && file.endsWith(".md")).map((file) => file.replace(/^commands\//, "").replace(/\.md$/, ""))
11405
+ );
11406
+ }
11275
11407
  function evaluateCommands(rootDir, metadata, checks) {
11276
11408
  if (!hasManagedCommands(metadata)) {
11277
11409
  addCheck(checks, {
@@ -11284,7 +11416,8 @@ function evaluateCommands(rootDir, metadata, checks) {
11284
11416
  return;
11285
11417
  }
11286
11418
  const failures = [];
11287
- for (const skill of metadata.skills) {
11419
+ const commandSkillNames = managedCommandSkillNames(metadata);
11420
+ for (const skill of metadata.skills.filter((entry) => commandSkillNames.has(entry.dirName))) {
11288
11421
  const relativePath = `commands/${skill.dirName}.md`;
11289
11422
  const filePath = resolve11(rootDir, relativePath);
11290
11423
  if (!existsSync19(filePath)) {
@@ -11332,10 +11465,36 @@ function evaluateCommands(rootDir, metadata, checks) {
11332
11465
  level: "success",
11333
11466
  code: "command-quality-contract",
11334
11467
  title: "Command scaffolds expose expected routing guidance and related surfaces",
11335
- detail: `Checked ${metadata.skills.length} generated command file(s) for argument hints, tool routing, and related discovery surfaces.`,
11468
+ detail: `Checked ${commandSkillNames.size} generated command file(s) for argument hints, tool routing, and related discovery surfaces.`,
11336
11469
  fix: "No action needed."
11337
11470
  });
11338
11471
  }
11472
+ function evaluateScaffoldArchitecture(metadata, checks) {
11473
+ const commandSkillNames = managedCommandSkillNames(metadata);
11474
+ if (metadata.tools.length === 0 || commandSkillNames.size === 0) return;
11475
+ const singletonSkills = metadata.skills.filter((skill) => (skill.toolNames?.length ?? 0) === 1);
11476
+ const commandBackedSingletons = singletonSkills.filter((skill) => commandSkillNames.has(skill.dirName));
11477
+ const commandPerTool = commandSkillNames.size >= metadata.tools.length && commandBackedSingletons.length >= Math.ceil(commandSkillNames.size * 0.8);
11478
+ if (commandPerTool) {
11479
+ addCheck(checks, {
11480
+ level: "warning",
11481
+ code: "scaffold-command-per-tool-shape",
11482
+ title: "Scaffold looks like command-per-tool output",
11483
+ detail: `This scaffold exposes ${commandSkillNames.size} command(s) for ${metadata.tools.length} MCP tool(s), with most commands wrapping a single tool.`,
11484
+ 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."
11485
+ });
11486
+ }
11487
+ const singletonRatio = singletonSkills.length / Math.max(metadata.skills.length, 1);
11488
+ if (metadata.skills.length >= 6 && singletonRatio >= 0.75) {
11489
+ addCheck(checks, {
11490
+ level: "warning",
11491
+ code: "scaffold-singleton-heavy-taxonomy",
11492
+ title: "Scaffold taxonomy is singleton-heavy",
11493
+ 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.`,
11494
+ fix: "Run `pluxx agent run taxonomy` or `pluxx autopilot --mode standard` to merge raw tools into product workflows before shipping."
11495
+ });
11496
+ }
11497
+ }
11339
11498
  function evaluateAgentContext(contextContent, metadata, checks) {
11340
11499
  const missing = [];
11341
11500
  if (((metadata.resources?.length ?? 0) > 0 || (metadata.resourceTemplates?.length ?? 0) > 0 || (metadata.prompts?.length ?? 0) > 0) && !contextContent.includes("## MCP Discovery Surfaces")) {
@@ -11475,6 +11634,7 @@ async function runEvalSuite(options = {}) {
11475
11634
  evaluateInstructions(rootDir, metadata, checks);
11476
11635
  evaluateSkills(rootDir, metadata, checks);
11477
11636
  evaluateCommands(rootDir, metadata, checks);
11637
+ evaluateScaffoldArchitecture(metadata, checks);
11478
11638
  }
11479
11639
  evaluateAgentContext(contextContent, metadata, checks);
11480
11640
  for (const kind of AGENT_PROMPT_KINDS) {
@@ -15249,6 +15409,10 @@ async function resolveInstallUserConfig(config, platforms = config.targets, opti
15249
15409
  const isTTY = options.isTTY ?? process.stdin.isTTY === true;
15250
15410
  for (const entry of planned) {
15251
15411
  if (entry.value !== void 0) {
15412
+ if (entry.field.type === "secret" && isPlaceholderSecretValue(entry.value)) {
15413
+ 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.";
15414
+ 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}`);
15415
+ }
15252
15416
  resolved.push({
15253
15417
  field: entry.field,
15254
15418
  value: entry.value,
@@ -15260,8 +15424,8 @@ async function resolveInstallUserConfig(config, platforms = config.targets, opti
15260
15424
  continue;
15261
15425
  }
15262
15426
  if (!isTTY) {
15263
- const hint = entry.envVar ? ` Export ${entry.envVar} or install interactively.` : " Re-run interactively to provide it.";
15264
- throw new Error(`Missing required userConfig "${entry.field.key}".${hint}`);
15427
+ const hint = entry.envVar ? ` Export it before installing: export ${entry.envVar}='your_real_key'. Then rerun pluxx install.` : " Re-run interactively to provide it.";
15428
+ throw new Error(`Missing required userConfig "${entry.field.key}". Installed plugins cannot prompt for this value later in every host UI.${hint}`);
15265
15429
  }
15266
15430
  const promptLabel = entry.field.title || entry.field.key;
15267
15431
  const envHint = entry.envVar ? ` [env: ${entry.envVar}]` : "";
@@ -15336,7 +15500,7 @@ async function ensureHookTrust(options) {
15336
15500
  const isTTY = options.isTTY ?? process.stdin.isTTY === true;
15337
15501
  if (!isTTY) {
15338
15502
  throw new Error(
15339
- `Refusing to install plugin with hooks in non-interactive mode. Re-run with --trust to continue.`
15503
+ `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.`
15340
15504
  );
15341
15505
  }
15342
15506
  const confirm = options.confirmPrompt ?? promptTrustConfirmation;
@@ -15535,7 +15699,7 @@ function getInstallFollowupNotes(platforms) {
15535
15699
  notes.push("Cursor note: if Cursor is already open, use Developer: Reload Window or restart Cursor to pick up the new install.");
15536
15700
  }
15537
15701
  if (platforms.includes("codex")) {
15538
- 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.");
15702
+ 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.");
15539
15703
  }
15540
15704
  if (platforms.includes("opencode")) {
15541
15705
  notes.push("OpenCode note: if OpenCode is already open, restart or reload it so the plugin is picked up.");
@@ -15755,11 +15919,159 @@ function getClaudeMarketplaceRoot(pluginName) {
15755
15919
  return resolve15(home, ".claude/plugins/data", getClaudeMarketplaceName(pluginName));
15756
15920
  }
15757
15921
  function resolveInstalledConsumerPath(target, pluginName) {
15758
- if (target.platform === "claude-code") {
15759
- return resolve15(getClaudeMarketplaceRoot(pluginName), "plugins", pluginName);
15922
+ if (target.platform === "claude-code" && pluginName !== "") {
15923
+ return target.pluginDir;
15760
15924
  }
15761
15925
  return target.pluginDir;
15762
15926
  }
15927
+ function manifestPathForPlatform(platform) {
15928
+ switch (platform) {
15929
+ case "claude-code":
15930
+ return ".claude-plugin/plugin.json";
15931
+ case "cursor":
15932
+ return ".cursor-plugin/plugin.json";
15933
+ case "codex":
15934
+ return ".codex-plugin/plugin.json";
15935
+ case "opencode":
15936
+ return "package.json";
15937
+ default:
15938
+ return void 0;
15939
+ }
15940
+ }
15941
+ function isRelativeBundlePath(value) {
15942
+ return value.startsWith("./") || value.startsWith("../") || value.startsWith(".\\") || value.startsWith("..\\");
15943
+ }
15944
+ function resolveBundleReference(rootDir, value) {
15945
+ if (isRelativeBundlePath(value)) {
15946
+ return resolve15(rootDir, value);
15947
+ }
15948
+ const pluginRootMatch = value.match(/^\$\{(?:CLAUDE_PLUGIN_ROOT|CURSOR_PLUGIN_ROOT|PLUGIN_ROOT)\}[\\/](.+)$/);
15949
+ if (pluginRootMatch) {
15950
+ return resolve15(rootDir, pluginRootMatch[1]);
15951
+ }
15952
+ return void 0;
15953
+ }
15954
+ function readBundleManifestReferences(manifest) {
15955
+ const references = [];
15956
+ for (const key of ["commands", "skills", "hooks", "mcpServers"]) {
15957
+ const value = manifest[key];
15958
+ if (typeof value === "string") {
15959
+ references.push(value);
15960
+ }
15961
+ }
15962
+ const agents = manifest.agents;
15963
+ if (typeof agents === "string") {
15964
+ references.push(agents);
15965
+ } else if (Array.isArray(agents)) {
15966
+ for (const entry of agents) {
15967
+ if (typeof entry === "string") {
15968
+ references.push(entry);
15969
+ }
15970
+ }
15971
+ }
15972
+ return references;
15973
+ }
15974
+ function collectHookCommandStrings(value, commands) {
15975
+ if (Array.isArray(value)) {
15976
+ for (const entry of value) {
15977
+ collectHookCommandStrings(entry, commands);
15978
+ }
15979
+ return;
15980
+ }
15981
+ if (!value || typeof value !== "object") return;
15982
+ for (const [key, child] of Object.entries(value)) {
15983
+ if (key === "command" && typeof child === "string") {
15984
+ commands.push(child);
15985
+ continue;
15986
+ }
15987
+ collectHookCommandStrings(child, commands);
15988
+ }
15989
+ }
15990
+ function extractBundleCommandTargets(command2) {
15991
+ const matches = command2.match(/\$\{(?:CLAUDE_PLUGIN_ROOT|CURSOR_PLUGIN_ROOT|PLUGIN_ROOT)\}[\\/][^\s"'`;$|&<>]+|\.\.?[\\/][^\s"'`;$|&<>]+/g);
15992
+ return matches ?? [];
15993
+ }
15994
+ function findInstalledBundleIntegrityIssues(rootDir, platform) {
15995
+ const manifestPath = manifestPathForPlatform(platform);
15996
+ if (!manifestPath) {
15997
+ return {
15998
+ missingManifestPaths: [],
15999
+ missingHookTargets: []
16000
+ };
16001
+ }
16002
+ const manifestFile = resolve15(rootDir, manifestPath);
16003
+ if (!existsSync23(manifestFile)) {
16004
+ return {
16005
+ manifestIssue: `missing plugin manifest at ${manifestPath}`,
16006
+ missingManifestPaths: [],
16007
+ missingHookTargets: []
16008
+ };
16009
+ }
16010
+ let manifest;
16011
+ try {
16012
+ manifest = JSON.parse(readFileSync10(manifestFile, "utf-8"));
16013
+ } catch (error) {
16014
+ return {
16015
+ manifestIssue: `plugin manifest at ${manifestPath} is not parseable: ${error instanceof Error ? error.message : String(error)}`,
16016
+ missingManifestPaths: [],
16017
+ missingHookTargets: []
16018
+ };
16019
+ }
16020
+ const missingManifestPaths = readBundleManifestReferences(manifest).filter((value) => {
16021
+ const resolved = resolveBundleReference(rootDir, value);
16022
+ return resolved !== void 0 && !existsSync23(resolved);
16023
+ }).sort();
16024
+ const hooksReference = typeof manifest.hooks === "string" ? manifest.hooks : void 0;
16025
+ if (!hooksReference) {
16026
+ return {
16027
+ missingManifestPaths,
16028
+ missingHookTargets: []
16029
+ };
16030
+ }
16031
+ const hooksPath = resolveBundleReference(rootDir, hooksReference);
16032
+ if (!hooksPath || !existsSync23(hooksPath)) {
16033
+ return {
16034
+ missingManifestPaths,
16035
+ missingHookTargets: []
16036
+ };
16037
+ }
16038
+ try {
16039
+ const hooks = JSON.parse(readFileSync10(hooksPath, "utf-8"));
16040
+ const commands = [];
16041
+ collectHookCommandStrings(hooks, commands);
16042
+ const missingHookTargets = [...new Set(
16043
+ commands.flatMap(extractBundleCommandTargets).filter((value) => {
16044
+ const resolved = resolveBundleReference(rootDir, value);
16045
+ return resolved !== void 0 && !existsSync23(resolved);
16046
+ })
16047
+ )].sort();
16048
+ return {
16049
+ missingManifestPaths,
16050
+ missingHookTargets
16051
+ };
16052
+ } catch {
16053
+ return {
16054
+ missingManifestPaths,
16055
+ missingHookTargets: []
16056
+ };
16057
+ }
16058
+ }
16059
+ function assertInstalledBundleIntegrity(rootDir, platform, label) {
16060
+ const issues = findInstalledBundleIntegrityIssues(rootDir, platform);
16061
+ const details = [];
16062
+ if (issues.manifestIssue) {
16063
+ details.push(issues.manifestIssue);
16064
+ }
16065
+ if (issues.missingManifestPaths.length > 0) {
16066
+ details.push(`manifest paths missing: ${issues.missingManifestPaths.join(", ")}`);
16067
+ }
16068
+ if (issues.missingHookTargets.length > 0) {
16069
+ details.push(`hook targets missing: ${issues.missingHookTargets.join(", ")}`);
16070
+ }
16071
+ if (details.length > 0) {
16072
+ throw new Error(`${label} is incomplete: ${details.join("; ")}`);
16073
+ }
16074
+ }
15763
16075
  function ensureClaudeMarketplace(pluginName, sourceDir, materialized) {
15764
16076
  const marketplaceName = getClaudeMarketplaceName(pluginName);
15765
16077
  const marketplaceRoot = getClaudeMarketplaceRoot(pluginName);
@@ -15770,12 +16082,11 @@ function ensureClaudeMarketplace(pluginName, sourceDir, materialized) {
15770
16082
  rmSync3(marketplaceRoot, { recursive: true, force: true });
15771
16083
  mkdirSync4(marketplaceManifestDir, { recursive: true });
15772
16084
  mkdirSync4(resolve15(marketplaceRoot, "plugins"), { recursive: true });
16085
+ cpSync3(sourceDir, marketplacePluginDir, { recursive: true });
15773
16086
  if (materialized && materialized.entries.length > 0) {
15774
- cpSync3(sourceDir, marketplacePluginDir, { recursive: true });
15775
16087
  materializeInstalledPlugin(marketplacePluginDir, "claude-code", materialized.config, materialized.entries);
15776
- } else {
15777
- symlinkSync(sourceDir, marketplacePluginDir);
15778
16088
  }
16089
+ assertInstalledBundleIntegrity(marketplacePluginDir, "claude-code", "Claude marketplace bundle");
15779
16090
  writeFileSync4(
15780
16091
  resolve15(marketplaceManifestDir, "marketplace.json"),
15781
16092
  JSON.stringify({
@@ -15825,6 +16136,7 @@ function installClaudePlugin(target, pluginName, runCommand, materialized) {
15825
16136
  if (install.status !== 0) {
15826
16137
  throw new Error(`Failed to install Claude plugin: ${install.stderr || install.stdout}`);
15827
16138
  }
16139
+ assertInstalledBundleIntegrity(target.pluginDir, "claude-code", "Installed Claude plugin bundle");
15828
16140
  }
15829
16141
  function uninstallClaudePlugin(target, pluginName, runCommand, options = {}) {
15830
16142
  const marketplaceName = getClaudeMarketplaceName(pluginName);
@@ -15906,7 +16218,11 @@ async function installPlugin(distDir, pluginName, platforms, options = {}) {
15906
16218
  console.log("Nothing to install. Run `pluxx build` first.");
15907
16219
  } else if (!options.quiet) {
15908
16220
  console.log(`
15909
- Installed ${installed} plugin(s). Reload or restart your tools to pick them up.`);
16221
+ Installed ${installed} plugin(s).`);
16222
+ console.log("Next checks:");
16223
+ console.log(` 1. Run: pluxx verify-install --target ${filtered.map((target) => target.platform).join(",")}`);
16224
+ console.log(" 2. Open the host plugin screen and confirm the plugin appears there.");
16225
+ console.log(" 3. Reload or restart the host if it was already open.");
15910
16226
  for (const note of getInstallFollowupNotes(filtered.map((target) => target.platform))) {
15911
16227
  console.log(note);
15912
16228
  }
@@ -16596,6 +16912,10 @@ function checkInstalledUserConfig(checks, rootDir) {
16596
16912
  const payload = JSON.parse(readFileSync11(resolvedPath, "utf-8"));
16597
16913
  const valueCount = Object.keys(payload.values ?? {}).length;
16598
16914
  const envCount = Object.keys(payload.env ?? {}).length;
16915
+ const placeholderKeys = [
16916
+ ...Object.entries(payload.values ?? {}).filter(([, value]) => isPlaceholderSecretValue(value)).map(([key]) => key),
16917
+ ...Object.entries(payload.env ?? {}).filter(([, value]) => isPlaceholderSecretValue(value)).map(([key]) => key)
16918
+ ];
16599
16919
  addCheck2(checks, {
16600
16920
  level: "success",
16601
16921
  code: "consumer-user-config-valid",
@@ -16604,6 +16924,16 @@ function checkInstalledUserConfig(checks, rootDir) {
16604
16924
  fix: "No action needed.",
16605
16925
  path: userConfigPath
16606
16926
  });
16927
+ if (placeholderKeys.length > 0) {
16928
+ addCheck2(checks, {
16929
+ level: "warning",
16930
+ code: "consumer-user-config-placeholder-secret",
16931
+ title: "Local install config contains placeholder-looking secret values",
16932
+ detail: `.pluxx-user.json contains placeholder-looking value${placeholderKeys.length === 1 ? "" : "s"} for ${placeholderKeys.join(", ")}.`,
16933
+ fix: "Reinstall the plugin with real secret values, or edit .pluxx-user.json and refresh/restart the host.",
16934
+ path: userConfigPath
16935
+ });
16936
+ }
16607
16937
  } catch (error) {
16608
16938
  addCheck2(checks, {
16609
16939
  level: "error",
@@ -16688,6 +17018,16 @@ function checkInstalledMcpConfig(checks, rootDir, layout) {
16688
17018
  if (servers.length === 0) {
16689
17019
  return;
16690
17020
  }
17021
+ if (layout.platform === "codex") {
17022
+ addCheck2(checks, {
17023
+ level: "info",
17024
+ code: "consumer-codex-mcp-bundled-visibility",
17025
+ title: "Codex plugin-bundled MCP visibility clarified",
17026
+ 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.`,
17027
+ 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.",
17028
+ path: layout.mcpConfigPath
17029
+ });
17030
+ }
16691
17031
  const remoteEntries = servers.filter((server) => "url" in server);
16692
17032
  const stdioEntries = servers.filter((server) => "command" in server);
16693
17033
  const inlineHeaderEntries = servers.filter((server) => {
@@ -16749,6 +17089,38 @@ function checkInstalledMcpConfig(checks, rootDir, layout) {
16749
17089
  });
16750
17090
  }
16751
17091
  }
17092
+ function checkInstalledBundleIntegrity(checks, rootDir, layout) {
17093
+ const issues = findInstalledBundleIntegrityIssues(rootDir, layout.platform);
17094
+ const details = [];
17095
+ if (issues.manifestIssue) {
17096
+ details.push(issues.manifestIssue);
17097
+ }
17098
+ if (issues.missingManifestPaths.length > 0) {
17099
+ details.push(`manifest references missing path${issues.missingManifestPaths.length === 1 ? "" : "s"}: ${issues.missingManifestPaths.join(", ")}`);
17100
+ }
17101
+ if (issues.missingHookTargets.length > 0) {
17102
+ details.push(`hook commands reference missing bundle target${issues.missingHookTargets.length === 1 ? "" : "s"}: ${issues.missingHookTargets.join(", ")}`);
17103
+ }
17104
+ if (details.length === 0) {
17105
+ addCheck2(checks, {
17106
+ level: "success",
17107
+ code: "consumer-bundle-integrity-valid",
17108
+ title: "Installed bundle references resolve inside the plugin",
17109
+ detail: "Every manifest-declared path and bundle-relative hook target exists in this installed bundle.",
17110
+ fix: "No action needed.",
17111
+ path: layout.manifestPath
17112
+ });
17113
+ return;
17114
+ }
17115
+ addCheck2(checks, {
17116
+ level: "error",
17117
+ code: "consumer-bundle-integrity-invalid",
17118
+ title: "Installed bundle is missing referenced files",
17119
+ detail: details.join("; "),
17120
+ fix: "Reinstall the plugin or rebuild the bundle so every manifest path and hook target exists in the installed plugin.",
17121
+ path: layout.manifestPath
17122
+ });
17123
+ }
16752
17124
  function findMissingInstalledStdioRuntimePaths(rootDir, stdioEntries) {
16753
17125
  const missing = /* @__PURE__ */ new Set();
16754
17126
  for (const server of stdioEntries) {
@@ -16934,6 +17306,7 @@ async function doctorConsumer(rootDir = process.cwd()) {
16934
17306
  path: layout.manifestPath
16935
17307
  });
16936
17308
  checkConsumerManifest(checks, rootDir, layout);
17309
+ checkInstalledBundleIntegrity(checks, rootDir, layout);
16937
17310
  checkInstalledUserConfig(checks, rootDir);
16938
17311
  checkInstalledEnvValidation(checks, rootDir);
16939
17312
  checkInstalledMcpConfig(checks, rootDir, layout);
@@ -19725,6 +20098,175 @@ echo
19725
20098
  echo "Installed __DISPLAY_NAME__ across ${installerTargets.join(", ")}."
19726
20099
  `.replaceAll("__REPO__", "REPO_PLACEHOLDER").replaceAll("__DISPLAY_NAME__", "DISPLAY_PLACEHOLDER");
19727
20100
  }
20101
+ function renderInstallerUserConfigSnippet(config, platform, installDirVariable) {
20102
+ const entries = collectUserConfigEntries(config, [platform]).map((entry) => ({
20103
+ key: entry.key,
20104
+ title: entry.title,
20105
+ type: entry.type ?? "string",
20106
+ required: entry.required !== false,
20107
+ envVar: entry.envVar ?? defaultUserConfigEnvVar(entry.key)
20108
+ }));
20109
+ if (entries.length === 0) return "";
20110
+ const promptLines = entries.map((entry) => {
20111
+ const functionName = entry.type === "secret" ? "pluxx_prompt_secret_config" : "pluxx_prompt_text_config";
20112
+ return `${functionName} ${JSON.stringify(entry.envVar)} ${JSON.stringify(entry.title)} ${entry.required ? "1" : "0"}`;
20113
+ });
20114
+ return `
20115
+ PLUXX_USER_CONFIG_SPEC="$(cat <<'PLUXX_USER_CONFIG_JSON'
20116
+ ${JSON.stringify(entries)}
20117
+ PLUXX_USER_CONFIG_JSON
20118
+ )"
20119
+
20120
+ pluxx_is_placeholder_secret() {
20121
+ case "$1" in
20122
+ *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*)
20123
+ return 0
20124
+ ;;
20125
+ *)
20126
+ return 1
20127
+ ;;
20128
+ esac
20129
+ }
20130
+
20131
+ pluxx_prompt_secret_config() {
20132
+ local env_var="$1"
20133
+ local label="$2"
20134
+ local required="$3"
20135
+ local current_value="\${!env_var:-}"
20136
+
20137
+ if [[ -z "$current_value" && "$required" == "1" ]]; then
20138
+ if [[ -t 0 || -r /dev/tty ]]; then
20139
+ read -r -s -p "$label [$env_var]: " current_value </dev/tty
20140
+ echo >/dev/tty
20141
+ else
20142
+ echo "Missing required config: export $env_var before running this installer." >&2
20143
+ exit 1
20144
+ fi
20145
+ fi
20146
+
20147
+ if [[ -n "$current_value" ]] && pluxx_is_placeholder_secret "$current_value"; then
20148
+ echo "Refusing placeholder-looking secret for $env_var. Set a real value and rerun the installer." >&2
20149
+ exit 1
20150
+ fi
20151
+
20152
+ export "$env_var=$current_value"
20153
+ }
20154
+
20155
+ pluxx_prompt_text_config() {
20156
+ local env_var="$1"
20157
+ local label="$2"
20158
+ local required="$3"
20159
+ local current_value="\${!env_var:-}"
20160
+
20161
+ if [[ -z "$current_value" && "$required" == "1" ]]; then
20162
+ if [[ -t 0 || -r /dev/tty ]]; then
20163
+ read -r -p "$label [$env_var]: " current_value </dev/tty
20164
+ else
20165
+ echo "Missing required config: export $env_var before running this installer." >&2
20166
+ exit 1
20167
+ fi
20168
+ fi
20169
+
20170
+ export "$env_var=$current_value"
20171
+ }
20172
+
20173
+ ${promptLines.join("\n")}
20174
+
20175
+ export PLUXX_USER_CONFIG_SPEC
20176
+ export PLUXX_INSTALL_DIR="${installDirVariable}"
20177
+
20178
+ node <<'NODE'
20179
+ const fs = require('fs')
20180
+ const path = require('path')
20181
+
20182
+ const installDir = process.env.PLUXX_INSTALL_DIR
20183
+ const spec = JSON.parse(process.env.PLUXX_USER_CONFIG_SPEC || '[]')
20184
+
20185
+ if (installDir && spec.length > 0) {
20186
+ const env = {}
20187
+ const values = {}
20188
+
20189
+ for (const entry of spec) {
20190
+ const value = process.env[entry.envVar]
20191
+ if (value === undefined || value === '') continue
20192
+ values[entry.key] = value
20193
+ env[entry.envVar] = value
20194
+ }
20195
+
20196
+ fs.writeFileSync(
20197
+ path.join(installDir, '.pluxx-user.json'),
20198
+ JSON.stringify({ values, env }, null, 2) + '\\n',
20199
+ )
20200
+
20201
+ const envScriptPath = path.join(installDir, 'scripts/check-env.sh')
20202
+ if (fs.existsSync(envScriptPath)) {
20203
+ fs.writeFileSync(
20204
+ envScriptPath,
20205
+ '#!/usr/bin/env bash\\nset -euo pipefail\\n# pluxx install materialized required config for this local plugin install.\\nexit 0\\n',
20206
+ )
20207
+ }
20208
+
20209
+ const materialize = (value) =>
20210
+ typeof value === 'string'
20211
+ ? value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_match, name) => env[name] || '${" + name + "}')
20212
+ : value
20213
+
20214
+ const materializeRecord = (record) => {
20215
+ if (!record || typeof record !== 'object') return record
20216
+ const next = {}
20217
+ for (const [key, value] of Object.entries(record)) {
20218
+ next[key] = materialize(value)
20219
+ }
20220
+ return next
20221
+ }
20222
+
20223
+ for (const relativePath of ['.mcp.json', 'mcp.json']) {
20224
+ const filepath = path.join(installDir, relativePath)
20225
+ if (!fs.existsSync(filepath)) continue
20226
+
20227
+ const payload = JSON.parse(fs.readFileSync(filepath, 'utf8'))
20228
+ for (const server of Object.values(payload.mcpServers || {})) {
20229
+ if (!server || typeof server !== 'object') continue
20230
+
20231
+ if (server.env) {
20232
+ server.env = materializeRecord(server.env)
20233
+ }
20234
+
20235
+ if (server.bearer_token_env_var && env[server.bearer_token_env_var]) {
20236
+ server.http_headers = {
20237
+ ...(server.http_headers || {}),
20238
+ Authorization: 'Bearer ' + env[server.bearer_token_env_var],
20239
+ }
20240
+ delete server.bearer_token_env_var
20241
+ }
20242
+
20243
+ if (server.env_http_headers && typeof server.env_http_headers === 'object') {
20244
+ server.http_headers = {
20245
+ ...(server.http_headers || {}),
20246
+ }
20247
+ for (const [headerName, envVar] of Object.entries(server.env_http_headers)) {
20248
+ if (env[envVar]) server.http_headers[headerName] = env[envVar]
20249
+ }
20250
+ delete server.env_http_headers
20251
+ }
20252
+
20253
+ if (server.headers) {
20254
+ server.headers = materializeRecord(server.headers)
20255
+ }
20256
+ if (server.http_headers) {
20257
+ server.http_headers = materializeRecord(server.http_headers)
20258
+ }
20259
+ }
20260
+
20261
+ fs.writeFileSync(filepath, JSON.stringify(payload, null, 2) + '\\n')
20262
+ }
20263
+ }
20264
+ NODE
20265
+ `;
20266
+ }
20267
+ function hasInstallerUserConfig(config, platform) {
20268
+ return collectUserConfigEntries(config, [platform]).length > 0;
20269
+ }
19728
20270
  function renderInstallClaudeCodeScript(config) {
19729
20271
  return `#!/usr/bin/env bash
19730
20272
  set -euo pipefail
@@ -19751,6 +20293,7 @@ need_cmd tar
19751
20293
  need_cmd mktemp
19752
20294
  need_cmd grep
19753
20295
  need_cmd sed
20296
+ ${hasInstallerUserConfig(config, "claude-code") ? "need_cmd node" : ""}
19754
20297
 
19755
20298
  if [[ "$SKIP_INSTALL" != "1" ]]; then
19756
20299
  need_cmd curl
@@ -19787,6 +20330,7 @@ DESCRIPTION="$(grep -E '"description"' "$PLUGIN_MANIFEST" | head -n1 | sed -E 's
19787
20330
  mkdir -p "$INSTALL_ROOT/.claude-plugin" "$INSTALL_ROOT/plugins"
19788
20331
  rm -rf "$INSTALL_ROOT/plugins/$PLUGIN_NAME"
19789
20332
  cp -R "$BUNDLE_DIR" "$INSTALL_ROOT/plugins/$PLUGIN_NAME"
20333
+ ${renderInstallerUserConfigSnippet(config, "claude-code", "$INSTALL_ROOT/plugins/$PLUGIN_NAME")}
19790
20334
 
19791
20335
  cat > "$INSTALL_ROOT/.claude-plugin/marketplace.json" <<JSON
19792
20336
  {
@@ -19850,6 +20394,7 @@ need_cmd() {
19850
20394
  need_cmd tar
19851
20395
  need_cmd mktemp
19852
20396
  need_cmd curl
20397
+ ${hasInstallerUserConfig(config, "cursor") ? "need_cmd node" : ""}
19853
20398
 
19854
20399
  TMP_DIR="$(mktemp -d)"
19855
20400
  cleanup() {
@@ -19878,6 +20423,7 @@ fi
19878
20423
  mkdir -p "$(dirname "$INSTALL_DIR")"
19879
20424
  rm -rf "$INSTALL_DIR"
19880
20425
  cp -R "$BUNDLE_DIR" "$INSTALL_DIR"
20426
+ ${renderInstallerUserConfigSnippet(config, "cursor", "$INSTALL_DIR")}
19881
20427
 
19882
20428
  echo "Installed $PLUGIN_NAME to $INSTALL_DIR"
19883
20429
  echo "If Cursor is already open, use Developer: Reload Window or restart Cursor so the plugin is picked up."
@@ -19935,6 +20481,7 @@ fi
19935
20481
  mkdir -p "$(dirname "$INSTALL_DIR")"
19936
20482
  rm -rf "$INSTALL_DIR"
19937
20483
  cp -R "$BUNDLE_DIR" "$INSTALL_DIR"
20484
+ ${renderInstallerUserConfigSnippet(config, "codex", "$INSTALL_DIR")}
19938
20485
 
19939
20486
  mkdir -p "$(dirname "$MARKETPLACE_PATH")"
19940
20487
 
@@ -20049,6 +20596,7 @@ fi
20049
20596
  mkdir -p "$(dirname "$INSTALL_DIR")" "$SKILLS_ROOT"
20050
20597
  rm -rf "$INSTALL_DIR"
20051
20598
  cp -R "$BUNDLE_DIR" "$INSTALL_DIR"
20599
+ ${renderInstallerUserConfigSnippet(config, "opencode", "$INSTALL_DIR")}
20052
20600
 
20053
20601
  export ENTRY_PATH
20054
20602
  export PLUGIN_NAME
@@ -20376,22 +20924,107 @@ function printJson(value) {
20376
20924
  }
20377
20925
 
20378
20926
  // src/cli/verify-install.ts
20379
- import { existsSync as existsSync27 } from "fs";
20927
+ import { existsSync as existsSync27, lstatSync as lstatSync4, readdirSync as readdirSync11, readFileSync as readFileSync15, readlinkSync, realpathSync, statSync as statSync5 } from "fs";
20380
20928
  import { resolve as resolve21 } from "path";
20381
20929
  function buildCheckFromReport(target, pluginName, report) {
20382
20930
  const consumerPath = resolveInstalledConsumerPath(target, pluginName);
20931
+ const staleReason = target.built && existsSync27(consumerPath) ? detectStaleInstall(target, pluginName, consumerPath) : void 0;
20932
+ const stale = staleReason !== void 0;
20383
20933
  return {
20384
20934
  platform: target.platform,
20385
20935
  installPath: target.pluginDir,
20386
20936
  consumerPath,
20387
20937
  built: target.built,
20388
20938
  installed: existsSync27(consumerPath),
20389
- ok: report.errors === 0,
20390
- errors: report.errors,
20939
+ stale,
20940
+ ...staleReason ? { staleReason } : {},
20941
+ ok: report.errors === 0 && !stale,
20942
+ errors: report.errors + (stale ? 1 : 0),
20391
20943
  warnings: report.warnings,
20392
20944
  infos: report.infos
20393
20945
  };
20394
20946
  }
20947
+ function manifestPathForPlatform2(platform) {
20948
+ switch (platform) {
20949
+ case "claude-code":
20950
+ return ".claude-plugin/plugin.json";
20951
+ case "cursor":
20952
+ return ".cursor-plugin/plugin.json";
20953
+ case "codex":
20954
+ return ".codex-plugin/plugin.json";
20955
+ case "opencode":
20956
+ return "package.json";
20957
+ default:
20958
+ return void 0;
20959
+ }
20960
+ }
20961
+ function readInstalledManifestVersion(rootDir, platform) {
20962
+ const manifestPath = manifestPathForPlatform2(platform);
20963
+ if (!manifestPath) return void 0;
20964
+ const filepath = resolve21(rootDir, manifestPath);
20965
+ if (!existsSync27(filepath)) return void 0;
20966
+ try {
20967
+ const manifest = JSON.parse(readFileSync15(filepath, "utf-8"));
20968
+ return typeof manifest.version === "string" ? manifest.version : void 0;
20969
+ } catch {
20970
+ return void 0;
20971
+ }
20972
+ }
20973
+ function findCodexCacheCandidates(pluginName) {
20974
+ const home = process.env.HOME ?? "~";
20975
+ const cacheRoot = resolve21(home, ".codex/plugins/cache");
20976
+ if (!existsSync27(cacheRoot)) return [];
20977
+ const candidates = [];
20978
+ for (const marketplace of readdirSync11(cacheRoot)) {
20979
+ const pluginRoot = resolve21(cacheRoot, marketplace, pluginName);
20980
+ if (!existsSync27(pluginRoot)) continue;
20981
+ for (const versionDir of readdirSync11(pluginRoot)) {
20982
+ const candidatePath = resolve21(pluginRoot, versionDir);
20983
+ try {
20984
+ const stats = statSync5(candidatePath);
20985
+ if (!stats.isDirectory()) continue;
20986
+ candidates.push({
20987
+ path: candidatePath,
20988
+ version: readInstalledManifestVersion(candidatePath, "codex") ?? versionDir,
20989
+ mtimeMs: stats.mtimeMs
20990
+ });
20991
+ } catch {
20992
+ }
20993
+ }
20994
+ }
20995
+ return candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
20996
+ }
20997
+ function detectCodexCacheStaleness(pluginName, builtVersion) {
20998
+ if (!builtVersion) return void 0;
20999
+ const candidates = findCodexCacheCandidates(pluginName);
21000
+ if (candidates.length === 0) return void 0;
21001
+ if (candidates.some((candidate) => candidate.version === builtVersion)) return void 0;
21002
+ const newest = candidates[0];
21003
+ 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.`;
21004
+ }
21005
+ function detectStaleInstall(target, pluginName, consumerPath) {
21006
+ try {
21007
+ const details = lstatSync4(consumerPath);
21008
+ if (details.isSymbolicLink()) {
21009
+ const installedRealPath = realpathSync(consumerPath);
21010
+ const builtRealPath = realpathSync(target.sourceDir);
21011
+ if (installedRealPath !== builtRealPath) {
21012
+ return `installed symlink points to ${readlinkSync(consumerPath)}, not the current build at ${target.sourceDir}`;
21013
+ }
21014
+ }
21015
+ } catch {
21016
+ return void 0;
21017
+ }
21018
+ const builtVersion = readInstalledManifestVersion(target.sourceDir, target.platform);
21019
+ const installedVersion = readInstalledManifestVersion(consumerPath, target.platform);
21020
+ if (builtVersion && installedVersion && builtVersion !== installedVersion) {
21021
+ return `installed version ${installedVersion} does not match built version ${builtVersion}`;
21022
+ }
21023
+ if (target.platform === "codex") {
21024
+ return detectCodexCacheStaleness(pluginName, builtVersion);
21025
+ }
21026
+ return void 0;
21027
+ }
20395
21028
  async function verifyInstall(config, options = {}) {
20396
21029
  const rootDir = options.rootDir ?? process.cwd();
20397
21030
  const distDir = resolve21(rootDir, config.outDir);
@@ -20419,12 +21052,39 @@ function printVerifyInstallResult(result) {
20419
21052
  console.log(`${prefix} ${check.platform}: ${check.consumerPath}`);
20420
21053
  console.log(` install path: ${check.installPath}`);
20421
21054
  console.log(` built: ${check.built ? "yes" : "no"}; installed: ${check.installed ? "yes" : "no"}; errors: ${check.errors}; warnings: ${check.warnings}; infos: ${check.infos}`);
21055
+ if (check.stale) {
21056
+ console.log(` stale install: ${check.staleReason}`);
21057
+ }
21058
+ if (!check.ok) {
21059
+ for (const action of getVerifyInstallRecoveryActions(check)) {
21060
+ console.log(` fix: ${action}`);
21061
+ }
21062
+ }
20422
21063
  }
20423
21064
  console.log(result.ok ? "pluxx verify-install passed." : "pluxx verify-install failed.");
20424
21065
  }
21066
+ function getVerifyInstallRecoveryActions(check) {
21067
+ const actions = [];
21068
+ if (!check.built) {
21069
+ actions.push(`run pluxx build --target ${check.platform}`);
21070
+ }
21071
+ if (check.built && !check.installed) {
21072
+ actions.push(`run pluxx install --target ${check.platform}${check.platform === "claude-code" ? " and accept/trust any hook prompt if expected" : ""}`);
21073
+ }
21074
+ if (check.stale) {
21075
+ actions.push(`rerun pluxx install --target ${check.platform} to replace the stale local install`);
21076
+ if (check.platform === "codex") {
21077
+ actions.push("in Codex, use Plugins > Refresh if available, or restart Codex so the plugin cache reloads");
21078
+ }
21079
+ }
21080
+ if (check.errors > 0 && actions.length === 0) {
21081
+ actions.push(`run pluxx doctor --consumer "${check.consumerPath}" for the detailed host-specific failure`);
21082
+ }
21083
+ return actions;
21084
+ }
20425
21085
 
20426
21086
  // src/cli/behavioral.ts
20427
- import { existsSync as existsSync28, readFileSync as readFileSync15 } from "fs";
21087
+ import { existsSync as existsSync28, readFileSync as readFileSync16 } from "fs";
20428
21088
  import { mkdtemp as mkdtemp2, rm as rm3 } from "fs/promises";
20429
21089
  import { tmpdir as tmpdir4 } from "os";
20430
21090
  import { resolve as resolve22 } from "path";
@@ -20466,7 +21126,7 @@ function loadBehavioralCases(rootDir, targets, promptOverride) {
20466
21126
  `No behavioral smoke config found at ${BEHAVIORAL_CONFIG_PATH}. Add that file or pass --behavioral-prompt to define a real example query.`
20467
21127
  );
20468
21128
  }
20469
- const parsed = JSON.parse(readFileSync15(filePath, "utf-8"));
21129
+ const parsed = JSON.parse(readFileSync16(filePath, "utf-8"));
20470
21130
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.cases) || parsed.cases.length === 0) {
20471
21131
  throw new Error(`${BEHAVIORAL_CONFIG_PATH} must contain a non-empty "cases" array.`);
20472
21132
  }
@@ -20576,7 +21236,7 @@ async function executeBehavioralCommand(platform, command2, cwd) {
20576
21236
  child.on("close", (code) => {
20577
21237
  const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
20578
21238
  const stderr = Buffer.concat(stderrChunks).toString("utf-8");
20579
- const codexMessage = codexLastMessagePath && existsSync28(codexLastMessagePath) ? readFileSync15(codexLastMessagePath, "utf-8") : "";
21239
+ const codexMessage = codexLastMessagePath && existsSync28(codexLastMessagePath) ? readFileSync16(codexLastMessagePath, "utf-8") : "";
20580
21240
  resolvePromise({
20581
21241
  exitCode: code ?? 1,
20582
21242
  response: codexMessage.trim() || stdout.trim() || stderr.trim()
@@ -20639,7 +21299,7 @@ function shellQuote2(value) {
20639
21299
  }
20640
21300
 
20641
21301
  // src/cli/discover-installed-mcp.ts
20642
- import { existsSync as existsSync29, readFileSync as readFileSync16 } from "fs";
21302
+ import { existsSync as existsSync29, readFileSync as readFileSync17 } from "fs";
20643
21303
  import { homedir as homedir2 } from "os";
20644
21304
  import { resolve as resolve23 } from "path";
20645
21305
  var INSTALLED_MCP_HOSTS = ["claude-code", "cursor", "codex", "opencode"];
@@ -20733,7 +21393,7 @@ function installedMcpFileCandidates(rootDir, homeDir) {
20733
21393
  }
20734
21394
  function parseJsonMcpFile(path, host) {
20735
21395
  try {
20736
- const raw = JSON.parse(readFileSync16(path, "utf-8"));
21396
+ const raw = JSON.parse(readFileSync17(path, "utf-8"));
20737
21397
  const servers = extractJsonMcpServers(raw, path, host);
20738
21398
  return Object.entries(servers).flatMap(([serverName, config]) => {
20739
21399
  const normalized = normalizeCommonMcpServer(config, host);
@@ -20768,7 +21428,7 @@ function extractJsonMcpServers(raw, path, host) {
20768
21428
  return {};
20769
21429
  }
20770
21430
  function parseCodexTomlMcpFile(path) {
20771
- const text = readFileSync16(path, "utf-8");
21431
+ const text = readFileSync17(path, "utf-8");
20772
21432
  const servers = {};
20773
21433
  let currentServer;
20774
21434
  let currentSubtable;
@@ -21141,7 +21801,7 @@ function isHelpRequested(input) {
21141
21801
  }
21142
21802
  function getCliPackageVersion() {
21143
21803
  const packageJsonPath = new URL("../../package.json", import.meta.url);
21144
- const raw = JSON.parse(readFileSync17(packageJsonPath, "utf-8"));
21804
+ const raw = JSON.parse(readFileSync18(packageJsonPath, "utf-8"));
21145
21805
  if (typeof raw.version !== "string" || raw.version.trim() === "") {
21146
21806
  throw new Error("Unable to determine the installed pluxx version from package.json.");
21147
21807
  }
@@ -21253,20 +21913,21 @@ function planAutopilotPasses(input) {
21253
21913
  };
21254
21914
  }
21255
21915
  function countAutopilotSteps(input) {
21256
- return 2 + Number(input.taxonomy.enabled) + Number(input.instructions.enabled) + Number(input.review.enabled) + Number(input.verify);
21916
+ return 2 + Number(input.taxonomy.enabled) + Number(input.instructions.enabled) + Number(input.review.enabled) + Number(input.verify) + Number(input.install) + Number(input.behavioral);
21257
21917
  }
21258
21918
  function formatAutopilotPassLine(label, decision) {
21259
21919
  return `${label}: ${decision.enabled ? "run" : "skip"} (${decision.reason})`;
21260
21920
  }
21261
21921
  function summarizeAutopilotWorkload(input) {
21262
21922
  const agentPassCount = Number(input.taxonomy.enabled) + Number(input.instructions.enabled) + Number(input.review.enabled);
21923
+ const suffix = `${input.verify ? " + verification" : ""}${input.install ? " + install" : ""}${input.behavioral ? " + behavioral smoke" : ""}`;
21263
21924
  if (agentPassCount === 0 && !input.verify) {
21264
- return "deterministic scaffold only";
21925
+ return `deterministic scaffold${input.install ? " + install" : ""}${input.behavioral ? " + behavioral smoke" : ""}`;
21265
21926
  }
21266
21927
  if (agentPassCount === 0) {
21267
- return "deterministic scaffold + verification";
21928
+ return `deterministic scaffold${suffix}`;
21268
21929
  }
21269
- return `${agentPassCount} agent pass${agentPassCount === 1 ? "" : "es"}${input.verify ? " + verification" : ""}`;
21930
+ return `${agentPassCount} agent pass${agentPassCount === 1 ? "" : "es"}${suffix}`;
21270
21931
  }
21271
21932
  function formatDuration(durationMs) {
21272
21933
  if (durationMs === void 0) {
@@ -21509,6 +22170,16 @@ function parseTargetFlagValues(rawArgs2) {
21509
22170
  if (!values) return void 0;
21510
22171
  return parseTargetPlatforms(values.join(","));
21511
22172
  }
22173
+ function parseInstallTargetFlag(rawArgs2, targets) {
22174
+ const raw = readOption2(rawArgs2, "--install-target");
22175
+ if (!raw) return [targets[0]];
22176
+ const installTargets = parseTargetPlatforms(raw);
22177
+ const unsupported = installTargets.filter((target) => !targets.includes(target));
22178
+ if (unsupported.length > 0) {
22179
+ throw new Error(`--install-target must be one of the configured autopilot targets: ${targets.join(", ")}`);
22180
+ }
22181
+ return installTargets;
22182
+ }
21512
22183
  function defaultHookMode(source) {
21513
22184
  if (source.auth?.type && source.auth.type !== "none" && source.auth.envVar) {
21514
22185
  return "safe";
@@ -22607,6 +23278,9 @@ async function runAutopilot() {
22607
23278
  const attach = readOption2(args, "--attach");
22608
23279
  const reviewRequested = args.includes("--review");
22609
23280
  const verify = !args.includes("--no-verify");
23281
+ const installRequested = args.includes("--install");
23282
+ const behavioralRequested = args.includes("--behavioral");
23283
+ const behavioralPrompt = readOption2(args, "--behavioral-prompt");
22610
23284
  const verboseRunner = args.includes("--verbose-runner");
22611
23285
  const interactive = !runtime.jsonOutput && runtime.isInteractive && !initOptions.assumeDefaults;
22612
23286
  let authEnv = initOptions.authEnv;
@@ -22615,17 +23289,23 @@ async function runAutopilot() {
22615
23289
  let authTemplate = initOptions.authTemplate;
22616
23290
  let runtimeAuthMode = resolveRuntimeAuthMode(initOptions.runtimeAuth);
22617
23291
  if (!initOptions.source && !interactive) {
22618
- 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]`);
23292
+ 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]`);
22619
23293
  process.exit(1);
22620
23294
  }
22621
23295
  if ((!runnerRaw || !AGENT_RUNNERS.includes(runnerRaw)) && !interactive) {
22622
- 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]`);
23296
+ 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]`);
22623
23297
  process.exit(1);
22624
23298
  }
22625
23299
  if (modeRaw && !AUTOPILOT_MODES.includes(modeRaw)) {
22626
23300
  console.error(`Autopilot mode must be one of: ${AUTOPILOT_MODES.join(", ")}`);
22627
23301
  process.exit(1);
22628
23302
  }
23303
+ if (installRequested && !verify) {
23304
+ throw new Error("--install requires verification so autopilot can build outputs before installing. Remove --no-verify or omit --install.");
23305
+ }
23306
+ if (behavioralRequested && !installRequested) {
23307
+ throw new Error("--behavioral requires --install so the selected host CLI can see the installed plugin bundle.");
23308
+ }
22629
23309
  let tempDir;
22630
23310
  try {
22631
23311
  if (!runtime.jsonOutput && !runtime.quiet && interactive) {
@@ -22793,6 +23473,7 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22793
23473
  const authorName = initOptions.author ?? (interactive ? await clackText("Author name", defaultAuthorName) : defaultAuthorName);
22794
23474
  const targetsRaw = initOptions.targets ?? (interactive ? await clackText("Platforms (comma-separated)", DEFAULT_INIT_TARGETS.join(",")) : DEFAULT_INIT_TARGETS.join(","));
22795
23475
  const targets = parseTargetPlatforms(targetsRaw);
23476
+ const installTargets = installRequested ? parseInstallTargetFlag(args, targets) : [];
22796
23477
  const grouping = initOptions.grouping ? parseChoiceOption(initOptions.grouping, MCP_SKILL_GROUPINGS, "Skill grouping") : interactive ? await clackSelect("Skill grouping", [
22797
23478
  { value: "workflow", label: "workflow", hint: "Group related tools into workflow skills" },
22798
23479
  { value: "tool", label: "tool", hint: "One skill per tool" }
@@ -22822,7 +23503,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22822
23503
  taxonomy: passDecisions.taxonomy,
22823
23504
  instructions: passDecisions.instructions,
22824
23505
  review: passDecisions.review,
22825
- verify
23506
+ verify,
23507
+ install: installRequested,
23508
+ behavioral: behavioralRequested
22826
23509
  });
22827
23510
  const workspaceRoot = runtime.dryRun ? await mkdtemp3(`${tmpdir5()}/pluxx-autopilot-`) : process.cwd();
22828
23511
  tempDir = runtime.dryRun ? workspaceRoot : void 0;
@@ -22908,6 +23591,12 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22908
23591
  quality,
22909
23592
  review: passDecisions.review.enabled,
22910
23593
  verify,
23594
+ install: installRequested ? {
23595
+ enabled: true,
23596
+ platforms: installTargets,
23597
+ notes: getInstallFollowupNotes(installTargets),
23598
+ installTargets: []
23599
+ } : void 0,
22911
23600
  steps: totalSteps,
22912
23601
  init: {
22913
23602
  createdFiles: initCreatedFiles,
@@ -22955,7 +23644,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22955
23644
  taxonomy: passDecisions.taxonomy,
22956
23645
  instructions: passDecisions.instructions,
22957
23646
  review: passDecisions.review,
22958
- verify
23647
+ verify,
23648
+ install: installRequested,
23649
+ behavioral: behavioralRequested
22959
23650
  })}`);
22960
23651
  console.log(` Quality: ${quality.warnings} warning(s), ${quality.infos} info message(s)`);
22961
23652
  console.log(` Scaffold create/update: ${[...initCreatedFiles, ...initUpdatedFiles].join(", ") || "none"}`);
@@ -22976,6 +23667,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22976
23667
  } else {
22977
23668
  console.log(" Verification: skipped (--no-verify)");
22978
23669
  }
23670
+ if (installRequested) {
23671
+ console.log(` Install: ${installTargets.join(", ")} after verification`);
23672
+ }
22979
23673
  }
22980
23674
  return;
22981
23675
  }
@@ -23055,9 +23749,13 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
23055
23749
  });
23056
23750
  return result;
23057
23751
  })() : void 0;
23058
- const ok = (taxonomyResult?.ok ?? true) && (instructionsResult?.ok ?? true) && (reviewResult?.ok ?? true) && (verification?.ok ?? true);
23059
- const failureStage = taxonomyResult && taxonomyResult.runnerExitCode !== 0 ? "runner" : instructionsResult && instructionsResult.runnerExitCode !== 0 ? "runner" : reviewResult && reviewResult.runnerExitCode !== 0 ? "runner" : verification && !verification.ok ? "verification" : void 0;
23060
- 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;
23752
+ const preInstallOk = (taxonomyResult?.ok ?? true) && (instructionsResult?.ok ?? true) && (reviewResult?.ok ?? true) && (verification?.ok ?? true);
23753
+ const installedConfig = installRequested && preInstallOk ? await loadConfig() : void 0;
23754
+ const install = installedConfig ? await maybeInstallBuiltOutputs(installedConfig, installTargets, { verifyConsumers: true }) : void 0;
23755
+ const behavioralResult = install && install.verification?.ok && behavioralRequested ? await runBehavioralSuite(process.cwd(), installedConfig, installTargets, { promptOverride: behavioralPrompt }) : void 0;
23756
+ const ok = preInstallOk && (install?.verification?.ok ?? true) && (behavioralResult?.ok ?? true);
23757
+ 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;
23758
+ 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;
23061
23759
  const summary = {
23062
23760
  ok,
23063
23761
  pluginName,
@@ -23119,6 +23817,8 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
23119
23817
  },
23120
23818
  verification,
23121
23819
  verificationDurationMs,
23820
+ install,
23821
+ behavioral: behavioralResult,
23122
23822
  failureStage,
23123
23823
  failureMessage
23124
23824
  };
@@ -23134,7 +23834,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
23134
23834
  taxonomy: passDecisions.taxonomy,
23135
23835
  instructions: passDecisions.instructions,
23136
23836
  review: passDecisions.review,
23137
- verify
23837
+ verify,
23838
+ install: installRequested,
23839
+ behavioral: behavioralRequested
23138
23840
  })}`);
23139
23841
  console.log(` Quality: ${quality.warnings} warning(s), ${quality.infos} info message(s)`);
23140
23842
  if (!verboseRunner) {
@@ -23157,6 +23859,23 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
23157
23859
  } else {
23158
23860
  console.log(" Verification: skipped (--no-verify)");
23159
23861
  }
23862
+ if (install) {
23863
+ console.log(` Install: ${install.verification?.ok === false ? "failed" : "passed"} for ${install.platforms.join(", ")}`);
23864
+ for (const target of install.installTargets) {
23865
+ console.log(` ${target.platform}: ${target.consumerPath}`);
23866
+ }
23867
+ if (install.verification) {
23868
+ console.log(` Verify-install: ${install.verification.ok ? "passed" : "failed"}`);
23869
+ }
23870
+ for (const note of install.notes) {
23871
+ console.log(` ${note}`);
23872
+ }
23873
+ } else if (installRequested) {
23874
+ console.log(` Install: skipped because verification did not produce a passing build`);
23875
+ }
23876
+ if (behavioralResult) {
23877
+ console.log(` Behavioral: ${behavioralResult.ok ? "passed" : "failed"}`);
23878
+ }
23160
23879
  if (failureStage && failureMessage) {
23161
23880
  console.log(` Failure stage: ${failureStage}`);
23162
23881
  console.log(` Failure detail: ${failureMessage}`);
@@ -23165,6 +23884,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
23165
23884
  console.log(" 1. Review INSTRUCTIONS.md and skills/");
23166
23885
  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)" : ""}`);
23167
23886
  console.log(` 3. Run: pluxx install${scaffoldPlan.generatedHookMode === "safe" ? " --trust" : ""} --target ${targets[0]}`);
23887
+ if (!installRequested) {
23888
+ console.log(` Or rerun onboarding: pluxx autopilot --from-mcp "${rawSource}" --runner ${runner} --mode ${mode} --install --install-target ${targets[0]}${scaffoldPlan.generatedHookMode === "safe" ? " --trust" : ""}`);
23889
+ }
23168
23890
  }
23169
23891
  if (!ok) {
23170
23892
  process.exit(1);
@@ -23489,6 +24211,10 @@ Common flags:
23489
24211
  --mode quick|standard|thorough Control how much agent refinement autopilot performs
23490
24212
  --approve-mcp-tools Preapprove all tools from the imported MCP in canonical permissions
23491
24213
  --ingest-provider auto|local|firecrawl Choose the docs/website ingestion backend for agent prepare/autopilot
24214
+ --install Install autopilot's verified build into one selected host
24215
+ --install-target <platform> Host to install after autopilot verification; defaults to the first target
24216
+ --trust Trust local hook commands during install/test flows
24217
+ --behavioral Run installed headless example-query smoke checks
23492
24218
 
23493
24219
  Targets:
23494
24220
  claude-code, cursor, codex, opencode, github-copilot, openhands,
@@ -23531,6 +24257,8 @@ Examples:
23531
24257
  --attach is only supported for the opencode runner
23532
24258
  pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode quick --yes
23533
24259
  pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode standard --yes --name acme --display-name "Acme"
24260
+ pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode standard --install --install-target codex --trust
24261
+ pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode standard --auth-env API_KEY --auth-type bearer --install --install-target codex --trust
23534
24262
  pluxx autopilot --from-mcp https://example.com/mcp --runner codex --yes --approve-mcp-tools
23535
24263
  pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode thorough --yes --verbose-runner
23536
24264
  pluxx autopilot --from-mcp https://mcp.linear.app/mcp --runner codex --yes --oauth-wrapper