Skip to content

Commit 05501b1

Browse files
spboyerCopilot
andauthored
Add waza update command (#288)
* feat: add waza update command Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: target update installers by OS Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: satisfy update command lint Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: harden update installer selection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: wait for async version check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 769fd04 commit 05501b1

10 files changed

Lines changed: 753 additions & 12 deletions

File tree

README.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@ Download and install the latest pre-built binary with the Bash install script on
1414
curl -fsSL https://raw.githubusercontent.com/microsoft/waza/main/install.sh | bash
1515
```
1616

17-
The script auto-detects the OS and architecture of the environment where Bash is running (linux/darwin/windows, amd64/arm64), downloads the binary, verifies the checksum, and installs to `/usr/local/bin` (or `~/bin` if not writable). On Windows, piping this command to `bash` from PowerShell may invoke WSL and install the Linux binary inside WSL. For native Windows, download `waza-windows-amd64.exe` or `waza-windows-arm64.exe` from the standalone waza release assets on GitHub Releases, rename it to `waza.exe`, and place it in a directory on your `PATH`.
17+
The Bash script auto-detects the OS and architecture of the environment where Bash is running (linux/darwin/windows, amd64/arm64), downloads the binary, verifies the checksum, and installs to `/usr/local/bin` (or `~/bin` if not writable).
18+
19+
For native Windows PowerShell:
20+
21+
```powershell
22+
irm https://raw.githubusercontent.com/microsoft/waza/main/install.ps1 | iex
23+
```
24+
25+
The PowerShell script downloads the native Windows binary, verifies the checksum, and installs to an existing `waza.exe` location or `%LOCALAPPDATA%\Microsoft\Waza`. On Windows, piping the Bash command from PowerShell may invoke WSL and install the Linux binary inside WSL.
1826

1927
Or browse the [GitHub Releases](https://github.com/microsoft/waza/releases) page and choose the standalone waza binary assets for the version you want.
2028

@@ -63,10 +71,10 @@ azd waza run examples/code-explainer/eval.yaml -v
6371
Waza automatically checks for new versions in the background. If an update is available, a notice appears after command output:
6472

6573
```
66-
A newer version of waza is available: v0.24.0 → v0.28.0. Run: curl -fsSL ... | bash
74+
A newer version of waza is available: v0.24.0 → v0.28.0. Run: waza update
6775
```
6876

69-
The check is non-blocking (never slows commands), cached for 24 hours, and can be disabled with `--no-update-check` or `WAZA_NO_UPDATE_CHECK=1`.
77+
Run `waza update` to download and execute the official OS-specific installer after an explicit confirmation prompt. It uses the Bash installer on macOS/Linux and the PowerShell installer on native Windows. Use `waza update --yes` to skip the prompt in scripted environments. The check is non-blocking (never slows commands), cached for 24 hours, and can be disabled with `--no-update-check` or `WAZA_NO_UPDATE_CHECK=1`.
7078

7179
## Quick Start
7280

@@ -101,6 +109,9 @@ make build
101109
# Initialize a project workspace
102110
waza init [directory]
103111

112+
# Update waza to the latest release
113+
waza update
114+
104115
# Create a new skill
105116
waza new skill skill-name
106117

@@ -144,6 +155,20 @@ waza tokens suggest skills/
144155

145156
## Commands
146157

158+
### `waza update`
159+
160+
Update waza to the latest release by running the official OS-specific installer after confirmation.
161+
162+
| Flag | Description |
163+
|------|-------------|
164+
| `--yes`, `-y` | Skip the confirmation prompt |
165+
166+
**Example:**
167+
```bash
168+
waza update
169+
waza update --yes
170+
```
171+
147172
### `waza init [directory]`
148173

149174
Initialize a waza project workspace with separated `skills/` and `evals/` directories. Idempotent — creates only missing files.

cmd/waza/cmd_update.go

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"runtime"
13+
"strings"
14+
15+
versionpkg "github.com/microsoft/waza/internal/version"
16+
"github.com/spf13/cobra"
17+
)
18+
19+
const latestReleaseURL = "https://github.com/microsoft/waza/releases/latest"
20+
21+
type updateCommandOptions struct {
22+
BashInstallerURL string
23+
PowerShellInstallerURL string
24+
GOOS string
25+
ExecutablePath string
26+
LookPath func(string) (string, error)
27+
RunCommand func(context.Context, string, []string, []string, io.Reader, io.Writer, io.Writer) error
28+
}
29+
30+
type updateInstaller struct {
31+
Name string
32+
ScriptURL string
33+
Candidates []string
34+
Requires []string
35+
Args []string
36+
Env []string
37+
Async bool
38+
}
39+
40+
func newUpdateCommand() *cobra.Command {
41+
return newUpdateCommandWithOptions(nil)
42+
}
43+
44+
func newUpdateCommandWithOptions(options *updateCommandOptions) *cobra.Command {
45+
opts := normalizeUpdateCommandOptions(options)
46+
var yes bool
47+
48+
cmd := &cobra.Command{
49+
Use: "update",
50+
Short: "Update waza to the latest release",
51+
Long: `Update waza to the latest release.
52+
53+
This command downloads and runs the official installer for your OS:
54+
macOS/Linux: Bash installer
55+
Windows: PowerShell installer
56+
57+
The selected installer detects the architecture for the current environment,
58+
downloads the matching release asset, verifies its checksum, and updates the
59+
waza binary.`,
60+
Args: cobra.NoArgs,
61+
RunE: func(cmd *cobra.Command, args []string) error {
62+
return runUpdateCommand(cmd, opts, yes)
63+
},
64+
}
65+
66+
cmd.Flags().BoolVarP(&yes, "yes", "y", false, "Run the update without prompting for confirmation")
67+
68+
return cmd
69+
}
70+
71+
func normalizeUpdateCommandOptions(options *updateCommandOptions) updateCommandOptions {
72+
opts := updateCommandOptions{
73+
BashInstallerURL: versionpkg.BashInstallScriptURL,
74+
PowerShellInstallerURL: versionpkg.PowerShellInstallScriptURL,
75+
GOOS: runtime.GOOS,
76+
LookPath: exec.LookPath,
77+
RunCommand: func(ctx context.Context, name string, args []string, env []string, stdin io.Reader, stdout, stderr io.Writer) error {
78+
cmd := exec.CommandContext(ctx, name, args...)
79+
if len(env) > 0 {
80+
cmd.Env = append(os.Environ(), env...)
81+
}
82+
cmd.Stdin = stdin
83+
cmd.Stdout = stdout
84+
cmd.Stderr = stderr
85+
return cmd.Run()
86+
},
87+
}
88+
if options == nil {
89+
if executablePath, err := os.Executable(); err == nil {
90+
opts.ExecutablePath = executablePath
91+
}
92+
return opts
93+
}
94+
if options.BashInstallerURL != "" {
95+
opts.BashInstallerURL = options.BashInstallerURL
96+
}
97+
if options.PowerShellInstallerURL != "" {
98+
opts.PowerShellInstallerURL = options.PowerShellInstallerURL
99+
}
100+
if options.GOOS != "" {
101+
opts.GOOS = options.GOOS
102+
}
103+
if options.ExecutablePath != "" {
104+
opts.ExecutablePath = options.ExecutablePath
105+
} else if executablePath, err := os.Executable(); err == nil {
106+
opts.ExecutablePath = executablePath
107+
}
108+
if options.LookPath != nil {
109+
opts.LookPath = options.LookPath
110+
}
111+
if options.RunCommand != nil {
112+
opts.RunCommand = options.RunCommand
113+
}
114+
return opts
115+
}
116+
117+
func runUpdateCommand(cmd *cobra.Command, opts updateCommandOptions, yes bool) error {
118+
out := cmd.OutOrStdout()
119+
errOut := cmd.ErrOrStderr()
120+
installer, err := installerForOS(opts.GOOS, opts)
121+
if err != nil {
122+
return err
123+
}
124+
125+
if !yes {
126+
ok, err := confirmUpdate(cmd.InOrStdin(), out, installer)
127+
if err != nil {
128+
return err
129+
}
130+
if !ok {
131+
if _, err := fmt.Fprintln(out, "Update canceled."); err != nil {
132+
return err
133+
}
134+
return nil
135+
}
136+
}
137+
138+
commandPath, err := lookPathAny(opts.LookPath, installer.Candidates)
139+
if err != nil {
140+
return missingInstallerError(installer)
141+
}
142+
for _, dependency := range installer.Requires {
143+
if _, err := opts.LookPath(dependency); err != nil {
144+
return missingDependencyError(dependency)
145+
}
146+
}
147+
148+
if _, err := fmt.Fprintf(out, "Updating waza with the %s installer...\n", installer.Name); err != nil {
149+
return err
150+
}
151+
if err := opts.RunCommand(cmd.Context(), commandPath, installer.Args, installer.Env, cmd.InOrStdin(), out, errOut); err != nil {
152+
return fmt.Errorf("running waza installer: %w", err)
153+
}
154+
155+
if installer.Async {
156+
if _, err := fmt.Fprintln(out, "Update started. The installer will finish after this waza process exits."); err != nil {
157+
return err
158+
}
159+
} else {
160+
if _, err := fmt.Fprintln(out, "Update complete."); err != nil {
161+
return err
162+
}
163+
}
164+
return nil
165+
}
166+
167+
func installerForOS(goos string, opts updateCommandOptions) (updateInstaller, error) {
168+
switch goos {
169+
case "darwin", "linux":
170+
return updateInstaller{
171+
Name: "Bash",
172+
ScriptURL: opts.BashInstallerURL,
173+
Candidates: []string{"bash"},
174+
Requires: []string{"curl"},
175+
Args: []string{"-c", `set -euo pipefail; curl -fsSL "$1" | bash`, "waza-installer", opts.BashInstallerURL},
176+
}, nil
177+
case "windows":
178+
script := `$ErrorActionPreference = 'Stop'; Invoke-Expression (Invoke-RestMethod -Uri $args[0])`
179+
env := []string{fmt.Sprintf("WAZA_UPDATE_PARENT_PID=%d", os.Getpid())}
180+
if opts.ExecutablePath != "" {
181+
env = append(env, fmt.Sprintf("WAZA_INSTALL_DIR=%s", filepath.Dir(opts.ExecutablePath)))
182+
}
183+
return updateInstaller{
184+
Name: "PowerShell",
185+
ScriptURL: opts.PowerShellInstallerURL,
186+
Candidates: []string{"pwsh", "powershell"},
187+
Args: []string{"-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script, opts.PowerShellInstallerURL},
188+
Env: env,
189+
Async: true,
190+
}, nil
191+
default:
192+
return updateInstaller{}, fmt.Errorf("unsupported OS for waza update: %s", goos)
193+
}
194+
}
195+
196+
func lookPathAny(lookPath func(string) (string, error), names []string) (string, error) {
197+
var errs []error
198+
for _, name := range names {
199+
path, err := lookPath(name)
200+
if err == nil {
201+
return path, nil
202+
}
203+
errs = append(errs, fmt.Errorf("%s: %w", name, err))
204+
}
205+
return "", errors.Join(errs...)
206+
}
207+
208+
func confirmUpdate(in io.Reader, out io.Writer, installer updateInstaller) (bool, error) {
209+
if _, err := fmt.Fprintf(out, "waza update will download and run the official %s installer:\n %s\n\nContinue? [y/N]: ", installer.Name, installer.ScriptURL); err != nil {
210+
return false, err
211+
}
212+
answer, err := bufio.NewReader(in).ReadString('\n')
213+
if err != nil && !errors.Is(err, io.EOF) {
214+
return false, fmt.Errorf("reading confirmation: %w", err)
215+
}
216+
217+
switch strings.ToLower(strings.TrimSpace(answer)) {
218+
case "y", "yes":
219+
return true, nil
220+
default:
221+
return false, nil
222+
}
223+
}
224+
225+
func missingInstallerError(installer updateInstaller) error {
226+
if installer.Name == "PowerShell" {
227+
return fmt.Errorf("PowerShell is required to run the native Windows waza installer; install PowerShell or download the native Windows binary from %s", latestReleaseURL)
228+
}
229+
return fmt.Errorf("bash is required to run the waza installer; install bash or download a release binary from %s", latestReleaseURL)
230+
}
231+
232+
func missingDependencyError(name string) error {
233+
if name == "curl" {
234+
return fmt.Errorf("curl is required to download the waza installer; install curl or download a release binary from %s", latestReleaseURL)
235+
}
236+
return fmt.Errorf("%s is required to run the waza installer", name)
237+
}

0 commit comments

Comments
 (0)