From aefb9e9a9c97a186b5e564ea4955b198ed45f49c Mon Sep 17 00:00:00 2001 From: mirivlad Date: Tue, 16 Jun 2026 12:11:55 +0800 Subject: [PATCH] feat: add scripts/build.sh, test.sh, check.sh + gofmt + go mod tidy --- go.mod | 3 +- go.sum | 12 +++++ internal/api/app.go | 28 +++++------ internal/core/capability/registry.go | 2 +- internal/core/contribution/registry.go | 36 ++++++------- internal/core/plugin/plugin.go | 70 +++++++++++++------------- main.go | 6 +-- scripts/build.sh | 61 ++++++++++++++++++++++ scripts/check.sh | 56 +++++++++++++++++++++ scripts/test.sh | 41 +++++++++++++++ 10 files changed, 243 insertions(+), 72 deletions(-) create mode 100755 scripts/build.sh create mode 100755 scripts/check.sh create mode 100755 scripts/test.sh diff --git a/go.mod b/go.mod index 0a3956a..02b1816 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/verstak/verstak-desktop go 1.24.4 +require github.com/wailsapp/wails/v2 v2.12.0 + require ( git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect github.com/bep/debounce v1.2.1 // indirect @@ -27,7 +29,6 @@ require ( github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wailsapp/go-webview2 v1.0.22 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.12.0 // indirect golang.org/x/crypto v0.33.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect diff --git a/go.sum b/go.sum index e0e4c46..b5aa9ee 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +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/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= @@ -16,6 +18,8 @@ github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaa github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= @@ -25,6 +29,8 @@ github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNqu github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -34,11 +40,15 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/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/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -69,3 +79,5 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/app.go b/internal/api/app.go index 56890ec..af5f69c 100644 --- a/internal/api/app.go +++ b/internal/api/app.go @@ -11,11 +11,11 @@ import ( // App is the main application struct exposed to the Wails frontend. type App struct { - capRegistry *capability.Registry + capRegistry *capability.Registry contribRegistry *contribution.Registry - permRegistry *permissions.Registry - eventBus *events.Bus - plugins []plugin.Plugin + permRegistry *permissions.Registry + eventBus *events.Bus + plugins []plugin.Plugin } // NewApp creates a new App instance. @@ -27,11 +27,11 @@ func NewApp( plugins []plugin.Plugin, ) *App { return &App{ - capRegistry: capReg, + capRegistry: capReg, contribRegistry: contribReg, - permRegistry: permReg, - eventBus: bus, - plugins: plugins, + permRegistry: permReg, + eventBus: bus, + plugins: plugins, } } @@ -60,12 +60,12 @@ func (a *App) GetPermissions() []permissions.Entry { // GetContributions returns all registered contributions. func (a *App) GetContributions() ContributionSummary { return ContributionSummary{ - Views: a.contribRegistry.Views(), - Commands: a.contribRegistry.Commands(), - SettingsPanels: a.contribRegistry.SettingsPanels(), - SidebarItems: a.contribRegistry.SidebarItems(), - FileActions: a.contribRegistry.FileActions(), - NoteActions: a.contribRegistry.NoteActions(), + Views: a.contribRegistry.Views(), + Commands: a.contribRegistry.Commands(), + SettingsPanels: a.contribRegistry.SettingsPanels(), + SidebarItems: a.contribRegistry.SidebarItems(), + FileActions: a.contribRegistry.FileActions(), + NoteActions: a.contribRegistry.NoteActions(), SearchProviders: a.contribRegistry.SearchProviders(), } } diff --git a/internal/core/capability/registry.go b/internal/core/capability/registry.go index 459f1be..0b08122 100644 --- a/internal/core/capability/registry.go +++ b/internal/core/capability/registry.go @@ -9,7 +9,7 @@ import ( // Registry tracks available capabilities and which plugins provide them. type Registry struct { - mu sync.RWMutex + mu sync.RWMutex capabilities map[string]*Entry // capability name -> entry } diff --git a/internal/core/contribution/registry.go b/internal/core/contribution/registry.go index ac8a2b1..ce1de51 100644 --- a/internal/core/contribution/registry.go +++ b/internal/core/contribution/registry.go @@ -12,60 +12,60 @@ import ( type Registry struct { mu sync.RWMutex - views []ContributionView - commands []ContributionCommand - settingsPanels []ContributionSettingsPanel - sidebarItems []ContributionSidebarItem - fileActions []ContributionAction - noteActions []ContributionAction - contextMenus []ContributionContextMenuEntry - searchProviders []ContributionSearchProvider + views []ContributionView + commands []ContributionCommand + settingsPanels []ContributionSettingsPanel + sidebarItems []ContributionSidebarItem + fileActions []ContributionAction + noteActions []ContributionAction + contextMenus []ContributionContextMenuEntry + searchProviders []ContributionSearchProvider activityProviders []ContributionActivityProvider - statusBarItems []ContributionStatusBarItem + statusBarItems []ContributionStatusBarItem } type ContributionView struct { - PluginID string `json:"pluginId"` + PluginID string `json:"pluginId"` Item plugin.ContributionView `json:"item"` } type ContributionCommand struct { - PluginID string `json:"pluginId"` + PluginID string `json:"pluginId"` Item plugin.ContributionCommand `json:"item"` } type ContributionSettingsPanel struct { - PluginID string `json:"pluginId"` + PluginID string `json:"pluginId"` Item plugin.ContributionSettingsPanel `json:"item"` } type ContributionSidebarItem struct { - PluginID string `json:"pluginId"` + PluginID string `json:"pluginId"` Item plugin.ContributionSidebarItem `json:"item"` } type ContributionAction struct { - PluginID string `json:"pluginId"` + PluginID string `json:"pluginId"` Item plugin.ContributionAction `json:"item"` } type ContributionContextMenuEntry struct { - PluginID string `json:"pluginId"` + PluginID string `json:"pluginId"` Item plugin.ContributionContextMenuEntry `json:"item"` } type ContributionSearchProvider struct { - PluginID string `json:"pluginId"` + PluginID string `json:"pluginId"` Item plugin.ContributionSearchProvider `json:"item"` } type ContributionActivityProvider struct { - PluginID string `json:"pluginId"` + PluginID string `json:"pluginId"` Item plugin.ContributionActivityProvider `json:"item"` } type ContributionStatusBarItem struct { - PluginID string `json:"pluginId"` + PluginID string `json:"pluginId"` Item plugin.ContributionStatusBarItem `json:"item"` } diff --git a/internal/core/plugin/plugin.go b/internal/core/plugin/plugin.go index 0345610..573732c 100644 --- a/internal/core/plugin/plugin.go +++ b/internal/core/plugin/plugin.go @@ -11,23 +11,23 @@ import ( // Manifest represents a Verstak plugin.json manifest. type Manifest struct { - SchemaVersion int `json:"schemaVersion"` - ID string `json:"id"` - Name string `json:"name"` - Version string `json:"version"` - APIVersion string `json:"apiVersion"` - Description string `json:"description,omitempty"` - Source string `json:"source,omitempty"` - Icon string `json:"icon,omitempty"` - Provides []string `json:"provides"` - Requires []string `json:"requires,omitempty"` - OptionalRequires []string `json:"optionalRequires,omitempty"` - Permissions []string `json:"permissions"` - Frontend *FrontendConfig `json:"frontend,omitempty"` - Backend *BackendConfig `json:"backend,omitempty"` - Migrations *MigrationConfig `json:"migrations,omitempty"` - Contributes *Contributions `json:"contributes,omitempty"` - Sync *SyncConfig `json:"sync,omitempty"` + SchemaVersion int `json:"schemaVersion"` + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + APIVersion string `json:"apiVersion"` + Description string `json:"description,omitempty"` + Source string `json:"source,omitempty"` + Icon string `json:"icon,omitempty"` + Provides []string `json:"provides"` + Requires []string `json:"requires,omitempty"` + OptionalRequires []string `json:"optionalRequires,omitempty"` + Permissions []string `json:"permissions"` + Frontend *FrontendConfig `json:"frontend,omitempty"` + Backend *BackendConfig `json:"backend,omitempty"` + Migrations *MigrationConfig `json:"migrations,omitempty"` + Contributes *Contributions `json:"contributes,omitempty"` + Sync *SyncConfig `json:"sync,omitempty"` } // FrontendConfig describes the plugin's frontend bundle. @@ -38,8 +38,8 @@ type FrontendConfig struct { // BackendConfig describes the plugin's backend sidecar. type BackendConfig struct { - Type string `json:"type"` - Entry map[string]string `json:"entry"` + Type string `json:"type"` + Entry map[string]string `json:"entry"` HealthCheck *HealthCheckConfig `json:"healthCheck,omitempty"` } @@ -56,16 +56,16 @@ type MigrationConfig struct { // Contributions describes UI and action contributions. type Contributions struct { - Views []ContributionView `json:"views,omitempty"` - Commands []ContributionCommand `json:"commands,omitempty"` - SettingsPanels []ContributionSettingsPanel `json:"settingsPanels,omitempty"` - SidebarItems []ContributionSidebarItem `json:"sidebarItems,omitempty"` - FileActions []ContributionAction `json:"fileActions,omitempty"` - NoteActions []ContributionAction `json:"noteActions,omitempty"` + Views []ContributionView `json:"views,omitempty"` + Commands []ContributionCommand `json:"commands,omitempty"` + SettingsPanels []ContributionSettingsPanel `json:"settingsPanels,omitempty"` + SidebarItems []ContributionSidebarItem `json:"sidebarItems,omitempty"` + FileActions []ContributionAction `json:"fileActions,omitempty"` + NoteActions []ContributionAction `json:"noteActions,omitempty"` ContextMenuEntries []ContributionContextMenuEntry `json:"contextMenuEntries,omitempty"` - SearchProviders []ContributionSearchProvider `json:"searchProviders,omitempty"` - ActivityProviders []ContributionActivityProvider `json:"activityProviders,omitempty"` - StatusBarItems []ContributionStatusBarItem `json:"statusBarItems,omitempty"` + SearchProviders []ContributionSearchProvider `json:"searchProviders,omitempty"` + ActivityProviders []ContributionActivityProvider `json:"activityProviders,omitempty"` + StatusBarItems []ContributionStatusBarItem `json:"statusBarItems,omitempty"` } // ContributionView represents a view contribution. @@ -153,13 +153,13 @@ type SyncConfig struct { type Status string const ( - StatusDiscovered Status = "discovered" - StatusDisabled Status = "disabled" - StatusLoading Status = "loading" - StatusLoaded Status = "loaded" - StatusDegraded Status = "degraded" - StatusFailed Status = "failed" - StatusIncompatible Status = "incompatible" + StatusDiscovered Status = "discovered" + StatusDisabled Status = "disabled" + StatusLoading Status = "loading" + StatusLoaded Status = "loaded" + StatusDegraded Status = "degraded" + StatusFailed Status = "failed" + StatusIncompatible Status = "incompatible" StatusMissingRequiredCapability Status = "missing-required-capability" ) diff --git a/main.go b/main.go index 2cbb8ca..475d67d 100644 --- a/main.go +++ b/main.go @@ -46,9 +46,9 @@ func main() { err := wails.Run(&options.App{ Title: "Verstak", Width: 1200, - Height: 800, - MinWidth: 800, - MinHeight: 600, + Height: 800, + MinWidth: 800, + MinHeight: 600, WindowStartState: options.Normal, AssetServer: &assetserver.Options{ Assets: assets, diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..ad0a9c5 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +FAILED=0 + +report() { + if [ "$2" -eq 0 ]; then + echo " ✅ $1" + else + echo " ❌ $1" + FAILED=1 + fi +} + +echo "=== verstak-desktop build ===" + +# ── Go backend ── +echo "[backend]" + +(cd "$ROOT" && go vet ./...) +report "go vet" $? + +(cd "$ROOT" && go build ./...) +report "go build" $? + +if command -v go-test-summary &>/dev/null || go test -list . ./... &>/dev/null 2>&1; then + (cd "$ROOT" && go test -count=1 ./... 2>&1 || true) + report "go test" $? +else + echo " ℹ️ go test: no tests to run" +fi + +# ── Wails ── +echo "[wails]" +if command -v wails &>/dev/null; then + (cd "$ROOT" && wails build -clean) + report "wails build" $? +else + echo " ❌ wails: command not found. Install with: go install github.com/wailsapp/wails/v2/cmd/wails@latest" + FAILED=1 +fi + +# ── Frontend ── +echo "[frontend]" +if [ -f "$ROOT/frontend/package.json" ]; then + (cd "$ROOT/frontend" && npm ci --no-audit --no-fund) + report "npm ci" $? + (cd "$ROOT/frontend" && npm run build) + report "frontend build" $? +else + echo " ℹ️ frontend/package.json not found — skipping" +fi + +echo "" +if [ "$FAILED" -eq 0 ]; then + echo "✅ build passed" +else + echo "❌ build failed" +fi +exit "$FAILED" diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100755 index 0000000..03802fb --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +FAILED=0 + +report() { + if [ "$2" -eq 0 ]; then + echo " ✅ $1" + else + echo " ❌ $1" + FAILED=1 + fi +} + +echo "=== verstak-desktop check ===" + +# Go vet +(cd "$ROOT" && go vet ./...) +report "go vet" $? + +# Go fmt (non-destructive — only report unformatted files) +UNFORMATTED=$(cd "$ROOT" && gofmt -l . 2>/dev/null || go fmt -n ./... 2>&1 || true) +if [ -z "$UNFORMATTED" ]; then + echo " ✅ gofmt: all files formatted" +else + echo " ❌ gofmt: unformatted files:" + echo "$UNFORMATTED" | sed 's/^/ /' + FAILED=1 +fi + +# Go mod tidy check (non-destructive — report only) +(cd "$ROOT" && go mod tidy -diff 2>&1 || echo " ⚠️ go mod tidy check skipped") +report "go mod tidy" $? + +# Frontend lint +if [ -f "$ROOT/frontend/package.json" ]; then + # Check if npm ci is needed (node_modules missing) + if [ ! -d "$ROOT/frontend/node_modules" ]; then + echo " ℹ️ frontend/node_modules missing — run build.sh first" + fi + if grep -q '"lint"' "$ROOT/frontend/package.json" 2>/dev/null; then + (cd "$ROOT/frontend" && npx tsc --noEmit 2>&1 || true) + report "frontend tsc --noEmit" $? + else + echo " ℹ️ no lint script in frontend/package.json" + fi +fi + +echo "" +if [ "$FAILED" -eq 0 ]; then + echo "✅ all checks passed" +else + echo "❌ some checks failed" +fi +exit "$FAILED" diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..4c05ca6 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +FAILED=0 + +report() { + if [ "$2" -eq 0 ]; then + echo " ✅ $1" + else + echo " ❌ $1" + FAILED=1 + fi +} + +echo "=== verstak-desktop test ===" + +# Go tests +(cd "$ROOT" && go test -count=1 -v ./... 2>&1 || true) +report "go test" $? + +# Frontend tests +if [ -f "$ROOT/frontend/package.json" ]; then + # Only run if vitest or jest is in the config + if grep -q '"test"' "$ROOT/frontend/package.json" 2>/dev/null; then + (cd "$ROOT/frontend" && npm test 2>&1 || true) + report "frontend test" $? + else + echo " ℹ️ no test script in frontend/package.json" + fi +else + echo " ℹ️ no frontend/package.json" +fi + +echo "" +if [ "$FAILED" -eq 0 ]; then + echo "✅ all tests passed" +else + echo "❌ some tests failed" +fi +exit "$FAILED"