Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
89 changes: 89 additions & 0 deletions openspec/changes/add-config-command/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
## Context

The `global-config` spec defines how OpenSpec reads/writes `config.json`, but users currently must edit it by hand. This command provides a CLI interface to that config.

## Goals / Non-Goals

**Goals:**
- Provide a discoverable CLI for config management
- Support scripting with machine-readable output
- Validate config changes with zod schema
- Handle nested keys gracefully

**Non-Goals:**
- Project-local config (reserved for future via `--scope` flag)
- Complex queries (JSONPath, filtering)
- Config file format migration

## Decisions

### Key Naming: camelCase with Dot Notation

**Decision:** Keys use camelCase matching the JSON structure, with dot notation for nesting.

**Rationale:**
- Matches the actual JSON keys (no translation layer)
- Dot notation is intuitive and widely used (lodash, jq, kubectl)
- Avoids complexity of supporting multiple casing styles

**Examples:**
```bash
openspec config get featureFlags # Returns object
openspec config get featureFlags.experimental # Returns nested value
openspec config set featureFlags.newFlag true
```

### Type Coercion: Auto-detect with `--string` Override

**Decision:** Parse values automatically; provide `--string` flag to force string storage.

**Rationale:**
- Most intuitive for common cases (`true`, `false`, `123`)
- Explicit override for edge cases (storing literal string "true")
- Follows npm/yarn config patterns

**Coercion rules:**
| Input | Stored As |
|-------|-----------|
| `true`, `false` | boolean |
| Numeric string (`123`, `3.14`) | number |
| Everything else | string |
| Any value with `--string` | string |

### Output Format: Raw by Default

**Decision:** `get` prints raw value only. `list` prints YAML-like format by default, JSON with `--json`.

**Rationale:**
- Raw output enables piping: `VAR=$(openspec config get key)`
- YAML-like is human-readable for inspection
- JSON for automation/scripting

### Schema Validation: Zod with Unknown Field Passthrough

**Decision:** Use zod for validation but preserve unknown fields per `global-config` spec.

**Rationale:**
- Type safety for known fields
- Forward compatibility (old CLI doesn't break new config)
- Follows existing `global-config` spec requirement

### Reserved Flag: `--scope`

**Decision:** Reserve `--scope global|project` but only implement `global` initially.

**Rationale:**
- Avoids breaking change if project-local config is added later
- Clear error message if someone tries `--scope project`

## Risks / Trade-offs

| Risk | Mitigation |
|------|------------|
| Dot notation conflicts with keys containing dots | Rare in practice; document limitation |
| Type coercion surprises | `--string` escape hatch; document rules |
| $EDITOR not set | Check and provide helpful error message |

## Open Questions

None - design is straightforward.
43 changes: 32 additions & 11 deletions openspec/changes/add-config-command/proposal.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,60 @@
## Why

Users need a way to view and modify their global OpenSpec settings without manually editing JSON files. The `add-global-config-dir` change provides the foundation, but there's no user-facing interface to interact with the config. A dedicated `openspec config` command provides discoverability and ease of use.
Users need a way to view and modify their global OpenSpec settings without manually editing JSON files. The `global-config` spec provides the foundation, but there's no user-facing interface to interact with the config. A dedicated `openspec config` command provides discoverability and ease of use.

## What Changes

Add `openspec config` subcommand with the following operations:

```bash
openspec config path # Show config file location
openspec config list # Show all current settings
openspec config get <key> # Get a specific value
openspec config set <key> <value> # Set a value
openspec config reset [key] # Reset to defaults (all or specific key)
openspec config path # Show config file location
openspec config list [--json] # Show all current settings
openspec config get <key> # Get a specific value (raw, scriptable)
openspec config set <key> <value> [--string] # Set a value (auto-coerce types)
openspec config unset <key> # Remove a key (revert to default)
openspec config reset --all [-y] # Reset everything to defaults
openspec config edit # Open config in $EDITOR
```

**Key design decisions:**
- **Key naming**: Use camelCase to match JSON structure (e.g., `featureFlags.someFlag`)
- **Nested keys**: Support dot notation for nested access
- **Type coercion**: Auto-detect types by default; `--string` flag forces string storage
- **Scriptable output**: `get` prints raw value only (no labels) for easy piping
- **Zod validation**: Use zod for config schema validation and type safety
- **Future-proofing**: Reserve `--scope global|project` flag for potential project-local config

**Example usage:**
```bash
$ openspec config path
/Users/me/.config/openspec/config.json

$ openspec config list
enableTelemetry: true
featureFlags: {}

$ openspec config set enableTelemetry false
Set enableTelemetry = false
$ openspec config set featureFlags.enableTelemetry false
Set featureFlags.enableTelemetry = false

$ openspec config get enableTelemetry
$ openspec config get featureFlags.enableTelemetry
false

$ openspec config list --json
{
"featureFlags": {}
}

$ openspec config unset featureFlags.enableTelemetry
Unset featureFlags.enableTelemetry (reverted to default)

$ openspec config edit
# Opens $EDITOR with config.json
```

## Impact

- Affected specs: New `cli-config` capability
- Affected code:
- New `src/commands/config.ts`
- New `src/core/config-schema.ts` (zod schema)
- Update CLI entry point to register config command
- Dependencies: Requires `add-global-config-dir` to be implemented first
- Dependencies: Requires `global-config` spec (already implemented)
213 changes: 213 additions & 0 deletions openspec/changes/add-config-command/specs/cli-config/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# cli-config Specification

## Purpose

Provide a CLI interface for viewing and modifying global OpenSpec configuration. Enables users to manage settings without manually editing JSON files, with support for scripting and automation.

## ADDED Requirements

### Requirement: Command Structure

The config command SHALL provide subcommands for all configuration operations.

#### Scenario: Available subcommands

- **WHEN** user executes `openspec config --help`
- **THEN** display available subcommands:
- `path` - Show config file location
- `list` - Show all current settings
- `get <key>` - Get a specific value
- `set <key> <value>` - Set a value
- `unset <key>` - Remove a key (revert to default)
- `reset` - Reset configuration to defaults
- `edit` - Open config in editor

### Requirement: Config Path

The config command SHALL display the config file location.

#### Scenario: Show config path

- **WHEN** user executes `openspec config path`
- **THEN** print the absolute path to the config file
- **AND** exit with code 0

### Requirement: Config List

The config command SHALL display all current configuration values.

#### Scenario: List config in human-readable format

- **WHEN** user executes `openspec config list`
- **THEN** display all config values in YAML-like format
- **AND** show nested objects with indentation

#### Scenario: List config as JSON

- **WHEN** user executes `openspec config list --json`
- **THEN** output the complete config as valid JSON
- **AND** output only JSON (no additional text)

### Requirement: Config Get

The config command SHALL retrieve specific configuration values.

#### Scenario: Get top-level key

- **WHEN** user executes `openspec config get <key>` with a valid top-level key
- **THEN** print the raw value only (no labels or formatting)
- **AND** exit with code 0

#### Scenario: Get nested key with dot notation

- **WHEN** user executes `openspec config get featureFlags.someFlag`
- **THEN** traverse the nested structure using dot notation
- **AND** print the value at that path

#### Scenario: Get non-existent key

- **WHEN** user executes `openspec config get <key>` with a key that does not exist
- **THEN** print nothing (empty output)
- **AND** exit with code 1

#### Scenario: Get object value

- **WHEN** user executes `openspec config get <key>` where the value is an object
- **THEN** print the object as JSON

### Requirement: Config Set

The config command SHALL set configuration values with automatic type coercion.

#### Scenario: Set string value

- **WHEN** user executes `openspec config set <key> <value>`
- **AND** value does not match boolean or number patterns
- **THEN** store value as a string
- **AND** display confirmation message

#### Scenario: Set boolean value

- **WHEN** user executes `openspec config set <key> true` or `openspec config set <key> false`
- **THEN** store value as boolean (not string)
- **AND** display confirmation message

#### Scenario: Set numeric value

- **WHEN** user executes `openspec config set <key> <value>`
- **AND** value is a valid number (integer or float)
- **THEN** store value as number (not string)

#### Scenario: Force string with --string flag

- **WHEN** user executes `openspec config set <key> <value> --string`
- **THEN** store value as string regardless of content
- **AND** this allows storing literal "true" or "123" as strings

#### Scenario: Set nested key

- **WHEN** user executes `openspec config set featureFlags.newFlag true`
- **THEN** create intermediate objects if they don't exist
- **AND** set the value at the nested path

### Requirement: Config Unset

The config command SHALL remove configuration overrides.

#### Scenario: Unset existing key

- **WHEN** user executes `openspec config unset <key>`
- **AND** the key exists in the config
- **THEN** remove the key from the config file
- **AND** the value reverts to its default
- **AND** display confirmation message

#### Scenario: Unset non-existent key

- **WHEN** user executes `openspec config unset <key>`
- **AND** the key does not exist in the config
- **THEN** display message indicating key was not set
- **AND** exit with code 0

### Requirement: Config Reset

The config command SHALL reset configuration to defaults.

#### Scenario: Reset all with confirmation

- **WHEN** user executes `openspec config reset --all`
- **THEN** prompt for confirmation before proceeding
- **AND** if confirmed, delete the config file or reset to defaults
- **AND** display confirmation message

#### Scenario: Reset all with -y flag

- **WHEN** user executes `openspec config reset --all -y`
- **THEN** reset without prompting for confirmation

#### Scenario: Reset without --all flag

- **WHEN** user executes `openspec config reset` without `--all`
- **THEN** display error indicating `--all` is required
- **AND** exit with code 1

### Requirement: Config Edit

The config command SHALL open the config file in the user's editor.

#### Scenario: Open editor successfully

- **WHEN** user executes `openspec config edit`
- **AND** `$EDITOR` or `$VISUAL` environment variable is set
- **THEN** open the config file in that editor
- **AND** create the config file with defaults if it doesn't exist
- **AND** wait for the editor to close before returning

#### Scenario: No editor configured

- **WHEN** user executes `openspec config edit`
- **AND** neither `$EDITOR` nor `$VISUAL` is set
- **THEN** display error message suggesting to set `$EDITOR`
- **AND** exit with code 1

### Requirement: Key Naming Convention

The config command SHALL use camelCase keys matching the JSON structure.

#### Scenario: Keys match JSON structure

- **WHEN** accessing configuration keys via CLI
- **THEN** use camelCase matching the actual JSON property names
- **AND** support dot notation for nested access (e.g., `featureFlags.someFlag`)

### Requirement: Schema Validation

The config command SHALL validate configuration writes against the config schema using zod, while allowing unknown fields for forward compatibility.

#### Scenario: Unknown key accepted

- **WHEN** user executes `openspec config set someFutureKey 123`
- **THEN** the value is saved successfully
- **AND** exit with code 0

#### Scenario: Invalid feature flag value rejected

- **WHEN** user executes `openspec config set featureFlags.someFlag notABoolean`
- **THEN** display a descriptive error message
- **AND** do not modify the config file
- **AND** exit with code 1

### Requirement: Reserved Scope Flag

The config command SHALL reserve the `--scope` flag for future extensibility.

#### Scenario: Scope flag defaults to global

- **WHEN** user executes any config command without `--scope`
- **THEN** operate on global configuration (default behavior)

#### Scenario: Project scope not yet implemented

- **WHEN** user executes `openspec config --scope project <subcommand>`
- **THEN** display error message: "Project-local config is not yet implemented"
- **AND** exit with code 1
Loading
Loading