Skip to content

Commit 3ce6e9a

Browse files
authored
Merge pull request #105 from codervisor/copilot/implement-spec-162
feat(desktop): migrate config from YAML to JSON format (spec 162)
2 parents 2e1123d + a41c378 commit 3ce6e9a

File tree

5 files changed

+163
-119
lines changed

5 files changed

+163
-119
lines changed

packages/desktop/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,10 @@ The Tauri v2 permission model is defined in `src-tauri/capabilities/desktop-main
9393

9494
## Configuration files
9595

96-
- `~/.leanspec/desktop-config.json` — Desktop app configuration and project registry
96+
- `~/.lean-spec/desktop.json` — Desktop app configuration (window size, shortcuts, theme, etc.)
97+
- `~/.lean-spec/projects.json` — Project registry
9798

98-
The config file is automatically created on first launch and updated as you add/remove projects.
99+
These config files are automatically created on first launch and updated as you add/remove projects.
99100

100101
## Features
101102

packages/desktop/src-tauri/src/config.rs

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use serde::{Deserialize, Serialize};
88
static CONFIG: Lazy<RwLock<DesktopConfig>> = Lazy::new(|| RwLock::new(DesktopConfig::load_or_default()));
99

1010
const CONFIG_DIR: &str = ".lean-spec";
11-
const CONFIG_FILE: &str = "desktop.yaml";
11+
const CONFIG_FILE: &str = "desktop.json";
12+
const LEGACY_CONFIG_FILE: &str = "desktop.yaml";
1213

1314
#[derive(Debug, Clone, Serialize, Deserialize)]
1415
#[serde(rename_all = "camelCase")]
@@ -98,26 +99,39 @@ impl Default for DesktopConfig {
9899
impl DesktopConfig {
99100
fn load_or_default() -> Self {
100101
let path = config_file_path();
102+
103+
// Try JSON first
101104
match fs::read_to_string(&path) {
102-
Ok(raw) => match serde_yaml::from_str::<DesktopConfig>(&raw) {
105+
Ok(raw) => match serde_json::from_str::<DesktopConfig>(&raw) {
103106
Ok(mut config) => {
104107
normalize_config(&mut config);
105-
config
108+
return config;
106109
}
107110
Err(error) => {
108-
eprintln!("Failed to parse desktop config: {error}");
109-
Self::default()
111+
eprintln!("Failed to parse desktop config as JSON: {error}");
112+
eprintln!("Will attempt migration from legacy YAML format");
110113
}
111114
},
112-
Err(_) => Self::default(),
115+
Err(_) => {}
113116
}
117+
118+
// Migration: Try legacy YAML
119+
if let Some(legacy_config) = load_legacy_yaml() {
120+
eprintln!("Migrating desktop config from YAML to JSON format");
121+
legacy_config.persist(); // Save as JSON
122+
backup_legacy_yaml();
123+
eprintln!("Migration complete: desktop.yaml → desktop.json");
124+
return legacy_config;
125+
}
126+
127+
Self::default()
114128
}
115129

116130
fn persist(&self) {
117131
if let Some(dir) = config_dir() {
118132
if fs::create_dir_all(&dir).is_ok() {
119133
let file = dir.join(CONFIG_FILE);
120-
if let Ok(serialized) = serde_yaml::to_string(self) {
134+
if let Ok(serialized) = serde_json::to_string_pretty(self) {
121135
if let Err(error) = fs::write(file, serialized) {
122136
eprintln!("Unable to write desktop config: {error}");
123137
}
@@ -137,6 +151,26 @@ fn normalize_config(config: &mut DesktopConfig) {
137151
}
138152
}
139153

154+
fn load_legacy_yaml() -> Option<DesktopConfig> {
155+
let dir = config_dir()?;
156+
let path = dir.join(LEGACY_CONFIG_FILE);
157+
let raw = fs::read_to_string(path).ok()?;
158+
let mut config: DesktopConfig = serde_yaml::from_str(&raw).ok()?;
159+
normalize_config(&mut config);
160+
Some(config)
161+
}
162+
163+
fn backup_legacy_yaml() {
164+
if let Some(dir) = config_dir() {
165+
let legacy = dir.join(LEGACY_CONFIG_FILE);
166+
let backup = dir.join("desktop.yaml.bak");
167+
match fs::rename(&legacy, &backup) {
168+
Ok(_) => eprintln!("Legacy config backed up: desktop.yaml.bak"),
169+
Err(error) => eprintln!("Failed to backup legacy config: {error}"),
170+
}
171+
}
172+
}
173+
140174
pub fn config_dir() -> Option<PathBuf> {
141175
home_dir().map(|home| home.join(CONFIG_DIR))
142176
}

specs/148-leanspec-desktop-app/README.md

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ The current web-based UI (`lean-spec ui`) requires:
5151
## Implementation Summary (Dec 10, 2025)
5252

5353
- **New package:** `packages/desktop` ships a Vite-powered chrome plus a Rust/Tauri backend. The shell embeds the existing Next.js UI by launching its standalone server in the background (dev uses `pnpm --filter @leanspec/ui dev`, production bundles `.next/standalone`).
54-
- **Windowing:** Frameless window with custom title bar + native controls, backed by `tauri-plugin-window-state` for automatic persistence and close-to-tray behavior (configurable via `desktop.yaml`).
54+
- **Windowing:** Frameless window with custom title bar + native controls, backed by `tauri-plugin-window-state` for automatic persistence and close-to-tray behavior (configurable via `desktop.json`).
5555
- **Project registry:** Rust port of the project registry keeps `~/.lean-spec/projects.json` in sync, validates folders, and exposes commands for refresh/add/switch. Config-driven active project switches restart the embedded UI with the right `SPECS_DIR`.
5656
- **Tray + shortcuts:** Dedicated modules (`tray.rs`, `shortcuts.rs`) manage recent-project menus, quick actions (open, add, refresh, check for updates), and global shortcuts (`Cmd/Ctrl+Shift+L/K/N`). Frontend listeners open the project switcher or project picker when shortcuts fire.
57-
- **Notifications + updater:** Desktop emits OS notifications on project changes and wires a `desktop_check_updates` command to the Tauri updater so tray actions can trigger update checks. Auto-update channels (`stable`/`beta`) live in `desktop.yaml`.
57+
- **Notifications + updater:** Desktop emits OS notifications on project changes and wires a `desktop_check_updates` command to the Tauri updater so tray actions can trigger update checks. Auto-update channels (`stable`/`beta`) live in `desktop.json`.
5858
- **Documentation:** Root `README.md` and `packages/desktop/README.md` describe the desktop workflow. A helper script (`pnpm prepare:ui`) copies the Next standalone build so `pnpm build:desktop` produces platform bundles.
5959

6060
### Developer Workflow
@@ -237,33 +237,35 @@ The desktop app wraps `@leanspec/ui` with minimal changes:
237237

238238
### Configuration
239239

240-
**Desktop-Specific Config (~/.lean-spec/desktop.yaml):**
241-
```yaml
242-
window:
243-
width: 1400
244-
height: 900
245-
x: 100
246-
y: 100
247-
maximized: false
248-
249-
behavior:
250-
startMinimized: false
251-
minimizeToTray: true
252-
launchAtLogin: false
253-
254-
shortcuts:
255-
global:
256-
toggleWindow: "CommandOrControl+Shift+L"
257-
quickSwitcher: "CommandOrControl+Shift+K"
258-
newSpec: "CommandOrControl+Shift+N"
259-
260-
updates:
261-
autoCheck: true
262-
autoInstall: false
263-
channel: "stable" # or "beta"
264-
265-
appearance:
266-
theme: "system" # "light", "dark", or "system"
240+
**Desktop-Specific Config (~/.lean-spec/desktop.json):**
241+
```json
242+
{
243+
"window": {
244+
"width": 1400,
245+
"height": 900,
246+
"x": 100,
247+
"y": 100,
248+
"maximized": false
249+
},
250+
"behavior": {
251+
"startMinimized": false,
252+
"minimizeToTray": true,
253+
"launchAtLogin": false
254+
},
255+
"shortcuts": {
256+
"toggleWindow": "CommandOrControl+Shift+L",
257+
"quickSwitcher": "CommandOrControl+Shift+K",
258+
"newSpec": "CommandOrControl+Shift+N"
259+
},
260+
"updates": {
261+
"autoCheck": true,
262+
"autoInstall": false,
263+
"channel": "stable"
264+
},
265+
"appearance": {
266+
"theme": "system"
267+
}
268+
}
267269
```
268270

269271
### Distribution
@@ -325,7 +327,7 @@ appearance:
325327

326328
**Day 10: Notifications**
327329
- [x] Implement native OS notifications (project add/switch events)
328-
- [ ] Add notification preferences _(desktop.yaml toggle TBD)_
330+
- [ ] Add notification preferences _(desktop.json toggle TBD)_
329331
- [ ] Test on all platforms
330332

331333
### Phase 3: Polish & Distribution (Week 3)
@@ -334,7 +336,7 @@ appearance:
334336
- [x] Configure Tauri updater (endpoints + channel config)
335337
- [ ] Set up update server (GitHub Releases)
336338
- [x] Implement update UI hooks (tray action + command)
337-
- [x] Add update channel selection (stable/beta via `desktop.yaml`)
339+
- [x] Add update channel selection (stable/beta via `desktop.json`)
338340
- [ ] Test update flow end-to-end _(requires release infra)_
339341

340342
**Day 13-14: Build & Release**

specs/162-desktop-config-json-migration/README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
status: planned
2+
status: complete
33
created: '2025-12-10'
44
tags:
55
- desktop
@@ -12,12 +12,17 @@ created_at: '2025-12-10T08:49:08.237Z'
1212
depends_on:
1313
- 147-json-config-format
1414
- 148-leanspec-desktop-app
15-
updated_at: '2025-12-10T08:49:08.287Z'
15+
updated_at: '2025-12-18T09:56:48.390Z'
16+
completed_at: '2025-12-18T09:56:48.390Z'
17+
completed: '2025-12-18'
18+
transitions:
19+
- status: complete
20+
at: '2025-12-18T09:56:48.390Z'
1621
---
1722

1823
# Migrate Desktop Config from YAML to JSON
1924

20-
> **Status**: 🗓️ Planned · **Priority**: High · **Created**: 2025-12-10 · **Tags**: desktop, config, migration, breaking-change, consistency
25+
> **Status**: ✅ Complete · **Priority**: High · **Created**: 2025-12-10 · **Tags**: desktop, config, migration, breaking-change, consistency
2126
2227
## Overview
2328

specs/168-leanspec-orchestration-platform/DESIGN.md

Lines changed: 78 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -520,82 +520,84 @@ Continue to testing? [Y/n]: y
520520

521521
## Configuration
522522

523-
### Desktop Config (~/.lean-spec/desktop.yaml)
524-
525-
```yaml
526-
orchestration:
527-
defaultAgent: claude
528-
guidedMode: true # Pause between phases
529-
autoValidate: true # Run validation after implementation
530-
autoComplete: false # Require manual completion
531-
532-
agents:
533-
claude:
534-
enabled: true
535-
priority: 1
536-
models:
537-
default: claude-3-5-sonnet-20241022
538-
fast: claude-3-5-haiku-20241022
539-
copilot:
540-
enabled: true
541-
priority: 2
542-
cursor:
543-
enabled: false
544-
aider:
545-
enabled: true
546-
priority: 3
547-
548-
agentRelay:
549-
endpoint: "http://localhost:8080"
550-
apiKey: "${AGENT_RELAY_API_KEY}"
551-
timeout: 300000 # 5 minutes
552-
retryAttempts: 3
553-
retryDelay: 5000 # 5 seconds
554-
555-
devlog:
556-
endpoint: "http://localhost:9090"
557-
apiKey: "${DEVLOG_API_KEY}"
558-
enabled: true
559-
batchSize: 10 # Batch telemetry events
560-
flushInterval: 5000 # Flush every 5 seconds
561-
562-
validation:
563-
autoRun: true
564-
runTests: true
565-
runLinters: true
566-
runTypeCheck: true
567-
aiReview: true
568-
569-
# Test runner
570-
testCommand: "npm test"
571-
testCoverage: true
572-
573-
# Linter
574-
linterCommand: "npm run lint"
575-
linterIgnore: ["*.test.ts", "*.spec.ts"]
576-
577-
# Type checker
578-
typeCheckCommand: "tsc --noEmit"
579-
580-
notifications:
581-
phaseComplete: true
582-
sessionComplete: true
583-
validationFailed: true
584-
validationPassed: false # Only notify on failure
585-
586-
shortcuts:
587-
implement: "CommandOrControl+Shift+I"
588-
validate: "CommandOrControl+Shift+V"
589-
newSpec: "CommandOrControl+Shift+N"
590-
quickSwitcher: "CommandOrControl+Shift+K"
591-
592-
ui:
593-
theme: "system" # light, dark, or system
594-
outputFontSize: 13
595-
outputFontFamily: "Menlo, Monaco, 'Courier New', monospace"
596-
animatePhaseProgress: true
597-
showTokenCount: true
598-
showDuration: true
523+
### Desktop Config (~/.lean-spec/desktop.json)
524+
525+
```json
526+
{
527+
"orchestration": {
528+
"defaultAgent": "claude",
529+
"guidedMode": true,
530+
"autoValidate": true,
531+
"autoComplete": false
532+
},
533+
"agents": {
534+
"claude": {
535+
"enabled": true,
536+
"priority": 1,
537+
"models": {
538+
"default": "claude-3-5-sonnet-20241022",
539+
"fast": "claude-3-5-haiku-20241022"
540+
}
541+
},
542+
"copilot": {
543+
"enabled": true,
544+
"priority": 2
545+
},
546+
"cursor": {
547+
"enabled": false
548+
},
549+
"aider": {
550+
"enabled": true,
551+
"priority": 3
552+
}
553+
},
554+
"agentRelay": {
555+
"endpoint": "http://localhost:8080",
556+
"apiKey": "${AGENT_RELAY_API_KEY}",
557+
"timeout": 300000,
558+
"retryAttempts": 3,
559+
"retryDelay": 5000
560+
},
561+
"devlog": {
562+
"endpoint": "http://localhost:9090",
563+
"apiKey": "${DEVLOG_API_KEY}",
564+
"enabled": true,
565+
"batchSize": 10,
566+
"flushInterval": 5000
567+
},
568+
"validation": {
569+
"autoRun": true,
570+
"runTests": true,
571+
"runLinters": true,
572+
"runTypeCheck": true,
573+
"aiReview": true,
574+
"testCommand": "npm test",
575+
"testCoverage": true,
576+
"linterCommand": "npm run lint",
577+
"linterIgnore": ["*.test.ts", "*.spec.ts"],
578+
"typeCheckCommand": "tsc --noEmit"
579+
},
580+
"notifications": {
581+
"phaseComplete": true,
582+
"sessionComplete": true,
583+
"validationFailed": true,
584+
"validationPassed": false
585+
},
586+
"shortcuts": {
587+
"implement": "CommandOrControl+Shift+I",
588+
"validate": "CommandOrControl+Shift+V",
589+
"newSpec": "CommandOrControl+Shift+N",
590+
"quickSwitcher": "CommandOrControl+Shift+K"
591+
},
592+
"ui": {
593+
"theme": "system",
594+
"outputFontSize": 13,
595+
"outputFontFamily": "Menlo, Monaco, 'Courier New', monospace",
596+
"animatePhaseProgress": true,
597+
"showTokenCount": true,
598+
"showDuration": true
599+
}
600+
}
599601
```
600602

601603
### Project-Specific Config (.leanspec/config.yaml)

0 commit comments

Comments
 (0)