Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24a50e73dd | ||
|
|
07c212d962 | ||
|
|
befcadd260 | ||
|
|
60cf195f45 | ||
|
|
a5726a7a60 |
+2
-2
@@ -241,9 +241,9 @@ func TestNpmInstalledNotFound(t *testing.T) {
|
|||||||
func TestIsCustomPkgInstalledNpmNames(t *testing.T) {
|
func TestIsCustomPkgInstalledNpmNames(t *testing.T) {
|
||||||
defer resetMocks()
|
defer resetMocks()
|
||||||
hasCmd = func(name string) bool {
|
hasCmd = func(name string) bool {
|
||||||
return name == "claude" || name == "codex" || name == "copilot" || name == "playwright"
|
return name == "claude" || name == "codex" || name == "copilot" || name == "playwright" || name == "mdts"
|
||||||
}
|
}
|
||||||
for _, name := range []string{"claude", "codex", "copilot", "playwright"} {
|
for _, name := range []string{"claude", "codex", "copilot", "playwright", "mdts"} {
|
||||||
ok, _ := isCustomPkgInstalled(&CustomPackage{Name: name})
|
ok, _ := isCustomPkgInstalled(&CustomPackage{Name: name})
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("expected %s detected as installed", name)
|
t.Errorf("expected %s detected as installed", name)
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ func isCustomPkgInstalled(pkg *CustomPackage) (bool, string) {
|
|||||||
if name == "playwright" {
|
if name == "playwright" {
|
||||||
return npmInstalled("playwright")
|
return npmInstalled("playwright")
|
||||||
}
|
}
|
||||||
|
if name == "mdts" {
|
||||||
|
return npmInstalled("mdts")
|
||||||
|
}
|
||||||
raw := pkg.InstallPath
|
raw := pkg.InstallPath
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
raw = defaultInstallPath(pkg)
|
raw = defaultInstallPath(pkg)
|
||||||
@@ -574,6 +577,7 @@ func nodeDependentPkgs() map[string]bool {
|
|||||||
"codex": true,
|
"codex": true,
|
||||||
"copilot": true,
|
"copilot": true,
|
||||||
"playwright": true,
|
"playwright": true,
|
||||||
|
"mdts": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,6 +701,7 @@ func installNpmToolsBatch(pkgs []*CustomPackage) {
|
|||||||
"codex": "@openai/codex",
|
"codex": "@openai/codex",
|
||||||
"copilot": "@github/copilot",
|
"copilot": "@github/copilot",
|
||||||
"playwright": "playwright",
|
"playwright": "playwright",
|
||||||
|
"mdts": "mdts",
|
||||||
}
|
}
|
||||||
|
|
||||||
var npmPkgs []string
|
var npmPkgs []string
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ func runMain(args []string) {
|
|||||||
if isMacOS && !*noVM {
|
if isMacOS && !*noVM {
|
||||||
setupFirecrackerVM()
|
setupFirecrackerVM()
|
||||||
}
|
}
|
||||||
|
ensureLibreOfficeAutoSave()
|
||||||
}
|
}
|
||||||
|
|
||||||
if pyenvWG != nil {
|
if pyenvWG != nil {
|
||||||
|
|||||||
@@ -19,7 +19,31 @@ var httpClient = &http.Client{Timeout: httpClientTimeout}
|
|||||||
|
|
||||||
// downloadReal streams url -> dest. Returns true on success.
|
// downloadReal streams url -> dest. Returns true on success.
|
||||||
func downloadReal(url, dest string) bool {
|
func downloadReal(url, dest string) bool {
|
||||||
fmt.Printf(" Downloading %s ...\n", filepath.Base(url))
|
taskPrintf(" Downloading %s ...\n", filepath.Base(url))
|
||||||
|
|
||||||
|
dir, file := filepath.Split(dest)
|
||||||
|
dir = filepath.Clean(dir)
|
||||||
|
|
||||||
|
// Try aria2c first if available
|
||||||
|
if hasCmd("aria2c") {
|
||||||
|
res := runCmd([]string{"aria2c", "-x", "16", "-s", "16", "-k", "1M", "-d", dir, "-o", file, url}, CmdOpts{Out: taskOut()})
|
||||||
|
if res.OK() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
taskPrintf(" [WARN] aria2c download failed for %s, falling back to curl ...\n", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to curl
|
||||||
|
if hasCmd("curl") {
|
||||||
|
res := runCmd([]string{"curl", "-L", "--fail", "-o", dest, url}, CmdOpts{Out: taskOut()})
|
||||||
|
if res.OK() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
taskPrintf(" [WARN] curl download failed for %s ...\n", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final fallback: Go built-in HTTP client
|
||||||
|
taskPrintf(" Falling back to built-in HTTP client for %s ...\n", url)
|
||||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errLog(fmt.Sprintf("Download failed for %s: %v", url, err))
|
errLog(fmt.Sprintf("Download failed for %s: %v", url, err))
|
||||||
|
|||||||
+121
@@ -236,3 +236,124 @@ func TestNetRealErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDownloadRealFallbackChain(t *testing.T) {
|
||||||
|
defer resetMocks()
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
destFile := filepath.Join(tmpDir, "out.txt")
|
||||||
|
|
||||||
|
// Case 1: aria2c works
|
||||||
|
var aria2cCalled bool
|
||||||
|
var curlCalled bool
|
||||||
|
hasCmd = func(name string) bool {
|
||||||
|
if name == "aria2c" || name == "curl" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
runCmd = func(argv []string, opts CmdOpts) CmdResult {
|
||||||
|
if argv[0] == "aria2c" {
|
||||||
|
aria2cCalled = true
|
||||||
|
_ = os.WriteFile(destFile, []byte("aria2c content"), 0644)
|
||||||
|
return CmdResult{ExitCode: 0}
|
||||||
|
}
|
||||||
|
if argv[0] == "curl" {
|
||||||
|
curlCalled = true
|
||||||
|
return CmdResult{ExitCode: 0}
|
||||||
|
}
|
||||||
|
return CmdResult{ExitCode: 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
success := downloadReal("https://example.com/file", destFile)
|
||||||
|
if !success {
|
||||||
|
t.Fatal("expected download via aria2c to succeed")
|
||||||
|
}
|
||||||
|
if !aria2cCalled {
|
||||||
|
t.Error("expected aria2c to be called")
|
||||||
|
}
|
||||||
|
if curlCalled {
|
||||||
|
t.Error("expected curl NOT to be called when aria2c succeeds")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: aria2c fails, falls back to curl, curl succeeds
|
||||||
|
resetMocks()
|
||||||
|
aria2cCalled = false
|
||||||
|
curlCalled = false
|
||||||
|
hasCmd = func(name string) bool {
|
||||||
|
if name == "aria2c" || name == "curl" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
runCmd = func(argv []string, opts CmdOpts) CmdResult {
|
||||||
|
if argv[0] == "aria2c" {
|
||||||
|
aria2cCalled = true
|
||||||
|
return CmdResult{ExitCode: 1, Err: fmt.Errorf("aria2c simulated error")}
|
||||||
|
}
|
||||||
|
if argv[0] == "curl" {
|
||||||
|
curlCalled = true
|
||||||
|
_ = os.WriteFile(destFile, []byte("curl content"), 0644)
|
||||||
|
return CmdResult{ExitCode: 0}
|
||||||
|
}
|
||||||
|
return CmdResult{ExitCode: 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
success = downloadReal("https://example.com/file", destFile)
|
||||||
|
if !success {
|
||||||
|
t.Fatal("expected download to succeed via curl fallback")
|
||||||
|
}
|
||||||
|
if !aria2cCalled {
|
||||||
|
t.Error("expected aria2c to be attempted")
|
||||||
|
}
|
||||||
|
if !curlCalled {
|
||||||
|
t.Error("expected curl to be attempted after aria2c failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 3: aria2c fails, curl fails, falls back to Go HTTP client
|
||||||
|
resetMocks()
|
||||||
|
aria2cCalled = false
|
||||||
|
curlCalled = false
|
||||||
|
hasCmd = func(name string) bool {
|
||||||
|
if name == "aria2c" || name == "curl" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
runCmd = func(argv []string, opts CmdOpts) CmdResult {
|
||||||
|
if argv[0] == "aria2c" {
|
||||||
|
aria2cCalled = true
|
||||||
|
return CmdResult{ExitCode: 1, Err: fmt.Errorf("aria2c simulated error")}
|
||||||
|
}
|
||||||
|
if argv[0] == "curl" {
|
||||||
|
curlCalled = true
|
||||||
|
return CmdResult{ExitCode: 1, Err: fmt.Errorf("curl simulated error")}
|
||||||
|
}
|
||||||
|
return CmdResult{ExitCode: 1}
|
||||||
|
}
|
||||||
|
oldTransport := httpClient.Transport
|
||||||
|
defer func() { httpClient.Transport = oldTransport }()
|
||||||
|
httpClient.Transport = &mockTripper{
|
||||||
|
roundTripFunc: func(req *http.Request) (*http.Response, error) {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: 200,
|
||||||
|
Body: io.NopCloser(bytes.NewBufferString("go http content")),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
success = downloadReal("https://example.com/file", destFile)
|
||||||
|
if !success {
|
||||||
|
t.Fatal("expected download to succeed via Go http fallback")
|
||||||
|
}
|
||||||
|
if !aria2cCalled {
|
||||||
|
t.Error("expected aria2c to be attempted")
|
||||||
|
}
|
||||||
|
if !curlCalled {
|
||||||
|
t.Error("expected curl to be attempted")
|
||||||
|
}
|
||||||
|
data, _ := os.ReadFile(destFile)
|
||||||
|
if string(data) != "go http content" {
|
||||||
|
t.Errorf("expected file content to be 'go http content', got %q", string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -10,10 +10,10 @@ package main
|
|||||||
// URL templates use the substitutions described in formatURL.
|
// URL templates use the substitutions described in formatURL.
|
||||||
|
|
||||||
var SystemPackages = []string{
|
var SystemPackages = []string{
|
||||||
|
"aria2",
|
||||||
"age",
|
"age",
|
||||||
"ansible",
|
"ansible",
|
||||||
"ansible-core",
|
"ansible-core",
|
||||||
"aria2",
|
|
||||||
"btm",
|
"btm",
|
||||||
"build-essential",
|
"build-essential",
|
||||||
"buildah",
|
"buildah",
|
||||||
@@ -151,6 +151,7 @@ func customPackages() []CustomPackage {
|
|||||||
{Name: "codex"},
|
{Name: "codex"},
|
||||||
{Name: "copilot"},
|
{Name: "copilot"},
|
||||||
{Name: "playwright"},
|
{Name: "playwright"},
|
||||||
|
{Name: "mdts"},
|
||||||
{Name: "gh-repo-bootstrap"},
|
{Name: "gh-repo-bootstrap"},
|
||||||
{
|
{
|
||||||
Name: "rustup",
|
Name: "rustup",
|
||||||
|
|||||||
@@ -544,3 +544,45 @@ func installGHExtension(repo string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── LibreOffice AutoSave Extension ──────────────────────────────────────
|
||||||
|
|
||||||
|
func ensureLibreOfficeAutoSave() {
|
||||||
|
var unopkgPath string
|
||||||
|
if isMacOS {
|
||||||
|
unopkgPath = "/Applications/LibreOffice.app/Contents/MacOS/unopkg"
|
||||||
|
if _, err := osStat(unopkgPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !hasCmd("unopkg") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
unopkgPath = "unopkg"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\n[LibreOffice] LibreOffice detected. Installing AutoSave extension ...")
|
||||||
|
|
||||||
|
tmp, err := os.MkdirTemp("", "libreoffice-autosave-")
|
||||||
|
if err != nil {
|
||||||
|
errLog(fmt.Sprintf("LibreOffice AutoSave temp dir failed: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer osRemoveAll(tmp)
|
||||||
|
|
||||||
|
url := "https://github.com/JMR-dev/LibreOfficeAutoSave/releases/latest/download/AutoSave.oxt"
|
||||||
|
dest := filepath.Join(tmp, "AutoSave.oxt")
|
||||||
|
|
||||||
|
if !download(url, dest) {
|
||||||
|
errLog("Failed to download LibreOffice AutoSave extension")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !runCmd([]string{unopkgPath, "add", "-f", dest}, CmdOpts{}).OK() {
|
||||||
|
errLog("Failed to install LibreOffice AutoSave extension")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(" LibreOffice AutoSave extension installed successfully.")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -905,4 +905,98 @@ func TestEnsurePythonLatestBackgroundInstall(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnsureLibreOfficeAutoSave(t *testing.T) {
|
||||||
|
defer resetMocks()
|
||||||
|
|
||||||
|
// 1. isMacOS = false, LibreOffice not installed
|
||||||
|
isMacOS = false
|
||||||
|
hasCmd = func(name string) bool {
|
||||||
|
if name == "unopkg" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var calledDownload bool
|
||||||
|
download = func(url, dest string) bool {
|
||||||
|
calledDownload = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
ensureLibreOfficeAutoSave()
|
||||||
|
if calledDownload {
|
||||||
|
t.Error("expected download not to be called when unopkg is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. isMacOS = true, LibreOffice not installed
|
||||||
|
isMacOS = true
|
||||||
|
osStat = func(name string) (os.FileInfo, error) {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
calledDownload = false
|
||||||
|
ensureLibreOfficeAutoSave()
|
||||||
|
if calledDownload {
|
||||||
|
t.Error("expected download not to be called when unopkg is missing on macOS")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. isMacOS = false, LibreOffice installed, download fails
|
||||||
|
isMacOS = false
|
||||||
|
hasCmd = func(name string) bool {
|
||||||
|
return name == "unopkg"
|
||||||
|
}
|
||||||
|
download = func(url, dest string) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var runCmdCalled bool
|
||||||
|
runCmd = func(argv []string, opts CmdOpts) CmdResult {
|
||||||
|
runCmdCalled = true
|
||||||
|
return CmdResult{ExitCode: 0}
|
||||||
|
}
|
||||||
|
ensureLibreOfficeAutoSave()
|
||||||
|
if runCmdCalled {
|
||||||
|
t.Error("expected runCmd not to be called when download fails")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. isMacOS = false, LibreOffice installed, download succeeds, runCmd fails
|
||||||
|
download = func(url, dest string) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var unopkgArgv []string
|
||||||
|
runCmd = func(argv []string, opts CmdOpts) CmdResult {
|
||||||
|
unopkgArgv = argv
|
||||||
|
return CmdResult{ExitCode: 1}
|
||||||
|
}
|
||||||
|
ensureLibreOfficeAutoSave()
|
||||||
|
if len(unopkgArgv) == 0 || unopkgArgv[0] != "unopkg" {
|
||||||
|
t.Errorf("expected runCmd with unopkg, got: %v", unopkgArgv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. isMacOS = false, LibreOffice installed, download succeeds, runCmd succeeds
|
||||||
|
runCmd = func(argv []string, opts CmdOpts) CmdResult {
|
||||||
|
unopkgArgv = argv
|
||||||
|
return CmdResult{ExitCode: 0}
|
||||||
|
}
|
||||||
|
ensureLibreOfficeAutoSave()
|
||||||
|
if len(unopkgArgv) == 0 || unopkgArgv[0] != "unopkg" || unopkgArgv[1] != "add" || unopkgArgv[2] != "-f" {
|
||||||
|
t.Errorf("expected runCmd with unopkg add -f, got: %v", unopkgArgv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. isMacOS = true, LibreOffice installed, download succeeds, runCmd succeeds
|
||||||
|
isMacOS = true
|
||||||
|
osStat = func(name string) (os.FileInfo, error) {
|
||||||
|
if name == "/Applications/LibreOffice.app/Contents/MacOS/unopkg" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
runCmd = func(argv []string, opts CmdOpts) CmdResult {
|
||||||
|
unopkgArgv = argv
|
||||||
|
return CmdResult{ExitCode: 0}
|
||||||
|
}
|
||||||
|
ensureLibreOfficeAutoSave()
|
||||||
|
expectedMacPath := "/Applications/LibreOffice.app/Contents/MacOS/unopkg"
|
||||||
|
if len(unopkgArgv) == 0 || unopkgArgv[0] != expectedMacPath {
|
||||||
|
t.Errorf("expected runCmd with %s, got: %v", expectedMacPath, unopkgArgv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -458,6 +458,38 @@ func brewInstallMany(pkgs []string) (failed []string) {
|
|||||||
func installSystemPackages(regular, special []string) {
|
func installSystemPackages(regular, special []string) {
|
||||||
fmt.Println("\n=== System Packages ===")
|
fmt.Println("\n=== System Packages ===")
|
||||||
|
|
||||||
|
// Ensure aria2 is installed first and on the system path
|
||||||
|
var installAria2 bool
|
||||||
|
var remainingRegular []string
|
||||||
|
for _, p := range regular {
|
||||||
|
if p == "aria2" {
|
||||||
|
installAria2 = true
|
||||||
|
} else {
|
||||||
|
remainingRegular = append(remainingRegular, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if installAria2 || !hasCmd("aria2c") {
|
||||||
|
fmt.Println(" Ensuring aria2 is installed first and on the system path ...")
|
||||||
|
var res CmdResult
|
||||||
|
switch pkgMgr {
|
||||||
|
case "brew":
|
||||||
|
res = runCmd([]string{"brew", "install", "aria2"}, CmdOpts{})
|
||||||
|
case "pacman":
|
||||||
|
res = runCmd([]string{"pacman", "-S", "--noconfirm", "--needed", "aria2"}, CmdOpts{AsSudo: true})
|
||||||
|
default: // dnf, apt-get
|
||||||
|
res = runCmd([]string{pkgMgr, "install", "-y", "aria2"}, CmdOpts{AsSudo: true})
|
||||||
|
}
|
||||||
|
if !res.OK() {
|
||||||
|
warn(fmt.Sprintf("Failed to install aria2: %v", res.Err))
|
||||||
|
} else if !hasCmd("aria2c") {
|
||||||
|
warn("aria2 was installed but 'aria2c' is not found on the system path")
|
||||||
|
} else {
|
||||||
|
fmt.Println(" aria2 is installed and on the system path.")
|
||||||
|
}
|
||||||
|
regular = remainingRegular
|
||||||
|
}
|
||||||
|
|
||||||
if pkgMgr == "brew" {
|
if pkgMgr == "brew" {
|
||||||
failed := pkgInstallMany(regular)
|
failed := pkgInstallMany(regular)
|
||||||
for _, p := range failed {
|
for _, p := range failed {
|
||||||
|
|||||||
@@ -495,3 +495,36 @@ func TestSystemGoEdgeCases(t *testing.T) {
|
|||||||
installSystemPackages([]string{"docker-ce"}, []string{})
|
installSystemPackages([]string{"docker-ce"}, []string{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInstallSystemPackagesAria2First(t *testing.T) {
|
||||||
|
defer resetMocks()
|
||||||
|
|
||||||
|
pkgMgr = "dnf"
|
||||||
|
var runCmdCalls [][]string
|
||||||
|
runCmd = func(argv []string, opts CmdOpts) CmdResult {
|
||||||
|
runCmdCalls = append(runCmdCalls, argv)
|
||||||
|
return CmdResult{ExitCode: 0}
|
||||||
|
}
|
||||||
|
hasCmd = func(name string) bool {
|
||||||
|
if name == "aria2c" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
installSystemPackages([]string{"git", "aria2", "tmux"}, []string{})
|
||||||
|
|
||||||
|
if len(runCmdCalls) < 2 {
|
||||||
|
t.Fatalf("expected at least 2 command calls, got %d: %v", len(runCmdCalls), runCmdCalls)
|
||||||
|
}
|
||||||
|
|
||||||
|
firstCall := runCmdCalls[0]
|
||||||
|
if len(firstCall) < 4 || firstCall[0] != "dnf" || firstCall[1] != "install" || firstCall[3] != "aria2" {
|
||||||
|
t.Errorf("expected first call to be installing aria2, got: %v", firstCall)
|
||||||
|
}
|
||||||
|
|
||||||
|
secondCall := runCmdCalls[1]
|
||||||
|
if len(secondCall) < 5 || secondCall[0] != "dnf" || secondCall[1] != "install" || secondCall[3] != "git" || secondCall[4] != "tmux" {
|
||||||
|
t.Errorf("expected second call to install remaining packages, got: %v", secondCall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user