fix(electron): install CLI launcher in local bin (#12664)

* fix(electron): install CLI launcher in local bin

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Tienson Qin
2026-05-18 10:28:29 +08:00
committed by GitHub
parent e3e548f0f0
commit e9d66de128
5 changed files with 221 additions and 73 deletions

View File

@@ -0,0 +1,83 @@
(ns electron.cli-install
(:require [clojure.string :as string]))
(def CLI_LAUNCHER_MARKER "logseq-cli-managed")
(defn split-path-env
[path-env windows?]
(let [separator (if windows? ";" ":")]
(->> (string/split (or path-env "") (re-pattern separator))
(remove string/blank?)
distinct)))
(defn preferred-unix-cli-dir
[{:keys [home-dir path-join ensure-dir! writable-dir?]}]
(let [user-bin (path-join home-dir ".local" "bin")]
(ensure-dir! user-bin)
(when (writable-dir? user-bin)
user-bin)))
(defn- render-unix-cli-launcher
[exe-path cli-path]
(str "#!/usr/bin/env sh\n"
"# " CLI_LAUNCHER_MARKER "\n"
"set -eu\n"
"ELECTRON_RUN_AS_NODE=1 exec \"" exe-path "\" \"" cli-path "\" \"$@\"\n"))
(defn- render-win-cli-launcher
[exe-path cli-path]
(str "@echo off\r\n"
"REM " CLI_LAUNCHER_MARKER "\r\n"
"set ELECTRON_RUN_AS_NODE=1\r\n"
"\"" exe-path "\" \"" cli-path "\" %*\r\n"))
(defn- write-cli-launcher!
[{:keys [target-path content windows? exists? read-file! write-file! chmod!]}]
(let [should-write? (if (exists? target-path)
(let [existing (read-file! target-path)]
(and (string/includes? existing CLI_LAUNCHER_MARKER)
(not= existing content)))
true)]
(when should-write?
(write-file! target-path content)
(when-not windows?
(chmod! target-path "755"))
true)))
(defn- error-message
[error]
(or (some-> error .-message)
(str error)))
(defn install-cli-launcher!
[{:keys [windows? cli-path cli-dir cli-dir! exe-path path-join exists? show-message-box!
show-error-box! t log-info! log-warn!]
:as deps}]
(try
(let [cli-dir (if cli-dir! (cli-dir!) cli-dir)]
(cond
(not (exists? cli-path))
(throw (js/Error. (str "Missing CLI script at " cli-path)))
(nil? cli-dir)
(throw (js/Error.
(if windows?
"No CLI install directory found. The configured install directory could not be selected or is not writable."
"No CLI install directory found. Expected to install the launcher in ~/.local/bin, but that directory could not be created or is not writable.")))
:else
(let [target-path (path-join cli-dir (if windows? "logseq.cmd" "logseq"))
content (if windows?
(render-win-cli-launcher exe-path cli-path)
(render-unix-cli-launcher exe-path cli-path))
display-dir (if windows? cli-dir "~/.local/bin")]
(when (write-cli-launcher! (assoc deps
:target-path target-path
:content content))
(log-info! :cli/install (str "Installed launcher at " target-path))
(show-message-box! {:title "Logseq"
:message (t :electron/cli-installed display-dir)})))))
(catch :default error
(let [message (error-message error)]
(log-warn! :cli/install "Failed to install logseq launcher" error)
(show-error-box! "Logseq" (t :electron/cli-install-failed message))))))

View File

@@ -7,6 +7,7 @@
["path" :as node-path]
[cljs-bean.core :as bean]
[clojure.string :as string]
[electron.cli-install :as cli-install]
[electron.db :as db]
[electron.exceptions :as exceptions]
[electron.handler :as handler]
@@ -35,7 +36,6 @@
(defonce *setup-fn (volatile! nil))
(defonce *teardown-fn (volatile! nil))
(defonce *quit-dirty? (volatile! true))
(defonce CLI_LAUNCHER_MARKER "logseq-cli-managed")
(defn setup-updater! [^js win]
;; manual/auto updater
@@ -276,16 +276,6 @@
(fn [error]
(logger/warn :electron/wrong-release-warning-failed error))))))
(defn- path-separator
[]
(if utils/win32? ";" ":"))
(defn- split-path-env
[path-env]
(->> (string/split (or path-env "") (re-pattern (path-separator)))
(remove string/blank?)
distinct))
(defn- writable-dir?
[dir]
(try
@@ -297,29 +287,27 @@
(catch :default _
false)))
(defn- find-first-writable-dir
[dirs]
(some #(when (writable-dir? %) %) dirs))
(defn- ensure-dir!
[dir]
(when (and (string? dir) (not (fs/existsSync dir)))
(fs/mkdirSync dir #js {:recursive true})))
(defn- path-join
[& paths]
(apply node-path/join paths))
(defn- preferred-unix-cli-dir
[]
(let [path-dirs (split-path-env (.-PATH js/process.env))
user-bin (node-path/join (.homedir os) ".local" "bin")]
(or (find-first-writable-dir path-dirs)
(do
(ensure-dir! user-bin)
(when (writable-dir? user-bin)
user-bin)))))
(cli-install/preferred-unix-cli-dir
{:home-dir (.homedir os)
:path-join path-join
:ensure-dir! ensure-dir!
:writable-dir? writable-dir?}))
(defn- preferred-win-cli-dir
[]
(let [path-env (or (.-PATH js/process.env) (.-Path js/process.env))
path-dirs (split-path-env path-env)
path-dirs (cli-install/split-path-env path-env utils/win32?)
local-appdata (.-LOCALAPPDATA js/process.env)
windows-apps-dir (when local-appdata
(node-path/join local-appdata "Microsoft" "WindowsApps"))]
@@ -327,7 +315,7 @@
(ensure-dir! windows-apps-dir)
(when (writable-dir? windows-apps-dir)
windows-apps-dir))
(find-first-writable-dir path-dirs))))
(some #(when (writable-dir? %) %) path-dirs))))
(defn- cli-script-path
[]
@@ -335,59 +323,27 @@
(node-path/join js/process.resourcesPath "app.asar" "js" "logseq-cli.js")
(node-path/join js/__dirname "logseq-cli.js")))
(defn- render-unix-cli-launcher
[exe-path cli-path]
(str "#!/usr/bin/env sh\n"
"# " CLI_LAUNCHER_MARKER "\n"
"set -eu\n"
"ELECTRON_RUN_AS_NODE=1 exec \"" exe-path "\" \"" cli-path "\" \"$@\"\n"))
(defn- render-win-cli-launcher
[exe-path cli-path]
(str "@echo off\r\n"
"REM " CLI_LAUNCHER_MARKER "\r\n"
"set ELECTRON_RUN_AS_NODE=1\r\n"
"\"" exe-path "\" \"" cli-path "\" %*\r\n"))
(defn- write-cli-launcher!
[path content windows?]
(let [should-write? (if (fs/existsSync path)
(let [existing (.readFileSync fs path "utf8")]
(and (string/includes? existing CLI_LAUNCHER_MARKER)
(not= existing content)))
true)]
(when should-write?
(.writeFileSync fs path content "utf8")
(when-not windows?
(fs/chmodSync path "755"))
true)))
(defn- install-cli-launcher!
[]
(try
(let [cli-path (cli-script-path)
cli-dir (if utils/win32?
(let [cli-path (cli-script-path)
cli-dir! #(if utils/win32?
(preferred-win-cli-dir)
(preferred-unix-cli-dir))]
(cond
(not (fs/existsSync cli-path))
(logger/warn :cli/install (str "Missing CLI script at " cli-path ", skip installing launcher"))
(nil? cli-dir)
(logger/warn :cli/install "No writable PATH directory found; skip installing logseq launcher")
:else
(let [target-path (if utils/win32?
(node-path/join cli-dir "logseq.cmd")
(node-path/join cli-dir "logseq"))
exe-path (.getPath app "exe")
content (if utils/win32?
(render-win-cli-launcher exe-path cli-path)
(render-unix-cli-launcher exe-path cli-path))]
(when (write-cli-launcher! target-path content utils/win32?)
(logger/info :cli/install (str "Installed launcher at " target-path))))))
(catch :default e
(logger/warn :cli/install "Failed to install logseq launcher" e))))
(cli-install/install-cli-launcher!
{:windows? utils/win32?
:cli-path cli-path
:cli-dir! cli-dir!
:exe-path (.getPath app "exe")
:path-join path-join
:exists? #(fs/existsSync %)
:read-file! #(.readFileSync fs % "utf8")
:write-file! #(.writeFileSync fs %1 %2 "utf8")
:chmod! #(fs/chmodSync %1 %2)
:show-message-box! #(.showMessageBox dialog (clj->js %))
:show-error-box! #(.showErrorBox dialog %1 %2)
:t t
:log-info! logger/info
:log-warn! logger/warn})))
(defn- on-app-ready!
[^js app']

View File

@@ -529,6 +529,8 @@
:electron/add-to-dictionary "Add to dictionary"
:electron/block-not-exist "Open link failed. Block-id `{1}` doesn't exist in the graph."
:electron/cancel "Cancel"
:electron/cli-install-failed "Failed to install Logseq CLI.\n{1}"
:electron/cli-installed "Logseq CLI was installed to {1}"
:electron/copy-image "Copy Image"
:electron/link-open-confirm "Are you sure you want to open this link? \n{1}"
:electron/link-open-failed-missing-graph "Failed to open link. Missing graph identifier after `logseq://graph/`."

View File

@@ -526,6 +526,8 @@
:electron/add-to-dictionary "添加到字典"
:electron/block-not-exist "打开链接失败。块 ID `{1}` 在当前图谱中不存在。"
:electron/cancel "取消"
:electron/cli-install-failed "安装 Logseq CLI 失败。\n{1}"
:electron/cli-installed "Logseq CLI 已安装到 {1}"
:electron/copy-image "复制图片"
:electron/link-open-confirm "确定要打开此链接吗?\n{1}"
:electron/link-open-failed-missing-graph "打开链接失败。在 `logseq://graph/` 后缺少图谱标识符。"

View File

@@ -0,0 +1,105 @@
(ns electron.cli-install-test
(:require [cljs.test :refer [deftest is testing]]
[clojure.string :as string]
[electron.cli-install :as cli-install]))
(defn- path-join
[& parts]
(string/join "/" parts))
(defn- t
[k & args]
(case k
:electron/cli-installed (str "Logseq CLI was installed to " (first args))
:electron/cli-install-failed (str "Failed to install Logseq CLI.\n" (first args))))
(deftest preferred-unix-cli-dir-prefers-local-bin
(testing "macOS/Linux CLI launcher directory is ~/.local/bin, not the first writable PATH directory"
(let [created (atom [])]
(is (= "/home/me/.local/bin"
(cli-install/preferred-unix-cli-dir
{:home-dir "/home/me"
:path-join path-join
:ensure-dir! #(swap! created conj %)
:writable-dir? (fn [dir]
(#{"first-writable-path-dir" "/home/me/.local/bin"} dir))})))
(is (= ["/home/me/.local/bin"] @created)))))
(defn- run-install!
[opts]
(let [writes (atom [])
chmods (atom [])
messages (atom [])
errors (atom [])
files (atom (set (:existing-files opts)))
contents (atom (:existing-contents opts))]
(cli-install/install-cli-launcher!
(merge
{:windows? false
:cli-path "/app/logseq-cli.js"
:cli-dir "/home/me/.local/bin"
:exe-path "/Applications/Logseq.app/Contents/MacOS/Logseq"
:path-join path-join
:exists? #(contains? @files %)
:read-file! #(get @contents %)
:write-file! (fn [path content]
(swap! writes conj [path content])
(swap! files conj path))
:chmod! #(swap! chmods conj [%1 %2])
:show-message-box! #(swap! messages conj %)
:show-error-box! (fn [title content]
(swap! errors conj {:title title :content content}))
:t t
:log-info! (fn [& _])
:log-warn! (fn [& _])}
opts))
{:writes @writes
:chmods @chmods
:messages @messages
:errors @errors}))
(deftest install-cli-launcher-shows-success-dialog
(testing "successful Unix install writes to ~/.local/bin and reports the user-facing directory"
(let [result (run-install! {:existing-files #{"/app/logseq-cli.js"}})]
(is (= "/home/me/.local/bin/logseq" (ffirst (:writes result))))
(is (= [["/home/me/.local/bin/logseq" "755"]] (:chmods result)))
(is (= [] (:errors result)))
(is (= [{:title "Logseq"
:message "Logseq CLI was installed to ~/.local/bin"}]
(:messages result))))))
(deftest install-cli-launcher-keeps-windows-path
(testing "Windows keeps the existing Windows install path behavior and reports that directory"
(let [windows-dir "C:/Users/me/AppData/Local/Microsoft/WindowsApps"
result (run-install! {:windows? true
:cli-dir windows-dir
:existing-files #{"/app/logseq-cli.js"}})]
(is (= (str windows-dir "/logseq.cmd") (ffirst (:writes result))))
(is (= [] (:chmods result)))
(is (= [] (:errors result)))
(is (= [{:title "Logseq"
:message (str "Logseq CLI was installed to " windows-dir)}]
(:messages result))))))
(deftest install-cli-launcher-shows-error-dialog-on-failure
(testing "installer failures are visible through an Electron error dialog"
(let [result (run-install! {:existing-files #{"/app/logseq-cli.js"}
:write-file! (fn [& _]
(throw (js/Error. "disk full")))})]
(is (= [] (:messages result)))
(is (= "Logseq" (:title (first (:errors result)))))
(is (string/includes? (:content (first (:errors result)))
"Failed to install Logseq CLI"))
(is (string/includes? (:content (first (:errors result)))
"disk full")))))
(deftest install-cli-launcher-shows-error-dialog-when-directory-selection-fails
(testing "directory selection failures are visible through an Electron error dialog"
(let [result (run-install! {:existing-files #{"/app/logseq-cli.js"}
:cli-dir nil
:cli-dir! (fn []
(throw (js/Error. "permission denied")))})]
(is (= [] (:messages result)))
(is (= "Logseq" (:title (first (:errors result)))))
(is (string/includes? (:content (first (:errors result)))
"permission denied")))))