From dd21e325b515cbf67fd4a461ae5b3e1b195aa53d Mon Sep 17 00:00:00 2001 From: haranishi <223786623+haranishi@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:33:05 +0900 Subject: [PATCH] fix(appimage): expose the bundled CLI and MCP entry point --- .github/workflows/packaging-lint.yml | 8 +++ README.md | 2 + docs/development.md | 11 ++++ docs/releasing.md | 6 +++ packaging/linux/AppRun | 11 ++++ packaging/linux/apprun-cli-test.sh | 79 ++++++++++++++++++++++++++++ 6 files changed, 117 insertions(+) create mode 100755 packaging/linux/apprun-cli-test.sh diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml index 1d110e28f2..0a84295f00 100644 --- a/.github/workflows/packaging-lint.yml +++ b/.github/workflows/packaging-lint.yml @@ -29,3 +29,11 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - run: bash packaging/linux/verified-download-test.sh + + apprun-cli: + name: AppImage bundled CLI and MCP entry point + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Verify bundled CLI and MCP entry point + run: bash packaging/linux/apprun-cli-test.sh diff --git a/README.md b/README.md index 12e2ef4b12..aa7ad6edc1 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,8 @@ The Flatpak prints through the host's CUPS socket (File › Print talks IPP to i The AppImage needs no install: the first run registers its launcher icon and menu entry in `~/.local/share` so the dock shows PhotoCraft's icon on Wayland. Set `PHOTOCRAFT_NO_DESKTOP_INTEGRATION=1` to skip that, and see [`docs/releasing.md`](docs/releasing.md) › Linux to undo it. +To use the bundled CLI, run `./photocraft--linux-x86_64.AppImage --cli --help`, or `--cli mcp` to start the MCP server on stdio. CLI launches skip desktop integration. A symlink named `photocraft-cli` also selects the CLI; see [`docs/development.md`](docs/development.md#headless-cli). + On a Wayland session, files dropped on the window don't open yet: winit 0.30, the windowing library under egui, has no Wayland drag and drop (#386). Use File › Open, or copy the image in your file manager and paste it with Ctrl+V. To drag and drop, start PhotoCraft under XWayland: `WAYLAND_DISPLAY= photocraft`, `WAYLAND_DISPLAY= ./photocraft--linux-x86_64.AppImage`, or `flatpak run --nosocket=wayland --socket=x11 ai.storyteller.photocraft`. On Wayland, PhotoCraft shows the command for your install in a notice until you dismiss it. Preferences › Performance › Linux display server = X11 does the same on every launch. With a fractional display scale (125%, 150%, 175%) an XWayland window is drawn at a lower resolution and enlarged by the compositor, so the interface is blurred there; native Wayland stays sharp (#1072). On macOS, the command-line tool comes as `photocraft-cli--macos-universal.zip`. The binary is signed with the same Developer ID as the app and notarized by Apple. A bare binary can't carry a stapled notarization ticket the way the DMG does, so the first time you run it macOS checks the notarization online. You can confirm it yourself: diff --git a/docs/development.md b/docs/development.md index 6891e88a9c..1314b6f32c 100644 --- a/docs/development.md +++ b/docs/development.md @@ -189,6 +189,17 @@ cargo run -p photocraft-cli -- batch --actions actions.json --in photos/ --out d cargo run -p photocraft-cli -- commands --filter blur # the command registry ``` +The Linux AppImage includes the same CLI. Select it with a leading `--cli`: + +```sh +./photocraft--linux-x86_64.AppImage --cli --help +./photocraft--linux-x86_64.AppImage --cli mcp +ln -s photocraft--linux-x86_64.AppImage photocraft-cli +./photocraft-cli commands --filter blur +``` + +The `--cli` selector is removed; all remaining arguments, stdin, stdout, stderr and the exit status are passed through. CLI launches skip desktop integration. The symlink form uses the AppImage runtime's `ARGV0`; the normal AppImage name launches the GUI. + `actions.json` holds the steps of a recorded action: `[["", {…}], …]`, `[{"command": "", "params": {…}}, …]` or bare ids, as a list or wrapped in `{"actions": …}` or `{"steps": …}`; a droplet file works too. It can also be an Actions set (the Actions panel's `actions.json`, `{"actions": [{"name": …, "steps": […]}, …]}`): `--action ` picks the action to play (optional when the set holds one), and the whole set is loaded for each file, so a step that plays another action of the set works. `batch` refuses an `--out` folder that is the `--in` folder, since the results would replace the originals, unless `--in-place` is given. `run` prints one JSON line per command result. ## Native format (.pcraft) diff --git a/docs/releasing.md b/docs/releasing.md index def1911aa8..15c0230c87 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -204,6 +204,12 @@ Why these formats: ~/.local/share/applications/ai.storyteller.photocraft.desktop` and `find ~/.local/share/icons/hicolor -name 'ai.storyteller.photocraft*' -delete`. The script is covered by `packaging/linux/apprun-test.sh` in the packaging-lint workflow. + A leading `--cli` launches the bundled `usr/bin/photocraft-cli`, without desktop + integration: `./photocraft--linux-x86_64.AppImage --cli mcp` serves MCP on + stdio. A symlink named `photocraft-cli` also selects that binary via the runtime's + `ARGV0`. Arguments and process streams pass through unchanged; the entry point + uses `exec` so signals and exit status reach the caller directly. CLI routing is + covered by `packaging/linux/apprun-cli-test.sh` in the same workflow. - **.deb** covers Debian, Ubuntu, Mint, Pop!_OS and elementary. **.rpm** covers Fedora, RHEL and its clones, and openSUSE. Both integrate with the menu, MIME and icon caches (the `postinst.sh` hook) and uninstall cleanly. Both are built by diff --git a/packaging/linux/AppRun b/packaging/linux/AppRun index b0c58d9d31..94f7346218 100755 --- a/packaging/linux/AppRun +++ b/packaging/linux/AppRun @@ -14,6 +14,17 @@ set -u APPDIR="$(dirname "$(readlink -f "$0")")" DESK_ID=ai.storyteller.photocraft +# Select the bundled headless binary before desktop integration. In particular, +# an MCP launch must leave stdio untouched and must not install a GUI launcher. +if [ "${1:-}" = --cli ]; then + shift + exec "$APPDIR/usr/bin/photocraft-cli" "$@" +fi +# Type 2 runtimes preserve a symlink's original name in ARGV0. +case "${ARGV0:-}" in + photocraft-cli | */photocraft-cli) exec "$APPDIR/usr/bin/photocraft-cli" "$@" ;; +esac + integrate() { case "${PHOTOCRAFT_NO_DESKTOP_INTEGRATION:-}" in 1 | true | yes) return 0 ;; diff --git a/packaging/linux/apprun-cli-test.sh b/packaging/linux/apprun-cli-test.sh new file mode 100755 index 0000000000..f6a7ed8af8 --- /dev/null +++ b/packaging/linux/apprun-cli-test.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# AppImage CLI selection, argument boundaries, stdio and exit status (#3385, #3176). +# No build or AppImage mount required; the bundled binaries are stand-ins. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +test_dir="$(mktemp -d)" +trap 'rm -rf "$test_dir"' EXIT +app_dir="$test_dir/PhotoCraft test.AppDir" +mkdir -p "$app_dir/usr/bin" "$app_dir/usr/share/applications" +cp "$HERE/AppRun" "$app_dir/AppRun" +printf '[Desktop Entry]\nType=Application\nName=PhotoCraft\nExec=photocraft %%F\n' \ + >"$app_dir/usr/share/applications/ai.storyteller.photocraft.desktop" +cat >"$app_dir/usr/bin/photocraft" <<'EOF' +#!/bin/sh +printf 'gui\n' +for arg do printf '<%s>\n' "$arg"; done +EOF +cat >"$app_dir/usr/bin/photocraft-cli" <<'EOF' +#!/bin/sh +case "${1:-}" in + mcp) + IFS= read -r request + printf '%s\n' "$request" + printf 'cli stderr\n' >&2 + ;; + --exit-37) exit 37 ;; + *) + printf 'cli\n' + for arg do printf '<%s>\n' "$arg"; done + ;; +esac +EOF +chmod +x "$app_dir/AppRun" "$app_dir/usr/bin/photocraft" "$app_dir/usr/bin/photocraft-cli" +export APPIMAGE="$test_dir/PhotoCraft test.AppImage" +: >"$APPIMAGE" +export ARGV0="$APPIMAGE" +export XDG_DATA_HOME="$test_dir/data" + +fail() { echo "FAIL: $*" >&2; exit 1; } + +out="$("$app_dir/AppRun" --cli convert 'input file.png' '' '*.psd' --out 'output file.psd')" +expected=$'cli\n\n\n<>\n<*.psd>\n<--out>\n' +[ "$out" = "$expected" ] || fail "CLI arguments changed: $out" +[ "$("$app_dir/AppRun" --cli)" = cli ] || fail "empty CLI invocation" + +# The AppImage runtime exposes the original symlink name through ARGV0. +for argv0 in photocraft-cli ./photocraft-cli "$test_dir/with spaces/photocraft-cli"; do + out="$(ARGV0="$argv0" "$app_dir/AppRun" --version)" + [ "$out" = $'cli\n<--version>' ] || fail "CLI symlink not selected: $argv0" +done +out="$(ARGV0=photocraft-cli "$app_dir/AppRun" --cli --version)" +[ "$out" = $'cli\n<--version>' ] || fail "CLI selector not removed with a symlink" + +request='{"jsonrpc":"2.0","id":1,"method":"initialize"}' +out="$(printf '%s\n' "$request" | "$app_dir/AppRun" --cli mcp 2>"$test_dir/stderr")" +[ "$out" = "$request" ] || fail "MCP stdin/stdout changed: $out" +[ "$(cat "$test_dir/stderr")" = 'cli stderr' ] || fail "CLI stderr lost" +status=0 +"$app_dir/AppRun" --cli --exit-37 || status=$? +[ "$status" = 37 ] || fail "CLI exit status changed: $status" +[ ! -e "$XDG_DATA_HOME" ] || fail "CLI performed desktop integration" + +# GUI invocations still forward all arguments; only a leading --cli is a selector. +out="$("$app_dir/AppRun" 'input file.png' --cli)" +[ "$out" = $'gui\n\n<--cli>' ] || fail "GUI arguments changed: $out" +[ -f "$XDG_DATA_HOME/applications/ai.storyteller.photocraft.desktop" ] || fail "GUI integration lost" +out="$(ARGV0=another-photocraft-cli "$app_dir/AppRun" mcp)" +[ "$out" = $'gui\n' ] || fail "an unrelated filename selected CLI" + +# A broken package must fail visibly, rather than silently opening the GUI. +rm "$app_dir/usr/bin/photocraft-cli" +status=0 +"$app_dir/AppRun" --cli --version >"$test_dir/missing-out" 2>"$test_dir/missing-err" || status=$? +[ "$status" != 0 ] || fail "missing CLI succeeded" +[ ! -s "$test_dir/missing-out" ] || fail "missing CLI launched the GUI" +[ -s "$test_dir/missing-err" ] || fail "missing CLI had no diagnostic" + +echo "AppRun CLI selection ok"