feat: ship the CLI inside every installer and manage it from settings - #177
Merged
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
YoungSx
force-pushed
the
cli-integration-issue-145
branch
from
September 4, 2026 07:26
c7e8048 to
afdd356
Compare
The self-contained single-file publish is about to be consumed by four packaging chains -- Windows MSIX, the Windows desktop MSI, the macOS app bundle and a Linux deb -- and not only by the standalone archive it was written for. Leaving the publish inside the archive script would mean each chain grew its own copy, and four independently produced binaries that all build successfully is exactly the shape in which they drift apart. publish-cli-binary.sh now owns the publish, the runtime-identifier allow-list check and the single-file assertion; build-cli-artifacts.sh owns only staging, archiving and the checksum sidecar. The publish reports its executable path and MinVer-derived version through the same key=value protocol GitHub Actions uses for step outputs, so the wrapper reads them back from a temporary file instead of parsing log lines, and its own GITHUB_OUTPUT is untouched because the assignment is scoped to the child. Verified on this host with --rid linux-arm64 --allow-unsupported-rid: the archive and sidecar land where they did before and the executable path is unchanged, so the release workflow's step outputs still resolve. Reverse-verified that an unsupported RID rejected inside the publish script fails the wrapper too and leaves no archive behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Installing SalmonEgg now installs the salmon-egg command. A packaged app cannot append to PATH -- the OS is the installer and writes no environment variables on the app's behalf -- so the MSIX registers a windows.appExecutionAlias instead. Windows materializes a stub under %LOCALAPPDATA%\Microsoft\WindowsApps, a directory it already keeps on the per-user PATH, and launching the stub starts the executable the extension names. EntryPoint="Windows.FullTrustApplication" is what makes that a classic process launch: as a UWP activation the command would lose the caller's console, arguments and exit code, which reads as a CLI that does nothing. The payload is passed in as SalmonEggBundledCliExecutable rather than added as a ProjectReference. It is a self-contained single-file publish for one runtime identifier; a ProjectReference would merge the CLI's assemblies into the app's own output, where they collide with the app's copies of the same shared projects and cannot be launched as a separate command at all. TargetPath uses a forward slash while Link uses a backslash, matching the font items above it: a backslash is an ordinary file-name character on Linux and macOS, so the same item would otherwise publish one file literally named "cli\salmon-egg" once the other packaging chains start using it. Three independent things have to hold for the command to work, and each fails silently on its own, so each is checked where it can be: - The manifest declaring the alias means MakeAppx rejects a package built without the payload, which is why the declaration is static rather than generated. - ValidateSalmonEggBundledCli fails at the front of packaging with the command to run, instead of leaving MakeAppx to report a missing package file minutes later. It also rejects a payload under any other file name, since the alias names a literal one, and an MSYS path that MSBuild cannot open. - The MSIX contract gate now asserts, on the built package, that exactly one alias extension exists, under the product's command name, with the full-trust entry point, pointing at an executable the package really carries. Verified: the gate's self-test covers 19 package cases, six of them new and each reverse-verified to reject exactly one of these defects (payload absent, alias absent, alias renamed, entry point changed, two alias extensions, alias path disagreeing with where the payload landed). Cross-verified against the real Package.appxmanifest by synthesizing a package from it -- accepted -- and the same manifest with the alias block deleted -- rejected as AppExecutionAliasMissing -- so the gate and the manifest agree rather than each being self-consistent. The content item's TargetPath was read back from MSBuild evaluation. Not verified on this host: installing the package and running salmon-egg from a Windows shell needs Windows. The PR-level MSIX gate builds the package on every change, so the payload and alias assertions run there; the alias resolving through WindowsApps at runtime is a release-time manual check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Windows desktop MSI now installs the salmon-egg command alongside the app and appends its directory to the user PATH, so the two Windows installers behave the same way. Windows Installer owns that value: written on install, removed on uninstall, which a script calling setx or editing the registry cannot guarantee. The command goes into a `cli` subdirectory of the install folder and the PATH row names that subdirectory, not the install folder. Registering the install folder would put every DLL shipped beside the app on PATH to expose one executable. That also means the CLI cannot be harvested: heat generates directory identifiers that change with the tree, and the PATH row has to name one, so the file and its Environment element are authored explicitly and the step now fails if a cli directory reaches the harvest at all -- two components declaring the same file is a duplicate light rejects, and an ICE error is a worse way to learn about it. The PATH encoding rule was already written down and rehearsed for the CLI-only MSI, so it moves rather than being restated: CliMsiPathContract.ps1 becomes MsiPathContract.ps1 with the command directory as a parameter, since that is the only part that differs between the two packages. Everything else -- which prefix characters mean set and remove, where the [~] marker has to sit, why a trailing marker prepends -- is one rule with one set of violation identifiers. DesktopMsiContract.ps1 now also asserts, on the built package, that the File table carries salmon-egg.exe and that the Environment table holds exactly one row which satisfies that rule for [CLIFOLDER]. Both halves fail silently on their own: a package that lost the CLI still installs a working app whose PATH entry points at an empty directory, and a package that ships the CLI without the row leaves the user a file they cannot invoke. The Environment table is read as Name/Value pairs rather than two column reads, because which value belongs to which variable is the whole question. Verified: the PATH gate covers 12 rows (two new, for the desktop shape and for registering the app folder by mistake) and the desktop gate 17 packages (six new: missing command payload, no PATH row, a second unreviewed row, prepending, surviving uninstall, wrong directory), each reverse-verified to the exact violation identifier. The Environment query is now part of the set the gate replays through a fake OpenView that rejects out-of-grammar SQL, which is what kept the last two release builds from dying inside OpenView. The authored WiX was parsed out of the workflow and checked as XML: CLIFOLDER nests inside INSTALLFOLDER, the component group targets it, the feature references it, and the Environment element is the one WiX compiles to the row the gate calls conforming. Not verified on this host: building the MSI needs Windows and WiX, and installing it to watch PATH change needs an interactive session. That stays a release-time manual check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Linux had no installer for the app at all -- only an unpackaged publish directory -- so there was nothing to register a command with. This adds the package, and installing it installs both halves: the app under /opt/salmon-egg and salmon-egg on PATH. Layout follows the FHS. /opt is where an add-on application that ships its own runtime belongs, which is what a self-contained .NET publish is. /usr/bin/salmon-egg is a symlink into that tree, so nothing edits a shell profile: dpkg owns the link and takes it away on purge, while an edited .bashrc is not reversible. The link is relative and two levels deep, because one level resolves to /usr/opt and dangles -- which the package's file list cannot show, since it records only the link text. Also shipped: a desktop entry, so the GUI is launchable from the shell's menu and registered for the same s8p scheme the Windows package declares as a protocol, and the app icon at all five sizes Uno's Resizetizer produced for this build. Those are used rather than the source artwork because the source is 200x200, which is not a size the hicolor theme defines -- an icon in a non-theme directory gets rescaled twice or ignored. StartupWMClass is deliberately absent: a wrong value silently breaks the taskbar's window-to-launcher association. The dependency list could not be derived from the build. dpkg-shlibdeps and readelf see only link-time NEEDED entries, and everything above libstdc++ is loaded at runtime by P/Invoke or by the graphics stack, so the publish output declares none of it. The list was read out of /proc/<pid>/maps while the published app ran headless under Xvfb: ICU, OpenSSL, brotli, the X11 family, GL/EGL, GLib, freetype and GStreamer are all mapped at startup. Alternative dependencies rather than pinned ones, because Microsoft's own .NET debs name a single libicu only by building one deb per distribution, and Ubuntu 24.04's t64 transition renamed several of these. Conflicts/Replaces on the old salmon-egg-cli package, whose /usr/bin/salmon-egg this one now owns. Verified end to end on this host, which is the point of doing Linux first: the package installs through apt (so every declared dependency name is real, not merely present), the symlink resolves to the CLI, `salmon-egg --version` runs in a clean login shell and matches the package version, the desktop entry's Exec and Icon both resolve, and purge removes all of it -- 16 checks. Reverse-verified by rebuilding the package with the symlink one level too shallow (4 checks fail, including the resolution assertion) and with the desktop entry naming a renamed executable (exactly that check fails), and by running the build script against a publish output with no bundled CLI (exit 1 with the command to fix it). The smoke gate now runs on every push through platform-build-gates, where the Linux publish switched from framework-dependent to self-contained: that is the shape the package ships, and the old one proved something about a layout nobody installs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t on PATH macOS had no way to register a command at all. A .dmg is dragged, so it has no install hook, and Uno can build a .pkg but its PackageAppBundle task accepts no scripts parameter, so that package cannot carry a postinstall either. This adds a pkgbuild step whose postinstall symlinks the bundled command into /usr/local/bin, which macOS keeps on the default PATH through /etc/paths -- the smallest thing that makes `salmon-egg` resolve without editing anyone's shell profile. The command reaches the bundle by being part of the publish Uno builds the bundle from, and it has to be there before signing: adding a Mach-O to a signed bundle invalidates the signature, so the .dmg gets it too and its users can link it by hand. Which directory inside the bundle it lands in is Uno's decision, so it is probed rather than assumed. Dissecting the shipped v1.4.2 bundle shows the split GenerateAppBundle performs: the apphost, its deps.json and runtimeconfig.json, and every .dylib go to Contents/MacOS (19 files); managed assemblies, satellite resource directories and asset subdirectories go to Contents/Resources with relative paths intact (589 files). A cli/ subdirectory holding one extension-less Mach-O matches neither pattern exactly, and the split is not documented, so the postinstall, the pkg builder and the bundle contract all accept either location and report which one they found. MacOS is preferred where present, because that is where Apple expects auxiliary executables. The pkg identifier is read from the bundle's own CFBundleIdentifier rather than repeated, since two identifiers for one product would let the installer treat an upgrade as a second independent install. Installer signing needs a Developer ID Installer certificate, which is a different one from the app and disk-image identities, so it is optional the same way the .dmg's is: MACOS_PKG_CODESIGN_KEY when configured, an unsigned package otherwise. Verified: the PATH gate drives the real postinstall against fake roots -- 15 checks, both bundle layouts, and reverse-verified for a bundle with no command (refused, no link left behind), a dangling link from an earlier version (replaced, which is why the script tests -L and not just -e), a regular file occupying the link path (replaced), and a second run (idempotent, as a reinstall is). The bundle contract gate gained the command assertion with cases for both layouts, for an absent command and for one whose mode bit was dropped. Both run on every push through ci-core. Not verified on this host: pkgbuild, productsign and installing the package need macOS. Two release-time checks remain manual: that notarization accepts a Mach-O in whichever directory Uno placed it, and that the link resolves in a fresh login shell. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The command now ships inside every SalmonEgg installer, so publishing it a second time as its own package would mean two delivery paths for one binary, two sets of install instructions, and two versions a user could end up with. Removed: the package-cli job and its three-runner matrix, the CLI MSI, the CLI deb, the Homebrew formula, the release archives and their checksum sidecars, and the CLI install smoke gate whose job the desktop package's own smoke now does. BREAKING CHANGE: `salmon-egg` is no longer published on its own. Every platform gets it by installing SalmonEgg. On Linux that means the desktop package pulls in the graphics stack the GUI needs, so a headless server can no longer install only the command -- if that use case matters, the deb would have to be split into a CLI package the GUI depends on, which is a decision this change deliberately does not make on its own. What stayed, because both are now load-bearing for four packaging chains instead of one: publish-cli-binary.sh, which produces the binary each installer embeds, and run-cli-release-artifact-smoke.sh, which proves that binary starts with no .NET on PATH and holds its exit-code and credential contracts. The PATH-encoding rule stayed too, now serving the desktop MSI. The support matrix in SalmonEgg.Cli.csproj kept its three runtime identifiers and changed meaning: they are the platforms whose installer embeds a command, so a RID outside the list has no delivery path at all. The publish job lost its checkout and SDK setup along with the CLI asset step that needed them -- it now works purely on downloaded artifacts and gh. That also retires the ordering hazard those two carried: `Setup .NET` ahead of the checkout resolved global.json against an empty workspace and once killed a tag build after all seven packaging jobs had passed. GitHubWorkflowContractTests replaces the MSYS-path test, which pinned a PowerShell regex at the CLI MSI call site, with one asserting that the translation lives in the publish script and that every Windows consumer takes its native-path output. A new test pins the mechanism each of the four installers uses to register the command, so any one of them silently ceasing to do it fails here. Verified: 16 workflow contract tests pass, and the smoke-script count assertion caught its own off-by-one first, which is how I know it reads the workflow rather than passing vacuously. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The release guide said the opposite of what the code now does: "GUI installers do not modify PATH; the global salmon-egg command comes only from the CLI packages." Every installer registers it now, and the CLI packages are gone, so the CLI section is rewritten around the four mechanisms and who verifies each -- and the manual checks that remain, since only the Linux chain can be installed by a runner. Two sections are new. Linux had none at all, because there was no installer to document: it now covers the package layout with the reasoning per path, why the dependency list came out of /proc/<pid>/maps instead of the build graph, and the known gap that a headless server can no longer install just the command. macOS gained the .pkg, why it exists alongside the .dmg, and the bundle-layout evidence from dissecting v1.4.2 that made both candidate locations for the command worth probing. The release checklist replaces "CLI artifacts for three RIDs are built" with the four installers embedding this build's CLI, and adds the Windows and macOS manual PATH checks. README and README.en lose the standalone install tables in favour of how each installer registers the command. AGENTS section 11 gains one rule, since this is the class of defect that repeats: the installer owns PATH registration, one publish script owns the binary every installer embeds, and the assertions must read the artifact this build produced with the rule split from the platform API that reads it -- so its positive and negative cases run on every push rather than on a tag. It also records the two things that only surfaced by doing this: a symlink must be asserted by resolution rather than by its text, because a wrong relative depth is invisible in the package's file list; and when the platform's placement of an auxiliary executable is not a documented contract, consumers probe and report rather than bet on one location. Verified: five gates pass locally (MSI PATH, desktop MSI, MSIX self-test, artifact contract self-test, macOS installer PATH), every workflow parses, every shell script passes syntax check including the postinstall under POSIX sh, and 37 contract tests plus the Application (120) and CLI (95) suites pass. The guide is CRLF and stayed CRLF: the diff is 108 lines added and 56 removed rather than the whole file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Groundwork for managing the CLI from the app's settings. Two contracts, both about the machine rather than about anything the app persists: nothing it could store would describe an MSIX alias, an MSI PATH entry, a dpkg-owned symlink and a link left by a macOS installer, and any of those can be absent, stale, or pointing at another installation. The inspector resolves PATH the way a shell does and asks whatever it found which version it is. PATH order is the whole point: with two installations present the earlier one is what typing the name reaches, so returning any other match would describe a command the user cannot invoke -- and that shadowing is the case most worth reporting. Four states a user can act on differently: not registered, registered, version mismatch (PATH reaches another installation), and unreadable (the file is there but would not say what it is, which is a broken install rather than a stale one). Versions are compared on release identity alone, because the CLI prints its informational version while the app knows its assembly version and a matched pair never compares equal verbatim. Linking is macOS-only and deliberately narrow. Everywhere else the installer owns the entry and removes it on uninstall; a second owner writing the same path means whichever uninstall runs second leaves a command pointing at a deleted app. macOS is the exception because a dragged .app has no install hook at all, so without this the .dmg's users have no path to the command. It reuses the pkg postinstall's exact shape -- remove then link, probing both bundle areas -- so an install and an in-app link leave the same result, and it treats a dismissed authorization dialog as a normal outcome rather than a failure. The shell escaping is a security boundary, not formatting: the text is parsed as an AppleScript literal and then by /bin/sh, both as root, and a bundle can sit under a directory containing a quote. So it is built apart from the code that executes it and tested with a real shell -- 34 cases over ten hostile paths, including one that asserts an injected `touch` does not create its file. Reverse-verified by removing the single-quote escape: seven tests fail, and the injection case fails because the file appears. 17 inspector cases cover the states, PATH order in both directions, empty and quoted PATH segments, link targets reported only when they differ, and the release-identity comparison. Verified locally: 34 + 17 pass, and the four capability fakes gained the two new members. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Installing SalmonEgg registers the salmon-egg command, but nothing in the app said whether that actually worked on this machine -- and on macOS, where a dragged .app has no install step, nothing could fix it. This adds the settings section that answers both. Everything shown is one observation, re-read on arrival rather than cached: PATH is a machine fact an installer or another installation can change while the page is open. Four states read differently because the next step differs -- available, a different version on PATH (another install is shadowing this one), nothing on PATH, and present but unable to say what it is. The resolution details name the path PATH produced and, separately, where a symlink leads, which is how a stale link into a replaced app bundle becomes visible. Which actions exist is a capability, not a preference. On Windows and Linux the page is read-only and says the installer owns the registration; offering a fix there would create a second owner for one path, and whichever uninstall ran second would leave a command pointing at a deleted app. macOS gets Link and Remove, going through the same remove-then-link shape as the pkg's postinstall so an install and an in-app link leave identical results. A dismissed authorization dialog is reported as a decision rather than a failure, and the status is always re-read from the machine afterwards instead of assuming the operation's own claim of success. Platforms with no PATH and no process host get an inspector that says so, so the view model never learns which platform it is on and one page serves every target. Verified locally: 14 view model cases (each state's projection, both readings of an absent command, the detail carried verbatim on an unreadable one, link/unlink re-inspecting, cancellation deliberately not re-inspecting, CanExecute refusing where the installer owns the entry, and an inspection failure reported rather than thrown); 534 settings, contract, XAML-compliance and localization cases pass, including the x:Uid gate over the new page's resource keys and the three shell tests that pin the section list -- they failed first, as they should have, and now cover the new entry. Application (120) and Infrastructure (785) suites pass. Desktop head compiles clean. Not verified on this host: the WebAssembly head, whose wasm-tools workload does not match this SDK. The #else branch references only types in Infrastructure, which every target references unconditionally, and its usings are present; CI's wasm job is the check. The macOS authorization prompt and the resulting link in a fresh login shell need a Mac. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review of the CLI integration work surfaced three loose ends, each fixed here: - The settings view model kept the previous action's message on screen after a fresh inspection, pairing a new status with an old outcome. RefreshAsync now clears it before re-probing. - The installer-owns-PATH rule in AGENTS.md had no escape hatch for platforms whose package format has no install hooks (macOS drag-drop). The rule now permits app-owned registration there, but only with the same path and shape as an installer would write, a removal entry, and logic testable against fake roots -- otherwise it is the same two-owner defect in a different costume. - The release guide and both READMEs never mentioned the new settings section where users can see the live command status (and on macOS, link or unlink it). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
YoungSx
force-pushed
the
cli-integration-issue-145
branch
from
September 4, 2026 08:34
afdd356 to
0ea7bb8
Compare
The app-execution-alias guard called [System.IO.Path]::GetFileName() inside an <Error> condition string. A condition cannot nest a quoted property expansion inside another quoted string -- the inner quote ends the outer one -- so every MSIX publish died at parse time with MSB4092 before any guard could fire. The Linux and macOS gates never evaluate this target, which is why only the Windows job saw it. The file name is now resolved into a property (property functions allow the quoting a condition does not) and the condition reads that property. Reproduced MSB4092 standalone before the fix and exercised all three guard branches after it: missing path, wrong file name (reports the resolved name), and valid input. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every pull_request trigger was develop-only, and ci-core plus code-quality have no workflow_dispatch trigger -- so a PR to main shipped with no CI at all, because their push triggers only fire after the merge, when the verdict can no longer block anything. The other five gate workflows can be dispatched by hand for a main PR, but that relies on someone remembering to do it. All seven pull-request gate workflows now declare [develop, main]. The workflow contract test pinned the develop-only trigger; it now pins the union, with the reasoning recorded there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The inspector tests spell their inventory with forward slashes, but ResolveOnSearchPath joined candidates with the host's Path.Combine, so on the Windows runner every candidate came out backslash-separated and matched nothing: five tests went red. The join is a machine fact like the PATH text, the file table, and the link targets -- the host's syntax is not derivable from either input -- so it moves behind ICliCommandProbeEnvironment.Combine alongside them. Tests supply a deterministic Unix grammar and keep asserting forward-slash inventory paths on every host; the real environment keeps Path.Combine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two tests assumed the runner was a Unix machine, went red on the Windows one, and have blocked every main-targeted CI Core run since 34f10d7: - StdioCommandResolverTests simulated Unix PATH splitting on ':' but fed it a Windows absolute path, whose drive colon the split shredded into fragments no File.Exists could hit. The search entry is now a colon-free relative directory, which resolves against the process current directory identically on every host, so the case still runs everywhere. - StdioTransportConnectionTests pinned the launched agent argument to the on-disk spelling, but the Windows resolver echoes the machine's PATHEXT casing (".CMD"), and Windows filenames compare case-insensitively. The assertion now compares case-insensitively too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three self-test cases came back red on the Windows runner -- every case
whose verdict depends on the executable named by Info.plist -- while
every deliberately-broken case failed as intended. The cause: a Windows
interpreter translates text-mode newlines to \r\n, and the command
substitution capturing read_plist_string's output strips only the \n.
The executable name arrived as "SalmonEgg\r", a file that never exists,
so every bundle that should have verified cleanly could not.
Emitting the value as raw bytes bypasses the text layer entirely.
Reproduced and closed with a CRLF-translating interpreter shim: the old
reader fails exactly those three cases, the byte reader passes all.
Also, a verdict alone ("expected pass but got fail") cost a debugging
session to explain from a CI log, because expect swallowed the check's
own diagnosis. An unexpected outcome now prints what the check said.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Linux Desktop gate started failing mid-install: the runner image's package lists name a webkit2gtk version the archive has since replaced, and the mirror only serves current ones, so apt 404s while resolving the package's dependencies. The install itself was never wrong -- the lists were stale. Refreshing them is part of installing cleanly and belongs inside the gate, where it cannot be forgotten by a workflow edit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With the CRLF fix in, the Windows self-test advanced to a new failure and the new diagnostics named it outright: "cli/salmon-egg is present but not executable". The fixtures chmod +x their bundled command, but a Windows host has no mode bits for chmod to set. There, test -x falls back to reading the file's first bytes and accepts only '#!', ':\n' or 'MZ' -- cygwin path.h has_exec_chars, reached from fstat when the name carries no executable suffix. A fixture whose content is the three letters "bin" can never be executable on that host, which is exactly the three expect-pass bundle cases that failed. The fixtures now satisfy both grammars without branching on the host: shebang content for the Windows content heuristic, and the same chmod +x for the POSIX mode bit. The negative case rewrites the content back too, since its chmod -x is the no-op there; on POSIX the rewrite cannot resurrect the cleared bit. Reproduced with a test -x shim modeled on the cygwin source: the old fixtures fail precisely the three CI cases, the new ones pass all 22 on both grammars. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On a Windows runner the gate runs under Git Bash, whose MSYS `ln -s` does not create symlinks by default: when the target exists it silently copies it (yielding exactly the regular-file-on-PATH defect the gate exists to catch), and when it does not it errors -- killing the dangling-link case under `set -e`. Export MSYS=winsymlinks:nativestrict so `ln -s` either creates a real symlink (runners have Developer Mode) or fails, and probe the capability once up front, skipping loudly when it is absent rather than rehearsing a fiction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Closes #145. Installing SalmonEgg now puts the
salmon-eggcommand on PATH on every platform, because the command ships inside the app's own installer:windows.appExecutionAlias→%LOCALAPPDATA%\Microsoft\WindowsAppsEnvironmenttable row appending[CLIFOLDER]to machine PATH.app+.pkgContents/MacOS/cli/salmon-egg; pkg postinstall symlinks/usr/local/bin/salmon-egg.deb/usr/bin/salmon-egg→../../opt/salmon-egg/cli/salmon-egg, dpkg-ownedOne publish (
scripts/release/publish-cli-binary.sh) produces the single self-contained binary all four installers embed, and each release job smokes that exact binary before sealing it.The standalone CLI distribution is retired: deb/MSI/Homebrew/tar packages and their workflow steps are deleted, with
Conflicts/Replaces: salmon-egg-clion the new deb so upgrades cleanly take over. BREAKING CHANGE: headless servers that installed only the CLI must now install the desktop package.Settings
A new "命令行工具 / Command line" settings section shows the live registration state (registered / missing / version mismatch / unreadable, resolved path, link target, versions). On macOS — the only platform whose package format has no install hooks — it also offers one-click link/unlink through an admin-privileged shell script, whose quoting is proven injection-safe by tests that run a real
/bin/sh.Rule change
AGENTS.md gains the rule this work embodies: installers own PATH registration; app-owned registration is only permitted where no install hook exists, and then only with the same path, the same shape, and a removable, script-testable implementation.
Verified locally
apt installresolves Depends → symlink resolves (readlink -f) → clean login shellsalmon-egg --versionmatches the package version → desktop entry Exec/Icon resolve → purge leaves nothing (14-check smoke gate, reverse-verified by breaking the symlink depth)/bin/shincluding an injection assertionNot verifiable on this host (CI covers these)
Windows alias runtime behavior and MSI install/uninstall PATH mutation need an interactive session; macOS pkgbuild/signing and fresh-login-shell link resolution need a mac; the WASM head compile needs the browserwasm workload (
UNOWA0001locally).🤖 Generated with Claude Code