Files
siyuan/kernel/cli/cmd/root.go
T
2026-06-24 08:40:42 +08:00

159 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package cmd
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/util"
"github.com/spf13/cobra"
)
var (
workspacePath string
outputFormat string
dryRun bool
)
var rootCmd = &cobra.Command{
Use: "SiYuan-Kernel",
Version: util.Ver,
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
// CLI 单次命令没有后台 cron 周期性 flush SQL 队列(server 模式才有 job.StartCron),进程在 main 返回后
// 即退出,内存里的 SQL 索引队列会随进程丢失(操作虽已落 index.queue,但要等下次启动 recoverIndexQueue
// 才恢复)。这里在命令执行完后统一落库,保证写完即可搜索。
name := cmd.Name()
// serve 子命令有自己的长驻退出流程(HandleSignal → model.Close 会 flush),不在此处理;
// workspace 子命令在 PersistentPreRunE 中跳过了数据库初始化,此时 sql 包未就绪,调用会 panic。
if name == "serve" || (cmd.Parent() != nil && cmd.Parent().Name() == "workspace") {
return nil
}
model.FlushTxQueue()
sql.FlushQueue()
return nil
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// workspace 子命令不需要工作空间校验
if cmd.Parent() != nil && cmd.Parent().Name() == "workspace" {
return nil
}
// 默认工作目录取内核可执行文件所在目录的上一级(打包后的 resources/appearance/、stage/ 所在目录),
// 而非内核可执行文件所在目录本身(resources/kernel/)。resolveWorkingDir() 会校验 appearance/langs 实际存在,
// 兼容开发态等多种目录布局。
if workingDir := resolveWorkingDir(); workingDir != "" {
util.WorkingDir = workingDir
}
langsDir := filepath.Join(util.WorkingDir, "appearance", "langs")
if _, err := os.Stat(langsDir); os.IsNotExist(err) {
return fmt.Errorf("appearance files not found at [%s]", langsDir)
}
// 设置工作空间路径
if workspacePath == "" {
workspacePath = os.Getenv("SIYUAN_WORKSPACE_PATH")
}
if workspacePath == "" {
workspacePath = filepath.Join(util.HomeDir, "SiYuan")
}
if _, err := os.Stat(workspacePath); os.IsNotExist(err) {
return fmt.Errorf("directory not found: %s", workspacePath)
}
if !util.IsWorkspaceDir(workspacePath) {
return fmt.Errorf("not a valid workspace: %s", workspacePath)
}
util.Mode = "prod"
util.InitWorkspace(workspacePath, util.WorkingDir)
logging.SetLogPath(filepath.Join(util.TempDir, "siyuan-cli.log"))
logging.SetLogToStdout(false)
model.InitConf()
sql.InitDatabase(false)
sql.InitHistoryDatabase(false)
sql.InitAssetContentDatabase(false)
sql.SetCaseSensitive(model.Conf.Search.CaseSensitive)
sql.SetIndexAssetPath(model.Conf.Search.IndexAssetPath)
return nil
},
}
// resolveWorkingDir 从内核可执行文件路径出发,探测若干候选目录,返回首个包含 appearance/langs 的目录作为
// 工作目录(打包后为 resources/,开发态视目录布局而定);找不到返回空串。rootCmd.PersistentPreRunE 与
// serve 子命令的 --wd 默认值都走这个函数,确保两条启动路径行为一致。
func resolveWorkingDir() string {
if exePath, err := os.Executable(); err == nil {
if resolved, err2 := filepath.EvalSymlinks(exePath); err2 == nil {
exePath = resolved
}
exeDir := filepath.Dir(exePath)
candidates := []string{
filepath.Join(exeDir, ".."), // resources/kernel/ → resources/ (production)
filepath.Join(exeDir, "..", "app"), // kernel/cli/ → kernel/ → app/
filepath.Join(exeDir, "app"), // kernel/ → app/
filepath.Join(exeDir, "..", "..", "app"), // kernel/cli/cmd/... → .../app/
}
// 添加 macOS app bundle 路径
if runtime.GOOS == "darwin" {
candidates = append(candidates,
filepath.Join(exeDir, "..", "..", "..", "..", "Resources"),
)
}
for _, d := range candidates {
langsDir := filepath.Join(d, "appearance", "langs")
if fi, err := os.Stat(langsDir); err == nil && fi.IsDir() {
return d
}
}
}
return ""
}
func init() {
rootCmd.Use = strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
rootCmd.Short = "SiYuan Kernel v" + util.Ver
rootCmd.Long = "SiYuan Kernel v" + util.Ver + ". Manage workspace data directly or start the HTTP server."
rootCmd.PersistentFlags().StringVarP(&workspacePath, "workspace", "w", "", "workspace path")
rootCmd.PersistentFlags().StringVarP(&outputFormat, "format", "f", "table", "output format: table | json")
rootCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "dry run mode: validate and print what would happen without making changes")
}
func Execute() error {
return rootCmd.Execute()
}
func HasSubCommand(name string) bool {
for _, c := range rootCmd.Commands() {
if c.Name() == name {
return true
}
}
return false
}