update, gui: Add to PATH on Windows writes a magpie.cmd beside an app not named magpie.exe, so the site's magpie-windows-amd64.exe runs as magpie in a new terminal, and says so (#942, mintonight)

Add to PATH put the portable download's folder first in the user's PATH, but a terminal looks for magpie by that name only, so the command was never found. cliPlace now writes magpie.cmd beside a program not named magpie.exe: it runs %~dp0<the program's name> %*, which an update keeps, since it replaces the program where it is under its name. ReadCLI reads that magpie.cmd as this app (shimRuns), and the GUI says, in Add's tooltip, the row and the status, that magpie.cmd runs magpie-windows-amd64.exe. Going through cmd also makes PowerShell wait for the GUI-linked program and gives it the console. TestCLIShim and cli-path.test.cjs's new case fail without the change.
This commit is contained in:
yetone committed 2026-10-06 02:18:07 +08:00
1 parent a5df344e43
commit 90b1ac7956
7 files changed
+164 -13

No files matched your search

+8 -3
View File
@@ -16326,6 +16326,7 @@ function setSetTab(tab, remember) {
// own button, with the file it names. Asked once a load and again after
// half a minute: each shell is run to read its PATH.
let cliView = null, cliAt = 0, cliAsk = null;
const exeName = (p) => String(p || "").split(/[\\/]/).pop();
function renderCLI(r) {
r.classList.add("cli-row");
const sub = r.querySelector(".sub");
@@ -16341,18 +16342,22 @@ function renderCLI(r) {
cliView = await api("cli", body);
cliAt = Date.now();
paint(cliView);
status(cliView.windows ? t("Added: open a new PowerShell or Command Prompt window to run magpie") : t("Added: open a new terminal window to run magpie"), "ok");
status(cliView.windows && cliView.shim ? t("Added: magpie.cmd beside {name} runs it as magpie; open a new PowerShell or Command Prompt window to run magpie", { name: exeName(cliView.exe) })
: cliView.windows ? t("Added: open a new PowerShell or Command Prompt window to run magpie") : t("Added: open a new terminal window to run magpie"), "ok");
} catch (e) { status(t(e.message), "err"); } finally { btn.disabled = false; }
};
const paint = (v) => {
sub.textContent = v.stuck === "translocated" ? t("macOS runs magpie from a temporary copy until it is moved to Applications: move it there first")
: v.stuck === "read-only" ? t("magpie runs from its disk image: copy it to Applications first")
: v.ours ? t("In a new terminal, magpie runs this app: {path}", { path: v.command })
: v.ours ? t("In a new terminal, magpie runs this app: {path}", { path: v.command }) + (v.shim && v.command === v.shim ? " (" + t("which runs {name}", { name: exeName(v.exe) }) + ")" : "")
: v.command ? t("In a new terminal, magpie runs another copy: {path}", { path: v.command }) + (v.version ? " (" + v.version + ")" : "")
: t("A new terminal can't find the magpie command");
add.hidden = v.ours || !v.dir || !!v.stuck;
add.textContent = v.command ? t("Use this app") : t("Add to PATH");
add.title = v.windows ? t("Puts {dir} first in your user PATH", { dir: v.dir || "" }) : t("Links magpie in {dir}", { dir: v.dir || "" });
// an app not named magpie.exe (the site's magpie-windows-amd64.exe,
// #942) is given a magpie.cmd beside it that runs it
add.title = v.windows && v.shim ? t("Puts {dir} first in your user PATH and writes magpie.cmd there, which runs {name}", { dir: v.dir || "", name: exeName(v.exe) })
: v.windows ? t("Puts {dir} first in your user PATH", { dir: v.dir || "" }) : t("Links magpie in {dir}", { dir: v.dir || "" });
shells.replaceChildren(...(v.shells || []).map((s) => {
const line = el("div", "cli-shell" + (s.ours ? " on" : ""));
line.dataset.shell = s.name;
+9
View File
@@ -2222,6 +2222,9 @@ const I18N = {
"Couldn't download {v} through the mirror {mirror}; it may be down or sent a file that doesn't match the release, which isn't installed": "经镜像 {mirror} 下载 {v} 失败:镜像可能不可用,或发来的文件与发布版本不符(不会安装)",
"Download from GitHub": "改用官方下载",
"Use this app": "改用这个 app",
"Puts {dir} first in your user PATH and writes magpie.cmd there, which runs {name}": "把 {dir} 放到你用户 PATH 的最前面,并在那里写一个 magpie.cmd 来运行 {name}",
"Added: magpie.cmd beside {name} runs it as magpie; open a new PowerShell or Command Prompt window to run magpie": "已添加:{name} 旁边的 magpie.cmd 会以 magpie 这个命令运行它;新开一个 PowerShell 或命令提示符窗口即可运行 magpie",
"which runs {name}": "它运行 {name}",
"Puts {dir} first in your user PATH": "把 {dir} 放到你用户 PATH 的最前面",
"Links magpie in {dir}": "在 {dir} 里链接 magpie",
"Added: open a new PowerShell or Command Prompt window to run magpie": "已添加:新开一个 PowerShell 或命令提示符窗口即可运行 magpie",
@@ -5616,6 +5619,9 @@ const I18N = {
"Couldn't download {v} through the mirror {mirror}; it may be down or sent a file that doesn't match the release, which isn't installed": "ミラー {mirror} 経由で {v} をダウンロードできませんでした。ミラーが停止しているか、リリースと一致しないファイルが送られました(インストールされません)",
"Download from GitHub": "公式からダウンロード",
"Use this app": "このアプリを使う",
"Puts {dir} first in your user PATH and writes magpie.cmd there, which runs {name}": "{dir} をユーザーの PATH の先頭に置き、{name} を実行する magpie.cmd をそこに書きます",
"Added: magpie.cmd beside {name} runs it as magpie; open a new PowerShell or Command Prompt window to run magpie": "追加しました:{name} の隣の magpie.cmd が magpie コマンドとしてそれを実行します。新しい PowerShell かコマンドプロンプトのウィンドウで magpie を実行できます",
"which runs {name}": "{name} を実行します",
"Puts {dir} first in your user PATH": "{dir} をユーザーの PATH の先頭に置きます",
"Links magpie in {dir}": "{dir} に magpie をリンクします",
"Added: open a new PowerShell or Command Prompt window to run magpie": "追加しました:新しい PowerShell かコマンドプロンプトのウィンドウで magpie を実行できます",
@@ -9000,6 +9006,9 @@ const I18N = {
"Couldn't download {v} through the mirror {mirror}; it may be down or sent a file that doesn't match the release, which isn't installed": "{v} konnte über den Spiegel {mirror} nicht geladen werden; er ist evtl. nicht erreichbar oder lieferte eine Datei, die nicht zum Release passt (wird nicht installiert)",
"Download from GitHub": "Von GitHub laden",
"Use this app": "Diese App verwenden",
"Puts {dir} first in your user PATH and writes magpie.cmd there, which runs {name}": "Setzt {dir} an den Anfang Ihres Benutzer-PATH und legt dort eine magpie.cmd ab, die {name} startet",
"Added: magpie.cmd beside {name} runs it as magpie; open a new PowerShell or Command Prompt window to run magpie": "Hinzugefügt: Die magpie.cmd neben {name} startet es als magpie. Öffnen Sie ein neues PowerShell- oder Eingabeaufforderungsfenster, um magpie auszuführen",
"which runs {name}": "startet {name}",
"Puts {dir} first in your user PATH": "Setzt {dir} an den Anfang Ihres Benutzer-PATH",
"Links magpie in {dir}": "Verlinkt magpie in {dir}",
"Added: open a new PowerShell or Command Prompt window to run magpie": "Hinzugefügt: Öffnen Sie ein neues PowerShell- oder Eingabeaufforderungsfenster, um magpie auszuführen",
+3 -1
View File
@@ -1849,7 +1849,9 @@ it says what `magpie` in a new terminal runs, with a dot for each shell (no
left border, no native select); Add to PATH posts `/api/cli` with no shell and
the row turns green, a shell whose PATH lacks the folder has a button naming
its profile that posts that shell alone, and no click scrolls. On Windows it
lists PowerShell and cmd with no profile buttons; a translocated app gets no
lists PowerShell and cmd with no profile buttons, and a program not named
magpie.exe (the site's portable download, #942) is said to get a magpie.cmd
that runs it, in Add's tooltip, the row and the status; a translocated app gets no
button. Every string has zh, ja and de. English and Chinese, Chromium and
WebKit; the API is faked, no shell profile is read or written.
+29 -6
View File
@@ -1,7 +1,9 @@
// Run with Node's test runner and Playwright on the module path; see README.md.
// Settings › About › Command line (PAMI on Discord): whether `magpie` in a
// terminal opened now runs this app, said for each shell (a dot, no left
// border), and Add to PATH, which posts /api/cli and shows the answer. A
// border), and Add to PATH, which posts /api/cli and shows the answer. On
// Windows a program not named magpie.exe (the site's portable download,
// #942) is said to get a magpie.cmd that runs it. A
// shell whose PATH lacks the folder has its own button naming its profile,
// and only that button posts that shell. On Windows PowerShell and cmd are
// listed, with no profile to write; a translocated app gets no button. No
@@ -17,18 +19,24 @@ const assets = path.resolve(__dirname, "../assets");
const words = {
en: { row: "Command line", add: "Add to PATH", none: "can't find the magpie command", ours: "runs this app", fish: "Add to ~/.config/fish/config.fish",
added: "open a new terminal window", ps: "PowerShell and cmd take up a change in new windows", moved: "moved to Applications" },
added: "open a new terminal window", ps: "PowerShell and cmd take up a change in new windows", moved: "moved to Applications",
shimTitle: "writes magpie.cmd there, which runs magpie-windows-amd64.exe", shimAdded: "magpie.cmd beside magpie-windows-amd64.exe runs it as magpie", shimRuns: "which runs magpie-windows-amd64.exe" },
zh: { row: "命令行", add: "添加到 PATH", none: "新终端里找不到 magpie 命令", ours: "运行这个 app", fish: "添加到 ~/.config/fish/config.fish",
added: "新开一个终端窗口", ps: "PowerShell 和 cmd 在新窗口里生效", moved: "移到「应用程序」" },
added: "新开一个终端窗口", ps: "PowerShell 和 cmd 在新窗口里生效", moved: "移到「应用程序」",
shimTitle: "写一个 magpie.cmd 来运行 magpie-windows-amd64.exe", shimAdded: "magpie-windows-amd64.exe 旁边的 magpie.cmd", shimRuns: "它运行 magpie-windows-amd64.exe" },
};
const EXE = "/Applications/magpie.app/Contents/MacOS/magpie";
function view(ctl) {
if (ctl.windows) {
const exe = "C:\\Users\\u\\AppData\\Local\\magpie\\magpie.exe";
return { exe, ours: ctl.added, command: ctl.added ? exe : "", dir: "C:\\Users\\u\\AppData\\Local\\magpie", windows: true,
shells: ["PowerShell", "cmd"].map((name, i) => ({ name, default: i === 0, known: true, ours: ctl.added, command: ctl.added ? exe : "" })) };
// the site's portable download (#942) runs as magpie through the
// magpie.cmd Add writes beside it
const dir = ctl.portable ? "D:\\portable_app" : "C:\\Users\\u\\AppData\\Local\\magpie";
const exe = dir + (ctl.portable ? "\\magpie-windows-amd64.exe" : "\\magpie.exe"), shim = ctl.portable ? dir + "\\magpie.cmd" : "";
const cmd = ctl.added ? shim || exe : "";
return { exe, ours: ctl.added, command: cmd, dir, windows: true, shim,
shells: ["PowerShell", "cmd"].map((name, i) => ({ name, default: i === 0, known: true, ours: ctl.added, command: cmd })) };
}
const link = "/Users/u/.local/bin/magpie";
return {
@@ -154,6 +162,21 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await page.context().close();
});
await t.test(lang + ": Windows' portable download gets a magpie.cmd, and says so (#942)", async () => {
const ctl = { posts: [], gets: 0, windows: true, portable: true };
const errors = [];
const { page, row } = await open(browser, lang, ctl, errors);
const add = row.locator("button.cli-add");
assert.match(await add.getAttribute("title"), new RegExp(w.shimTitle));
await add.click();
await row.locator(".sub", { hasText: "D:\\portable_app\\magpie.cmd" }).waitFor();
assert.match(await row.locator(".sub").innerText(), new RegExp(w.shimRuns));
assert.match(await page.locator("#status").innerText(), new RegExp(w.shimAdded));
assert.deepEqual(ctl.posts, [{}]);
assert.deepEqual(errors, []);
await page.context().close();
});
await t.test(lang + ": a translocated app gets no button", async () => {
const ctl = { posts: [], gets: 0, stuck: "translocated" };
const errors = [];
+51 -1
View File
@@ -50,6 +50,7 @@ type CLI struct {
Stuck string `json:"stuck,omitempty"` // translocated, read-only: why it can't be linked from where it is
Shells []CLIShell `json:"shells"`
Windows bool `json:"windows,omitempty"`
Shim string `json:"shim,omitempty"` // Windows: the magpie.cmd Add writes beside an app not named magpie.exe, which runs it
}
// cliShell is a shell installed here.
@@ -76,6 +77,9 @@ func ReadCLI() *CLI {
return v
}
v.Exe, v.Stuck = exe, cliStuck()
if v.Windows && needsShim(exe) {
v.Shim = filepath.Join(filepath.Dir(exe), shimName)
}
shells := cliShells()
paths := make([][]string, len(shells))
var wg sync.WaitGroup
@@ -90,7 +94,7 @@ func ReadCLI() *CLI {
var def []string
for i, sh := range shells {
s := CLIShell{Name: sh.Name, Default: sh.Default, Known: paths[i] != nil, Command: findCLI(paths[i])}
s.Ours = s.Command != "" && sameFile(s.Command, exe)
s.Ours = s.Command != "" && (sameFile(s.Command, exe) || shimRuns(s.Command, exe))
if p := cliProfile(sh.Name); p != "" {
s.Profile = tildeHome(p)
}
@@ -335,6 +339,52 @@ func tildeHome(p string) string {
return p
}
// A Windows app not named magpie.exe — the site's download is
// magpie-windows-amd64.exe (#942) — is given a magpie.cmd beside it that
// runs it, since a terminal looks for `magpie` by that name only. It names
// the program by its folder (%~dp0) and file name, which an update keeps:
// it replaces the program where it is, under its name.
// shimName is the file a shim is written to.
const shimName = "magpie.cmd"
// shimText is the magpie.cmd that runs the program exe in its folder.
func shimText(exe string) string {
name := filepath.Base(strings.ReplaceAll(exe, `\`, "/"))
return "@echo off\r\nrem added by magpie (Settings > Command line): the magpie command runs " + name + " in this folder\r\n\"%~dp0" + name + "\" %*\r\n"
}
// needsShim says whether the program exe can't be run as `magpie` by its
// own name.
func needsShim(exe string) bool {
return !strings.EqualFold(filepath.Base(strings.ReplaceAll(exe, `\`, "/")), "magpie.exe")
}
// writeShim writes the magpie.cmd for exe beside it, when its name needs
// one; it is the file's path, "" when none was needed.
func writeShim(exe string) (string, error) {
if !needsShim(exe) {
return "", nil
}
p := filepath.Join(filepath.Dir(exe), shimName)
if b, err := os.ReadFile(p); err == nil && string(b) == shimText(exe) {
return p, nil
}
if err := os.WriteFile(p, []byte(shimText(exe)), 0o755); err != nil {
return "", fmt.Errorf("writing %s, which runs %s as magpie: %w", p, filepath.Base(exe), err)
}
return p, nil
}
// shimRuns says whether cmd is the magpie.cmd magpie wrote for exe.
func shimRuns(cmd, exe string) bool {
if !strings.EqualFold(filepath.Base(cmd), shimName) || !strings.EqualFold(filepath.Dir(cmd), filepath.Dir(exe)) {
return false
}
b, err := os.ReadFile(cmd)
return err == nil && string(b) == shimText(exe)
}
// pathFirst is the PATH value v (Windows', ;-separated) with dir first and
// nowhere else.
func pathFirst(v, dir string) string {
+6 -2
View File
@@ -22,8 +22,12 @@ func installedShells() []cliShell {
func cliDir(exe string, _ []string, _ string) string { return filepath.Dir(exe) }
// cliPlace puts dir first in the user's PATH in the registry, where a
// terminal opened from now on takes it up, and tells Explorer.
func cliPlace(_, dir string) error {
// terminal opened from now on takes it up, and tells Explorer; an app not
// named magpie.exe gets its magpie.cmd there first (writeShim).
func cliPlace(exe, dir string) error {
if _, err := writeShim(exe); err != nil {
return err
}
k, err := registry.OpenKey(registry.CURRENT_USER, `Environment`, registry.QUERY_VALUE|registry.SET_VALUE)
if err != nil {
return err
+58
View File
@@ -0,0 +1,58 @@
package update
import (
"os"
"path/filepath"
"strings"
"testing"
)
// #942: the site's Windows download is magpie-windows-amd64.exe, and Add
// to PATH put its folder on PATH, where a terminal finds no `magpie`. Add
// writes a magpie.cmd beside a program not named magpie.exe that runs it by
// its folder and name, which an update keeps, and the Command line reads
// that magpie.cmd as this app; one written for another program isn't.
func TestCLIShim(t *testing.T) {
dir := t.TempDir()
exe := filepath.Join(dir, "magpie-windows-amd64.exe")
if err := os.WriteFile(exe, []byte("MZ"), 0o755); err != nil {
t.Fatal(err)
}
p, err := writeShim(exe)
if err != nil {
t.Fatal(err)
}
if p != filepath.Join(dir, "magpie.cmd") {
t.Fatalf("shim at %q", p)
}
b, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(b), "\"%~dp0magpie-windows-amd64.exe\" %*\r\n") || !strings.HasPrefix(string(b), "@echo off\r\n") {
t.Fatalf("magpie.cmd:\n%q", b)
}
if !shimRuns(p, exe) {
t.Fatal("the magpie.cmd written isn't read as running this app")
}
if shimRuns(p, filepath.Join(dir, "magpie-old.exe")) {
t.Fatal("magpie.cmd read as running another program")
}
// renamed, as a second download is: written again for the new name
exe2 := filepath.Join(dir, "magpie-windows-amd64 (1).exe")
if _, err := writeShim(exe2); err != nil {
t.Fatal(err)
}
if !shimRuns(p, exe2) || shimRuns(p, exe) {
t.Fatal("magpie.cmd not written again for the renamed program")
}
// magpie.exe is found by its own name: nothing written
dir2 := t.TempDir()
if p, err := writeShim(filepath.Join(dir2, "Magpie.exe")); err != nil || p != "" {
t.Fatalf("magpie.exe given a shim: %q, %v", p, err)
}
if _, err := os.Stat(filepath.Join(dir2, "magpie.cmd")); !os.IsNotExist(err) {
t.Fatal("magpie.cmd written beside magpie.exe")
}
}