@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/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 +771 -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,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
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
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
|
|
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
|
-
|
|
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 ${
|
|
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}
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
20390
|
-
|
|
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
|
|
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(
|
|
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) ?
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
21923
|
+
return `deterministic scaffold${input.install ? " + install" : ""}${input.behavioral ? " + behavioral smoke" : ""}`;
|
|
21265
21924
|
}
|
|
21266
21925
|
if (agentPassCount === 0) {
|
|
21267
|
-
return
|
|
21926
|
+
return `deterministic scaffold${suffix}`;
|
|
21268
21927
|
}
|
|
21269
|
-
return `${agentPassCount} agent pass${agentPassCount === 1 ? "" : "es"}${
|
|
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
|
|
23059
|
-
const
|
|
23060
|
-
const
|
|
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
|