Merge pull request #237 from trheyi/main

[add] setup
This commit is contained in:
Max 2022-10-28 12:18:37 +08:00 committed by GitHub
commit 7841ca6459
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 2110 additions and 101 deletions

View file

@ -160,11 +160,14 @@ artifacts-linux: clean
rm -f ../xgen-v1.0/pnpm-lock.yaml
echo "BASE=__yao_admin_root" > ../xgen-v1.0/packages/xgen/.env
cd ../xgen-v1.0 && pnpm install && pnpm run build
# Setup UI
cd ../xgen-v1.0/packages/setup && pnpm install && pnpm run build
# Packing
mkdir -p .tmp/data/xgen
cp -r ./ui .tmp/data/ui
cp -r ../xgen-v0.9/dist .tmp/data/xgen/v0.9
cp -r ../xgen-v1.0/packages/setup/build .tmp/data/xgen/setup
cp -r ../xgen-v1.0/packages/xgen/dist .tmp/data/xgen/v1.0
cp -r yao .tmp/data/
go-bindata -fs -pkg data -o data/bindata.go -prefix ".tmp/data/" .tmp/data/...
@ -203,11 +206,15 @@ artifacts-macos: clean
rm -f ../xgen-v1.0/pnpm-lock.yaml
echo "BASE=__yao_admin_root" > ../xgen-v1.0/packages/xgen/.env
cd ../xgen-v1.0 && pnpm install && pnpm run build
# Setup UI
cd ../xgen-v1.0/packages/setup && pnpm install && pnpm run build
# Packing
mkdir -p .tmp/data/xgen
cp -r ./ui .tmp/data/ui
cp -r ../xgen-v0.9/dist .tmp/data/xgen/v0.9
cp -r ../xgen-v1.0/packages/setup/build .tmp/data/xgen/setup
cp -r ../xgen-v1.0/packages/xgen/dist .tmp/data/xgen/v1.0
cp -r yao .tmp/data/
go-bindata -fs -pkg data -o data/bindata.go -prefix ".tmp/data/" .tmp/data/...
@ -271,11 +278,15 @@ release: clean
echo "BASE=__yao_admin_root" > .tmp/xgen/v1.0/packages/xgen/.env
cd .tmp/xgen/v1.0 && pnpm install && pnpm run build
# Setup UI
cd .tmp/xgen/v1.0/packages/setup && pnpm install && pnpm run build
# Packing
mkdir -p .tmp/data/xgen
cp -r ./ui .tmp/data/ui
cp -r ./yao .tmp/data/yao
cp -r .tmp/xgen/v0.9/dist .tmp/data/xgen/v0.9
cp -r .tmp/xgen/v1.0/packages/setup/build .tmp/data/xgen/setup
cp -r .tmp/xgen/v1.0/packages/xgen/dist .tmp/data/xgen/v1.0
go-bindata -fs -pkg data -o data/bindata.go -prefix ".tmp/data/" .tmp/data/...
rm -rf .tmp/data
@ -312,11 +323,15 @@ linux-release: clean
echo "BASE=__yao_admin_root" > .tmp/xgen/v1.0/packages/xgen/.env
cd .tmp/xgen/v1.0 && pnpm install && pnpm run build
# Setup UI
cd .tmp/xgen/v1.0/packages/setup && pnpm install && pnpm run build
# Packing
mkdir -p .tmp/data/xgen
cp -r ./ui .tmp/data/ui
cp -r ./yao .tmp/data/yao
cp -r .tmp/xgen/v0.9/dist .tmp/data/xgen/v0.9
cp -r .tmp/xgen/v1.0/packages/setup/build .tmp/data/xgen/setup
cp -r .tmp/xgen/v1.0/packages/xgen/dist .tmp/data/xgen/v1.0
go-bindata -fs -pkg data -o data/bindata.go -prefix ".tmp/data/" .tmp/data/...
rm -rf .tmp/data

View file

@ -22,6 +22,7 @@ import (
"github.com/yaoapp/yao/engine"
"github.com/yaoapp/yao/fs"
"github.com/yaoapp/yao/service"
"github.com/yaoapp/yao/setup"
"github.com/yaoapp/yao/share"
"github.com/yaoapp/yao/studio"
)
@ -35,6 +36,19 @@ var startCmd = &cobra.Command{
Long: L("Start Engine"),
Run: func(cmd *cobra.Command, args []string) {
// Setup
if setup.Check() {
go setup.Start()
select {
case <-setup.Done:
setup.Stop()
break
case <-setup.Canceled:
os.Exit(1)
break
}
}
// recive interrupt signal
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
@ -58,10 +72,6 @@ var startCmd = &cobra.Command{
}
host := config.Conf.Host
if host == "0.0.0.0" {
host = "127.0.0.1"
}
dataRoot, _ := fs.Root(config.Conf)
fmt.Println(color.WhiteString("\n---------------------------------"))
@ -73,15 +83,29 @@ var startCmd = &cobra.Command{
}
if share.App.XGen == "1.0" {
root, _ := adminRoot()
fmt.Println(color.WhiteString(L(" XGen")), color.GreenString(" 1.0"))
urls := []string{fmt.Sprintf("http://%s:%s", host, port)}
if host == "0.0.0.0" {
urls, _ = setup.URLs(config.Conf)
}
fmt.Println(color.WhiteString(L("XGen")), color.GreenString(" 1.0"))
fmt.Println(color.WhiteString(L("Data")), color.GreenString(" %s", dataRoot))
fmt.Println(color.WhiteString(L("Frontend")), color.GreenString(" http://%s%s/", host, port))
fmt.Println(color.WhiteString(L("Dashboard")), color.GreenString(" http://%s%s%slogin/admin", host, port, root))
fmt.Println(color.WhiteString(L("API")), color.GreenString(" http://%s%s/api", host, port))
for _, url := range urls {
fmt.Println(color.WhiteString(L("Frontend ")), color.GreenString(" %s", url))
fmt.Println(color.WhiteString(L("Dashboard")), color.GreenString(" %s/%s/login/admin", url, strings.Trim(root, "/")))
fmt.Println(color.WhiteString(L("API ")), color.GreenString(" %s/api", url))
}
fmt.Println(color.WhiteString(L("Listening")), color.GreenString(" %s:%d", config.Conf.Host, config.Conf.Port))
} else {
if host == "0.0.0.0" {
host = "127.0.0.1"
}
fmt.Println(color.WhiteString(L("Data")), color.GreenString(" %s", dataRoot))
fmt.Println(color.WhiteString(L("Frontend")), color.GreenString(" http://%s%s/", host, port))
fmt.Println(color.WhiteString(L("Dashboard")), color.GreenString(" http://%s%s/xiang/login/admin", host, port))

View file

@ -40,13 +40,15 @@ func LoadFrom(envfile string) Config {
file, err := filepath.Abs(envfile)
if err != nil {
log.Warn("Can't load env file. %s", err.Error())
}
err = godotenv.Overload(file)
if err != nil {
log.Warn("Can't load env file. %s", err.Error())
// log.Warn("Can't load env file. %s", err.Error())
return Load()
}
godotenv.Overload(file)
// if err != nil {
// // log.Warn("Can't load env file. %s", err.Error())
// }
return Load()
}

View file

@ -1,6 +1,7 @@
// Code generated for package data by go-bindata DO NOT EDIT. (@generated)
// sources:
// .tmp/data/ui/index.html
// .tmp/data/xgen/setup/index.html
// .tmp/data/xgen/v0.9/index.html
// .tmp/data/xgen/v1.0/index.html
// .tmp/data/xgen/v1.0/layouts__index.async.js
@ -216,7 +217,27 @@ func uiIndexHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _xgenSetupIndexHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8a\x70\x77\xf5\x53\x08\x76\x0d\x09\x0d\xe0\x02\x04\x00\x00\xff\xff\xdb\x53\x4c\xdc\x0b\x00\x00\x00")
func xgenSetupIndexHtmlBytes() ([]byte, error) {
return bindataRead(
_xgenSetupIndexHtml,
"xgen/setup/index.html",
)
}
func xgenSetupIndexHtml() (*asset, error) {
bytes, err := xgenSetupIndexHtmlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "xgen/setup/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -236,7 +257,7 @@ func xgenV09IndexHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "xgen/v0.9/index.html", size: 14, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "xgen/v0.9/index.html", size: 14, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -256,7 +277,7 @@ func xgenV10IndexHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "xgen/v1.0/index.html", size: 50, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "xgen/v1.0/index.html", size: 50, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -276,7 +297,7 @@ func xgenV10Layouts__indexAsyncJs() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "xgen/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "xgen/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -296,7 +317,7 @@ func xgenV10UmiJs() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "xgen/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "xgen/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -316,7 +337,7 @@ func yaoApisReadmeMd() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/apis/README.md", size: 13, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/apis/README.md", size: 13, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -336,7 +357,7 @@ func yaoApisChartHttpJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/apis/chart.http.json", size: 624, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/apis/chart.http.json", size: 624, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -356,7 +377,7 @@ func yaoApisImportHttpJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/apis/import.http.json", size: 1676, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/apis/import.http.json", size: 1676, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -376,7 +397,7 @@ func yaoApisPageHttpJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/apis/page.http.json", size: 609, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/apis/page.http.json", size: 609, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -396,7 +417,7 @@ func yaoApisStorageHttpJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/apis/storage.http.json", size: 1026, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/apis/storage.http.json", size: 1026, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -416,7 +437,7 @@ func yaoApisTableHttpJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/apis/table.http.json", size: 3213, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/apis/table.http.json", size: 3213, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -436,7 +457,7 @@ func yaoApisUserHttpJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/apis/user.http.json", size: 1453, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/apis/user.http.json", size: 1453, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -456,7 +477,7 @@ func yaoApisXiangHttpJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/apis/xiang.http.json", size: 1239, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/apis/xiang.http.json", size: 1239, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -476,7 +497,7 @@ func yaoDataAppJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/data/app.json", size: 277, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/data/app.json", size: 277, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -496,7 +517,7 @@ func yaoDataIcons404Png() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -516,7 +537,7 @@ func yaoDataIconsIconIcns() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -536,7 +557,7 @@ func yaoDataIconsIconIco() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -556,7 +577,7 @@ func yaoDataIconsIconPng() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -576,7 +597,7 @@ func yaoDataIndexHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -596,7 +617,7 @@ func yaoFieldsModelTransJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14777, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14777, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -616,7 +637,7 @@ func yaoFlowsReadmeMd() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/flows/README.md", size: 12, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/flows/README.md", size: 12, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -636,7 +657,7 @@ func yaoFlowsAdminGuardFlowJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/flows/admin/guard.flow.json", size: 559, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/flows/admin/guard.flow.json", size: 559, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -656,7 +677,7 @@ func yaoFlowsMenuFlowJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/flows/menu.flow.json", size: 1045, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/flows/menu.flow.json", size: 1045, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -676,7 +697,7 @@ func yaoFlowsUserFindFlowJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/flows/user/find.flow.json", size: 328, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/flows/user/find.flow.json", size: 328, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -696,7 +717,7 @@ func yaoLangsEnUsJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -716,7 +737,7 @@ func yaoLangsZhCnGlobalYml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1629, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1629, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -736,7 +757,7 @@ func yaoLangsZhCnLoginsAdminLoginYml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -756,7 +777,7 @@ func yaoLangsZhCnLoginsUserLoginYml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -776,7 +797,7 @@ func yaoLangsZhHkGlobalYml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1629, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1629, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -796,7 +817,7 @@ func yaoLangsZhHkLoginsAdminLoginYml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -816,7 +837,7 @@ func yaoLangsZhHkLoginsUserLoginYml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -836,7 +857,7 @@ func yaoModelsReadmeMd() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/models/README.md", size: 15, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/models/README.md", size: 15, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -856,7 +877,7 @@ func yaoModelsMenuModJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/models/menu.mod.json", size: 2378, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/models/menu.mod.json", size: 2378, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -876,7 +897,7 @@ func yaoModelsUserModJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/models/user.mod.json", size: 8826, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/models/user.mod.json", size: 8826, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -896,7 +917,7 @@ func yaoModelsWorkflowModJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/models/workflow.mod.json", size: 3333, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/models/workflow.mod.json", size: 3333, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -916,7 +937,7 @@ func yaoTablesReadmeMd() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/tables/README.md", size: 15, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/tables/README.md", size: 15, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -936,7 +957,7 @@ func yaoTablesMenuTabJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/tables/menu.tab.json", size: 10471, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/tables/menu.tab.json", size: 10471, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -956,7 +977,7 @@ func yaoTablesUserTabJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "yao/tables/user.tab.json", size: 2357, mode: os.FileMode(420), modTime: time.Unix(1666415327, 0)}
info := bindataFileInfo{name: "yao/tables/user.tab.json", size: 2357, mode: os.FileMode(420), modTime: time.Unix(1666927855, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -1014,6 +1035,7 @@ func AssetNames() []string {
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
"ui/index.html": uiIndexHtml,
"xgen/setup/index.html": xgenSetupIndexHtml,
"xgen/v0.9/index.html": xgenV09IndexHtml,
"xgen/v1.0/index.html": xgenV10IndexHtml,
"xgen/v1.0/layouts__index.async.js": xgenV10Layouts__indexAsyncJs,
@ -1100,6 +1122,9 @@ var _bintree = &bintree{nil, map[string]*bintree{
"index.html": {uiIndexHtml, map[string]*bintree{}},
}},
"xgen": {nil, map[string]*bintree{
"setup": {nil, map[string]*bintree{
"index.html": {xgenSetupIndexHtml, map[string]*bintree{}},
}},
"v0.9": {nil, map[string]*bintree{
"index.html": {xgenV09IndexHtml, map[string]*bintree{}},
}},

View file

@ -33,6 +33,18 @@ func XgenV1() *assetfs.AssetFS {
panic("unreachable")
}
// Setup Setup ui
func Setup() *assetfs.AssetFS {
assetInfo := func(path string) (os.FileInfo, error) {
return os.Stat(path)
}
for k := range _bintree.Children {
k = "xgen/setup"
return &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: assetInfo, Prefix: k, Fallback: "index.html"}
}
panic("unreachable")
}
// ReplaceXGen bindata file
func ReplaceXGen(search, replace string) error {
err := replaceXGenIndex(search, replace)

View file

@ -53,7 +53,6 @@ func TestCommandStart(t *testing.T) {
request := func() (maps.MapStr, error) {
time.Sleep(time.Microsecond * 2000)
url := fmt.Sprintf("http://%s:%d/api/user/find/1?select=id,name", "127.0.0.1", config.Conf.Port)
// utils.Dump(url)
resp, err := http.Get(url)
if err != nil {
return nil, err

View file

@ -1,38 +1,17 @@
package service
import (
"fmt"
"net/http"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/data"
"github.com/yaoapp/yao/share"
)
// XGenFileServerV0 XGen v0.9
var XGenFileServerV0 http.Handler = http.FileServer(data.XgenV0())
// XGenFileServerV1 XGen v1.0
var XGenFileServerV1 http.Handler = http.FileServer(data.XgenV1())
// AppFileServer 应用静态文件
var AppFileServer http.Handler = http.FileServer(http.Dir(filepath.Join(config.Conf.Root, "ui")))
// Middlewares 服务中间件
var Middlewares = []gin.HandlerFunc{
// BindDomain,
BinStatic,
}
// AdminRoot cache
var AdminRoot = ""
// AdminRootLen cache
var AdminRootLen = 0
// BinStatic 静态文件服务
func BinStatic(c *gin.Context) {
@ -44,10 +23,10 @@ func BinStatic(c *gin.Context) {
return
} else if share.App.XGen == "1.0" {
// Xgen 1.0
adminRoot, adminRootLen := adminRoot()
if length >= adminRootLen && c.Request.URL.Path[0:adminRootLen] == adminRoot {
c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, c.Request.URL.Path[0:adminRootLen-1])
if length >= AdminRootLen && c.Request.URL.Path[0:AdminRootLen] == AdminRoot {
c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, c.Request.URL.Path[0:AdminRootLen-1])
XGenFileServerV1.ServeHTTP(c.Writer, c.Request)
c.Abort()
return
@ -69,24 +48,7 @@ func BinStatic(c *gin.Context) {
}
// 应用内静态文件目录(/ui)
// 应用内静态文件目录(/ui or public)
AppFileServer.ServeHTTP(c.Writer, c.Request)
c.Abort()
}
func adminRoot() (string, int) {
if AdminRoot != "" {
return AdminRoot, AdminRootLen
}
adminRoot := "/yao/"
if share.App.AdminRoot != "" {
root := strings.TrimPrefix(share.App.AdminRoot, "/")
root = strings.TrimSuffix(root, "/")
adminRoot = fmt.Sprintf("/%s/", root)
}
adminRootLen := len(adminRoot)
AdminRoot = adminRoot
AdminRootLen = adminRootLen
return AdminRoot, AdminRootLen
}

View file

@ -10,12 +10,13 @@ import (
)
var shutdown = make(chan bool, 1)
var shutdownComplete = make(chan bool, 1)
// Start 启动服务
func Start() error {
err := share.SessionStart()
err := prepare()
if err != nil {
return err
}
@ -84,3 +85,19 @@ func StopWithContext(ctx context.Context, onComplete func()) {
onComplete()
}
}
func prepare() error {
// Session server
err := share.SessionStart()
if err != nil {
return err
}
err = SetupStatic()
if err != nil {
return err
}
return nil
}

126
service/static.go Normal file
View file

@ -0,0 +1,126 @@
package service
import (
"errors"
"fmt"
"io/fs"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/data"
"github.com/yaoapp/yao/share"
)
// AppFileServer static file server
var AppFileServer http.Handler
// XGenFileServerV0 XGen v0.9
var XGenFileServerV0 http.Handler = http.FileServer(data.XgenV0())
// XGenFileServerV1 XGen v1.0
var XGenFileServerV1 http.Handler = http.FileServer(data.XgenV1())
// AdminRoot cache
var AdminRoot = ""
// AdminRootLen cache
var AdminRootLen = 0
// Dir files
type Dir string
// SetupStatic setup static file server
func SetupStatic() error {
// SetAdmin Root
adminRoot()
// Static file server
AppFileServer = http.FileServer(Dir(filepath.Join(config.Conf.Root, "public")))
if share.App.XGen == "" || share.App.XGen == "0.9" {
AppFileServer = http.FileServer(Dir(filepath.Join(config.Conf.Root, "ui")))
}
return nil
}
// Open implements FileSystem using os.Open, opening files for reading rooted
// and relative to the directory d.
func (d Dir) Open(name string) (http.File, error) {
if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) {
return nil, errors.New("http: invalid character in file path")
}
dir := string(d)
if dir == "" {
dir = "."
}
fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))
// Close dir views Disable directory listing
stat, err := os.Stat(fullName)
if err != nil {
return nil, mapOpenError(err, fullName, filepath.Separator, os.Stat)
}
if stat.IsDir() {
indexFile := filepath.Join(fullName, "index.html")
if _, err := os.Stat(indexFile); os.IsNotExist(err) {
return nil, mapOpenError(fs.ErrNotExist, fullName, filepath.Separator, os.Stat)
}
}
f, err := os.Open(fullName)
if err != nil {
return nil, mapOpenError(err, fullName, filepath.Separator, os.Stat)
}
return f, nil
}
// mapOpenError maps the provided non-nil error from opening name
// to a possibly better non-nil error. In particular, it turns OS-specific errors
// about opening files in non-directories into fs.ErrNotExist. See Issues 18984 and 49552.
func mapOpenError(originalErr error, name string, sep rune, stat func(string) (fs.FileInfo, error)) error {
if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) {
return originalErr
}
parts := strings.Split(name, string(sep))
for i := range parts {
if parts[i] == "" {
continue
}
fi, err := stat(strings.Join(parts[:i+1], string(sep)))
if err != nil {
return originalErr
}
if !fi.IsDir() {
return fs.ErrNotExist
}
}
return originalErr
}
// SetupAdmin setup admin static root
func adminRoot() (string, int) {
if AdminRoot != "" {
return AdminRoot, AdminRootLen
}
adminRoot := "/yao/"
if share.App.AdminRoot != "" {
root := strings.TrimPrefix(share.App.AdminRoot, "/")
root = strings.TrimSuffix(root, "/")
adminRoot = fmt.Sprintf("/%s/", root)
}
adminRootLen := len(adminRoot)
AdminRoot = adminRoot
AdminRootLen = adminRootLen
return AdminRoot, AdminRootLen
}

View file

@ -215,14 +215,14 @@ func watchModel(root string, file string, event string, cfg config.Config) {
fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error()))
return
}
mod, err := gou.LoadModelReturn(string(content), name)
_, err = gou.LoadModelReturn(string(content), name)
if err != nil {
fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error()))
return
}
mod.Migrate(true)
fmt.Println(color.GreenString("[Watch] Model: %s Created", name))
// mod.Migrate(true)
fmt.Println(color.GreenString("[Watch] Model: %s Created (Please run yao migrate manually)", name))
break
case "WRITE":
@ -231,13 +231,13 @@ func watchModel(root string, file string, event string, cfg config.Config) {
fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error()))
return
}
mod, err := gou.LoadModelReturn(string(content), name)
_, err = gou.LoadModelReturn(string(content), name)
if err != nil {
fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error()))
return
}
mod.Migrate(false)
fmt.Println(color.GreenString("[Watch] Model: %s Reloaded", name))
// mod.Migrate(false)
fmt.Println(color.GreenString("[Watch] Model: %s Reloaded (Please run yao migrate manually)", name))
break
case "REMOVE", "RENAME":

116
setup/check.go Normal file
View file

@ -0,0 +1,116 @@
package setup
import (
"fmt"
"os"
"path/filepath"
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/yao/config"
)
// Check setup programe
func Check() bool {
root := appRoot()
appfile := filepath.Join(root, "app.json")
if _, err := os.Stat(appfile); err != nil && os.IsNotExist(err) {
return true
}
envfile := filepath.Join(root, ".env")
if _, err := os.Stat(envfile); err != nil && os.IsNotExist(err) {
cfg, err := getConfig()
if err != nil || !hasInstalled(cfg) {
return true
}
}
return false
}
// Validate db link
func Validate() (err error) {
root := appRoot()
path := filepath.Join(root, "db", "yao.db")
data := []byte(fmt.Sprintf(`{
"type": "sqlite3",
"options": {
"file": "%s"
}
}`, path))
data = []byte(`{
"type": "mysql",
"options": {
"db": "test",
"hosts": [{ "host": "127.0.0.1", "user":"root", "pass":"123456" }]
}
}`)
_, err = connector.Load(string(data), "test")
return err
}
func appRoot() string {
root := os.Getenv("YAO_ROOT")
if root == "" {
path, err := os.Getwd()
if err != nil {
printError("无法获取应用目录: %s", err)
Stop()
}
root = path
}
root, err := filepath.Abs(root)
if err != nil {
printError("无法获取应用目录: %s", err)
Stop()
}
return root
}
func getConfig() (config.Config, error) {
root := appRoot()
envfile := filepath.Join(root, ".env")
cfg := config.LoadFrom(envfile)
return cfg, nil
}
func hasInstalled(cfg config.Config) bool {
switch cfg.DB.Driver {
case "sqlite3":
if cfg.DB.Primary != nil && len(cfg.DB.Primary) > 0 {
dbfile, err := filepath.Abs(cfg.DB.Primary[0])
if err != nil {
return false
}
if _, err := os.Stat(dbfile); err != nil && os.IsNotExist(err) {
fmt.Println(dbfile)
return false
}
return true
}
break
case "mysql":
if cfg.DB.Primary != nil && len(cfg.DB.Primary) > 0 {
return true
}
break
}
return false
}

14
setup/check_test.go Normal file
View file

@ -0,0 +1,14 @@
package setup
import (
"testing"
)
func TestValidate(t *testing.T) {
// err := Validate()
// fmt.Println(err)
// if err != nil {
// t.Fatal(err)
// }
}

65
setup/env.go Normal file
View file

@ -0,0 +1,65 @@
package setup
import (
"os"
"path/filepath"
"github.com/joho/godotenv"
)
func envSet(file, name, value string) error {
file, err := filepath.Abs(file)
if err != nil {
dir := filepath.Dir(file)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil && !os.IsExist(err) {
return err
}
}
if _, err := os.Stat(file); err != nil && os.IsNotExist(err) {
err = os.WriteFile(file, []byte{}, 0644)
if err != nil {
return err
}
err = godotenv.Write(map[string]string{"YAO_ENV": "development"}, file)
if err != nil {
return err
}
}
vars, err := godotenv.Read(file)
if err != nil {
return err
}
vars[name] = value
return godotenv.Write(vars, file)
}
func envGet(file, name string) (string, error) {
v := os.Getenv(name)
if v != "" {
return v, nil
}
vars, err := godotenv.Read(file)
if err == nil {
return "", err
}
return vars[name], nil
}
func envHas(file, name string) (bool, error) {
v, err := envGet(file, name)
if err != nil {
return false, nil
}
return v != "", nil
}

115
setup/handler.go Normal file
View file

@ -0,0 +1,115 @@
package setup
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/yaoapp/kun/exception"
"github.com/yaoapp/xun"
)
// recovered custom recovered
func recovered(c *gin.Context, recovered interface{}) {
var code = http.StatusInternalServerError
if err, ok := recovered.(string); ok {
c.JSON(code, xun.R{
"code": code,
"message": fmt.Sprintf("%s", err),
})
} else if err, ok := recovered.(exception.Exception); ok {
code = err.Code
c.JSON(code, xun.R{
"code": code,
"message": err.Message,
})
} else if err, ok := recovered.(*exception.Exception); ok {
code = err.Code
c.JSON(code, xun.R{
"code": code,
"message": err.Message,
})
} else {
c.JSON(code, xun.R{
"code": code,
"message": fmt.Sprintf("%v", recovered),
})
}
c.AbortWithStatus(code)
}
// {
// "env": {
// "YAO_LANG": "中文",
// "YAO_ENV": "开发模式(推荐)",
// "YAO_PORT": "5099",
// "YAO_STUDIO_PORT": "5077"
// },
// "db": {
// "type": "sqlite",
// "option.file": "db/yao.db"
// }
// }
func runSetup(c *gin.Context) {
payload := getPayload(c)
cfg, err := getConfig()
if err != nil {
c.JSON(500, gin.H{"code": 500, "message": err.Error()})
return
}
if hasInstalled(cfg) {
c.JSON(403, gin.H{"code": 400, "message": "应用已安装, 删除 .env 文件后重试"})
return
}
err = Install(payload)
if err != nil {
c.JSON(500, gin.H{"code": 500, "message": err.Error()})
return
}
// Reload Config
cfg, err = getConfig()
if err != nil {
c.JSON(500, gin.H{"code": 500, "message": err.Error()})
return
}
// Return
urls, err := AdminURL(cfg)
if err != nil {
c.JSON(500, gin.H{"code": 500, "message": err.Error()})
return
}
c.JSON(200, gin.H{
"code": 200,
"message": "安装成功",
"urls": urls,
})
Complete()
}
func runCheck(c *gin.Context) {
time.Sleep(2 * time.Second)
c.JSON(200, gin.H{"code": 200})
}
func getPayload(c *gin.Context) map[string]map[string]string {
var payload map[string]map[string]string
err := c.ShouldBindJSON(&payload)
if err != nil {
c.JSON(500, gin.H{"code": 400, "message": err.Error()})
return nil
}
return payload
}

1254
setup/install.go Normal file

File diff suppressed because it is too large Load diff

12
setup/install_test.go Normal file
View file

@ -0,0 +1,12 @@
package setup
import (
"testing"
)
func TestInstall(t *testing.T) {
// err := Install()
// if err != nil {
// t.Fatal(err)
// }
}

230
setup/setup.go Normal file
View file

@ -0,0 +1,230 @@
package setup
import (
"fmt"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"github.com/fatih/color"
"github.com/gin-gonic/gin"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/data"
"github.com/yaoapp/yao/engine"
"github.com/yaoapp/yao/share"
"github.com/yaoapp/yao/widgets/app"
)
// XGenSetupServer XGen Setup
var XGenSetupServer http.Handler = http.FileServer(data.Setup())
// Done check done
var Done = make(chan bool, 1)
// shutdown check done
var shutdown = make(chan bool, 1)
// Canceled check if setup is canceld
var Canceled = make(chan bool, 1)
// Start start the studio api server
func Start() (err error) {
// recive interrupt signal
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
errCh := make(chan error, 1)
// Set router
gin.SetMode(gin.ReleaseMode)
router := gin.New()
if os.Getenv("YAO_SETUP_DEV") != "" {
fmt.Println(color.WhiteString("\nSETUP DEV: %s\n", os.Getenv("YAO_SETUP_DEV")))
}
router.Use(func(c *gin.Context) {
length := len(c.Request.URL.Path)
if length >= 5 && c.Request.URL.Path[0:5] == "/api/" {
c.Next()
return
}
if os.Getenv("YAO_SETUP_DEV") != "" {
root, err := filepath.Abs(os.Getenv("YAO_SETUP_DEV"))
if err != nil {
printError("%s", err)
}
static := http.FileServer(http.Dir(root))
static.ServeHTTP(c.Writer, c.Request)
return
}
// Setup Pages
XGenSetupServer.ServeHTTP(c.Writer, c.Request)
return
}, gin.CustomRecovery(recovered))
router.POST("/api/__yao/app/check", runCheck)
router.POST("/api/__yao/app/setup", runSetup)
// Server setting
addr := ":5099"
// Listen
l, err := net.Listen("tcp4", addr)
if err != nil {
addr = ":0"
l, err = net.Listen("tcp4", addr)
if err != nil {
return err
}
}
srv := &http.Server{
Addr: addr,
Handler: router,
}
defer func() {
// printInfo("[Setup] %s Close Serve", addr)
err = srv.Close()
if err != nil {
printError("[Setup] Error (%v)", err)
}
}()
// start serve
go func() {
// printInfo("[Setup] Starting: %s", addr)
if err := srv.Serve(l); err != nil && err != http.ErrServerClosed {
errCh <- err
}
}()
welcome(l)
select {
case <-shutdown:
printInfo("Setup Shutdown")
return err
case <-interrupt:
printInfo("Setup Interrupt")
Canceled <- true
return err
case err := <-errCh:
printError("Setup Error: ", err)
Canceled <- true
return err
}
}
// Complete stop the studio api server
func Complete() {
Done <- true
}
// Stop stop the studio api server
func Stop() {
shutdown <- true
}
// AdminURL get admin url
func AdminURL(cfg config.Config) ([]string, error) {
err := engine.Load(cfg)
if err != nil {
return nil, err
}
urls, err := URLs(cfg)
adminRoot := "yao"
if app.Setting.AdminRoot != "" {
adminRoot = app.Setting.AdminRoot
}
adminRoot = strings.Trim(adminRoot, "/")
for i := range urls {
urls[i] = fmt.Sprintf("%s/%s/", urls[i], adminRoot)
}
return urls, nil
}
// URLs get admin url
func URLs(cfg config.Config) ([]string, error) {
err := engine.Load(cfg)
if err != nil {
return nil, err
}
ips, err := Ips()
if err != nil {
return nil, err
}
for i := range ips {
ips[i] = fmt.Sprintf("http://%s:%d", ips[i], cfg.Port)
}
return ips, nil
}
func welcome(l net.Listener) {
fmt.Println(color.WhiteString("---------------------------------"))
fmt.Println(color.WhiteString("Yao Application Setup v%s", share.VERSION))
fmt.Println(color.WhiteString("---------------------------------"))
ips, err := Ips()
if err != nil {
printError("Error: ", err.Error())
}
addr := strings.Split(l.Addr().String(), ":")
if len(addr) != 2 {
printError("Error: can't get port")
}
port := addr[1]
for _, ip := range ips {
fmt.Println(color.WhiteString("\nOpen URL in the browser to continue:\n"))
printInfo("http://%s:%s", ip, port)
}
fmt.Println()
}
func printError(message string, args ...interface{}) {
fmt.Println(color.RedString(message, args...))
os.Exit(1)
}
func printInfo(message string, args ...interface{}) {
fmt.Println(color.GreenString(message, args...))
}
// Ips get the local ip list
func Ips() ([]string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return nil, err
}
iplist := []string{}
for _, address := range addrs {
// check the address type and if it is not a loopback the display it
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
iplist = append(iplist, ipnet.IP.String())
}
}
}
return iplist, nil
}

View file

@ -51,6 +51,26 @@ func (c *Computable) ComputeEdit(name string, process *gou.Process, args []inter
return nil
}
if columns, ok := args[0].([]interface{}); ok {
new := []string{}
for _, col := range columns {
new = append(new, fmt.Sprintf("%v", col))
}
args[0] = new
}
if values, ok := args[1].([]interface{}); ok {
new := [][]interface{}{}
for _, value := range values {
arr, ok := value.([]interface{})
if !ok {
return fmt.Errorf("args[1] is not a [][] %s", reflect.ValueOf(args[1]).Type().Name())
}
new = append(new, arr)
}
args[1] = new
}
if _, ok := args[0].([]string); !ok {
return fmt.Errorf("args[0] is not a []string %s", reflect.ValueOf(args[0]).Type().Name())
}

1
xgen/setup/index.html Normal file
View file

@ -0,0 +1 @@
XGEN SETUP