Skip to content

Commit 4ff8930

Browse files
authored
feat(spec): add XDG global config directory and config command proposals (#376)
Create two OpenSpec change proposals: 1. add-global-config-dir: Foundation for user-level configuration following XDG Base Directory Specification with cross-platform support 2. add-config-command: User-facing CLI command for viewing and managing global settings Both proposals are minimal and focused on providing a clean, extensible base for OpenSpec settings and future feature flags.
1 parent cefb471 commit 4ff8930

File tree

5 files changed

+265
-0
lines changed

5 files changed

+265
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
## Why
2+
3+
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.
4+
5+
## What Changes
6+
7+
Add `openspec config` subcommand with the following operations:
8+
9+
```bash
10+
openspec config path # Show config file location
11+
openspec config list # Show all current settings
12+
openspec config get <key> # Get a specific value
13+
openspec config set <key> <value> # Set a value
14+
openspec config reset [key] # Reset to defaults (all or specific key)
15+
```
16+
17+
**Example usage:**
18+
```bash
19+
$ openspec config path
20+
/Users/me/.config/openspec/config.json
21+
22+
$ openspec config list
23+
enableTelemetry: true
24+
featureFlags: {}
25+
26+
$ openspec config set enableTelemetry false
27+
Set enableTelemetry = false
28+
29+
$ openspec config get enableTelemetry
30+
false
31+
```
32+
33+
## Impact
34+
35+
- Affected specs: New `cli-config` capability
36+
- Affected code:
37+
- New `src/commands/config.ts`
38+
- Update CLI entry point to register config command
39+
- Dependencies: Requires `add-global-config-dir` to be implemented first
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
## Context
2+
3+
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.
4+
5+
## Goals / Non-Goals
6+
7+
**Goals:**
8+
- Provide a single, well-defined location for global config
9+
- Follow XDG Base Directory Specification (widely adopted by CLI tools)
10+
- Support cross-platform usage (Unix, macOS, Windows)
11+
- Keep implementation minimal - just the foundation
12+
- Enable future expansion (cache, state, workflows)
13+
14+
**Non-Goals:**
15+
- Project-local config override (not in scope)
16+
- Config file migration tooling
17+
- Config validation CLI commands
18+
- Multiple config profiles
19+
20+
## Decisions
21+
22+
### Path Resolution Strategy
23+
24+
**Decision:** Use XDG Base Directory Specification with platform fallbacks.
25+
26+
```
27+
Unix/macOS: $XDG_CONFIG_HOME/openspec/ or ~/.config/openspec/
28+
Windows: %APPDATA%/openspec/
29+
```
30+
31+
**Rationale:**
32+
- XDG is the de facto standard for CLI tools (used by gh, bat, ripgrep, etc.)
33+
- Environment variable override allows user customization
34+
- Windows uses its native convention (%APPDATA%) for better integration
35+
36+
**Alternatives considered:**
37+
- `~/.openspec/` - Simple but clutters home directory
38+
- `~/Library/Application Support/` on macOS - Overkill for a CLI tool
39+
40+
### Config File Format
41+
42+
**Decision:** JSON (`config.json`)
43+
44+
**Rationale:**
45+
- Native Node.js support (no dependencies)
46+
- Human-readable and editable
47+
- Type-safe with TypeScript
48+
- Matches project.md's "minimal dependencies" principle
49+
50+
**Alternatives considered:**
51+
- YAML - Requires dependency, more error-prone to edit
52+
- TOML - Less common in Node.js ecosystem
53+
- Environment variables only - Too limited for structured settings
54+
55+
### Config Schema
56+
57+
**Decision:** Flat structure with typed fields, start minimal.
58+
59+
```typescript
60+
interface GlobalConfig {
61+
featureFlags?: Record<string, boolean>;
62+
}
63+
```
64+
65+
**Rationale:**
66+
- `featureFlags` enables controlled rollout of new features
67+
- Optional fields with defaults avoid breaking changes
68+
- Flat structure is easy to understand and extend
69+
70+
### Loading Strategy
71+
72+
**Decision:** Read from disk on each call, no caching.
73+
74+
```typescript
75+
export function getGlobalConfig(): GlobalConfig {
76+
return loadConfigFromDisk();
77+
}
78+
```
79+
80+
**Rationale:**
81+
- CLI commands are short-lived; caching adds complexity without benefit
82+
- Reading a small JSON file is ~1ms; negligible overhead
83+
- Always returns fresh data; no cache invalidation concerns
84+
- Simpler implementation
85+
86+
### Directory Creation
87+
88+
**Decision:** Create directory only when saving, not when reading.
89+
90+
**Rationale:**
91+
- Don't create empty directories on read operations
92+
- Users who never save config won't have unnecessary directories
93+
- Aligns with principle of least surprise
94+
95+
## Risks / Trade-offs
96+
97+
| Risk | Mitigation |
98+
|------|------------|
99+
| Config file corruption | Return defaults on parse error, log warning |
100+
| Permissions issues | Check write permissions before save, clear error message |
101+
| Future schema changes | Use optional fields, add version field if needed later |
102+
103+
## Open Questions
104+
105+
None - this proposal is intentionally minimal.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## Why
2+
3+
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.
4+
5+
## What Changes
6+
7+
- Add new `src/core/global-config.ts` module with:
8+
- Path resolution following XDG Base Directory spec (`$XDG_CONFIG_HOME/openspec/` or fallback)
9+
- Cross-platform support (Unix, macOS, Windows)
10+
- Lazy config loading with sensible defaults
11+
- TypeScript types for config shape
12+
- Export a global config directory path getter for future use (workflows, templates, cache)
13+
- Initial config schema supports 1-2 settings/feature flags only
14+
15+
## Impact
16+
17+
- Affected specs: New `global-config` capability (no existing specs modified)
18+
- Affected code:
19+
- New `src/core/global-config.ts`
20+
- Update `src/core/index.ts` to export new module
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Global Config Directory Path
4+
5+
The system SHALL resolve the global configuration directory path following XDG Base Directory Specification with platform-specific fallbacks.
6+
7+
#### Scenario: Unix/macOS with XDG_CONFIG_HOME set
8+
- **WHEN** `$XDG_CONFIG_HOME` environment variable is set to `/custom/config`
9+
- **THEN** `getGlobalConfigDir()` returns `/custom/config/openspec`
10+
11+
#### Scenario: Unix/macOS without XDG_CONFIG_HOME
12+
- **WHEN** `$XDG_CONFIG_HOME` environment variable is not set
13+
- **AND** the platform is Unix or macOS
14+
- **THEN** `getGlobalConfigDir()` returns `~/.config/openspec` (expanded to absolute path)
15+
16+
#### Scenario: Windows platform
17+
- **WHEN** the platform is Windows
18+
- **AND** `%APPDATA%` is set to `C:\Users\User\AppData\Roaming`
19+
- **THEN** `getGlobalConfigDir()` returns `C:\Users\User\AppData\Roaming\openspec`
20+
21+
### Requirement: Global Config Loading
22+
23+
The system SHALL load global configuration from the config directory with sensible defaults when the config file does not exist or cannot be parsed.
24+
25+
#### Scenario: Config file exists and is valid
26+
- **WHEN** `config.json` exists in the global config directory
27+
- **AND** the file contains valid JSON matching the config schema
28+
- **THEN** `getGlobalConfig()` returns the parsed configuration
29+
30+
#### Scenario: Config file does not exist
31+
- **WHEN** `config.json` does not exist in the global config directory
32+
- **THEN** `getGlobalConfig()` returns the default configuration
33+
- **AND** no directory or file is created
34+
35+
#### Scenario: Config file is invalid JSON
36+
- **WHEN** `config.json` exists but contains invalid JSON
37+
- **THEN** `getGlobalConfig()` returns the default configuration
38+
- **AND** a warning is logged to stderr
39+
40+
### Requirement: Global Config Saving
41+
42+
The system SHALL save global configuration to the config directory, creating the directory if it does not exist.
43+
44+
#### Scenario: Save config to new directory
45+
- **WHEN** `saveGlobalConfig(config)` is called
46+
- **AND** the global config directory does not exist
47+
- **THEN** the directory is created
48+
- **AND** `config.json` is written with the provided configuration
49+
50+
#### Scenario: Save config to existing directory
51+
- **WHEN** `saveGlobalConfig(config)` is called
52+
- **AND** the global config directory already exists
53+
- **THEN** `config.json` is written (overwriting if exists)
54+
55+
### Requirement: Default Configuration
56+
57+
The system SHALL provide a default configuration that is used when no config file exists.
58+
59+
#### Scenario: Default config structure
60+
- **WHEN** no config file exists
61+
- **THEN** the default configuration includes an empty `featureFlags` object
62+
63+
### Requirement: Config Schema Evolution
64+
65+
The system SHALL merge loaded configuration with default values to ensure new config fields are available even when loading older config files.
66+
67+
#### Scenario: Config file missing new fields
68+
- **WHEN** `config.json` exists with `{ "featureFlags": {} }`
69+
- **AND** the current schema includes a new field `defaultAiTool`
70+
- **THEN** `getGlobalConfig()` returns `{ featureFlags: {}, defaultAiTool: <default> }`
71+
- **AND** the loaded values take precedence over defaults for fields that exist in both
72+
73+
#### Scenario: Config file has extra unknown fields
74+
- **WHEN** `config.json` contains fields not in the current schema
75+
- **THEN** the unknown fields are preserved in the returned configuration
76+
- **AND** no error or warning is raised
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
## 1. Core Implementation
2+
3+
- [ ] 1.1 Create `src/core/global-config.ts` with path resolution
4+
- Implement `getGlobalConfigDir()` following XDG spec
5+
- Support `$XDG_CONFIG_HOME` environment variable override
6+
- Platform-specific fallbacks (Unix: `~/.config/`, Windows: `%APPDATA%`)
7+
- [ ] 1.2 Define TypeScript interfaces for config shape
8+
- `GlobalConfig` interface with optional fields
9+
- Start minimal: just `featureFlags?: Record<string, boolean>`
10+
- [ ] 1.3 Implement config loading with defaults
11+
- `getGlobalConfig()` - reads config.json if exists, merges with defaults
12+
- No directory/file creation on read (lazy initialization)
13+
- [ ] 1.4 Implement config saving
14+
- `saveGlobalConfig(config)` - writes config.json, creates directory if needed
15+
16+
## 2. Integration
17+
18+
- [ ] 2.1 Export new module from `src/core/index.ts`
19+
- [ ] 2.2 Add constants for config file name and directory name
20+
21+
## 3. Testing
22+
23+
- [ ] 3.1 Manual testing of path resolution on current platform
24+
- [ ] 3.2 Test with/without `$XDG_CONFIG_HOME` set
25+
- [ ] 3.3 Test config load when file doesn't exist (should return defaults)

0 commit comments

Comments
 (0)