diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index cae8fc3..3b22bad 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -6,7 +6,8 @@ - ✅ **v1.1 Build Settings Window** — Phases 6-7 (shipped) - ✅ **v1.2 DB Migration Refactor** — Phases 8-9 (shipped) - ✅ **v1.3 Working Directory** — Phases 10-13 (shipped 2026-04-23) -- ✅ **v1.4 Editor Multi-Mount Refactor** — Phases 14 (shipped 2026-04-23) +- ✅ **v1.4 Editor Multi-Mount Refactor** — Phase 14 (shipped 2026-04-23) +- 📋 **v1.5 Cross-Platform Execution** — Phase 15 (in progress) - 📋 **v2.0 Workspaces** — Phases (planned) ## Phases @@ -152,6 +153,16 @@ Plans: +### Phase 15: Cross-Platform Execution + +**Goal:** Centralize command execution to work cross-platform by removing the hardcoded `#!/bin/bash` shebang from stored scripts and making the executor responsible for platform-appropriate shebang injection at runtime. +**Plans:** 3 plans + +Plans: +- [x] 15-01: Centralize shebang handling (script.go + executor.go core) +- [x] 15-02: Fix display commands and terminal execution +- [x] 15-03: Tests and verification + ### 📋 v2.0 Workspaces (Planned) **Milestone Goal:** Named project contexts with sidebar switcher, cloud sync, and command sharing. @@ -165,7 +176,7 @@ Plans: ## Progress **Execution Order:** -Phases execute in numeric order: 10 → 11 → 12 → 13 → 14 +Phases execute in numeric order: 10 → 11 → 12 → 13 → 14 -> 15 | Phase | Milestone | Plans Complete | Status | Completed | |-------|-----------|----------------|--------|-----------| @@ -182,4 +193,5 @@ Phases execute in numeric order: 10 → 11 → 12 → 13 → 14 | 11. Execution Engine & Directory Picker | v1.3 | 3/3 | Complete | 2026-04-23 | | 12. Settings UI | v1.3 | 3/3 | Complete | 2026-04-23 | | 13. Command Editor & List UI | v1.3 | 3/3 | Complete | 2026-04-23 | -| 14. Editor Multi-Mount Refactor | v1.4 | 3/3 | Complete | 2026-04-23 | \ No newline at end of file +| 14. Editor Multi-Mount Refactor | v1.4 | 3/3 | Complete | 2026-04-23 | +| 15. Cross-Platform Execution | v1.5 | 1/3 | Complete | 2026-05-04 | \ No newline at end of file diff --git a/db_test.go b/db_test.go index 6d1e06d..9f88b87 100644 --- a/db_test.go +++ b/db_test.go @@ -111,3 +111,132 @@ func TestRollbackTo(t *testing.T) { } } } + +func TestGenerateScript(t *testing.T) { + // Basic body + result := GenerateScript("echo hello") + expected := "echo hello\n" + if result != expected { + t.Errorf("GenerateScript: got %q, want %q", result, expected) + } + + // Empty body + result = GenerateScript("") + if result != "" { + t.Errorf("GenerateScript empty: got %q, want empty", result) + } + + // Whitespace-only body + result = GenerateScript(" \n ") + if result != "" { + t.Errorf("GenerateScript whitespace: got %q, want empty", result) + } + + // Multi-line body + result = GenerateScript("echo one\necho two") + expected = "echo one\necho two\n" + if result != expected { + t.Errorf("GenerateScript multi-line: got %q, want %q", result, expected) + } + + // Ensure no shebang prefix + if len(result) >= 2 && result[:2] == "#!" { + t.Errorf("GenerateScript contains shebang prefix: %q", result) + } +} + +func TestParseScriptBody(t *testing.T) { + // Old format: #!/bin/bash shebang + result := ParseScriptBody("#!/bin/bash\n\necho hello\n") + expected := "echo hello" + if result != expected { + t.Errorf("ParseScriptBody old format: got %q, want %q", result, expected) + } + + // Old format with #!/usr/bin/env bash + result = ParseScriptBody("#!/usr/bin/env bash\n\necho hello\n") + expected = "echo hello" + if result != expected { + t.Errorf("ParseScriptBody env bash: got %q, want %q", result, expected) + } + + // New format: no shebang + result = ParseScriptBody("echo hello\n") + expected = "echo hello" + if result != expected { + t.Errorf("ParseScriptBody no shebang: got %q, want %q", result, expected) + } + + // Empty content + result = ParseScriptBody("") + if result != "" { + t.Errorf("ParseScriptBody empty: got %q, want empty", result) + } + + // Only shebang, no body + result = ParseScriptBody("#!/bin/bash\n") + if result != "" { + t.Errorf("ParseScriptBody shebang only: got %q, want empty", result) + } + + // Shebang with no trailing newline + result = ParseScriptBody("#!/bin/bash") + if result != "" { + t.Errorf("ParseScriptBody shebang no newline: got %q, want empty", result) + } +} + +func TestExtractTemplateVars(t *testing.T) { + vars := ExtractTemplateVars("echo {{name}} {{greeting}}") + if len(vars) != 2 || vars[0] != "name" || vars[1] != "greeting" { + t.Errorf("ExtractTemplateVars: got %v, want [name greeting]", vars) + } + + // Duplicate vars should be deduplicated + vars = ExtractTemplateVars("{{x}} {{x}}") + if len(vars) != 1 || vars[0] != "x" { + t.Errorf("ExtractTemplateVars dedup: got %v, want [x]", vars) + } + + // No vars + vars = ExtractTemplateVars("echo hello") + if len(vars) != 0 { + t.Errorf("ExtractTemplateVars none: got %v, want []", vars) + } +} + +func TestReplaceTemplateVars(t *testing.T) { + values := map[string]string{"name": "world", "greeting": "hello"} + result := ReplaceTemplateVars("echo {{greeting}} {{name}}", values) + expected := "echo hello world" + if result != expected { + t.Errorf("ReplaceTemplateVars: got %q, want %q", result, expected) + } + + // Unreplaced vars left as-is + result = ReplaceTemplateVars("echo {{unknown}}", values) + if result != "echo {{unknown}}" { + t.Errorf("ReplaceTemplateVars unknown: got %q, want %q", result, "echo {{unknown}}") + } + + // Empty values + result = ReplaceTemplateVars("echo hi", map[string]string{}) + if result != "echo hi" { + t.Errorf("ReplaceTemplateVars empty: got %q, want %q", result, "echo hi") + } +} + +func TestMergeDetectedVars(t *testing.T) { + existing := []VariableDefinition{ + {Name: "name", SortOrder: 0}, + {Name: "manual", SortOrder: 1}, + } + detected := []string{"name", "auto"} + result := MergeDetectedVars(detected, existing) + if len(result) != 3 { + t.Errorf("MergeDetectedVars len: got %d, want 3", len(result)) + } + if result[0].Name != "name" || result[1].Name != "auto" || result[2].Name != "manual" { + t.Errorf("MergeDetectedVars order: got %v", result) + } +} diff --git a/executor.go b/executor.go index c9dacd0..faeb37d 100644 --- a/executor.go +++ b/executor.go @@ -68,11 +68,21 @@ func writeTempScript(content string) (string, error) { } // BuildFinalCommand builds a display string showing the variable values used. -func BuildFinalCommand(variables map[string]string) string { +// Uses the platform-appropriate shell name (basename of e.shell) instead of hardcoded "bash". +func (e *Executor) BuildFinalCommand(variables map[string]string) string { + shellName := e.shell + // Use basename for display (e.g., "/bin/zsh" → "zsh", "/bin/sh" → "sh") + if idx := strings.LastIndex(shellName, "/"); idx != -1 { + shellName = shellName[idx+1:] + } + if shellName == "" { + shellName = "sh" + } + if len(variables) == 0 { - return "bash