@orchid-labs/pluxx 0.1.7 → 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 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,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
  }
@@ -9317,6 +9373,16 @@ import { basename as basename5, dirname as dirname4, relative as relative7, reso
9317
9373
 
9318
9374
  // src/user-config.ts
9319
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
+ ];
9320
9386
  function normalizeUserConfigKey(value) {
9321
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, "");
9322
9388
  }
@@ -9331,6 +9397,12 @@ function extractEnvReference(value) {
9331
9397
  const match = value.match(/^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/);
9332
9398
  return match?.[1];
9333
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
+ }
9334
9406
  function isRuntimePlatformManaged(config, target, server) {
9335
9407
  if (server.transport === "stdio") return false;
9336
9408
  if (server.auth?.type === "platform") {
@@ -9507,6 +9579,42 @@ var WORKFLOW_SKILL_DEFINITIONS = [
9507
9579
  title: "General Research",
9508
9580
  description: "Handle broad search and query workflows when there is not a more specific product surface match.",
9509
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"]
9510
9618
  }
9511
9619
  ];
9512
9620
  var WORKFLOW_MATCH_MIN_SCORE = 2;
@@ -9589,6 +9697,8 @@ async function planMcpScaffold(options) {
9589
9697
  options.persistedSkills,
9590
9698
  options.toolRenames
9591
9699
  );
9700
+ const skillGrouping = options.skillGrouping ?? "workflow";
9701
+ const plannedCommands = planCommandScaffolds(plannedSkills, skillGrouping);
9592
9702
  const description = options.description ?? deriveScaffoldDescription({
9593
9703
  displayName,
9594
9704
  introspection: options.introspection,
@@ -9641,7 +9751,7 @@ async function planMcpScaffold(options) {
9641
9751
  passthroughPaths,
9642
9752
  runtimeAuthMode,
9643
9753
  permissions,
9644
- commandsPath: "./commands/"
9754
+ commandsPath: plannedCommands.length > 0 ? "./commands/" : void 0
9645
9755
  })
9646
9756
  );
9647
9757
  await addPlannedFile(
@@ -9671,8 +9781,6 @@ async function planMcpScaffold(options) {
9671
9781
  for (const skill of plannedSkills) {
9672
9782
  const relativeSkillPath = `skills/${skill.dirName}`;
9673
9783
  const skillPath = resolve10(skillRoot, skill.dirName, "SKILL.md");
9674
- const relativeCommandPath = `commands/${skill.dirName}.md`;
9675
- const commandPath = resolve10(commandsRoot, `${skill.dirName}.md`);
9676
9784
  await addPlannedFile(
9677
9785
  `${relativeSkillPath}/SKILL.md`,
9678
9786
  wrapManagedMarkdown(
@@ -9686,6 +9794,10 @@ async function planMcpScaffold(options) {
9686
9794
  );
9687
9795
  skillDirectories.push(relativeSkillPath);
9688
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`);
9689
9801
  await addPlannedFile(
9690
9802
  relativeCommandPath,
9691
9803
  buildCommandContent(
@@ -9705,7 +9817,7 @@ async function planMcpScaffold(options) {
9705
9817
  introspection: options.introspection,
9706
9818
  pluginName,
9707
9819
  displayName,
9708
- skillGrouping: options.skillGrouping ?? "workflow",
9820
+ skillGrouping,
9709
9821
  requestedHookMode: options.hookMode ?? "none",
9710
9822
  generatedHookMode: generatedHooks.mode,
9711
9823
  generatedHookEvents: Object.keys(generatedHooks.hookEntries ?? {}),
@@ -9932,6 +10044,19 @@ function buildCommandContent(skill, existingContent) {
9932
10044
  }
9933
10045
  );
9934
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
+ }
9935
10060
  function formatArgumentHintFrontmatter(value) {
9936
10061
  const trimmed = value.trim();
9937
10062
  if (!trimmed) return '""';
@@ -11272,6 +11397,11 @@ function evaluateSkills(rootDir, metadata, checks) {
11272
11397
  function hasManagedCommands(metadata) {
11273
11398
  return metadata.managedFiles.some((file) => file.startsWith("commands/"));
11274
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
+ }
11275
11405
  function evaluateCommands(rootDir, metadata, checks) {
11276
11406
  if (!hasManagedCommands(metadata)) {
11277
11407
  addCheck(checks, {
@@ -11284,7 +11414,8 @@ function evaluateCommands(rootDir, metadata, checks) {
11284
11414
  return;
11285
11415
  }
11286
11416
  const failures = [];
11287
- for (const skill of metadata.skills) {
11417
+ const commandSkillNames = managedCommandSkillNames(metadata);
11418
+ for (const skill of metadata.skills.filter((entry) => commandSkillNames.has(entry.dirName))) {
11288
11419
  const relativePath = `commands/${skill.dirName}.md`;
11289
11420
  const filePath = resolve11(rootDir, relativePath);
11290
11421
  if (!existsSync19(filePath)) {
@@ -11332,10 +11463,36 @@ function evaluateCommands(rootDir, metadata, checks) {
11332
11463
  level: "success",
11333
11464
  code: "command-quality-contract",
11334
11465
  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.`,
11466
+ detail: `Checked ${commandSkillNames.size} generated command file(s) for argument hints, tool routing, and related discovery surfaces.`,
11336
11467
  fix: "No action needed."
11337
11468
  });
11338
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
+ }
11339
11496
  function evaluateAgentContext(contextContent, metadata, checks) {
11340
11497
  const missing = [];
11341
11498
  if (((metadata.resources?.length ?? 0) > 0 || (metadata.resourceTemplates?.length ?? 0) > 0 || (metadata.prompts?.length ?? 0) > 0) && !contextContent.includes("## MCP Discovery Surfaces")) {
@@ -11475,6 +11632,7 @@ async function runEvalSuite(options = {}) {
11475
11632
  evaluateInstructions(rootDir, metadata, checks);
11476
11633
  evaluateSkills(rootDir, metadata, checks);
11477
11634
  evaluateCommands(rootDir, metadata, checks);
11635
+ evaluateScaffoldArchitecture(metadata, checks);
11478
11636
  }
11479
11637
  evaluateAgentContext(contextContent, metadata, checks);
11480
11638
  for (const kind of AGENT_PROMPT_KINDS) {
@@ -15249,6 +15407,10 @@ async function resolveInstallUserConfig(config, platforms = config.targets, opti
15249
15407
  const isTTY = options.isTTY ?? process.stdin.isTTY === true;
15250
15408
  for (const entry of planned) {
15251
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
+ }
15252
15414
  resolved.push({
15253
15415
  field: entry.field,
15254
15416
  value: entry.value,
@@ -15260,8 +15422,8 @@ async function resolveInstallUserConfig(config, platforms = config.targets, opti
15260
15422
  continue;
15261
15423
  }
15262
15424
  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}`);
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}`);
15265
15427
  }
15266
15428
  const promptLabel = entry.field.title || entry.field.key;
15267
15429
  const envHint = entry.envVar ? ` [env: ${entry.envVar}]` : "";
@@ -15336,7 +15498,7 @@ async function ensureHookTrust(options) {
15336
15498
  const isTTY = options.isTTY ?? process.stdin.isTTY === true;
15337
15499
  if (!isTTY) {
15338
15500
  throw new Error(
15339
- `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.`
15340
15502
  );
15341
15503
  }
15342
15504
  const confirm = options.confirmPrompt ?? promptTrustConfirmation;
@@ -15535,7 +15697,7 @@ function getInstallFollowupNotes(platforms) {
15535
15697
  notes.push("Cursor note: if Cursor is already open, use Developer: Reload Window or restart Cursor to pick up the new install.");
15536
15698
  }
15537
15699
  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.");
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.");
15539
15701
  }
15540
15702
  if (platforms.includes("opencode")) {
15541
15703
  notes.push("OpenCode note: if OpenCode is already open, restart or reload it so the plugin is picked up.");
@@ -15755,11 +15917,159 @@ function getClaudeMarketplaceRoot(pluginName) {
15755
15917
  return resolve15(home, ".claude/plugins/data", getClaudeMarketplaceName(pluginName));
15756
15918
  }
15757
15919
  function resolveInstalledConsumerPath(target, pluginName) {
15758
- if (target.platform === "claude-code") {
15759
- return resolve15(getClaudeMarketplaceRoot(pluginName), "plugins", pluginName);
15920
+ if (target.platform === "claude-code" && pluginName !== "") {
15921
+ return target.pluginDir;
15760
15922
  }
15761
15923
  return target.pluginDir;
15762
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
+ }
15763
16073
  function ensureClaudeMarketplace(pluginName, sourceDir, materialized) {
15764
16074
  const marketplaceName = getClaudeMarketplaceName(pluginName);
15765
16075
  const marketplaceRoot = getClaudeMarketplaceRoot(pluginName);
@@ -15770,12 +16080,11 @@ function ensureClaudeMarketplace(pluginName, sourceDir, materialized) {
15770
16080
  rmSync3(marketplaceRoot, { recursive: true, force: true });
15771
16081
  mkdirSync4(marketplaceManifestDir, { recursive: true });
15772
16082
  mkdirSync4(resolve15(marketplaceRoot, "plugins"), { recursive: true });
16083
+ cpSync3(sourceDir, marketplacePluginDir, { recursive: true });
15773
16084
  if (materialized && materialized.entries.length > 0) {
15774
- cpSync3(sourceDir, marketplacePluginDir, { recursive: true });
15775
16085
  materializeInstalledPlugin(marketplacePluginDir, "claude-code", materialized.config, materialized.entries);
15776
- } else {
15777
- symlinkSync(sourceDir, marketplacePluginDir);
15778
16086
  }
16087
+ assertInstalledBundleIntegrity(marketplacePluginDir, "claude-code", "Claude marketplace bundle");
15779
16088
  writeFileSync4(
15780
16089
  resolve15(marketplaceManifestDir, "marketplace.json"),
15781
16090
  JSON.stringify({
@@ -15825,6 +16134,7 @@ function installClaudePlugin(target, pluginName, runCommand, materialized) {
15825
16134
  if (install.status !== 0) {
15826
16135
  throw new Error(`Failed to install Claude plugin: ${install.stderr || install.stdout}`);
15827
16136
  }
16137
+ assertInstalledBundleIntegrity(target.pluginDir, "claude-code", "Installed Claude plugin bundle");
15828
16138
  }
15829
16139
  function uninstallClaudePlugin(target, pluginName, runCommand, options = {}) {
15830
16140
  const marketplaceName = getClaudeMarketplaceName(pluginName);
@@ -15906,7 +16216,11 @@ async function installPlugin(distDir, pluginName, platforms, options = {}) {
15906
16216
  console.log("Nothing to install. Run `pluxx build` first.");
15907
16217
  } else if (!options.quiet) {
15908
16218
  console.log(`
15909
- 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.");
15910
16224
  for (const note of getInstallFollowupNotes(filtered.map((target) => target.platform))) {
15911
16225
  console.log(note);
15912
16226
  }
@@ -16596,6 +16910,10 @@ function checkInstalledUserConfig(checks, rootDir) {
16596
16910
  const payload = JSON.parse(readFileSync11(resolvedPath, "utf-8"));
16597
16911
  const valueCount = Object.keys(payload.values ?? {}).length;
16598
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
+ ];
16599
16917
  addCheck2(checks, {
16600
16918
  level: "success",
16601
16919
  code: "consumer-user-config-valid",
@@ -16604,6 +16922,16 @@ function checkInstalledUserConfig(checks, rootDir) {
16604
16922
  fix: "No action needed.",
16605
16923
  path: userConfigPath
16606
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
+ }
16607
16935
  } catch (error) {
16608
16936
  addCheck2(checks, {
16609
16937
  level: "error",
@@ -16688,6 +17016,16 @@ function checkInstalledMcpConfig(checks, rootDir, layout) {
16688
17016
  if (servers.length === 0) {
16689
17017
  return;
16690
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
+ }
16691
17029
  const remoteEntries = servers.filter((server) => "url" in server);
16692
17030
  const stdioEntries = servers.filter((server) => "command" in server);
16693
17031
  const inlineHeaderEntries = servers.filter((server) => {
@@ -16749,6 +17087,38 @@ function checkInstalledMcpConfig(checks, rootDir, layout) {
16749
17087
  });
16750
17088
  }
16751
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
+ }
16752
17122
  function findMissingInstalledStdioRuntimePaths(rootDir, stdioEntries) {
16753
17123
  const missing = /* @__PURE__ */ new Set();
16754
17124
  for (const server of stdioEntries) {
@@ -16934,6 +17304,7 @@ async function doctorConsumer(rootDir = process.cwd()) {
16934
17304
  path: layout.manifestPath
16935
17305
  });
16936
17306
  checkConsumerManifest(checks, rootDir, layout);
17307
+ checkInstalledBundleIntegrity(checks, rootDir, layout);
16937
17308
  checkInstalledUserConfig(checks, rootDir);
16938
17309
  checkInstalledEnvValidation(checks, rootDir);
16939
17310
  checkInstalledMcpConfig(checks, rootDir, layout);
@@ -19725,6 +20096,175 @@ echo
19725
20096
  echo "Installed __DISPLAY_NAME__ across ${installerTargets.join(", ")}."
19726
20097
  `.replaceAll("__REPO__", "REPO_PLACEHOLDER").replaceAll("__DISPLAY_NAME__", "DISPLAY_PLACEHOLDER");
19727
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
+ }
19728
20268
  function renderInstallClaudeCodeScript(config) {
19729
20269
  return `#!/usr/bin/env bash
19730
20270
  set -euo pipefail
@@ -19751,6 +20291,7 @@ need_cmd tar
19751
20291
  need_cmd mktemp
19752
20292
  need_cmd grep
19753
20293
  need_cmd sed
20294
+ ${hasInstallerUserConfig(config, "claude-code") ? "need_cmd node" : ""}
19754
20295
 
19755
20296
  if [[ "$SKIP_INSTALL" != "1" ]]; then
19756
20297
  need_cmd curl
@@ -19787,6 +20328,7 @@ DESCRIPTION="$(grep -E '"description"' "$PLUGIN_MANIFEST" | head -n1 | sed -E 's
19787
20328
  mkdir -p "$INSTALL_ROOT/.claude-plugin" "$INSTALL_ROOT/plugins"
19788
20329
  rm -rf "$INSTALL_ROOT/plugins/$PLUGIN_NAME"
19789
20330
  cp -R "$BUNDLE_DIR" "$INSTALL_ROOT/plugins/$PLUGIN_NAME"
20331
+ ${renderInstallerUserConfigSnippet(config, "claude-code", "$INSTALL_ROOT/plugins/$PLUGIN_NAME")}
19790
20332
 
19791
20333
  cat > "$INSTALL_ROOT/.claude-plugin/marketplace.json" <<JSON
19792
20334
  {
@@ -19850,6 +20392,7 @@ need_cmd() {
19850
20392
  need_cmd tar
19851
20393
  need_cmd mktemp
19852
20394
  need_cmd curl
20395
+ ${hasInstallerUserConfig(config, "cursor") ? "need_cmd node" : ""}
19853
20396
 
19854
20397
  TMP_DIR="$(mktemp -d)"
19855
20398
  cleanup() {
@@ -19878,6 +20421,7 @@ fi
19878
20421
  mkdir -p "$(dirname "$INSTALL_DIR")"
19879
20422
  rm -rf "$INSTALL_DIR"
19880
20423
  cp -R "$BUNDLE_DIR" "$INSTALL_DIR"
20424
+ ${renderInstallerUserConfigSnippet(config, "cursor", "$INSTALL_DIR")}
19881
20425
 
19882
20426
  echo "Installed $PLUGIN_NAME to $INSTALL_DIR"
19883
20427
  echo "If Cursor is already open, use Developer: Reload Window or restart Cursor so the plugin is picked up."
@@ -19935,6 +20479,7 @@ fi
19935
20479
  mkdir -p "$(dirname "$INSTALL_DIR")"
19936
20480
  rm -rf "$INSTALL_DIR"
19937
20481
  cp -R "$BUNDLE_DIR" "$INSTALL_DIR"
20482
+ ${renderInstallerUserConfigSnippet(config, "codex", "$INSTALL_DIR")}
19938
20483
 
19939
20484
  mkdir -p "$(dirname "$MARKETPLACE_PATH")"
19940
20485
 
@@ -20049,6 +20594,7 @@ fi
20049
20594
  mkdir -p "$(dirname "$INSTALL_DIR")" "$SKILLS_ROOT"
20050
20595
  rm -rf "$INSTALL_DIR"
20051
20596
  cp -R "$BUNDLE_DIR" "$INSTALL_DIR"
20597
+ ${renderInstallerUserConfigSnippet(config, "opencode", "$INSTALL_DIR")}
20052
20598
 
20053
20599
  export ENTRY_PATH
20054
20600
  export PLUGIN_NAME
@@ -20376,22 +20922,107 @@ function printJson(value) {
20376
20922
  }
20377
20923
 
20378
20924
  // src/cli/verify-install.ts
20379
- import { existsSync as existsSync27 } from "fs";
20925
+ import { existsSync as existsSync27, lstatSync as lstatSync4, readdirSync as readdirSync11, readFileSync as readFileSync15, readlinkSync, realpathSync, statSync as statSync5 } from "fs";
20380
20926
  import { resolve as resolve21 } from "path";
20381
20927
  function buildCheckFromReport(target, pluginName, report) {
20382
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;
20383
20931
  return {
20384
20932
  platform: target.platform,
20385
20933
  installPath: target.pluginDir,
20386
20934
  consumerPath,
20387
20935
  built: target.built,
20388
20936
  installed: existsSync27(consumerPath),
20389
- ok: report.errors === 0,
20390
- errors: report.errors,
20937
+ stale,
20938
+ ...staleReason ? { staleReason } : {},
20939
+ ok: report.errors === 0 && !stale,
20940
+ errors: report.errors + (stale ? 1 : 0),
20391
20941
  warnings: report.warnings,
20392
20942
  infos: report.infos
20393
20943
  };
20394
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
+ }
20395
21026
  async function verifyInstall(config, options = {}) {
20396
21027
  const rootDir = options.rootDir ?? process.cwd();
20397
21028
  const distDir = resolve21(rootDir, config.outDir);
@@ -20419,12 +21050,39 @@ function printVerifyInstallResult(result) {
20419
21050
  console.log(`${prefix} ${check.platform}: ${check.consumerPath}`);
20420
21051
  console.log(` install path: ${check.installPath}`);
20421
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
+ }
20422
21061
  }
20423
21062
  console.log(result.ok ? "pluxx verify-install passed." : "pluxx verify-install failed.");
20424
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
+ }
20425
21083
 
20426
21084
  // src/cli/behavioral.ts
20427
- import { existsSync as existsSync28, readFileSync as readFileSync15 } from "fs";
21085
+ import { existsSync as existsSync28, readFileSync as readFileSync16 } from "fs";
20428
21086
  import { mkdtemp as mkdtemp2, rm as rm3 } from "fs/promises";
20429
21087
  import { tmpdir as tmpdir4 } from "os";
20430
21088
  import { resolve as resolve22 } from "path";
@@ -20466,7 +21124,7 @@ function loadBehavioralCases(rootDir, targets, promptOverride) {
20466
21124
  `No behavioral smoke config found at ${BEHAVIORAL_CONFIG_PATH}. Add that file or pass --behavioral-prompt to define a real example query.`
20467
21125
  );
20468
21126
  }
20469
- const parsed = JSON.parse(readFileSync15(filePath, "utf-8"));
21127
+ const parsed = JSON.parse(readFileSync16(filePath, "utf-8"));
20470
21128
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.cases) || parsed.cases.length === 0) {
20471
21129
  throw new Error(`${BEHAVIORAL_CONFIG_PATH} must contain a non-empty "cases" array.`);
20472
21130
  }
@@ -20576,7 +21234,7 @@ async function executeBehavioralCommand(platform, command2, cwd) {
20576
21234
  child.on("close", (code) => {
20577
21235
  const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
20578
21236
  const stderr = Buffer.concat(stderrChunks).toString("utf-8");
20579
- const codexMessage = codexLastMessagePath && existsSync28(codexLastMessagePath) ? readFileSync15(codexLastMessagePath, "utf-8") : "";
21237
+ const codexMessage = codexLastMessagePath && existsSync28(codexLastMessagePath) ? readFileSync16(codexLastMessagePath, "utf-8") : "";
20580
21238
  resolvePromise({
20581
21239
  exitCode: code ?? 1,
20582
21240
  response: codexMessage.trim() || stdout.trim() || stderr.trim()
@@ -20639,7 +21297,7 @@ function shellQuote2(value) {
20639
21297
  }
20640
21298
 
20641
21299
  // src/cli/discover-installed-mcp.ts
20642
- import { existsSync as existsSync29, readFileSync as readFileSync16 } from "fs";
21300
+ import { existsSync as existsSync29, readFileSync as readFileSync17 } from "fs";
20643
21301
  import { homedir as homedir2 } from "os";
20644
21302
  import { resolve as resolve23 } from "path";
20645
21303
  var INSTALLED_MCP_HOSTS = ["claude-code", "cursor", "codex", "opencode"];
@@ -20733,7 +21391,7 @@ function installedMcpFileCandidates(rootDir, homeDir) {
20733
21391
  }
20734
21392
  function parseJsonMcpFile(path, host) {
20735
21393
  try {
20736
- const raw = JSON.parse(readFileSync16(path, "utf-8"));
21394
+ const raw = JSON.parse(readFileSync17(path, "utf-8"));
20737
21395
  const servers = extractJsonMcpServers(raw, path, host);
20738
21396
  return Object.entries(servers).flatMap(([serverName, config]) => {
20739
21397
  const normalized = normalizeCommonMcpServer(config, host);
@@ -20768,7 +21426,7 @@ function extractJsonMcpServers(raw, path, host) {
20768
21426
  return {};
20769
21427
  }
20770
21428
  function parseCodexTomlMcpFile(path) {
20771
- const text = readFileSync16(path, "utf-8");
21429
+ const text = readFileSync17(path, "utf-8");
20772
21430
  const servers = {};
20773
21431
  let currentServer;
20774
21432
  let currentSubtable;
@@ -21141,7 +21799,7 @@ function isHelpRequested(input) {
21141
21799
  }
21142
21800
  function getCliPackageVersion() {
21143
21801
  const packageJsonPath = new URL("../../package.json", import.meta.url);
21144
- const raw = JSON.parse(readFileSync17(packageJsonPath, "utf-8"));
21802
+ const raw = JSON.parse(readFileSync18(packageJsonPath, "utf-8"));
21145
21803
  if (typeof raw.version !== "string" || raw.version.trim() === "") {
21146
21804
  throw new Error("Unable to determine the installed pluxx version from package.json.");
21147
21805
  }
@@ -21253,20 +21911,21 @@ function planAutopilotPasses(input) {
21253
21911
  };
21254
21912
  }
21255
21913
  function countAutopilotSteps(input) {
21256
- 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);
21257
21915
  }
21258
21916
  function formatAutopilotPassLine(label, decision) {
21259
21917
  return `${label}: ${decision.enabled ? "run" : "skip"} (${decision.reason})`;
21260
21918
  }
21261
21919
  function summarizeAutopilotWorkload(input) {
21262
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" : ""}`;
21263
21922
  if (agentPassCount === 0 && !input.verify) {
21264
- return "deterministic scaffold only";
21923
+ return `deterministic scaffold${input.install ? " + install" : ""}${input.behavioral ? " + behavioral smoke" : ""}`;
21265
21924
  }
21266
21925
  if (agentPassCount === 0) {
21267
- return "deterministic scaffold + verification";
21926
+ return `deterministic scaffold${suffix}`;
21268
21927
  }
21269
- return `${agentPassCount} agent pass${agentPassCount === 1 ? "" : "es"}${input.verify ? " + verification" : ""}`;
21928
+ return `${agentPassCount} agent pass${agentPassCount === 1 ? "" : "es"}${suffix}`;
21270
21929
  }
21271
21930
  function formatDuration(durationMs) {
21272
21931
  if (durationMs === void 0) {
@@ -21509,6 +22168,16 @@ function parseTargetFlagValues(rawArgs2) {
21509
22168
  if (!values) return void 0;
21510
22169
  return parseTargetPlatforms(values.join(","));
21511
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
+ }
21512
22181
  function defaultHookMode(source) {
21513
22182
  if (source.auth?.type && source.auth.type !== "none" && source.auth.envVar) {
21514
22183
  return "safe";
@@ -22607,6 +23276,9 @@ async function runAutopilot() {
22607
23276
  const attach = readOption2(args, "--attach");
22608
23277
  const reviewRequested = args.includes("--review");
22609
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");
22610
23282
  const verboseRunner = args.includes("--verbose-runner");
22611
23283
  const interactive = !runtime.jsonOutput && runtime.isInteractive && !initOptions.assumeDefaults;
22612
23284
  let authEnv = initOptions.authEnv;
@@ -22615,17 +23287,23 @@ async function runAutopilot() {
22615
23287
  let authTemplate = initOptions.authTemplate;
22616
23288
  let runtimeAuthMode = resolveRuntimeAuthMode(initOptions.runtimeAuth);
22617
23289
  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]`);
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]`);
22619
23291
  process.exit(1);
22620
23292
  }
22621
23293
  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]`);
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]`);
22623
23295
  process.exit(1);
22624
23296
  }
22625
23297
  if (modeRaw && !AUTOPILOT_MODES.includes(modeRaw)) {
22626
23298
  console.error(`Autopilot mode must be one of: ${AUTOPILOT_MODES.join(", ")}`);
22627
23299
  process.exit(1);
22628
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
+ }
22629
23307
  let tempDir;
22630
23308
  try {
22631
23309
  if (!runtime.jsonOutput && !runtime.quiet && interactive) {
@@ -22793,6 +23471,7 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22793
23471
  const authorName = initOptions.author ?? (interactive ? await clackText("Author name", defaultAuthorName) : defaultAuthorName);
22794
23472
  const targetsRaw = initOptions.targets ?? (interactive ? await clackText("Platforms (comma-separated)", DEFAULT_INIT_TARGETS.join(",")) : DEFAULT_INIT_TARGETS.join(","));
22795
23473
  const targets = parseTargetPlatforms(targetsRaw);
23474
+ const installTargets = installRequested ? parseInstallTargetFlag(args, targets) : [];
22796
23475
  const grouping = initOptions.grouping ? parseChoiceOption(initOptions.grouping, MCP_SKILL_GROUPINGS, "Skill grouping") : interactive ? await clackSelect("Skill grouping", [
22797
23476
  { value: "workflow", label: "workflow", hint: "Group related tools into workflow skills" },
22798
23477
  { value: "tool", label: "tool", hint: "One skill per tool" }
@@ -22822,7 +23501,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22822
23501
  taxonomy: passDecisions.taxonomy,
22823
23502
  instructions: passDecisions.instructions,
22824
23503
  review: passDecisions.review,
22825
- verify
23504
+ verify,
23505
+ install: installRequested,
23506
+ behavioral: behavioralRequested
22826
23507
  });
22827
23508
  const workspaceRoot = runtime.dryRun ? await mkdtemp3(`${tmpdir5()}/pluxx-autopilot-`) : process.cwd();
22828
23509
  tempDir = runtime.dryRun ? workspaceRoot : void 0;
@@ -22908,6 +23589,12 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22908
23589
  quality,
22909
23590
  review: passDecisions.review.enabled,
22910
23591
  verify,
23592
+ install: installRequested ? {
23593
+ enabled: true,
23594
+ platforms: installTargets,
23595
+ notes: getInstallFollowupNotes(installTargets),
23596
+ installTargets: []
23597
+ } : void 0,
22911
23598
  steps: totalSteps,
22912
23599
  init: {
22913
23600
  createdFiles: initCreatedFiles,
@@ -22955,7 +23642,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22955
23642
  taxonomy: passDecisions.taxonomy,
22956
23643
  instructions: passDecisions.instructions,
22957
23644
  review: passDecisions.review,
22958
- verify
23645
+ verify,
23646
+ install: installRequested,
23647
+ behavioral: behavioralRequested
22959
23648
  })}`);
22960
23649
  console.log(` Quality: ${quality.warnings} warning(s), ${quality.infos} info message(s)`);
22961
23650
  console.log(` Scaffold create/update: ${[...initCreatedFiles, ...initUpdatedFiles].join(", ") || "none"}`);
@@ -22976,6 +23665,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
22976
23665
  } else {
22977
23666
  console.log(" Verification: skipped (--no-verify)");
22978
23667
  }
23668
+ if (installRequested) {
23669
+ console.log(` Install: ${installTargets.join(", ")} after verification`);
23670
+ }
22979
23671
  }
22980
23672
  return;
22981
23673
  }
@@ -23055,9 +23747,13 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
23055
23747
  });
23056
23748
  return result;
23057
23749
  })() : 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;
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;
23061
23757
  const summary = {
23062
23758
  ok,
23063
23759
  pluginName,
@@ -23119,6 +23815,8 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
23119
23815
  },
23120
23816
  verification,
23121
23817
  verificationDurationMs,
23818
+ install,
23819
+ behavioral: behavioralResult,
23122
23820
  failureStage,
23123
23821
  failureMessage
23124
23822
  };
@@ -23134,7 +23832,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
23134
23832
  taxonomy: passDecisions.taxonomy,
23135
23833
  instructions: passDecisions.instructions,
23136
23834
  review: passDecisions.review,
23137
- verify
23835
+ verify,
23836
+ install: installRequested,
23837
+ behavioral: behavioralRequested
23138
23838
  })}`);
23139
23839
  console.log(` Quality: ${quality.warnings} warning(s), ${quality.infos} info message(s)`);
23140
23840
  if (!verboseRunner) {
@@ -23157,6 +23857,23 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
23157
23857
  } else {
23158
23858
  console.log(" Verification: skipped (--no-verify)");
23159
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
+ }
23160
23877
  if (failureStage && failureMessage) {
23161
23878
  console.log(` Failure stage: ${failureStage}`);
23162
23879
  console.log(` Failure detail: ${failureMessage}`);
@@ -23165,6 +23882,9 @@ ${formatAuthRequiredMessage("autopilot", retryError, source)}`);
23165
23882
  console.log(" 1. Review INSTRUCTIONS.md and skills/");
23166
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)" : ""}`);
23167
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
+ }
23168
23888
  }
23169
23889
  if (!ok) {
23170
23890
  process.exit(1);
@@ -23489,6 +24209,10 @@ Common flags:
23489
24209
  --mode quick|standard|thorough Control how much agent refinement autopilot performs
23490
24210
  --approve-mcp-tools Preapprove all tools from the imported MCP in canonical permissions
23491
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
23492
24216
 
23493
24217
  Targets:
23494
24218
  claude-code, cursor, codex, opencode, github-copilot, openhands,
@@ -23531,6 +24255,8 @@ Examples:
23531
24255
  --attach is only supported for the opencode runner
23532
24256
  pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode quick --yes
23533
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
23534
24260
  pluxx autopilot --from-mcp https://example.com/mcp --runner codex --yes --approve-mcp-tools
23535
24261
  pluxx autopilot --from-mcp https://example.com/mcp --runner codex --mode thorough --yes --verbose-runner
23536
24262
  pluxx autopilot --from-mcp https://mcp.linear.app/mcp --runner codex --yes --oauth-wrapper