fix(plugins): retry claude plugin install|update without -y on an old CLI

An older Claude Code has no -y on `plugin install|update` and exits with
"error: unknown option '-y'", so the dashboard's Update now chain failed at
its plugin_update step (and so did Update all, the plugin buttons and the
superpowers auto-install). Those versions also predate the confirmation
prompt -y answers, so every wrapper now retries the same argv without it
when, and only when, the CLI rejects the flag. /agenthub-update, which the
failure message points users to, falls back the same way.
This commit is contained in:
2026-09-23 16:46:20 +07:00
parent 764b967856
commit aff863c69f
7 changed files with 79 additions and 3 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -17,7 +17,9 @@ if [ -z "$MARKETPLACE" ]; then
echo "agenthub is not installed as a plugin (checkout install?) - nothing to update this way."
else
claude plugin marketplace update "$MARKETPLACE"
claude plugin update "agenthub@$MARKETPLACE" -y
# An older Claude Code has no `-y` (and no prompt for it to answer).
claude plugin update "agenthub@$MARKETPLACE" -y 2>/dev/null ||
claude plugin update "agenthub@$MARKETPLACE"
fi
```
+15
View File
@@ -83,6 +83,21 @@ function resolveClaude() {
}
function run(cmd, args, timeout) {
try {
return runOnce(cmd, args, timeout);
} catch (err) {
// An older Claude Code rejects `-y` ("unknown option") and never prompts,
// so the same command without the flag is the right retry.
if (!args.includes("-y") || !/unknown option '-y'/.test(String(err.stderr || ""))) throw err;
return runOnce(
cmd,
args.filter((a) => a !== "-y"),
timeout
);
}
}
function runOnce(cmd, args, timeout) {
log(`$ ${cmd} ${args.join(" ")}`);
const out = execFileSync(cmd, args, {
encoding: "utf8",
+15
View File
@@ -102,6 +102,21 @@ function findNewRoot(exclude) {
}
function run(cmd, args, timeout) {
try {
return runOnce(cmd, args, timeout);
} catch (err) {
// An older Claude Code rejects `-y` ("unknown option") and never prompts,
// so the same command without the flag is the right retry.
if (!args.includes("-y") || !/unknown option '-y'/.test(String(err.stderr || ""))) throw err;
return runOnce(
cmd,
args.filter((a) => a !== "-y"),
timeout
);
}
}
function runOnce(cmd, args, timeout) {
log(`$ ${cmd} ${args.join(" ")}`);
const out = execFileSync(cmd, args, {
encoding: "utf8",
+16
View File
@@ -116,6 +116,22 @@ describe("cc-plugin-cli argv construction", () => {
}
});
it("retries without -y on a Claude Code too old to know the flag", async () => {
// Older CLIs answer `error: unknown option '-y'` and exit 1; they also
// never prompt, so the same command without the flag is the right retry.
const restore = stubClaude(
'for a in "$@"; do [ "$a" = "-y" ] && { echo "error: unknown option \'-y\'" >&2; exit 1; }; done\n' +
'for a in "$@"; do echo "$a"; done'
);
try {
const r = await cli.pluginUpdate({ spec: "demo@m" });
assert.equal(r.ok, true);
assert.deepEqual(argvOf(r), ["plugin", "update", "demo@m"]);
} finally {
restore();
}
});
it("passes -y on install so a non-TTY run never waits for a prompt", async () => {
const restore = stubClaude();
try {
+17 -1
View File
@@ -130,7 +130,23 @@ function serialize(fn) {
* @param {{timeoutMs?:number}} [opts]
* @returns {Promise<{ok:boolean, code:number|null, stdout:string, stderr:string, args:string[]}>}
*/
function runClaude(args, opts = {}) {
// A Claude Code older than `plugin install|update -y` rejects the flag outright.
// It also predates the confirmation prompt the flag answers, so the same
// command without it is the correct retry, not a weaker one.
const NO_YES_FLAG = /unknown option '-y'/;
async function runClaude(args, opts = {}) {
const result = await runClaudeOnce(args, opts);
if (!result.ok && args.includes("-y") && NO_YES_FLAG.test(result.stderr)) {
return runClaudeOnce(
args.filter((a) => a !== "-y"),
opts
);
}
return result;
}
function runClaudeOnce(args, opts = {}) {
const timeout = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
return new Promise((resolve, reject) => {
// Only pin CLAUDE_CONFIG_DIR when the operator explicitly set CLAUDE_HOME
+12
View File
@@ -128,6 +128,18 @@ function missingSkillPlugins() {
* @returns {{ok: boolean, output: string}}
*/
function runClaudePlugin(args, timeoutMs) {
const result = runClaudePluginOnce(args, timeoutMs);
// An older Claude Code rejects `-y` and never prompts; retry without it.
if (!result.ok && args.includes("-y") && /unknown option '-y'/.test(result.output)) {
return runClaudePluginOnce(
args.filter((a) => a !== "-y"),
timeoutMs
);
}
return result;
}
function runClaudePluginOnce(args, timeoutMs) {
const env = { ...process.env };
if (process.env.CLAUDE_HOME) env.CLAUDE_CONFIG_DIR = getClaudeHome();
const result = spawnSync("claude", args, {