From 9147821075a833b6bdb5bbb5f184726267f694ed Mon Sep 17 00:00:00 2001 From: trick77 Date: Mon, 11 May 2026 08:48:39 +0200 Subject: [PATCH 1/2] 0.5.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a8968bf..2a02948 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "opencode-presets", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opencode-presets", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "dependencies": { "ajv": "8.20.0", diff --git a/package.json b/package.json index 5d9cf6a..21eeef0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opencode-presets", - "version": "0.4.0", + "version": "0.5.0", "description": "Interactive CLI that patches opencode.json with curated config presets — LSP, MCP, permissions.", "type": "module", "bin": { From 66ed72709a62719b8a541c669f670d2a8a625e87 Mon Sep 17 00:00:00 2001 From: trick77 Date: Mon, 11 May 2026 08:52:42 +0200 Subject: [PATCH 2/2] feat(install): reject --set with multi-preset install --set / --set-env target a single preset. Bundling them with multiple confs led to confusing behavior: only one preset's prompts got filled, and the rest would block on a readline that never gets input in a non-TTY shell script. New validateInstallPolicy guard rejects the combination with a clear error before anything is written. --- README.md | 10 ++++++---- bin/opencode-presets.ts | 7 ++++++- src/cli-args.ts | 14 ++++++++++++++ test/cli-args.test.ts | 33 ++++++++++++++++++++++++++++++++- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fc2710b..fd8a372 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,12 @@ opencode-presets install mcp-http \ --set-env headerValue=BITBUCKET_TOKEN ``` -When installing several modules in one call, scope a value with -`.` if the same prompt name appears in more than one -of them: `--set mcp-http.name=openrag`. Unscoped values across -ambiguous prompts are rejected with a clear error. +`--set` / `--set-env` apply to a single preset per invocation — run +the command once per preset rather than bundling several with shared +flags. This keeps the wiring obvious ("this `--set` goes to *that* +preset") and avoids surprise: in a non-TTY shell script, a bundled +install would happily fill the first preset's prompts and then hang +on a readline for the next. ## Built-in presets diff --git a/bin/opencode-presets.ts b/bin/opencode-presets.ts index f8021c9..5fb8f81 100755 --- a/bin/opencode-presets.ts +++ b/bin/opencode-presets.ts @@ -10,7 +10,7 @@ import { backup } from '../src/backup.js'; import { c, confirm, closeUi } from '../src/ui.js'; import { listConfs } from '../src/list.js'; import { runBatch, runRemoveBatch } from '../src/batch.js'; -import { parseInstallArgs, CliArgsError } from '../src/cli-args.js'; +import { parseInstallArgs, validateInstallPolicy, CliArgsError } from '../src/cli-args.js'; import { validateAgainstSchema } from '../src/validate.js'; const CACHE_DIR = process.env.OPENCODE_PRESETS_CACHE @@ -104,6 +104,11 @@ async function main(): Promise { printUsage(); process.exit(1); } + const policyError = validateInstallPolicy(parsed); + if (policyError !== null) { + console.error(c.err('error: ') + policyError); + process.exit(1); + } const resolved = await Promise.all(confPaths.map(resolveConfArg)); await runBatch({ resets, diff --git a/src/cli-args.ts b/src/cli-args.ts index 1758245..4dd41b8 100644 --- a/src/cli-args.ts +++ b/src/cli-args.ts @@ -28,6 +28,20 @@ export class CliArgsError extends Error { } } +// Policy check separate from syntax parsing: --set / --set-env target a +// single preset, so combining them with a multi-preset install is rejected. +// Returns null if the args are policy-valid, or an error message otherwise. +export function validateInstallPolicy(parsed: InstallArgs): string | null { + if (parsed.setValues.length > 0 && parsed.confPaths.length !== 1) { + if (parsed.confPaths.length === 0) { + return '--set / --set-env requires a preset to install.'; + } + return '--set / --set-env requires installing exactly one preset; got ' + + `${parsed.confPaths.length}. Run the command once per preset, or drop --set.`; + } + return null; +} + // args is everything after `install` (so --reset, --set, --set-env, and conf paths). // Forms accepted: // --reset --reset= diff --git a/test/cli-args.test.ts b/test/cli-args.test.ts index 1509a62..11c3086 100644 --- a/test/cli-args.test.ts +++ b/test/cli-args.test.ts @@ -1,6 +1,6 @@ import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; -import { parseInstallArgs, CliArgsError } from '../src/cli-args.js'; +import { parseInstallArgs, validateInstallPolicy, CliArgsError } from '../src/cli-args.js'; describe('parseInstallArgs — existing behavior', () => { test('plain conf paths', () => { @@ -141,3 +141,34 @@ describe('parseInstallArgs — mixed', () => { assert.equal(r.setValues.length, 2); }); }); + +describe('validateInstallPolicy', () => { + test('null when no --set is used (any number of presets)', () => { + assert.equal(validateInstallPolicy(parseInstallArgs(['a', 'b', 'c'])), null); + assert.equal(validateInstallPolicy(parseInstallArgs([])), null); + }); + + test('null when --set targets exactly one preset', () => { + const r = parseInstallArgs(['mcp-http', '--set', 'name=x']); + assert.equal(validateInstallPolicy(r), null); + }); + + test('null when --set + one preset + --reset', () => { + const r = parseInstallArgs(['--reset', 'mcp.foo', 'mcp-http', '--set', 'name=x']); + assert.equal(validateInstallPolicy(r), null); + }); + + test('errors when --set is used with multiple presets', () => { + const r = parseInstallArgs(['mcp-http', 'mcp-http-noauth', '--set', 'name=x']); + const err = validateInstallPolicy(r); + assert.ok(err, 'expected an error'); + assert.match(err!, /exactly one preset/); + }); + + test('errors when --set is used with no presets', () => { + const r = parseInstallArgs(['--set', 'name=x']); + const err = validateInstallPolicy(r); + assert.ok(err, 'expected an error'); + assert.match(err!, /requires a preset/); + }); +});