Repository: daisy/pipeline-cli-go
Pull request #52: feat: use DAISY Pipeline app if available
Author: login: NPavie
Created: 2025-02-26T10:53:11Z | Merged: 2025-04-04T14:58:19Z

This PR changes the initialisation process of the command line interface, to allow the use of the DAISY Pipeline electron app :

Additionally, the following settings have been removed or renamed as requested :
- `exec_line` has been renamed as `app_path`
  - If this setting is pointing to a DAISY Pipeline app executable (`DAISY Pipeline` or `DAISY Pipeline.exe`), it will try to launch the app with the requested command line arguments
  - Using the string value `DAISY Pipeline` will look for the DAISY Pipeline app in the user path.
- `ws_timeup` has been removed
  - It's usage in code is replaced by the previous default value `25` 
- `local` has been removed
- `timeout` has been removed
  - It's usage in code is replaced by the previous default value `25` 



Diff:
diff --git a/README.md b/README.md
index f15077e..0ba9a0a 100644
--- a/README.md
+++ b/README.md
@@ -57,20 +57,5 @@ Detailed help for a single command:     dp2 help COMMAND
 Configuration
 -------------
 
-Modify the settings in the file config.yml or alternatively use the global witches:
-
-```
-Global Options:
-       --host [HOST]    Pipeline's webservice host (default http://localhost)
-       --port [PORT]    Pipeline's webserivce port (default 8181)
-       --exec_line_win [EXEC_LINE_WIN]  Pipeline webserivice executable path in windows systems (default )
-       --debug [DEBUG]  Print debug messages. true or false.  (default false)
-       --ws_path [WS_PATH]      Pipeline's webservice path, as in http://daisy.org:8181/path (default ws)
-       --exec_line_nix [EXEC_LINE_NIX]  Pipeline webserivice executable path in unix-like systems (default /home/javi/bin/pipeline2)
-       --client_secret [CLIENT_SECRET]  Client secrect for authenticated requests (default supersecret)
-       --timeout [TIMEOUT]      Http connection timeout in seconds (default 10)
-       --starting [STARTING]    Start the webservice in the local computer if it is not running. true or false (default false)
-       --ws_timeup [WS_TIMEUP]  Time to wait until the webserivce starts in seconds (default 25)
-       --client_key [CLIENT_KEY]        Client key for authenticated requests (default clientid)
-       -f,--file [FILE]        Alternative configuration file
-```
+Modify the settings in config.yml or alternatively use the global
+switches (run `dp2 help -g` to get the list).
diff --git a/cli/cli.go b/cli/cli.go
index 7f714ea..1625f05 100644
--- a/cli/cli.go
+++ b/cli/cli.go
@@ -168,7 +168,10 @@ func (c *Cli) setHelp() {
 //Adds the configuration global options to the parser
 func (c *Cli) addConfigOptions(conf Config) {
 	for option, desc := range config_descriptions {
-		c.AddOption(option, "", fmt.Sprintf("%v (default %v)", desc, conf[option]), "", "", func(optName string, value string) error {
+		if conf[option] != "" {
+			desc = fmt.Sprintf("%v (default %v)", desc, conf[option])
+		}
+		c.AddOption(option, "", desc, "", "", func(optName string, value string) error {
 			log.Println("option:", optName, "value:", value)
 			switch conf[optName].(type) {
 			case int:
@@ -202,6 +205,7 @@ func (c *Cli) addConfigOptions(conf Config) {
 			log.Printf(err.Error())
 			return fmt.Errorf("File not found %v", filePath)
 		}
+		conf[CONFPATH] = filePath
 		return conf.FromYaml(file)
 	})
 }
diff --git a/cli/cli.go.in b/cli/cli.go.in
index a55efbd..43d900d 100644
--- a/cli/cli.go.in
+++ b/cli/cli.go.in
@@ -168,7 +168,10 @@ func (c *Cli) setHelp() {
 //Adds the configuration global options to the parser
 func (c *Cli) addConfigOptions(conf Config) {
 	for option, desc := range config_descriptions {
-		c.AddOption(option, "", fmt.Sprintf("%v (default %v)", desc, conf[option]), "", "", func(optName string, value string) error {
+		if conf[option] != "" {
+			desc = fmt.Sprintf("%v (default %v)", desc, conf[option])
+		}
+		c.AddOption(option, "", desc, "", "", func(optName string, value string) error {
 			log.Println("option:", optName, "value:", value)
 			switch conf[optName].(type) {
 			case int:
@@ -202,6 +205,7 @@ func (c *Cli) addConfigOptions(conf Config) {
 			log.Printf(err.Error())
 			return fmt.Errorf("File not found %v", filePath)
 		}
+		conf[CONFPATH] = filePath
 		return conf.FromYaml(file)
 	})
 }
diff --git a/cli/cli_test.go b/cli/cli_test.go
index 84b1544..500b246 100644
--- a/cli/cli_test.go
+++ b/cli/cli_test.go
@@ -219,10 +219,7 @@ func TestConfigIntOptions(t *testing.T) {
 	if err == nil {
 		t.Errorf("Port: non numeric type controll failed")
 	}
-	err = cli.Run([]string{"--" + WSTIMEUP, "abit", "test"})
-	if err == nil {
-		t.Errorf("ws_time_out: non numeric type controll failed")
-	}
+
 	err = cli.Run([]string{"--timeout" + TIMEOUT, "now!", "test"})
 	if err == nil {
 		t.Errorf("Port: non numeric type controll failed")
@@ -264,20 +261,20 @@ func TestConfigOptions(t *testing.T) {
 		HOST:         "http://google.com",
 		PORT:         80,
 		PATH:         "pipeline",
-		WSTIMEUP:     1,
-		EXECLINE:     "the_noose",
+		APPPATH:      "the_noose",
+		EXECLINE:     "",
 		CLIENTKEY:    "rounded",
 		CLIENTSECRET: "he_likes_justin_beiber",
 		TIMEOUT:      3,
 		DEBUG:        true,
 		STARTING:     true,
+		CONFPATH:     DEFAULT_FILE,
 	}
 
 	err = cli.Run([]string{"--" + HOST, exp[HOST].(string),
 		"--" + PORT, strconv.Itoa(exp[PORT].(int)),
 		"--" + PATH, exp[PATH].(string),
-		"--" + WSTIMEUP, strconv.Itoa(exp[WSTIMEUP].(int)),
-		"--" + EXECLINE, exp[EXECLINE].(string),
+		"--" + APPPATH, exp[APPPATH].(string),
 		"--" + CLIENTKEY, exp[CLIENTKEY].(string),
 		"--" + CLIENTSECRET, exp[CLIENTSECRET].(string),
 		"--" + TIMEOUT, strconv.Itoa(exp[TIMEOUT].(int)),
@@ -341,6 +338,8 @@ func TestConfigFile(t *testing.T) {
 	if err != nil {
 		t.Errorf("Unexpected error %v", err)
 	}
-	tCompareCnfs(res, EXP, t)
+	EXP2 := copyMap(EXP)
+	EXP2[CONFPATH] = tmpFile.Name()
+	tCompareCnfs(res, EXP2, t)
 
 }
diff --git a/cli/config.go b/cli/config.go
index b48cadd..165adcf 100644
--- a/cli/config.go
+++ b/cli/config.go
@@ -1,13 +1,17 @@
 package cli
 
 import (
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"log"
 	"os"
+	"os/exec"
 	"path/filepath"
+	"runtime"
 	"slices"
+	"strings"
 
 	"github.com/kardianos/osext"
 	"launchpad.net/goyaml"
@@ -18,13 +22,14 @@ const (
 	HOST         = "host"
 	PORT         = "port"
 	PATH         = "ws_path"
-	WSTIMEUP     = "ws_timeup"
-	EXECLINE     = "exec_line"
+	APPPATH      = "app_path"
+	EXECLINE     = "exec_line" // hidden feature
 	CLIENTKEY    = "client_key"
 	CLIENTSECRET = "client_secret"
 	TIMEOUT      = "timeout"
 	DEBUG        = "debug"
 	STARTING     = "starting"
+	CONFPATH     = "conf_path"
 )
 
 //Other convinience constants
@@ -33,6 +38,8 @@ const (
 	DEFAULT_FILE = "config.yml"
 )
 
+var WIN_EXT = []string{".exe", ".bat", ".cmd", ".ps1"}
+
 //Config is just a map
 type Config map[string]interface{}
 
@@ -42,30 +49,31 @@ var config = Config{
 	HOST:         "http://localhost",
 	PORT:         8181,
 	PATH:         "ws",
-	WSTIMEUP:     25,
+	APPPATH:      "",
 	EXECLINE:     "",
 	CLIENTKEY:    "",
 	CLIENTSECRET: "",
 	TIMEOUT:      10,
 	DEBUG:        false,
 	STARTING:     false,
+	CONFPATH:     DEFAULT_FILE, // path to the config file, for path resolution (not exposed through config_descriptions)
 }
 
 //Config items descriptions
 var config_descriptions = map[string]string{
 
-	HOST:         "Pipeline's webservice host",
-	PORT:         "Pipeline's webserivce port",
-	PATH:         "Pipeline's webservice path, as in http://daisy.org:8181/path",
-	WSTIMEUP:     "Time to wait until the webserivce starts in seconds",
-	EXECLINE:     "Pipeline webserivice executable path",
+	HOST:         "Host part of webservice address. Leave empty if you wish to use the app_path setting",
+	PORT:         "Port part of webservice address. Leave empty if you wish to use the app_path setting",
+	PATH:         "Path part of webservice address, as in HOST:PORT/PATH (e.g. http://localhost:8181/ws). Leave empty if you wish to use the app_path setting",
+	APPPATH:      "Path to the DAISY Pipeline app. If left empty, the PATH is searched for a \"DAISY Pipeline\" executable",
 	CLIENTKEY:    "Client key for authenticated requests",
-	CLIENTSECRET: "Client secrect for authenticated requests",
-	TIMEOUT:      "Http connection timeout in seconds",
-	DEBUG:        "Print debug messages. true or false. ",
-	STARTING:     "Start the webservice in the local computer if it is not running. true or false",
+	CLIENTSECRET: "Client secret for authenticated requests",
+	TIMEOUT:      "Timeout for requests to the webservice in seconds",
+	DEBUG:        "Print debug messages",
+	STARTING:     "Start the DAISY Pipeline app if it is not running",
 }
 
+
 //Makes a copy of the default config
 func copyConf() Config {
 	ret := make(Config)
@@ -95,15 +103,40 @@ func NewConfig() Config {
 
 //Loads the default configuration file
 func loadDefault(cnf Config) error {
-	folder, err := osext.ExecutableFolder()
+	// first check if a config file is present in the current working directory
+	cwd, err := os.Getwd()
+	if err == nil {
+		file, err := os.Open(filepath.Join(cwd, DEFAULT_FILE))
+		if err == nil {
+			cnf[CONFPATH] = filepath.Join(cwd, DEFAULT_FILE)
+			err = cnf.FromYaml(file)
+			defer file.Close()
+			if err == nil {
+				return nil
+			}
+		}
+	}
+	// no config file found in folder, check next to the dp2 executable
+	// check if the current executable is a symlink, and if so, get the real path
+	execPath, err := osext.Executable()
 	if err != nil {
 		return err
 	}
-	file, err := os.Open(folder + string(os.PathSeparator) + DEFAULT_FILE)
+	if info, err := os.Lstat(execPath); err == nil && (info.Mode()&os.ModeSymlink != 0) {
+		if resolvedPath, err := os.Readlink(execPath); err == nil {
+			execPath = resolvedPath
+		} else {
+			return err
+		}
+	}
+
+	folder := filepath.Dir(execPath)
+	file, err := os.Open(filepath.Join(folder, DEFAULT_FILE))
 	if err != nil {
 		return err
 	}
 	defer file.Close()
+	cnf[CONFPATH] = filepath.Join(folder, DEFAULT_FILE)
 	err = cnf.FromYaml(file)
 	if err != nil {
 		return err
@@ -113,6 +146,11 @@ func loadDefault(cnf Config) error {
 
 //Loads the contents of the yaml file into the configuration
 func (c Config) FromYaml(r io.Reader) error {
+	// only use the default webservice values if app_path is not set or can
+	// not be found (and the webservice is not configured by the user)
+	c[HOST] = ""
+	c[PORT] = 0
+	c[PATH] = ""
 	bytes, err := ioutil.ReadAll(r)
 	if err != nil {
 		return err
@@ -122,6 +160,48 @@ func (c Config) FromYaml(r io.Reader) error {
 		return err
 	}
 	c.UpdateDebug()
+	// Check app path validity
+	if (c[APPPATH].(string) != "" ) {
+		apppath := c.AppPath()
+		switch runtime.GOOS {
+		case "windows":
+			for _, ext := range WIN_EXT {
+				if _, err := os.Stat(apppath + ext); !errors.Is(err, os.ErrNotExist) {
+					// found an executable with the given extension
+					c[APPPATH] = apppath + ext
+					break
+				}
+			}
+			// Check if the app path exists as is
+			if _, err := os.Stat(apppath); errors.Is(err, os.ErrNotExist) {
+				// app does not exists, disable the app path and warn the user
+				fmt.Printf("warning - provided app path was not found : %s (resolved as %s)\n", c[APPPATH].(string), apppath)
+				c[APPPATH] = ""
+			}
+		default:
+			if _, err := os.Stat(apppath); errors.Is(err, os.ErrNotExist) {
+				// app does not exists, disable the app path and warn the user
+				fmt.Printf("warning - provided app path was not found : %s (resolved as %s)\n", c[APPPATH].(string), apppath)
+				c[APPPATH] = ""
+			}
+		}
+		
+	}
+	// App path is empty or points to a non-existing path or a webservice config is defined
+	// - Reset webservice default values if not defined in config file
+	// - Disable starting the app
+	if(c[APPPATH].(string) == "" || c[HOST].(string) != "" || c[PORT].(int) != 0 || c[PATH].(string) != "") {
+		c[STARTING] = false
+		if(c[HOST].(string) == "") {
+			c[HOST] = config[HOST]
+		}
+		if(c[PORT].(int) == 0) {
+			c[PORT] = config[PORT]
+		}
+		if(c[PATH].(string) == "") {
+			c[PATH] = config[PATH]
+		}
+	}
 	return err
 }
 
@@ -135,23 +215,102 @@ func (c Config) UpdateDebug() {
 	}
 }
 
-//Returns the Url composed by HOSTNAME:PORT/PATH/
+// Returns the Url composed by `HOST:PORT/PATH/`
+//
+// For each HOST, PORT and PATH values, if not defined in the config file,
+// the default values are used to build the url (http://localhost:8181/ws/).
+//
+// Note that the PATH is trimmed from slashes at begining or end of the string
 func (c Config) Url() string {
-	return fmt.Sprintf("%v:%v/%v/", c[HOST], c[PORT], c[PATH])
+	var path = config[PATH].(string);
+	if c[PATH] != nil {
+		path = strings.Trim(c[PATH].(string), "/")
+	}
+	if (path != "") {
+		// re-add trailing slash at the end of the path
+		path += "/"
+	}
+	var host = config[HOST].(string)
+	if (c[HOST] != nil) {
+		host = c[HOST].(string)
+	}
+	var port = config[PORT].(int)
+	if (c[PORT] != nil) {
+		port = c[PORT].(int)
+	}
+	testUrl := fmt.Sprintf("%v:%v/%v", host, port, path)
+	return testUrl
 }
-func (c Config) ExecPath() string {
-	base, err := osext.ExecutableFolder()
-	if err != nil {
-		panic("Error getting executable path")
+
+func (c Config) AppPath() string {
+	var base = ""
+	err := error(nil)
+	if c[CONFPATH] != nil {
+		base = filepath.Dir(c[CONFPATH].(string))
+	} else {
+		// this will possibly not resolve symlinked executables
+		base, err = osext.ExecutableFolder()
+		if err != nil {
+			panic("Error getting executable path")
+		}
+	}
+	
+	execpath := c[APPPATH].(string)
+	// An empty app path defaults to looking for DAISY Pipeline app in
+	// the PATH. Note that we choose not to use "DAISY Pipeline" as
+	// the default config value, because this would result in a
+	// "provided app path was not found" warning if no app is found.
+	if execpath == "" {
+		execpath = "DAISY Pipeline"
 	}
-	return c.buildPath(base)
+	return buildPath(base, execpath)
 }
 
-func (c Config) buildPath(base string) string {
-	p := filepath.FromSlash(c[EXECLINE].(string))
+func (c Config) ExecLine() string {
+	var base = ""
+	err := error(nil)
+	if c[CONFPATH] != nil {
+		base = filepath.Dir(c[CONFPATH].(string))
+	} else {
+		// this will possibly not resolve symlinked executables
+		base, err = osext.ExecutableFolder()
+		if err != nil {
+			panic("Error getting executable path")
+		}
+	}
+	return buildPath(base, c[EXECLINE].(string))
+}
+
+func buildPath(base string, execpath string) string {
+	if (execpath == "") {
+		return ""
+	}
+	// Check if the execpath is available the PATH
+	switch runtime.GOOS {
+	case "windows":
+		//on windows, check for all extensions if a matching executable can be found in PATH
+		for _, ext := range WIN_EXT {
+			testingapp := execpath
+			if len(testingapp) < len(ext) || testingapp[len(testingapp)-len(ext):] != ext {
+				testingapp = testingapp + ext
+			}
+			if path, _err := exec.LookPath(testingapp); _err == nil {
+				//app or executable found in path, return the absolute path of it
+				return filepath.FromSlash(path)
+			}
+		}
+		fallthrough
+	default:
+		// Check if the execpath is available the PATH
+		if path, _err := exec.LookPath(execpath); _err == nil {
+			//app or executable found in path, return the absolute path of it
+			return filepath.FromSlash(path)
+		}
+	}
+	p := filepath.FromSlash(execpath)
 	if filepath.IsAbs(p) {
 		return p
 	} else {
-		return base + string(filepath.Separator) + p
+		return filepath.Join(base, p)
 	}
 }
diff --git a/cli/config_test.go b/cli/config_test.go
index 4d037dd..8e5a98e 100644
--- a/cli/config_test.go
+++ b/cli/config_test.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"runtime"
 	"testing"
 
 	"github.com/kardianos/osext"
@@ -12,18 +13,12 @@ import (
 
 var (
 	YAML = `
-#WS CONFIGURATION
 host: http://daisy.org
 port: 9999
 ws_path: ws
-ws_timeup: 10
-#DP2 launch config 
-exec_line: prog 
-local: true
-# ROBOT CONF
+app_path: prog
 client_key: clientid
 client_secret: supersecret
-#connection settings
 timeout: 10
 #debug
 debug: true 
@@ -35,16 +30,26 @@ starting: true
 		"host":          "http://daisy.org",
 		"port":          9999,
 		"ws_path":       "ws",
-		"ws_timeup":     10,
-		"exec_line":     "prog",
+		"app_path":      "", // value should be emptied by the loading to avoid loading non existing programs
+		"exec_line":     "",
 		"client_key":    "clientid",
 		"client_secret": "supersecret",
 		"timeout":       10,
-		"starting":      true,
+		"starting":      false, // should be reset to false as the prog exec does not exists
 		"debug":         true,
+		"conf_path":     "config.yml", // default value, relative to the executable path
 	}
 )
 
+func copyMap(m map[string]interface{})map[string]interface{} {
+    m2 := make(map[string]interface{}, len(m))
+    var id string
+    for id = range m {
+		m2[id] = m[id]
+    }
+    return m2
+}
+
 func tCompareCnfs(one, exp Config, t *testing.T) {
 	var res interface{}
 	var test string
@@ -65,14 +70,9 @@ func tCompareCnfs(one, exp Config, t *testing.T) {
 	if res != exp[test] {
 		t.Errorf(T_STRING, test, exp[test], res)
 	}
-	test = WSTIMEUP
-	res = one[WSTIMEUP]
-	if res != exp[test] {
-		t.Errorf(T_STRING, test, exp[test], res)
-	}
 
-	test = EXECLINE
-	res = one[EXECLINE]
+	test = APPPATH
+	res = one[APPPATH]
 	if res != exp[test] {
 		t.Errorf(T_STRING, test, exp[test], res)
 	}
@@ -105,7 +105,15 @@ func tCompareCnfs(one, exp Config, t *testing.T) {
 	if res != exp[test] {
 		t.Errorf(T_STRING, test, exp[test], res)
 	}
+
+	test = CONFPATH
+	res = one[test]
+	if res != exp[test] {
+		t.Errorf(T_STRING, test, exp[test], res)
+	}
 }
+
+
 func TestConfigYaml(t *testing.T) {
 	yalmStr := bytes.NewBufferString(YAML)
 	cnf := copyConf()
@@ -136,7 +144,11 @@ func TestNewConfigDefaultFile(t *testing.T) {
 	if err != nil {
 		t.Errorf("Unexpected error %v", err)
 	}
-	file, err := os.Create(folder + string(os.PathSeparator) + DEFAULT_FILE)
+	confpath := filepath.Join(folder, DEFAULT_FILE)
+	file, err := os.Create(confpath)
+	if err != nil {
+		t.Errorf("Unexpected error %v", err)
+	}
 	_, err = file.WriteString(YAML)
 	if err != nil {
 		t.Errorf("Unexpected error %v", err)
@@ -147,25 +159,59 @@ func TestNewConfigDefaultFile(t *testing.T) {
 	}
 
 	cnf := NewConfig()
-	tCompareCnfs(cnf, EXP, t)
+	// When reading from a file, the confpath should point to the file path
+	EXP2 := copyMap(EXP)
+	EXP2[CONFPATH] = confpath
+	tCompareCnfs(cnf, EXP2, t)
+
+	// testing with a dummy executable file relative to config
+	dummypath := filepath.Join(folder, "prog")
+	_, err = os.Create(dummypath)
+	if err != nil {
+		t.Errorf("Unexpected error %v", err)
+	}
+	cnf = NewConfig()
+	apppath := cnf.AppPath()
+	expected := filepath.Join(folder, cnf[APPPATH].(string))
+	if apppath != expected {
+		t.Errorf("The app path is not being resolved (result: %v | expected: %v)", apppath, expected)
+	}
 }
 
+
 func TestBuildPath(t *testing.T) {
+
 	//from a absolute path
 	conf := Config{}
-	conf[EXECLINE] = "/home/cosa/pipeline2"
+	conf[APPPATH] = "/home/cosa/pipeline2"
 	base := "/tmp"
-	path := conf.buildPath(base)
+	switch runtime.GOOS {
+	case "windows":
+		//on windows, check by its extension if the provided runner is a script
+		//or an executable, and add the .exe extension if it's not there
+		//(to handle default execpath value)
+		conf[APPPATH] = "C:\\Users\\cosa\\pipeline2.exe"
+		base = "F:\\tmp"
+	}
+	
+	path := buildPath(base, conf[APPPATH].(string))
 	fmt.Printf("path %+v\n", path)
-	if path != conf[EXECLINE] {
-		t.Errorf("If the path is absolute no resolving against base should be done %v %v", path, conf[EXECLINE])
-
-	}
-	conf[EXECLINE] = "../cosa/pipeline2"
-	path = conf.buildPath(base)
-	if path != filepath.FromSlash("/tmp/../cosa/pipeline2") {
-		t.Errorf("The path is not being resolved %v", path)
-
+	if path != conf[APPPATH] {
+		t.Errorf("If the path is absolute no resolving against base should be done (result: %v | tested: %v)", path, conf[APPPATH])
+
+	}
+	conf[APPPATH] = "../cosa/pipeline2"
+	switch runtime.GOOS {
+	case "windows":
+		//on windows, check by its extension if the provided runner is a script
+		//or an executable, and add the .exe extension if it's not there
+		//(to handle default execpath value)
+		conf[APPPATH] = "../cosa/pipeline2.exe"
+	}
+	path = buildPath(base, conf[APPPATH].(string))
+	expected := filepath.Join(base, conf[APPPATH].(string))
+	if path != expected {
+		t.Errorf("The path is not being resolved (result: %v | expected: %v)", path, expected)
 	}
 
 }
diff --git a/cli/link.go b/cli/link.go
index d193621..d14fe34 100644
--- a/cli/link.go
+++ b/cli/link.go
@@ -6,11 +6,13 @@ import (
 	"io"
 	"log"
 	"os"
-	"time"
+	"os/exec"
 	"regexp"
 	"strings"
+	"time"
 
 	"github.com/daisy/pipeline-clientlib-go"
+	"github.com/mitchellh/go-ps"
 )
 
 const (
@@ -88,16 +90,107 @@ func (p PipelineLink) IsLocal() bool {
 //otherwise it brings it up and fills the
 //link object
 func bringUp(pLink *PipelineLink) error {
-	alive, err := pLink.pipeline.Alive()
-	if err != nil {
-		if pLink.config[STARTING].(bool) {
-			alive, err = NewPipelineLauncher(pLink.pipeline,
-				pLink.config.ExecPath(), pLink.config[TIMEOUT].(int)).Launch(os.Stdout)
-			if err != nil {
-				return fmt.Errorf("Error bringing the pipeline2 up %v", err.Error())
+	var alive pipeline.Alive
+	var err error
+	if !((pLink.config[HOST] == nil || pLink.config[HOST].(string) == "") &&
+		 (pLink.config[HOST] == nil || pLink.config[PORT].(int) == 0) &&
+		 (pLink.config[PATH] == nil || pLink.config[PATH].(string) == "")) {
+		// A webservice is configured to be used in loaded configuration either from
+		// - the user provided config (config file or command line)
+		// - The default webservice config, reinstated due to missing or incorrect app path
+		alive, err = pLink.pipeline.Alive()
+		if err != nil {
+			if pLink.config[STARTING] != nil && pLink.config[STARTING].(bool) {
+				execpath := pLink.config.ExecLine()
+				if (execpath != "") {
+					// the exec_line setting was kept for backward compatibility, intended
+					// for use on Linux (where there is no Pipeline app available)
+					alive, err = NewPipelineLauncher(
+						pLink.pipeline,
+						execpath,
+						pLink.config[TIMEOUT].(int),
+					).Launch(os.Stdout)
+					if err != nil {
+						return fmt.Errorf("failed to bring up the webservice: %v", err.Error())
+					}
+				}
 			}
-		} else {
-			return fmt.Errorf("Could not connect to the webservice and I'm not configured to start one\n\tError: %v", err.Error())
+			if (err != nil) {
+				return fmt.Errorf("could not connect to the webservice at %s\n\tError: %v",
+					pLink.config.Url(),
+					err.Error())
+			}
+		}
+	} else {
+		// No webservice configured from initial configuration checks
+		// Check if Pipeline app is running
+		processes, err := ps.Processes()
+		if err == nil {
+			execFound := ""
+			for _, element := range processes {
+				app := element.Executable()
+				if strings.HasSuffix(app, "DAISY Pipeline") ||
+					strings.HasSuffix(app, "DAISY Pipeline.exe") ||
+					pLink.config[APPPATH] != nil && pLink.config[APPPATH].(string) == app {
+					execFound = app
+					break
+				}
+			}
+			if execFound != "" {
+				// Found a running instance of the Pipeline app: reuse it with the command line tool
+				// (Note: multiple instance of the app are not allowed in the electron app)
+				args := os.Args[1:]
+				if len(os.Args) == 1 {
+					args = append(args, "help")
+				}
+				cmd := exec.Command(execFound, args...)
+				cmd.Stdout = os.Stdout
+				cmd.Stderr = os.Stderr
+				if err := cmd.Run(); err != nil {
+					return fmt.Errorf(
+						"could not connect to the running instance of the DAISY Pipeline app: %v", err)
+				} else {
+					os.Exit(cmd.ProcessState.ExitCode())
+				}
+			}
+		}
+		// if no running app process was found, continue here
+		// If the starting flag was set to true (set in config + valid path was provided)
+		if pLink.config[STARTING] != nil && pLink.config[STARTING].(bool) {
+			// app_path is validated at the config level in config.FromYaml()
+			// config.AppPath() returns the resolved path
+			execpath := pLink.config.AppPath()
+			if (execpath != "") {
+				// forward the command to the electron app
+				args := os.Args[1:]
+				if len(os.Args) == 1 {
+					args = append(args, "help")
+				}
+				cmd := exec.Command(execpath, args...)
+				cmd.Stdout = os.Stdout
+				cmd.Stderr = os.Stderr
+				if err := cmd.Run(); err != nil {
+					return fmt.Errorf(
+						"could not start the DAISY Pipeline app at %s: %v", execpath, err)
+				} else {
+					os.Exit(cmd.ProcessState.ExitCode())
+				}
+			} else {
+				return fmt.Errorf(
+					"could not locate the Pipeline app")
+			}
+		}
+		// fallback to default configuration
+		pLink.config = copyConf()
+		pLink.pipeline.SetUrl(pLink.config.Url())
+		alive, err = pLink.pipeline.Alive()
+		if err != nil {
+			return fmt.Errorf(
+				"The DAISY Pipeline app is not running," +
+				" and could not connect to the default webservice address (%s)." +
+				" Pass \"--starting true\" to start the app automatically.\n\tError: %v",
+				pLink.config.Url(),
+				err.Error())
 		}
 	}
 	log.Println("Setting values")
diff --git a/cli/link_test.go b/cli/link_test.go
index 4afec6c..0c114ac 100644
--- a/cli/link_test.go
+++ b/cli/link_test.go
@@ -252,7 +252,7 @@ func TestBadStart(t *testing.T) {
 	pipeline := newPipelineTest(true)
 	pipeline.authentication = true
 	config[STARTING] = true
-	config[EXECLINE] = "nonexistingprogram"
+	config[APPPATH] = "nonexistingprogram"
 	link := PipelineLink{pipeline: pipeline, config: config}
 	err := link.Init()
 	if err == nil {
diff --git a/dp2/config.yml b/dp2/config.yml
index 65def01..1a49a3c 100644
--- a/dp2/config.yml
+++ b/dp2/config.yml
@@ -1,18 +1,30 @@
-#WS CONFIGURATION
-host: http://localhost
-port: 8181
-ws_path: ws
-ws_timeup: 25
-#DP2 launch config 
-exec_line: ../../../../../../../daisy/pipeline-assembly/target/dev-launcher/bin/pipeline2
-
-local: true
-# ROBOT CONF
-client_key: clientid
-client_secret: supersecret
-#connection settings
+# Host part of webservice address
+# Leave empty if you wish to use the app_path setting
+#host: http://localhost
+
+# Port part of webservice address
+# Leave empty if you wish to use the app_path setting
+#port: 8181
+
+# Path part of webservice address, as in HOST:PORT/PATH (e.g. http://localhost:8181/ws)
+# Leave empty if you wish to use the app_path setting
+#ws_path: ws
+
+# Timeout for requests to the webservice in seconds
 timeout: 10
-#debug
+
+# Path to the DAISY Pipeline app
+# If left empty, the PATH is searched for a "DAISY Pipeline" executable
+#app_path:
+
+# Client key for authenticated requests
+#client_key: your-id
+
+# Client secrect for authenticated requests
+#client_secret: your-password
+
+# Print debug messages
 debug: false
-starting: true
 
+# Start the DAISY Pipeline app if it is not running
+starting: false
diff --git a/go.mod b/go.mod
index baada55..37d8944 100644
--- a/go.mod
+++ b/go.mod
@@ -20,4 +20,5 @@ require (
 	golang.org/x/mod v0.7.0
 	golang.org/x/tools v0.5.0
 	launchpad.net/goyaml v0.0.0-20140305200416-000000000051
+	github.com/mitchellh/go-ps v1.0.0
 )
diff --git a/go.sum b/go.sum
index 71ed6e8..0855871 100644
--- a/go.sum
+++ b/go.sum
@@ -24,6 +24,8 @@ github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uia
 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
 github.com/mattn/goveralls v0.0.11 h1:eJXea6R6IFlL1QMKNMzDvvHv/hwGrnvyig4N+0+XiMM=
 github.com/mattn/goveralls v0.0.11/go.mod h1:gU8SyhNswsJKchEV93xRQxX6X3Ei4PJdQk/6ZHvrvRk=
+github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
+github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
 github.com/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI=
 github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4=
 github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=
diff --git a/pom.xml b/pom.xml
index f18696d..1853f32 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@
 
   <groupId>org.daisy.pipeline</groupId>
   <artifactId>cli</artifactId>
-  <version>2.2.2-SNAPSHOT</version>
+  <version>2.3.0-SNAPSHOT</version>
   <packaging>pom</packaging>
   <name>DAISY Pipeline 2 :: Command Line Interface</name>
   <description>Command Line Interface for the DAISY Pipeline 2.</description>


Reviews:
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T12:26:00Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T12:28:28Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T12:35:42Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T12:54:07Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T12:55:54Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T13:50:33Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T13:54:56Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T13:58:08Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T14:02:54Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T14:04:10Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T14:04:43Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T14:05:36Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T14:57:59Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T15:23:44Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T12:30:51Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T12:33:34Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T12:35:25Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T12:35:35Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T13:50:03Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T14:06:13Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T14:28:08Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T14:31:32Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T14:56:51Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T14:59:18Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T10:35:39Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T10:37:09Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T10:52:05Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T11:07:14Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T12:02:50Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T16:24:26Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T10:34:47Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T08:01:46Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T08:04:28Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T13:09:18Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T15:05:38Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T07:08:05Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T09:07:22Z
user: login: bertfrees
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T11:42:38Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T12:18:23Z
user: login: NPavie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T12:18:46Z

Review comments:
diff_hunk: @@ -18,8 +18,7 @@ const (
 )
 
 const (
-	MAIN_HELP_TEMPLATE = `
-Usage {{.Name}} [GLOBAL_OPTIONS] command [COMMAND_OPTIONS] [PARAMS]
+	MAIN_HELP_TEMPLATE = `Usage {{.Name}} [GLOBAL_OPTIONS] command [COMMAND_OPTIONS] [PARAMS]
path: cli/cli.go
user: login: bertfrees
body: Does this mean the empty line before "Usage dp2 ..." is removed? Could this be put in a separate commit?
created_at: 2025-03-13T12:26:00Z
author_association: MEMBER
diff_hunk: @@ -84,18 +80,17 @@ List of global options:       {{.Parent.Name}} help -g
 `
 
 	GLOBAL_OPTIONS_TEMPLATE = `
-
 Global Options:
 {{range .Flags }}       {{flagAligner .FlagStringPrefix}} {{.ShortDesc}}
 {{end}}
 
 `
 
-//{{range .Flags}}{{if len .Short}}-{{.Short}},{{end}}--{{.Long}}{{if isOption .}}   {{upper .Long}}{{end}}       {{.Description}}{{end}}
+// {{range .Flags}}{{if len .Short}}-{{.Short}},{{end}}--{{.Long}}{{if isOption .}}   {{upper .Long}}{{end}}       {{.Description}}{{end}}
path: cli/cli.go
user: login: bertfrees
body: All this reformatting is good (or most of it), but it would be nice if it were in a separate commit, so we could better see what actually changed (namely the `bringUp` function).
created_at: 2025-03-13T12:28:27Z
author_association: MEMBER
diff_hunk: @@ -84,18 +80,17 @@ List of global options:       {{.Parent.Name}} help -g
 `
 
 	GLOBAL_OPTIONS_TEMPLATE = `
-
 Global Options:
 {{range .Flags }}       {{flagAligner .FlagStringPrefix}} {{.ShortDesc}}
 {{end}}
 
 `
 
-//{{range .Flags}}{{if len .Short}}-{{.Short}},{{end}}--{{.Long}}{{if isOption .}}   {{upper .Long}}{{end}}       {{.Description}}{{end}}
+// {{range .Flags}}{{if len .Short}}-{{.Short}},{{end}}--{{.Long}}{{if isOption .}}   {{upper .Long}}{{end}}       {{.Description}}{{end}}
path: cli/cli.go
user: login: bertfrees
body: (Same remark for the "chore: cleanup" commit.)
created_at: 2025-03-13T12:35:42Z
author_association: MEMBER
diff_hunk: @@ -77,18 +77,44 @@ func NewConfig() Config {
 	cnf := copyConf()
 	if err := loadDefault(cnf); err != nil {
 		fmt.Println("Warning : no default configuration file found")
-		log.Println(err.Error())
-		return copyConf()
+		cnf = copyConf()
+		if cnf[DEBUG].(bool) || slices.Contains(os.Args[1:], "--debug") {
+			log.Println(err.Error())
+		}
 	}
+	cnf.UpdateDebug()
path: cli/config.go
user: login: bertfrees
body: This is great, but I'm going to move it to a separate commit. By the way, @NPavie have you got any idea about [this issue](https://github.com/daisy/pipeline-cli-go/issues/50)?
created_at: 2025-03-13T12:54:07Z
author_association: MEMBER
diff_hunk: @@ -77,18 +77,44 @@ func NewConfig() Config {
 	cnf := copyConf()
 	if err := loadDefault(cnf); err != nil {
 		fmt.Println("Warning : no default configuration file found")
-		log.Println(err.Error())
-		return copyConf()
+		cnf = copyConf()
+		if cnf[DEBUG].(bool) || slices.Contains(os.Args[1:], "--debug") {
+			log.Println(err.Error())
+		}
 	}
+	cnf.UpdateDebug()
path: cli/config.go
user: login: bertfrees
body: I would even go further and print the "Warning : no default configuration file found" only when debugging is enabled.

Oh and one more thing: shouldn't the `slices.Contains(os.Args[1:], "--debug")` check for a value `true`?
created_at: 2025-03-13T12:55:54Z
author_association: MEMBER
diff_hunk: @@ -84,17 +86,47 @@ func (p PipelineLink) IsLocal() bool {
 	return p.FsAllow
 }
 
-//checks if the pipeline is up
-//otherwise it brings it up and fills the
-//link object
+// checks if the pipeline is up
+// otherwise it brings it up and fills the
+// link object
 func bringUp(pLink *PipelineLink) error {
 	alive, err := pLink.pipeline.Alive()
 	if err != nil {
-		if pLink.config[STARTING].(bool) {
-			alive, err = NewPipelineLauncher(pLink.pipeline,
-				pLink.config.ExecPath(), pLink.config[TIMEOUT].(int)).Launch(os.Stdout)
-			if err != nil {
-				return fmt.Errorf("Error bringing the pipeline2 up %v", err.Error())
+		// Requested behavior :
+		// dp2 launches pipeline-ui (if needed); dp2 calls pipeline-ui as cli handler; pipeline-ui calls dp2 with the port info as an argument
+
+		// When port attribute is set, the cli tool should not try to start a pipeline instance
+		// and directly connect to pipeline instance for which the port is set on the command line
+		if pLink.config[STARTING].(bool) && !(slices.Contains(os.Args[1:], "--port") || slices.Contains(os.Args[1:], "--host")) {
path: cli/link.go
user: login: bertfrees
body: I think we should not only check the command, but also check in the config file whether `port`, `host` (or `ws_path`) has been set.

created_at: 2025-03-13T13:50:33Z
author_association: MEMBER
diff_hunk: @@ -77,18 +77,44 @@ func NewConfig() Config {
 	cnf := copyConf()
 	if err := loadDefault(cnf); err != nil {
 		fmt.Println("Warning : no default configuration file found")
-		log.Println(err.Error())
-		return copyConf()
+		cnf = copyConf()
+		if cnf[DEBUG].(bool) || slices.Contains(os.Args[1:], "--debug") {
+			log.Println(err.Error())
+		}
 	}
+	cnf.UpdateDebug()
path: cli/config.go
user: login: NPavie
body: Oh wait it's expecting true or false value, i'll change that
created_at: 2025-03-13T13:54:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,8 +18,7 @@ const (
 )
 
 const (
-	MAIN_HELP_TEMPLATE = `
-Usage {{.Name}} [GLOBAL_OPTIONS] command [COMMAND_OPTIONS] [PARAMS]
+	MAIN_HELP_TEMPLATE = `Usage {{.Name}} [GLOBAL_OPTIONS] command [COMMAND_OPTIONS] [PARAMS]
path: cli/cli.go
user: login: NPavie
body: Oups might have wrongly added this to the PR

(Got mixed up with the vs code go extension doing auto format with goeverywhere)

I might clean the PR branch to discard the reformat stuff that got in the way.
created_at: 2025-03-13T13:58:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,17 +86,47 @@ func (p PipelineLink) IsLocal() bool {
 	return p.FsAllow
 }
 
-//checks if the pipeline is up
-//otherwise it brings it up and fills the
-//link object
+// checks if the pipeline is up
+// otherwise it brings it up and fills the
+// link object
 func bringUp(pLink *PipelineLink) error {
 	alive, err := pLink.pipeline.Alive()
 	if err != nil {
-		if pLink.config[STARTING].(bool) {
-			alive, err = NewPipelineLauncher(pLink.pipeline,
-				pLink.config.ExecPath(), pLink.config[TIMEOUT].(int)).Launch(os.Stdout)
-			if err != nil {
-				return fmt.Errorf("Error bringing the pipeline2 up %v", err.Error())
+		// Requested behavior :
+		// dp2 launches pipeline-ui (if needed); dp2 calls pipeline-ui as cli handler; pipeline-ui calls dp2 with the port info as an argument
+
+		// When port attribute is set, the cli tool should not try to start a pipeline instance
+		// and directly connect to pipeline instance for which the port is set on the command line
+		if pLink.config[STARTING].(bool) && !(slices.Contains(os.Args[1:], "--port") || slices.Contains(os.Args[1:], "--host")) {
+			execpath := pLink.config.ExecPath()
+			// if execpath ends with DAISY Pipeline or DAISY Pipeline.exe
+			// we forward the command to the electron app
+			if strings.HasSuffix(execpath, "DAISY Pipeline") || strings.HasSuffix(execpath, "DAISY Pipeline.exe") {
+				args := os.Args[1:]
+				if len(os.Args) == 1 {
+					args = append(args, "help")
+				}
+				absPath, err := exec.LookPath(execpath)
+				if err != nil {
+					absPath = execpath
+				}
+				cmd := exec.Command(absPath, args...)
+				cmd.Stdout = os.Stdout
+				cmd.Stderr = os.Stderr
+				if err := cmd.Run(); err != nil {
+					return fmt.Errorf("Could not use the pipeline app: %v", err)
+				} else {
+					os.Exit(cmd.ProcessState.ExitCode())
+				}
+			} else {
+				alive, err = NewPipelineLauncher(
+					pLink.pipeline,
+					execpath,
+					10,
+				).Launch(os.Stdout)
+				if err != nil {
+					return fmt.Errorf("Error bringing the pipeline2 up %v", err.Error())
+				}
 			}
path: cli/link.go
user: login: bertfrees
body: I didn't think about retaining the support for other web service launcher scripts. I guess it is indeed a possibility. I'm not totally convinced of the usefulness of this feature, now that the web service is launched through the app, but it doesn't hurt either. Three comments:

- If the executable is not the app, it should be allow to have `port`/`host`/`ws_path` in the config/command.
- I'm not sure about reusing the `app_path` setting. OTOH having two settings (`app_path` and `exec_line`) is also a bit confusing.
- On Linux systems, you'd typically start the web service through `systemctl` or something similar. So 1. it's not a file path per se, and 2. it's also easy to start it through the command line (without `dp2` doing it for you).
created_at: 2025-03-13T14:02:54Z
author_association: MEMBER
diff_hunk: @@ -84,17 +86,47 @@ func (p PipelineLink) IsLocal() bool {
 	return p.FsAllow
 }
 
-//checks if the pipeline is up
-//otherwise it brings it up and fills the
-//link object
+// checks if the pipeline is up
+// otherwise it brings it up and fills the
+// link object
 func bringUp(pLink *PipelineLink) error {
 	alive, err := pLink.pipeline.Alive()
 	if err != nil {
-		if pLink.config[STARTING].(bool) {
-			alive, err = NewPipelineLauncher(pLink.pipeline,
-				pLink.config.ExecPath(), pLink.config[TIMEOUT].(int)).Launch(os.Stdout)
-			if err != nil {
-				return fmt.Errorf("Error bringing the pipeline2 up %v", err.Error())
+		// Requested behavior :
+		// dp2 launches pipeline-ui (if needed); dp2 calls pipeline-ui as cli handler; pipeline-ui calls dp2 with the port info as an argument
+
+		// When port attribute is set, the cli tool should not try to start a pipeline instance
+		// and directly connect to pipeline instance for which the port is set on the command line
+		if pLink.config[STARTING].(bool) && !(slices.Contains(os.Args[1:], "--port") || slices.Contains(os.Args[1:], "--host")) {
+			execpath := pLink.config.ExecPath()
+			// if execpath ends with DAISY Pipeline or DAISY Pipeline.exe
+			// we forward the command to the electron app
+			if strings.HasSuffix(execpath, "DAISY Pipeline") || strings.HasSuffix(execpath, "DAISY Pipeline.exe") {
+				args := os.Args[1:]
+				if len(os.Args) == 1 {
+					args = append(args, "help")
+				}
+				absPath, err := exec.LookPath(execpath)
+				if err != nil {
+					absPath = execpath
+				}
+				cmd := exec.Command(absPath, args...)
+				cmd.Stdout = os.Stdout
+				cmd.Stderr = os.Stderr
+				if err := cmd.Run(); err != nil {
+					return fmt.Errorf("Could not use the pipeline app: %v", err)
+				} else {
+					os.Exit(cmd.ProcessState.ExitCode())
+				}
path: cli/link.go
user: login: bertfrees
body: Just wondering: is there nothing like the [`exec()`](https://man7.org/linux/man-pages/man3/exec.3.html) function in Go?
created_at: 2025-03-13T14:04:10Z
author_association: MEMBER
diff_hunk: @@ -1,17 +1,12 @@
-#WS CONFIGURATION
+#WS CONFIGURATION if app is not available
 host: http://localhost
 port: 8181
 ws_path: ws
-ws_timeup: 25
-#DP2 launch config 
-exec_line: DAISY Pipeline
-
-local: true
+#DAISY Pipeline app launch path (default to PATH lookup)
+app_path: DAISY Pipeline
 # ROBOT CONF
 client_key: clientid
 client_secret: supersecret
-#connection settings
-timeout: 10
 #debug
 debug: false
 starting: true
path: dp2/config.yml
user: login: bertfrees
body: Should we change `starting` to false? In the config.yml that we ship with the app, it should of course be set to `true`, but I don't know if there is a need/reason to set it to true in other cases. The same goes for `app_path`: In the config.yml that we ship with the app, this field should be set to the relative path to the app executable, but in the general case I think we can assume there is no app?
created_at: 2025-03-13T14:04:43Z
author_association: MEMBER
diff_hunk: @@ -1,17 +1,12 @@
-#WS CONFIGURATION
+#WS CONFIGURATION if app is not available
path: dp2/config.yml
user: login: bertfrees
body: What does "if the app is not available" mean here?
created_at: 2025-03-13T14:05:36Z
author_association: MEMBER
diff_hunk: @@ -84,17 +86,47 @@ func (p PipelineLink) IsLocal() bool {
 	return p.FsAllow
 }
 
-//checks if the pipeline is up
-//otherwise it brings it up and fills the
-//link object
+// checks if the pipeline is up
+// otherwise it brings it up and fills the
+// link object
 func bringUp(pLink *PipelineLink) error {
 	alive, err := pLink.pipeline.Alive()
 	if err != nil {
-		if pLink.config[STARTING].(bool) {
-			alive, err = NewPipelineLauncher(pLink.pipeline,
-				pLink.config.ExecPath(), pLink.config[TIMEOUT].(int)).Launch(os.Stdout)
-			if err != nil {
-				return fmt.Errorf("Error bringing the pipeline2 up %v", err.Error())
+		// Requested behavior :
+		// dp2 launches pipeline-ui (if needed); dp2 calls pipeline-ui as cli handler; pipeline-ui calls dp2 with the port info as an argument
+
+		// When port attribute is set, the cli tool should not try to start a pipeline instance
+		// and directly connect to pipeline instance for which the port is set on the command line
+		if pLink.config[STARTING].(bool) && !(slices.Contains(os.Args[1:], "--port") || slices.Contains(os.Args[1:], "--host")) {
path: cli/link.go
user: login: NPavie
body: so the default config for those field would be set to empty right ?
created_at: 2025-03-13T14:57:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,17 +86,47 @@ func (p PipelineLink) IsLocal() bool {
 	return p.FsAllow
 }
 
-//checks if the pipeline is up
-//otherwise it brings it up and fills the
-//link object
+// checks if the pipeline is up
+// otherwise it brings it up and fills the
+// link object
 func bringUp(pLink *PipelineLink) error {
 	alive, err := pLink.pipeline.Alive()
 	if err != nil {
-		if pLink.config[STARTING].(bool) {
-			alive, err = NewPipelineLauncher(pLink.pipeline,
-				pLink.config.ExecPath(), pLink.config[TIMEOUT].(int)).Launch(os.Stdout)
-			if err != nil {
-				return fmt.Errorf("Error bringing the pipeline2 up %v", err.Error())
+		// Requested behavior :
+		// dp2 launches pipeline-ui (if needed); dp2 calls pipeline-ui as cli handler; pipeline-ui calls dp2 with the port info as an argument
+
+		// When port attribute is set, the cli tool should not try to start a pipeline instance
+		// and directly connect to pipeline instance for which the port is set on the command line
+		if pLink.config[STARTING].(bool) && !(slices.Contains(os.Args[1:], "--port") || slices.Contains(os.Args[1:], "--host")) {
path: cli/link.go
user: login: bertfrees
body: In the config.yml file of the app, these settings would be absent.

They could possibly still be defined in the default config in the Go code, however it's important that these default values are only used if `app_path` is not set or the app can not be found (or possibly also if the app is not running and `starting` is false). Otherwise, the CLI embedded in the app will not be able to connect to the app while a server is running on localhost:8080.

The benefit of keeping some default, is that `dp2` will still "just work" even without a config file, and that's a nice thing to have. I think it would be nice if the defaults defined in the code match the general config.yml, so that config.yml is only really needed for users that want to edit it (to use as template), and for embedment in the app.
created_at: 2025-03-13T15:23:44Z
author_association: MEMBER
diff_hunk: @@ -1,17 +1,12 @@
-#WS CONFIGURATION
+#WS CONFIGURATION if app is not available
path: dp2/config.yml
user: login: NPavie
body: I was thinking to use the WS configuration if app is not available in path or through the app_path variable.

(but from your comment in the argument test to launch the app, that would need to be change)
created_at: 2025-03-14T12:30:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,17 +1,12 @@
-#WS CONFIGURATION
+#WS CONFIGURATION if app is not available
 host: http://localhost
 port: 8181
 ws_path: ws
-ws_timeup: 25
-#DP2 launch config 
-exec_line: DAISY Pipeline
-
-local: true
+#DAISY Pipeline app launch path (default to PATH lookup)
+app_path: DAISY Pipeline
 # ROBOT CONF
 client_key: clientid
 client_secret: supersecret
-#connection settings
-timeout: 10
 #debug
 debug: false
 starting: true
path: dp2/config.yml
user: login: NPavie
body: Yeah we might need to assume for general distribution that app is not available.

I'm not really sure what would be the default config file to be shipped for general distribution.
created_at: 2025-03-14T12:33:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,17 +86,47 @@ func (p PipelineLink) IsLocal() bool {
 	return p.FsAllow
 }
 
-//checks if the pipeline is up
-//otherwise it brings it up and fills the
-//link object
+// checks if the pipeline is up
+// otherwise it brings it up and fills the
+// link object
 func bringUp(pLink *PipelineLink) error {
 	alive, err := pLink.pipeline.Alive()
 	if err != nil {
-		if pLink.config[STARTING].(bool) {
-			alive, err = NewPipelineLauncher(pLink.pipeline,
-				pLink.config.ExecPath(), pLink.config[TIMEOUT].(int)).Launch(os.Stdout)
-			if err != nil {
-				return fmt.Errorf("Error bringing the pipeline2 up %v", err.Error())
+		// Requested behavior :
+		// dp2 launches pipeline-ui (if needed); dp2 calls pipeline-ui as cli handler; pipeline-ui calls dp2 with the port info as an argument
+
+		// When port attribute is set, the cli tool should not try to start a pipeline instance
+		// and directly connect to pipeline instance for which the port is set on the command line
+		if pLink.config[STARTING].(bool) && !(slices.Contains(os.Args[1:], "--port") || slices.Contains(os.Args[1:], "--host")) {
+			execpath := pLink.config.ExecPath()
+			// if execpath ends with DAISY Pipeline or DAISY Pipeline.exe
+			// we forward the command to the electron app
+			if strings.HasSuffix(execpath, "DAISY Pipeline") || strings.HasSuffix(execpath, "DAISY Pipeline.exe") {
+				args := os.Args[1:]
+				if len(os.Args) == 1 {
+					args = append(args, "help")
+				}
+				absPath, err := exec.LookPath(execpath)
+				if err != nil {
+					absPath = execpath
+				}
+				cmd := exec.Command(absPath, args...)
+				cmd.Stdout = os.Stdout
+				cmd.Stderr = os.Stderr
+				if err := cmd.Run(); err != nil {
+					return fmt.Errorf("Could not use the pipeline app: %v", err)
+				} else {
+					os.Exit(cmd.ProcessState.ExitCode())
+				}
path: cli/link.go
user: login: NPavie
body: Hum not sure

(Note that I don't think exec has an equivalent on windows to "replace" an ongoing executable process, so maybe they cannot make a replicate of that in go)
created_at: 2025-03-14T12:35:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,17 +86,47 @@ func (p PipelineLink) IsLocal() bool {
 	return p.FsAllow
 }
 
-//checks if the pipeline is up
-//otherwise it brings it up and fills the
-//link object
+// checks if the pipeline is up
+// otherwise it brings it up and fills the
+// link object
 func bringUp(pLink *PipelineLink) error {
 	alive, err := pLink.pipeline.Alive()
 	if err != nil {
-		if pLink.config[STARTING].(bool) {
-			alive, err = NewPipelineLauncher(pLink.pipeline,
-				pLink.config.ExecPath(), pLink.config[TIMEOUT].(int)).Launch(os.Stdout)
-			if err != nil {
-				return fmt.Errorf("Error bringing the pipeline2 up %v", err.Error())
+		// Requested behavior :
+		// dp2 launches pipeline-ui (if needed); dp2 calls pipeline-ui as cli handler; pipeline-ui calls dp2 with the port info as an argument
+
+		// When port attribute is set, the cli tool should not try to start a pipeline instance
+		// and directly connect to pipeline instance for which the port is set on the command line
+		if pLink.config[STARTING].(bool) && !(slices.Contains(os.Args[1:], "--port") || slices.Contains(os.Args[1:], "--host")) {
+			execpath := pLink.config.ExecPath()
+			// if execpath ends with DAISY Pipeline or DAISY Pipeline.exe
+			// we forward the command to the electron app
+			if strings.HasSuffix(execpath, "DAISY Pipeline") || strings.HasSuffix(execpath, "DAISY Pipeline.exe") {
+				args := os.Args[1:]
+				if len(os.Args) == 1 {
+					args = append(args, "help")
+				}
+				absPath, err := exec.LookPath(execpath)
+				if err != nil {
+					absPath = execpath
+				}
+				cmd := exec.Command(absPath, args...)
+				cmd.Stdout = os.Stdout
+				cmd.Stderr = os.Stderr
+				if err := cmd.Run(); err != nil {
+					return fmt.Errorf("Could not use the pipeline app: %v", err)
+				} else {
+					os.Exit(cmd.ProcessState.ExitCode())
+				}
+			} else {
+				alive, err = NewPipelineLauncher(
+					pLink.pipeline,
+					execpath,
+					10,
+				).Launch(os.Stdout)
+				if err != nil {
+					return fmt.Errorf("Error bringing the pipeline2 up %v", err.Error())
+				}
 			}
path: cli/link.go
user: login: NPavie
body: > On Linux systems, you'd typically start the web service through systemctl or something similar

Oh i see, i did not think of this usage of exec_line variable !


created_at: 2025-03-14T12:35:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,17 +86,47 @@ func (p PipelineLink) IsLocal() bool {
 	return p.FsAllow
 }
 
-//checks if the pipeline is up
-//otherwise it brings it up and fills the
-//link object
+// checks if the pipeline is up
+// otherwise it brings it up and fills the
+// link object
 func bringUp(pLink *PipelineLink) error {
 	alive, err := pLink.pipeline.Alive()
 	if err != nil {
-		if pLink.config[STARTING].(bool) {
-			alive, err = NewPipelineLauncher(pLink.pipeline,
-				pLink.config.ExecPath(), pLink.config[TIMEOUT].(int)).Launch(os.Stdout)
-			if err != nil {
-				return fmt.Errorf("Error bringing the pipeline2 up %v", err.Error())
+		// Requested behavior :
+		// dp2 launches pipeline-ui (if needed); dp2 calls pipeline-ui as cli handler; pipeline-ui calls dp2 with the port info as an argument
+
+		// When port attribute is set, the cli tool should not try to start a pipeline instance
+		// and directly connect to pipeline instance for which the port is set on the command line
+		if pLink.config[STARTING].(bool) && !(slices.Contains(os.Args[1:], "--port") || slices.Contains(os.Args[1:], "--host")) {
path: cli/link.go
user: login: NPavie
body: > (or possibly also if the app is not running and starting is false)

This implies that the code have to check if the app is running depending on the "starting" parameter, right ?
(It should be doable but might need a little research, possibly using a package Marisa mentionned)

So if I summary, forwarding args to the app should only be done if :
```
No host/port/wspath defined in config (including command line flags)
and (
    app path is set 
    and app exists 
    and (
        starting is true
        or app is already started
    )
) 
```
And with that, we assume the custom config file distributed with the app would have the following fields set
- host/port/wspath undefined or set to empty
- app_path set to the app it is embedded with
- starting set to true
created_at: 2025-03-14T13:50:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,17 +86,43 @@ func (p PipelineLink) IsLocal() bool {
 	return p.FsAllow
 }
 
-//checks if the pipeline is up
-//otherwise it brings it up and fills the
-//link object
+// checks if the pipeline is up
+// otherwise it brings it up and fills the
+// link object
 func bringUp(pLink *PipelineLink) error {
 	alive, err := pLink.pipeline.Alive()
 	if err != nil {
-		if pLink.config[STARTING].(bool) {
-			alive, err = NewPipelineLauncher(pLink.pipeline,
-				pLink.config.ExecPath(), pLink.config[TIMEOUT].(int)).Launch(os.Stdout)
path: cli/link.go
user: login: bertfrees
body: > timeout has been removed
>
> It's usage in code is replaced by the previous default value 25

If we are keeping the support for launch scripts, it might make sense to also keep the timeout setting.
created_at: 2025-03-14T14:06:13Z
author_association: MEMBER
diff_hunk: @@ -1,17 +1,12 @@
-#WS CONFIGURATION
+#WS CONFIGURATION if app is not available
 host: http://localhost
 port: 8181
 ws_path: ws
-ws_timeup: 25
-#DP2 launch config 
-exec_line: DAISY Pipeline
-
-local: true
+#DAISY Pipeline app launch path (default to PATH lookup)
+app_path: DAISY Pipeline
 # ROBOT CONF
 client_key: clientid
 client_secret: supersecret
-#connection settings
-timeout: 10
 #debug
 debug: false
 starting: true
path: dp2/config.yml
user: login: bertfrees
body: IMO it would make sense to have the same values in the file as in the Go code, and with the same comments as in the help command.

```yaml
# Pipeline's webservice host
host: http://localhost
# Pipeline's webserivce port
port: 8181
# Pipeline's webservice path, as in HOST:PORT/PATH (e.g. http://localhost:8181/ws)
path: ws
# Path of the DAISY Pipeline app or the pipeline launch script to use the webservice.
# If empty or only containing 'DAISY Pipeline', the app is searched in the PATH
#app_path:
# Client key for authenticated requests
#client_key:
# Client secrect for authenticated requests
#client_secret:
# Print debug messages
debug: false
# Start the webservice or the DAISY Pipeline app in the local computer if it is not running
starting: false
```

Note that all settings could have been commented out (because they're the default anyway), but I chose to comment out only the ones with an empty/no value, to make it slightly clearer. We could possibly change the values of those commented out lines, so they become suggestions for possible values:

```yaml
# Client key for authenticated requests
#client_key: your-id
# Client secrect for authenticated requests
#client_secret: your-password
```
created_at: 2025-03-14T14:28:08Z
author_association: MEMBER
diff_hunk: @@ -84,17 +86,47 @@ func (p PipelineLink) IsLocal() bool {
 	return p.FsAllow
 }
 
-//checks if the pipeline is up
-//otherwise it brings it up and fills the
-//link object
+// checks if the pipeline is up
+// otherwise it brings it up and fills the
+// link object
 func bringUp(pLink *PipelineLink) error {
 	alive, err := pLink.pipeline.Alive()
 	if err != nil {
-		if pLink.config[STARTING].(bool) {
-			alive, err = NewPipelineLauncher(pLink.pipeline,
-				pLink.config.ExecPath(), pLink.config[TIMEOUT].(int)).Launch(os.Stdout)
-			if err != nil {
-				return fmt.Errorf("Error bringing the pipeline2 up %v", err.Error())
+		// Requested behavior :
+		// dp2 launches pipeline-ui (if needed); dp2 calls pipeline-ui as cli handler; pipeline-ui calls dp2 with the port info as an argument
+
+		// When port attribute is set, the cli tool should not try to start a pipeline instance
+		// and directly connect to pipeline instance for which the port is set on the command line
+		if pLink.config[STARTING].(bool) && !(slices.Contains(os.Args[1:], "--port") || slices.Contains(os.Args[1:], "--host")) {
+			execpath := pLink.config.ExecPath()
+			// if execpath ends with DAISY Pipeline or DAISY Pipeline.exe
+			// we forward the command to the electron app
+			if strings.HasSuffix(execpath, "DAISY Pipeline") || strings.HasSuffix(execpath, "DAISY Pipeline.exe") {
+				args := os.Args[1:]
+				if len(os.Args) == 1 {
+					args = append(args, "help")
+				}
+				absPath, err := exec.LookPath(execpath)
+				if err != nil {
+					absPath = execpath
+				}
+				cmd := exec.Command(absPath, args...)
+				cmd.Stdout = os.Stdout
+				cmd.Stderr = os.Stderr
+				if err := cmd.Run(); err != nil {
+					return fmt.Errorf("Could not use the pipeline app: %v", err)
+				} else {
+					os.Exit(cmd.ProcessState.ExitCode())
+				}
path: cli/link.go
user: login: bertfrees
body: Ah yes, that could be the reason.
created_at: 2025-03-14T14:31:32Z
author_association: MEMBER
diff_hunk: @@ -84,18 +80,17 @@ List of global options:       {{.Parent.Name}} help -g
 `
 
 	GLOBAL_OPTIONS_TEMPLATE = `
-
 Global Options:
 {{range .Flags }}       {{flagAligner .FlagStringPrefix}} {{.ShortDesc}}
 {{end}}
 
 `
 
-//{{range .Flags}}{{if len .Short}}-{{.Short}},{{end}}--{{.Long}}{{if isOption .}}   {{upper .Long}}{{end}}       {{.Description}}{{end}}
+// {{range .Flags}}{{if len .Short}}-{{.Short}},{{end}}--{{.Long}}{{if isOption .}}   {{upper .Long}}{{end}}       {{.Description}}{{end}}
path: cli/cli.go
user: login: NPavie
body: (Found a way to deactivate format on save enforced by my vscode Go extension, on the way to ditch it)
created_at: 2025-03-14T14:56:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,17 +86,47 @@ func (p PipelineLink) IsLocal() bool {
 	return p.FsAllow
 }
 
-//checks if the pipeline is up
-//otherwise it brings it up and fills the
-//link object
+// checks if the pipeline is up
+// otherwise it brings it up and fills the
+// link object
 func bringUp(pLink *PipelineLink) error {
 	alive, err := pLink.pipeline.Alive()
 	if err != nil {
-		if pLink.config[STARTING].(bool) {
-			alive, err = NewPipelineLauncher(pLink.pipeline,
-				pLink.config.ExecPath(), pLink.config[TIMEOUT].(int)).Launch(os.Stdout)
-			if err != nil {
-				return fmt.Errorf("Error bringing the pipeline2 up %v", err.Error())
+		// Requested behavior :
+		// dp2 launches pipeline-ui (if needed); dp2 calls pipeline-ui as cli handler; pipeline-ui calls dp2 with the port info as an argument
+
+		// When port attribute is set, the cli tool should not try to start a pipeline instance
+		// and directly connect to pipeline instance for which the port is set on the command line
+		if pLink.config[STARTING].(bool) && !(slices.Contains(os.Args[1:], "--port") || slices.Contains(os.Args[1:], "--host")) {
path: cli/link.go
user: login: bertfrees
body: >> (or possibly also if the app is not running and starting is false)
>
> This implies that the code have to check if the app is running depending on the "starting" parameter, right?

Hmm, did not think of that, actually.

But indeed, strictly speaking if we want to support `starting=false`, we need such an additional check!

> And with that, we assume the custom config file distributed with the app would have the following fields set
> -  starting set to true

Yes, I guess it is fine to set it to true. The alternative is to set it to false, and show an informative message that the app must be started manually, or that the `--starting` switch must be passed (instead of the generic "Could not connect to the webservice and I'm not configured to start one" message we currently have). In addition, there could be a setting in the app to enable/disable the automatic starting behavior.
created_at: 2025-03-14T14:59:18Z
author_association: MEMBER
diff_hunk: @@ -141,7 +209,27 @@ func (c Config) ExecPath() string {
 }
 
 func (c Config) buildPath(base string) string {
-	p := filepath.FromSlash(c[EXECLINE].(string))
+	execpath := c[APPPATH].(string)
+	//empty app path defaults to looking for DAISY Pipeline app in the PATH
path: cli/config.go
user: login: bertfrees
body: Notes to self (possibly add these as comments in the code):
- We choose not to use "DAISY Pipeline" as the default config value, because this would result in a "provided app path was not found" warning if no app is found.
- Having no default value at all would also have been a possibility, because the field will be set in the config.yml that we ship with the app anyway, and in the general case we can assume there is no app. However it doesn't hurt to add it either, especially because the app will not be searched for anyway when `host`, `port` and `ws_path` are set.
created_at: 2025-03-20T10:35:39Z
author_association: MEMBER
diff_hunk: @@ -15,8 +15,7 @@ var (
 #WS CONFIGURATION
 host: http://daisy.org
 port: 9999
-ws_path: ws
-ws_timeup: 10
+path: ws
path: cli/config_test.go
user: login: bertfrees
body: I kinda liked `ws_path`, rather than `path`. Especially now that we have another "path", namely `app_path`.
created_at: 2025-03-20T10:37:09Z
author_association: MEMBER
diff_hunk: @@ -1,18 +1,20 @@
-#WS CONFIGURATION
+# Pipeline's webservice host
 host: http://localhost
+# Pipeline's webservice port
 port: 8181
path: dp2/config.yml
user: login: bertfrees
body: A note (here and in the help) saying that these should be (left) commented out if one wishes to use `app_path`, would be useful.

For this reason, it might also be better to make them commented out by default, since these are the values that will be used when `app_path` is not set and "DAISY Pipeline" is not found in PATH.

(I can do this.)
created_at: 2025-03-20T10:52:05Z
author_association: MEMBER
diff_hunk: @@ -1,18 +1,20 @@
-#WS CONFIGURATION
+# Pipeline's webservice host
 host: http://localhost
+# Pipeline's webservice port
 port: 8181
-ws_path: ws
-ws_timeup: 25
-#DP2 launch config 
-exec_line: ../../../../../../../daisy/pipeline-assembly/target/dev-launcher/bin/pipeline2
-
-local: true
-# ROBOT CONF
-client_key: clientid
-client_secret: supersecret
-#connection settings
+# Pipeline's webservice path, as in HOST:PORT/PATH (e.g. http://localhost:8181/ws)
+path: ws
+# Timeout for requests to the webservice in seconds
 timeout: 10
-#debug
-debug: false
-starting: true
+# Path of the DAISY Pipeline app or the pipeline launch script to use the webservice.
+# If empty or only containing 'DAISY Pipeline', the app is searched in the PATH
path: dp2/config.yml
user: login: bertfrees
body: Are paths resolved relative to this config file? If so, it's probably good to add a note here and in the help.
created_at: 2025-03-20T11:07:14Z
author_association: MEMBER
diff_hunk: @@ -77,18 +77,44 @@ func NewConfig() Config {
 	cnf := copyConf()
 	if err := loadDefault(cnf); err != nil {
 		fmt.Println("Warning : no default configuration file found")
-		log.Println(err.Error())
-		return copyConf()
+		cnf = copyConf()
+		if cnf[DEBUG].(bool) || slices.Contains(os.Args[1:], "--debug") {
+			log.Println(err.Error())
+		}
 	}
+	cnf.UpdateDebug()
path: cli/config.go
user: login: bertfrees
body: Moved to https://github.com/daisy/pipeline-cli-go/commit/f93348600c5fe544c76a9c66895b12d5dceed385.
created_at: 2025-03-24T12:02:50Z
author_association: MEMBER
diff_hunk: @@ -15,8 +15,7 @@ var (
 #WS CONFIGURATION
 host: http://daisy.org
 port: 9999
-ws_path: ws
-ws_timeup: 10
+path: ws
path: cli/config_test.go
user: login: NPavie
body: We can change it back as you like.
created_at: 2025-03-24T16:24:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,18 +1,20 @@
-#WS CONFIGURATION
+# Pipeline's webservice host
 host: http://localhost
+# Pipeline's webservice port
 port: 8181
-ws_path: ws
-ws_timeup: 25
-#DP2 launch config 
-exec_line: ../../../../../../../daisy/pipeline-assembly/target/dev-launcher/bin/pipeline2
-
-local: true
-# ROBOT CONF
-client_key: clientid
-client_secret: supersecret
-#connection settings
+# Pipeline's webservice path, as in HOST:PORT/PATH (e.g. http://localhost:8181/ws)
+path: ws
+# Timeout for requests to the webservice in seconds
 timeout: 10
-#debug
-debug: false
-starting: true
+# Path of the DAISY Pipeline app or the pipeline launch script to use the webservice.
+# If empty or only containing 'DAISY Pipeline', the app is searched in the PATH
path: dp2/config.yml
user: login: bertfrees
body: @NPavie There is only this question left. Then, if it is OK with you, we can merge this PR and I can make a new CLI release.
created_at: 2025-04-02T10:34:46Z
author_association: MEMBER
diff_hunk: @@ -1,18 +1,20 @@
-#WS CONFIGURATION
+# Pipeline's webservice host
 host: http://localhost
+# Pipeline's webservice port
 port: 8181
-ws_path: ws
-ws_timeup: 25
-#DP2 launch config 
-exec_line: ../../../../../../../daisy/pipeline-assembly/target/dev-launcher/bin/pipeline2
-
-local: true
-# ROBOT CONF
-client_key: clientid
-client_secret: supersecret
-#connection settings
+# Pipeline's webservice path, as in HOST:PORT/PATH (e.g. http://localhost:8181/ws)
+path: ws
+# Timeout for requests to the webservice in seconds
 timeout: 10
-#debug
-debug: false
-starting: true
+# Path of the DAISY Pipeline app or the pipeline launch script to use the webservice.
+# If empty or only containing 'DAISY Pipeline', the app is searched in the PATH
path: dp2/config.yml
user: login: NPavie
body: Hi @bertfrees, I rechecked the code that resolves the path : if it is a relative path that is not registered in the PATH variable (that's the bit I added in the path retrieval), it is evaluated as relative to the "executable folder" in the config.go functions that resolve it.

From the doc of the function, this is the folder that contains the running executable (but i'm not sure if this resolve symlinked executables)

https://github.com/NPavie/pipeline-cli-go/blob/88ce4298e1afb356ec61640ba97b013f2cf703ab/cli/config.go#L219-L242



created_at: 2025-04-03T08:01:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,18 +1,20 @@
-#WS CONFIGURATION
+# Pipeline's webservice host
 host: http://localhost
+# Pipeline's webservice port
 port: 8181
-ws_path: ws
-ws_timeup: 25
-#DP2 launch config 
-exec_line: ../../../../../../../daisy/pipeline-assembly/target/dev-launcher/bin/pipeline2
-
-local: true
-# ROBOT CONF
-client_key: clientid
-client_secret: supersecret
-#connection settings
+# Pipeline's webservice path, as in HOST:PORT/PATH (e.g. http://localhost:8181/ws)
+path: ws
+# Timeout for requests to the webservice in seconds
 timeout: 10
-#debug
-debug: false
-starting: true
+# Path of the DAISY Pipeline app or the pipeline launch script to use the webservice.
+# If empty or only containing 'DAISY Pipeline', the app is searched in the PATH
path: dp2/config.yml
user: login: NPavie
body: Thinking about it, it might be a good idea to change this logic and indeed use a "config relative" evaluation of the path if the path is not absolute or if it is not an executable exposed in the PATH environment variable
created_at: 2025-04-03T08:04:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,18 +1,20 @@
-#WS CONFIGURATION
+# Pipeline's webservice host
 host: http://localhost
+# Pipeline's webservice port
 port: 8181
-ws_path: ws
-ws_timeup: 25
-#DP2 launch config 
-exec_line: ../../../../../../../daisy/pipeline-assembly/target/dev-launcher/bin/pipeline2
-
-local: true
-# ROBOT CONF
-client_key: clientid
-client_secret: supersecret
-#connection settings
+# Pipeline's webservice path, as in HOST:PORT/PATH (e.g. http://localhost:8181/ws)
+path: ws
+# Timeout for requests to the webservice in seconds
 timeout: 10
-#debug
-debug: false
-starting: true
+# Path of the DAISY Pipeline app or the pipeline launch script to use the webservice.
+# If empty or only containing 'DAISY Pipeline', the app is searched in the PATH
path: dp2/config.yml
user: login: bertfrees
body: Yes, I too think that makes sense.
created_at: 2025-04-03T13:09:18Z
author_association: MEMBER
diff_hunk: @@ -1,18 +1,20 @@
-#WS CONFIGURATION
+# Pipeline's webservice host
 host: http://localhost
+# Pipeline's webservice port
 port: 8181
-ws_path: ws
-ws_timeup: 25
-#DP2 launch config 
-exec_line: ../../../../../../../daisy/pipeline-assembly/target/dev-launcher/bin/pipeline2
-
-local: true
-# ROBOT CONF
-client_key: clientid
-client_secret: supersecret
-#connection settings
+# Pipeline's webservice path, as in HOST:PORT/PATH (e.g. http://localhost:8181/ws)
+path: ws
+# Timeout for requests to the webservice in seconds
 timeout: 10
-#debug
-debug: false
-starting: true
+# Path of the DAISY Pipeline app or the pipeline launch script to use the webservice.
+# If empty or only containing 'DAISY Pipeline', the app is searched in the PATH
path: dp2/config.yml
user: login: bertfrees
body: @NPavie Will you make that change?
created_at: 2025-04-03T15:05:38Z
author_association: MEMBER
diff_hunk: @@ -1,18 +1,20 @@
-#WS CONFIGURATION
+# Pipeline's webservice host
 host: http://localhost
+# Pipeline's webservice port
 port: 8181
-ws_path: ws
-ws_timeup: 25
-#DP2 launch config 
-exec_line: ../../../../../../../daisy/pipeline-assembly/target/dev-launcher/bin/pipeline2
-
-local: true
-# ROBOT CONF
-client_key: clientid
-client_secret: supersecret
-#connection settings
+# Pipeline's webservice path, as in HOST:PORT/PATH (e.g. http://localhost:8181/ws)
+path: ws
+# Timeout for requests to the webservice in seconds
 timeout: 10
-#debug
-debug: false
-starting: true
+# Path of the DAISY Pipeline app or the pipeline launch script to use the webservice.
+# If empty or only containing 'DAISY Pipeline', the app is searched in the PATH
path: dp2/config.yml
user: login: NPavie
body: i'm on it
created_at: 2025-04-04T07:08:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,18 +1,20 @@
-#WS CONFIGURATION
+# Pipeline's webservice host
 host: http://localhost
+# Pipeline's webservice port
 port: 8181
-ws_path: ws
-ws_timeup: 25
-#DP2 launch config 
-exec_line: ../../../../../../../daisy/pipeline-assembly/target/dev-launcher/bin/pipeline2
-
-local: true
-# ROBOT CONF
-client_key: clientid
-client_secret: supersecret
-#connection settings
+# Pipeline's webservice path, as in HOST:PORT/PATH (e.g. http://localhost:8181/ws)
+path: ws
+# Timeout for requests to the webservice in seconds
 timeout: 10
-#debug
-debug: false
-starting: true
+# Path of the DAISY Pipeline app or the pipeline launch script to use the webservice.
+# If empty or only containing 'DAISY Pipeline', the app is searched in the PATH
path: dp2/config.yml
user: login: bertfrees
body: thanks!
created_at: 2025-04-04T09:07:21Z
author_association: MEMBER
diff_hunk: @@ -155,11 +162,30 @@ func (c Config) FromYaml(r io.Reader) error {
 	c.UpdateDebug()
 	// Check app path validity
 	if (c[APPPATH].(string) != "" ) {
-		if _, err := os.Stat(c.AppPath()); errors.Is(err, os.ErrNotExist) {
-			// app does not exists, disable the app path and warn the user
-			fmt.Printf("warning - provided app path was not found : %s\n", c[APPPATH].(string))
-			c[APPPATH] = ""
+		apppath := c.AppPath()
+		switch runtime.GOOS {
+		case "windows":
+			for _, ext := range WIN_EXT {
+				if _, err := os.Stat(apppath + ext); !errors.Is(err, os.ErrNotExist) {
+					// found an executable with the given extension
+					c[APPPATH] = apppath + ext
+					break
+				}
+			}
path: cli/config.go
user: login: bertfrees
body: @NPavie Just checking: isn't this check already done within `AppPath()`? 
created_at: 2025-04-04T11:42:38Z
author_association: MEMBER
diff_hunk: @@ -155,11 +162,30 @@ func (c Config) FromYaml(r io.Reader) error {
 	c.UpdateDebug()
 	// Check app path validity
 	if (c[APPPATH].(string) != "" ) {
-		if _, err := os.Stat(c.AppPath()); errors.Is(err, os.ErrNotExist) {
-			// app does not exists, disable the app path and warn the user
-			fmt.Printf("warning - provided app path was not found : %s\n", c[APPPATH].(string))
-			c[APPPATH] = ""
+		apppath := c.AppPath()
+		switch runtime.GOOS {
+		case "windows":
+			for _, ext := range WIN_EXT {
+				if _, err := os.Stat(apppath + ext); !errors.Is(err, os.ErrNotExist) {
+					// found an executable with the given extension
+					c[APPPATH] = apppath + ext
+					break
+				}
+			}
path: cli/config.go
user: login: NPavie
body: Partially : the code in appPath() is a bit different and is more designed to check if the app default value ("Daisy pipeline") has an extension before testing if it is present in PATH.

Here I test if the resolved app path exists, assuming the user could have put a relative path but without extension.

Thinking of it, I could also do a similar "multi extension" check in the app path just in case (to handle the case were a launch script could be accessible from the PATH)
created_at: 2025-04-04T12:18:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -155,11 +162,30 @@ func (c Config) FromYaml(r io.Reader) error {
 	c.UpdateDebug()
 	// Check app path validity
 	if (c[APPPATH].(string) != "" ) {
-		if _, err := os.Stat(c.AppPath()); errors.Is(err, os.ErrNotExist) {
-			// app does not exists, disable the app path and warn the user
-			fmt.Printf("warning - provided app path was not found : %s\n", c[APPPATH].(string))
-			c[APPPATH] = ""
+		apppath := c.AppPath()
+		switch runtime.GOOS {
+		case "windows":
+			for _, ext := range WIN_EXT {
+				if _, err := os.Stat(apppath + ext); !errors.Is(err, os.ErrNotExist) {
+					// found an executable with the given extension
+					c[APPPATH] = apppath + ext
+					break
+				}
+			}
path: cli/config.go
user: login: NPavie
body: (will do it to while i'm at it just in case)
created_at: 2025-04-04T12:18:46Z
author_association: CONTRIBUTOR

PR comments:
user: login: bertfrees
created_at: 2025-03-04T15:47:13Z
body: @NPavie Would it be possible to allow omitting the `--output` option when going through the app, and let the app use its result folder (which can be set in the settings) when the output option was omitted?
author_association: MEMBER
user: login: NPavie
created_at: 2025-03-10T16:25:56Z
body: Quick resume of DAISY meeting regarding this :
- `--output` omission should be feasible on the pipeline-ui side (parameters are not checked before the args forwarding to the app
- I'll add an improved config file checking process, following this order of config file presence : 
  - Check the commande line flag (with -f or --file)
  - Else from the current working directory
  - Else next to the dp2 tool
  - Else use a default config that tries to looks for the app near the dp2 tool or in the path

author_association: CONTRIBUTOR
user: login: bertfrees
created_at: 2025-03-14T14:07:37Z
body: > ws_timeup has been removed
>
> It's usage in code is replaced by the previous default value 25

This setting was not used anymore, or did I look wrong?
author_association: MEMBER
user: login: NPavie
created_at: 2025-03-17T14:39:31Z
body: @bertfrees I reseted the branch to take in account your latest remarks and behaviour requests, and added some additionnal changes.

I removed all the reformat that was done automatically by my vscode extension.

On the behavior change

-  in the config side, if app_path is either not set or set to an invalid path
    - webservice settings are set with the default values declared in code
    - app_path is reset to empty
    - starting is reset to false

I also added a small sanitizing in the config url function to allow the use of slash at the begining or the end of the webservice path in the config.
(So if pipeline webservice is to be bound to the root of an address like http://localhost;8181/  on a custom install of the pipeline, users can still point to it, it won't be discarded in default configuration evaluation and will be resolved as previously)

- In the link side :
    - if web service fields are set (either by the user or by reloading the default values due to invalid/empty app_path), the previous behaviour is kept
    - else if pipeline app is found running on the system, commands are forward to the app
    - else if starting is set to true, the tool tries to launch the app with the commands
    - else the default webservice is tried
    - else the error stating the app nor the webservice could be used

I added a dependency to the gp-ps package to list running process on the user system.
author_association: CONTRIBUTOR
user: login: bertfrees
created_at: 2025-03-17T14:51:57Z
body: @NPavie Sounds good, thanks for the changes. I'll look at it in more detail.
author_association: MEMBER
user: login: NPavie
created_at: 2025-03-17T15:05:37Z
body: Oh wait, my sanitizing in the url function is not correct (i did not reread the format string)
Will change that asap
author_association: CONTRIBUTOR
user: login: bertfrees
created_at: 2025-04-02T10:28:49Z
body: @NPavie I have rebased the branch and made some additions and tweaks.

I have added back the `exec_line` setting instead of reusing app_path for this purpose. See 88ce4298e1afb356ec61640ba97b013f2cf703ab.

Also, we now don't mention this ability of specifying a webservice launch script anymore. We just keep it for backward compatibility. Because it is not certain that it will actually be useful, we keep it a hidden feature in order to not complicate things. See b38f3d685fbd1057b25d2fcf74c66c4d8ebd3cff.
author_association: MEMBER
user: login: NPavie
created_at: 2025-04-04T11:09:58Z
body: @bertfrees there is a test reported in error in the build, that is passing on my dev environment with my latest changes.
(If i read the log correctly, it's the test where a config file is passed as an argument of the command line. In the test, the config file path seems not correctly kept in memory, maybe i missed something for non-windows system)

Can you test it on your side ?

author_association: CONTRIBUTOR
user: login: bertfrees
created_at: 2025-04-04T11:46:41Z
body: >  In the test, the config file path seems not correctly kept in memory

This was because `cli.go` is generated from `cli.go.in`, so you need to edit `cli.go.in`.
author_association: MEMBER
user: login: NPavie
created_at: 2025-04-04T12:09:11Z
body: oh ok ! thanks for the fix !
author_association: CONTRIBUTOR
user: login: bertfrees
created_at: 2025-04-04T14:07:34Z
body: OK I'm going to make release now.
author_association: MEMBER