diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d8037db5..6f70b889 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,13 +17,13 @@ jobs: uses: actions/checkout@v4 - name: Setup Go id: go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: ^1.22 + go-version: ^1.23 - name: Install golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: v1.59 + version: v1.61 - name: Install dependency run: if [ $(uname) == "Darwin" ]; then brew install gnu-sed ;fi - name: Build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 431ad257..0fb8d330 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,9 +14,9 @@ jobs: - name: Check out code uses: actions/checkout@v4 - name: Setup Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: ^1.22 + go-version: ^1.23 - name: Set GOVERSION run: echo "GOVERSION=$(go version | sed -r 's/go version go(.*)\ .*/\1/')" >> $GITHUB_ENV @@ -51,4 +51,4 @@ jobs: platforms: linux/amd64,linux/arm64 tags: cosmtrek/air:${{ env.VERSION }} - name: Show docker image digest - run: echo ${{ steps.docker_build.outputs.digest }} \ No newline at end of file + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/smoke_test_reuse_job.yml b/.github/workflows/smoke_test_reuse_job.yml index 889204eb..77609046 100644 --- a/.github/workflows/smoke_test_reuse_job.yml +++ b/.github/workflows/smoke_test_reuse_job.yml @@ -16,9 +16,9 @@ jobs: uses: actions/checkout@v4 - name: Setup Go id: go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: ^1.22 + go-version: ^1.23 - name: Install run: make install - name: Check rebuild @@ -34,4 +34,4 @@ jobs: - uses: nick-invision/assert-action@v2 with: expected: "PASS" - actual: ${{ steps.check_rebuild.outputs.value }} \ No newline at end of file + actual: ${{ steps.check_rebuild.outputs.value }} diff --git a/.github/workflows/smoke_test_reuse_job_windows.yml b/.github/workflows/smoke_test_reuse_job_windows.yml index ec707558..4a01aa67 100644 --- a/.github/workflows/smoke_test_reuse_job_windows.yml +++ b/.github/workflows/smoke_test_reuse_job_windows.yml @@ -18,7 +18,7 @@ jobs: id: go uses: actions/setup-go@v4 with: - go-version: ^1.22 + go-version: ^1.23 - name: Setup Python uses: actions/setup-python@v5 with: diff --git a/.golangci.yml b/.golangci.yml index c84882c6..ddadae49 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,6 +4,7 @@ run: linters: disable-all: true enable: + - copyloopvar # detects places where loop variables are copied - errcheck # Errcheck is a program for checking for unchecked errors in go programs. - gci # Gci controls Go package import order and makes it always deterministic - goimports # checks that goimports was run @@ -12,5 +13,6 @@ linters: - revive # configurable linter for Go. Drop-in replacement of golint - staticcheck # go vet on steroids - stylecheck # static analysis, finds bugs and performance issues, offers simplifications, and enforces style rules + - testifylint # checks usage of github.com/stretchr/testify - unconvert # Remove unnecessary type conversions - unused # Checks Go code for unused constants, variables, functions and types diff --git a/Dockerfile b/Dockerfile index 3175b207..24c87609 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,18 @@ -FROM golang:1.22 AS builder +FROM golang:1.23 AS builder LABEL maintainer="Rick Yu " ENV GOPATH /go ENV GO111MODULE on -COPY . /go/src/github.com/cosmtrek/air -WORKDIR /go/src/github.com/cosmtrek/air +COPY . /go/src/github.com/air-verse/air +WORKDIR /go/src/github.com/air-verse/air RUN --mount=type=cache,target=/go/pkg/mod go mod download RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build make ci && make install -FROM golang:1.22 +FROM golang:1.23 COPY --from=builder /go/bin/air /go/bin/air diff --git a/Makefile b/Makefile index d2fc38ac..0bd28b93 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ LDFLAGS += -X "main.airVersion=$(AIRVER)" LDFLAGS += -X "main.goVersion=$(shell go version | sed -r 's/go version go(.*)\ .*/\1/')" GO := GO111MODULE=on CGO_ENABLED=0 go -GOLANGCI_LINT_VERSION = v1.56.2 +GOLANGCI_LINT_VERSION = v1.61.0 .PHONY: init init: install-golangci-lint diff --git a/README-zh_cn.md b/README-zh_cn.md index 2e0084b1..2a5d87d5 100644 --- a/README-zh_cn.md +++ b/README-zh_cn.md @@ -1,4 +1,4 @@ -# Air [![Go](https://github.com/air-verse/air/workflows/Go/badge.svg)](https://github.com/air-verse/air/actions?query=workflow%3AGo+branch%3Amaster) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/dcb95264cc504cad9c2a3d8b0795a7f8)](https://www.codacy.com/gh/air-verse/air/dashboard?utm_source=github.com&utm_medium=referral&utm_content=air-verse/air&utm_campaign=Badge_Grade) [![Go Report Card](https://goreportcard.com/badge/github.com/air-verse/air)](https://goreportcard.com/report/github.com/air-verse/air) [![codecov](https://codecov.io/gh/air-verse/air/branch/master/graph/badge.svg)](https://codecov.io/gh/air-verse/air) +# Air [![Go](https://github.com/air-verse/air/actions/workflows/release.yml/badge.svg)](https://github.com/air-verse/air/actions?query=workflow%3AGo+branch%3Amaster) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/dcb95264cc504cad9c2a3d8b0795a7f8)](https://www.codacy.com/gh/air-verse/air/dashboard?utm_source=github.com&utm_medium=referral&utm_content=air-verse/air&utm_campaign=Badge_Grade) [![Go Report Card](https://goreportcard.com/badge/github.com/air-verse/air)](https://goreportcard.com/report/github.com/air-verse/air) [![codecov](https://codecov.io/gh/air-verse/air/branch/master/graph/badge.svg)](https://codecov.io/gh/air-verse/air) :cloud: 热重载 Go 应用的工具 @@ -16,11 +16,11 @@ Air 是为 Go 应用开发设计的另外一个热重载的命令行工具。只 ## 特色 -* 彩色的日志输出 -* 自定义构建或必要的命令 -* 支持外部子目录 -* 在 Air 启动之后,允许监听新创建的路径 -* 更棒的构建过程 +- 彩色的日志输出 +- 自定义构建或必要的命令 +- 支持外部子目录 +- 在 Air 启动之后,允许监听新创建的路径 +- 更棒的构建过程 ### 使用参数覆盖指定配置 @@ -42,7 +42,7 @@ air --build.cmd "go build -o bin/api cmd/run.go" --build.bin "./bin/api" --build ### 使用 `go install` (推荐) -使用 go 1.22 或更高版本: +使用 go 1.23 或更高版本: ```shell go install github.com/air-verse/air@latest @@ -60,7 +60,6 @@ curl -sSfL https://raw.githubusercontent.com/air-verse/air/master/install.sh | s air -v ``` - ### 使用 [goblin.run](https://goblin.run) ```shell @@ -122,6 +121,7 @@ AIR_PORT=8080 air -c "config.toml" ``` 这将用当前目录替换 `$PWD`,`$AIR_PORT` 是要发布的端口,而 `$@` 用于接受应用程序本身的参数,例如 `-c` + ## 使用方法 @@ -205,7 +205,7 @@ services: ```Dockerfile # 选择你想要的版本,>= 1.16 -FROM golang:1.22-alpine +FROM golang:1.23-alpine WORKDIR /app @@ -259,12 +259,11 @@ export PATH=$PATH:$(go env GOPATH)/bin <---- 请确认这行在您的配置信 ### 如何在静态文件更改时自动重新加载浏览器? - 请参考 [#512](https://github.com/cosmtrek/air/issues/512). -* 确保你的静态文件在 `include_dir`、`include_ext` 或 `include_file` 中。 -* 确保你的 HTML 有一个 `` 标签。 -* 通过配置以下内容开启代理: +- 确保你的静态文件在 `include_dir`、`include_ext` 或 `include_file` 中。 +- 确保你的 HTML 有一个 `` 标签。 +- 通过配置以下内容开启代理: ```toml [proxy] diff --git a/README-zh_tw.md b/README-zh_tw.md index 08cc6f40..8643b07c 100644 --- a/README-zh_tw.md +++ b/README-zh_tw.md @@ -1,6 +1,6 @@ # :cloud: Air - Live reload for Go apps -[![Go](https://github.com/air-verse/air/actions/workflows/release.yml/badge.svg)](https://github.com/air-verse/air/actions?query=workflow%3AGo+branch%3Amaster) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/dcb95264cc504cad9c2a3d8b0795a7f8)](https://www.codacy.com/gh/air-verse/air/dashboard?utm_source=github.com&utm_medium=referral&utm_content=air-verse/air&utm_campaign=Badge_Grade) [![Go Report Card](https://goreportcard.com/badge/github.com/air-verse/air)](https://goreportcard.com/report/github.com/air-verse/air) [![codecov](https://codecov.io/gh/air-verse/air/branch/master/graph/badge.svg)](https://codecov.io/gh/air-verse/air) +[![Go](https://github.com/air-verse/air/actions/workflows/release.yml/badge.svg)](https://github.com/air-verse/air/actions?query=workflow%3AGo+branch%3Amaster) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/dcb95264cc504cad9c2a3d8b0795a7f8)](https://www.codacy.com/gh/air-verse/air/dashboard?utm_source=github.com&utm_medium=referral&utm_content=air-verse/air&utm_campaign=Badge_Grade) [![Go Report Card](https://goreportcard.com/badge/github.com/air-verse/air)](https://goreportcard.com/report/github.com/air-verse/air) [![codecov](https://codecov.io/gh/air-verse/air/branch/master/graph/badge.svg)](https://codecov.io/gh/air-verse/air) ![air](docs/air.png) @@ -16,11 +16,11 @@ Air 是一個另類的自動重新編譯執行命令列工具,用於開發 Go ## 功能列表 -* 彩色的日誌輸出 -* 自訂建立或任何命令 -* 支援排除子目錄 -* 允許在 Air 開始後監視新目錄 -* 更佳的建置過程 +- 彩色的日誌輸出 +- 自訂建立或任何命令 +- 支援排除子目錄 +- 允許在 Air 開始後監視新目錄 +- 更佳的建置過程 ### 用參數覆寫指定的配置 @@ -42,7 +42,7 @@ air --build.cmd "go build -o bin/api cmd/run.go" --build.bin "./bin/api" --build ### 使用 `go install` (推薦) -需要使用 go 1.22 或更高版本: +需要使用 go 1.23 或更高版本: ```bash go install github.com/air-verse/air@latest diff --git a/README.md b/README.md index 59ad8a01..7001bfaf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # :cloud: Air - Live reload for Go apps -[![Go](https://github.com/air-verse/air/actions/workflows/release.yml/badge.svg)](https://github.com/air-verse/air/actions?query=workflow%3AGo+branch%3Amaster) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/dcb95264cc504cad9c2a3d8b0795a7f8)](https://www.codacy.com/gh/air-verse/air/dashboard?utm_source=github.com&utm_medium=referral&utm_content=air-verse/air&utm_campaign=Badge_Grade) [![Go Report Card](https://goreportcard.com/badge/github.com/air-verse/air)](https://goreportcard.com/report/github.com/air-verse/air) [![codecov](https://codecov.io/gh/air-verse/air/branch/master/graph/badge.svg)](https://codecov.io/gh/air-verse/air) +[![Go](https://github.com/air-verse/air/actions/workflows/release.yml/badge.svg)](https://github.com/air-verse/air/actions?query=workflow%3AGo+branch%3Amaster) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/dcb95264cc504cad9c2a3d8b0795a7f8)](https://www.codacy.com/gh/air-verse/air/dashboard?utm_source=github.com&utm_medium=referral&utm_content=air-verse/air&utm_campaign=Badge_Grade) [![Go Report Card](https://goreportcard.com/badge/github.com/air-verse/air)](https://goreportcard.com/report/github.com/air-verse/air) [![codecov](https://codecov.io/gh/air-verse/air/branch/master/graph/badge.svg)](https://codecov.io/gh/air-verse/air) ![air](docs/air.png) @@ -20,16 +20,26 @@ Note: This tool has nothing to do with hot-deploy for production. ## Features -* Colorful log output -* Customize build or any command -* Support excluding subdirectories -* Allow watching new directories after Air started -* Better building process +- Colorful log output +- Customize build or any command +- Support excluding subdirectories +- Allow watching new directories after Air started +- Better building process ### Overwrite specify configuration from arguments Support air config fields as arguments: +You can view the available command-line arguments by running the following commands: + +``` +air -h +``` +or +``` +air --help +``` + If you want to config build command and run command, you can use like the following command without the config file: ```shell @@ -46,7 +56,7 @@ air --build.cmd "go build -o bin/api cmd/run.go" --build.bin "./bin/api" --build ### Via `go install` (Recommended) -With go 1.22 or higher: +With go 1.23 or higher: ```bash go install github.com/air-verse/air@latest @@ -209,7 +219,7 @@ services: ```Dockerfile # Choose whatever you want, version >= 1.16 -FROM golang:1.22-alpine +FROM golang:1.23-alpine WORKDIR /app @@ -245,12 +255,12 @@ services: ```shell export GOPATH=$HOME/xxxxx export PATH=$PATH:$GOROOT/bin:$GOPATH/bin -export PATH=$PATH:$(go env GOPATH)/bin <---- Confirm this line in you profile!!! +export PATH=$PATH:$(go env GOPATH)/bin #Confirm this line in your .profile and make sure to source the .profile if you add it!!! ``` ### Error under wsl when ' is included in the bin -Should use `\` to escape the `' in the bin. related issue: [#305](https://github.com/air-verse/air/issues/305) +Should use `\` to escape the `'` in the bin. related issue: [#305](https://github.com/air-verse/air/issues/305) ### Question: how to do hot compile only and do not run anything? @@ -263,12 +273,11 @@ Should use `\` to escape the `' in the bin. related issue: [#305](https://github ### How to Reload the Browser Automatically on Static File Changes - Refer to issue [#512](https://github.com/air-verse/air/issues/512) for additional details. -* Ensure your static files in `include_dir`, `include_ext`, or `include_file`. -* Ensure your HTML has a `` tag -* Activate the proxy by configuring the following config: +- Ensure your static files in `include_dir`, `include_ext`, or `include_file`. +- Ensure your HTML has a `` tag +- Activate the proxy by configuring the following config: ```toml [proxy] diff --git a/air_example.toml b/air_example.toml index ac00e8c0..32d45ccb 100644 --- a/air_example.toml +++ b/air_example.toml @@ -34,7 +34,7 @@ exclude_regex = ["_test\\.go"] exclude_unchanged = true # Follow symlink for directories follow_symlink = true -# This log file places in your tmp_dir. +# This log file is placed in your tmp_dir. log = "air.log" # Poll files for changes instead of using fsnotify. poll = false @@ -58,6 +58,8 @@ rerun_delay = 500 time = false # Only show main log (silences watcher, build, runner) main_only = false +# silence all logs produced by air +silent = false [color] # Customize each part's color. If no color found, use the raw app log. @@ -74,8 +76,8 @@ clean_on_exit = true clear_on_rebuild = true keep_scroll = true -# Enable live-reloading on the browser. [proxy] - enabled = true - proxy_port = 8090 - app_port = 8080 +# Enable live-reloading on the browser. +enabled = true +proxy_port = 8090 +app_port = 8080 diff --git a/docs/air.png b/docs/air.png index f0948f05..9c198d29 100644 Binary files a/docs/air.png and b/docs/air.png differ diff --git a/go.mod b/go.mod index e579d5eb..eea9d629 100644 --- a/go.mod +++ b/go.mod @@ -1,36 +1,46 @@ module github.com/air-verse/air -go 1.22 +go 1.23 require ( - dario.cat/mergo v1.0.0 - github.com/creack/pty v1.1.21 - github.com/fatih/color v1.16.0 + dario.cat/mergo v1.0.1 + github.com/creack/pty v1.1.23 + github.com/fatih/color v1.17.0 github.com/fsnotify/fsnotify v1.7.0 - github.com/gohugoio/hugo v0.123.3 + github.com/gohugoio/hugo v0.134.3 github.com/pelletier/go-toml v1.9.5 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 ) require ( + github.com/bep/debounce v1.2.1 // indirect github.com/bep/godartsass v1.2.0 // indirect - github.com/bep/godartsass/v2 v2.0.0 // indirect - github.com/bep/golibsass v1.1.1 // indirect + github.com/bep/godartsass/v2 v2.1.0 // indirect + github.com/bep/golibsass v1.2.0 // indirect + github.com/bep/gowebp v0.4.0 // indirect + github.com/bep/lazycache v0.5.0 // indirect github.com/cli/safeexec v1.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/getkin/kin-openapi v0.127.0 // indirect + github.com/gobuffalo/flect v1.0.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/hashstructure v1.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/tdewolff/parse/v2 v2.7.12 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/tdewolff/parse/v2 v2.7.15 // indirect github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/image v0.20.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/tools v0.25.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7293cc8f..19205447 100644 --- a/go.sum +++ b/go.sum @@ -1,38 +1,42 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU= github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw= -github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M= github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU= -github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo= -github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= -github.com/bep/gitmap v1.1.2 h1:zk04w1qc1COTZPPYWDQHvns3y1afOsdRfraFQ3qI840= -github.com/bep/gitmap v1.1.2/go.mod h1:g9VRETxFUXNWzMiuxOwcudo6DfZkW9jOsOW0Ft4kYaY= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bep/gitmap v1.6.0 h1:sDuQMm9HoTL0LtlrfxjbjgAg2wHQd4nkMup2FInYzhA= +github.com/bep/gitmap v1.6.0/go.mod h1:n+3W1f/rot2hynsqEGxGMErPRgT41n9CkGuzPvz9cIw= github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA= github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c= github.com/bep/godartsass v1.2.0 h1:E2VvQrxAHAFwbjyOIExAMmogTItSKodoKuijNrGm5yU= github.com/bep/godartsass v1.2.0/go.mod h1:6LvK9RftsXMxGfsA0LDV12AGc4Jylnu6NgHL+Q5/pE8= -github.com/bep/godartsass/v2 v2.0.0 h1:Ruht+BpBWkpmW+yAM2dkp7RSSeN0VLaTobyW0CiSP3Y= -github.com/bep/godartsass/v2 v2.0.0/go.mod h1:AcP8QgC+OwOXEq6im0WgDRYK7scDsmZCEW62o1prQLo= -github.com/bep/golibsass v1.1.1 h1:xkaet75ygImMYjM+FnHIT3xJn7H0xBA9UxSOJjk8Khw= -github.com/bep/golibsass v1.1.1/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= -github.com/bep/gowebp v0.3.0 h1:MhmMrcf88pUY7/PsEhMgEP0T6fDUnRTMpN8OclDrbrY= -github.com/bep/gowebp v0.3.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI= -github.com/bep/lazycache v0.4.0 h1:X8yVyWNVupPd4e1jV7efi3zb7ZV/qcjKQgIQ5aPbkYI= -github.com/bep/lazycache v0.4.0/go.mod h1:NmRm7Dexh3pmR1EignYR8PjO2cWybFQ68+QgY3VMCSc= +github.com/bep/godartsass/v2 v2.1.0 h1:fq5Y1xYf4diu4tXABiekZUCA+5l/dmNjGKCeQwdy+s0= +github.com/bep/godartsass/v2 v2.1.0/go.mod h1:AcP8QgC+OwOXEq6im0WgDRYK7scDsmZCEW62o1prQLo= +github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI= +github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= +github.com/bep/gowebp v0.4.0 h1:QihuVnvIKbRoeBNQkN0JPMM8ClLmD6V2jMftTFwSK3Q= +github.com/bep/gowebp v0.4.0/go.mod h1:95gtYkAA8iIn1t3HkAPurRCVGV/6NhgaHJ1urz0iIwc= +github.com/bep/imagemeta v0.8.1 h1:tjZLPRftjxU7PTI87o5e5WKOFQ4S9S0engiP1OTpJTI= +github.com/bep/imagemeta v0.8.1/go.mod h1:5piPAq5Qomh07m/dPPCLN3mDJyFusvUG7VwdRD/vX0s= +github.com/bep/lazycache v0.5.0 h1:9FJRrEp/s3BUpGEfTvLhmv50N4dXzoZnyRPU6NOUv0w= +github.com/bep/lazycache v0.5.0/go.mod h1:NmRm7Dexh3pmR1EignYR8PjO2cWybFQ68+QgY3VMCSc= github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ= github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0= -github.com/bep/overlayfs v0.9.1 h1:SL54SV8A3zRkmQ+83Jj4TLE88jadHd5d1L4NpfmqJJs= -github.com/bep/overlayfs v0.9.1/go.mod h1:aYY9W7aXQsGcA7V9x/pzeR8LjEgIxbtisZm8Q7zPz40= +github.com/bep/overlayfs v0.9.2 h1:qJEmFInsW12L7WW7dOTUhnMfyk/fN9OCDEO5Gr8HSDs= +github.com/bep/overlayfs v0.9.2/go.mod h1:aYY9W7aXQsGcA7V9x/pzeR8LjEgIxbtisZm8Q7zPz40= github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI= github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= @@ -40,45 +44,51 @@ github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= -github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= +github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc= github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= -github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= -github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanw/esbuild v0.20.1 h1:ueyMIL19umCcJTSxiBH/QmPipgGt8hEDM24pdfowgEc= -github.com/evanw/esbuild v0.20.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/evanw/esbuild v0.23.1 h1:ociewhY6arjTarKLdrXfDTgy25oxhTZmzP8pfuBTfTA= +github.com/evanw/esbuild v0.23.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8= -github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= +github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= +github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= -github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= -github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= -github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= -github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY= github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ= -github.com/gohugoio/hugo v0.123.3 h1:a96Kex2xrqmrSYAYJ8MKzsKCVvCUPjW3+YyXtsEXRmE= -github.com/gohugoio/hugo v0.123.3/go.mod h1:7AHCGAy5MIFEhnvQMG5DfpVGpgrXfkoZ4z6y0zwQHLQ= -github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.1.0 h1:oFQ3f1M3Ook6amHmbqVu/uBRrQ6yjMDFkIv4HQr0f1Y= -github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.1.0/go.mod h1:g9CCh+Ci2IMbPUrVJuXbBTrA+rIIx5+hDQ4EXYaQDoM= +github.com/gohugoio/hashstructure v0.1.0 h1:kBSTMLMyTXbrJVAxaKI+wv30MMJJxn9Q8kfQtJaZ400= +github.com/gohugoio/hashstructure v0.1.0/go.mod h1:8ohPTAfQLTs2WdzB6k9etmQYclDUeNsIHGPAFejbsEA= +github.com/gohugoio/httpcache v0.7.0 h1:ukPnn04Rgvx48JIinZvZetBfHaWE7I01JR2Q2RrQ3Vs= +github.com/gohugoio/httpcache v0.7.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI= +github.com/gohugoio/hugo v0.134.3 h1:Pn2KECXAAQWCd2uryDcmtzVhNJWGF5Pt6CplQvLcWe0= +github.com/gohugoio/hugo v0.134.3/go.mod h1:/1gnGxlWfAzQarxcQ+tMvKw4e/IMBwy0DFbRxORwOtY= +github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0 h1:MNdY6hYCTQEekY0oAfsxWZU1CDt6iH+tMLgyMJQh/sg= +github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0/go.mod h1:oBdBVuiZ0fv9xd8xflUgt53QxW5jOCb1S+xntcN4SKo= +github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0 h1:7PY5PIJ2mck7v6R52yCFvvYHvsPMEbulgRviw3I9lP4= +github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0/go.mod h1:r8g5S7bHfdj0+9ShBog864ufCsVODKQZNjYYY8OnJpM= github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc= github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4= github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo= @@ -101,12 +111,12 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hairyhenderson/go-codeowners v0.4.0 h1:Wx/tRXb07sCyHeC8mXfio710Iu35uAy5KYiBdLHdv4Q= -github.com/hairyhenderson/go-codeowners v0.4.0/go.mod h1:iJgZeCt+W/GzXo5uchFCqvVHZY2T4TAIpvuVlKVkLxc= +github.com/hairyhenderson/go-codeowners v0.5.0 h1:dpQB+hVHiRc2VVvc2BHxkuM+tmu9Qej/as3apqUbsWc= +github.com/hairyhenderson/go-codeowners v0.5.0/go.mod h1:R3uW1OQXEj2Gu6/OvZ7bt6hr0qdkLvUWPiqNaWnexpo= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= -github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= +github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -120,8 +130,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kyokomi/emoji/v2 v2.2.12 h1:sSVA5nH9ebR3Zji1o31wu3yOwD1zKXQA2z0zUyeit60= -github.com/kyokomi/emoji/v2 v2.2.12/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= +github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U= +github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE= @@ -133,10 +143,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= -github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= @@ -151,8 +159,8 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2D github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= -github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -160,73 +168,75 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= -github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= -github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= -github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/tdewolff/minify/v2 v2.20.17 h1:zGqEDhspr3XjSrQI/56vw9IdAhLAaKTLXWnDBsxNVt8= -github.com/tdewolff/minify/v2 v2.20.17/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM= -github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ= -github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tdewolff/minify/v2 v2.20.37 h1:Q97cx4STXCh1dlWDlNHZniE8BJ2EBL0+2b0n92BJQhw= +github.com/tdewolff/minify/v2 v2.20.37/go.mod h1:L1VYef/jwKw6Wwyk5A+T0mBjjn3mMPgmjjA688RNsxU= +github.com/tdewolff/parse/v2 v2.7.15 h1:hysDXtdGZIRF5UZXwpfn3ZWRbm+ru4l53/ajBRGpCTw= +github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= -github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= -github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= -github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= +github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g= +github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= +github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= +github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= +github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE= -golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= -golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= -golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -244,8 +254,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/main.go b/main.go index 75916cfe..dd64104b 100644 --- a/main.go +++ b/main.go @@ -70,7 +70,7 @@ func GetVersionInfo() versionInfo { //revive:disable:unexported-return } } -func main() { +func printSplash() { versionInfo := GetVersionInfo() fmt.Printf(` __ _ ___ @@ -78,14 +78,13 @@ func main() { /_/--\ |_| |_| \_ %s, built with Go %s `, versionInfo.airVersion, versionInfo.goVersion) +} +func main() { if showVersion { + printSplash() return } - - if debugMode { - fmt.Println("[debug] mode") - } sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) @@ -96,6 +95,12 @@ func main() { return } cfg.WithArgs(cmdArgs) + if !cfg.Log.Silent { + printSplash() + } + if debugMode && !cfg.Log.Silent { + fmt.Println("[debug] mode") + } r, err := runner.NewEngineWithConfig(cfg, debugMode) if err != nil { log.Fatal(err) diff --git a/runner/config.go b/runner/config.go index 78a4eaa5..681be82d 100644 --- a/runner/config.go +++ b/runner/config.go @@ -23,8 +23,8 @@ const ( // Config is the main configuration structure for Air. type Config struct { - Root string `toml:"root"` - TmpDir string `toml:"tmp_dir"` + Root string `toml:"root" usage:"Working directory, . or absolute path, please note that the directories following must be under root"` + TmpDir string `toml:"tmp_dir" usage:"Temporary directory for air"` TestDataDir string `toml:"testdata_dir"` Build cfgBuild `toml:"build"` Color cfgColor `toml:"color"` @@ -35,72 +35,63 @@ type Config struct { } type cfgBuild struct { - PreCmd []string `toml:"pre_cmd"` - Cmd string `toml:"cmd"` - PostCmd []string `toml:"post_cmd"` - Bin string `toml:"bin"` - FullBin string `toml:"full_bin"` - ArgsBin []string `toml:"args_bin"` - Log string `toml:"log"` - IncludeExt []string `toml:"include_ext"` - ExcludeDir []string `toml:"exclude_dir"` - IncludeDir []string `toml:"include_dir"` - ExcludeFile []string `toml:"exclude_file"` - IncludeFile []string `toml:"include_file"` - ExcludeRegex []string `toml:"exclude_regex"` - ExcludeUnchanged bool `toml:"exclude_unchanged"` - FollowSymlink bool `toml:"follow_symlink"` - Poll bool `toml:"poll"` - PollInterval int `toml:"poll_interval"` - Delay int `toml:"delay"` - StopOnError bool `toml:"stop_on_error"` - SendInterrupt bool `toml:"send_interrupt"` - KillDelay time.Duration `toml:"kill_delay"` - Rerun bool `toml:"rerun"` - RerunDelay int `toml:"rerun_delay"` + PreCmd []string `toml:"pre_cmd" usage:"Array of commands to run before each build"` + Cmd string `toml:"cmd" usage:"Just plain old shell command. You could use 'make' as well"` + PostCmd []string `toml:"post_cmd" usage:"Array of commands to run after ^C"` + Bin string `toml:"bin" usage:"Binary file yields from 'cmd'"` + FullBin string `toml:"full_bin" usage:"Customize binary, can setup environment variables when run your app"` + ArgsBin []string `toml:"args_bin" usage:"Add additional arguments when running binary (bin/full_bin)."` + Log string `toml:"log" usage:"This log file is placed in your tmp_dir"` + IncludeExt []string `toml:"include_ext" usage:"Watch these filename extensions"` + ExcludeDir []string `toml:"exclude_dir" usage:"Ignore these filename extensions or directories"` + IncludeDir []string `toml:"include_dir" usage:"Watch these directories if you specified"` + ExcludeFile []string `toml:"exclude_file" usage:"Exclude files"` + IncludeFile []string `toml:"include_file" usage:"Watch these files"` + ExcludeRegex []string `toml:"exclude_regex" usage:"Exclude specific regular expressions"` + ExcludeUnchanged bool `toml:"exclude_unchanged" usage:"Exclude unchanged files"` + FollowSymlink bool `toml:"follow_symlink" usage:"Follow symlink for directories"` + Poll bool `toml:"poll" usage:"Poll files for changes instead of using fsnotify"` + PollInterval int `toml:"poll_interval" usage:"Poll interval (defaults to the minimum interval of 500ms)"` + Delay int `toml:"delay" usage:"It's not necessary to trigger build each time file changes if it's too frequent"` + StopOnError bool `toml:"stop_on_error" usage:"Stop running old binary when build errors occur"` + SendInterrupt bool `toml:"send_interrupt" usage:"Send Interrupt signal before killing process (windows does not support this feature)"` + KillDelay time.Duration `toml:"kill_delay" usage:"Delay after sending Interrupt signal"` + Rerun bool `toml:"rerun" usage:"Rerun binary or not"` + RerunDelay int `toml:"rerun_delay" usage:"Delay after each execution"` regexCompiled []*regexp.Regexp } func (c *cfgBuild) RegexCompiled() ([]*regexp.Regexp, error) { - if len(c.ExcludeRegex) > 0 && len(c.regexCompiled) == 0 { - c.regexCompiled = make([]*regexp.Regexp, 0, len(c.ExcludeRegex)) - for _, s := range c.ExcludeRegex { - re, err := regexp.Compile(s) - if err != nil { - return nil, err - } - c.regexCompiled = append(c.regexCompiled, re) - } - } return c.regexCompiled, nil } type cfgLog struct { - AddTime bool `toml:"time"` - MainOnly bool `toml:"main_only"` + AddTime bool `toml:"time" usage:"Show log time"` + MainOnly bool `toml:"main_only" usage:"Only show main log (silences watcher, build, runner)"` + Silent bool `toml:"silent" usage:"silence all logs produced by air"` } type cfgColor struct { - Main string `toml:"main"` - Watcher string `toml:"watcher"` - Build string `toml:"build"` - Runner string `toml:"runner"` + Main string `toml:"main" usage:"Customize main part's color. If no color found, use the raw app log"` + Watcher string `toml:"watcher" usage:"Customize watcher part's color"` + Build string `toml:"build" usage:"Customize build part's color"` + Runner string `toml:"runner" usage:"Customize runner part's color"` App string `toml:"app"` } type cfgMisc struct { - CleanOnExit bool `toml:"clean_on_exit"` + CleanOnExit bool `toml:"clean_on_exit" usage:"Delete tmp directory on exit"` } type cfgScreen struct { - ClearOnRebuild bool `toml:"clear_on_rebuild"` - KeepScroll bool `toml:"keep_scroll"` + ClearOnRebuild bool `toml:"clear_on_rebuild" usage:"Clear screen on rebuild"` + KeepScroll bool `toml:"keep_scroll" usage:"Keep scroll position after rebuild"` } type cfgProxy struct { - Enabled bool `toml:"enabled"` - ProxyPort int `toml:"proxy_port"` - AppPort int `toml:"app_port"` + Enabled bool `toml:"enabled" usage:"Enable live-reloading on the browser"` + ProxyPort int `toml:"proxy_port" usage:"Port for proxy server"` + AppPort int `toml:"app_port" usage:"Port for your app"` } type sliceTransformer struct{} @@ -186,7 +177,7 @@ func defaultPathConfig() (*Config, error) { for _, name := range []string{dftTOML, dftConf} { cfg, err := readConfByName(name) if err == nil { - if name == dftConf { + if name == dftConf && !cfg.Log.Silent { fmt.Println("`.air.conf` will be deprecated soon, recommend using `.air.toml`.") } return cfg, nil @@ -237,6 +228,7 @@ func defaultConfig() Config { log := cfgLog{ AddTime: false, MainOnly: false, + Silent: false, } color := cfgColor{ Main: "magenta", @@ -305,9 +297,6 @@ func (c *Config) preprocess() error { if c.TestDataDir == "" { c.TestDataDir = "testdata" } - if err != nil { - return err - } ed := c.Build.ExcludeDir for i := range ed { ed[i] = cleanPath(ed[i]) @@ -319,6 +308,19 @@ func (c *Config) preprocess() error { runtimeArgs := flag.Args() c.Build.ArgsBin = append(c.Build.ArgsBin, runtimeArgs...) + // Compile the exclude regexes if there are any patterns in the config file + if len(c.Build.ExcludeRegex) > 0 { + regexCompiled := make([]*regexp.Regexp, len(c.Build.ExcludeRegex)) + for idx, expr := range c.Build.ExcludeRegex { + re, err := regexp.Compile(expr) + if err != nil { + return fmt.Errorf("failed to compile regex %s", expr) + } + regexCompiled[idx] = re + } + c.Build.regexCompiled = regexCompiled + } + c.Build.ExcludeDir = ed if len(c.Build.FullBin) > 0 { c.Build.Bin = c.Build.FullBin @@ -341,7 +343,7 @@ func (c *Config) colorInfo() map[string]string { } func (c *Config) buildLogPath() string { - return filepath.Join(c.tmpPath(), c.Build.Log) + return joinPath(c.tmpPath(), c.Build.Log) } func (c *Config) buildDelay() time.Duration { @@ -363,15 +365,15 @@ func (c *Config) killDelay() time.Duration { } func (c *Config) binPath() string { - return filepath.Join(c.Root, c.Build.Bin) + return joinPath(c.Root, c.Build.Bin) } func (c *Config) tmpPath() string { - return filepath.Join(c.Root, c.TmpDir) + return joinPath(c.Root, c.TmpDir) } func (c *Config) testDataPath() string { - return filepath.Join(c.Root, c.TestDataDir) + return joinPath(c.Root, c.TestDataDir) } func (c *Config) rel(path string) string { @@ -385,7 +387,9 @@ func (c *Config) rel(path string) string { // WithArgs returns a new config with the given arguments added to the configuration. func (c *Config) WithArgs(args map[string]TomlInfo) { for _, value := range args { - if value.Value != nil && *value.Value != unsetDefault { + // Ignore values that match the default configuration. + // This ensures user-specified configurations are not overwritten by default values. + if value.Value != nil && *value.Value != value.fieldValue { v := reflect.ValueOf(c) setValue2Struct(v, value.fieldPath, *value.Value) } diff --git a/runner/config_test.go b/runner/config_test.go index 97a1bd2a..34618de2 100644 --- a/runner/config_test.go +++ b/runner/config_test.go @@ -93,7 +93,6 @@ func TestDefaultPathConfig(t *testing.T) { }} for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Setenv(airWd, tt.path) c, err := defaultPathConfig() diff --git a/runner/engine.go b/runner/engine.go index 0a69fa10..ce2fa843 100644 --- a/runner/engine.go +++ b/runner/engine.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" "sync" + "sync/atomic" "time" "github.com/gohugoio/hugo/watcher/filenotify" @@ -16,22 +17,25 @@ import ( // Engine ... type Engine struct { - config *Config + config *Config + + exiter exiter proxy *Proxy logger *logger watcher filenotify.FileWatcher debugMode bool runArgs []string - running bool + running atomic.Bool eventCh chan string watcherStopCh chan bool buildRunCh chan bool buildRunStopCh chan bool - binStopCh chan bool - exitCh chan bool + // binStopCh is a channel for process termination control + // Type chan<- chan int indicates it's a send-only channel that transmits another channel(chan int) + binStopCh chan<- chan int + exitCh chan bool - procKillWg sync.WaitGroup mu sync.RWMutex watchers uint fileChecksums *checksumMap @@ -48,6 +52,7 @@ func NewEngineWithConfig(cfg *Config, debugMode bool) (*Engine, error) { } e := Engine{ config: cfg, + exiter: defaultExiter{}, proxy: NewProxy(&cfg.Proxy), logger: logger, watcher: watcher, @@ -57,7 +62,6 @@ func NewEngineWithConfig(cfg *Config, debugMode bool) (*Engine, error) { watcherStopCh: make(chan bool, 10), buildRunCh: make(chan bool, 1), buildRunStopCh: make(chan bool, 1), - binStopCh: make(chan bool), exitCh: make(chan bool), fileChecksums: &checksumMap{m: make(map[string]string)}, watchers: 0, @@ -192,7 +196,7 @@ func (e *Engine) cacheFileChecksums(root string) error { } } - if e.isExcludeFile(path) || !e.isIncludeExt(path) { + if e.isExcludeFile(path) || !e.isIncludeExt(path) && !e.checkIncludeFile(path) { e.watcherDebug("!exclude checksum %s", e.config.rel(path)) return nil } @@ -257,7 +261,7 @@ func (e *Engine) watchPath(path string) error { if excludeRegex { break } - if !e.isIncludeExt(ev.Name) { + if !e.isIncludeExt(ev.Name) && !e.checkIncludeFile(ev.Name) { break } e.watcherDebug("%s has changed", e.config.rel(ev.Name)) @@ -316,7 +320,7 @@ func (e *Engine) start() { e.mainLog("Proxy server listening on http://localhost%s", e.proxy.server.Addr) } - e.running = true + e.running.Store(true) firstRunCh := make(chan bool, 1) firstRunCh <- true @@ -328,7 +332,7 @@ func (e *Engine) start() { e.mainDebug("exit in start") return case filename = <-e.eventCh: - if !e.isIncludeExt(filename) { + if !e.isIncludeExt(filename) && !e.checkIncludeFile(filename) { continue } if e.config.Build.ExcludeUnchanged { @@ -338,7 +342,7 @@ func (e *Engine) start() { } } - // cannot set buldDelay to 0, because when the write multiple events received in short time + // cannot set buildDelay to 0, because when the write multiple events received in short time // it will start Multiple buildRuns: https://github.com/air-verse/air/issues/473 time.Sleep(e.config.buildDelay()) e.flushEvents() @@ -366,10 +370,8 @@ func (e *Engine) start() { } // if current app is running, stop it - e.withLock(func() { - close(e.binStopCh) - e.binStopCh = make(chan bool) - }) + e.stopBin() + go e.buildRun() } } @@ -392,10 +394,20 @@ func (e *Engine) buildRun() { return } } - if err = e.building(); err != nil { + if output, err := e.building(); err != nil { e.buildLog("failed to build, error: %s", err.Error()) _ = e.writeBuildErrorLog(err.Error()) if e.config.Build.StopOnError { + // It only makes sense to run it if we stop on error. Otherwise when + // running the binary again the error modal will be overwritten by + // the reload. + if e.config.Proxy.Enabled { + e.proxy.BuildFailed(BuildFailedMsg{ + Error: err.Error(), + Command: e.config.Build.Cmd, + Output: output, + }) + } return } } @@ -444,14 +456,36 @@ func (e *Engine) runCommand(command string) error { return nil } +func (e *Engine) runCommandCopyOutput(command string) (string, error) { + // both stdout and stderr are piped to the same buffer, so ignore the second + // one + cmd, stdout, _, err := e.startCmd(command) + if err != nil { + return "", err + } + defer func() { + stdout.Close() + }() + + stdoutBytes, _ := io.ReadAll(stdout) + _, _ = io.Copy(os.Stdout, strings.NewReader(string(stdoutBytes))) + + // wait for command to finish + err = cmd.Wait() + if err != nil { + return string(stdoutBytes), err + } + return string(stdoutBytes), nil +} + // run cmd option in .air.toml -func (e *Engine) building() error { +func (e *Engine) building() (string, error) { e.buildLog("building...") - err := e.runCommand(e.config.Build.Cmd) + output, err := e.runCommandCopyOutput(e.config.Build.Cmd) if err != nil { - return err + return output, err } - return nil + return output, nil } // run pre_cmd option in .air.toml @@ -479,41 +513,64 @@ func (e *Engine) runPostCmd() error { } func (e *Engine) runBin() error { - killFunc := func(cmd *exec.Cmd, stdout io.ReadCloser, stderr io.ReadCloser, killCh chan struct{}, processExit chan struct{}) { - select { - // listen to binStopCh - // cleanup() will close binStopCh when engine stop - // start() will close binStopCh when file changed - case <-e.binStopCh: - close(killCh) - break - - // the process is exited, return - case <-processExit: - return - } + // killFunc returns a chan of chan of int that should be used to shutdown the bin currently being run + // The chan int that is passed in will be used to signal completion of the shutdown + killFunc := func(cmd *exec.Cmd, stdout io.ReadCloser, stderr io.ReadCloser, killCh chan<- struct{}, processExit <-chan struct{}) chan<- chan int { + shutdown := make(chan chan int) + var closer chan int + + go func() { + defer func() { + stdout.Close() + stderr.Close() + }() - e.mainDebug("trying to kill pid %d, cmd %+v", cmd.Process.Pid, cmd.Args) - defer func() { - stdout.Close() - stderr.Close() - }() - pid, err := e.killCmd(cmd) - if err != nil { - e.mainDebug("failed to kill PID %d, error: %s", pid, err.Error()) - if cmd.ProcessState != nil && !cmd.ProcessState.Exited() { - os.Exit(1) + select { + case closer = <-shutdown: + // stopBin has been called from start or cleanup + // defer the signalling of shutdown completion before attempting to kill further down + defer close(closer) + defer close(killCh) + case <-processExit: + // the process is exited, return + e.withLock(func() { + // Avoid deadlocking any racing shutdown request + select { + case c := <-shutdown: + close(c) + default: + } + e.binStopCh = nil + }) + return } - } else { - e.mainDebug("cmd killed, pid: %d", pid) - } - cmdBinPath := cmdPath(e.config.rel(e.config.binPath())) - if _, err = os.Stat(cmdBinPath); os.IsNotExist(err) { - return - } - if err = os.Remove(cmdBinPath); err != nil { - e.mainLog("failed to remove %s, error: %s", e.config.rel(e.config.binPath()), err) - } + + e.mainDebug("trying to kill pid %d, cmd %+v", cmd.Process.Pid, cmd.Args) + + pid, err := e.killCmd(cmd) + if err != nil { + e.mainDebug("failed to kill PID %d, error: %s", pid, err.Error()) + if cmd.ProcessState != nil && !cmd.ProcessState.Exited() { + // Pass a non zero exit code to the closer to delegate the + // decision wether to os.Exit or not + closer <- 1 + } + } else { + e.mainDebug("cmd killed, pid: %d", pid) + } + + if e.config.Build.StopOnError { + cmdBinPath := cmdPath(e.config.rel(e.config.binPath())) + if _, err = os.Stat(cmdBinPath); os.IsNotExist(err) { + return + } + if err = os.Remove(cmdBinPath); err != nil { + e.mainLog("failed to remove %s, error: %s", e.config.rel(e.config.binPath()), err) + } + } + }() + + return shutdown } e.runnerLog("running...") @@ -534,19 +591,23 @@ func (e *Engine) runBin() error { case <-killCh: return default: - e.procKillWg.Add(1) command := strings.Join(append([]string{e.config.Build.Bin}, e.runArgs...), " ") - cmd, stdout, stderr, _ := e.startCmd(command) + cmd, stdout, stderr, err := e.startCmd(command) + if err != nil { + e.mainLog("failed to start %s, error: %s", e.config.rel(e.config.binPath()), err.Error()) + close(killCh) + continue + } + processExit := make(chan struct{}) e.mainDebug("running process pid %v", cmd.Process.Pid) if e.config.Proxy.Enabled { e.proxy.Reload() } + e.stopBin() e.withLock(func() { - close(e.binStopCh) - e.binStopCh = make(chan bool) - go killFunc(cmd, stdout, stderr, killCh, processExit) + e.binStopCh = killFunc(cmd, stdout, stderr, killCh, processExit) }) go func() { @@ -560,6 +621,7 @@ func (e *Engine) runBin() error { }() state, _ := cmd.Process.Wait() close(processExit) + switch state.ExitCode() { case 0: e.runnerLog("Process Exit with Code 0") @@ -568,7 +630,6 @@ func (e *Engine) runBin() error { default: e.runnerLog("Process Exit with Code: %v", state.ExitCode()) } - e.procKillWg.Done() if !e.config.Build.Rerun { return @@ -581,9 +642,37 @@ func (e *Engine) runBin() error { return nil } +func (e *Engine) stopBin() { + e.mainDebug("initiating shutdown sequence") + start := time.Now() + e.mainDebug("shutdown completed in %v", time.Since(start)) + + exitCode := make(chan int) + + e.withLock(func() { + if e.binStopCh != nil { + e.mainDebug("sending shutdown command to killfunc") + e.binStopCh <- exitCode + e.binStopCh = nil + } else { + close(exitCode) + } + }) + + select { + case ret := <-exitCode: + if ret != 0 { + e.exiter.Exit(ret) // Use exiter instead of direct os.Exit, it's for tests purpose. + } + case <-time.After(5 * time.Second): + e.mainDebug("timed out waiting for process exit") + } +} + func (e *Engine) cleanup() { e.mainLog("cleaning...") defer e.mainLog("see you again~") + defer e.mainDebug("exited") if e.config.Proxy.Enabled { e.mainDebug("powering down the proxy...") @@ -592,11 +681,8 @@ func (e *Engine) cleanup() { } } - e.withLock(func() { - close(e.binStopCh) - e.binStopCh = make(chan bool) - }) - e.mainDebug("waiting for close watchers..") + e.stopBin() + e.mainDebug("waiting for close watchers..") e.withLock(func() { for i := 0; i < int(e.watchers); i++ { @@ -619,10 +705,7 @@ func (e *Engine) cleanup() { } } - e.mainDebug("waiting for exit...") - e.procKillWg.Wait() - e.running = false - e.mainDebug("exited") + e.running.Store(false) } // Stop the air diff --git a/runner/engine_test.go b/runner/engine_test.go index a4529f49..2155743b 100644 --- a/runner/engine_test.go +++ b/runner/engine_test.go @@ -70,6 +70,10 @@ func TestRegexes(t *testing.T) { t.Fatalf("Should not be fail: %s.", err) } engine.config.Build.ExcludeRegex = []string{"foo\\.html$", "bar", "_test\\.go"} + err = engine.config.preprocess() + if err != nil { + t.Fatalf("Should not be fail: %s.", err) + } result, err := engine.isExcludeRegex("./test/foo.html") if err != nil { @@ -337,7 +341,7 @@ func TestCtrlCWhenHaveKillDelay(t *testing.T) { t.Fatalf("Should not be fail: %s.", err) } time.Sleep(time.Second * 3) - assert.False(t, engine.running) + assert.False(t, engine.running.Load()) } func TestCtrlCWhenREngineIsRunning(t *testing.T) { @@ -373,7 +377,7 @@ func TestCtrlCWhenREngineIsRunning(t *testing.T) { if err != nil { t.Fatalf("Should not be fail: %s.", err) } - assert.False(t, engine.running) + assert.False(t, engine.running.Load()) } func TestCtrlCWithFailedBin(t *testing.T) { @@ -451,7 +455,7 @@ func TestFixCloseOfChannelAfterCtrlC(t *testing.T) { if err := waitingPortConnectionRefused(t, port, time.Second*10); err != nil { t.Fatalf("Should not be fail: %s.", err) } - assert.False(t, engine.running) + assert.False(t, engine.running.Load()) } func TestFixCloseOfChannelAfterTwoFailedBuild(t *testing.T) { @@ -494,7 +498,7 @@ func TestFixCloseOfChannelAfterTwoFailedBuild(t *testing.T) { // ctrl + c sigs <- syscall.SIGINT time.Sleep(time.Second * 1) - assert.False(t, engine.running) + assert.False(t, engine.running.Load()) } // waitingPortReady waits until the port is ready to be used. @@ -774,7 +778,7 @@ func TestWriteDefaultConfig(t *testing.T) { if err != nil { t.Fatal(err) } - // check the file is exist + // check the file exists if _, err := os.Stat(configName); err != nil { t.Fatal(err) } @@ -857,21 +861,23 @@ func Test(t *testing.T) { t.Fatal(err) } // run sed - // check the file is exist + // check the file exists if _, err := os.Stat(dftTOML); err != nil { t.Fatal(err) } // check is MacOS var cmd *exec.Cmd + toolName := "sed" + if runtime.GOOS == "darwin" { - cmd = exec.Command("gsed", "-i", "s/\"_test.*go\"//g", ".air.toml") - } else { - cmd = exec.Command("sed", "-i", "s/\"_test.*go\"//g", ".air.toml") + toolName = "gsed" } + + cmd = exec.Command(toolName, "-i", "s/\"_test.*go\"//g", ".air.toml") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - t.Fatal(err) + t.Skipf("unable to run %s, make sure the tool is installed to run this test", toolName) } time.Sleep(time.Second * 2) @@ -993,3 +999,145 @@ include_file = ["main.sh"] } assert.Equal(t, []byte("modified"), bytes) } + +func TestShouldIncludeIncludedFileWithoutIncludedExt(t *testing.T) { + port, f := GetPort() + f() + t.Logf("port: %d", port) + + tmpDir := initTestEnv(t, port) + + chdir(t, tmpDir) + + config := ` +[build] +cmd = "true" # do nothing +full_bin = "sh main.sh" +include_ext = ["go"] +include_dir = ["nonexist"] # prevent default "." watch from taking effect +include_file = ["main.sh"] +` + if err := os.WriteFile(dftTOML, []byte(config), 0o644); err != nil { + t.Fatal(err) + } + + err := os.WriteFile("main.sh", []byte("#!/bin/sh\nprintf original > output"), 0o755) + if err != nil { + t.Fatal(err) + } + + engine, err := NewEngine(dftTOML, false) + if err != nil { + t.Fatal(err) + } + go func() { + engine.Run() + }() + + time.Sleep(time.Second * 1) + + bytes, err := os.ReadFile("output") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, []byte("original"), bytes) + + t.Logf("start change main.sh") + go func() { + err = os.WriteFile("main.sh", []byte("#!/bin/sh\nprintf modified > output"), 0o755) + if err != nil { + log.Fatalf("Error updating file: %s.", err) + } + }() + + time.Sleep(time.Second * 3) + + bytes, err = os.ReadFile("output") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, []byte("modified"), bytes) +} + +type testExiter struct { + t *testing.T + called bool + expectCode int +} + +func (te *testExiter) Exit(code int) { + te.called = true + if code != te.expectCode { + te.t.Fatalf("expected exit code %d, got %d", te.expectCode, code) + } +} + +func TestEngineExit(t *testing.T) { + tests := []struct { + name string + setup func(*Engine, chan<- int) + expectCode int + wantCalled bool + }{ + { + name: "normal exit - no error", + setup: func(_ *Engine, exitCode chan<- int) { + go func() { + exitCode <- 0 + }() + }, + expectCode: 0, + wantCalled: false, + }, + { + name: "error exit - non-zero code", + setup: func(_ *Engine, exitCode chan<- int) { + go func() { + exitCode <- 1 + }() + }, + expectCode: 1, + wantCalled: true, + }, + { + name: "process timeout", + setup: func(_ *Engine, _ chan<- int) { + }, + expectCode: 0, + wantCalled: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e, err := NewEngine("", true) + if err != nil { + t.Fatal(err) + } + + exiter := &testExiter{ + t: t, + expectCode: tt.expectCode, + } + e.exiter = exiter + + exitCode := make(chan int) + + if tt.setup != nil { + tt.setup(e, exitCode) + } + select { + case ret := <-exitCode: + if ret != 0 { + e.exiter.Exit(ret) + } + case <-time.After(1 * time.Millisecond): + // timeout case + } + + if tt.wantCalled != exiter.called { + t.Errorf("Exit() called = %v, want %v", exiter.called, tt.wantCalled) + } + }) + } +} diff --git a/runner/exiter.go b/runner/exiter.go new file mode 100644 index 00000000..21cf850d --- /dev/null +++ b/runner/exiter.go @@ -0,0 +1,13 @@ +package runner + +import "os" + +type exiter interface { + Exit(code int) +} + +type defaultExiter struct{} + +func (d defaultExiter) Exit(code int) { + os.Exit(code) +} diff --git a/runner/flag.go b/runner/flag.go index af8f478b..1844ce27 100644 --- a/runner/flag.go +++ b/runner/flag.go @@ -4,14 +4,12 @@ import ( "flag" ) -const unsetDefault = "DEFAULT" - // ParseConfigFlag parse toml information for flag func ParseConfigFlag(f *flag.FlagSet) map[string]TomlInfo { - c := Config{} + c := defaultConfig() m := flatConfig(c) for k, v := range m { - f.StringVar(v.Value, k, unsetDefault, "") + f.StringVar(v.Value, k, v.fieldValue, v.usage) } return m } diff --git a/runner/flag_test.go b/runner/flag_test.go index 8c14455b..abfa45e5 100644 --- a/runner/flag_test.go +++ b/runner/flag_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFlag(t *testing.T) { @@ -54,7 +55,7 @@ func TestFlag(t *testing.T) { t.Run(tc.name, func(t *testing.T) { flag := flag.NewFlagSet(t.Name(), flag.ExitOnError) cmdArgs := ParseConfigFlag(flag) - assert.NoError(t, flag.Parse(tc.args)) + require.NoError(t, flag.Parse(tc.args)) assert.Equal(t, tc.expected, *cmdArgs[tc.key].Value) }) } @@ -98,7 +99,7 @@ func TestConfigRuntimeArgs(t *testing.T) { args: []string{"--build.exclude_unchanged", "true"}, key: "build.exclude_unchanged", check: func(t *testing.T, conf *Config) { - assert.Equal(t, true, conf.Build.ExcludeUnchanged) + assert.True(t, conf.Build.ExcludeUnchanged) }, }, { @@ -121,7 +122,7 @@ func TestConfigRuntimeArgs(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { dir := t.TempDir() - assert.NoError(t, os.Chdir(dir)) + require.NoError(t, os.Chdir(dir)) flag := flag.NewFlagSet(t.Name(), flag.ExitOnError) cmdArgs := ParseConfigFlag(flag) _ = flag.Parse(tc.args) diff --git a/runner/logger.go b/runner/logger.go index d76d5cd8..3675c2fe 100644 --- a/runner/logger.go +++ b/runner/logger.go @@ -53,6 +53,9 @@ func newLogFunc(colorname string, cfg cfgLog) logFunc { return func(msg string, v ...interface{}) { // There are some escape sequences to format color in terminal, so cannot // just trim new line from right. + if cfg.Silent { + return + } msg = strings.ReplaceAll(msg, "\n", "") msg = strings.TrimSpace(msg) if len(msg) == 0 { diff --git a/runner/proxy.go b/runner/proxy.go index 6e3bad76..09121ef3 100644 --- a/runner/proxy.go +++ b/runner/proxy.go @@ -2,21 +2,24 @@ package runner import ( "bytes" - "errors" + _ "embed" "fmt" "io" "log" "net/http" "strconv" "strings" - "syscall" "time" ) -type Reloader interface { +//go:embed proxy.js +var ProxyScript string + +type Streamer interface { AddSubscriber() *Subscriber RemoveSubscriber(id int32) Reload() + BuildFailed(msg BuildFailedMsg) Stop() } @@ -24,7 +27,7 @@ type Proxy struct { server *http.Server client *http.Client config *cfgProxy - stream Reloader + stream Streamer } func NewProxy(cfg *cfgProxy) *Proxy { @@ -45,7 +48,7 @@ func NewProxy(cfg *cfgProxy) *Proxy { func (p *Proxy) Run() { http.HandleFunc("/", p.proxyHandler) - http.HandleFunc("/internal/reload", p.reloadHandler) + http.HandleFunc("/__air_internal/sse", p.reloadHandler) if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(p.Stop()) } @@ -55,6 +58,10 @@ func (p *Proxy) Reload() { p.stream.Reload() } +func (p *Proxy) BuildFailed(msg BuildFailedMsg) { + p.stream.BuildFailed(msg) +} + func (p *Proxy) injectLiveReload(resp *http.Response) (string, error) { buf := new(bytes.Buffer) if _, err := buf.ReadFrom(resp.Body); err != nil { @@ -68,16 +75,11 @@ func (p *Proxy) injectLiveReload(resp *http.Response) (string, error) { return page, nil } - script := fmt.Sprintf( - ``, - p.config.ProxyPort, - ) + script := "" return page[:body] + script + page[body:], nil } func (p *Proxy) proxyHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - appURL := r.URL appURL.Scheme = "http" appURL.Host = fmt.Sprintf("localhost:%d", p.config.AppPort) @@ -106,18 +108,24 @@ func (p *Proxy) proxyHandler(w http.ResponseWriter, r *http.Request) { } req.Header.Set("X-Forwarded-For", r.RemoteAddr) - // retry on connection refused error since after a file change air will restart the server and it may take a few milliseconds for the server to be up-and-running. + // set the via header + viaHeaderValue := fmt.Sprintf("%s %s", r.Proto, r.Host) + req.Header.Set("Via", viaHeaderValue) + + // air will restart the server. it may take a few milliseconds for it to start back up. + // therefore, we retry until the server becomes available or this retry loop exits with an error. var resp *http.Response + resp, err = p.client.Do(req) for i := 0; i < 10; i++ { - resp, err = p.client.Do(req) if err == nil { break } - if !errors.Is(err, syscall.ECONNREFUSED) { - http.Error(w, "proxy handler: unable to reach app", http.StatusInternalServerError) - return - } time.Sleep(100 * time.Millisecond) + resp, err = p.client.Do(req) + } + if err != nil { + http.Error(w, "proxy handler: unable to reach app", http.StatusInternalServerError) + return } defer resp.Body.Close() @@ -130,6 +138,8 @@ func (p *Proxy) proxyHandler(w http.ResponseWriter, r *http.Request) { w.Header().Add(k, v) } } + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Add("Via", viaHeaderValue) w.WriteHeader(resp.StatusCode) if !strings.Contains(resp.Header.Get("Content-Type"), "text/html") { @@ -173,8 +183,8 @@ func (p *Proxy) reloadHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) flusher.Flush() - for range sub.reloadCh { - fmt.Fprintf(w, "data: reload\n\n") + for msg := range sub.msgCh { + fmt.Fprint(w, msg.AsSSE()) flusher.Flush() } } diff --git a/runner/proxy.js b/runner/proxy.js new file mode 100644 index 00000000..c9d0233d --- /dev/null +++ b/runner/proxy.js @@ -0,0 +1,86 @@ +(() => { + const eventSource = new EventSource("/__air_internal/sse"); + + eventSource.addEventListener('reload', () => { + location.reload(); + }); + + eventSource.addEventListener('build-failed', (event) => { + const data = JSON.parse(event.data); + showErrorInModal(data); + }); + + function showErrorInModal(data) { + document.body.insertAdjacentHTML(`beforeend`, ` + +
+
+
Build Error
+
+ +
+
+ `); + const modal = document.getElementById('air__modal'); + const modalBody = document.getElementById('air__modal-body'); + const modalClose = document.getElementById('air__modal-close'); + modalBody.innerHTML = ` + Build Cmd:
${data.command}

+ Output:
${data.output}

+ Error:
${data.error}
+ `; + modal.style.display = 'flex'; + + modalClose.addEventListener('click', () => { + modal.style.display = 'none'; + }); + } +})(); diff --git a/runner/proxy_stream.go b/runner/proxy_stream.go index f3a4bd47..6d1048e6 100644 --- a/runner/proxy_stream.go +++ b/runner/proxy_stream.go @@ -1,6 +1,8 @@ package runner import ( + "encoding/json" + "fmt" "sync" "sync/atomic" ) @@ -11,9 +13,27 @@ type ProxyStream struct { count atomic.Int32 } +type StreamMessageType string + +const ( + StreamMessageReload StreamMessageType = "reload" + StreamMessageBuildFailed StreamMessageType = "build-failed" +) + +type StreamMessage struct { + Type StreamMessageType + Data interface{} +} + +type BuildFailedMsg struct { + Error string `json:"error"` + Command string `json:"command"` + Output string `json:"output"` +} + type Subscriber struct { - id int32 - reloadCh chan struct{} + id int32 + msgCh chan StreamMessage } func NewProxyStream() *ProxyStream { @@ -32,7 +52,7 @@ func (stream *ProxyStream) AddSubscriber() *Subscriber { defer stream.mu.Unlock() stream.count.Add(1) - sub := &Subscriber{id: stream.count.Load(), reloadCh: make(chan struct{})} + sub := &Subscriber{id: stream.count.Load(), msgCh: make(chan StreamMessage)} stream.subscribers[stream.count.Load()] = sub return sub } @@ -42,13 +62,39 @@ func (stream *ProxyStream) RemoveSubscriber(id int32) { defer stream.mu.Unlock() if _, ok := stream.subscribers[id]; ok { - close(stream.subscribers[id].reloadCh) + close(stream.subscribers[id].msgCh) delete(stream.subscribers, id) } } func (stream *ProxyStream) Reload() { for _, sub := range stream.subscribers { - sub.reloadCh <- struct{}{} + sub.msgCh <- StreamMessage{ + Type: StreamMessageReload, + Data: nil, + } + } +} + +func (stream *ProxyStream) BuildFailed(err BuildFailedMsg) { + for _, sub := range stream.subscribers { + sub.msgCh <- StreamMessage{ + Type: StreamMessageBuildFailed, + Data: err, + } + } +} + +func (m StreamMessage) AsSSE() string { + s := "event: " + string(m.Type) + "\n" + s += "data: " + stringify(m.Data) + "\n" + return s + "\n" +} + +func stringify(v any) string { + b, err := json.Marshal(v) + if err != nil { + return fmt.Sprintf("{\"error\":\"Failed to marshal message: %s\"}", err) } + return string(b) } diff --git a/runner/proxy_stream_test.go b/runner/proxy_stream_test.go index ca1e78cb..bf4a0978 100644 --- a/runner/proxy_stream_test.go +++ b/runner/proxy_stream_test.go @@ -4,6 +4,8 @@ import ( "sync" "sync/atomic" "testing" + + "github.com/stretchr/testify/assert" ) func find(s map[int32]*Subscriber, id int32) bool { @@ -43,7 +45,7 @@ func TestProxyStream(t *testing.T) { wg.Add(1) go func(sub *Subscriber) { defer wg.Done() - <-sub.reloadCh + <-sub.msgCh reloadCount.Add(1) }(sub) } @@ -69,3 +71,20 @@ func TestProxyStream(t *testing.T) { t.Errorf("expected subscribers count to be %d, got %d", exp, got) } } + +func TestBuildFailureMessage(t *testing.T) { + stream := NewProxyStream() + sub := stream.AddSubscriber() + + msg := BuildFailedMsg{ + Error: "build failed", + Command: "go build", + Output: "error output", + } + + go stream.BuildFailed(msg) + + received := <-sub.msgCh + assert.Equal(t, StreamMessageBuildFailed, received.Type) + assert.Equal(t, msg, received.Data) +} diff --git a/runner/proxy_test.go b/runner/proxy_test.go index a14ec3d6..69f86631 100644 --- a/runner/proxy_test.go +++ b/runner/proxy_test.go @@ -15,24 +15,26 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type reloader struct { subCh chan struct{} - reloadCh chan struct{} + reloadCh chan StreamMessage } func (r *reloader) AddSubscriber() *Subscriber { r.subCh <- struct{}{} - return &Subscriber{reloadCh: r.reloadCh} + return &Subscriber{msgCh: r.reloadCh} } func (r *reloader) RemoveSubscriber(_ int32) { close(r.subCh) } -func (r *reloader) Reload() {} -func (r *reloader) Stop() {} +func (r *reloader) Reload() {} +func (r *reloader) BuildFailed(BuildFailedMsg) {} +func (r *reloader) Stop() {} var proxyPort = 8090 @@ -97,8 +99,8 @@ func TestProxy_proxyHandler(t *testing.T) { return req }, assert: func(resp *http.Request) { - assert.NoError(t, resp.ParseForm()) - assert.Equal(t, resp.Form.Get("foo"), "bar") + require.NoError(t, resp.ParseForm()) + assert.Equal(t, "bar", resp.Form.Get("foo")) }, }, { @@ -108,7 +110,7 @@ func TestProxy_proxyHandler(t *testing.T) { }, assert: func(resp *http.Request) { q := resp.URL.Query() - assert.Equal(t, q.Encode(), "q=air") + assert.Equal(t, "q=air", q.Encode()) }, }, { @@ -124,9 +126,19 @@ func TestProxy_proxyHandler(t *testing.T) { Foo string `json:"foo"` } var r Response - assert.NoError(t, json.NewDecoder(resp.Body).Decode(&r)) - assert.Equal(t, resp.URL.Path, "/a/b/c") - assert.Equal(t, r.Foo, "bar") + require.NoError(t, json.NewDecoder(resp.Body).Decode(&r)) + assert.Equal(t, "/a/b/c", resp.URL.Path) + assert.Equal(t, "bar", r.Foo) + }, + }, + { + name: "set_via_header", + req: func() *http.Request { + req := httptest.NewRequest("GET", fmt.Sprintf("http://localhost:%d", proxyPort), nil) + return req + }, + assert: func(resp *http.Request) { + assert.Equal(t, fmt.Sprintf("HTTP/1.1 localhost:%d", proxyPort), resp.Header.Get("Via")) }, }, } @@ -190,7 +202,7 @@ func TestProxy_injectLiveReload(t *testing.T) { }, Body: io.NopCloser(strings.NewReader(`

test

`)), }, - expect: `

test

`, + expect: fmt.Sprintf(`

test

`, ProxyScript), }, } for _, tt := range tests { @@ -200,8 +212,15 @@ func TestProxy_injectLiveReload(t *testing.T) { ProxyPort: 1111, AppPort: 2222, }) - if got, _ := proxy.injectLiveReload(tt.given); got != tt.expect { - t.Errorf("expected page %+v, got %v", tt.expect, got) + got, _ := proxy.injectLiveReload(tt.given) + if got != tt.expect { + // Use a more descriptive error message + if len(got) > 100 || len(tt.expect) > 100 { + t.Errorf("Script injection mismatch.\nGot length: %d\nExpected length: %d", + len(got), len(tt.expect)) + } else { + t.Errorf("expected page %+v, got %v", tt.expect, got) + } } }) } @@ -214,7 +233,7 @@ func TestProxy_reloadHandler(t *testing.T) { srvPort := getServerPort(t, srv) defer srv.Close() - reloader := &reloader{subCh: make(chan struct{}), reloadCh: make(chan struct{})} + reloader := &reloader{subCh: make(chan struct{}), reloadCh: make(chan StreamMessage)} cfg := &cfgProxy{ Enabled: true, ProxyPort: proxyPort, @@ -237,11 +256,12 @@ func TestProxy_reloadHandler(t *testing.T) { proxy.reloadHandler(rec, req) }() - // wait for subscriber to be added <-reloader.subCh - // send a reload event and wait for http response - reloader.reloadCh <- struct{}{} + reloader.reloadCh <- StreamMessage{ + Type: StreamMessageReload, + Data: nil, + } close(reloader.reloadCh) wg.Wait() @@ -254,7 +274,22 @@ func TestProxy_reloadHandler(t *testing.T) { if err != nil { t.Errorf("reading body: %v", err) } - if got, exp := string(bodyBytes), "data: reload\n\n"; got != exp { - t.Errorf("expected %q but got %q", exp, got) + + expected := "event: reload\ndata: null\n\n" + if got := string(bodyBytes); got != expected { + t.Errorf("expected %q but got %q", expected, got) + } + + expectedHeaders := map[string]string{ + "Access-Control-Allow-Origin": "*", + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + } + + for key, value := range expectedHeaders { + if got := resp.Header.Get(key); got != value { + t.Errorf("expected header %s to be %q but got %q", key, value, got) + } } } diff --git a/runner/util.go b/runner/util.go index e0b03b85..03d32afe 100644 --- a/runner/util.go +++ b/runner/util.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/hex" "errors" + "fmt" "log" "os" "path/filepath" @@ -21,18 +22,27 @@ const ( ) func (e *Engine) mainLog(format string, v ...interface{}) { + if e.config.Log.Silent { + return + } e.logWithLock(func() { e.logger.main()(format, v...) }) } func (e *Engine) mainDebug(format string, v ...interface{}) { + if e.config.Log.Silent { + return + } if e.debugMode { e.mainLog(format, v...) } } func (e *Engine) buildLog(format string, v ...interface{}) { + if e.config.Log.Silent { + return + } if e.debugMode || !e.config.Log.MainOnly { e.logWithLock(func() { e.logger.build()(format, v...) @@ -41,6 +51,9 @@ func (e *Engine) buildLog(format string, v ...interface{}) { } func (e *Engine) runnerLog(format string, v ...interface{}) { + if e.config.Log.Silent { + return + } if e.debugMode || !e.config.Log.MainOnly { e.logWithLock(func() { e.logger.runner()(format, v...) @@ -49,6 +62,9 @@ func (e *Engine) runnerLog(format string, v ...interface{}) { } func (e *Engine) watcherLog(format string, v ...interface{}) { + if e.config.Log.Silent { + return + } if e.debugMode || !e.config.Log.MainOnly { e.logWithLock(func() { e.logger.watcher()(format, v...) @@ -57,6 +73,9 @@ func (e *Engine) watcherLog(format string, v ...interface{}) { } func (e *Engine) watcherDebug(format string, v ...interface{}) { + if e.config.Log.Silent { + return + } if e.debugMode { e.watcherLog(format, v...) } @@ -297,9 +316,11 @@ func (a *checksumMap) updateFileChecksum(filename, newChecksum string) (ok bool) // TomlInfo is a struct for toml config file type TomlInfo struct { - fieldPath string - field reflect.StructField - Value *string + fieldPath string + field reflect.StructField + Value *string + fieldValue string + usage string } func setValue2Struct(v reflect.Value, fieldName string, value string) { @@ -354,28 +375,57 @@ func setValue2Struct(v reflect.Value, fieldName string, value string) { func flatConfig(stut interface{}) map[string]TomlInfo { m := make(map[string]TomlInfo) t := reflect.TypeOf(stut) - setTage2Map("", t, m, "") + v := reflect.ValueOf(stut) + setTage2Map("", t, v, m, "") return m } -func setTage2Map(root string, t reflect.Type, m map[string]TomlInfo, fieldPath string) { +func getFieldValueString(fieldValue reflect.Value) string { + switch fieldValue.Kind() { + case reflect.Slice: + sliceLen := fieldValue.Len() + strSlice := make([]string, sliceLen) + for j := 0; j < sliceLen; j++ { + strSlice[j] = fmt.Sprintf("%v", fieldValue.Index(j).Interface()) + } + return strings.Join(strSlice, ",") + default: + return fmt.Sprintf("%v", fieldValue.Interface()) + } +} + +func setTage2Map(root string, t reflect.Type, v reflect.Value, m map[string]TomlInfo, fieldPath string) { for i := 0; i < t.NumField(); i++ { field := t.Field(i) + fieldValue := v.Field(i) tomlVal := field.Tag.Get("toml") - switch field.Type.Kind() { - case reflect.Struct: + + if field.Type.Kind() == reflect.Struct { path := fieldPath + field.Name + "." - setTage2Map(root+tomlVal+".", field.Type, m, path) - default: - if tomlVal == "" { - continue - } - tomlPath := root + tomlVal - path := fieldPath + field.Name - var v *string - str := "" - v = &str - m[tomlPath] = TomlInfo{field: field, Value: v, fieldPath: path} + setTage2Map(root+tomlVal+".", field.Type, fieldValue, m, path) + continue + } + + if tomlVal == "" { + continue } + + tomlPath := root + tomlVal + path := fieldPath + field.Name + var v *string + str := "" + v = &str + + fieldValueStr := getFieldValueString(fieldValue) + usage := field.Tag.Get("usage") + m[tomlPath] = TomlInfo{field: field, Value: v, fieldPath: path, fieldValue: fieldValueStr, usage: usage} } } + +func joinPath(root, path string) string { + if filepath.IsAbs(path) { + return path + } + + return filepath.Join(root, path) +} diff --git a/runner/util_linux.go b/runner/util_linux.go index 658671b3..26061d85 100644 --- a/runner/util_linux.go +++ b/runner/util_linux.go @@ -31,5 +31,8 @@ func (e *Engine) killCmd(cmd *exec.Cmd) (pid int, err error) { func (e *Engine) startCmd(cmd string) (*exec.Cmd, io.ReadCloser, io.ReadCloser, error) { c := exec.Command("/bin/sh", "-c", cmd) f, err := pty.Start(c) - return c, f, f, err + if err != nil { + return nil, nil, nil, err + } + return c, f, f, nil } diff --git a/runner/util_test.go b/runner/util_test.go index 275d5b78..3c18cf36 100644 --- a/runner/util_test.go +++ b/runner/util_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIsDirRootPath(t *testing.T) { @@ -219,7 +220,7 @@ func Test_killCmd_SendInterrupt_false(t *testing.T) { // check processes were being killed // read pids from file bytesRead, err := os.ReadFile("pid") - assert.NoError(t, err) + require.NoError(t, err) lines := strings.Split(string(bytesRead), "\n") for _, line := range lines { _, err := strconv.Atoi(line) @@ -283,7 +284,37 @@ func TestCheckIncludeFile(t *testing.T) { }, }, } - assert.Equal(t, e.checkIncludeFile("main.go"), true) - assert.Equal(t, e.checkIncludeFile("no.go"), false) - assert.Equal(t, e.checkIncludeFile("."), false) + assert.True(t, e.checkIncludeFile("main.go")) + assert.False(t, e.checkIncludeFile("no.go")) + assert.False(t, e.checkIncludeFile(".")) +} + +func TestJoinPathRelative(t *testing.T) { + root, err := filepath.Abs("test") + + if err != nil { + t.Fatalf("couldn't get absolute path for testing: %v", err) + } + + result := joinPath(root, "x") + + assert.Equal(t, result, filepath.Join(root, "x")) +} + +func TestJoinPathAbsolute(t *testing.T) { + root, err := filepath.Abs("test") + + if err != nil { + t.Fatalf("couldn't get absolute path for testing: %v", err) + } + + path, err := filepath.Abs("x") + + if err != nil { + t.Fatalf("couldn't get absolute path for testing: %v", err) + } + + result := joinPath(root, path) + + assert.Equal(t, result, path) } diff --git a/runner/util_windows.go b/runner/util_windows.go index eaa09e06..80e7d7b2 100644 --- a/runner/util_windows.go +++ b/runner/util_windows.go @@ -2,16 +2,28 @@ package runner import ( "io" + "os" "os/exec" "strconv" "strings" + "time" ) func (e *Engine) killCmd(cmd *exec.Cmd) (pid int, err error) { pid = cmd.Process.Pid // https://stackoverflow.com/a/44551450 kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(pid)) - return pid, kill.Run() + + if e.config.Build.SendInterrupt { + if err = kill.Run(); err != nil { + return + } + time.Sleep(e.config.killDelay()) + } + err = kill.Run() + // Wait releases any resources associated with the Process. + _, _ = cmd.Process.Wait() + return pid, err } func (e *Engine) startCmd(cmd string) (*exec.Cmd, io.ReadCloser, io.ReadCloser, error) { @@ -20,7 +32,7 @@ func (e *Engine) startCmd(cmd string) (*exec.Cmd, io.ReadCloser, io.ReadCloser, if !strings.Contains(cmd, ".exe") { e.runnerLog("CMD will not recognize non .exe file for execution, path: %s", cmd) } - c := exec.Command("cmd", "/c", cmd) + c := exec.Command("powershell", cmd) stderr, err := c.StderrPipe() if err != nil { return nil, nil, nil, err @@ -29,6 +41,10 @@ func (e *Engine) startCmd(cmd string) (*exec.Cmd, io.ReadCloser, io.ReadCloser, if err != nil { return nil, nil, nil, err } + + c.Stdout = os.Stdout + c.Stderr = os.Stderr + err = c.Start() if err != nil { return nil, nil, nil, err