Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
`<preset>.<name>` 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

Expand Down
7 changes: 6 additions & 1 deletion bin/opencode-presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -104,6 +104,11 @@ async function main(): Promise<void> {
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,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
14 changes: 14 additions & 0 deletions src/cli-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path> --reset=<path>
Expand Down
33 changes: 32 additions & 1 deletion test/cli-args.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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/);
});
});