feat(cli): ship the CLI in every installer and put it on PATH at launch

The `tty7` CLI was built by every release run and thrown away: all four
bundle scripts copied only `tty7-app`, and the upload glob covers `dist/`,
which the CLI never reached. Nothing put it on PATH either, so the
agent-facing half of the product was unreachable from a shipped install.

Bundle it on all four platforms, and have the GUI link it up itself rather
than hiding the step behind a menu item most people never find.

The install has two halves. The environment half prepends the CLI's
directory to this process's PATH before the daemon is spawned, so every
pane inherits it — that alone makes `tty7` work where agents actually run,
writes nothing to disk, and behaves the same everywhere. The on-disk half
symlinks into a directory already on PATH (Unix) or appends to
HKCU\Environment (Windows), and is allowed to fail.

Candidate directories are a fixed list intersected with PATH, not the first
writable entry on it: pyenv/rbenv/asdf/mise shim directories sit at the
front of PATH on many machines and are writable, and anything dropped there
is deleted on the next rehash — silently, days later.

Debug builds get the environment half only. `target/debug` holds a `tty7`
too, so otherwise a `cargo run` would repoint the developer's real `tty7`
at a debug binary, and each isolated dev-verify instance would rewrite the
PATH of the machine it is meant to stay away from.
This commit is contained in:
l0ng-ai
2026-07-31 16:49:22 +08:00
committed by l0ng-ai
parent e230583108
commit c275960ceb
11 changed files with 654 additions and 1 deletions
+7
View File
@@ -55,6 +55,13 @@ mkdir -p "$APPDIR/usr/bin"
cp "target/${TARGET}/release/tty7-app" "$APPDIR/usr/bin/tty7-app"
chmod +x "$APPDIR/usr/bin/tty7-app"
# The CLI, beside the GUI as everywhere else. Unlike the tarball, an AppImage is
# mounted at a fresh /tmp/.mount_XXXX per run, so `core::cli_install` must copy
# this onto PATH rather than symlink it — a link into the mount dies the moment
# the app exits. That branch keys off $APPIMAGE, which the runtime sets.
cp "target/${TARGET}/release/tty7" "$APPDIR/usr/bin/tty7"
chmod +x "$APPDIR/usr/bin/tty7"
# A desktop entry + icon are mandatory AppImage metadata; linuxdeploy places
# them and generates AppRun. Icon basename must match the desktop's Icon= key.
cat > "$TOOLS/tty7.desktop" <<'DESKTOP'
+5
View File
@@ -29,9 +29,14 @@ mkdir -p "$STAGE"
cp "target/${TARGET}/release/tty7-app" "$STAGE/tty7-app"
chmod +x "$STAGE/tty7-app"
# The CLI ships beside the GUI, which symlinks it onto PATH at launch (see
# core::cli_install) by resolving it relative to its own executable.
cp "target/${TARGET}/release/tty7" "$STAGE/tty7"
chmod +x "$STAGE/tty7"
# Release builds keep symbols (thin LTO, no profile strip); drop them here so
# the archive isn't ~100 MB of debug info.
strip "$STAGE/tty7-app" || echo "⚠️ strip unavailable — shipping unstripped binary"
strip "$STAGE/tty7" || true
mkdir -p "$STAGE/completions"
cp assets/completions/*.json "$STAGE/completions/"
cp LICENSE "$STAGE/LICENSE"
+18 -1
View File
@@ -27,6 +27,13 @@ rm -rf dist
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
cp "target/${TARGET}/release/tty7-app" "$APP/Contents/MacOS/tty7-app"
chmod +x "$APP/Contents/MacOS/tty7-app"
# The CLI rides inside the bundle rather than beside it: a DMG is drag-to-
# Applications, so anything not in the .app never reaches the user's disk. The
# GUI symlinks it onto PATH at launch (see core::cli_install), which is why it
# sits next to tty7-app under MacOS/ — that is the directory the GUI resolves
# relative to its own executable.
cp "target/${TARGET}/release/tty7" "$APP/Contents/MacOS/tty7"
chmod +x "$APP/Contents/MacOS/tty7"
cp assets/tty7.icns "$APP/Contents/Resources/tty7.icns"
# Completion signatures are loaded at runtime (not embedded), resolved relative
# to the executable as ../Resources/completions — see terminal::signature.
@@ -94,7 +101,17 @@ if [[ -n "$SIGN_ID" && -n "${APPLE_CERTIFICATE:-}" ]]; then
</plist>
ENT
# Sign inner-out: the executable first, then the bundle.
# Sign inner-out: the executables first, then the bundle. The CLI must be
# signed explicitly — notarization rejects a bundle carrying an unsigned
# Mach-O, and the outer `codesign "$APP"` does not descend into MacOS/ for
# anything but CFBundleExecutable.
#
# It gets hardened runtime (notarization requires it) but none of the GUI's
# entitlements: the JIT and library-validation exemptions exist for gpui's
# Metal path, and a CLI that never renders anything has no business holding
# them.
codesign --force --options runtime --timestamp \
--sign "$SIGN_ID" "$APP/Contents/MacOS/tty7"
codesign --force --options runtime --timestamp --entitlements "$ENTITLEMENTS" \
--sign "$SIGN_ID" "$APP/Contents/MacOS/tty7-app"
codesign --force --options runtime --timestamp --entitlements "$ENTITLEMENTS" \
+4
View File
@@ -21,6 +21,10 @@ Remove-Item -Recurse -Force dist -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Stage | Out-Null
Copy-Item "target/$Target/release/tty7-app.exe" "$Stage/tty7-app.exe"
# The CLI, staged beside the GUI so both the zip and the installer carry it.
# `core::cli_install` resolves it relative to tty7-app.exe and puts that
# directory on the user's PATH.
Copy-Item "target/$Target/release/tty7.exe" "$Stage/tty7.exe"
New-Item -ItemType Directory -Force -Path "$Stage/completions" | Out-Null
Copy-Item "assets/completions/*.json" "$Stage/completions/"
Copy-Item LICENSE "$Stage/LICENSE.txt"
+12
View File
@@ -58,11 +58,23 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
; it. The user would keep launching the previous version from their pinned icon,
; against the same daemon endpoint as the new one. Delete it on upgrade; a fresh
; install simply has nothing to remove.
;
; That name is now reused by the CLI below, which lands at the very same path.
; The order saves us: [InstallDelete] runs before [Files], so the stale GUI is
; gone before the CLI is written, and what survives is never a mix of the two.
; The taskbar pin is the loose end — post-upgrade it points at a CLI, so
; clicking it flashes a console instead of opening a window. That is louder
; than the bug it replaces (silently running last release's GUI), and the pin
; is not ours to rewrite; the Start Menu entry in [Icons] is correct either way.
[InstallDelete]
Type: files; Name: "{app}\tty7.exe"
[Files]
Source: "{#StageDir}\tty7-app.exe"; DestDir: "{app}"; Flags: ignoreversion
; The CLI. `core::cli_install` adds {app} to the user's PATH at first launch,
; so this is not registered as an [Env] change here — the portable zip has no
; installer to do it, and one code path serving both is one behaviour to debug.
Source: "{#StageDir}\tty7.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#StageDir}\completions\*"; DestDir: "{app}\completions"; Flags: ignoreversion recursesubdirs
Source: "{#StageDir}\LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#StageDir}\README.md"; DestDir: "{app}"; Flags: ignoreversion