mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-02 05:24:34 +08:00
docs(config): document opener settings (#1945)
* docs(config): document opener settings * test(config): harden opener argument contract
This commit is contained in:
@@ -15,7 +15,7 @@ The CLI keeps its machine-level settings at `~/.config/openspec/config.json` on
|
||||
| `workflows` | list of strings | No | The workflow list a `custom` profile installs |
|
||||
| `featureFlags` | map: flag → boolean | No | Boolean feature toggles |
|
||||
| `defaultStore` | string | No | Machine-level fallback store for root resolution |
|
||||
| `openers` | list | No | The tools worksets open in, and how each is launched |
|
||||
| `openers` | map: tool id → settings | No | The tools worksets open in, and how each is launched |
|
||||
| `telemetry` | map | No | Telemetry opt-out, anonymous id, and notice-seen state |
|
||||
|
||||
### profile
|
||||
@@ -40,7 +40,41 @@ The machine-level fallback store id for root resolution, consulted only when no
|
||||
|
||||
### openers
|
||||
|
||||
The tools a workset can open in, and how each is launched. Entries are hand-edited and validated on use. Each may set `style` (`workspace-file` or `attach-dirs`), `label`, `command`, `args`, and `attach_flag`, and is merged over the built-in defaults.
|
||||
The tools a workset can open in, keyed by tool id. Edit `openers` in the global `config.json` with `openspec config edit` in your terminal.
|
||||
|
||||
| Field | Contract |
|
||||
| --- | --- |
|
||||
| `style` | `workspace-file` or `attach-dirs`. Required for a new tool; optional for a built-in. |
|
||||
| `label` | Non-empty string shown in the tool picker. Defaults to the id for a new tool. |
|
||||
| `command` | Non-empty executable name or path. Defaults to the id for a new tool. Put arguments in `args`, not in this string. |
|
||||
| `args` | Array of strings passed before the workspace file or attach flags. Defaults to `[]` for a new tool. |
|
||||
| `attach_flag` | Non-empty string paired with each member path for `attach-dirs`. Defaults to `--add-dir` for a new tool. Ignored for `workspace-file`. |
|
||||
|
||||
**Built-in overrides:** `code`, `cursor`, `claude`, and `codex` retain any fields you omit. Setting `args` replaces the entire argument list; `[]` clears it.
|
||||
|
||||
**Launch styles:** `workspace-file` passes the generated `.code-workspace` path to the executable. `attach-dirs` passes one flag/path pair per member, including the primary member.
|
||||
|
||||
**Availability:** `attach-dirs` openers, including Claude Code and Codex, are disabled by default. You cannot select or save them with `--tool`, and OpenSpec refuses to open a workset that already names one. Configuration overrides do not enable the `attach-dirs` launch style.
|
||||
|
||||
**Validation:** unknown fields, invalid types, and a new tool without `style` fail when a workset command reads the opener table.
|
||||
|
||||
This example adds VS Code Insiders and passes `--new-window` whenever the built-in VS Code opener launches:
|
||||
|
||||
```json
|
||||
{
|
||||
"openers": {
|
||||
"code-insiders": {
|
||||
"style": "workspace-file",
|
||||
"label": "VS Code Insiders"
|
||||
},
|
||||
"code": {
|
||||
"args": ["--new-window"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The corresponding `code-insiders` or `code` executable must be installed and available on `PATH`.
|
||||
|
||||
### telemetry
|
||||
|
||||
|
||||
@@ -613,24 +613,35 @@ describe('openspec workset (7.1)', () => {
|
||||
});
|
||||
|
||||
describe('opener config', () => {
|
||||
it('adds a new workspace-file tool from config', async () => {
|
||||
writeOpenersConfig({ zed: { style: 'workspace-file' } });
|
||||
await createPlatform(['--tool', 'zed']);
|
||||
const fakeZed = createFakeTool(tempDir, 'zed');
|
||||
it.each([
|
||||
{ tool: 'code-insiders', args: [] },
|
||||
{ tool: 'code', args: ['--new-window'] },
|
||||
])('launches the documented $tool opener configuration', async ({ tool, args }) => {
|
||||
delete process.env.OPENSPEC_ENABLE_CLI_AGENT_OPENERS;
|
||||
writeOpenersConfig({
|
||||
'code-insiders': { style: 'workspace-file', label: 'VS Code Insiders' },
|
||||
code: { args: ['--new-window'] },
|
||||
});
|
||||
const created = await createPlatform(['--tool', tool]);
|
||||
expect(created.exitCode).toBe(0);
|
||||
const fakeEditor = createFakeTool(tempDir, tool);
|
||||
|
||||
const result = await runCLI(['workset', 'open', 'platform'], {
|
||||
cwd: tempDir,
|
||||
env: envWithFakeTools(env, [fakeZed]),
|
||||
env: envWithFakeTools(env, [fakeEditor]),
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(readLaunchLog(fakeZed.logPath).args).toEqual([
|
||||
getWorksetCodeWorkspacePath('platform', pathOptions()),
|
||||
]);
|
||||
expect(readLaunchLog(fakeEditor.logPath)).toEqual({
|
||||
cwd: memberA,
|
||||
args: [...args, getWorksetCodeWorkspacePath('platform', pathOptions())],
|
||||
});
|
||||
});
|
||||
|
||||
it('renaming an attach flag is a one-line local fix', async () => {
|
||||
writeOpenersConfig({ claude: { attach_flag: '--dir' } });
|
||||
it('passes configured args before configured attach pairs', async () => {
|
||||
writeOpenersConfig({
|
||||
claude: { args: ['--model', 'opus'], attach_flag: '--dir' },
|
||||
});
|
||||
await createPlatform(['--tool', 'claude']);
|
||||
const fakeClaude = createFakeTool(tempDir, 'claude');
|
||||
|
||||
@@ -641,6 +652,8 @@ describe('openspec workset (7.1)', () => {
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(readLaunchLog(fakeClaude.logPath).args).toEqual([
|
||||
'--model',
|
||||
'opus',
|
||||
'--dir',
|
||||
memberA,
|
||||
'--dir',
|
||||
|
||||
@@ -71,6 +71,17 @@ describe('openers core', () => {
|
||||
expect(claude?.style).toBe('attach-dirs');
|
||||
});
|
||||
|
||||
it.each([{ args: [] }, { args: ['--model', 'custom model'] }])(
|
||||
'replaces built-in args with $args without changing omitted fields',
|
||||
({ args }) => {
|
||||
const builtin = findOpener([...BUILTIN_OPENERS], 'codex')!;
|
||||
const table = mergeOpenerTable({ codex: { args } }, CONFIG_PATH);
|
||||
|
||||
expect(findOpener(table, 'codex')).toEqual({ ...builtin, args });
|
||||
expect(builtin.args).toEqual(['--sandbox', 'workspace-write']);
|
||||
}
|
||||
);
|
||||
|
||||
it('rejects an unknown style naming the two valid styles', () => {
|
||||
try {
|
||||
mergeOpenerTable({ vim: { style: 'tabs' } }, CONFIG_PATH);
|
||||
@@ -98,6 +109,9 @@ describe('openers core', () => {
|
||||
expect(() =>
|
||||
mergeOpenerTable({ zed: { style: 'workspace-file', extra: 1 } }, CONFIG_PATH)
|
||||
).toThrowError(/Invalid openers config/);
|
||||
expect(() =>
|
||||
mergeOpenerTable({ code: { args: ['--wait', 1] } }, CONFIG_PATH)
|
||||
).toThrowError(/Invalid openers config/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user