@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/doctor.d.ts.map +1 -1
- package/dist/cli/eval.d.ts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +773 -45
- package/dist/cli/init-from-mcp.d.ts.map +1 -1
- package/dist/cli/install.d.ts +6 -0
- package/dist/cli/install.d.ts.map +1 -1
- package/dist/cli/publish.d.ts.map +1 -1
- package/dist/cli/verify-install.d.ts +2 -0
- package/dist/cli/verify-install.d.ts.map +1 -1
- package/dist/index.js +621 -9
- package/dist/user-config.d.ts +1 -0
- package/dist/user-config.d.ts.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
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
|
|
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
|
-
|
|
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 ${
|
|
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}
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
20390
|
-
|
|
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
|
|
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(
|
|
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) ?
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
21925
|
+
return `deterministic scaffold${input.install ? " + install" : ""}${input.behavioral ? " + behavioral smoke" : ""}`;
|
|
21265
21926
|
}
|
|
21266
21927
|
if (agentPassCount === 0) {
|
|
21267
|
-
return
|
|
21928
|
+
return `deterministic scaffold${suffix}`;
|
|
21268
21929
|
}
|
|
21269
|
-
return `${agentPassCount} agent pass${agentPassCount === 1 ? "" : "es"}${
|
|
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
|
|
23059
|
-
const
|
|
23060
|
-
const
|
|
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
|