diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 0aab6fe..9574ed9 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -2,7 +2,7 @@ name: CI
on: [push]
env:
- UIPATHCLI_BASE_VERSION: "v1.1"
+ UIPATHCLI_BASE_VERSION: "v2.0"
GO_VERSION: "1.22.2"
jobs:
diff --git a/auth/browser_launcher.go b/auth/browser_launcher.go
index 7698c7c..f60aece 100644
--- a/auth/browser_launcher.go
+++ b/auth/browser_launcher.go
@@ -1,6 +1,36 @@
package auth
-// BrowserLauncher interface for opening browser windows.
-type BrowserLauncher interface {
- Open(url string) error
+import (
+ "fmt"
+ "time"
+
+ "github.com/UiPath/uipathcli/utils"
+)
+
+// BrowserLauncher tries to open the default browser on the local system.
+type BrowserLauncher struct {
+ Exec utils.ExecProcess
+}
+
+func (l BrowserLauncher) Open(url string) error {
+ cmd := l.openBrowser(url)
+ err := cmd.Start()
+ if err != nil {
+ return err
+ }
+ done := make(chan error)
+ go func() {
+ done <- cmd.Wait()
+ }()
+
+ select {
+ case err := <-done:
+ return err
+ case <-time.After(5 * time.Second):
+ return fmt.Errorf("Timed out waiting for browser to start")
+ }
+}
+
+func NewBrowserLauncher() *BrowserLauncher {
+ return &BrowserLauncher{utils.NewExecProcess()}
}
diff --git a/auth/browser_launcher_darwin.go b/auth/browser_launcher_darwin.go
new file mode 100644
index 0000000..013ef5e
--- /dev/null
+++ b/auth/browser_launcher_darwin.go
@@ -0,0 +1,11 @@
+//go:build darwin
+
+package auth
+
+import (
+ "github.com/UiPath/uipathcli/utils"
+)
+
+func (l BrowserLauncher) openBrowser(url string) utils.ExecCmd {
+ return l.Exec.Command("open", url)
+}
diff --git a/auth/browser_launcher_linux.go b/auth/browser_launcher_linux.go
new file mode 100644
index 0000000..03d7160
--- /dev/null
+++ b/auth/browser_launcher_linux.go
@@ -0,0 +1,11 @@
+//go:build linux
+
+package auth
+
+import (
+ "github.com/UiPath/uipathcli/utils"
+)
+
+func (l BrowserLauncher) openBrowser(url string) utils.ExecCmd {
+ return l.Exec.Command("xdg-open", url)
+}
diff --git a/auth/browser_launcher_windows.go b/auth/browser_launcher_windows.go
new file mode 100644
index 0000000..45ab3b2
--- /dev/null
+++ b/auth/browser_launcher_windows.go
@@ -0,0 +1,11 @@
+//go:build windows
+
+package auth
+
+import (
+ "github.com/UiPath/uipathcli/utils"
+)
+
+func (l BrowserLauncher) openBrowser(url string) utils.ExecCmd {
+ return l.Exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
+}
diff --git a/auth/exec_browser_launcher.go b/auth/exec_browser_launcher.go
deleted file mode 100644
index e949029..0000000
--- a/auth/exec_browser_launcher.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package auth
-
-import (
- "fmt"
- "os/exec"
- "runtime"
- "time"
-)
-
-// ExecBrowserLauncher is the default implementation for the browser launcher which
-// tries to open the default browser on the local system.
-type ExecBrowserLauncher struct{}
-
-func (l ExecBrowserLauncher) Open(url string) error {
- var cmd *exec.Cmd
-
- switch runtime.GOOS {
- case "windows":
- cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
- case "linux":
- cmd = exec.Command("xdg-open", url)
- case "darwin":
- cmd = exec.Command("open", url)
- default:
- return fmt.Errorf("Platform not supported: %s", runtime.GOOS)
- }
-
- err := cmd.Start()
- if err != nil {
- return err
- }
- done := make(chan error)
- go func() {
- done <- cmd.Wait()
- }()
-
- select {
- case err := <-done:
- return err
- case <-time.After(5 * time.Second):
- return fmt.Errorf("Timed out waiting for browser to start")
- }
-}
-
-func NewExecBrowserLauncher() *ExecBrowserLauncher {
- return &ExecBrowserLauncher{}
-}
diff --git a/auth/oauth_authenticator_test.go b/auth/oauth_authenticator_test.go
index 7a9563f..36a3961 100644
--- a/auth/oauth_authenticator_test.go
+++ b/auth/oauth_authenticator_test.go
@@ -11,10 +11,12 @@ import (
"net/http"
"net/http/httptest"
"net/url"
+ "runtime"
"strings"
"testing"
"github.com/UiPath/uipathcli/cache"
+ "github.com/UiPath/uipathcli/utils"
)
func TestOAuthAuthenticatorNotEnabled(t *testing.T) {
@@ -25,7 +27,7 @@ func TestOAuthAuthenticatorNotEnabled(t *testing.T) {
request := NewAuthenticatorRequest("http:/localhost", map[string]string{})
context := NewAuthenticatorContext("login", config, createIdentityUrl(""), false, false, *request)
- authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
+ authenticator := NewOAuthAuthenticator(cache.NewFileCache(), *NewBrowserLauncher())
result := authenticator.Auth(*context)
if result.Error != "" {
t.Errorf("Expected no error when oauth flow is skipped, but got: %v", result.Error)
@@ -46,7 +48,7 @@ func TestOAuthAuthenticatorPreservesExistingHeaders(t *testing.T) {
request := NewAuthenticatorRequest("http:/localhost", headers)
context := NewAuthenticatorContext("login", config, createIdentityUrl(""), false, false, *request)
- authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
+ authenticator := NewOAuthAuthenticator(cache.NewFileCache(), *NewBrowserLauncher())
result := authenticator.Auth(*context)
if result.Error != "" {
t.Errorf("Expected no error when oauth flow is skipped, but got: %v", result.Error)
@@ -65,7 +67,7 @@ func TestOAuthAuthenticatorInvalidConfig(t *testing.T) {
request := NewAuthenticatorRequest("http:/localhost", map[string]string{})
context := NewAuthenticatorContext("login", config, createIdentityUrl(""), false, false, *request)
- authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
+ authenticator := NewOAuthAuthenticator(cache.NewFileCache(), *NewBrowserLauncher())
result := authenticator.Auth(*context)
if result.Error != "Invalid oauth authenticator configuration: Invalid value for clientId: '1'" {
t.Errorf("Expected error with invalid config, but got: %v", result.Error)
@@ -122,7 +124,7 @@ func TestOAuthFlowIsCached(t *testing.T) {
performLogin(loginUrl, t)
<-resultChannel
- authenticator := NewOAuthAuthenticator(cache.NewFileCache(), nil)
+ authenticator := NewOAuthAuthenticator(cache.NewFileCache(), *NewBrowserLauncher())
result := authenticator.Auth(context)
if result.Error != "" {
@@ -208,8 +210,15 @@ func TestMissingCodeShowsErrorMessage(t *testing.T) {
func callAuthenticator(context AuthenticatorContext) (url.URL, chan AuthenticatorResult) {
loginChan := make(chan string)
- authenticator := NewOAuthAuthenticator(cache.NewFileCache(), NoOpBrowserLauncher{
- loginUrlChannel: loginChan,
+ authenticator := NewOAuthAuthenticator(cache.NewFileCache(), BrowserLauncher{
+ Exec: utils.NewExecCustomProcess(0, "", "", func(name string, args []string) {
+ switch runtime.GOOS {
+ case "windows":
+ loginChan <- args[1]
+ default:
+ loginChan <- args[0]
+ }
+ }),
})
resultChannel := make(chan AuthenticatorResult)
@@ -323,12 +332,3 @@ func (i identityServerFake) writeValidationErrorResponse(response http.ResponseW
response.WriteHeader(400)
_, _ = response.Write([]byte(message))
}
-
-type NoOpBrowserLauncher struct {
- loginUrlChannel chan string
-}
-
-func (l NoOpBrowserLauncher) Open(url string) error {
- l.loginUrlChannel <- url
- return nil
-}
diff --git a/cache/file_cache.go b/cache/file_cache.go
index 54cbc30..00a8822 100644
--- a/cache/file_cache.go
+++ b/cache/file_cache.go
@@ -9,11 +9,11 @@ import (
"strconv"
"strings"
"time"
+
+ "github.com/UiPath/uipathcli/utils"
)
const cacheFilePermissions = 0600
-const cacheDirectoryPermissions = 0700
-const cacheDirectory = "uipath"
const separator = "|"
// The FileCache stores data on disk in order to preserve them across
@@ -63,15 +63,12 @@ func (c FileCache) readValue(key string) (int64, string, error) {
}
func (c FileCache) cacheFilePath(key string) (string, error) {
- userCacheDirectory, err := os.UserCacheDir()
+ cacheDirectory, err := utils.Directories{}.Cache()
if err != nil {
return "", err
}
- cacheDirectory := filepath.Join(userCacheDirectory, cacheDirectory)
- _ = os.MkdirAll(cacheDirectory, cacheDirectoryPermissions)
-
hash := sha256.Sum256([]byte(key))
- fileName := fmt.Sprintf("%x.cache", hash)
+ fileName := fmt.Sprintf("%x", hash)
return filepath.Join(cacheDirectory, fileName), nil
}
diff --git a/definitions/studio.yaml b/definitions/studio.yaml
new file mode 100644
index 0000000..6c742bc
--- /dev/null
+++ b/definitions/studio.yaml
@@ -0,0 +1,14 @@
+openapi: 3.0.1
+info:
+ title: UiPath Studio
+ description: UiPath Studio
+ version: v1
+servers:
+ - url: https://cloud.uipath.com/{organization}/studio_/backend
+ description: The production url
+ variables:
+ organization:
+ description: The organization name (or id)
+ default: my-org
+paths:
+ {}
\ No newline at end of file
diff --git a/executor/http_executor.go b/executor/http_executor.go
index bba736a..536f765 100644
--- a/executor/http_executor.go
+++ b/executor/http_executor.go
@@ -169,7 +169,7 @@ func (e HttpExecutor) progressReader(text string, completedText string, reader i
if progress.Completed {
displayText = completedText
}
- progressBar.Update(displayText, progress.BytesRead, length, progress.BytesPerSecond)
+ progressBar.UpdateProgress(displayText, progress.BytesRead, length, progress.BytesPerSecond)
})
return progressReader
}
diff --git a/log/debug_logger.go b/log/debug_logger.go
index e72aea5..05963c5 100644
--- a/log/debug_logger.go
+++ b/log/debug_logger.go
@@ -47,6 +47,10 @@ func (l DebugLogger) LogResponse(response ResponseInfo) {
fmt.Fprint(l.writer, "\n\n\n")
}
+func (l DebugLogger) Log(message string) {
+ fmt.Fprint(l.writer, message)
+}
+
func (l DebugLogger) LogError(message string) {
fmt.Fprint(l.writer, message)
}
diff --git a/log/default_logger.go b/log/default_logger.go
index 45c08e1..c14a2d7 100644
--- a/log/default_logger.go
+++ b/log/default_logger.go
@@ -18,6 +18,9 @@ func (l *DefaultLogger) LogRequest(request RequestInfo) {
func (l DefaultLogger) LogResponse(response ResponseInfo) {
}
+func (l DefaultLogger) Log(message string) {
+}
+
func (l DefaultLogger) LogError(message string) {
fmt.Fprint(l.writer, message)
}
diff --git a/log/logger.go b/log/logger.go
index de132df..a65e75f 100644
--- a/log/logger.go
+++ b/log/logger.go
@@ -8,6 +8,7 @@ package log
// The Logger interface which is used to provide additional information to the
// user about what operations the CLI is performing.
type Logger interface {
+ Log(message string)
LogError(message string)
LogRequest(request RequestInfo)
LogResponse(response ResponseInfo)
diff --git a/main.go b/main.go
index 17153b4..7bcdfae 100644
--- a/main.go
+++ b/main.go
@@ -18,6 +18,7 @@ import (
"github.com/UiPath/uipathcli/plugin"
plugin_digitizer "github.com/UiPath/uipathcli/plugin/digitizer"
plugin_orchestrator "github.com/UiPath/uipathcli/plugin/orchestrator"
+ plugin_studio "github.com/UiPath/uipathcli/plugin/studio"
"github.com/UiPath/uipathcli/utils"
)
@@ -27,7 +28,7 @@ var embedded embed.FS
func authenticators() []auth.Authenticator {
return []auth.Authenticator{
auth.NewPatAuthenticator(),
- auth.NewOAuthAuthenticator(cache.NewFileCache(), auth.NewExecBrowserLauncher()),
+ auth.NewOAuthAuthenticator(cache.NewFileCache(), *auth.NewBrowserLauncher()),
auth.NewBearerAuthenticator(cache.NewFileCache()),
}
}
@@ -63,9 +64,11 @@ func main() {
commandline.NewDefinitionFileStore(os.Getenv("UIPATH_DEFINITIONS_PATH"), embedded),
parser.NewOpenApiParser(),
[]plugin.CommandPlugin{
- plugin_digitizer.DigitizeCommand{},
- plugin_orchestrator.UploadCommand{},
- plugin_orchestrator.DownloadCommand{},
+ plugin_digitizer.NewDigitizeCommand(),
+ plugin_orchestrator.NewUploadCommand(),
+ plugin_orchestrator.NewDownloadCommand(),
+ plugin_studio.NewPackagePackCommand(),
+ plugin_studio.NewPackageAnalyzeCommand(),
},
),
*configProvider,
diff --git a/plugin/digitizer/digitize_command.go b/plugin/digitizer/digitize_command.go
index 6b847be..c0495dd 100644
--- a/plugin/digitizer/digitize_command.go
+++ b/plugin/digitizer/digitize_command.go
@@ -129,12 +129,9 @@ func (c DigitizeCommand) createDigitizeRequest(context plugin.ExecutionContext,
var err error
file := context.Input
if file == nil {
- file, err = c.getFileParameter(context.Parameters)
- if err != nil {
- return nil, err
- }
+ file = c.getFileParameter(context.Parameters)
}
- contentType, _ := c.getParameter("content-type", context.Parameters)
+ contentType := c.getParameter("content-type", context.Parameters)
if contentType == "" {
contentType = "application/octet-stream"
}
@@ -164,7 +161,7 @@ func (c DigitizeCommand) progressReader(text string, completedText string, reade
if progress.Completed {
displayText = completedText
}
- progressBar.Update(displayText, progress.BytesRead, length, progress.BytesPerSecond)
+ progressBar.UpdateProgress(displayText, progress.BytesRead, length, progress.BytesPerSecond)
})
return progressReader
}
@@ -262,33 +259,37 @@ func (c DigitizeCommand) sendRequest(request *http.Request, insecure bool) (*htt
}
func (c DigitizeCommand) getProjectId(parameters []plugin.ExecutionParameter) string {
- projectId, _ := c.getParameter("project-id", parameters)
+ projectId := c.getParameter("project-id", parameters)
if projectId == "" {
projectId = "00000000-0000-0000-0000-000000000000"
}
return projectId
}
-func (c DigitizeCommand) getParameter(name string, parameters []plugin.ExecutionParameter) (string, error) {
+func (c DigitizeCommand) getParameter(name string, parameters []plugin.ExecutionParameter) string {
+ result := ""
for _, p := range parameters {
if p.Name == name {
if data, ok := p.Value.(string); ok {
- return data, nil
+ result = data
+ break
}
}
}
- return "", fmt.Errorf("Could not find '%s' parameter", name)
+ return result
}
-func (c DigitizeCommand) getFileParameter(parameters []plugin.ExecutionParameter) (utils.Stream, error) {
+func (c DigitizeCommand) getFileParameter(parameters []plugin.ExecutionParameter) utils.Stream {
+ var result utils.Stream
for _, p := range parameters {
if p.Name == "file" {
if stream, ok := p.Value.(utils.Stream); ok {
- return stream, nil
+ result = stream
+ break
}
}
}
- return nil, fmt.Errorf("Could not find 'file' parameter")
+ return result
}
func (c DigitizeCommand) logRequest(logger log.Logger, request *http.Request) {
@@ -304,3 +305,7 @@ func (c DigitizeCommand) logResponse(logger log.Logger, response *http.Response,
responseInfo := log.NewResponseInfo(response.StatusCode, response.Status, response.Proto, response.Header, bytes.NewReader(body))
logger.LogResponse(*responseInfo)
}
+
+func NewDigitizeCommand() *DigitizeCommand {
+ return &DigitizeCommand{}
+}
diff --git a/plugin/digitizer/digitizer_plugin_test.go b/plugin/digitizer/digitizer_plugin_test.go
index 37d55ff..b4f3512 100644
--- a/plugin/digitizer/digitizer_plugin_test.go
+++ b/plugin/digitizer/digitizer_plugin_test.go
@@ -20,7 +20,7 @@ paths:
context := test.NewContextBuilder().
WithDefinition("du", definition).
- WithCommandPlugin(DigitizeCommand{}).
+ WithCommandPlugin(NewDigitizeCommand()).
Build()
result := test.RunCli([]string{"du", "digitization", "digitize", "--project-id", "1234"}, context)
@@ -47,7 +47,7 @@ paths:
context := test.NewContextBuilder().
WithConfig(config).
WithDefinition("du", definition).
- WithCommandPlugin(DigitizeCommand{}).
+ WithCommandPlugin(NewDigitizeCommand()).
Build()
result := test.RunCli([]string{"du", "digitization", "digitize", "--project-id", "1234", "--file", "does-not-exist"}, context)
@@ -67,7 +67,7 @@ paths:
context := test.NewContextBuilder().
WithDefinition("du", definition).
- WithCommandPlugin(DigitizeCommand{}).
+ WithCommandPlugin(NewDigitizeCommand()).
Build()
result := test.RunCli([]string{"du", "digitization", "digitize", "--project-id", "1234", "--file", "hello-world"}, context)
@@ -87,7 +87,7 @@ paths:
context := test.NewContextBuilder().
WithDefinition("du", definition).
- WithCommandPlugin(DigitizeCommand{}).
+ WithCommandPlugin(NewDigitizeCommand()).
Build()
result := test.RunCli([]string{"du", "digitization", "digitize", "--organization", "myorg", "--project-id", "1234", "--file", "hello-world"}, context)
@@ -117,7 +117,7 @@ paths:
context := test.NewContextBuilder().
WithDefinition("du", definition).
WithConfig(config).
- WithCommandPlugin(DigitizeCommand{}).
+ WithCommandPlugin(NewDigitizeCommand()).
WithResponse(400, "validation error").
Build()
@@ -148,7 +148,7 @@ paths:
context := test.NewContextBuilder().
WithDefinition("du", definition).
WithConfig(config).
- WithCommandPlugin(DigitizeCommand{}).
+ WithCommandPlugin(NewDigitizeCommand()).
WithResponse(202, `{"documentId":"04908673-2b65-4647-8ab3-dde8a3aa7885"}`).
WithUrlResponse("/my-org/my-tenant/du_/api/framework/projects/1234/digitization/result/04908673-2b65-4647-8ab3-dde8a3aa7885?api-version=1", 400, `validation error`).
Build()
@@ -190,7 +190,7 @@ paths:
context := test.NewContextBuilder().
WithDefinition("du", definition).
WithConfig(config).
- WithCommandPlugin(DigitizeCommand{}).
+ WithCommandPlugin(NewDigitizeCommand()).
WithResponse(202, `{"documentId":"648ea1c2-7dbe-42a8-b112-6474d07e61c1"}`).
WithUrlResponse("/my-org/my-tenant/du_/api/framework/projects/00000000-0000-0000-0000-000000000000/digitization/result/648ea1c2-7dbe-42a8-b112-6474d07e61c1?api-version=1", 200, `{"status":"Done"}`).
Build()
@@ -236,7 +236,7 @@ paths:
context := test.NewContextBuilder().
WithDefinition("du", definition).
WithConfig(config).
- WithCommandPlugin(DigitizeCommand{}).
+ WithCommandPlugin(NewDigitizeCommand()).
WithResponse(202, `{"documentId":"eb80e441-05de-4a13-9aaa-f65b1babba05"}`).
WithUrlResponse("/my-org/my-tenant/du_/api/framework/projects/1234/digitization/result/eb80e441-05de-4a13-9aaa-f65b1babba05?api-version=1", 200, `{"status":"Done"}`).
Build()
@@ -272,7 +272,7 @@ paths:
context := test.NewContextBuilder().
WithDefinition("du", definition).
WithConfig(config).
- WithCommandPlugin(DigitizeCommand{}).
+ WithCommandPlugin(NewDigitizeCommand()).
WithResponse(202, `{"documentId":"eb80e441-05de-4a13-9aaa-f65b1babba05"}`).
WithUrlResponse("/my-org/my-tenant/du_/api/framework/projects/1234/digitization/result/eb80e441-05de-4a13-9aaa-f65b1babba05?api-version=1", 200, `{"pages":[],"status":"Done"}`).
Build()
@@ -313,7 +313,7 @@ paths:
context := test.NewContextBuilder().
WithDefinition("du", definition).
WithConfig(config).
- WithCommandPlugin(DigitizeCommand{}).
+ WithCommandPlugin(NewDigitizeCommand()).
WithStdIn(stdIn).
WithResponse(202, `{"documentId":"eb80e441-05de-4a13-9aaa-f65b1babba05"}`).
WithUrlResponse("/my-org/my-tenant/du_/api/framework/projects/1234/digitization/result/eb80e441-05de-4a13-9aaa-f65b1babba05?api-version=1", 200, `{"status":"Done"}`).
@@ -360,7 +360,7 @@ paths:
context := test.NewContextBuilder().
WithDefinition("du", definition).
WithConfig(config).
- WithCommandPlugin(DigitizeCommand{}).
+ WithCommandPlugin(NewDigitizeCommand()).
WithResponse(202, `{"documentId":"eb80e441-05de-4a13-9aaa-f65b1babba05"}`).
WithUrlResponse("/my-org/my-tenant/du_/api/framework/projects/1234/digitization/result/eb80e441-05de-4a13-9aaa-f65b1babba05?api-version=1", 200, `{"status":"Done"}`).
Build()
diff --git a/plugin/external_plugin.go b/plugin/external_plugin.go
new file mode 100644
index 0000000..35d2a8c
--- /dev/null
+++ b/plugin/external_plugin.go
@@ -0,0 +1,108 @@
+package plugin
+
+import (
+ "crypto/rand"
+ "crypto/sha256"
+ "fmt"
+ "io"
+ "math"
+ "math/big"
+ "net/http"
+ "os"
+ "path/filepath"
+
+ "github.com/UiPath/uipathcli/log"
+ "github.com/UiPath/uipathcli/utils"
+)
+
+const pluginDirectoryPermissions = 0700
+
+type ExternalPlugin struct {
+ Logger log.Logger
+}
+
+func (p ExternalPlugin) GetTool(name string, url string, executable string) (string, error) {
+ pluginDirectory, err := p.pluginDirectory(name, url)
+ if err != nil {
+ return "", fmt.Errorf("Could not download %s: %v", name, err)
+ }
+ path := filepath.Join(pluginDirectory, executable)
+ if _, err := os.Stat(path); err == nil {
+ return path, nil
+ }
+
+ tmpPluginDirectory := pluginDirectory + "-" + p.randomFolderName()
+ _ = os.MkdirAll(tmpPluginDirectory, pluginDirectoryPermissions)
+
+ progressBar := utils.NewProgressBar(p.Logger)
+ defer progressBar.Remove()
+ progressBar.UpdatePercentage("downloading...", 0)
+ zipArchivePath := filepath.Join(tmpPluginDirectory, name)
+ err = p.download(name, url, zipArchivePath, progressBar)
+ if err != nil {
+ return "", err
+ }
+ err = newZipArchive().Extract(zipArchivePath, tmpPluginDirectory, pluginDirectoryPermissions)
+ if err != nil {
+ return "", fmt.Errorf("Could not extract %s archive: %v", name, err)
+ }
+ os.Remove(zipArchivePath)
+ err = os.Rename(tmpPluginDirectory, pluginDirectory)
+ if err != nil {
+ return "", fmt.Errorf("Could not install %s: %v", name, err)
+ }
+ return path, nil
+}
+
+func (p ExternalPlugin) download(name string, url string, destination string, progressBar *utils.ProgressBar) error {
+ out, err := os.Create(destination)
+ if err != nil {
+ return fmt.Errorf("Could not download %s: %v", name, err)
+ }
+ defer out.Close()
+
+ request, err := http.NewRequest(http.MethodGet, url, nil)
+ if err != nil {
+ return fmt.Errorf("Could not download %s: %v", name, err)
+ }
+ response, err := http.DefaultClient.Do(request)
+ if err != nil {
+ return fmt.Errorf("Could not download %s: %v", name, err)
+ }
+ downloadReader := p.progressReader("downloading...", "installing... ", response.Body, response.ContentLength, progressBar)
+ _, err = io.Copy(out, downloadReader)
+ if err != nil {
+ return fmt.Errorf("Could not download %s: %v", name, err)
+ }
+ return nil
+}
+
+func (p ExternalPlugin) progressReader(text string, completedText string, reader io.Reader, length int64, progressBar *utils.ProgressBar) io.Reader {
+ progressReader := utils.NewProgressReader(reader, func(progress utils.Progress) {
+ displayText := text
+ if progress.Completed {
+ displayText = completedText
+ }
+ progressBar.UpdateProgress(displayText, progress.BytesRead, length, progress.BytesPerSecond)
+ })
+ return progressReader
+}
+
+func (p ExternalPlugin) pluginDirectory(name string, url string) (string, error) {
+ pluginDirectory, err := utils.Directories{}.Plugin()
+ if err != nil {
+ return "", err
+ }
+ hash := sha256.Sum256([]byte(url))
+ subdirectory := fmt.Sprintf("%s-%x", name, hash)
+ return filepath.Join(pluginDirectory, subdirectory), nil
+}
+
+func (p ExternalPlugin) randomFolderName() string {
+ value, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
+ return value.String()
+}
+
+func NewExternalPlugin(logger log.Logger) *ExternalPlugin {
+ return &ExternalPlugin{logger}
+}
diff --git a/plugin/orchestrator/download_command.go b/plugin/orchestrator/download_command.go
index c0dfb6c..7818114 100644
--- a/plugin/orchestrator/download_command.go
+++ b/plugin/orchestrator/download_command.go
@@ -77,7 +77,7 @@ func (c DownloadCommand) progressReader(text string, completedText string, reade
if progress.Completed {
displayText = completedText
}
- progressBar.Update(displayText, progress.BytesRead, length, progress.BytesPerSecond)
+ progressBar.UpdateProgress(displayText, progress.BytesRead, length, progress.BytesPerSecond)
})
return progressReader
}
@@ -119,18 +119,9 @@ func (c DownloadCommand) createReadUrlRequest(context plugin.ExecutionContext) (
if context.Tenant == "" {
return nil, errors.New("Tenant is not set")
}
- folderId, err := c.getIntParameter("folder-id", context.Parameters)
- if err != nil {
- return nil, err
- }
- bucketId, err := c.getIntParameter("key", context.Parameters)
- if err != nil {
- return nil, err
- }
- path, err := c.getStringParameter("path", context.Parameters)
- if err != nil {
- return nil, err
- }
+ folderId := c.getIntParameter("folder-id", context.Parameters)
+ bucketId := c.getIntParameter("key", context.Parameters)
+ path := c.getStringParameter("path", context.Parameters)
uri := c.formatUri(context.BaseUri, context.Organization, context.Tenant) + fmt.Sprintf("/odata/Buckets(%d)/UiPath.Server.Configuration.OData.GetReadUri?path=%s", bucketId, path)
request, err := http.NewRequest("GET", uri, &bytes.Buffer{})
@@ -182,26 +173,30 @@ func (c DownloadCommand) sendRequest(request *http.Request, insecure bool) (*htt
return client.Do(request)
}
-func (c DownloadCommand) getStringParameter(name string, parameters []plugin.ExecutionParameter) (string, error) {
+func (c DownloadCommand) getStringParameter(name string, parameters []plugin.ExecutionParameter) string {
+ result := ""
for _, p := range parameters {
if p.Name == name {
if data, ok := p.Value.(string); ok {
- return data, nil
+ result = data
+ break
}
}
}
- return "", fmt.Errorf("Could not find '%s' parameter", name)
+ return result
}
-func (c DownloadCommand) getIntParameter(name string, parameters []plugin.ExecutionParameter) (int, error) {
+func (c DownloadCommand) getIntParameter(name string, parameters []plugin.ExecutionParameter) int {
+ result := 0
for _, p := range parameters {
if p.Name == name {
if data, ok := p.Value.(int); ok {
- return data, nil
+ result = data
+ break
}
}
}
- return 0, fmt.Errorf("Could not find '%s' parameter", name)
+ return result
}
func (c DownloadCommand) logRequest(logger log.Logger, request *http.Request) {
@@ -217,3 +212,7 @@ func (c DownloadCommand) logResponse(logger log.Logger, response *http.Response,
responseInfo := log.NewResponseInfo(response.StatusCode, response.Status, response.Proto, response.Header, bytes.NewReader(body))
logger.LogResponse(*responseInfo)
}
+
+func NewDownloadCommand() *DownloadCommand {
+ return &DownloadCommand{}
+}
diff --git a/plugin/orchestrator/orchestrator_plugin_test.go b/plugin/orchestrator/orchestrator_plugin_test.go
index 6fcd320..d4d3a99 100644
--- a/plugin/orchestrator/orchestrator_plugin_test.go
+++ b/plugin/orchestrator/orchestrator_plugin_test.go
@@ -16,7 +16,7 @@ import (
func TestUploadWithoutFolderIdParameterShowsValidationError(t *testing.T) {
context := test.NewContextBuilder().
WithDefinition("orchestrator", "").
- WithCommandPlugin(UploadCommand{}).
+ WithCommandPlugin(NewUploadCommand()).
Build()
result := test.RunCli([]string{"orchestrator", "buckets", "upload", "--key", "2", "--path", "file.txt", "--file", "does-not-exist"}, context)
@@ -29,7 +29,7 @@ func TestUploadWithoutFolderIdParameterShowsValidationError(t *testing.T) {
func TestUploadWithoutKeyParameterShowsValidationError(t *testing.T) {
context := test.NewContextBuilder().
WithDefinition("orchestrator", "").
- WithCommandPlugin(UploadCommand{}).
+ WithCommandPlugin(NewUploadCommand()).
Build()
result := test.RunCli([]string{"orchestrator", "buckets", "upload", "--folder-id", "1", "--path", "file.txt", "--file", "does-not-exist"}, context)
@@ -42,7 +42,7 @@ func TestUploadWithoutKeyParameterShowsValidationError(t *testing.T) {
func TestUploadWithInvalidFolderIdParameterShowsValidationError(t *testing.T) {
context := test.NewContextBuilder().
WithDefinition("orchestrator", "").
- WithCommandPlugin(UploadCommand{}).
+ WithCommandPlugin(NewUploadCommand()).
Build()
result := test.RunCli([]string{"orchestrator", "buckets", "upload", "--folder-id", "invalid", "--key", "2", "--path", "file.txt", "--file", "does-not-exist"}, context)
@@ -55,7 +55,7 @@ func TestUploadWithInvalidFolderIdParameterShowsValidationError(t *testing.T) {
func TestUploadWithoutPathParameterShowsValidationError(t *testing.T) {
context := test.NewContextBuilder().
WithDefinition("orchestrator", "").
- WithCommandPlugin(UploadCommand{}).
+ WithCommandPlugin(NewUploadCommand()).
Build()
result := test.RunCli([]string{"orchestrator", "buckets", "upload", "--folder-id", "1", "--key", "2", "--file", "does-not-exist"}, context)
@@ -68,7 +68,7 @@ func TestUploadWithoutPathParameterShowsValidationError(t *testing.T) {
func TestUploadWithoutFileParameterShowsValidationError(t *testing.T) {
context := test.NewContextBuilder().
WithDefinition("orchestrator", "").
- WithCommandPlugin(UploadCommand{}).
+ WithCommandPlugin(NewUploadCommand()).
Build()
result := test.RunCli([]string{"orchestrator", "buckets", "upload", "--folder-id", "1", "--key", "2", "--path", "file.txt"}, context)
@@ -88,7 +88,7 @@ func TestUploadFileDoesNotExistShowsValidationError(t *testing.T) {
context := test.NewContextBuilder().
WithConfig(config).
WithDefinition("orchestrator", "").
- WithCommandPlugin(UploadCommand{}).
+ WithCommandPlugin(NewUploadCommand()).
WithResponse(200, `{"Uri":"http://localhost"}`).
Build()
@@ -102,7 +102,7 @@ func TestUploadFileDoesNotExistShowsValidationError(t *testing.T) {
func TestUploadWithoutOrganizationShowsValidationError(t *testing.T) {
context := test.NewContextBuilder().
WithDefinition("orchestrator", "").
- WithCommandPlugin(UploadCommand{}).
+ WithCommandPlugin(NewUploadCommand()).
Build()
result := test.RunCli([]string{"orchestrator", "buckets", "upload", "--folder-id", "1", "--key", "2", "--path", "file.txt", "--file", "hello-world"}, context)
@@ -115,7 +115,7 @@ func TestUploadWithoutOrganizationShowsValidationError(t *testing.T) {
func TestUploadWithoutTenantShowsValidationError(t *testing.T) {
context := test.NewContextBuilder().
WithDefinition("orchestrator", "").
- WithCommandPlugin(UploadCommand{}).
+ WithCommandPlugin(NewUploadCommand()).
Build()
result := test.RunCli([]string{"orchestrator", "buckets", "upload", "--organization", "myorg", "--folder-id", "1", "--key", "2", "--path", "file.txt", "--file", "hello-world"}, context)
@@ -138,7 +138,7 @@ func TestUploadWithFailedResponseReturnsError(t *testing.T) {
context := test.NewContextBuilder().
WithDefinition("orchestrator", "").
WithConfig(config).
- WithCommandPlugin(UploadCommand{}).
+ WithCommandPlugin(NewUploadCommand()).
WithResponse(400, "validation error").
Build()
@@ -197,7 +197,7 @@ servers:
context := test.NewContextBuilder().
WithDefinition("orchestrator", definition).
WithConfig(config).
- WithCommandPlugin(UploadCommand{}).
+ WithCommandPlugin(NewUploadCommand()).
WithResponse(200, `{"Uri":"`+srv.URL+`"}`).
Build()
@@ -242,7 +242,7 @@ servers:
context := test.NewContextBuilder().
WithDefinition("orchestrator", definition).
- WithCommandPlugin(UploadCommand{}).
+ WithCommandPlugin(NewUploadCommand()).
WithResponse(200, `{"Uri":"`+srv.URL+`"}`).
Build()
@@ -277,7 +277,7 @@ servers:
context := test.NewContextBuilder().
WithDefinition("orchestrator", definition).
- WithCommandPlugin(UploadCommand{}).
+ WithCommandPlugin(NewUploadCommand()).
WithResponse(200, `{"Uri":"`+srv.URL+"/upload/file.txt"+`"}`).
Build()
@@ -294,7 +294,7 @@ servers:
func TestDownloadWithoutFolderIdParameterShowsValidationError(t *testing.T) {
context := test.NewContextBuilder().
WithDefinition("orchestrator", "").
- WithCommandPlugin(DownloadCommand{}).
+ WithCommandPlugin(NewDownloadCommand()).
Build()
result := test.RunCli([]string{"orchestrator", "buckets", "download", "--key", "2", "--path", "file.txt"}, context)
@@ -307,7 +307,7 @@ func TestDownloadWithoutFolderIdParameterShowsValidationError(t *testing.T) {
func TestDownloadWithoutKeyParameterShowsValidationError(t *testing.T) {
context := test.NewContextBuilder().
WithDefinition("orchestrator", "").
- WithCommandPlugin(DownloadCommand{}).
+ WithCommandPlugin(NewDownloadCommand()).
Build()
result := test.RunCli([]string{"orchestrator", "buckets", "download", "--folder-id", "1", "--path", "file.txt"}, context)
@@ -320,7 +320,7 @@ func TestDownloadWithoutKeyParameterShowsValidationError(t *testing.T) {
func TestDownloadWithoutPathParameterShowsValidationError(t *testing.T) {
context := test.NewContextBuilder().
WithDefinition("orchestrator", "").
- WithCommandPlugin(DownloadCommand{}).
+ WithCommandPlugin(NewDownloadCommand()).
Build()
result := test.RunCli([]string{"orchestrator", "buckets", "download", "--folder-id", "1", "--key", "2"}, context)
@@ -333,7 +333,7 @@ func TestDownloadWithoutPathParameterShowsValidationError(t *testing.T) {
func TestDownloadWithoutOrganizationShowsValidationError(t *testing.T) {
context := test.NewContextBuilder().
WithDefinition("orchestrator", "").
- WithCommandPlugin(DownloadCommand{}).
+ WithCommandPlugin(NewDownloadCommand()).
Build()
result := test.RunCli([]string{"orchestrator", "buckets", "download", "--folder-id", "1", "--key", "2", "--path", "file.txt"}, context)
@@ -346,7 +346,7 @@ func TestDownloadWithoutOrganizationShowsValidationError(t *testing.T) {
func TestDownloadWithoutTenantShowsValidationError(t *testing.T) {
context := test.NewContextBuilder().
WithDefinition("orchestrator", "").
- WithCommandPlugin(DownloadCommand{}).
+ WithCommandPlugin(NewDownloadCommand()).
Build()
result := test.RunCli([]string{"orchestrator", "buckets", "download", "--organization", "myorg", "--folder-id", "1", "--key", "2", "--path", "file.txt"}, context)
@@ -366,7 +366,7 @@ func TestDownloadWithFailedResponseReturnsError(t *testing.T) {
context := test.NewContextBuilder().
WithDefinition("orchestrator", "").
WithConfig(config).
- WithCommandPlugin(DownloadCommand{}).
+ WithCommandPlugin(NewDownloadCommand()).
WithResponse(400, "validation error").
Build()
@@ -411,7 +411,7 @@ servers:
context := test.NewContextBuilder().
WithDefinition("orchestrator", definition).
WithConfig(config).
- WithCommandPlugin(DownloadCommand{}).
+ WithCommandPlugin(NewDownloadCommand()).
WithResponse(200, `{"Uri":"`+srv.URL+`"}`).
Build()
@@ -452,7 +452,7 @@ servers:
context := test.NewContextBuilder().
WithDefinition("orchestrator", definition).
- WithCommandPlugin(DownloadCommand{}).
+ WithCommandPlugin(NewDownloadCommand()).
WithResponse(200, `{"Uri":"`+srv.URL+`"}`).
Build()
@@ -485,7 +485,7 @@ servers:
context := test.NewContextBuilder().
WithDefinition("orchestrator", definition).
- WithCommandPlugin(DownloadCommand{}).
+ WithCommandPlugin(NewDownloadCommand()).
WithResponse(200, `{"Uri":"`+srv.URL+`/download/file.txt"}`).
Build()
diff --git a/plugin/orchestrator/upload_command.go b/plugin/orchestrator/upload_command.go
index 10f587b..ba5bb36 100644
--- a/plugin/orchestrator/upload_command.go
+++ b/plugin/orchestrator/upload_command.go
@@ -70,11 +70,7 @@ func (c UploadCommand) upload(context plugin.ExecutionContext, logger log.Logger
func (c UploadCommand) createUploadRequest(context plugin.ExecutionContext, url string, uploadBar *utils.ProgressBar, requestError chan error) (*http.Request, error) {
file := context.Input
if file == nil {
- var err error
- file, err = c.getFileParameter(context.Parameters)
- if err != nil {
- return nil, err
- }
+ file = c.getFileParameter(context.Parameters)
}
bodyReader, bodyWriter := io.Pipe()
contentType, contentLength := c.writeBody(bodyWriter, file, requestError)
@@ -118,7 +114,7 @@ func (c UploadCommand) progressReader(text string, completedText string, reader
if progress.Completed {
displayText = completedText
}
- progressBar.Update(displayText, progress.BytesRead, length, progress.BytesPerSecond)
+ progressBar.UpdateProgress(displayText, progress.BytesRead, length, progress.BytesPerSecond)
})
return progressReader
}
@@ -160,18 +156,9 @@ func (c UploadCommand) createWriteUrlRequest(context plugin.ExecutionContext) (*
if context.Tenant == "" {
return nil, errors.New("Tenant is not set")
}
- folderId, err := c.getIntParameter("folder-id", context.Parameters)
- if err != nil {
- return nil, err
- }
- bucketId, err := c.getIntParameter("key", context.Parameters)
- if err != nil {
- return nil, err
- }
- path, err := c.getStringParameter("path", context.Parameters)
- if err != nil {
- return nil, err
- }
+ folderId := c.getIntParameter("folder-id", context.Parameters)
+ bucketId := c.getIntParameter("key", context.Parameters)
+ path := c.getStringParameter("path", context.Parameters)
uri := c.formatUri(context.BaseUri, context.Organization, context.Tenant) + fmt.Sprintf("/odata/Buckets(%d)/UiPath.Server.Configuration.OData.GetWriteUri?path=%s", bucketId, path)
request, err := http.NewRequest("GET", uri, &bytes.Buffer{})
@@ -223,37 +210,43 @@ func (c UploadCommand) sendRequest(request *http.Request, insecure bool) (*http.
return client.Do(request)
}
-func (c UploadCommand) getStringParameter(name string, parameters []plugin.ExecutionParameter) (string, error) {
+func (c UploadCommand) getStringParameter(name string, parameters []plugin.ExecutionParameter) string {
+ result := ""
for _, p := range parameters {
if p.Name == name {
if data, ok := p.Value.(string); ok {
- return data, nil
+ result = data
+ break
}
}
}
- return "", fmt.Errorf("Could not find '%s' parameter", name)
+ return result
}
-func (c UploadCommand) getIntParameter(name string, parameters []plugin.ExecutionParameter) (int, error) {
+func (c UploadCommand) getIntParameter(name string, parameters []plugin.ExecutionParameter) int {
+ result := 0
for _, p := range parameters {
if p.Name == name {
if data, ok := p.Value.(int); ok {
- return data, nil
+ result = data
+ break
}
}
}
- return 0, fmt.Errorf("Could not find '%s' parameter", name)
+ return result
}
-func (c UploadCommand) getFileParameter(parameters []plugin.ExecutionParameter) (utils.Stream, error) {
+func (c UploadCommand) getFileParameter(parameters []plugin.ExecutionParameter) utils.Stream {
+ var result utils.Stream
for _, p := range parameters {
if p.Name == "file" {
if stream, ok := p.Value.(utils.Stream); ok {
- return stream, nil
+ result = stream
+ break
}
}
}
- return nil, fmt.Errorf("Could not find 'file' parameter")
+ return result
}
func (c UploadCommand) logRequest(logger log.Logger, request *http.Request) {
@@ -269,3 +262,7 @@ func (c UploadCommand) logResponse(logger log.Logger, response *http.Response, b
responseInfo := log.NewResponseInfo(response.StatusCode, response.Status, response.Proto, response.Header, bytes.NewReader(body))
logger.LogResponse(*responseInfo)
}
+
+func NewUploadCommand() *UploadCommand {
+ return &UploadCommand{}
+}
diff --git a/plugin/studio/analyze_result_json.go b/plugin/studio/analyze_result_json.go
new file mode 100644
index 0000000..e034383
--- /dev/null
+++ b/plugin/studio/analyze_result_json.go
@@ -0,0 +1,25 @@
+package studio
+
+type analyzeResultJson []struct {
+ ErrorCode string `json:"ErrorCode"`
+ Description string `json:"Description"`
+ RuleName string `json:"RuleName"`
+ FilePath string `json:"FilePath"`
+ ActivityId *analyzeResultActivityId `json:"ActivityId"`
+ ActivityDisplayName string `json:"ActivityDisplayName"`
+ WorkflowDisplayName string `json:"WorkflowDisplayName"`
+ Item *analyzeResultItem `json:"Item"`
+ ErrorSeverity int `json:"ErrorSeverity"`
+ Recommendation string `json:"Recommendation"`
+ DocumentationLink string `json:"DocumentationLink"`
+}
+
+type analyzeResultActivityId struct {
+ Id string `json:"Id"`
+ IdRef string `json:"IdRef"`
+}
+
+type analyzeResultItem struct {
+ Name string `json:"Name"`
+ Type int `json:"Type"`
+}
diff --git a/plugin/studio/package_analyze_command.go b/plugin/studio/package_analyze_command.go
new file mode 100644
index 0000000..5aef3da
--- /dev/null
+++ b/plugin/studio/package_analyze_command.go
@@ -0,0 +1,288 @@
+package studio
+
+import (
+ "bufio"
+ "bytes"
+ "crypto/rand"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "math"
+ "math/big"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/UiPath/uipathcli/log"
+ "github.com/UiPath/uipathcli/output"
+ "github.com/UiPath/uipathcli/plugin"
+ "github.com/UiPath/uipathcli/utils"
+)
+
+// The PackageAnalyzeCommand runs static code analyis on the project to detect common errors.
+type PackageAnalyzeCommand struct {
+ Exec utils.ExecProcess
+}
+
+func (c PackageAnalyzeCommand) Command() plugin.Command {
+ return *plugin.NewCommand("studio").
+ WithCategory("package", "Package", "UiPath Studio package-related actions").
+ WithOperation("analyze", "Analyze Project", "Runs static code analysis on the project to detect common errors").
+ WithParameter("source", plugin.ParameterTypeString, "Path to a project.json file or a folder containing project.json file", true).
+ WithParameter("treat-warnings-as-errors", plugin.ParameterTypeBoolean, "Treat warnings as errors", false).
+ WithParameter("stop-on-rule-violation", plugin.ParameterTypeBoolean, "Fail when any rule is violated", false)
+}
+
+func (c PackageAnalyzeCommand) Execute(context plugin.ExecutionContext, writer output.OutputWriter, logger log.Logger) error {
+ source, err := c.getSource(context)
+ if err != nil {
+ return err
+ }
+ treatWarningsAsErrors := c.getBoolParameter("treat-warnings-as-errors", context.Parameters)
+ stopOnRuleViolation := c.getBoolParameter("stop-on-rule-violation", context.Parameters)
+ exitCode, result, err := c.execute(source, treatWarningsAsErrors, stopOnRuleViolation, context.Debug, logger)
+ if err != nil {
+ return err
+ }
+
+ json, err := json.Marshal(result)
+ if err != nil {
+ return fmt.Errorf("analyze command failed: %v", err)
+ }
+ err = writer.WriteResponse(*output.NewResponseInfo(200, "200 OK", "HTTP/1.1", map[string][]string{}, bytes.NewReader(json)))
+ if err != nil {
+ return err
+ }
+ if exitCode != 0 {
+ return errors.New("")
+ }
+ return nil
+}
+
+func (c PackageAnalyzeCommand) execute(source string, treatWarningsAsErrors bool, stopOnRuleViolation bool, debug bool, logger log.Logger) (int, *packageAnalyzeResult, error) {
+ if !debug {
+ bar := c.newAnalyzingProgressBar(logger)
+ defer close(bar)
+ }
+
+ jsonResultFilePath, err := c.getTemporaryJsonResultFilePath()
+ if err != nil {
+ return 1, nil, err
+ }
+ defer os.Remove(jsonResultFilePath)
+
+ args := []string{"package", "analyze", source, "--resultPath", jsonResultFilePath}
+ if treatWarningsAsErrors {
+ args = append(args, "--treatWarningsAsErrors")
+ }
+ if stopOnRuleViolation {
+ args = append(args, "--stopOnRuleViolation")
+ }
+
+ projectReader := newStudioProjectReader(source)
+
+ uipcli := newUipcli(c.Exec, logger)
+ cmd, err := uipcli.Execute(projectReader.GetTargetFramework(), args...)
+ if err != nil {
+ return 1, nil, err
+ }
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return 1, nil, fmt.Errorf("Could not run analyze command: %v", err)
+ }
+ defer stdout.Close()
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ return 1, nil, fmt.Errorf("Could not run analyze command: %v", err)
+ }
+ defer stderr.Close()
+ err = cmd.Start()
+ if err != nil {
+ return 1, nil, fmt.Errorf("Could not run analyze command: %v", err)
+ }
+
+ stderrOutputBuilder := new(strings.Builder)
+ stderrReader := io.TeeReader(stderr, stderrOutputBuilder)
+
+ var wg sync.WaitGroup
+ wg.Add(3)
+ go c.readOutput(stdout, logger, &wg)
+ go c.readOutput(stderrReader, logger, &wg)
+ go c.wait(cmd, &wg)
+ wg.Wait()
+
+ violations, err := c.readAnalyzeResult(jsonResultFilePath)
+ if err != nil {
+ return 1, nil, err
+ }
+
+ exitCode := cmd.ExitCode()
+ var result *packageAnalyzeResult
+ if exitCode == 0 {
+ result = newSucceededPackageAnalyzeResult(violations)
+ } else {
+ result = newFailedPackageAnalyzeResult(
+ violations,
+ stderrOutputBuilder.String(),
+ )
+ }
+ return exitCode, result, nil
+}
+
+func (c PackageAnalyzeCommand) getTemporaryJsonResultFilePath() (string, error) {
+ tempDirectory, err := utils.Directories{}.Temp()
+ if err != nil {
+ return "", err
+ }
+ fileName := c.randomJsonResultFileName()
+ return filepath.Join(tempDirectory, fileName), nil
+}
+
+func (c PackageAnalyzeCommand) randomJsonResultFileName() string {
+ value, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
+ return "analyzeresult-" + value.String() + ".json"
+}
+
+func (c PackageAnalyzeCommand) readAnalyzeResult(path string) ([]packageAnalyzeViolation, error) {
+ file, err := os.Open(path)
+ if err != nil && errors.Is(err, os.ErrNotExist) {
+ return []packageAnalyzeViolation{}, nil
+ }
+ if err != nil {
+ return []packageAnalyzeViolation{}, fmt.Errorf("Error reading %s file: %v", filepath.Base(path), err)
+ }
+ defer file.Close()
+ byteValue, err := io.ReadAll(file)
+ if err != nil {
+ return []packageAnalyzeViolation{}, fmt.Errorf("Error reading %s file: %v", filepath.Base(path), err)
+ }
+
+ var result analyzeResultJson
+ err = json.Unmarshal(byteValue, &result)
+ if err != nil {
+ return []packageAnalyzeViolation{}, fmt.Errorf("Error parsing %s file: %v", filepath.Base(path), err)
+ }
+ return c.convertToViolations(result), nil
+}
+
+func (c PackageAnalyzeCommand) convertToViolations(json analyzeResultJson) []packageAnalyzeViolation {
+ violations := []packageAnalyzeViolation{}
+ for _, entry := range json {
+ var activityId *packageAnalyzeActivityId
+ if entry.ActivityId != nil {
+ activityId = &packageAnalyzeActivityId{
+ Id: entry.ActivityId.Id,
+ IdRef: entry.ActivityId.IdRef,
+ }
+ }
+ var item *packageAnalyzeItem
+ if entry.Item != nil {
+ item = &packageAnalyzeItem{
+ Name: entry.Item.Name,
+ Type: entry.Item.Type,
+ }
+ }
+ violation := packageAnalyzeViolation{
+ ErrorCode: entry.ErrorCode,
+ Description: entry.Description,
+ RuleName: entry.RuleName,
+ FilePath: entry.FilePath,
+ ActivityDisplayName: entry.ActivityDisplayName,
+ WorkflowDisplayName: entry.WorkflowDisplayName,
+ ErrorSeverity: entry.ErrorSeverity,
+ Recommendation: entry.Recommendation,
+ DocumentationLink: entry.DocumentationLink,
+ ActivityId: activityId,
+ Item: item,
+ }
+ violations = append(violations, violation)
+ }
+ return violations
+}
+
+func (c PackageAnalyzeCommand) wait(cmd utils.ExecCmd, wg *sync.WaitGroup) {
+ defer wg.Done()
+ _ = cmd.Wait()
+}
+
+func (c PackageAnalyzeCommand) newAnalyzingProgressBar(logger log.Logger) chan struct{} {
+ progressBar := utils.NewProgressBar(logger)
+ ticker := time.NewTicker(10 * time.Millisecond)
+ cancel := make(chan struct{})
+ var percent float64 = 0
+ go func() {
+ for {
+ select {
+ case <-ticker.C:
+ progressBar.UpdatePercentage("analyzing... ", percent)
+ percent = percent + 1
+ if percent > 100 {
+ percent = 0
+ }
+ case <-cancel:
+ ticker.Stop()
+ progressBar.Remove()
+ return
+ }
+ }
+ }()
+ return cancel
+}
+
+func (c PackageAnalyzeCommand) getSource(context plugin.ExecutionContext) (string, error) {
+ source := c.getParameter("source", context.Parameters)
+ if source == "" {
+ return "", errors.New("source is not set")
+ }
+ source, _ = filepath.Abs(source)
+ fileInfo, err := os.Stat(source)
+ if err != nil {
+ return "", fmt.Errorf("%s not found", defaultProjectJson)
+ }
+ if fileInfo.IsDir() {
+ source = filepath.Join(source, defaultProjectJson)
+ }
+ return source, nil
+}
+
+func (c PackageAnalyzeCommand) readOutput(output io.Reader, logger log.Logger, wg *sync.WaitGroup) {
+ defer wg.Done()
+ scanner := bufio.NewScanner(output)
+ scanner.Split(bufio.ScanRunes)
+ for scanner.Scan() {
+ logger.Log(scanner.Text())
+ }
+}
+
+func (c PackageAnalyzeCommand) getParameter(name string, parameters []plugin.ExecutionParameter) string {
+ result := ""
+ for _, p := range parameters {
+ if p.Name == name {
+ if data, ok := p.Value.(string); ok {
+ result = data
+ break
+ }
+ }
+ }
+ return result
+}
+
+func (c PackageAnalyzeCommand) getBoolParameter(name string, parameters []plugin.ExecutionParameter) bool {
+ result := false
+ for _, p := range parameters {
+ if p.Name == name {
+ if data, ok := p.Value.(bool); ok {
+ result = data
+ break
+ }
+ }
+ }
+ return result
+}
+
+func NewPackageAnalyzeCommand() *PackageAnalyzeCommand {
+ return &PackageAnalyzeCommand{utils.NewExecProcess()}
+}
diff --git a/plugin/studio/package_analyze_result.go b/plugin/studio/package_analyze_result.go
new file mode 100644
index 0000000..5a58dc2
--- /dev/null
+++ b/plugin/studio/package_analyze_result.go
@@ -0,0 +1,38 @@
+package studio
+
+type packageAnalyzeResult struct {
+ Status string `json:"status"`
+ Violations []packageAnalyzeViolation `json:"violations"`
+ Error *string `json:"error"`
+}
+
+type packageAnalyzeViolation struct {
+ ErrorCode string `json:"errorCode"`
+ Description string `json:"description"`
+ RuleName string `json:"ruleName"`
+ FilePath string `json:"filePath"`
+ ActivityId *packageAnalyzeActivityId `json:"activityId"`
+ ActivityDisplayName string `json:"activityDisplayName"`
+ WorkflowDisplayName string `json:"workflowDisplayName"`
+ Item *packageAnalyzeItem `json:"item"`
+ ErrorSeverity int `json:"errorSeverity"`
+ Recommendation string `json:"recommendation"`
+ DocumentationLink string `json:"documentationLink"`
+}
+type packageAnalyzeActivityId struct {
+ Id string `json:"id"`
+ IdRef string `json:"idRef"`
+}
+
+type packageAnalyzeItem struct {
+ Name string `json:"name"`
+ Type int `json:"type"`
+}
+
+func newSucceededPackageAnalyzeResult(violations []packageAnalyzeViolation) *packageAnalyzeResult {
+ return &packageAnalyzeResult{"Succeeded", violations, nil}
+}
+
+func newFailedPackageAnalyzeResult(violations []packageAnalyzeViolation, err string) *packageAnalyzeResult {
+ return &packageAnalyzeResult{"Failed", violations, &err}
+}
diff --git a/plugin/studio/package_pack_command.go b/plugin/studio/package_pack_command.go
new file mode 100644
index 0000000..1f43fcc
--- /dev/null
+++ b/plugin/studio/package_pack_command.go
@@ -0,0 +1,270 @@
+package studio
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/UiPath/uipathcli/log"
+ "github.com/UiPath/uipathcli/output"
+ "github.com/UiPath/uipathcli/plugin"
+ "github.com/UiPath/uipathcli/utils"
+)
+
+const defaultProjectJson = "project.json"
+
+// The PackagePackCommand packs a project into a single NuGet package
+type PackagePackCommand struct {
+ Exec utils.ExecProcess
+}
+
+func (c PackagePackCommand) Command() plugin.Command {
+ return *plugin.NewCommand("studio").
+ WithCategory("package", "Package", "UiPath Studio package-related actions").
+ WithOperation("pack", "Package Project", "Packs a project into a single package").
+ WithParameter("source", plugin.ParameterTypeString, "Path to a project.json file or a folder containing project.json file", true).
+ WithParameter("destination", plugin.ParameterTypeString, "The output folder", true).
+ WithParameter("package-version", plugin.ParameterTypeString, "The package version", false).
+ WithParameter("auto-version", plugin.ParameterTypeBoolean, "Auto-generate package version", false).
+ WithParameter("output-type", plugin.ParameterTypeString, "Force the output to a specific type", false).
+ WithParameter("split-output", plugin.ParameterTypeBoolean, "Enables the output split to runtime and design libraries", false).
+ WithParameter("release-notes", plugin.ParameterTypeString, "Add release notes", false)
+}
+
+func (c PackagePackCommand) Execute(context plugin.ExecutionContext, writer output.OutputWriter, logger log.Logger) error {
+ source, err := c.getSource(context)
+ if err != nil {
+ return err
+ }
+ destination, err := c.getDestination(context)
+ if err != nil {
+ return err
+ }
+ packageVersion := c.getParameter("package-version", context.Parameters)
+ autoVersion := c.getBoolParameter("auto-version", context.Parameters)
+ outputType := c.getParameter("output-type", context.Parameters)
+ splitOutput := c.getBoolParameter("split-output", context.Parameters)
+ releaseNotes := c.getParameter("release-notes", context.Parameters)
+ params := newPackagePackParams(source, destination, packageVersion, autoVersion, outputType, splitOutput, releaseNotes)
+
+ result, err := c.execute(*params, context.Debug, logger)
+ if err != nil {
+ return err
+ }
+
+ json, err := json.Marshal(result)
+ if err != nil {
+ return fmt.Errorf("pack command failed: %v", err)
+ }
+ return writer.WriteResponse(*output.NewResponseInfo(200, "200 OK", "HTTP/1.1", map[string][]string{}, bytes.NewReader(json)))
+}
+
+func (c PackagePackCommand) execute(params packagePackParams, debug bool, logger log.Logger) (*packagePackResult, error) {
+ if !debug {
+ bar := c.newPackagingProgressBar(logger)
+ defer close(bar)
+ }
+
+ args := []string{"package", "pack", params.Source, "--output", params.Destination}
+ if params.PackageVersion != "" {
+ args = append(args, "--version", params.PackageVersion)
+ }
+ if params.AutoVersion {
+ args = append(args, "--autoVersion")
+ }
+ if params.OutputType != "" {
+ args = append(args, "--outputType", params.OutputType)
+ }
+ if params.SplitOutput {
+ args = append(args, "--splitOutput")
+ }
+ if params.ReleaseNotes != "" {
+ args = append(args, "--releaseNotes", params.ReleaseNotes)
+ }
+
+ projectReader := newStudioProjectReader(params.Source)
+
+ uipcli := newUipcli(c.Exec, logger)
+ cmd, err := uipcli.Execute(projectReader.GetTargetFramework(), args...)
+ if err != nil {
+ return nil, err
+ }
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return nil, fmt.Errorf("Could not run pack command: %v", err)
+ }
+ defer stdout.Close()
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ return nil, fmt.Errorf("Could not run pack command: %v", err)
+ }
+ defer stderr.Close()
+ err = cmd.Start()
+ if err != nil {
+ return nil, fmt.Errorf("Could not run pack command: %v", err)
+ }
+
+ stderrOutputBuilder := new(strings.Builder)
+ stderrReader := io.TeeReader(stderr, stderrOutputBuilder)
+
+ var wg sync.WaitGroup
+ wg.Add(3)
+ go c.readOutput(stdout, logger, &wg)
+ go c.readOutput(stderrReader, logger, &wg)
+ go c.wait(cmd, &wg)
+ wg.Wait()
+
+ project, err := projectReader.ReadMetadata()
+ if err != nil {
+ return nil, err
+ }
+
+ exitCode := cmd.ExitCode()
+ var result *packagePackResult
+ if exitCode == 0 {
+ nupkgFile := c.findNupkg(params.Destination)
+ version := c.extractVersion(nupkgFile)
+ result = newSucceededPackagePackResult(
+ filepath.Join(params.Destination, nupkgFile),
+ project.Name,
+ project.Description,
+ project.ProjectId,
+ version)
+ } else {
+ result = newFailedPackagePackResult(
+ stderrOutputBuilder.String(),
+ &project.Name,
+ &project.Description,
+ &project.ProjectId)
+ }
+ return result, nil
+}
+
+func (c PackagePackCommand) findNupkg(destination string) string {
+ newestFile := ""
+ newestTime := time.Time{}
+
+ files, _ := os.ReadDir(destination)
+ for _, file := range files {
+ extension := filepath.Ext(file.Name())
+ if strings.EqualFold(extension, ".nupkg") {
+ fileInfo, _ := file.Info()
+ time := fileInfo.ModTime()
+ if time.After(newestTime) {
+ newestTime = time
+ newestFile = file.Name()
+ }
+ }
+ }
+ return newestFile
+}
+
+func (c PackagePackCommand) extractVersion(nupkgFile string) string {
+ parts := strings.Split(nupkgFile, ".")
+ len := len(parts)
+ if len < 4 {
+ return ""
+ }
+ return fmt.Sprintf("%s.%s.%s", parts[len-4], parts[len-3], parts[len-2])
+}
+
+func (c PackagePackCommand) wait(cmd utils.ExecCmd, wg *sync.WaitGroup) {
+ defer wg.Done()
+ _ = cmd.Wait()
+}
+
+func (c PackagePackCommand) newPackagingProgressBar(logger log.Logger) chan struct{} {
+ progressBar := utils.NewProgressBar(logger)
+ ticker := time.NewTicker(10 * time.Millisecond)
+ cancel := make(chan struct{})
+ var percent float64 = 0
+ go func() {
+ for {
+ select {
+ case <-ticker.C:
+ progressBar.UpdatePercentage("packaging... ", percent)
+ percent = percent + 1
+ if percent > 100 {
+ percent = 0
+ }
+ case <-cancel:
+ ticker.Stop()
+ progressBar.Remove()
+ return
+ }
+ }
+ }()
+ return cancel
+}
+
+func (c PackagePackCommand) getSource(context plugin.ExecutionContext) (string, error) {
+ source := c.getParameter("source", context.Parameters)
+ if source == "" {
+ return "", errors.New("source is not set")
+ }
+ source, _ = filepath.Abs(source)
+ fileInfo, err := os.Stat(source)
+ if err != nil {
+ return "", fmt.Errorf("%s not found", defaultProjectJson)
+ }
+ if fileInfo.IsDir() {
+ source = filepath.Join(source, defaultProjectJson)
+ }
+ return source, nil
+}
+
+func (c PackagePackCommand) getDestination(context plugin.ExecutionContext) (string, error) {
+ destination := c.getParameter("destination", context.Parameters)
+ if destination == "" {
+ return "", errors.New("destination is not set")
+ }
+ destination, _ = filepath.Abs(destination)
+ return destination, nil
+}
+
+func (c PackagePackCommand) readOutput(output io.Reader, logger log.Logger, wg *sync.WaitGroup) {
+ defer wg.Done()
+ scanner := bufio.NewScanner(output)
+ scanner.Split(bufio.ScanRunes)
+ for scanner.Scan() {
+ logger.Log(scanner.Text())
+ }
+}
+
+func (c PackagePackCommand) getParameter(name string, parameters []plugin.ExecutionParameter) string {
+ result := ""
+ for _, p := range parameters {
+ if p.Name == name {
+ if data, ok := p.Value.(string); ok {
+ result = data
+ break
+ }
+ }
+ }
+ return result
+}
+
+func (c PackagePackCommand) getBoolParameter(name string, parameters []plugin.ExecutionParameter) bool {
+ result := false
+ for _, p := range parameters {
+ if p.Name == name {
+ if data, ok := p.Value.(bool); ok {
+ result = data
+ break
+ }
+ }
+ }
+ return result
+}
+
+func NewPackagePackCommand() *PackagePackCommand {
+ return &PackagePackCommand{utils.NewExecProcess()}
+}
diff --git a/plugin/studio/package_pack_params.go b/plugin/studio/package_pack_params.go
new file mode 100644
index 0000000..ebee3e7
--- /dev/null
+++ b/plugin/studio/package_pack_params.go
@@ -0,0 +1,22 @@
+package studio
+
+type packagePackParams struct {
+ Source string
+ Destination string
+ PackageVersion string
+ AutoVersion bool
+ OutputType string
+ SplitOutput bool
+ ReleaseNotes string
+}
+
+func newPackagePackParams(
+ source string,
+ destination string,
+ packageVersion string,
+ autoVersion bool,
+ outputType string,
+ splitOutput bool,
+ releaseNotes string) *packagePackParams {
+ return &packagePackParams{source, destination, packageVersion, autoVersion, outputType, splitOutput, releaseNotes}
+}
diff --git a/plugin/studio/package_pack_result.go b/plugin/studio/package_pack_result.go
new file mode 100644
index 0000000..2b89441
--- /dev/null
+++ b/plugin/studio/package_pack_result.go
@@ -0,0 +1,19 @@
+package studio
+
+type packagePackResult struct {
+ Status string `json:"status"`
+ Name *string `json:"name"`
+ Description *string `json:"description"`
+ ProjectId *string `json:"projectId"`
+ Version *string `json:"version"`
+ Output *string `json:"output"`
+ Error *string `json:"error"`
+}
+
+func newSucceededPackagePackResult(output string, name string, description string, projectId string, version string) *packagePackResult {
+ return &packagePackResult{"Succeeded", &name, &description, &projectId, &version, &output, nil}
+}
+
+func newFailedPackagePackResult(err string, name *string, description *string, projectId *string) *packagePackResult {
+ return &packagePackResult{"Failed", name, description, projectId, nil, nil, &err}
+}
diff --git a/plugin/studio/projects/.gitignore b/plugin/studio/projects/.gitignore
new file mode 100644
index 0000000..57c19d8
--- /dev/null
+++ b/plugin/studio/projects/.gitignore
@@ -0,0 +1,6 @@
+crossplatform/*
+!crossplatform/Main.xaml
+!crossplatform/project.json
+windows/*
+!windows/Main.xaml
+!windows/project.json
\ No newline at end of file
diff --git a/plugin/studio/projects/crossplatform/Main.xaml b/plugin/studio/projects/crossplatform/Main.xaml
new file mode 100644
index 0000000..c7168e2
--- /dev/null
+++ b/plugin/studio/projects/crossplatform/Main.xaml
@@ -0,0 +1,87 @@
+
+
+
+ System.Activities
+ System.Activities.Statements
+ System.Activities.Expressions
+ System.Activities.Validation
+ System.Activities.XamlIntegration
+ Microsoft.VisualBasic
+ Microsoft.VisualBasic.Activities
+ System
+ System.Collections
+ System.Collections.Generic
+ System.Collections.ObjectModel
+ System.Data
+ System.Diagnostics
+ System.Drawing
+ System.IO
+ System.Linq
+ System.Net.Mail
+ System.Xml
+ System.Text
+ System.Xml.Linq
+ UiPath.Core
+ UiPath.Core.Activities
+ System.Windows.Markup
+ GlobalVariablesNamespace
+ GlobalConstantsNamespace
+ System.Linq.Expressions
+ System.Runtime.Serialization
+
+
+
+
+ Microsoft.CSharp
+ Microsoft.VisualBasic
+ mscorlib
+ System
+ System.Activities
+ System.ComponentModel.TypeConverter
+ System.Core
+ System.Data
+ System.Data.Common
+ System.Data.DataSetExtensions
+ System.Drawing
+ System.Drawing.Common
+ System.Drawing.Primitives
+ System.Linq
+ System.Net.Mail
+ System.ObjectModel
+ System.Private.CoreLib
+ System.Runtime.Serialization
+ System.ServiceModel
+ System.ServiceModel.Activities
+ System.Xaml
+ System.Xml
+ System.Xml.Linq
+ UiPath.System.Activities
+ UiPath.UiAutomation.Activities
+ UiPath.Studio.Constants
+ System.Configuration.ConfigurationManager
+ System.Security.Permissions
+ System.Console
+ System.ComponentModel
+ System.Memory
+ System.Private.Uri
+ System.Linq.Expressions
+ System.Runtime.Serialization.Formatters
+ System.Private.DataContractSerialization
+ System.Runtime.Serialization.Primitives
+
+
+
+
+
+ True
+
+
+
+
+
+ "Hello World"
+
+
+
+
+
\ No newline at end of file
diff --git a/plugin/studio/projects/crossplatform/project.json b/plugin/studio/projects/crossplatform/project.json
new file mode 100644
index 0000000..e6c5ed0
--- /dev/null
+++ b/plugin/studio/projects/crossplatform/project.json
@@ -0,0 +1,61 @@
+{
+ "name": "MyProcess",
+ "projectId": "9011ee47-8dd4-4726-8850-299bd6ef057c",
+ "description": "Blank Process",
+ "main": "Main.xaml",
+ "dependencies": {
+ "UiPath.System.Activities": "[24.10.3]",
+ "UiPath.Testing.Activities": "[24.10.0]",
+ "UiPath.UIAutomation.Activities": "[24.10.0]",
+ "UiPath.WebAPI.Activities": "[1.21.0]"
+ },
+ "webServices": [],
+ "entitiesStores": [],
+ "schemaVersion": "4.0",
+ "studioVersion": "24.10.1.0",
+ "projectVersion": "1.0.2",
+ "runtimeOptions": {
+ "autoDispose": false,
+ "netFrameworkLazyLoading": false,
+ "isPausable": true,
+ "isAttended": false,
+ "requiresUserInteraction": false,
+ "supportsPersistence": false,
+ "workflowSerialization": "DataContract",
+ "excludedLoggedData": [
+ "Private:*",
+ "*password*"
+ ],
+ "executionType": "Workflow",
+ "readyForPiP": false,
+ "startsInPiP": false,
+ "mustRestoreAllDependencies": true,
+ "pipType": "ChildSession"
+ },
+ "designOptions": {
+ "projectProfile": "Developement",
+ "outputType": "Process",
+ "libraryOptions": {
+ "includeOriginalXaml": false,
+ "privateWorkflows": []
+ },
+ "processOptions": {
+ "ignoredFiles": []
+ },
+ "fileInfoCollection": [],
+ "saveToCloud": false
+ },
+ "expressionLanguage": "CSharp",
+ "entryPoints": [
+ {
+ "filePath": "Main.xaml",
+ "uniqueId": "ac610120-f85b-4ed4-a014-05dca3380186",
+ "input": [],
+ "output": []
+ }
+ ],
+ "isTemplate": false,
+ "templateProjectData": {},
+ "publishData": {},
+ "targetFramework": "Portable"
+}
\ No newline at end of file
diff --git a/plugin/studio/projects/windows/Main.xaml b/plugin/studio/projects/windows/Main.xaml
new file mode 100644
index 0000000..4c906dc
--- /dev/null
+++ b/plugin/studio/projects/windows/Main.xaml
@@ -0,0 +1,94 @@
+
+
+
+ System.Activities
+ System.Activities.Statements
+ System.Activities.Expressions
+ System.Activities.Validation
+ System.Activities.XamlIntegration
+ Microsoft.VisualBasic
+ Microsoft.VisualBasic.Activities
+ System
+ System.Collections
+ System.Collections.Generic
+ System.Collections.ObjectModel
+ System.Data
+ System.Diagnostics
+ System.Drawing
+ System.IO
+ System.Linq
+ System.Net.Mail
+ System.Xml
+ System.Text
+ System.Xml.Linq
+ UiPath.Core
+ UiPath.Core.Activities
+ System.Windows.Markup
+ GlobalVariablesNamespace
+ GlobalConstantsNamespace
+ System.Linq.Expressions
+ System.Runtime.Serialization
+
+
+
+
+ Microsoft.CSharp
+ Microsoft.VisualBasic
+ mscorlib
+ PresentationCore
+ PresentationFramework
+ System
+ System.Activities
+ System.ComponentModel.TypeConverter
+ System.Core
+ System.Data
+ System.Data.Common
+ System.Data.DataSetExtensions
+ System.Drawing
+ System.Drawing.Common
+ System.Drawing.Primitives
+ System.Linq
+ System.Net.Mail
+ System.ObjectModel
+ System.Private.CoreLib
+ System.Runtime.Serialization
+ System.ServiceModel
+ System.ServiceModel.Activities
+ System.Xaml
+ System.Xml
+ System.Xml.Linq
+ UiPath.System.Activities
+ UiPath.UiAutomation.Activities
+ WindowsBase
+ UiPath.Studio.Constants
+ System.Memory.Data
+ UiPath.Excel.Activities.Design
+ NPOI
+ System.Console
+ System.Security.Permissions
+ System.Configuration.ConfigurationManager
+ System.ComponentModel
+ System.Memory
+ System.Private.Uri
+ System.Linq.Expressions
+ System.Private.ServiceModel
+ System.Runtime.Serialization.Formatters
+ System.Private.DataContractSerialization
+ System.Runtime.Serialization.Primitives
+
+
+
+
+
+ True
+
+
+
+
+
+ "Hello World"
+
+
+
+
+
\ No newline at end of file
diff --git a/plugin/studio/projects/windows/project.json b/plugin/studio/projects/windows/project.json
new file mode 100644
index 0000000..4798b88
--- /dev/null
+++ b/plugin/studio/projects/windows/project.json
@@ -0,0 +1,62 @@
+{
+ "name": "MyWindowsProcess",
+ "projectId": "94c4c9c1-68c3-45d4-be14-d4427f17eddd",
+ "description": "Blank Process",
+ "main": "Main.xaml",
+ "dependencies": {
+ "UiPath.Excel.Activities": "[2.23.4]",
+ "UiPath.Mail.Activities": "[1.23.1]",
+ "UiPath.System.Activities": "[24.10.3]",
+ "UiPath.Testing.Activities": "[24.10.0]",
+ "UiPath.UIAutomation.Activities": "[24.10.0]"
+ },
+ "webServices": [],
+ "entitiesStores": [],
+ "schemaVersion": "4.0",
+ "studioVersion": "24.10.1.0",
+ "projectVersion": "1.0.0",
+ "runtimeOptions": {
+ "autoDispose": false,
+ "netFrameworkLazyLoading": false,
+ "isPausable": true,
+ "isAttended": false,
+ "requiresUserInteraction": true,
+ "supportsPersistence": false,
+ "workflowSerialization": "DataContract",
+ "excludedLoggedData": [
+ "Private:*",
+ "*password*"
+ ],
+ "executionType": "Workflow",
+ "readyForPiP": false,
+ "startsInPiP": false,
+ "mustRestoreAllDependencies": true,
+ "pipType": "ChildSession"
+ },
+ "designOptions": {
+ "projectProfile": "Developement",
+ "outputType": "Process",
+ "libraryOptions": {
+ "includeOriginalXaml": false,
+ "privateWorkflows": []
+ },
+ "processOptions": {
+ "ignoredFiles": []
+ },
+ "fileInfoCollection": [],
+ "saveToCloud": false
+ },
+ "expressionLanguage": "CSharp",
+ "entryPoints": [
+ {
+ "filePath": "Main.xaml",
+ "uniqueId": "657507a4-4df3-47eb-810e-2548d3a59824",
+ "input": [],
+ "output": []
+ }
+ ],
+ "isTemplate": false,
+ "templateProjectData": {},
+ "publishData": {},
+ "targetFramework": "Windows"
+}
\ No newline at end of file
diff --git a/plugin/studio/studio_plugin_notwin_test.go b/plugin/studio/studio_plugin_notwin_test.go
new file mode 100644
index 0000000..85b5a17
--- /dev/null
+++ b/plugin/studio/studio_plugin_notwin_test.go
@@ -0,0 +1,83 @@
+//go:build !windows
+
+package studio
+
+import (
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/UiPath/uipathcli/test"
+ "github.com/UiPath/uipathcli/utils"
+)
+
+func TestPackOnLinuxWithCorrectArguments(t *testing.T) {
+ commandName := ""
+ commandArgs := []string{}
+ exec := utils.NewExecCustomProcess(0, "", "", func(name string, args []string) {
+ commandName = name
+ commandArgs = args
+ })
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(PackagePackCommand{exec}).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ destination := createDirectory(t)
+ test.RunCli([]string{"studio", "package", "pack", "--source", source, "--destination", destination}, context)
+
+ if !strings.HasSuffix(commandName, "dotnet") {
+ t.Errorf("Expected command name to be dotnet, but got: %v", commandName)
+ }
+ if !strings.HasSuffix(commandArgs[0], "uipcli.dll") {
+ t.Errorf("Expected 1st argument to be the uipcli.dll, but got: %v", commandArgs[0])
+ }
+ if commandArgs[1] != "package" {
+ t.Errorf("Expected 2nd argument to be package, but got: %v", commandArgs[1])
+ }
+ if commandArgs[2] != "pack" {
+ t.Errorf("Expected 3rd argument to be pack, but got: %v", commandArgs[2])
+ }
+ if commandArgs[3] != filepath.Join(source, "project.json") {
+ t.Errorf("Expected 4th argument to be the project.json, but got: %v", commandArgs[3])
+ }
+ if commandArgs[4] != "--output" {
+ t.Errorf("Expected 5th argument to be output, but got: %v", commandArgs[4])
+ }
+ if commandArgs[5] != destination {
+ t.Errorf("Expected 6th argument to be the output path, but got: %v", commandArgs[5])
+ }
+}
+
+func TestAnalyzeOnLinuxWithCorrectArguments(t *testing.T) {
+ commandName := ""
+ commandArgs := []string{}
+ exec := utils.NewExecCustomProcess(0, "", "", func(name string, args []string) {
+ commandName = name
+ commandArgs = args
+ })
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(PackageAnalyzeCommand{exec}).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ test.RunCli([]string{"studio", "package", "analyze", "--source", source}, context)
+
+ if !strings.HasSuffix(commandName, "dotnet") {
+ t.Errorf("Expected command name to be dotnet, but got: %v", commandName)
+ }
+ if !strings.HasSuffix(commandArgs[0], "uipcli.dll") {
+ t.Errorf("Expected 1st argument to be the uipcli.dll, but got: %v", commandArgs[0])
+ }
+ if commandArgs[1] != "package" {
+ t.Errorf("Expected 2nd argument to be package, but got: %v", commandArgs[1])
+ }
+ if commandArgs[2] != "analyze" {
+ t.Errorf("Expected 3rd argument to be analyze, but got: %v", commandArgs[2])
+ }
+ if commandArgs[3] != filepath.Join(source, "project.json") {
+ t.Errorf("Expected 4th argument to be the project.json, but got: %v", commandArgs[3])
+ }
+}
diff --git a/plugin/studio/studio_plugin_test.go b/plugin/studio/studio_plugin_test.go
new file mode 100644
index 0000000..e04d8a5
--- /dev/null
+++ b/plugin/studio/studio_plugin_test.go
@@ -0,0 +1,368 @@
+package studio
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "runtime"
+ "slices"
+ "strings"
+ "testing"
+
+ "github.com/UiPath/uipathcli/test"
+ "github.com/UiPath/uipathcli/utils"
+)
+
+const studioDefinition = `
+openapi: 3.0.1
+info:
+ title: UiPath Studio
+ description: UiPath Studio
+ version: v1
+servers:
+ - url: https://cloud.uipath.com/{organization}/studio_/backend
+ description: The production url
+ variables:
+ organization:
+ description: The organization name (or id)
+ default: my-org
+paths:
+ {}
+`
+
+func TestPackWithoutSourceParameterShowsValidationError(t *testing.T) {
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(NewPackagePackCommand()).
+ Build()
+
+ result := test.RunCli([]string{"studio", "package", "pack", "--destination", "test.nupkg"}, context)
+
+ if !strings.Contains(result.StdErr, "Argument --source is missing") {
+ t.Errorf("Expected stderr to show that source parameter is missing, but got: %v", result.StdErr)
+ }
+}
+
+func TestPackWithoutDestinationParameterShowsValidationError(t *testing.T) {
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(NewPackagePackCommand()).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ result := test.RunCli([]string{"studio", "package", "pack", "--source", source}, context)
+
+ if !strings.Contains(result.StdErr, "Argument --destination is missing") {
+ t.Errorf("Expected stderr to show that destination parameter is missing, but got: %v", result.StdErr)
+ }
+}
+
+func TestPackNonExistentProjectShowsProjectJsonNotFound(t *testing.T) {
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(NewPackagePackCommand()).
+ Build()
+
+ result := test.RunCli([]string{"studio", "package", "pack", "--source", "non-existent", "--destination", "test.nupkg"}, context)
+
+ if !strings.Contains(result.StdErr, "project.json not found") {
+ t.Errorf("Expected stderr to show that project.json was not found, but got: %v", result.StdErr)
+ }
+}
+
+func TestFailedPackagingReturnsFailureStatus(t *testing.T) {
+ exec := utils.NewExecCustomProcess(1, "Build output", "There was an error", func(name string, args []string) {})
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(PackagePackCommand{exec}).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ destination := createDirectory(t)
+ result := test.RunCli([]string{"studio", "package", "pack", "--source", source, "--destination", destination}, context)
+
+ stdout := map[string]interface{}{}
+ err := json.Unmarshal([]byte(result.StdOut), &stdout)
+ if err != nil {
+ t.Errorf("Failed to deserialize pack command result: %v", err)
+ }
+ if stdout["status"] != "Failed" {
+ t.Errorf("Expected status to be Failed, but got: %v", result.StdOut)
+ }
+ if stdout["error"] != "There was an error" {
+ t.Errorf("Expected error to be set, but got: %v", result.StdOut)
+ }
+}
+
+func TestPackCrossPlatformSuccessfully(t *testing.T) {
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(NewPackagePackCommand()).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ destination := createDirectory(t)
+ result := test.RunCli([]string{"studio", "package", "pack", "--source", source, "--destination", destination}, context)
+
+ stdout := map[string]interface{}{}
+ err := json.Unmarshal([]byte(result.StdOut), &stdout)
+ if err != nil {
+ t.Errorf("Failed to deserialize pack command result: %v", err)
+ }
+ if stdout["status"] != "Succeeded" {
+ t.Errorf("Expected status to be Succeeded, but got: %v", result.StdOut)
+ }
+ if stdout["error"] != nil {
+ t.Errorf("Expected error to be nil, but got: %v", result.StdOut)
+ }
+ if stdout["name"] != "MyProcess" {
+ t.Errorf("Expected name to be set, but got: %v", result.StdOut)
+ }
+ if stdout["description"] != "Blank Process" {
+ t.Errorf("Expected version to be set, but got: %v", result.StdOut)
+ }
+ if stdout["projectId"] != "9011ee47-8dd4-4726-8850-299bd6ef057c" {
+ t.Errorf("Expected projectId to be set, but got: %v", result.StdOut)
+ }
+ if stdout["version"] != "1.0.2" {
+ t.Errorf("Expected version to be set, but got: %v", result.StdOut)
+ }
+ outputFile := stdout["output"].(string)
+ if outputFile != filepath.Join(destination, "MyProcess.1.0.2.nupkg") {
+ t.Errorf("Expected output path to be set, but got: %v", result.StdOut)
+ }
+ if _, err := os.Stat(outputFile); err != nil {
+ t.Errorf("Expected output file %s to exists, but could not find it: %v", outputFile, err)
+ }
+}
+
+func TestPackWithAutoVersionArgument(t *testing.T) {
+ commandArgs := []string{}
+ exec := utils.NewExecCustomProcess(0, "", "", func(name string, args []string) {
+ commandArgs = args
+ })
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(PackagePackCommand{exec}).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ destination := createDirectory(t)
+ test.RunCli([]string{"studio", "package", "pack", "--source", source, "--destination", destination, "--auto-version", "true"}, context)
+
+ if !slices.Contains(commandArgs, "--autoVersion") {
+ t.Errorf("Expected argument --autoVersion, but got: %v", strings.Join(commandArgs, " "))
+ }
+}
+
+func TestPackWithOutputTypeArgument(t *testing.T) {
+ commandArgs := []string{}
+ exec := utils.NewExecCustomProcess(0, "", "", func(name string, args []string) {
+ commandArgs = args
+ })
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(PackagePackCommand{exec}).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ destination := createDirectory(t)
+ test.RunCli([]string{"studio", "package", "pack", "--source", source, "--destination", destination, "--output-type", "Process"}, context)
+
+ if !slices.Contains(commandArgs, "--outputType") {
+ t.Errorf("Expected argument --outputType, but got: %v", strings.Join(commandArgs, " "))
+ }
+}
+
+func TestPackWithSplitOutputArgument(t *testing.T) {
+ commandArgs := []string{}
+ exec := utils.NewExecCustomProcess(0, "", "", func(name string, args []string) {
+ commandArgs = args
+ })
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(PackagePackCommand{exec}).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ destination := createDirectory(t)
+ test.RunCli([]string{"studio", "package", "pack", "--source", source, "--destination", destination, "--split-output", "true"}, context)
+
+ if !slices.Contains(commandArgs, "--splitOutput") {
+ t.Errorf("Expected argument --splitOutput, but got: %v", strings.Join(commandArgs, " "))
+ }
+}
+
+func TestPackWithReleaseNotesArgument(t *testing.T) {
+ commandArgs := []string{}
+ exec := utils.NewExecCustomProcess(0, "", "", func(name string, args []string) {
+ commandArgs = args
+ })
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(PackagePackCommand{exec}).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ destination := createDirectory(t)
+ test.RunCli([]string{"studio", "package", "pack", "--source", source, "--destination", destination, "--release-notes", "These are release notes."}, context)
+
+ index := slices.Index(commandArgs, "--releaseNotes")
+ if commandArgs[index] != "--releaseNotes" {
+ t.Errorf("Expected argument --releaseNotes, but got: %v", strings.Join(commandArgs, " "))
+ }
+ if commandArgs[index+1] != "These are release notes." {
+ t.Errorf("Expected release notes argument, but got: %v", strings.Join(commandArgs, " "))
+ }
+}
+
+func TestAnalyzeWithoutSourceParameterShowsValidationError(t *testing.T) {
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(NewPackageAnalyzeCommand()).
+ Build()
+
+ result := test.RunCli([]string{"studio", "package", "analyze"}, context)
+
+ if !strings.Contains(result.StdErr, "Argument --source is missing") {
+ t.Errorf("Expected stderr to show that source parameter is missing, but got: %v", result.StdErr)
+ }
+}
+
+func TestAnalyzeCrossPlatformSuccessfully(t *testing.T) {
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(NewPackageAnalyzeCommand()).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ result := test.RunCli([]string{"studio", "package", "analyze", "--source", source}, context)
+
+ stdout := map[string]interface{}{}
+ err := json.Unmarshal([]byte(result.StdOut), &stdout)
+ if err != nil {
+ t.Errorf("Failed to deserialize analyze command result: %v", err)
+ }
+ if stdout["status"] != "Succeeded" {
+ t.Errorf("Expected status to be Succeeded, but got: %v", result.StdOut)
+ }
+ if stdout["error"] != nil {
+ t.Errorf("Expected error to be nil, but got: %v", result.StdOut)
+ }
+ violations := stdout["violations"].([]interface{})
+ if len(violations) == 0 {
+ t.Errorf("Expected violations not to be empty, but got: %v", result.StdOut)
+ }
+ violation := findViolation(violations, "TA-DBP-002")
+ if violation == nil {
+ t.Errorf("Could not find violation TA-DBP-002, got: %v", result.StdOut)
+ }
+ if violation["activityDisplayName"] != "" {
+ t.Errorf("Expected violation to have a activityDisplayName, but got: %v", result.StdOut)
+ }
+ if violation["description"] != "Workflow Main.xaml does not have any assigned Test Cases." {
+ t.Errorf("Expected violation to have a description, but got: %v", result.StdOut)
+ }
+ if violation["documentationLink"] != "https://docs.uipath.com/activities/lang-en/docs/ta-dbp-002" {
+ t.Errorf("Expected violation to have a documentationLink, but got: %v", result.StdOut)
+ }
+ if violation["errorSeverity"] != 1.0 {
+ t.Errorf("Expected violation to have a errorSeverity, but got: %v", result.StdOut)
+ }
+ if violation["filePath"] != "" {
+ t.Errorf("Expected violation to have a filePath, but got: %v", result.StdOut)
+ }
+ if violation["recommendation"] != "Creating Test Cases for your workflows allows you to run them frequently to discover potential issues early on before they are introduced in your production environment. [Learn more.](https://docs.uipath.com/activities/lang-en/docs/ta-dbp-002)" {
+ t.Errorf("Expected violation to have a recommendation, but got: %v", result.StdOut)
+ }
+ if violation["ruleName"] != "Untested Workflows" {
+ t.Errorf("Expected violation to have a ruleName, but got: %v", result.StdOut)
+ }
+ if violation["workflowDisplayName"] != "Main" {
+ t.Errorf("Expected violation to have a workflowDisplayName, but got: %v", result.StdOut)
+ }
+}
+
+func TestFailedAnalyzeReturnsFailureStatus(t *testing.T) {
+ exec := utils.NewExecCustomProcess(1, "Analyze output", "There was an error", func(name string, args []string) {})
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(PackageAnalyzeCommand{exec}).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ result := test.RunCli([]string{"studio", "package", "analyze", "--source", source}, context)
+
+ stdout := map[string]interface{}{}
+ err := json.Unmarshal([]byte(result.StdOut), &stdout)
+ if err != nil {
+ t.Errorf("Failed to deserialize analyze command result: %v", err)
+ }
+ if stdout["status"] != "Failed" {
+ t.Errorf("Expected status to be Failed, but got: %v", result.StdOut)
+ }
+ if stdout["error"] != "There was an error" {
+ t.Errorf("Expected error to be set, but got: %v", result.StdOut)
+ }
+}
+
+func TestAnalyzeWithTreatWarningsAsErrorsArgument(t *testing.T) {
+ commandArgs := []string{}
+ exec := utils.NewExecCustomProcess(0, "", "", func(name string, args []string) {
+ commandArgs = args
+ })
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(PackageAnalyzeCommand{exec}).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ test.RunCli([]string{"studio", "package", "analyze", "--source", source, "--treat-warnings-as-errors", "true"}, context)
+
+ if !slices.Contains(commandArgs, "--treatWarningsAsErrors") {
+ t.Errorf("Expected argument --treatWarningsAsErrors, but got: %v", strings.Join(commandArgs, " "))
+ }
+}
+
+func TestAnalyzeWithStopOnRuleViolationArgument(t *testing.T) {
+ commandArgs := []string{}
+ exec := utils.NewExecCustomProcess(0, "", "", func(name string, args []string) {
+ commandArgs = args
+ })
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(PackageAnalyzeCommand{exec}).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ test.RunCli([]string{"studio", "package", "analyze", "--source", source, "--stop-on-rule-violation", "true"}, context)
+
+ if !slices.Contains(commandArgs, "--stopOnRuleViolation") {
+ t.Errorf("Expected argument --stopOnRuleViolation, but got: %v", strings.Join(commandArgs, " "))
+ }
+}
+
+func findViolation(violations []interface{}, errorCode string) map[string]interface{} {
+ var violation map[string]interface{}
+ for _, v := range violations {
+ vMap := v.(map[string]interface{})
+ if vMap["errorCode"] == errorCode {
+ violation = vMap
+ }
+ }
+ return violation
+}
+
+func studioCrossPlatformProjectDirectory() string {
+ _, filename, _, _ := runtime.Caller(0)
+ return filepath.Join(filepath.Dir(filename), "projects", "crossplatform")
+}
+
+func createDirectory(t *testing.T) string {
+ tmp, err := os.MkdirTemp("", "uipath-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { os.RemoveAll(tmp) })
+ return tmp
+}
diff --git a/plugin/studio/studio_plugin_windows_test.go b/plugin/studio/studio_plugin_windows_test.go
new file mode 100644
index 0000000..f9dcdad
--- /dev/null
+++ b/plugin/studio/studio_plugin_windows_test.go
@@ -0,0 +1,153 @@
+//go:build windows
+
+package studio
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+
+ "github.com/UiPath/uipathcli/test"
+ "github.com/UiPath/uipathcli/utils"
+)
+
+func TestPackWindowsSuccessfully(t *testing.T) {
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(NewPackagePackCommand()).
+ Build()
+
+ source := studioWindowsProjectDirectory()
+ destination := createDirectory(t)
+ result := test.RunCli([]string{"studio", "package", "pack", "--source", source, "--destination", destination}, context)
+
+ stdout := map[string]interface{}{}
+ err := json.Unmarshal([]byte(result.StdOut), &stdout)
+ if err != nil {
+ t.Errorf("Failed to deserialize pack command result: %v", err)
+ }
+ if stdout["status"] != "Succeeded" {
+ t.Errorf("Expected status to be Succeeded, but got: %v", result.StdOut)
+ }
+ if stdout["error"] != nil {
+ t.Errorf("Expected error to be nil, but got: %v", result.StdOut)
+ }
+ if stdout["name"] != "MyWindowsProcess" {
+ t.Errorf("Expected name to be set, but got: %v", result.StdOut)
+ }
+ if stdout["description"] != "Blank Process" {
+ t.Errorf("Expected version to be set, but got: %v", result.StdOut)
+ }
+ if stdout["projectId"] != "94c4c9c1-68c3-45d4-be14-d4427f17eddd" {
+ t.Errorf("Expected projectId to be set, but got: %v", result.StdOut)
+ }
+ if stdout["version"] != "1.0.0" {
+ t.Errorf("Expected version to be set, but got: %v", result.StdOut)
+ }
+ outputFile := stdout["output"].(string)
+ if outputFile != filepath.Join(destination, "MyWindowsProcess.1.0.0.nupkg") {
+ t.Errorf("Expected output path to be set, but got: %v", result.StdOut)
+ }
+ if _, err := os.Stat(outputFile); err != nil {
+ t.Errorf("Expected output file %s to exists, but could not find it: %v", outputFile, err)
+ }
+}
+
+func TestPackOnWindowsWithCorrectArguments(t *testing.T) {
+ commandName := ""
+ commandArgs := []string{}
+ exec := utils.NewExecCustomProcess(0, "", "", func(name string, args []string) {
+ commandName = name
+ commandArgs = args
+ })
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(PackagePackCommand{exec}).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ destination := createDirectory(t)
+ test.RunCli([]string{"studio", "package", "pack", "--source", source, "--destination", destination}, context)
+
+ if !strings.HasSuffix(commandName, "uipcli.exe") {
+ t.Errorf("Expected command name to be uipcli.exe, but got: %v", commandName)
+ }
+ if commandArgs[0] != "package" {
+ t.Errorf("Expected 1st argument to be package, but got: %v", commandArgs[0])
+ }
+ if commandArgs[1] != "pack" {
+ t.Errorf("Expected 2nd argument to be pack, but got: %v", commandArgs[1])
+ }
+ if commandArgs[2] != filepath.Join(source, "project.json") {
+ t.Errorf("Expected 3rd argument to be the project.json, but got: %v", commandArgs[2])
+ }
+ if commandArgs[3] != "--output" {
+ t.Errorf("Expected 4th argument to be output, but got: %v", commandArgs[3])
+ }
+ if commandArgs[4] != destination {
+ t.Errorf("Expected 5th argument to be the output path, but got: %v", commandArgs[4])
+ }
+}
+
+func TestAnalyzeWindowsSuccessfully(t *testing.T) {
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(NewPackageAnalyzeCommand()).
+ Build()
+
+ source := studioWindowsProjectDirectory()
+ result := test.RunCli([]string{"studio", "package", "analyze", "--source", source}, context)
+
+ stdout := map[string]interface{}{}
+ err := json.Unmarshal([]byte(result.StdOut), &stdout)
+ if err != nil {
+ t.Errorf("Failed to deserialize analyze command result: %v", err)
+ }
+ if stdout["status"] != "Succeeded" {
+ t.Errorf("Expected status to be Succeeded, but got: %v", result.StdOut)
+ }
+ if stdout["error"] != nil {
+ t.Errorf("Expected error to be nil, but got: %v", result.StdOut)
+ }
+ violations := stdout["violations"].([]interface{})
+ if len(violations) == 0 {
+ t.Errorf("Expected violations not to be empty, but got: %v", result.StdOut)
+ }
+}
+
+func TestAnalyzeOnWindowsWithCorrectArguments(t *testing.T) {
+ commandName := ""
+ commandArgs := []string{}
+ exec := utils.NewExecCustomProcess(0, "", "", func(name string, args []string) {
+ commandName = name
+ commandArgs = args
+ })
+ context := test.NewContextBuilder().
+ WithDefinition("studio", studioDefinition).
+ WithCommandPlugin(PackageAnalyzeCommand{exec}).
+ Build()
+
+ source := studioCrossPlatformProjectDirectory()
+ test.RunCli([]string{"studio", "package", "analyze", "--source", source}, context)
+
+ if !strings.HasSuffix(commandName, "uipcli.exe") {
+ t.Errorf("Expected command name to be uipcli.exe, but got: %v", commandName)
+ }
+ if commandArgs[0] != "package" {
+ t.Errorf("Expected 2nd argument to be package, but got: %v", commandArgs[0])
+ }
+ if commandArgs[1] != "analyze" {
+ t.Errorf("Expected 3rd argument to be analyze, but got: %v", commandArgs[1])
+ }
+ if commandArgs[2] != filepath.Join(source, "project.json") {
+ t.Errorf("Expected 4th argument to be the project.json, but got: %v", commandArgs[2])
+ }
+}
+
+func studioWindowsProjectDirectory() string {
+ _, filename, _, _ := runtime.Caller(0)
+ return filepath.Join(filepath.Dir(filename), "projects", "windows")
+}
diff --git a/plugin/studio/studio_project_json.go b/plugin/studio/studio_project_json.go
new file mode 100644
index 0000000..25b2db3
--- /dev/null
+++ b/plugin/studio/studio_project_json.go
@@ -0,0 +1,8 @@
+package studio
+
+type studioProjectJson struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ ProjectId string `json:"projectId"`
+ TargetFramework string `json:"targetFramework"`
+}
diff --git a/plugin/studio/studio_project_reader.go b/plugin/studio/studio_project_reader.go
new file mode 100644
index 0000000..e96be3b
--- /dev/null
+++ b/plugin/studio/studio_project_reader.go
@@ -0,0 +1,44 @@
+package studio
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+)
+
+type studioProjectReader struct {
+ Path string
+}
+
+func (p studioProjectReader) GetTargetFramework() TargetFramework {
+ project, _ := p.ReadMetadata()
+ if strings.EqualFold(project.TargetFramework, "windows") {
+ return TargetFrameworkWindows
+ }
+ return TargetFrameworkCrossPlatform
+}
+
+func (p studioProjectReader) ReadMetadata() (studioProjectJson, error) {
+ file, err := os.Open(p.Path)
+ if err != nil {
+ return studioProjectJson{}, fmt.Errorf("Error reading %s file: %v", defaultProjectJson, err)
+ }
+ defer file.Close()
+ byteValue, err := io.ReadAll(file)
+ if err != nil {
+ return studioProjectJson{}, fmt.Errorf("Error reading %s file: %v", defaultProjectJson, err)
+ }
+
+ var project studioProjectJson
+ err = json.Unmarshal(byteValue, &project)
+ if err != nil {
+ return studioProjectJson{}, fmt.Errorf("Error parsing %s file: %v", defaultProjectJson, err)
+ }
+ return project, nil
+}
+
+func newStudioProjectReader(path string) *studioProjectReader {
+ return &studioProjectReader{path}
+}
diff --git a/plugin/studio/target_framework.go b/plugin/studio/target_framework.go
new file mode 100644
index 0000000..969cc5d
--- /dev/null
+++ b/plugin/studio/target_framework.go
@@ -0,0 +1,8 @@
+package studio
+
+type TargetFramework int
+
+const (
+ TargetFrameworkCrossPlatform TargetFramework = iota + 1
+ TargetFrameworkWindows
+)
diff --git a/plugin/studio/uipcli.go b/plugin/studio/uipcli.go
new file mode 100644
index 0000000..04ddde9
--- /dev/null
+++ b/plugin/studio/uipcli.go
@@ -0,0 +1,66 @@
+package studio
+
+import (
+ "fmt"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+
+ "github.com/UiPath/uipathcli/log"
+ "github.com/UiPath/uipathcli/plugin"
+ "github.com/UiPath/uipathcli/utils"
+)
+
+const uipcliVersion = "24.12.9111.31003"
+const uipcliUrl = "https://uipath.pkgs.visualstudio.com/Public.Feeds/_apis/packaging/feeds/1c781268-d43d-45ab-9dfc-0151a1c740b7/nuget/packages/UiPath.CLI/versions/" + uipcliVersion + "/content"
+
+const uipcliWindowsVersion = "24.12.9111.31003"
+const uipcliWindowsUrl = "https://uipath.pkgs.visualstudio.com/Public.Feeds/_apis/packaging/feeds/1c781268-d43d-45ab-9dfc-0151a1c740b7/nuget/packages/UiPath.CLI.Windows/versions/" + uipcliWindowsVersion + "/content"
+
+type uipcli struct {
+ Exec utils.ExecProcess
+ Logger log.Logger
+}
+
+func (c uipcli) Execute(targetFramework TargetFramework, args ...string) (utils.ExecCmd, error) {
+ if targetFramework == TargetFrameworkWindows {
+ return c.execute("uipcli-win", uipcliWindowsUrl, args)
+ }
+ return c.execute("uipcli", uipcliUrl, args)
+}
+
+func (c uipcli) execute(name string, url string, args []string) (utils.ExecCmd, error) {
+ uipcliPath, err := c.getPath(name, url)
+ if err != nil {
+ return nil, err
+ }
+
+ path := uipcliPath
+ if filepath.Ext(uipcliPath) == ".dll" {
+ path, err = exec.LookPath("dotnet")
+ if err != nil {
+ return nil, fmt.Errorf("Could not find dotnet runtime to run command: %v", err)
+ }
+ args = append([]string{uipcliPath}, args...)
+ }
+
+ cmd := c.Exec.Command(path, args...)
+ return cmd, nil
+}
+
+func (c uipcli) getPath(name string, url string) (string, error) {
+ externalPlugin := plugin.NewExternalPlugin(c.Logger)
+ executable := "tools/uipcli.dll"
+ if c.isWindows() {
+ executable = "tools/uipcli.exe"
+ }
+ return externalPlugin.GetTool(name, url, executable)
+}
+
+func (c uipcli) isWindows() bool {
+ return runtime.GOOS == "windows"
+}
+
+func newUipcli(exec utils.ExecProcess, logger log.Logger) *uipcli {
+ return &uipcli{exec, logger}
+}
diff --git a/plugin/zip_archive.go b/plugin/zip_archive.go
new file mode 100644
index 0000000..aa6b64d
--- /dev/null
+++ b/plugin/zip_archive.go
@@ -0,0 +1,75 @@
+package plugin
+
+import (
+ "archive/zip"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+const MaxArchiveSize = 1 * 1024 * 1024 * 1024
+
+type zipArchive struct{}
+
+func (z zipArchive) Extract(zipArchive string, destinationFolder string, permissions os.FileMode) error {
+ archive, err := zip.OpenReader(zipArchive)
+ if err != nil {
+ return err
+ }
+ defer archive.Close()
+
+ for _, file := range archive.File {
+ err := z.extractFile(file, destinationFolder, permissions)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (z zipArchive) extractFile(zipFile *zip.File, destinationFolder string, permissions os.FileMode) error {
+ path, err := z.sanitizeArchivePath(destinationFolder, zipFile.Name)
+ if err != nil {
+ return err
+ }
+
+ if zipFile.FileInfo().IsDir() {
+ return os.MkdirAll(path, permissions)
+ }
+ err = os.MkdirAll(filepath.Dir(path), permissions)
+ if err != nil {
+ return err
+ }
+
+ zipFileReader, err := zipFile.Open()
+ if err != nil {
+ return err
+ }
+ defer zipFileReader.Close()
+
+ destinationFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zipFile.Mode())
+ if err != nil {
+ return err
+ }
+ defer destinationFile.Close()
+
+ _, err = io.CopyN(destinationFile, zipFileReader, MaxArchiveSize)
+ if err != nil && err != io.EOF {
+ return err
+ }
+ return nil
+}
+
+func (z zipArchive) sanitizeArchivePath(directory string, name string) (string, error) {
+ result := filepath.Join(directory, name)
+ if strings.HasPrefix(result, filepath.Clean(directory)) {
+ return result, nil
+ }
+ return "", fmt.Errorf("File path '%s' is not allowed", directory)
+}
+
+func newZipArchive() *zipArchive {
+ return &zipArchive{}
+}
diff --git a/test/setup.go b/test/setup.go
index 46809f0..2fdf2b8 100644
--- a/test/setup.go
+++ b/test/setup.go
@@ -187,7 +187,7 @@ func RunCli(args []string, context Context) Result {
stderr := new(bytes.Buffer)
authenticators := []auth.Authenticator{
auth.NewPatAuthenticator(),
- auth.NewOAuthAuthenticator(cache.NewFileCache(), auth.NewExecBrowserLauncher()),
+ auth.NewOAuthAuthenticator(cache.NewFileCache(), *auth.NewBrowserLauncher()),
auth.NewBearerAuthenticator(cache.NewFileCache()),
}
commandPlugins := []plugin.CommandPlugin{}
diff --git a/utils/directories.go b/utils/directories.go
new file mode 100644
index 0000000..b57caf6
--- /dev/null
+++ b/utils/directories.go
@@ -0,0 +1,43 @@
+package utils
+
+import (
+ "os"
+ "path/filepath"
+)
+
+const directoryPermissions = 0700
+
+type Directories struct {
+}
+
+func (d Directories) Temp() (string, error) {
+ return d.userDirectory("tmp")
+}
+
+func (d Directories) Cache() (string, error) {
+ return d.userDirectory("cache")
+}
+
+func (d Directories) Plugin() (string, error) {
+ return d.userDirectory("plugins")
+}
+
+func (d Directories) userDirectory(name string) (string, error) {
+ userDirectory, err := d.baseUserDirectory()
+ if err != nil {
+ return "", err
+ }
+ directory := filepath.Join(userDirectory, name)
+ _ = os.MkdirAll(directory, directoryPermissions)
+ return directory, nil
+}
+
+func (d Directories) baseUserDirectory() (string, error) {
+ userCacheDirectory, err := os.UserCacheDir()
+ if err != nil {
+ return "", err
+ }
+ userDirectory := filepath.Join(userCacheDirectory, "uipath", "uipathcli")
+ _ = os.MkdirAll(userDirectory, directoryPermissions)
+ return userDirectory, nil
+}
diff --git a/utils/exec_cmd.go b/utils/exec_cmd.go
new file mode 100644
index 0000000..5fa7344
--- /dev/null
+++ b/utils/exec_cmd.go
@@ -0,0 +1,11 @@
+package utils
+
+import "io"
+
+type ExecCmd interface {
+ StdoutPipe() (io.ReadCloser, error)
+ StderrPipe() (io.ReadCloser, error)
+ Start() error
+ Wait() error
+ ExitCode() int
+}
diff --git a/utils/exec_custom_cmd.go b/utils/exec_custom_cmd.go
new file mode 100644
index 0000000..35643b1
--- /dev/null
+++ b/utils/exec_custom_cmd.go
@@ -0,0 +1,35 @@
+package utils
+
+import (
+ "io"
+)
+
+type ExecCustomCmd struct {
+ Name string
+ Args []string
+ Exit int
+ StdOut io.ReadCloser
+ StdErr io.ReadCloser
+ OnStart func(name string, args []string)
+}
+
+func (c ExecCustomCmd) StdoutPipe() (io.ReadCloser, error) {
+ return c.StdOut, nil
+}
+
+func (c ExecCustomCmd) StderrPipe() (io.ReadCloser, error) {
+ return c.StdErr, nil
+}
+
+func (c ExecCustomCmd) Start() error {
+ c.OnStart(c.Name, c.Args)
+ return nil
+}
+
+func (c ExecCustomCmd) Wait() error {
+ return nil
+}
+
+func (c ExecCustomCmd) ExitCode() int {
+ return c.Exit
+}
diff --git a/utils/exec_custom_process.go b/utils/exec_custom_process.go
new file mode 100644
index 0000000..18a4c96
--- /dev/null
+++ b/utils/exec_custom_process.go
@@ -0,0 +1,33 @@
+package utils
+
+import (
+ "io"
+ "strings"
+)
+
+type ExecCustomProcess struct {
+ ExitCode int
+ Stdout string
+ Stderr string
+ OnStart func(name string, args []string)
+}
+
+func (e ExecCustomProcess) Command(name string, args ...string) ExecCmd {
+ return ExecCustomCmd{
+ Name: name,
+ Args: args,
+ StdOut: io.NopCloser(strings.NewReader(e.Stdout)),
+ StdErr: io.NopCloser(strings.NewReader(e.Stderr)),
+ Exit: e.ExitCode,
+ OnStart: e.OnStart,
+ }
+}
+
+func NewExecCustomProcess(exitCode int, stdout string, stderr string, onStart func(name string, args []string)) *ExecCustomProcess {
+ return &ExecCustomProcess{
+ ExitCode: exitCode,
+ Stdout: stdout,
+ Stderr: stderr,
+ OnStart: onStart,
+ }
+}
diff --git a/utils/exec_default_cmd.go b/utils/exec_default_cmd.go
new file mode 100644
index 0000000..9aa6ffd
--- /dev/null
+++ b/utils/exec_default_cmd.go
@@ -0,0 +1,30 @@
+package utils
+
+import (
+ "io"
+ "os/exec"
+)
+
+type ExecDefaultCmd struct {
+ Cmd *exec.Cmd
+}
+
+func (c ExecDefaultCmd) StdoutPipe() (io.ReadCloser, error) {
+ return c.Cmd.StdoutPipe()
+}
+
+func (c ExecDefaultCmd) StderrPipe() (io.ReadCloser, error) {
+ return c.Cmd.StderrPipe()
+}
+
+func (c ExecDefaultCmd) Start() error {
+ return c.Cmd.Start()
+}
+
+func (c ExecDefaultCmd) Wait() error {
+ return c.Cmd.Wait()
+}
+
+func (c ExecDefaultCmd) ExitCode() int {
+ return c.Cmd.ProcessState.ExitCode()
+}
diff --git a/utils/exec_default_process.go b/utils/exec_default_process.go
new file mode 100644
index 0000000..f8382e3
--- /dev/null
+++ b/utils/exec_default_process.go
@@ -0,0 +1,14 @@
+package utils
+
+import "os/exec"
+
+type ExecDefaultProcess struct {
+}
+
+func (e ExecDefaultProcess) Command(name string, args ...string) ExecCmd {
+ return ExecDefaultCmd{exec.Command(name, args...)}
+}
+
+func NewExecProcess() ExecProcess {
+ return &ExecDefaultProcess{}
+}
diff --git a/utils/exec_process.go b/utils/exec_process.go
new file mode 100644
index 0000000..dc69d03
--- /dev/null
+++ b/utils/exec_process.go
@@ -0,0 +1,5 @@
+package utils
+
+type ExecProcess interface {
+ Command(name string, args ...string) ExecCmd
+}
diff --git a/utils/progress_bar.go b/utils/progress_bar.go
index 9147021..eb81c82 100644
--- a/utils/progress_bar.go
+++ b/utils/progress_bar.go
@@ -16,9 +16,19 @@ type ProgressBar struct {
renderedLength int
}
-func (b *ProgressBar) Update(text string, current int64, total int64, bytesPerSecond int64) {
+func (b *ProgressBar) UpdatePercentage(text string, percent float64) {
b.logger.LogError("\r")
- length := b.render(text, current, total, bytesPerSecond)
+ length := b.renderTick(text, percent)
+ left := b.renderedLength - length
+ if left > 0 {
+ b.logger.LogError(strings.Repeat(" ", left))
+ }
+ b.renderedLength = length
+}
+
+func (b *ProgressBar) UpdateProgress(text string, current int64, total int64, bytesPerSecond int64) {
+ b.logger.LogError("\r")
+ length := b.renderProgress(text, current, total, bytesPerSecond)
left := b.renderedLength - length
if left > 0 {
b.logger.LogError(strings.Repeat(" ", left))
@@ -33,10 +43,18 @@ func (b *ProgressBar) Remove() {
}
}
-func (b ProgressBar) render(text string, currentBytes int64, totalBytes int64, bytesPerSecond int64) int {
+func (b ProgressBar) renderTick(text string, percent float64) int {
+ bar := b.createBar(percent)
+ output := fmt.Sprintf("%s |%s|",
+ text,
+ bar)
+ b.logger.LogError(output)
+ return utf8.RuneCountInString(output)
+}
+
+func (b ProgressBar) renderProgress(text string, currentBytes int64, totalBytes int64, bytesPerSecond int64) int {
percent := math.Min(float64(currentBytes)/float64(totalBytes)*100.0, 100.0)
- barCount := int(percent / 5.0)
- bar := strings.Repeat("█", barCount) + strings.Repeat(" ", 20-barCount)
+ bar := b.createBar(percent)
totalBytesFormatted, unit := b.formatBytes(totalBytes)
currentBytesFormatted, unit := b.formatBytesInUnit(currentBytes, unit)
bytesPerSecondFormatted, bytesPerSecondUnit := b.formatBytes(bytesPerSecond)
@@ -53,6 +71,14 @@ func (b ProgressBar) render(text string, currentBytes int64, totalBytes int64, b
return utf8.RuneCountInString(output)
}
+func (b ProgressBar) createBar(percent float64) string {
+ barCount := int(percent / 5.0)
+ if barCount > 20 {
+ barCount = 20
+ }
+ return strings.Repeat("█", barCount) + strings.Repeat(" ", 20-barCount)
+}
+
func (b ProgressBar) formatBytes(count int64) (string, string) {
if count < 1000 {
return b.formatBytesInUnit(count, "B")
diff --git a/utils/progress_bar_test.go b/utils/progress_bar_test.go
index 364361b..3f670d4 100644
--- a/utils/progress_bar_test.go
+++ b/utils/progress_bar_test.go
@@ -11,23 +11,23 @@ import (
"github.com/UiPath/uipathcli/log"
)
-func TestProgressBarShowsZeroAtTheBeginning(t *testing.T) {
+func TestProgressBarUpdateProgressShowsZeroAtTheBeginning(t *testing.T) {
var output bytes.Buffer
progressBar := NewProgressBar(log.NewDefaultLogger(&output))
- progressBar.Update("downloading...", 0, 1000, 0)
+ progressBar.UpdateProgress("downloading...", 0, 1000, 0)
if output.String() != "\rdownloading... 0% | | (0.0/1.0 kB, 0 B/s)" {
t.Errorf("Should display progress of 10 percent, but got: %v", output.String())
}
}
-func TestProgressBarUpdatesMultipleTimes(t *testing.T) {
+func TestProgressBarUpdateProgressMultipleTimes(t *testing.T) {
var output bytes.Buffer
progressBar := NewProgressBar(log.NewDefaultLogger(&output))
- progressBar.Update("downloading...", 0, 1000, 1)
- progressBar.Update("downloading...", 200, 1000, 1024)
+ progressBar.UpdateProgress("downloading...", 0, 1000, 1)
+ progressBar.UpdateProgress("downloading...", 200, 1000, 1024)
if output.String() != "\rdownloading... 0% | | (0.0/1.0 kB, 1 B/s)"+
"\rdownloading... 20% |████ | (0.2/1.0 kB, 1.0 kB/s)" {
@@ -39,7 +39,7 @@ func TestProgressBarShowsReadsFromProgressReader(t *testing.T) {
var output bytes.Buffer
progressBar := NewProgressBar(log.NewDefaultLogger(&output))
progressReader := NewProgressReader(data(1000), func(progress Progress) {
- progressBar.Update("download", progress.BytesRead, 1000, progress.BytesPerSecond)
+ progressBar.UpdateProgress("download", progress.BytesRead, 1000, progress.BytesPerSecond)
})
_, _ = progressReader.Read(make([]byte, 100))
@@ -54,7 +54,7 @@ func TestProgressBarShowsMultipleReadsFromProgressReader(t *testing.T) {
var output bytes.Buffer
progressBar := NewProgressBar(log.NewDefaultLogger(&output))
progressReader := NewProgressReader(data(1000), func(progress Progress) {
- progressBar.Update("download", progress.BytesRead, 1000, progress.BytesPerSecond)
+ progressBar.UpdateProgress("download", progress.BytesRead, 1000, progress.BytesPerSecond)
})
_, _ = progressReader.Read(make([]byte, 500))
@@ -67,6 +67,44 @@ func TestProgressBarShowsMultipleReadsFromProgressReader(t *testing.T) {
}
}
+func TestProgressBarUpdatePercentageShowsSimpleBar(t *testing.T) {
+ var output bytes.Buffer
+ progressBar := NewProgressBar(log.NewDefaultLogger(&output))
+
+ progressBar.UpdatePercentage("building...", 0)
+
+ lastLine := lastLine(output)
+ if !strings.HasPrefix(lastLine, "building... | |") {
+ t.Errorf("Should display simple bar, but got: %v", lastLine)
+ }
+}
+
+func TestProgressBarUpdatePercentageMovesBar(t *testing.T) {
+ var output bytes.Buffer
+ progressBar := NewProgressBar(log.NewDefaultLogger(&output))
+ progressBar.UpdatePercentage("building...", 0)
+
+ progressBar.UpdatePercentage("building...", 10)
+
+ lastLine := lastLine(output)
+ if !strings.HasPrefix(lastLine, "building... |██ |") {
+ t.Errorf("Should display simple bar, but got: %v", lastLine)
+ }
+}
+
+func TestProgressBarUpdatePercentageTo100(t *testing.T) {
+ var output bytes.Buffer
+ progressBar := NewProgressBar(log.NewDefaultLogger(&output))
+ progressBar.UpdatePercentage("building...", 0)
+ progressBar.UpdatePercentage("building...", 50)
+ progressBar.UpdatePercentage("building...", 100)
+
+ lastLine := lastLine(output)
+ if !strings.HasPrefix(lastLine, "building... |████████████████████|") {
+ t.Errorf("Should display simple bar, but got: %v", lastLine)
+ }
+}
+
func lastLine(output bytes.Buffer) string {
lines := strings.Split(output.String(), "\r")
return lines[len(lines)-1]