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
39 changes: 39 additions & 0 deletions openspec/changes/add-config-command/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
## 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.

## 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)
```

**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 get enableTelemetry
false
```

## Impact

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

OpenSpec needs a standard location for user-level configuration that works across platforms and follows established conventions. This will serve as the foundation for settings, feature flags, and future artifacts like workflows or templates.

## Goals / Non-Goals

**Goals:**
- Provide a single, well-defined location for global config
- Follow XDG Base Directory Specification (widely adopted by CLI tools)
- Support cross-platform usage (Unix, macOS, Windows)
- Keep implementation minimal - just the foundation
- Enable future expansion (cache, state, workflows)

**Non-Goals:**
- Project-local config override (not in scope)
- Config file migration tooling
- Config validation CLI commands
- Multiple config profiles

## Decisions

### Path Resolution Strategy

**Decision:** Use XDG Base Directory Specification with platform fallbacks.

```
Unix/macOS: $XDG_CONFIG_HOME/openspec/ or ~/.config/openspec/
Windows: %APPDATA%/openspec/
```

**Rationale:**
- XDG is the de facto standard for CLI tools (used by gh, bat, ripgrep, etc.)
- Environment variable override allows user customization
- Windows uses its native convention (%APPDATA%) for better integration

**Alternatives considered:**
- `~/.openspec/` - Simple but clutters home directory
- `~/Library/Application Support/` on macOS - Overkill for a CLI tool

### Config File Format

**Decision:** JSON (`config.json`)

**Rationale:**
- Native Node.js support (no dependencies)
- Human-readable and editable
- Type-safe with TypeScript
- Matches project.md's "minimal dependencies" principle

**Alternatives considered:**
- YAML - Requires dependency, more error-prone to edit
- TOML - Less common in Node.js ecosystem
- Environment variables only - Too limited for structured settings

### Config Schema

**Decision:** Flat structure with typed fields, start minimal.

```typescript
interface GlobalConfig {
featureFlags?: Record<string, boolean>;
}
```

**Rationale:**
- `featureFlags` enables controlled rollout of new features
- Optional fields with defaults avoid breaking changes
- Flat structure is easy to understand and extend

### Loading Strategy

**Decision:** Read from disk on each call, no caching.

```typescript
export function getGlobalConfig(): GlobalConfig {
return loadConfigFromDisk();
}
```

**Rationale:**
- CLI commands are short-lived; caching adds complexity without benefit
- Reading a small JSON file is ~1ms; negligible overhead
- Always returns fresh data; no cache invalidation concerns
- Simpler implementation

### Directory Creation

**Decision:** Create directory only when saving, not when reading.

**Rationale:**
- Don't create empty directories on read operations
- Users who never save config won't have unnecessary directories
- Aligns with principle of least surprise

## Risks / Trade-offs

| Risk | Mitigation |
|------|------------|
| Config file corruption | Return defaults on parse error, log warning |
| Permissions issues | Check write permissions before save, clear error message |
| Future schema changes | Use optional fields, add version field if needed later |

## Open Questions

None - this proposal is intentionally minimal.
20 changes: 20 additions & 0 deletions openspec/changes/add-global-config-dir/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## Why

OpenSpec currently has no mechanism for user-level global settings or feature flags. As the CLI grows, we need a standard location to store user preferences, experimental features, and other configuration that persists across projects. Following XDG Base Directory Specification provides a well-understood, cross-platform approach.

## What Changes

- Add new `src/core/global-config.ts` module with:
- Path resolution following XDG Base Directory spec (`$XDG_CONFIG_HOME/openspec/` or fallback)
- Cross-platform support (Unix, macOS, Windows)
- Lazy config loading with sensible defaults
- TypeScript types for config shape
- Export a global config directory path getter for future use (workflows, templates, cache)
- Initial config schema supports 1-2 settings/feature flags only

## Impact

- Affected specs: New `global-config` capability (no existing specs modified)
- Affected code:
- New `src/core/global-config.ts`
- Update `src/core/index.ts` to export new module
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
## ADDED Requirements

### Requirement: Global Config Directory Path

The system SHALL resolve the global configuration directory path following XDG Base Directory Specification with platform-specific fallbacks.

#### Scenario: Unix/macOS with XDG_CONFIG_HOME set
- **WHEN** `$XDG_CONFIG_HOME` environment variable is set to `/custom/config`
- **THEN** `getGlobalConfigDir()` returns `/custom/config/openspec`

#### Scenario: Unix/macOS without XDG_CONFIG_HOME
- **WHEN** `$XDG_CONFIG_HOME` environment variable is not set
- **AND** the platform is Unix or macOS
- **THEN** `getGlobalConfigDir()` returns `~/.config/openspec` (expanded to absolute path)

#### Scenario: Windows platform
- **WHEN** the platform is Windows
- **AND** `%APPDATA%` is set to `C:\Users\User\AppData\Roaming`
- **THEN** `getGlobalConfigDir()` returns `C:\Users\User\AppData\Roaming\openspec`

### Requirement: Global Config Loading

The system SHALL load global configuration from the config directory with sensible defaults when the config file does not exist or cannot be parsed.

#### Scenario: Config file exists and is valid
- **WHEN** `config.json` exists in the global config directory
- **AND** the file contains valid JSON matching the config schema
- **THEN** `getGlobalConfig()` returns the parsed configuration

#### Scenario: Config file does not exist
- **WHEN** `config.json` does not exist in the global config directory
- **THEN** `getGlobalConfig()` returns the default configuration
- **AND** no directory or file is created

#### Scenario: Config file is invalid JSON
- **WHEN** `config.json` exists but contains invalid JSON
- **THEN** `getGlobalConfig()` returns the default configuration
- **AND** a warning is logged to stderr

### Requirement: Global Config Saving

The system SHALL save global configuration to the config directory, creating the directory if it does not exist.

#### Scenario: Save config to new directory
- **WHEN** `saveGlobalConfig(config)` is called
- **AND** the global config directory does not exist
- **THEN** the directory is created
- **AND** `config.json` is written with the provided configuration

#### Scenario: Save config to existing directory
- **WHEN** `saveGlobalConfig(config)` is called
- **AND** the global config directory already exists
- **THEN** `config.json` is written (overwriting if exists)

### Requirement: Default Configuration

The system SHALL provide a default configuration that is used when no config file exists.

#### Scenario: Default config structure
- **WHEN** no config file exists
- **THEN** the default configuration includes an empty `featureFlags` object

### Requirement: Config Schema Evolution

The system SHALL merge loaded configuration with default values to ensure new config fields are available even when loading older config files.

#### Scenario: Config file missing new fields
- **WHEN** `config.json` exists with `{ "featureFlags": {} }`
- **AND** the current schema includes a new field `defaultAiTool`
- **THEN** `getGlobalConfig()` returns `{ featureFlags: {}, defaultAiTool: <default> }`
- **AND** the loaded values take precedence over defaults for fields that exist in both

#### Scenario: Config file has extra unknown fields
- **WHEN** `config.json` contains fields not in the current schema
- **THEN** the unknown fields are preserved in the returned configuration
- **AND** no error or warning is raised
25 changes: 25 additions & 0 deletions openspec/changes/add-global-config-dir/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## 1. Core Implementation

- [ ] 1.1 Create `src/core/global-config.ts` with path resolution
- Implement `getGlobalConfigDir()` following XDG spec
- Support `$XDG_CONFIG_HOME` environment variable override
- Platform-specific fallbacks (Unix: `~/.config/`, Windows: `%APPDATA%`)
- [ ] 1.2 Define TypeScript interfaces for config shape
- `GlobalConfig` interface with optional fields
- Start minimal: just `featureFlags?: Record<string, boolean>`
- [ ] 1.3 Implement config loading with defaults
- `getGlobalConfig()` - reads config.json if exists, merges with defaults
- No directory/file creation on read (lazy initialization)
- [ ] 1.4 Implement config saving
- `saveGlobalConfig(config)` - writes config.json, creates directory if needed

## 2. Integration

- [ ] 2.1 Export new module from `src/core/index.ts`
- [ ] 2.2 Add constants for config file name and directory name

## 3. Testing

- [ ] 3.1 Manual testing of path resolution on current platform
- [ ] 3.2 Test with/without `$XDG_CONFIG_HOME` set
- [ ] 3.3 Test config load when file doesn't exist (should return defaults)
Loading